diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs')
-rw-r--r-- | Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs | 601 |
1 files changed, 601 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs new file mode 100644 index 0000000..d97befa --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/RVO/RVOController.cs @@ -0,0 +1,601 @@ +using UnityEngine; +using System.Collections.Generic; +using UnityEngine.Serialization; + +namespace Pathfinding.RVO { + using Pathfinding.Util; + using Pathfinding.Drawing; + + /// <summary> + /// RVO Character Controller. + /// Similar to Unity's CharacterController. It handles movement calculations and takes other agents into account. + /// It does not handle movement itself, but allows the calling script to get the calculated velocity and + /// use that to move the object using a method it sees fit (for example using a CharacterController, using + /// transform.Translate or using a rigidbody). + /// + /// <code> + /// public void Update () { + /// // Just some point far away + /// var targetPoint = transform.position + transform.forward * 100; + /// + /// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12 + /// controller.SetTarget(targetPoint, 10, 12, targetPoint); + /// + /// // Calculate how much to move during this frame + /// // This information is based on movement commands from earlier frames + /// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component + /// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime); + /// transform.position = transform.position + delta; + /// } + /// </code> + /// + /// For documentation of many of the variables of this class: refer to the Pathfinding.RVO.IAgent interface. + /// + /// Note: Requires a single RVOSimulator component in the scene + /// + /// See: Pathfinding.RVO.IAgent + /// See: RVOSimulator + /// See: local-avoidance (view in online documentation for working links) + /// </summary> + [AddComponentMenu("Pathfinding/Local Avoidance/RVO Controller")] + [UniqueComponent(tag = "rvo")] + [HelpURL("https://arongranberg.com/astar/documentation/stable/rvocontroller.html")] + public class RVOController : VersionedMonoBehaviour { + [SerializeField][FormerlySerializedAs("radius")] + internal float radiusBackingField = 0.5f; + + [SerializeField][FormerlySerializedAs("height")] + float heightBackingField = 2; + + [SerializeField][FormerlySerializedAs("center")] + float centerBackingField = 1; + + /// <summary> + /// Radius of the agent in world units. + /// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script. + /// </summary> + public float radius { + get { + if (ai != null) return ai.radius; + return radiusBackingField; + } + set { + if (ai != null) ai.radius = value; + radiusBackingField = value; + } + } + + /// <summary> + /// Height of the agent in world units. + /// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script. + /// </summary> + public float height { + get { + if (ai != null) return ai.height; + return heightBackingField; + } + set { + if (ai != null) ai.height = value; + heightBackingField = value; + } + } + + /// <summary>A locked unit cannot move. Other units will still avoid it but avoidance quality is not the best.</summary> + [Tooltip("A locked unit cannot move. Other units will still avoid it. But avoidance quality is not the best")] + public bool locked; + + /// <summary> + /// Automatically set <see cref="locked"/> to true when desired velocity is approximately zero. + /// This prevents other units from pushing them away when they are supposed to e.g block a choke point. + /// + /// When this is true every call to <see cref="SetTarget"/> or <see cref="Move"/> will set the <see cref="locked"/> field to true if the desired velocity + /// was non-zero or false if it was zero. + /// </summary> + [Tooltip("Automatically set #locked to true when desired velocity is approximately zero")] + public bool lockWhenNotMoving = false; + + /// <summary>How far into the future to look for collisions with other agents (in seconds)</summary> + [Tooltip("How far into the future to look for collisions with other agents (in seconds)")] + public float agentTimeHorizon = 2; + + /// <summary>How far into the future to look for collisions with obstacles (in seconds)</summary> + [Tooltip("How far into the future to look for collisions with obstacles (in seconds)")] + public float obstacleTimeHorizon = 0.5f; + + /// <summary> + /// Max number of other agents to take into account. + /// A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality. + /// </summary> + [Tooltip("Max number of other agents to take into account.\n" + + "A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.")] + public int maxNeighbours = 10; + + /// <summary> + /// Specifies the avoidance layer for this agent. + /// The <see cref="collidesWith"/> mask on other agents will determine if they will avoid this agent. + /// </summary> + public RVOLayer layer = RVOLayer.DefaultAgent; + + /// <summary> + /// Layer mask specifying which layers this agent will avoid. + /// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ... + /// + /// This can be very useful in games which have multiple teams of some sort. For example you usually + /// want the agents in one team to avoid each other, but you do not want them to avoid the enemies. + /// + /// This field only affects which other agents that this agent will avoid, it does not affect how other agents + /// react to this agent. + /// + /// See: bitmasks (view in online documentation for working links) + /// See: http://en.wikipedia.org/wiki/Mask_(computing) + /// </summary> + [Pathfinding.EnumFlag] + public RVOLayer collidesWith = (RVOLayer)(-1); + + /// <summary> + /// An extra force to avoid walls. + /// This can be good way to reduce "wall hugging" behaviour. + /// + /// Deprecated: This feature is currently disabled as it didn't work that well and was tricky to support after some changes to the RVO system. It may be enabled again in a future version. + /// </summary> + [HideInInspector] + [System.Obsolete] + public float wallAvoidForce = 1; + + /// <summary> + /// How much the wallAvoidForce decreases with distance. + /// The strenght of avoidance is: + /// <code> str = 1/dist*wallAvoidFalloff </code> + /// + /// See: wallAvoidForce + /// + /// Deprecated: This feature is currently disabled as it didn't work that well and was tricky to support after some changes to the RVO system. It may be enabled again in a future version. + /// </summary> + [HideInInspector] + [System.Obsolete] + public float wallAvoidFalloff = 1; + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.Priority}</summary> + [Tooltip("How strongly other agents will avoid this agent")] + [UnityEngine.Range(0, 1)] + public float priority = 0.5f; + + /// <summary> + /// Priority multiplier. + /// This functions identically to the <see cref="priority"/>, however it is not exposed in the Unity inspector. + /// It is primarily used by the <see cref="Pathfinding.RVO.RVODestinationCrowdedBehavior"/>. + /// </summary> + [System.NonSerialized] + public float priorityMultiplier = 1.0f; + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.FlowFollowingStrength}</summary> + [System.NonSerialized] + public float flowFollowingStrength = 0.0f; + + GraphNode obstacleQuery; + + /// <summary> + /// Center of the agent relative to the pivot point of this game object. + /// Note: If a movement script (AIPath/RichAI/AILerp, anything implementing the IAstarAI interface) is attached to the same GameObject, this value will be driven by that script. + /// </summary> + public float center { + get { + // With an AI attached, this will always be driven to height/2 because the movement script expects the object position to be at its feet + if (ai != null) return ai.height/2; + return centerBackingField; + } + set { + centerBackingField = value; + } + } + + /// <summary>\details Deprecated:</summary> + [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")] + public LayerMask mask { get { return 0; } set {} } + + /// <summary>\details Deprecated:</summary> + [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")] + public bool enableRotation { get { return false; } set {} } + + /// <summary>\details Deprecated:</summary> + [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")] + public float rotationSpeed { get { return 0; } set {} } + + /// <summary>\details Deprecated:</summary> + [System.Obsolete("This field is obsolete in version 4.0 and will not affect anything. Use the LegacyRVOController if you need the old behaviour")] + public float maxSpeed { get { return 0; } set {} } + + public MovementPlane movementPlaneMode => simulator?.MovementPlane ?? RVOSimulator.active?.movementPlane ?? MovementPlane.XZ; + + /// <summary>Determines if the XY (2D) or XZ (3D) plane is used for movement</summary> + public SimpleMovementPlane movementPlane { + get { + var mode = simulator?.MovementPlane ?? RVOSimulator.active?.movementPlane; + if (mode != null) { + if (mode.Value == MovementPlane.Arbitrary) return movementPlaneBackingField; + if (mode.Value == MovementPlane.XY) return SimpleMovementPlane.XYPlane; + } + return SimpleMovementPlane.XZPlane; + } + set { + var mode = simulator?.MovementPlane ?? RVOSimulator.active?.movementPlane; + if (mode != null && mode.Value != MovementPlane.Arbitrary) throw new System.InvalidOperationException("Cannot set the movement plane unless the RVOSimulator's movement plane setting is set to Arbitrary."); + movementPlaneBackingField = value; + } + } + + /// <summary>Reference to the internal agent</summary> + public IAgent rvoAgent { get; private set; } + + /// <summary>Reference to the rvo simulator</summary> + SimulatorBurst simulator { get; set; } + + /// <summary>Cached tranform component</summary> + protected Transform tr; + + [SerializeField] + [FormerlySerializedAs("ai")] + IAstarAI aiBackingField; + + internal SimpleMovementPlane movementPlaneBackingField = GraphTransform.xzPlane.ToSimpleMovementPlane(); + + /// <summary>Cached reference to a movement script (if one is used)</summary> + protected IAstarAI ai { + get { +#if UNITY_EDITOR + if (aiBackingField == null && !Application.isPlaying) aiBackingField = GetComponent<IAstarAI>(); +#endif + // Note: have to cast to MonoBehaviour to get Unity's special overloaded == operator. + // If we didn't do this then this property could return a non-null value that pointed to a destroyed component. + if ((aiBackingField as MonoBehaviour) == null) aiBackingField = null; + return aiBackingField; + } + set { + aiBackingField = value; + } + } + + /// <summary>Enables drawing debug information in the scene view</summary> + public AgentDebugFlags debug; + + /// <summary> + /// Current position of the agent. + /// Note that this is only updated every local avoidance simulation step, not every frame. + /// </summary> + public Vector3 position { + get { + simulator.BlockUntilSimulationStepDone(); + return rvoAgent.Position; + } + } + + /// <summary> + /// Current calculated velocity of the agent. + /// This is not necessarily the velocity the agent is actually moving with + /// (that is up to the movement script to decide) but it is the velocity + /// that the RVO system has calculated is best for avoiding obstacles and + /// reaching the target. + /// + /// See: CalculateMovementDelta + /// + /// You can also set the velocity of the agent. This will override the local avoidance input completely. + /// It is useful if you have a player controlled character and want other agents to avoid it. + /// + /// Setting the velocity using this property will mark the agent as being externally controlled for 1 simulation step. + /// Local avoidance calculations will be skipped for the next simulation step but will be resumed + /// after that unless this property is set again. + /// + /// Note that if you set the velocity the value that can be read from this property will not change until + /// the next simulation step. + /// + /// See: <see cref="Pathfinding::RVO::IAgent::ForceSetVelocity"/> + /// See: ManualRVOAgent.cs (view in online documentation for working links) + /// </summary> + public Vector3 velocity { + get { + // For best accuracy and to allow other code to do things like Move(agent.velocity * Time.deltaTime) + // the code bases the velocity on how far the agent should move during this frame. + // Unless the game is paused (timescale is zero) then just use a very small dt. + var dt = Time.deltaTime > 0.0001f ? Time.deltaTime : 0.02f; + return CalculateMovementDelta(dt) / dt; + } + set { + simulator.BlockUntilSimulationStepDone(); + rvoAgent.ForceSetVelocity(value); + } + } + + /// <summary> + /// Direction and distance to move in a single frame to avoid obstacles. + /// + /// The position of the agent is taken from the attached movement script's position (see <see cref="Pathfinding.IAstarAI.position)"/> or if none is attached then transform.position. + /// </summary> + /// <param name="deltaTime">How far to move [seconds]. + /// Usually set to Time.deltaTime.</param> + public Vector3 CalculateMovementDelta(float deltaTime) => CalculateMovementDelta(ai != null ? ai.position : tr.position, deltaTime); + + /// <summary> + /// Direction and distance to move in a single frame to avoid obstacles. + /// + /// <code> + /// public void Update () { + /// // Just some point far away + /// var targetPoint = transform.position + transform.forward * 100; + /// + /// // Set the desired point to move towards using a desired speed of 10 and a max speed of 12 + /// controller.SetTarget(targetPoint, 10, 12, targetPoint); + /// + /// // Calculate how much to move during this frame + /// // This information is based on movement commands from earlier frames + /// // as local avoidance is calculated globally at regular intervals by the RVOSimulator component + /// var delta = controller.CalculateMovementDelta(transform.position, Time.deltaTime); + /// transform.position = transform.position + delta; + /// } + /// </code> + /// </summary> + /// <param name="position">Position of the agent.</param> + /// <param name="deltaTime">How far to move [seconds]. + /// Usually set to Time.deltaTime.</param> + public Vector3 CalculateMovementDelta (Vector3 position, float deltaTime) { + if (rvoAgent == null) return Vector3.zero; + var delta = movementPlane.ToPlane(rvoAgent.CalculatedTargetPoint - position); + return movementPlane.ToWorld(Vector2.ClampMagnitude(delta, rvoAgent.CalculatedSpeed * deltaTime), 0); + } + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.AvoidingAnyAgents}</summary> + public bool AvoidingAnyAgents { + get { + if (rvoAgent == null) return false; + return rvoAgent.AvoidingAnyAgents; + } + } + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.SetCollisionNormal}</summary> + public void SetCollisionNormal (Vector3 normal) { + simulator.BlockUntilSimulationStepDone(); + rvoAgent.SetCollisionNormal(normal); + } + + /// <summary>\copydocref{Pathfinding.RVO.IAgent.SetObstacleQuery}</summary> + public void SetObstacleQuery (GraphNode sourceNode) { + obstacleQuery = sourceNode; + } + + /// <summary> + /// \copydocref{Pathfinding.RVO.IAgent.ForceSetVelocity}. + /// Deprecated: Set the <see cref="velocity"/> property instead + /// </summary> + [System.Obsolete("Set the 'velocity' property instead")] + public void ForceSetVelocity (Vector3 velocity) { + this.velocity = velocity; + } + + /// <summary> + /// Converts a 3D vector to a 2D vector in the movement plane. + /// If movementPlane is XZ it will be projected onto the XZ plane + /// otherwise it will be projected onto the XY plane. + /// </summary> + public Vector2 To2D (Vector3 p) { + return movementPlane.ToPlane(p); + } + + /// <summary> + /// Converts a 3D vector to a 2D vector in the movement plane. + /// If movementPlane is XZ it will be projected onto the XZ plane + /// and the elevation coordinate will be the Y coordinate + /// otherwise it will be projected onto the XY plane and elevation + /// will be the Z coordinate. + /// </summary> + public Vector2 To2D (Vector3 p, out float elevation) { + return movementPlane.ToPlane(p, out elevation); + } + + /// <summary> + /// Converts a 2D vector in the movement plane as well as an elevation to a 3D coordinate. + /// See: To2D + /// See: movementPlane + /// </summary> + public Vector3 To3D (Vector2 p, float elevationCoordinate) { + return movementPlane.ToWorld(p, elevationCoordinate); + } + + void OnDisable () { + if (simulator == null) return; + + // Remove the agent from the simulation but keep the reference + // this component might get enabled and then we can simply + // add it to the simulation again + simulator.RemoveAgent(rvoAgent); + simulator = null; + rvoAgent = null; + } + + void OnEnable () { + tr = transform; + ai = GetComponent<IAstarAI>(); + + // Make sure the AI finds this component + // This is useful if the RVOController was added during runtime. + if (ai is AIBase aiBase) aiBase.FindComponents(); + + if (RVOSimulator.active == null) { + Debug.LogError("No RVOSimulator component found in the scene. Please add one."); + enabled = false; + } else { + simulator = RVOSimulator.active.GetSimulator(); + rvoAgent = simulator.AddAgent(Vector3.zero); + rvoAgent.PreCalculationCallback = UpdateAgentProperties; + rvoAgent.DestroyedCallback = OnAgentDestroyed; + } + } + + void OnAgentDestroyed () { + // This can happen if the RVOSimulator component is destroyed. + if (gameObject.activeInHierarchy) { + // We clear the fields to avoid calling simulator.RemoveAgent during OnDisable + simulator = null; + rvoAgent = null; + enabled = false; + } else { + // If the gameObject is not active, we do not want to set this.enabled to false + // as that will also disable this component. + // If the user used e.g. a pooling system, they might inactivate the gameObject, + // but would expect the RVOController component to be enabled when they activate it again. + } + } + + protected void UpdateAgentProperties () { + var scale = tr.localScale; + + rvoAgent.Radius = Mathf.Max(0.001f, radius * Mathf.Abs(scale.x)); + rvoAgent.AgentTimeHorizon = agentTimeHorizon; + rvoAgent.ObstacleTimeHorizon = obstacleTimeHorizon; + rvoAgent.Locked = locked; + rvoAgent.MaxNeighbours = maxNeighbours; + rvoAgent.DebugFlags = debug; + rvoAgent.Layer = layer; + rvoAgent.CollidesWith = collidesWith; + + var plane = movementPlane; + rvoAgent.MovementPlane = plane; + + // Use the position from the movement script if one is attached + // as the movement script's position may not be the same as the transform's position + // (in particular if IAstarAI.updatePosition is false). + var pos = plane.ToPlane(ai != null ? ai.position : tr.position, out float elevation); + + if (movementPlaneMode == MovementPlane.XY) { + // In 2D it is assumed the Z coordinate differences of agents is ignored. + rvoAgent.Height = 1; + rvoAgent.Position = plane.ToWorld(pos, 0); + } else { + rvoAgent.Height = height * scale.y; + rvoAgent.Position = plane.ToWorld(pos, elevation + (center - 0.5f * height) * scale.y); + } + + // TODO: Move this to a separate file + var reached = rvoAgent.CalculatedEffectivelyReachedDestination; + var prio = priority * priorityMultiplier; + var flow = flowFollowingStrength; + if (reached == ReachedEndOfPath.Reached) { + flow = 1.0f; + prio *= 0.3f; + } else if (reached == ReachedEndOfPath.ReachedSoon) { + flow = 1.0f; + prio *= 0.45f; + } + rvoAgent.Priority = prio; + rvoAgent.FlowFollowingStrength = flow; + + // Note: We need to set this during UpdateAgentProperties to avoid a race condition. + // The HierarchicalNodeIndex, which is what is stored in the rvoAgent, can be invalidated by graph updates. + // So we must ensure that there cannot be any graph updates between when we set this, and when the simulation step happens. + // So setting it here, which is right before the simulation step, is a good option. + rvoAgent.SetObstacleQuery(obstacleQuery); + obstacleQuery = null; + } + + /// <summary> + /// Set the target point for the agent to move towards. + /// Similar to the <see cref="Move"/> method but this is more flexible. + /// It is also better to use near the end of the path as when using the Move + /// method the agent does not know where to stop, so it may overshoot the target. + /// When using this method the agent will not overshoot the target. + /// The agent will assume that it will stop when it reaches the target so make sure that + /// you don't place the point too close to the agent if you actually just want to move in a + /// particular direction. + /// + /// The target point is assumed to stay the same until something else is requested (as opposed to being reset every frame). + /// + /// See: Also take a look at the documentation for <see cref="IAgent.SetTarget"/> which has a few more details. + /// See: <see cref="Move"/> + /// </summary> + /// <param name="pos">Point in world space to move towards.</param> + /// <param name="speed">Desired speed in world units per second.</param> + /// <param name="maxSpeed">Maximum speed in world units per second. + /// The agent will use this speed if it is necessary to avoid collisions with other agents. + /// Should be at least as high as speed, but it is recommended to use a slightly higher value than speed (for example speed*1.2).</param> + /// <param name="endOfPath">Point in world space which is the agent's final desired destination on the navmesh. + /// This is typically the end of the path the agent is following. + /// May be set to (+inf,+inf,+inf) to mark the agent as not having a well defined end of path. + /// If this is set, multiple agents with roughly the same end of path will crowd more naturally around this point. + /// They will be able to realize that they cannot get closer if there are many agents trying to get closer to the same destination and then stop.</param> + public void SetTarget (Vector3 pos, float speed, float maxSpeed, Vector3 endOfPath) { + if (rvoAgent == null) return; + + simulator.BlockUntilSimulationStepDone(); + rvoAgent.SetTarget(pos, speed, maxSpeed, endOfPath); + + if (lockWhenNotMoving) { + locked = speed < 0.001f; + } + } + + /// <summary> + /// Set the desired velocity for the agent. + /// + /// This is assumed to stay the same until something else is requested (as opposed to being reset every frame). + /// + /// Note: In most cases the <see cref="SetTarget"/> method is better to use. + /// What this will actually do is call <see cref="SetTarget"/> with (position + velocity). + /// See the note in the documentation for IAgent.SetTarget about the potential + /// issues that this can cause (in particular that it might be hard to get the agent + /// to stop at a precise point). + /// + /// See: <see cref="SetTarget"/> + /// </summary> + /// <param name="velocity">Velocity in units/second that you want the agent to move with.</param> + public void Move (Vector3 velocity) { + if (rvoAgent == null) return; + + simulator.BlockUntilSimulationStepDone(); + var speed = movementPlane.ToPlane(velocity).magnitude; + rvoAgent.SetTarget((ai != null ? ai.position : tr.position) + velocity, speed, speed, new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity)); + + if (lockWhenNotMoving) { + locked = speed < 0.001f; + } + } + + /// <summary> + /// Teleport the agent to a new position. + /// Deprecated: Use transform.position instead, the RVOController can now handle that without any issues. + /// </summary> + [System.Obsolete("Use transform.position instead, the RVOController can now handle that without any issues.")] + public void Teleport (Vector3 pos) { + tr.position = pos; + } + + public override void DrawGizmos () { + tr = transform; + // The AI script will draw similar gizmos + if (ai == null) { + var color = AIBase.ShapeGizmoColor * (locked ? 0.5f : 1.0f); + var pos = transform.position; + + var scale = tr.localScale; + if (movementPlaneMode == MovementPlane.XY) { + Draw.WireCylinder(pos, Vector3.forward, 0, radius * scale.x, color); + } else { + Draw.WireCylinder(pos + To3D(Vector2.zero, center - height * 0.5f) * scale.y, To3D(Vector2.zero, 1), height * scale.y, radius * scale.x, color); + } + } + } + + [System.Flags] + enum RVOControllerMigrations { + MigrateScale, + } + + protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) { + if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) { + if (legacyVersion > 1) migrations.MarkMigrationFinished((int)RVOControllerMigrations.MigrateScale); + } + if (migrations.AddAndMaybeRunMigration((int)RVOControllerMigrations.MigrateScale, unityThread)) { + if (transform.localScale.y != 0) centerBackingField /= Mathf.Abs(transform.localScale.y); + if (transform.localScale.y != 0) heightBackingField /= Mathf.Abs(transform.localScale.y); + if (transform.localScale.x != 0) radiusBackingField /= Mathf.Abs(transform.localScale.x); + } + } + } +} |