In this post I will describe my simple system for moving platforms and other objects in Unity. What is great is, that it is simple, flexible and easily extensible.
Here are see some results achieved with it:
- moves between two nodes in "ping-pong" way,
- stops little in upper node for defined time,
- falls fast with some gravity-like effect ,
- waits very little time,
- slowly returns up
- moves in "loop" way,
- stops at first node,
- moves in gravity way from second to third node,
- moves in "cos" way from third to fourth node
1. Timer class
First we will create small helper class called Timer. If you often need to track time in your Update method you are probably used to create float variable holding some amount of time and adjust it with Time.delta or Time.smoothDeltaTime. Doing this again and again is boring so we will create class that can:
- hold current time,
- increase time from zero to specified time or decrease time from initial time to zero,
- return current time,
- return current time as percent from initial or maximum time,
- say if timer expired
Here is listing for Timer.cs:
To use this class create Timer object and first call Set on it. You can call it like this:
- Set(5.0f); ... timer will start with 0 and increase until it reaches 5 seconds,
- Set(5.0f, false); ... timer will has its initial value 5 seconds and will decrease towards zero
2. Path node, path definition and path follower
Now we can focus on real subject. Our path following system will have three parts:
- PathNode - class that defines single node in path,
- PathDefinition - is object managing two or more path nodes. As having only two nodes is very common we will make PathDefinition class abstract and build two implementations: SimplePathDefinition for two nodes only and LongPathDefinition for unlimited number of nodes. In fact, LongPathDefinition can also handle two nodes without problem. But the main reason is that SimplePathDefinition component has easier input interface for user (two fields for node 1 and 2 instead of nodes array),
- PathFollower - is object that will hold PathDefinition and will move object from node to node. This is script is then added in component view to platform or log or any other object that shell follow the path.
2.1. PathNode class
Path node is single node the path consists of. The position of node is given with its Transform component. Beside position the node holds other information:
- delay in seconds - how long the moving object will pause its move in this node,
- movement type - type of movement from this node to next. Supported moves are: linear, cos, quadratic,
- speed modifier - is in % and affects speed of moving object for move from current node to next one. For example, if set to 0,5 (50%) then distance to next move is overcome in half of the time. So, numbers below zero means faster move.
- OnArriveEvent, OnLeaveEvent - two events fired when leaving current node and when arrived to next node.
----- wait (delay) ---- trigger OnLeaveEvent ----- move to next node ----- trigger OnArriveEvent -----
For future improvements it would be better to bind the OnArriveEvent event to node that defines it and trigger it as first when object reaches node like this:
----- trigger OnArriveEvent ----- wait (delay) ----- trigger OnLeaveEvent ----- move to next node -----
Currently events are implemented only as some enum values. Update: read next part of this tutorial here - enum values are replaced with event objects that can be chained to create series of events, have delays and are easily extensible (also source download is available there).
Here is listing for core of PathNode class:
Using enums is convenient for users as they can select from it in editor then. Here is picture how the component looks like in editor. Notice that PathNode has no visual representation. It is just point in space with some additional parameters. So to visualise it add some visual tag to it (see small yellow square in top left of image):
Rest of the class are only get properties, but to make it complete here is the listing:
2.2 PathDefinition class
Path definition will manage PathNodes and it will be abstract class. The abstaract method will be method:
public abstract IEnumerator<PathNode> GetPathEnumerator();
This method is:
- abstract, so some other class will have to define implementation for this method,
- returns IEnumerator, which means that implementation will manage set of PathNodes and on request it will return correct PathNode or report that there are no more nodes.
Notice the ePathRepeatType enum. This enum says what type of path it is. It can be traveled by the object only once or back and forth forever (ping-pong) or in loop when from last node the object will continue to the first node again.
As it would be very pleasant to visuailise the path between nodes, we will implment OnDrawGizmos method:
As you can see we are asking for enumerator which is provided by the abstract method. We draw debug lines between nodes until enumerator is giving us nodes. In case the enumerator is returning nodes in "loop" mode, we check if loop is closed and exit the method.
Now we can provide concrete implementation in class that will be able to hold only two PathNodes. Let's call it SimplePathDefinition:
We are not inheriting directly form MonoBehaviour here but from PathDefinition. It means in editor our parent's OnDrawGizmos will be called and only thing we have to do is provide GetPathEnumerator method. This class will be the component you can add to GameObject in editor and you will also have to give it two PathNodes:
In case of "ping-pong" or "loop" modes, the iterator never runs out of nodes. It just switches between nodes and return first or second in turn. In case of "once" move mode the iterator check if reached second node and if yes it terminates (yield break;)
Now you can go to editor and create three empty objects. Add PathNode component to two of them and add SimplePathDefinition to the last one. Then drag PathNode objects to SimplePathDefinition object as Path Node 1 and Path Node 2 and you should see line between them (if you see nothing, check if the position of PathNodes is different).
Now, we will create another class - LongPathDefinition. This class will have different GetPathEnumerator implementation and also different way how to store PathNodes. PathNodes will be stored in array and we will also add one additional feature that had no sense for only two nodes. This feature is ReverseDirection and it says whether the move should be from first to last node or from last to first node. Here stars LongPathDefinition listing:
GetPathEnumerator is only last thing we have to write:
In case of "once" move we have to set the last node as first one. In case of "ping-pong" the reversing has no sense and in case of "loop" the first node is still the first one, but we start to move to last one instead to second one if direction is reversed.
The rest is just infinite enumerator loop (except for "once", which terminates when last (first if reversed) node is reached).
As with SimplePathDefinition, you can now go to editor and create new GameObject, add LongPathDefinition script component to it and create and add several PathNodes to it:
2.3 PathFollwer class
PathFollower is last piece into mosaic. This script will be the component that you add to object you want to move. This component will hold some path definition (Simple or Long) which in turn holds set of path nodes.
This class is the longest one so I will go step by step. First, listing shows variables. The public ones are those you can set in editor. Beside PathDefinition (Simple/Long) it has general speed in Unity units. 1 = 1 Unity unit in second. Next public variable - mStopped - says whether the move is stopped.
Private variables then holds inner state of the PathFollower. If it is currently moving on waiting in actual node. It also stores concrete IEnumerator implementation. Here we also finally utilize the Timer class to simplify all timings.
Next we store current and target node and also whether the path following is finished (in case of "once" movetype).
In Start method we initialize enumerator and also we ask it for current and next node. We read delay from path node (can be 0) and set inner state into "waiting".
Here is Update method where most of the things take place:
We first check if still running and not paused. If running, we update timer - we are not interested what it is timing now - it is either moving or waiting. If moving, we adjust position of object.
Then we check if timer expired and if yes we change inner state and get next node if needed.
Now I will list explain how moving works. When two nodes are read (current and target), distance between then is calculated in Unity units. This is divided with speed in Unity units and you get time how long it should take to the object to travel from current to target node. This time is then affected by PathNode's speed modifier (0,5 = 50% ... time should be only half). this is all calculated in CalculateMoveTime method.
Timer is then set to calculated value (MoveToTargetNode). As the timer has GetActualRatio method, we can ask it in which distance between two nodes the moving object should be. As the result is between 0 and 1 it is ideal entry for LERP.
Look into AdjustPosition method and you will see that it is called with value returned from Timer.GetActualRatio. If movement type is linear we just calculate new position as Vector3.Lerp between current and target nodes positions. If movement type is quadratic, we power the ratio by 2. In case of cos we do this:
- cos (ratio * PI) => gives 1 to -1 values,
- -cos (ratio * PI) => gives -1 to 1,
- -cos (ratio * PI) + 1 => gives 0 to 2,
- (-cos (ratio * PI) + ) / 2 => gives 0 to 1 ... correct entry for lerp
And finally, here are two last methods for handling events:
These methods are only "default implementation" and you can change them in derived classes.
We are finished. You can go to editor and add new game object. Give it some appearance through SpriteRenderer, add PathFollower component to it and attach some PathDefinition. then you can pres Play and see how the object is moving along defined path!
This is end of my first Unity tutorial. I hope you made it here and now you have also this small system working.