// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System.Collections.Generic; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Reactor.Animations; using Doozy.Runtime.Reactor.Animators; using Doozy.Runtime.UIManager.Animators; using Doozy.Runtime.UIManager.Components; using Doozy.Runtime.UIManager.Layouts.Internal; using UnityEngine; using UnityEngine.UI; // ReSharper disable MemberCanBePrivate.Global namespace Doozy.Runtime.UIManager.Layouts { /// /// The Radial Layout component sets child elements in a radial or circular arrangement. /// [AddComponentMenu("Doozy/UI/Layouts/UI RadialLayout")] [RequireComponent(typeof(RectTransform))] [DisallowMultipleComponent] [ExecuteAlways] public class UIRadialLayout : UILayoutGroup { #if UNITY_EDITOR [UnityEditor.MenuItem("GameObject/Doozy/UI/Layouts/UI RadialLayout", false, 8)] private static void CreateComponent(UnityEditor.MenuCommand menuCommand) { GameObjectUtils.AddToScene("UI RadialLayout", false, true); } #endif public const bool k_AutoRebuildDefaultValue = true; public const bool k_ClockwiseDefaultValue = true; public const bool k_ControlChildHeightDefaultValue = false; public const bool k_ControlChildWidthDefaultValue = false; public const bool k_RadiusControlsHeightDefaultValue = false; public const bool k_RadiusControlsWidthDefaultValue = false; public const bool k_RotateChildrenDefaultValue = false; public const float k_ChildHeightDefaultValue = k_RadiusDefaultValue; public const float k_ChildRotationDefaultValue = 0f; public const float k_ChildWidthDefaultValue = k_RadiusDefaultValue; public const float k_MAXAngle = 360f; public const float k_MAXAngleDefaultValue = 360f; public const float k_MAXRadiusDefaultValue = 1000f; public const float k_MINAngle = 0f; public const float k_MINAngleDefaultValue = 0f; public const float k_RadiusDefaultValue = 100f; public const float k_RadiusHeightFactorDefaultValue = 1f; public const float k_RadiusWidthFactorDefaultValue = 1f; public const float k_SpacingDefaultValue = 0f; public const float k_StartAngleDefaultValue = 180f; [SerializeField] protected bool AutoRebuild = k_AutoRebuildDefaultValue; [SerializeField] protected float ChildHeight = k_ChildHeightDefaultValue; [SerializeField] protected float ChildRotation = k_ChildRotationDefaultValue; [SerializeField] protected float ChildWidth = k_ChildWidthDefaultValue; [SerializeField] protected bool Clockwise = k_ClockwiseDefaultValue; [SerializeField] protected bool ControlChildHeight = k_ControlChildHeightDefaultValue; [SerializeField] protected bool ControlChildWidth = k_ControlChildWidthDefaultValue; [Range(k_MINAngle, k_MAXAngle)] [SerializeField] protected float MaxAngle = k_MAXAngleDefaultValue; [SerializeField] protected float MaxRadius = k_MAXRadiusDefaultValue; [Range(k_MINAngle, k_MAXAngle)] [SerializeField] protected float MinAngle = k_MINAngleDefaultValue; [SerializeField] protected float Radius = k_RadiusDefaultValue; [SerializeField] protected bool RadiusControlsHeight = k_RadiusControlsHeightDefaultValue; [SerializeField] protected bool RadiusControlsWidth = k_RadiusControlsWidthDefaultValue; [SerializeField] protected float RadiusHeightFactor = k_RadiusHeightFactorDefaultValue; [SerializeField] protected float RadiusWidthFactor = k_RadiusWidthFactorDefaultValue; [SerializeField] protected bool RotateChildren = k_RotateChildrenDefaultValue; [SerializeField] protected float Spacing = k_SpacingDefaultValue; [Range(k_MINAngle, k_MAXAngle)] [SerializeField] protected float StartAngle = k_StartAngleDefaultValue; /// Internal list used to count the number of child elements this layout has. It's main purpose is to improve layout performance by reducing GC private List m_ChildList = new List(); /// Automatically rebuild the layout when a parameter has changed and update the layout public bool autoRebuild { get => AutoRebuild; set { if (AutoRebuild == value) return; AutoRebuild = value; OnValueChanged(); } } /// Child elements height when control child height is enabled public float childHeight { get => ChildHeight; set { if (Mathf.Approximately(ChildHeight, value)) return; ChildHeight = value; OnValueChanged(); } } /// Child elements custom rotation public float childRotation { get => ChildRotation; set { if (Mathf.Approximately(ChildRotation, value)) return; ChildRotation = value; OnValueChanged(); } } /// Child elements width when control child width is enabled public float childWidth { get => ChildWidth; set { if (Mathf.Approximately(ChildWidth, value)) return; ChildWidth = value; OnValueChanged(); } } /// Order the child elements clockwise and update the layout public bool clockwise { get => Clockwise; set { if (Clockwise == value) return; Clockwise = value; OnValueChanged(); } } /// Override the child elements height and update the layout public bool controlChildHeight { get => ControlChildHeight; set { ControlChildHeight = value; OnValueChanged(); } } /// Override the child elements width and update the layout public bool controlChildWidth { get => ControlChildWidth; set { ControlChildWidth = value; OnValueChanged(); } } /// Maximum angle a child element can have inside the layout. Used to make the radial layout look as an arc public float maxAngle { get => MaxAngle; set { if (Mathf.Approximately(MaxAngle, value)) return; MaxAngle = value; OnValueChanged(); } } /// Minimum angle a child element can have inside the layout. Used to make the radial layout look as an arc public float minAngle { get => MinAngle; set { if (Mathf.Approximately(MinAngle, value)) return; MinAngle = value; OnValueChanged(); } } /// Layout radius that determines the size of the circle public float radius { get => Radius; set { if (Mathf.Approximately(Radius, value)) return; Radius = value; OnValueChanged(); } } /// Set the child elements height to be influenced by the layout radius and update the layout public bool radiusControlsHeight { get => RadiusControlsHeight; set { RadiusControlsHeight = value; OnValueChanged(); } } /// Set the child elements width to be influenced by the layout radius and update the layout public bool radiusControlsWidth { get => RadiusControlsWidth; set { RadiusControlsWidth = value; OnValueChanged(); } } /// Factor by which the radius influences the child elements height, if radius controls height is enabled public float radiusHeightFactor { get => RadiusHeightFactor; set { if (Mathf.Approximately(RadiusHeightFactor, value)) return; RadiusHeightFactor = value; OnValueChanged(); } } /// Factor by which the radius influences the child elements width, if the radius controls width is enabled public float radiusWidthFactor { get => RadiusWidthFactor; set { if (Mathf.Approximately(RadiusWidthFactor, value)) return; RadiusWidthFactor = value; OnValueChanged(); } } /// Automatically rotate child elements with the layout, when the start angle changes and update the layout public bool rotateChildren { get => RotateChildren; set { RotateChildren = value; OnValueChanged(); } } /// Extra spacing between child elements public float spacing { get => Spacing; set { if (Mathf.Approximately(Spacing, value)) return; Spacing = value; OnValueChanged(); } } /// Start angle for the first child element of the layout. This places all the child elements around the layout radius public float startAngle { get => StartAngle; set { if (Mathf.Approximately(StartAngle, value)) return; StartAngle = value; OnValueChanged(); } } private bool runUpdateAnimatorsStartPosition { get; set; } #if UNITY_EDITOR protected override void Reset() { base.Reset(); CalculateRadial(); } #endif protected override void OnEnable() { if (!Application.isPlaying) return; // base.OnEnable(); runUpdateAnimatorsStartPosition = false; CalculateRadial(); } public override void SetLayoutHorizontal() {} public override void SetLayoutVertical() {} public override void CalculateLayoutInputVertical() => CalculateRadial(); public override void CalculateLayoutInputHorizontal() => CalculateRadial(); /// Rebuild the layout public void CalculateRadial() { m_ChildList ??= new List(); m_ChildList.Clear(); int activeChildCount = 0; for (int i = 0; i < transform.childCount; i++) { var child = transform.GetChild(i) as RectTransform; if (child == null) continue; LayoutElement childLayout = child.GetComponent(); if (child == null || !child.gameObject.activeSelf || (childLayout != null && childLayout.ignoreLayout)) continue; m_ChildList.Add(child); activeChildCount++; } m_Tracker.Clear(); if (activeChildCount == 0) return; if (Application.isPlaying & !runUpdateAnimatorsStartPosition) { runUpdateAnimatorsStartPosition = true; UpdateAnimatorsStartValues(); } rectTransform.sizeDelta = new Vector2(Radius, Radius) * 2f; float sAngle = 360f / activeChildCount * (activeChildCount - 1f); float angleOffset = MinAngle; if (angleOffset > sAngle) angleOffset = sAngle; float maximumAngle = 360f - MaxAngle; if (maximumAngle > sAngle) maximumAngle = sAngle; if (angleOffset > sAngle) angleOffset = sAngle; float buff = sAngle - angleOffset; float fOffsetAngle = ((buff - maximumAngle)) / (activeChildCount - 1f) + Spacing; float fAngle = StartAngle + angleOffset; bool controlChildrenSize = ControlChildWidth | ControlChildHeight; DrivenTransformProperties drivenTransformProperties = DrivenTransformProperties.Anchors | DrivenTransformProperties.AnchoredPosition | DrivenTransformProperties.Pivot; if (ControlChildWidth) drivenTransformProperties |= DrivenTransformProperties.SizeDeltaX; if (ControlChildHeight) drivenTransformProperties |= DrivenTransformProperties.SizeDeltaY; if (RotateChildren) drivenTransformProperties |= DrivenTransformProperties.Rotation; if (Clockwise) fOffsetAngle *= -1f; foreach (RectTransform child in m_ChildList) { if (child == null || !child.gameObject.activeSelf) continue; //if child is null or not active -> continue m_Tracker.Add(this, child, drivenTransformProperties); //add elements to the tracker to stop the user from modifying their positions via the editor var vPos = new Vector3(Mathf.Cos(fAngle * Mathf.Deg2Rad), Mathf.Sin(fAngle * Mathf.Deg2Rad), 0); //calculate the child position child.localPosition = vPos * Radius; //set the child position child.anchorMin = child.anchorMax = child.pivot = new Vector2(0.5f, 0.5f); //force children to be center aligned, to keep all of the objects with the same anchor points float elementAngle = ChildRotation; if (RotateChildren) elementAngle += fAngle; child.localEulerAngles = new Vector3(0f, 0f, elementAngle); if (controlChildrenSize) { Vector2 childSizeDelta = child.sizeDelta; if (controlChildWidth) childSizeDelta.x = RadiusControlsWidth ? ChildWidth * Radius * RadiusWidthFactor / 100 : ChildWidth; if (controlChildHeight) childSizeDelta.y = RadiusControlsHeight ? ChildHeight * Radius * RadiusHeightFactor / 100 : ChildHeight; child.sizeDelta = childSizeDelta; } fAngle += fOffsetAngle; } } private void UpdateAnimatorsStartValues() { LayoutRebuilder.MarkLayoutForRebuild(rectTransform); for (int i = 0; i < transform.childCount; i++) { var child = transform.GetChild(i) as RectTransform; if (child == null) continue; UIAnimator uiAnimator = child.GetComponent(); if (uiAnimator != null) { uiAnimator.animation.startPosition = uiAnimator.rectTransform.anchoredPosition3D; uiAnimator.animation.startRotation = uiAnimator.rectTransform.localEulerAngles; if (uiAnimator.animation.isPlaying) uiAnimator.UpdateValues(); } UIContainerUIAnimator uiContainerUIAnimator = child.GetComponent(); if (uiContainerUIAnimator != null) { if (uiContainerUIAnimator.isConnected && uiContainerUIAnimator.controller.isVisible) { uiContainerUIAnimator.showAnimation.startPosition = uiContainerUIAnimator.rectTransform.anchoredPosition3D; uiContainerUIAnimator.showAnimation.startRotation = uiContainerUIAnimator.rectTransform.localEulerAngles; } // uiContainerAnimator.UpdateSettings(); } UISelectableUIAnimator uiSelectableUIAnimator = child.GetComponent(); if (uiSelectableUIAnimator != null) { if (uiSelectableUIAnimator.isConnected && uiSelectableUIAnimator.controller.currentUISelectionState == UISelectionState.Normal & !uiSelectableUIAnimator.anyAnimationIsActive) { foreach (UISelectionState state in UISelectable.uiSelectionStates) { UIAnimation uiAnimation = uiSelectableUIAnimator.GetAnimation(state); if (uiAnimation == null) continue; uiAnimation.startPosition = uiAnimation.rectTransform.anchoredPosition3D; uiAnimation.startRotation = uiAnimation.rectTransform.localEulerAngles; } } // uiSelectableUIAnimator.UpdateSettings(); } } runUpdateAnimatorsStartPosition = false; } private void OnValueChanged() { if (!AutoRebuild) return; CalculateRadial(); } } }