There are times when we need scripted routes, and it's just inconceivable to do this entirely by code. Imagine you're working on a stealth game. Would you code a route for every single guard? This technique will help you build a flexible path system for those situations:
We need to define a custom data type called PathSegment
:
using UnityEngine; using System.Collections; public class PathSegment { public Vector3 a; public Vector3 b; public PathSegment () : this (Vector3.zero, Vector3.zero){} public PathSegment (Vector3 a, Vector3 b) { this.a = a; this.b = b; } }
This is a long recipe that could be seen as a big two-step process. First, we build the Path
class, which abstracts points in the path from their specific spatial representations, and then we build the PathFollower
behavior, which makes use of that abstraction in order to get actual spatial points to follow:
Path
class, which consists of nodes and segments but only the nodes are public and assigned manually:using UnityEngine; using System.Collections; using System.Collections.Generic; public class Path : MonoBehaviour { public List<GameObject> nodes; List<PathSegment> segments; }
Start
function to set the segments when the scene starts:void Start() { segments = GetSegments(); }
GetSegments
function to build the segments from the nodes:public List<PathSegment> GetSegments () { List<PathSegment> segments = new List<PathSegment>(); int i; for (i = 0; i < nodes.Count - 1; i++) { Vector3 src = nodes[i].transform.position; Vector3 dst = nodes[i+1].transform.position; PathSegment segment = new PathSegment(src, dst); segments.Add(segment); } return segments; }
GetParam
:public float GetParam(Vector3 position, float lastParam) { // body }
float param = 0f; PathSegment currentSegment = null; float tempParam = 0f; foreach (PathSegment ps in segments) { tempParam += Vector3.Distance(ps.a, ps.b); if (lastParam <= tempParam) { currentSegment = ps; break; } } if (currentSegment == null) return 0f;
Vector3 currPos = position - currentSegment.a; Vector3 segmentDirection = currentSegment.b - currentSegment.a; segmentDirection.Normalize();
Vector3 pointInSegment = Vector3.Project(currPos, segmentDirection);
GetParam
returns the next position to go to along the path:param = tempParam - Vector3.Distance(currentSegment.a, currentSegment.b); param += pointInSegment.magnitude; return param;
GetPosition
function:public Vector3 GetPosition(float param) { // body }
Vector3 position = Vector3.zero; PathSegment currentSegment = null; float tempParam = 0f; foreach (PathSegment ps in segments) { tempParam += Vector3.Distance(ps.a, ps.b); if (param <= tempParam) { currentSegment = ps; break; } } if (currentSegment == null) return Vector3.zero;
GetPosition
converts the parameter as a spatial point and returns it:Vector3 segmentDirection = currentSegment.b - currentSegment.a; segmentDirection.Normalize(); tempParam -= Vector3.Distance(currentSegment.a, currentSegment.b); tempParam = param - tempParam; position = currentSegment.a + segmentDirection * tempParam; return position;
PathFollower
behavior, which derives from Seek
(remember to set the order of execution):using UnityEngine;
using System.Collections;
public class PathFollower : Seek
{
public Path path;
public float pathOffset = 0.0f;
float currentParam;
}
Awake
function to set the target:public override void Awake() { base.Awake(); target = new GameObject(); currentParam = 0f; }
GetSteering
function, which relies on the abstraction created by the Path
class to set the target position and apply Seek
:public override Steering GetSteering() { currentParam = path.GetParam(transform.position, currentParam); float targetParam = currentParam + pathOffset; target.transform.position = path.GetPosition(targetParam); return base.GetSteering(); }
We use the Path
class in order to have a movement guideline. It is the cornerstone, because it relies on GetParam
to map an offset point to follow in its internal guideline, and it also uses GetPosition
to convert that referential point to a position in the three-dimensional space along the segments.
The path-following algorithm just makes use of the path's functions in order to get a new position, update the target, and apply the Seek
behavior.
It's important to take into account the order in which the nodes are linked in the Inspector for the path to work as expected. A practical way to achieve this is to manually name the nodes with a reference number.
Also, we could define the OnDrawGizmos
function in order to have a better visual reference of the path:
void OnDrawGizmos () { Vector3 direction; Color tmp = Gizmos.color; Gizmos.color = Color.magenta;//example color int i; for (i = 0; i < nodes.Count - 1; i++) { Vector3 src = nodes[i].transform.position; Vector3 dst = nodes[i+1].transform.position; direction = dst - src; Gizmos.DrawRay(src, direction); } Gizmos.color = tmp; }