View difference between Paste ID: tAsRFKyF and enHVbev4
SHOW: | | - or go back to the newest paste.
1-
//#define ASTARDEBUG
1+
2
using System.Collections;
3
using System.Collections.Generic;
4
using Pathfinding;
5
6-
using Pathfinding.RVO;
6+
/** Simple movement script.
7
 * This movement script will follow the path exactly, it uses linear interpolation to move between the waypoints in the path.
8-
/** AI for 2D top down games.
8+
 * This is desirable for some types of games.
9-
 * This is one of the default movement scripts which comes with the A* Pathfinding Project.
9+
 * It also works in 2D.
10-
 * 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
10+
11-
 * to set up movement for the characters in your game. 
11+
 * For nicer movement, I recommend adding the Simple Smooth Modifier to the GameObject as well.
12-
 * \n
12+
13-
 * This script will try to follow a target transform, and at regular intervals the path to that target will be recalculated.
13+
14
 */
15-
 * \section variables Quick overview of the variables
15+
16-
 * 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.\n
16+
[AddComponentMenu("Pathfinding/AI/AISimpleLerp (2D,3D generic)")]
17-
 * The #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.\n
17+
public class AISimpleLerp : MonoBehaviour
18-
 * The #target variable 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.
18+
{
19-
 * Or it can be the player object in a zombie game.\n
19+
20-
 * The speed is self-explanatory, so is turningSpeed, however #slowdownDistance might require some explanation.
20+
	/** Determines how often it will search for new paths. 
21-
 * It is the approximate distance from the target where the AI will start to slow down. Note that this doesn't only affect the end point of the path
21+
	 * If you have fast moving targets or AIs, you might want to set it to a lower value.
22-
 * but also any intermediate points, so be sure to set #forwardLook and #pickNextWaypointDist to a higher value than this.\n
22+
	 * The value is in seconds between path requests.
23-
 * #pickNextWaypointDist is simply determines within what range it will switch to target the next waypoint in the path.\n
23+
24-
 * #forwardLook will try to calculate an interpolated target point on the current segment in the path so that it has a distance of #forwardLook from the AI\n
24+
	public float repathRate = 0.5F;
25-
 * Below is an image illustrating several variables as well as some internal ones, but which are relevant for understanding how it works.
25+
26-
 * Note that the #forwardLook range will not match up exactly with the target point practically, even though that's the goal.
26+
	/** Target to move towards.
27-
 * \shadowimage{aipath_variables.png}
27+
	 * The AI will try to follow/move towards this target.
28-
 * This script can use a Rigidbody2D for movement or it can simply use transform.position.
28+
	 * It can be a point on the ground where the player has clicked in an RTS for example, or it can be the player object in a zombie game.
29-
 * If it finds a Rigidbody2D attached to the same GameObject, it will start to use it automatically.
29+
30
	public Transform target;
31
	
32
	/** Enables or disables searching for paths.
33
	 * Setting this to false does not stop any active path requests from being calculated or stop it from continuing to follow the current path.
34-
[AddComponentMenu("Pathfinding/AI/AIPath (2D top down)")]
34+
	 * \see #canMove
35-
public class AIPath2DTopDown : AIPathBase {
35+
36
	public bool canSearch = true;
37-
	/** Cached Rigidbody component */
37+
38-
	protected Rigidbody2D rigid;
38+
	/** Enables or disables movement.
39
	  * \see #canSearch */
40
	public bool canMove = true;
41
	
42
	/** Speed in world units */
43-
	protected override void Awake () {
43+
	public float speed = 3;
44-
		base.Awake ();
44+
45
	/** If true, the AI will rotate to face the movement direction */
46-
		//Cache some other components (not all are necessarily there)
46+
	public bool enableRotation = true;
47-
		rigid = GetComponent<Rigidbody2D>();
47+
48
	/** If true, rotation will only be done along the Z axis */
49
	public bool rotationIn2D = false;
50-
	public override Vector3 GetFeetPosition () {
50+
51
	/** How quickly to rotate */
52
	public float rotationSpeed = 10;
53
54-
	public virtual void Update () {
54+
	/** If true, some interpolation will be done when a new path has been calculated.
55
	 * This is used to avoid short distance teleportation.
56-
		if (!canMove) { return; }
56+
57
	public bool interpolatePathSwitches = true;
58-
		Vector2 dir = CalculateVelocity (GetFeetPosition());
58+
59
	/** How quickly to interpolate to the new path */
60-
		//Rotate towards targetDirection (filled in by CalculateVelocity)
60+
	public float switchPathInterpolationSpeed = 5;
61-
		RotateTowards (targetDirection);
61+
62
	/** Cached Seeker component */
63-
		if (rigid != null) {
63+
	protected Seeker seeker;
64-
			//rigid.AddForce (dir, ForceMode2D.Impulse);
64+
65-
			rigid.MovePosition (rigid.position + dir*Time.deltaTime);
65+
	/** Cached Transform component */
66
	protected Transform tr;
67-
			transform.Translate (dir*Time.deltaTime, Space.World);
67+
68
	/** Time when the last path request was sent */
69
	protected float lastRepath = -9999;
70
	
71-
	/** Point to where the AI is heading.
71+
	/** Current path which is followed */
72-
	  * Filled in by #CalculateVelocity */
72+
	protected ABPath path;
73-
	protected Vector2 targetPoint;
73+
74-
	/** Relative direction to where the AI is heading.
74+
	/** Current index in the path which is current target */
75-
	 * Filled in by #CalculateVelocity */
75+
	protected int currentWaypointIndex = 0;
76-
	protected Vector2 targetDirection;
76+
77
	/** How far the AI has moved on the current segment */
78-
	/** Calculates desired velocity.
78+
	protected float lerpTime = 0;
79-
	 * Finds the target path segment and returns the forward direction, scaled with speed.
79+
80-
	 * A whole bunch of restrictions on the velocity is applied to make sure it doesn't overshoot, does not look too far ahead,
80+
	/** Holds if the end-of-path is reached
81-
	 * and slows down when close to the target.
81+
	 * \see TargetReached */
82-
	 * /see speed
82+
	protected bool targetReached = false;
83-
	 * /see endReachedDistance
83+
84-
	 * /see slowdownDistance
84+
	/** Only when the previous path has been returned should be search for a new path */
85-
	 * /see CalculateTargetPoint
85+
	protected bool canSearchAgain = true;
86-
	 * /see targetPoint
86+
87-
	 * /see targetDirection
87+
	/** When a new path was returned, the AI was moving along this ray.
88-
	 * /see currentWaypointIndex
88+
	 * Used to smoothly interpolate between the previous movement and the movement along the new path.
89
	 * The speed is equal to movement direction.
90-
	protected Vector2 CalculateVelocity (Vector2 currentPosition) {
90+
91-
		if (path == null || path.vectorPath == null || path.vectorPath.Count == 0) return Vector3.zero; 
91+
	protected Vector3 previousMovementOrigin;
92
	protected Vector3 previousMovementDirection;
93
	protected float previousMovementStartTime = -9999;
94
95
	/** Returns if the end-of-path has been reached
96
	 * \see targetReached */
97-
		if (vPath.Count == 1) {
97+
	public bool TargetReached {
98-
			vPath.Insert (0,currentPosition);
98+
		get {
99
			return targetReached;
100
		}
101
	}
102
	
103
	/** Holds if the Start function has been run.
104-
		// Possibly pick the next segment
104+
	 * Used to test if coroutines should be started in OnEnable to prevent calculating paths
105
	 * in the awake stage (or rather before start on frame 0).
106-
			if (currentWaypointIndex < vPath.Count-1) {
106+
107-
				//There is a "next path segment"
107+
	private bool startHasRun = false;
108-
				Vector2 nextWaypointDir = (Vector2)vPath[currentWaypointIndex] - currentPosition;
108+
109-
				float dist = nextWaypointDir.sqrMagnitude;
109+
110-
					//Mathfx.DistancePointSegmentStrict (vPath[currentWaypointIndex+1],vPath[currentWaypointIndex+2],currentPosition);
110+
111-
				if (dist < pickNextWaypointDist*pickNextWaypointDist) {
111+
112-
					lastFoundWaypointPosition = currentPosition;
112+
	protected virtual void Awake () {
113-
					lastFoundWaypointTime = Time.time;
113+
		seeker = GetComponent<Seeker>();
114-
					currentWaypointIndex++;
114+
115
		//This is a simple optimization, cache the transform component lookup
116
		tr = transform;
117
	}
118-
			} else {
118+
119-
				break;
119+
	/** Starts searching for paths.
120
	 * If you override this function you should in most cases call base.Start () at the start of it.
121
	 * \see OnEnable
122
	 * \see RepeatTrySearchPath
123-
		// Calculate the point we should move towards
123+
124-
		Vector2 targetPosition = CalculateTargetPoint (currentPosition,vPath[currentWaypointIndex-1] , vPath[currentWaypointIndex]);
124+
	protected virtual void Start () {
125
		startHasRun = true;
126-
		// Vector to the target position
126+
		OnEnable ();
127-
		Vector2 dir = targetPosition-currentPosition;
127+
128-
		float targetDist = dir.magnitude;
128+
129
	/** Run at start and when reenabled.
130-
		float slowdown = Mathf.Clamp01 (targetDist / slowdownDistance);
130+
	 * Starts RepeatTrySearchPath.
131
	 * 
132-
		this.targetDirection = dir;
132+
	 * \see Start
133-
		this.targetPoint = targetPosition;
133+
134
	protected virtual void OnEnable () {
135-
		if (currentWaypointIndex == vPath.Count-1 && targetDist <= endReachedDistance) {
135+
136-
			if (!targetReached) { targetReached = true; OnTargetReached (); }
136+
		lastRepath = -9999;
137
		canSearchAgain = true;
138-
			//Send a move request, this ensures gravity is applied
138+
139-
			return Vector3.zero;
139+
		if (startHasRun) {
140
			//Make sure we receive callbacks when paths complete
141
			seeker.pathCallback += OnPathComplete;
142-
		// The Y axis is forward in 2D space
142+
143-
		Vector2 forward = -tr.up;
143+
			StartCoroutine (RepeatTrySearchPath ());
144-
		float dot = Vector2.Dot (dir.normalized,forward);
144+
145-
		float sp = speed * Mathf.Max (dot,minMoveScale) * slowdown;
145+
146
	
147-
#if ASTARDEBUG
147+
	public void OnDisable () {
148-
		Debug.DrawLine (vPath[currentWaypointIndex-1] , vPath[currentWaypointIndex],Color.black);
148+
		// Abort calculation of path
149-
		Debug.DrawLine (GetFeetPosition(),targetPosition,Color.red);
149+
		if (seeker != null && !seeker.IsDone()) seeker.GetCurrentPath().Error();
150-
		Debug.DrawRay (targetPosition,Vector3.up, Color.red);
150+
151-
		Debug.DrawRay (GetFeetPosition(),dir,Color.yellow);
151+
		// Release current path
152-
		Debug.DrawRay (GetFeetPosition(),forward*sp,Color.cyan);
152+
		if (path != null) path.Release (this);
153-
#endif
153+
		path = null;
154
		
155-
		// Make sure we don't overshoot the target
155+
		//Make sure we receive callbacks when paths complete
156-
		// when the framerate is low
156+
		seeker.pathCallback -= OnPathComplete;
157-
		if (Time.deltaTime	> 0) {
157+
158-
			sp = Mathf.Clamp (sp,0,targetDist/(Time.deltaTime*2));
158+
159
	/** Tries to search for a path every #repathRate seconds.
160-
		return forward*sp;
160+
	  * \see TrySearchPath
161
	  */
162
	protected IEnumerator RepeatTrySearchPath () {
163-
	/** Rotates in the specified direction.
163+
164-
	 * Rotates around the Y-axis.
164+
			float v = TrySearchPath ();
165-
	 * \see turningSpeed
165+
			yield return new WaitForSeconds (v);
166
		}
167-
	protected void RotateTowards (Vector2 dir) {
167+
168
	
169-
		if (dir == Vector2.zero) return;
169+
	/** Tries to search for a path.
170
	 * Will search for a new path if there was a sufficient time since the last repath and both
171-
		// Figure out the angle so that the Y axis faces dir
171+
	 * #canSearchAgain and #canSearch are true and there is a target.
172-
		float angle = Mathf.Atan2 (dir.x, -dir.y)*Mathf.Rad2Deg;
172+
	 * 
173
	 * \returns The time to wait until calling this function again (based on #repathRate) 
174-
		Vector3 rot = tr.eulerAngles;
174+
175-
		rot.z = Mathf.LerpAngle (rot.z, angle, turningSpeed * Time.deltaTime);
175+
	public float TrySearchPath () {
176-
		tr.eulerAngles = rot;
176+
		if (Time.time - lastRepath >= repathRate && canSearchAgain && canSearch && target != null) {
177
			SearchPath ();
178
			return repathRate;
179-
	/** Calculates target point from the current line segment.
179+
180-
	 * \param p Current position
180+
			float v = repathRate - (Time.time-lastRepath);
181-
	 * \param a Line segment start
181+
			return v < 0 ? 0 : v;
182-
	 * \param b Line segment end
182+
183-
	 * The returned point will lie somewhere on the line segment.
183+
184-
	 * \see #forwardLook
184+
185-
	 * \todo This function uses .magnitude quite a lot, can it be optimized?
185+
	/** Requests a path to the target.
186
	 * Some inheriting classes will prevent the path from being requested immediately when
187-
	protected Vector2 CalculateTargetPoint (Vector2 p, Vector2 a, Vector2 b) {
187+
	 * this function is called, for example when the AI is currently traversing a special path segment
188
	 * in which case it is usually a bad idea to search for a new path.
189-
		float magn = (a-b).magnitude;
189+
	  */
190-
		if (magn == 0) return a;
190+
	public virtual void SearchPath () {
191
		ForceSearchPath ();
192-
		float closest = AstarMath.Clamp01 (AstarMath.NearestPointFactor (a, b, p));
192+
193-
		Vector2 point = (b-a)*closest + a;
193+
194-
		float distance = (point-p).magnitude;
194+
	/** Requests a path to the target.
195
	 * Bypasses 'is-it-a-good-time-to-request-a-path' checks.
196-
		float lookAhead = Mathf.Clamp (forwardLook - distance, 0.0F, forwardLook);
196+
	  */
197
	public virtual void ForceSearchPath () {
198-
		float offset = lookAhead / magn;
198+
		if (target == null) throw new System.InvalidOperationException ("Target is null");
199-
		offset = Mathf.Clamp (offset+closest,0.0F,1.0F);
199+
200-
		return (b-a)*offset + a;
200+
		lastRepath = Time.time;
201
		//This is where we should search to
202
		Vector3 targetPosition = target.position;
203
		
204
		canSearchAgain = false;
205
		
206
		//Alternative way of requesting the path
207
		//ABPath p = ABPath.Construct (GetFeetPosition(),targetPoint,null);
208
		//seeker.StartPath (p);
209
		
210
		//We should search from the current position
211
		seeker.StartPath (GetFeetPosition(), targetPosition);
212
	}
213
214
	/** The end of the path has been reached.
215
	  * If you want custom logic for when the AI has reached it's destination
216
	  * add it here
217
	  * You can also create a new script which inherits from this one
218
	  * and override the function in that script.
219
	  */
220
	public virtual void OnTargetReached () {
221
222
	}
223
	
224
	/** Called when a requested path has finished calculation.
225
	  * A path is first requested by #SearchPath, it is then calculated, probably in the same or the next frame.
226
	  * Finally it is returned to the seeker which forwards it to this function.\n
227
	  */
228
	public virtual void OnPathComplete (Path _p) {
229
		ABPath p = _p as ABPath;
230
		if (p == null) throw new System.Exception ("This function only handles ABPaths, do not use special path types");
231
		
232
		canSearchAgain = true;
233
		
234
		//Claim the new path
235
		p.Claim (this);
236
		
237
		// Path couldn't be calculated of some reason.
238
		// More info in p.errorLog (debug string)
239
		if (p.error) {
240
			p.Release (this);
241
			return;
242
		}
243
244
		if (interpolatePathSwitches) {
245
			ConfigurePathSwitchInterpolation ();
246
		}
247
248
		//Release the previous path
249
		if (path != null) path.Release (this);
250
		
251
		//Replace the old path
252
		path = p;
253
254
		// Just for the rest of the code to work, if there is only one waypoint in the path
255
		// add another one
256
		if (path.vectorPath != null && path.vectorPath.Count == 1) {
257
			path.vectorPath.Insert (0,GetFeetPosition());
258
		}
259
260
		targetReached = false;
261
262
		//Reset some variables
263
		ConfigureNewPath ();
264
	}
265
266
	protected virtual void ConfigurePathSwitchInterpolation () {
267
		if (path != null && path.vectorPath != null && path.vectorPath.Count > 1) {
268
			
269
			List<Vector3> vPath = path.vectorPath;
270
			
271
			// Make sure we stay inside valid ranges
272
			currentWaypointIndex = Mathf.Clamp (currentWaypointIndex, 1, vPath.Count - 1);
273
			
274
			// Current segment vector
275
			Vector3 segment = vPath [currentWaypointIndex] - vPath [currentWaypointIndex - 1];
276
			float segmentLength = segment.magnitude;
277
			
278
			// Find the approximate length of the path that is left on the current path
279
			float approximateLengthLeft = segmentLength * Mathf.Clamp01 (1 - lerpTime);
280
			for (int i = currentWaypointIndex; i < vPath.Count-1; i++) {
281
				approximateLengthLeft += (vPath [i + 1] - vPath [i]).magnitude;
282
			}
283
			
284
			previousMovementOrigin = GetFeetPosition ();
285
			previousMovementDirection = segment.normalized * approximateLengthLeft;
286
			previousMovementStartTime = Time.time;
287
		} else {
288
			previousMovementOrigin = Vector3.zero;
289
			previousMovementDirection = Vector3.zero;
290
			previousMovementStartTime = -9999;
291
		}
292
	}
293
294
	public virtual Vector3 GetFeetPosition () {
295
		return tr.position;
296
	}
297
298
	/** Finds the closest point on the current path.
299
	 * Sets #currentWaypointIndex and #lerpTime to the appropriate values.
300
	 */
301
	protected virtual void ConfigureNewPath () {
302
		var points = path.vectorPath;
303
304
		var currentPosition = GetFeetPosition ();
305
306
		float bestFactor = 0;
307
		float bestDist = float.PositiveInfinity;
308
		int bestIndex = 0;
309
310
		for (int i = 0; i < points.Count-1; i++) {
311
			float factor = AstarMath.NearestPointFactor (points[i], points[i+1], currentPosition);
312
			Vector3 point = Vector3.Lerp (points[i], points[i+1], factor);
313
			float dist = (currentPosition - point).sqrMagnitude;
314
315
			if (dist < bestDist) {
316
				bestDist = dist;
317
				bestFactor = factor;
318
				bestIndex = i+1;
319
			}
320
		}
321
322
		currentWaypointIndex = bestIndex;
323
		lerpTime = bestFactor;
324
	}
325
326
	protected virtual void Update () {
327
		if (canMove) {
328
			Vector3 direction;
329
			Vector3 nextPos = CalculateNextPosition (out direction);
330
331
			// Rotate unless we are really close to the target
332
			if (enableRotation && direction != Vector3.zero) {
333
334
				if (rotationIn2D) {
335
336
					float angle = Mathf.Atan2(-direction.x, direction.y) * Mathf.Rad2Deg + 180;
337
					Vector3 euler = tr.eulerAngles;
338
					euler.z = Mathf.LerpAngle (euler.z, angle, Time.deltaTime * rotationSpeed);
339
					tr.eulerAngles = euler;
340
				} else {
341
342
					Quaternion rot = tr.rotation;
343
					Quaternion desiredRot = Quaternion.LookRotation (direction);
344
345
					tr.rotation = Quaternion.Slerp (rot, desiredRot, Time.deltaTime * rotationSpeed);
346
				}
347
			}
348
349
			tr.position = nextPos;
350
		}
351
	}
352
353
	/** Calculate the AI's next position (one frame in the future).
354
	 * \param direction The direction of the segment the AI is currently traversing. Not normalized.
355
	 */
356
	protected virtual Vector3 CalculateNextPosition ( out Vector3 direction ) {
357
358
		if (path == null || path.vectorPath == null || path.vectorPath.Count == 0) {
359
			direction = Vector3.zero;
360
			return Vector3.zero; 
361
		}
362
363
		List<Vector3> vPath = path.vectorPath;
364
365
		// Make sure we stay inside valid ranges
366
		currentWaypointIndex = Mathf.Clamp (currentWaypointIndex, 1, vPath.Count - 1);
367
368
		// Current segment vector
369
		Vector3 segment = vPath[currentWaypointIndex] - vPath[currentWaypointIndex - 1];
370
		float segmentLength = segment.magnitude;
371
372
		if (segmentLength > 0) {
373
			// Move forwards
374
			// lerpTime is between 0 and 1
375
			lerpTime += Time.deltaTime * speed / segmentLength;
376
		} else {
377
			// Make sure zero length segments are handled correctly
378
			lerpTime = 1;
379
		}
380
381
		// Pick the next segment if we have traversed the current one completely
382
		if (lerpTime > 1 && currentWaypointIndex < vPath.Count-1) {
383
			float overshootDistance = (lerpTime-1) * segmentLength;
384
385
			while (true) {
386
				currentWaypointIndex++;
387
388
				// Next segment vector
389
				Vector3 nextSegment = vPath[currentWaypointIndex] - vPath[currentWaypointIndex - 1];
390
				float nextSegmentLength = segment.magnitude;
391
392
				if (overshootDistance <= nextSegmentLength || currentWaypointIndex == vPath.Count-1) {
393
					segment = nextSegment;
394
					segmentLength = nextSegmentLength;
395
					lerpTime = Mathf.Clamp01 (overshootDistance/nextSegmentLength);
396
					break;
397
				} else {
398
					overshootDistance -= nextSegmentLength;
399
				}
400
			}
401
		}
402
403
		if (lerpTime >= 1 && currentWaypointIndex == vPath.Count - 1) {
404
			if (!targetReached) {
405
				OnTargetReached ();
406
			}
407
			targetReached = true;
408
		}
409
410
		// Find our position along the path using a simple linear interpolation
411
		Vector3 positionAlongCurrentPath = segment * Mathf.Clamp01 (lerpTime) + vPath [currentWaypointIndex - 1];
412
413
		direction = segment;
414
415
		if (interpolatePathSwitches) {
416
			// Find the approximate position we would be at if we
417
			// would have continued to follow the previous path
418
			Vector3 positionAlongPreviousPath = previousMovementOrigin + Vector3.ClampMagnitude (previousMovementDirection, speed * (Time.time - previousMovementStartTime));
419
420
			// Use this to debug
421
			//Debug.DrawLine (previousMovementOrigin, positionAlongPreviousPath, Color.yellow);
422
423
424
			return Vector3.Lerp (positionAlongPreviousPath, positionAlongCurrentPath, switchPathInterpolationSpeed * (Time.time - previousMovementStartTime));
425
		} else {
426
			return positionAlongCurrentPath;
427
		}
428
	}
429
}