· 6 years ago · Jul 02, 2019, 10:08 PM
1using UnityEngine;
2using System.Collections;
3using System.Collections.Generic;
4
5namespace Pathfinding {
6 using Pathfinding.RVO;
7 using Pathfinding.Util;
8
9 /// <summary>
10 /// AI for following paths.
11 /// This AI is the default movement script which comes with the A* Pathfinding Project.
12 /// It is in no way required by the rest of the system, so feel free to write your own. But I hope this script will make it easier
13 /// to set up movement for the characters in your game.
14 /// This script works well for many types of units, but if you need the highest performance (for example if you are moving hundreds of characters) you
15 /// may want to customize this script or write a custom movement script to be able to optimize it specifically for your game.
16 ///
17 /// This script will try to move to a given <see cref="destination"/>. At <see cref="repathRate regular"/>, the path to the destination will be recalculated.
18 /// If you want to make the AI to follow a particular object you can attach the <see cref="Pathfinding.AIDestinationSetter"/> component.
19 /// Take a look at the getstarted (view in online documentation for working links) tutorial for more instructions on how to configure this script.
20 ///
21 /// Here is a video of this script being used move an agent around (technically it uses the <see cref="Pathfinding.Examples.MineBotAI"/> script that inherits from this one but adds a bit of animation support for the example scenes):
22 /// [Open online documentation to see videos]
23 ///
24 /// \section variables Quick overview of the variables
25 /// In the inspector in Unity, you will see a bunch of variables. You can view detailed information further down, but here's a quick overview.
26 ///
27 /// The <see cref="repathRate"/> determines how often it will search for new paths, if you have fast moving targets, you might want to set it to a lower value.
28 /// The <see cref="destination"/> field is where the AI will try to move, it can be a point on the ground where the player has clicked in an RTS for example.
29 /// Or it can be the player object in a zombie game.
30 /// The <see cref="maxSpeed"/> is self-explanatory, as is <see cref="rotationSpeed"/>. however <see cref="slowdownDistance"/> might require some explanation:
31 /// It is the approximate distance from the target where the AI will start to slow down. Setting it to a large value will make the AI slow down very gradually.
32 /// <see cref="pickNextWaypointDist"/> determines the distance to the point the AI will move to (see image below).
33 ///
34 /// Below is an image illustrating several variables that are exposed by this class (<see cref="pickNextWaypointDist"/>, <see cref="steeringTarget"/>, <see cref="desiredVelocity)"/>
35 /// [Open online documentation to see images]
36 ///
37 /// This script has many movement fallbacks.
38 /// If it finds an RVOController attached to the same GameObject as this component, it will use that. If it finds a character controller it will also use that.
39 /// If it finds a rigidbody it will use that. Lastly it will fall back to simply modifying Transform.position which is guaranteed to always work and is also the most performant option.
40 ///
41 /// \section how-aipath-works How it works
42 /// In this section I'm going to go over how this script is structured and how information flows.
43 /// This is useful if you want to make changes to this script or if you just want to understand how it works a bit more deeply.
44 /// However you do not need to read this section if you are just going to use the script as-is.
45 ///
46 /// This script inherits from the <see cref="AIBase"/> class. The movement happens either in Unity's standard <see cref="Update"/> or <see cref="FixedUpdate"/> method.
47 /// They are both defined in the AIBase class. Which one is actually used depends on if a rigidbody is used for movement or not.
48 /// Rigidbody movement has to be done inside the FixedUpdate method while otherwise it is better to do it in Update.
49 ///
50 /// From there a call is made to the <see cref="MovementUpdate"/> method (which in turn calls <see cref="MovementUpdateInternal)"/>.
51 /// This method contains the main bulk of the code and calculates how the AI *wants* to move. However it doesn't do any movement itself.
52 /// Instead it returns the position and rotation it wants the AI to move to have at the end of the frame.
53 /// The <see cref="Update"/> (or <see cref="FixedUpdate)"/> method then passes these values to the <see cref="FinalizeMovement"/> method which is responsible for actually moving the character.
54 /// That method also handles things like making sure the AI doesn't fall through the ground using raycasting.
55 ///
56 /// The AI recalculates its path regularly. This happens in the Update method which checks <see cref="shouldRecalculatePath"/> and if that returns true it will call <see cref="SearchPath"/>.
57 /// The <see cref="SearchPath"/> method will prepare a path request and send it to the <see cref="Pathfinding.Seeker"/> component which should be attached to the same GameObject as this script.
58 /// Since this script will when waking up register to the <see cref="Pathfinding.Seeker.pathCallback"/> delegate this script will be notified every time a new path is calculated by the <see cref="OnPathComplete"/> method being called.
59 /// It may take one or sometimes multiple frames for the path to be calculated, but finally the <see cref="OnPathComplete"/> method will be called and the current path that the AI is following will be replaced.
60 /// </summary>
61 [AddComponentMenu("Pathfinding/AI/AIPath (2D,3D)")]
62 public partial class AIPath : AIBase, IAstarAI {
63 /// <summary>
64 /// How quickly the agent accelerates.
65 /// Positive values represent an acceleration in world units per second squared.
66 /// Negative values are interpreted as an inverse time of how long it should take for the agent to reach its max speed.
67 /// For example if it should take roughly 0.4 seconds for the agent to reach its max speed then this field should be set to -1/0.4 = -2.5.
68 /// For a negative value the final acceleration will be: -acceleration*maxSpeed.
69 /// This behaviour exists mostly for compatibility reasons.
70 ///
71 /// In the Unity inspector there are two modes: Default and Custom. In the Default mode this field is set to -2.5 which means that it takes about 0.4 seconds for the agent to reach its top speed.
72 /// In the Custom mode you can set the acceleration to any positive value.
73 /// </summary>
74 public float maxAcceleration = -2.5f;
75
76 /// <summary>
77 /// Rotation speed in degrees per second.
78 /// Rotation is calculated using Quaternion.RotateTowards. This variable represents the rotation speed in degrees per second.
79 /// The higher it is, the faster the character will be able to rotate.
80 /// </summary>
81 [UnityEngine.Serialization.FormerlySerializedAs("turningSpeed")]
82 public float rotationSpeed = 360;
83
84 /// <summary>Distance from the end of the path where the AI will start to slow down</summary>
85 public float slowdownDistance = 0.6F;
86
87 /// <summary>
88 /// How far the AI looks ahead along the path to determine the point it moves to.
89 /// In world units.
90 /// If you enable the <see cref="alwaysDrawGizmos"/> toggle this value will be visualized in the scene view as a blue circle around the agent.
91 /// [Open online documentation to see images]
92 ///
93 /// Here are a few example videos showing some typical outcomes with good values as well as how it looks when this value is too low and too high.
94 /// <table>
95 /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too low</span><br/></verbatim>\endxmlonly A too low value and a too low acceleration will result in the agent overshooting a lot and not managing to follow the path well.</td></tr>
96 /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-warning">Ok</span><br/></verbatim>\endxmlonly A low value but a high acceleration works decently to make the AI follow the path more closely. Note that the <see cref="Pathfinding.AILerp"/> component is better suited if you want the agent to follow the path without any deviations.</td></tr>
97 /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example.</td></tr>
98 /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example, but the path is followed slightly more loosely than in the previous video.</td></tr>
99 /// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too high</span><br/></verbatim>\endxmlonly A too high value will make the agent follow the path too loosely and may cause it to try to move through obstacles.</td></tr>
100 /// </table>
101 /// </summary>
102 public float pickNextWaypointDist = 2;
103
104 /// <summary>
105 /// Distance to the end point to consider the end of path to be reached.
106 /// When the end is within this distance then <see cref="OnTargetReached"/> will be called and <see cref="reachedEndOfPath"/> will return true.
107 /// </summary>
108 public float endReachedDistance = 0.2F;
109
110 /// <summary>Draws detailed gizmos constantly in the scene view instead of only when the agent is selected and settings are being modified</summary>
111 public bool alwaysDrawGizmos;
112
113 /// <summary>
114 /// Slow down when not facing the target direction.
115 /// Incurs at a small performance overhead.
116 /// </summary>
117 public bool slowWhenNotFacingTarget = true;
118
119 /// <summary>
120 /// What to do when within <see cref="endReachedDistance"/> units from the destination.
121 /// The character can either stop immediately when it comes within that distance, which is useful for e.g archers
122 /// or other ranged units that want to fire on a target. Or the character can continue to try to reach the exact
123 /// destination point and come to a full stop there. This is useful if you want the character to reach the exact
124 /// point that you specified.
125 ///
126 /// Note: <see cref="reachedEndOfPath"/> will become true when the character is within <see cref="endReachedDistance"/> units from the destination
127 /// regardless of what this field is set to.
128 /// </summary>
129 public CloseToDestinationMode whenCloseToDestination = CloseToDestinationMode.Stop;
130
131 /// <summary>
132 /// Ensure that the character is always on the traversable surface of the navmesh.
133 /// When this option is enabled a <see cref="AstarPath.GetNearest"/> query will be done every frame to find the closest node that the agent can walk on
134 /// and if the agent is not inside that node, then the agent will be moved to it.
135 ///
136 /// This is especially useful together with local avoidance in order to avoid agents pushing each other into walls.
137 /// See: local-avoidance (view in online documentation for working links) for more info about this.
138 ///
139 /// This option also integrates with local avoidance so that if the agent is say forced into a wall by other agents the local avoidance
140 /// system will be informed about that wall and can take that into account.
141 ///
142 /// Enabling this has some performance impact depending on the graph type (pretty fast for grid graphs, slightly slower for navmesh/recast graphs).
143 /// If you are using a navmesh/recast graph you may want to switch to the <see cref="Pathfinding.RichAI"/> movement script which is specifically written for navmesh/recast graphs and
144 /// does this kind of clamping out of the box. In many cases it can also follow the path more smoothly around sharp bends in the path.
145 ///
146 /// It is not recommended that you use this option together with the funnel modifier on grid graphs because the funnel modifier will make the path
147 /// go very close to the border of the graph and this script has a tendency to try to cut corners a bit. This may cause it to try to go slightly outside the
148 /// traversable surface near corners and that will look bad if this option is enabled.
149 ///
150 /// Warning: This option makes no sense to use on point graphs because point graphs do not have a surface.
151 /// Enabling this option when using a point graph will lead to the agent being snapped to the closest node every frame which is likely not what you want.
152 ///
153 /// Below you can see an image where several agents using local avoidance were ordered to go to the same point in a corner.
154 /// When not constraining the agents to the graph they are easily pushed inside obstacles.
155 /// [Open online documentation to see images]
156 /// </summary>
157 public bool constrainInsideGraph = false;
158
159 /// <summary>Current path which is followed</summary>
160 protected Path path;
161
162 /// <summary>Helper which calculates points along the current path</summary>
163 protected PathInterpolator interpolator = new PathInterpolator();
164
165 #region IAstarAI implementation
166
167 /// <summary>\copydoc Pathfinding::IAstarAI::Teleport</summary>
168 public override void Teleport (Vector3 newPosition, bool clearPath = true) {
169 if (clearPath) interpolator.SetPath(null);
170 reachedEndOfPath = false;
171 base.Teleport(newPosition, clearPath);
172 }
173
174 /// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary>
175 public float remainingDistance {
176 get {
177 return interpolator.valid ? interpolator.remainingDistance + movementPlane.ToPlane(interpolator.position - position).magnitude : float.PositiveInfinity;
178 }
179 }
180
181 /// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
182 public bool reachedDestination {
183 get {
184 if (!reachedEndOfPath) return false;
185 if (remainingDistance + movementPlane.ToPlane(destination - interpolator.endPoint).magnitude > endReachedDistance) return false;
186
187 // Don't do height checks in 2D mode
188 if (orientation != OrientationMode.YAxisForward) {
189 // Check if the destination is above the head of the character or far below the feet of it
190 float yDifference;
191 movementPlane.ToPlane(destination - position, out yDifference);
192 var h = tr.localScale.y * height;
193 if (yDifference > h || yDifference < -h*0.5) return false;
194 }
195
196 return true;
197 }
198 }
199
200 /// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary>
201 public bool reachedEndOfPath { get; protected set; }
202
203 /// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
204 public bool hasPath {
205 get {
206 return interpolator.valid;
207 }
208 }
209
210 /// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
211 public bool pathPending {
212 get {
213 return waitingForPathCalculation;
214 }
215 }
216
217 /// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
218 public Vector3 steeringTarget {
219 get {
220 return interpolator.valid ? interpolator.position : position;
221 }
222 }
223
224 /// <summary>\copydoc Pathfinding::IAstarAI::radius</summary>
225 float IAstarAI.radius { get { return radius; } set { radius = value; } }
226
227 /// <summary>\copydoc Pathfinding::IAstarAI::height</summary>
228 float IAstarAI.height { get { return height; } set { height = value; } }
229
230 /// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary>
231 float IAstarAI.maxSpeed { get { return maxSpeed; } set { maxSpeed = value; } }
232
233 /// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary>
234 bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } }
235
236 /// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
237 bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } }
238
239 #endregion
240
241 protected override void OnDisable () {
242 base.OnDisable();
243
244 // Release current path so that it can be pooled
245 if (path != null) path.Release(this);
246 path = null;
247 interpolator.SetPath(null);
248 }
249
250 /// <summary>
251 /// The end of the path has been reached.
252 /// If you want custom logic for when the AI has reached it's destination add it here. You can
253 /// also create a new script which inherits from this one and override the function in that script.
254 ///
255 /// This method will be called again if a new path is calculated as the destination may have changed.
256 /// So when the agent is close to the destination this method will typically be called every <see cref="repathRate"/> seconds.
257 /// </summary>
258 public virtual void OnTargetReached () {
259 }
260
261 /// <summary>
262 /// Called when a requested path has been calculated.
263 /// A path is first requested by <see cref="UpdatePath"/>, it is then calculated, probably in the same or the next frame.
264 /// Finally it is returned to the seeker which forwards it to this function.
265 /// </summary>
266 protected override void OnPathComplete (Path newPath) {
267 ABPath p = newPath as ABPath;
268
269 if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types");
270
271 waitingForPathCalculation = false;
272
273 // Increase the reference count on the new path.
274 // This is used for object pooling to reduce allocations.
275 p.Claim(this);
276
277 // Path couldn't be calculated of some reason.
278 // More info in p.errorLog (debug string)
279 if (p.error) {
280 p.Release(this);
281 return;
282 }
283
284 // Release the previous path.
285 if (path != null) path.Release(this);
286
287 // Replace the old path
288 path = p;
289
290 // Make sure the path contains at least 2 points
291 if (path.vectorPath.Count == 1) path.vectorPath.Add(path.vectorPath[0]);
292 interpolator.SetPath(path.vectorPath);
293
294 var graph = path.path.Count > 0 ? AstarData.GetGraph(path.path[0]) as ITransformedGraph : null;
295 movementPlane = graph != null ? graph.transform : (orientation == OrientationMode.YAxisForward ? new GraphTransform(Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(-90, 270, 90), Vector3.one)) : GraphTransform.identityTransform);
296
297 // Reset some variables
298 reachedEndOfPath = false;
299
300 // Simulate movement from the point where the path was requested
301 // to where we are right now. This reduces the risk that the agent
302 // gets confused because the first point in the path is far away
303 // from the current position (possibly behind it which could cause
304 // the agent to turn around, and that looks pretty bad).
305 interpolator.MoveToLocallyClosestPoint((GetFeetPosition() + p.originalStartPoint) * 0.5f);
306 interpolator.MoveToLocallyClosestPoint(GetFeetPosition());
307
308 // Update which point we are moving towards.
309 // Note that we need to do this here because otherwise the remainingDistance field might be incorrect for 1 frame.
310 // (due to interpolator.remainingDistance being incorrect).
311 interpolator.MoveToCircleIntersection2D(position, pickNextWaypointDist, movementPlane);
312
313 var distanceToEnd = remainingDistance;
314 if (distanceToEnd <= endReachedDistance) {
315 reachedEndOfPath = true;
316 OnTargetReached();
317 }
318 }
319
320 /// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary>
321 protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
322 float currentAcceleration = maxAcceleration;
323
324 // If negative, calculate the acceleration from the max speed
325 if (currentAcceleration < 0) currentAcceleration *= -maxSpeed;
326
327 if (updatePosition) {
328 // Get our current position. We read from transform.position as few times as possible as it is relatively slow
329 // (at least compared to a local variable)
330 simulatedPosition = tr.position;
331 }
332 if (updateRotation) simulatedRotation = tr.rotation;
333
334 var currentPosition = simulatedPosition;
335
336 // Update which point we are moving towards
337 interpolator.MoveToCircleIntersection2D(currentPosition, pickNextWaypointDist, movementPlane);
338 var dir = movementPlane.ToPlane(steeringTarget - currentPosition);
339
340 // Calculate the distance to the end of the path
341 float distanceToEnd = dir.magnitude + Mathf.Max(0, interpolator.remainingDistance);
342
343 // Check if we have reached the target
344 var prevTargetReached = reachedEndOfPath;
345 reachedEndOfPath = distanceToEnd <= endReachedDistance && interpolator.valid;
346 if (!prevTargetReached && reachedEndOfPath) OnTargetReached();
347 float slowdown;
348
349 // Normalized direction of where the agent is looking
350 var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
351
352 // Check if we have a valid path to follow and some other script has not stopped the character
353 if (interpolator.valid && !isStopped) {
354 // How fast to move depending on the distance to the destination.
355 // Move slower as the character gets closer to the destination.
356 // This is always a value between 0 and 1.
357 slowdown = distanceToEnd < slowdownDistance ? Mathf.Sqrt(distanceToEnd / slowdownDistance) : 1;
358
359 if (reachedEndOfPath && whenCloseToDestination == CloseToDestinationMode.Stop) {
360 // Slow down as quickly as possible
361 velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime);
362 } else {
363 velocity2D += MovementUtilities.CalculateAccelerationToReachPoint(dir, dir.normalized*maxSpeed, velocity2D, currentAcceleration, rotationSpeed, maxSpeed, forwards) * deltaTime;
364 }
365 } else {
366 slowdown = 1;
367 // Slow down as quickly as possible
368 velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime);
369 }
370
371 velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, slowdown, slowWhenNotFacingTarget && enableRotation, forwards);
372
373 ApplyGravity(deltaTime);
374
375
376 // Set how much the agent wants to move during this frame
377 var delta2D = lastDeltaPosition = CalculateDeltaToMoveThisFrame(movementPlane.ToPlane(currentPosition), distanceToEnd, deltaTime);
378 nextPosition = currentPosition + movementPlane.ToWorld(delta2D, verticalVelocity * lastDeltaTime);
379 CalculateNextRotation(slowdown, out nextRotation);
380 }
381
382 protected virtual void CalculateNextRotation (float slowdown, out Quaternion nextRotation) {
383 if (lastDeltaTime > 0.00001f && enableRotation) {
384 Vector2 desiredRotationDirection;
385 desiredRotationDirection = velocity2D;
386
387 // Rotate towards the direction we are moving in.
388 // Don't rotate when we are very close to the target.
389 var currentRotationSpeed = rotationSpeed * Mathf.Max(0, (slowdown - 0.3f) / 0.7f);
390 nextRotation = SimulateRotationTowards(desiredRotationDirection, currentRotationSpeed * lastDeltaTime);
391 } else {
392 // TODO: simulatedRotation
393 nextRotation = rotation;
394 }
395 }
396
397 static NNConstraint cachedNNConstraint = NNConstraint.Default;
398 protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
399 if (constrainInsideGraph) {
400 cachedNNConstraint.tags = seeker.traversableTags;
401 cachedNNConstraint.graphMask = seeker.graphMask;
402 cachedNNConstraint.distanceXZ = true;
403 var clampedPosition = AstarPath.active.GetNearest(position, cachedNNConstraint).position;
404
405 // We cannot simply check for equality because some precision may be lost
406 // if any coordinate transformations are used.
407 var difference = movementPlane.ToPlane(clampedPosition - position);
408 float sqrDifference = difference.sqrMagnitude;
409 if (sqrDifference > 0.001f*0.001f) {
410 // The agent was outside the navmesh. Remove that component of the velocity
411 // so that the velocity only goes along the direction of the wall, not into it
412 velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference;
413
414 positionChanged = true;
415 // Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate
416 return position + movementPlane.ToWorld(difference);
417 }
418 }
419
420 positionChanged = false;
421 return position;
422 }
423
424 #if UNITY_EDITOR
425 [System.NonSerialized]
426 int gizmoHash = 0;
427
428 [System.NonSerialized]
429 float lastChangedTime = float.NegativeInfinity;
430
431 protected static readonly Color GizmoColor = new Color(46.0f/255, 104.0f/255, 201.0f/255);
432
433 protected override void OnDrawGizmos () {
434 base.OnDrawGizmos();
435 if (alwaysDrawGizmos) OnDrawGizmosInternal();
436 }
437
438 protected override void OnDrawGizmosSelected () {
439 base.OnDrawGizmosSelected();
440 if (!alwaysDrawGizmos) OnDrawGizmosInternal();
441 }
442
443 void OnDrawGizmosInternal () {
444 var newGizmoHash = pickNextWaypointDist.GetHashCode() ^ slowdownDistance.GetHashCode() ^ endReachedDistance.GetHashCode();
445
446 if (newGizmoHash != gizmoHash && gizmoHash != 0) lastChangedTime = Time.realtimeSinceStartup;
447 gizmoHash = newGizmoHash;
448 float alpha = alwaysDrawGizmos ? 1 : Mathf.SmoothStep(1, 0, (Time.realtimeSinceStartup - lastChangedTime - 5f)/0.5f) * (UnityEditor.Selection.gameObjects.Length == 1 ? 1 : 0);
449
450 if (alpha > 0) {
451 // Make sure the scene view is repainted while the gizmos are visible
452 if (!alwaysDrawGizmos) UnityEditor.SceneView.RepaintAll();
453 Draw.Gizmos.Line(position, steeringTarget, GizmoColor * new Color(1, 1, 1, alpha));
454 Gizmos.matrix = Matrix4x4.TRS(position, transform.rotation * (orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity), Vector3.one);
455 Draw.Gizmos.CircleXZ(Vector3.zero, pickNextWaypointDist, GizmoColor * new Color(1, 1, 1, alpha));
456 Draw.Gizmos.CircleXZ(Vector3.zero, slowdownDistance, Color.Lerp(GizmoColor, Color.red, 0.5f) * new Color(1, 1, 1, alpha));
457 Draw.Gizmos.CircleXZ(Vector3.zero, endReachedDistance, Color.Lerp(GizmoColor, Color.red, 0.8f) * new Color(1, 1, 1, alpha));
458 }
459 }
460 #endif
461
462 protected override int OnUpgradeSerializedData (int version, bool unityThread) {
463 base.OnUpgradeSerializedData(version, unityThread);
464 // Approximately convert from a damping value to a degrees per second value.
465 if (version < 1) rotationSpeed *= 90;
466 return 2;
467 }
468 }
469}