#define CURVEDUI_PRESENT //If you're an asset creator and want to see if CurvedUI is imported, just use "#if CURVEDUI_PRESENT [your code] #endif" using System; using UnityEngine; using UnityEngine.UI; using UnityEngine.EventSystems; using System.Collections.Generic; #if CURVEDUI_TMP || TMP_PRESENT using TMPro; #endif /// /// This class stores settings for the entire canvas. It also stores useful methods for converting cooridinates to and from 2d canvas to curved canvas, or world space. /// CurvedUIVertexEffect components (added to every canvas gameobject)ask this class for per-canvas settings when applying their curve effect. /// namespace CurvedUI { [AddComponentMenu("CurvedUI/CurvedUISettings")] [RequireComponent(typeof(Canvas))] public class CurvedUISettings : MonoBehaviour { public const string Version = "3.2"; #region SETTINGS //Global settings [SerializeField] CurvedUIShape shape; [SerializeField] float quality = 1f; [SerializeField] bool interactable = true; [SerializeField] bool blocksRaycasts = true; [SerializeField] bool forceUseBoxCollider = false; //Cyllinder settings [SerializeField] int angle = 90; [SerializeField] bool preserveAspect = true; //Sphere settings [SerializeField] int vertAngle = 90; //ring settings [SerializeField] float ringFill = 0.5f; [SerializeField] int ringExternalDiamater = 1000; [SerializeField] bool ringFlipVertical = false; //internal system settings int baseCircleSegments = 16; //stored variables Vector2 savedRectSize; float savedRadius; Canvas myCanvas; RectTransform m_rectTransform; #endregion #region LIFECYCLE void Awake() { // If this canvas is on Default layer, switch it to UI layer.. // this is to make sure that when using raycasting to detect interactions, // nothing will interfere with it. if (gameObject.layer == 0) this.gameObject.layer = 5; //save initial variables savedRectSize = RectTransform.rect.size; } void Start() { if (Application.isPlaying) { // lets get rid of any raycasters and add our custom one // It will be responsible for handling interactions. BaseRaycaster[] raycasters = GetComponents(); foreach(BaseRaycaster caster in raycasters) { if (!(caster is CurvedUIRaycaster)) caster.enabled = false; } this.gameObject.AddComponentIfMissing(); //find if there are any child canvases that may break interactions Canvas[] canvases = GetComponentsInChildren(); foreach(Canvas cnv in canvases) { if (cnv.gameObject != this.gameObject) { Transform trans = cnv.transform; string hierarchyName = trans.name; while(trans.parent != null) { hierarchyName = trans.parent.name + "/" + hierarchyName; trans = trans.parent; } Debug.LogWarning("CURVEDUI: Interactions on nested canvases are not supported. You won't be able to interact with any child object of [" + hierarchyName + "]", cnv.gameObject); } } } //find needed references if (myCanvas == null) myCanvas = GetComponent(); savedRadius = GetCyllinderRadiusInCanvasSpace(); } void OnEnable() { //Redraw canvas object on enable. foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren()) { graph.SetAllDirty(); } } void OnDisable() { foreach (UnityEngine.UI.Graphic graph in (this).GetComponentsInChildren()) { graph.SetAllDirty(); } } void Update() { //recreate the geometry if entire canvas has been resized if (RectTransform.rect.size != savedRectSize) { savedRectSize = RectTransform.rect.size; SetUIAngle(angle); } //check for improper canvas size if (savedRectSize.x == 0 || savedRectSize.y == 0) Debug.LogError("CurvedUI: Your Canvas size must be bigger than 0!"); } #endregion #region PRIVATE /// /// Changes the horizontal angle of the canvas. /// /// void SetUIAngle(int newAngle) { if (myCanvas == null) myCanvas = GetComponent(); //temp fix to make interactions with angle 0 possible if (newAngle == 0) newAngle = 1; angle = newAngle; savedRadius = GetCyllinderRadiusInCanvasSpace(); foreach (CurvedUIVertexEffect ve in GetComponentsInChildren()) ve.SetDirty(); foreach (Graphic graph in GetComponentsInChildren()) graph.SetAllDirty(); if (Application.isPlaying && GetComponent() != null) //tell raycaster to update its collider now that angle has changed. GetComponent().RebuildCollider(); } Vector3 CanvasToCyllinder(Vector3 pos) { float theta = (pos.x / savedRectSize.x) * Angle * Mathf.Deg2Rad; pos.x = Mathf.Sin(theta) * (SavedRadius + pos.z); pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z); return pos; } Vector3 CanvasToCyllinderVertical(Vector3 pos) { float theta = (pos.y / savedRectSize.y) * Angle * Mathf.Deg2Rad; pos.y = Mathf.Sin(theta) * (SavedRadius + pos.z); pos.z += Mathf.Cos(theta) * (SavedRadius + pos.z) - (SavedRadius + pos.z); return pos; } Vector3 CanvasToRing(Vector3 pos) { float r = pos.y.Remap(savedRectSize.y * 0.5f * (RingFlipVertical ? 1 : -1), -savedRectSize.y * 0.5f * (RingFlipVertical ? 1 : -1), RingExternalDiameter * (1 - RingFill) * 0.5f, RingExternalDiameter * 0.5f); float theta = (pos.x / savedRectSize.x).Remap(-0.5f, 0.5f, Mathf.PI / 2.0f, angle * Mathf.Deg2Rad + Mathf.PI / 2.0f); pos.x = r * Mathf.Cos(theta); pos.y = r * Mathf.Sin(theta); return pos; } Vector3 CanvasToSphere(Vector3 pos) { float radius = SavedRadius; float vAngle = VerticalAngle; if (PreserveAspect) { vAngle = angle * (savedRectSize.y / savedRectSize.x); radius += Angle > 0 ? -pos.z : pos.z; } else { radius = savedRectSize.x / 2.0f + pos.z; if (vAngle == 0) return Vector3.zero; } //convert planar coordinates to spherical coordinates float theta = (pos.x / savedRectSize.x).Remap(-0.5f, 0.5f, (180 - angle) / 2.0f - 90, 180 - (180 - angle) / 2.0f - 90); theta *= Mathf.Deg2Rad; float gamma = (pos.y / savedRectSize.y).Remap(-0.5f, 0.5f, (180 - vAngle) / 2.0f, 180 - (180 - vAngle) / 2.0f); gamma *= Mathf.Deg2Rad; pos.z = Mathf.Sin(gamma) * Mathf.Cos(theta) * radius; pos.y = -radius * Mathf.Cos(gamma); pos.x = Mathf.Sin(gamma) * Mathf.Sin(theta) * radius; if (PreserveAspect) pos.z -= radius; return pos; } #endregion #region PUBLIC RectTransform RectTransform { get { if (m_rectTransform == null) m_rectTransform = transform as RectTransform; return m_rectTransform; } } /// /// Adds the CurvedUIVertexEffect component to every child gameobject that requires it. /// CurvedUIVertexEffect creates the curving effect. /// public void AddEffectToChildren() { foreach (UnityEngine.UI.Graphic graph in GetComponentsInChildren(true)) { if (graph.GetComponent() == null) { graph.gameObject.AddComponent(); graph.SetAllDirty(); } } //additional script that will create a curved caret for input fields foreach(UnityEngine.UI.InputField iField in GetComponentsInChildren(true)) { if (iField.GetComponent() == null) { iField.gameObject.AddComponent(); } } //TextMeshPro experimental support. Go to CurvedUITMP.cs to learn how to enable it. #if CURVEDUI_TMP || TMP_PRESENT foreach(TextMeshProUGUI tmp in GetComponentsInChildren(true)){ if(tmp.GetComponent() == null){ tmp.gameObject.AddComponent(); tmp.SetAllDirty(); } } foreach (TMP_InputField tmp in GetComponentsInChildren(true)) { tmp.AddComponentIfMissing(); } #endif } /// /// Maps a world space vector to a curved canvas. /// Operates in Canvas's local space. /// /// World space vector /// /// A vector on curved canvas in canvas's local space /// public Vector3 VertexPositionToCurvedCanvas(Vector3 pos) { switch (Shape) { case CurvedUIShape.CYLINDER: { return CanvasToCyllinder(pos); } case CurvedUIShape.CYLINDER_VERTICAL: { return CanvasToCyllinderVertical(pos); } case CurvedUIShape.RING: { return CanvasToRing(pos); } case CurvedUIShape.SPHERE: { return CanvasToSphere(pos); } default: { return Vector3.zero; } } } /// /// Converts a point in Canvas space to a point on Curved surface in world space units. /// /// Position on canvas in canvas space /// /// Position on curved canvas in world space. /// public Vector3 CanvasToCurvedCanvas(Vector3 pos) { pos = VertexPositionToCurvedCanvas(pos); if (float.IsNaN(pos.x) || float.IsInfinity(pos.x)) return Vector3.zero; else return transform.localToWorldMatrix.MultiplyPoint3x4(pos); } /// /// Returns a normal direction on curved canvas for a given point on flat canvas. Works in canvas' local space. /// /// /// public Vector3 CanvasToCurvedCanvasNormal(Vector3 pos) { //find the position in canvas space pos = VertexPositionToCurvedCanvas(pos); switch (Shape) { case CurvedUIShape.CYLINDER: { // find the direction to the center of cyllinder on flat XZ plane return transform.localToWorldMatrix.MultiplyVector((pos - new Vector3(0, 0, -GetCyllinderRadiusInCanvasSpace())).ModifyY(0)).normalized; } case CurvedUIShape.CYLINDER_VERTICAL: { // find the direction to the center of cyllinder on flat YZ plane return transform.localToWorldMatrix.MultiplyVector((pos - new Vector3(0, 0, -GetCyllinderRadiusInCanvasSpace())).ModifyX(0)).normalized; } case CurvedUIShape.RING: { // just return the back direction of the canvas return -transform.forward; } case CurvedUIShape.SPHERE: { //return the direction towards the sphere's center Vector3 center = (PreserveAspect ? new Vector3(0, 0, -GetCyllinderRadiusInCanvasSpace()) : Vector3.zero); return transform.localToWorldMatrix.MultiplyVector((pos - center)).normalized; } default: { return Vector3.zero; } } } /// /// Raycasts along the given ray and returns the intersection with the flat canvas. /// Use to convert from world space to flat canvas space. /// /// /// /// Returns the true if ray hits the CurvedCanvas. /// Outputs intersection point in flat canvas space. /// public bool RaycastToCanvasSpace(Ray ray, out Vector2 o_positionOnCanvas) { CurvedUIRaycaster caster = this.GetComponent(); o_positionOnCanvas = Vector2.zero; switch (Shape) { case CurvedUISettings.CurvedUIShape.CYLINDER: { return caster.RaycastToCyllinderCanvas(ray, out o_positionOnCanvas, true); } case CurvedUISettings.CurvedUIShape.CYLINDER_VERTICAL: { return caster.RaycastToCyllinderVerticalCanvas(ray, out o_positionOnCanvas, true); } case CurvedUISettings.CurvedUIShape.RING: { return caster.RaycastToRingCanvas(ray, out o_positionOnCanvas, true); } case CurvedUISettings.CurvedUIShape.SPHERE: { return caster.RaycastToSphereCanvas(ray, out o_positionOnCanvas, true); } default: { return false; } } } /// /// Returns the radius of curved canvas cyllinder, expressed in Cavas's local space units. /// public float GetCyllinderRadiusInCanvasSpace() { float ret; if (PreserveAspect) { if(shape == CurvedUIShape.CYLINDER_VERTICAL) ret = (RectTransform.rect.size.y / ((2 * Mathf.PI) * (angle / 360.0f))); else ret = (RectTransform.rect.size.x / ((2 * Mathf.PI) * (angle / 360.0f))); } else ret = (RectTransform.rect.size.x * 0.5f) / Mathf.Sin(Mathf.Clamp(angle, -180.0f, 180.0f) * 0.5f * Mathf.Deg2Rad); return angle == 0 ? 0 : ret; } /// /// Tells you how big UI quads can get before they should be tesselate to look good on current canvas settings. /// Used by CurvedUIVertexEffect to determine how many quads need to be created for each graphic. /// public Vector2 GetTesslationSize(bool modifiedByQuality = true) { Vector2 ret = RectTransform.rect.size; if (Angle != 0 || (!PreserveAspect && vertAngle != 0)) { switch (shape) { case CurvedUIShape.CYLINDER: ret /= GetSegmentsByAngle(angle); break; case CurvedUIShape.CYLINDER_VERTICAL: goto case CurvedUIShape.CYLINDER; case CurvedUIShape.RING: goto case CurvedUIShape.CYLINDER; case CurvedUIShape.SPHERE: { ret.x /= GetSegmentsByAngle(angle); if (PreserveAspect) ret.y = ret.x * RectTransform.rect.size.y / RectTransform.rect.size.x; else ret.y /= GetSegmentsByAngle(VerticalAngle); break; } } } //Debug.Log(this.gameObject.name + " returning size " + ret + " which is " + ret * this.transform.localScale.x + " in world space.", this.gameObject); return ret / (modifiedByQuality ? Mathf.Clamp(Quality, 0.01f, 10.0f) : 1); } float GetSegmentsByAngle(float angle) { if (angle.Abs() <= 1) return 1; else if (angle.Abs() < 90)//proportionaly twice as many segments for small angle canvases return baseCircleSegments * (Mathf.Abs(angle).Remap(0, 90, 0.01f, 0.5f)); else return baseCircleSegments * (Mathf.Abs(angle).Remap(90, 360.0f, 0.5f, 1)); } /// /// Tells you how many segmetens should the entire 360 deg. cyllinder or sphere consist of. /// Used by CurvedUIVertexEffect /// public int BaseCircleSegments { get { return baseCircleSegments; } } /// /// The measure of the arc of the Canvas. /// public int Angle { get { return angle; } set { if (angle != value) SetUIAngle(value); } } /// /// Multiplier used to deremine how many segments a base curve of a shape has. /// Default 1. Lower values greatly increase performance. Higher values give you sharper curve. /// public float Quality { get { return quality; } set { if (quality != value) { quality = value; SetUIAngle(angle); } } } /// /// Current Shape of the canvas /// public CurvedUIShape Shape { get { return shape; } set { if (shape != value) { shape = value; SetUIAngle(angle); } } } /// /// Vertical angle of the canvas. Used in sphere shape and ring shape. /// public int VerticalAngle { get { return vertAngle; } set { if (vertAngle != value) { vertAngle = value; SetUIAngle(angle); } } } /// /// Fill of a ring in ring shaped canvas. 0-1 /// public float RingFill { get { return ringFill; } set { if (ringFill != value) { ringFill = value; SetUIAngle(angle); } } } /// /// Calculated radius of the curved canvas. /// public float SavedRadius { get { if (savedRadius == 0) savedRadius = GetCyllinderRadiusInCanvasSpace(); return savedRadius; } } /// /// External diameter of the ring shaped canvas. /// public int RingExternalDiameter { get { return ringExternalDiamater; } set { if (ringExternalDiamater != value) { ringExternalDiamater = value; SetUIAngle(angle); } } } /// /// Whether the center of the ring should be bottom or top of the canvas. /// public bool RingFlipVertical { get { return ringFlipVertical; } set { if (ringFlipVertical != value) { ringFlipVertical = value; SetUIAngle(angle); } } } /// /// If enabled, CurvedUI will try to preserve aspect ratio of original canvas. /// public bool PreserveAspect { get { return preserveAspect; } set { if (preserveAspect != value) { preserveAspect = value; SetUIAngle(angle); } } } /// /// Can the canvas be interacted with? /// public bool Interactable { get { return interactable; } set { interactable = value; } } /// /// Should The collider for this canvas be created using more expensive box colliders? /// DEfault false. /// public bool ForceUseBoxCollider { get { return forceUseBoxCollider; } set { forceUseBoxCollider = value; } } /// /// Will the canvas block raycasts /// Settings this to false will destroy the canvas' collider. /// public bool BlocksRaycasts { get { return blocksRaycasts; } set { if (blocksRaycasts != value) { blocksRaycasts = value; //tell raycaster to update its collider now that angle has changed. if (Application.isPlaying && GetComponent() != null) GetComponent().RebuildCollider(); } } } /// /// Should the raycaster take other layers into account to determine if canvas has been interacted with. /// [Obsolete("Use RaycastLayerMask property instead.")] public bool RaycastMyLayerOnly { get { return true; } set { } } /// /// Forces all child CurvedUI objects to recalculate /// /// Forces children to recalculate only the curvature. Will not make them retesselate vertices. Much faster. public void SetAllChildrenDirty(bool recalculateCurveOnly = false) { foreach (CurvedUIVertexEffect eff in this.GetComponentsInChildren()) { if (recalculateCurveOnly) eff.SetDirty(); else eff.CurvingRequired = true; } } #endregion #region SHORTCUTS /// /// Returns true if user's pointer is currently pointing inside this canvas. /// This is a shortcut to CurvedUIRaycaster's PointingAtCanvas bool. /// public bool PointingAtCanvas { get { if (GetComponent() != null) return GetComponent().PointingAtCanvas; else { Debug.LogWarning("CURVEDUI: Can't check if user is pointing at this canvas - No CurvedUIRaycaster is added to this canvas."); return false; } } } /// /// Sends OnClick event to every Button under pointer. /// This is a shortcut to CurvedUIRaycaster's Click method. /// public void Click() { if (GetComponent() != null) GetComponent().Click(); } /// /// Current controller mode. Decides how user can interact with the canvas. /// This is a shortcut to CurvedUIInputModule's property. /// public CurvedUIInputModule.CUIControlMethod ControlMethod { get { return CurvedUIInputModule.ControlMethod; } set { CurvedUIInputModule.ControlMethod = value; } } /// /// Returns all objects currently under the pointer. /// This is a shortcut to CurvedUIInputModule's method. /// public List GetObjectsUnderPointer() { if (GetComponent() != null) return GetComponent().GetObjectsUnderPointer(); else return new List(); } /// /// Returns all the canvas objects that are visible under given Screen Position of EventCamera /// This is a shortcut to CurvedUIInputModule's method. /// public List GetObjectsUnderScreenPos(Vector2 pos, Camera eventCamera = null) { if (eventCamera == null) eventCamera = myCanvas.worldCamera; if (GetComponent() != null) return GetComponent().GetObjectsUnderScreenPos(pos, eventCamera); else return new List(); } /// /// Returns all the canvas objects that are intersected by given ray. /// This is a shortcut to CurvedUIInputModule's method. /// public List GetObjectsHitByRay(Ray ray) { if (GetComponent() != null) return GetComponent().GetObjectsHitByRay(ray); else return new List(); } /// /// Gaze Control Method. Should execute OnClick events on button after user points at them? /// This is a shortcut to CurvedUIInputModule's property. /// public bool GazeUseTimedClick { get { return CurvedUIInputModule.Instance.GazeUseTimedClick; } set { CurvedUIInputModule.Instance.GazeUseTimedClick = value; } } /// /// Gaze Control Method. How long after user points on a button should we click it? /// This is a shortcut to CurvedUIInputModule's property. /// public float GazeClickTimer { get { return CurvedUIInputModule.Instance.GazeClickTimer; } set { CurvedUIInputModule.Instance.GazeClickTimer = value; } } /// /// Gaze Control Method. How long after user looks at a button should we start the timer? Default 1 second. /// This is a shortcut to CurvedUIInputModule's property. /// public float GazeClickTimerDelay { get { return CurvedUIInputModule.Instance.GazeClickTimerDelay; } set { CurvedUIInputModule.Instance.GazeClickTimerDelay = value; } } /// /// Gaze Control Method. How long till Click method is executed on Buttons under gaze? Goes 0-1. /// This is a shortcut to CurvedUIInputModule's property. /// public float GazeTimerProgress { get { return CurvedUIInputModule.Instance.GazeTimerProgress; } } #endregion #region ENUMS public enum CurvedUIShape { CYLINDER = 0, RING = 1, SPHERE = 2, CYLINDER_VERTICAL = 3, } #endregion } }