Showing posts with label programming. Show all posts
Showing posts with label programming. Show all posts

Saturday, March 28, 2015

Unity: Two flavours of LERP





 In this post I will show two uses of LERP as you can often see in Unity scripts.


LERP


 LERP stands for linear interpolation and it is easy way how to get "mid-points" if you need to travel from point A to point B. Let's say you need to get from point A to point B in 3 seconds. We will call these 3 seconds as "MOVEMENT_TIME". You set up some counter "_currentTime" that will increase every frame with elapsed time until you reach MOVEMENT_TIME. Your current position on every frame is then calculated like this:

newPosition = point A + (point B - point A) * (_curerntTime / MOVEMENT_TIME)

 What it does is:
  • elapsed time is changed into range 0 to 1 (_curerntTime / MOVEMENT_TIME),
  • distance between ending and starting point (point B - point A) is calculated,
  • these two values are multiplied to know how much of the distance we already traveled (if time is 0 we are still in beginning and if time is 1 we are in target position),
  • finally we add the distance to starting position (point A) to get current position
 As this is used very frequently Unity has Lerp function in its Mathf class. It simplifies whole thing to single call to:

float newValue = Mathf.Lerp(valueA, valueB, time);

 But points are not single values! Fortunately, Unity has whole bunch of Lerp functions for Vector2, Vector3, Color, ... Our position change would look like this in code:

Vector2 newPosition = Vector2.Lerp(PointA, PointB, _currentTime / MOVEMENT_TIME);



Two common usages of LERP


 Now look at following picture:

 Both, ship and zeppelin are using LERP for its movement. But, it is obvious that zeppelin is not moving linearly.

 The ship is using our previous formula: we have starting point, end point and every frame we are looking for current point between them. Here is chart with our x position with regard to time:


 Here is complete code for ship:

using UnityEngine;
using System.Collections;

public class Ship : MonoBehaviour {

    public const float MOVEMENT_TIME = 3f;

    private float _startX;
    private float _endX;
    private float _currentTime;

    void Start() {
        _startX = -1f;
        _endX = 1f;
        _currentTime = 0;
    }

    void Update() {
        // update time
        _currentTime = Mathf.Min(_currentTime + Time.deltaTime, MOVEMENT_TIME);

        // update position
        Vector3 newPosition = transform.position;
        newPosition.x = Mathf.Lerp(_startX, _endX, _currentTime / MOVEMENT_TIME);
        transform.position = newPosition;

        // check if already in destination
        if (_currentTime >= MOVEMENT_TIME) {
            // move again, but switch start and destination
            float tmpX = _startX;
            _startX = _endX;
            _endX = tmpX;

            // init timer again
            _currentTime = 0;

            // turn ship to head in right direction
            Vector3 newScale = transform.localScale;
            newScale.x *= -1;
            transform.localScale = newScale;
        }
    }
}


 Zeppelin is moving also using LERP, but instead of interpolating between start and end position, it is interpolating between current and end position. It also does not calculate time between 0 and 1. Instead it uses fixed value. This fixed value says how much of the remaining distance (from current position to target position) should be traveled. As the remaining distance is getting shorter and shorter then also the calculated distance to move in every frame is shorter and shorter, so the zeppelin is slowing down. Exactly, it does never reach target position, but it gets so close to it that this is not problem for us. The chart for movement looks like this:


 And here is complete code for zeppelin:

using UnityEngine;
using System.Collections;

public class Zeppelin : MonoBehaviour {

    public const float MOVEMENT_TIME = 3f;

    private float _endX;

    void Start() {
        _endX = 1f;
        StartCoroutine(InfiniteSwap());
    }

    void Update() {
        // update position
        Vector3 newPosition = transform.position;
        newPosition.x = Mathf.Lerp(transform.position.x, _endX, MOVEMENT_TIME * 0.5f * Time.deltaTime);
        transform.position = newPosition;
    }

    IEnumerator InfiniteSwap() {
        while (true) {
            // suspend for 3 seconds
            yield return new WaitForSeconds(MOVEMENT_TIME);

            // change end position
            _endX *= -1;

            // turn zeppelin to head in right direction
            Vector3 newScale = transform.localScale;
            newScale.x *= -1;
            transform.localScale = newScale;
        }
    }
}

 We are using coroutine here to change target position every 3 seconds. In both cases we are moving form -1 to 1 in x direction.

 The second way of using Lerp has lot of usages. For example if you want your camera move from player to focus on some object, it looks much better if you do it in this way. In Unity example projects you can see this for camera movement in Survival shooter or for alarm lights intensity in Stealth project.



Friday, December 5, 2014

Unity 2D: Simple system for moving platforms (not only!)





 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using UnityEngine;

public class Timer
{
    private float mTime;
    private float mCurrentTime;
    private bool mRunning;
    private bool mReversed;

    // -------------------------------------------------------------------------
    public void Set(float aInitialTime, bool aReversed = false)
    {
        mTime = aInitialTime;
        mCurrentTime = aReversed ? aInitialTime : 0.0f;

        mReversed = aReversed;

        mRunning = (aInitialTime > 0.0f);
    }

    // -------------------------------------------------------------------------
    public float Update(float aDeltaTime)
    {
        if (!mRunning)
            return GetActual();

        if (mReversed)
            mCurrentTime -= aDeltaTime;
        else
            mCurrentTime += aDeltaTime;

        if (mCurrentTime < 0 || mCurrentTime > mTime)
        {
            mCurrentTime = Mathf.Clamp(mCurrentTime, 0.0f, mTime);
            mRunning = false;
        }

        return mCurrentTime;
    }

    // -------------------------------------------------------------------------
    public float GetActual()
    {
        return mCurrentTime;
    }

    // -------------------------------------------------------------------------
    public float GetActualRatio()
    {
        float ratio = mCurrentTime / mTime;

        if (ratio < 0.0f)
            ratio = 0.0f;
        else if (ratio > 1.0f)
            ratio = 1.0f;

        return ratio;
    }

    // -------------------------------------------------------------------------
    public bool IsRunning()
    {
        return mRunning;
    }

    // -------------------------------------------------------------------------
    public bool IsReversed()
    {
        return mReversed;
    }
}

 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
 In Update method of parent object then call Update(Time.deltaTime) on Timer object to adjust it. To check if timer is finnished simply check if IsRunning is false.



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.
 Currently the events are little bit confusing, because in OnArriveEvent node says which event will be triggered when next node is reached. The order of processing for single node is like this:

   ----- 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using UnityEngine;

public class PathNode : MonoBehaviour
{
    public enum ePathNodeEventType
    {
        Nothing,
        Hit
    }

    public enum eMovementType
    {
        Linear,
        Cos,
        Qudratic
    }

    public eMovementType mMovementType = eMovementType.Linear;

    // delay in this point in seconds
    public float mDelay = 0.0f;

    public ePathNodeEventType mOnArriveEvent;
    public ePathNodeEventType mOnLeaveEvent;

    public float mSpeedModifier = 1.0f;

 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    // -------------------------------------------------------------------------
    public float Delay
    {
        get
        {
            return mDelay;
        }
    }

    // -------------------------------------------------------------------------
    public eMovementType MovementType
    {
        get
        {
            return mMovementType;
        }
    }

    // -------------------------------------------------------------------------
    public ePathNodeEventType ArriveEvent
    {
        get
        {
            return mOnArriveEvent;
        }
    }

    // -------------------------------------------------------------------------
    public ePathNodeEventType LeaveEvent
    {
        get
        {
            return mOnLeaveEvent;
        }
    }

    // -------------------------------------------------------------------------
    public float SpeedModifier
    {
        get
        {
            return mSpeedModifier;
        }
    }
}


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.
 PathDefinition class looks simple like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public abstract class PathDefinition : MonoBehaviour
{
    public enum ePathRepeatType
    {
        Once,
        PingPong,
        Loop
    }

    public ePathRepeatType mMovementType;

    // -------------------------------------------------------------------------
    public abstract IEnumerator<PathNode> GetPathEnumerator();

 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    // -------------------------------------------------------------------------
    public void OnDrawGizmos()
    {
        IEnumerator<PathNode> nodes = GetPathEnumerator();

        if (!nodes.MoveNext())
            return;

        Gizmos.color = Color.grey;
        PathNode firstNode = nodes.Current;

        PathNode node1 = firstNode;
        PathNode node2 = null;

        while (nodes.MoveNext())
        {
            node2 = nodes.Current;

            Gizmos.DrawLine(node1.transform.position, node2.transform.position);

            if (node2 == firstNode)
                break;

            node1 = node2;
        }
    }
}

 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using UnityEngine;
using System.Collections.Generic;

public class SimplePathDefinition : PathDefinition
{
    public PathNode mPathNode1;
    public PathNode mPathNode2;

    // -------------------------------------------------------------------------
    public override IEnumerator<PathNode> GetPathEnumerator()
    {
        // check if both path nodes entered
        if (mPathNode1 == null || mPathNode2 == null)
            yield break;


        // else set pointer to current node
        PathNode currentNode = mPathNode1;

        // return points based on movement type
        // in case of simple path loop and ping pong are the same
        while (true)
        {
            yield return currentNode;

            // swap to next node
            if (currentNode == mPathNode1)
                currentNode = mPathNode2;
            else
                currentNode = mPathNode1;

            // if move only once mode and returned to node1 then exit
            if (mMovementType == ePathRepeatType.Once && currentNode == mPathNode1)
                yield break;
        }
    }
}

 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:

1
2
3
4
5
6
7
using UnityEngine;
using System.Collections.Generic;

public class LongPathDefinition : PathDefinition
{
    public bool mReverseDirection;
    public PathNode[] mPathNodes;

 GetPathEnumerator is only last thing we have to write:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    // -------------------------------------------------------------------------
    public override IEnumerator<PathNode> GetPathEnumerator()
    {
        // check if nodes array is valid entered and longer than 2 nodes (1 node = no path)
        if (mPathNodes == null || mPathNodes.Length < 2)
            yield break;
        
        // set index to start
        int currentNodeIndex = 0;
        int directionAdjustment = mReverseDirection ? -1 : 1;
        // if move type is only once and direction is reversed, start from last node
        if (mReverseDirection && mMovementType == ePathRepeatType.Once)
            currentNodeIndex = mPathNodes.Length - 1;
        
        // return points based on movement type
        // in case of simple path loop and ping pong are the same
        while (true)
        {
            yield return mPathNodes[currentNodeIndex];

            // adjust in move direction
            currentNodeIndex += directionAdjustment;

            if (currentNodeIndex < 0 || currentNodeIndex >= mPathNodes.Length)
            {
                if (mMovementType == ePathRepeatType.Once)
                {
                    yield break;
                }
                else if (mMovementType == ePathRepeatType.PingPong)
                {
                    directionAdjustment *= -1;
                    // adjust twice - first return to current node, second move in new direction
                    currentNodeIndex += directionAdjustment * 2;
                }
                else if (mMovementType == ePathRepeatType.Loop)
                {
                    if (currentNodeIndex < 0)
                        currentNodeIndex = mPathNodes.Length - 1;
                    else
                        currentNodeIndex = 0;
                }
            }
        }
    }
}

 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;
using System.Collections.Generic;

public class PathFollower : MonoBehaviour
{
    // path definition
    public PathDefinition mPathDefinition;
    // speed in units per second
    public float mSpeed = 1.0f;
    // stopped or in action?
    public bool mStopped = false;

    private enum ePathFollowerState
    {
        Waiting,
        Moving
    }

    // enumerator
    private IEnumerator<PathNode> mPathNodes;

    // inner state of follower
    private ePathFollowerState mState = ePathFollowerState.Waiting;
    // inner waiting time
    private Timer mTimer = new Timer();

    // curren path node, object is in (or going from)
    private PathNode mCurrentPathNode;
    // target path node (or node object is going to)
    private PathNode mTargetPathNode;

    // finished
    private bool mFinished;

 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".

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    // -------------------------------------------------------------------------
    public void Start()
    {
        if (mSpeed == 0.0f)
            Debug.LogError("Speed cannot be zero!");

        Reset();
    }

    // -------------------------------------------------------------------------
    public void Reset()
    {
        if (mPathDefinition == null)
        {
            Debug.LogError(gameObject + ": path definition cannot be null");
            return;
        }

        mPathNodes = mPathDefinition.GetPathEnumerator();
        mPathNodes.MoveNext();

        mCurrentPathNode = mPathNodes.Current;

        mPathNodes.MoveNext();
        mTargetPathNode = mPathNodes.Current;

        mTimer.Set(mCurrentPathNode.Delay);
        mState = ePathFollowerState.Waiting;

        mFinished = false;
    }

 Here is Update method where most of the things take place:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
    // -------------------------------------------------------------------------
    public void Update()
    {
        if (mStopped || mFinished)
            return;


        // adjust timer (regardless it is for waiting or moving)
        mTimer.Update(Time.smoothDeltaTime);

        // if not waiting (moving) then adjust position of object
        if (mState == ePathFollowerState.Moving)
        {
            AdjustPosition(mTimer.GetActualRatio());
        }

        // if timer finished its running - do next steps
        if (!mTimer.IsRunning())
        {
            if (mState == ePathFollowerState.Waiting)
            {
                // any event on start of move from current position?
                OnLeaveEvent(mCurrentPathNode.LeaveEvent);
                MoveToTargetNode();

            }
            else if (mState == ePathFollowerState.Moving)
            {
                // any event in the end of move?
                OnArriveEvent(mCurrentPathNode.ArriveEvent);

                // if no new nodes we are finished
                if (!PrepareNextTargetNode())
                {
                    mFinished = true;
                    return;
                }

                // set waiting state (if any)
                float waitingTime = mCurrentPathNode.Delay;
                if (waitingTime > 0.0f)
                {
                    mTimer.Set(waitingTime);
                    mState = ePathFollowerState.Waiting;
                }
                else
                {
                    OnLeaveEvent(mCurrentPathNode.LeaveEvent);
                    MoveToTargetNode();
                }
            }
        }
    }

 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    // -------------------------------------------------------------------------
    private void MoveToTargetNode()
    {
        // calculate time to move based on nodes positions and speed per unit
        mTimer.Set(CalculateMoveTime());
        // set object as moving
        mState = ePathFollowerState.Moving;
    }

    // -------------------------------------------------------------------------
    private bool PrepareNextTargetNode()
    {
        // set target node as current node
        mCurrentPathNode = mTargetPathNode;

        // read new target node - if no nodes we are finished (only resetting can start it again
        if (!mPathNodes.MoveNext())
        {
            mCurrentPathNode = null;
            mTargetPathNode = null;
            return false;
        }
        
        mTargetPathNode = mPathNodes.Current;
        return true;
    }
    
    // -------------------------------------------------------------------------
    private void AdjustPosition(float aRatio)
    {
        PathNode.eMovementType movementType = mCurrentPathNode.MovementType;

        // adjust ratio based on movement type
        if (movementType == PathNode.eMovementType.Cos)
        {
            // return cos from 0 to 1 in PI range (1...-1 => 0...1)
            aRatio = (-Mathf.Cos(Mathf.PI * aRatio) + 1) / 2.0f;
        }
        else if (movementType == PathNode.eMovementType.Qudratic)
        {
            aRatio *= aRatio;
        }

        transform.position = Vector3.Lerp(mCurrentPathNode.transform.position,
                                          mTargetPathNode.transform.position,
                                          aRatio);
    }

    // -------------------------------------------------------------------------
    private float CalculateMoveTime()
    {
        Vector3 distance = mTargetPathNode.transform.position -
            mCurrentPathNode.transform.position;

        return (distance.magnitude / mSpeed) * mCurrentPathNode.SpeedModifier;
    }

 And finally, here are two last methods for handling events:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    // -------------------------------------------------------------------------
    public virtual void OnLeaveEvent(PathNode.ePathNodeEventType aEvent)
    {
        if (aEvent != PathNode.ePathNodeEventType.Nothing)
            Debug.Log("Leave event: " + aEvent);
    }

    // -------------------------------------------------------------------------
    public virtual void OnArriveEvent(PathNode.ePathNodeEventType aEvent)
    {
        if (aEvent != PathNode.ePathNodeEventType.Nothing)
            Debug.Log("Arrive event: " + aEvent);
    }

 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!



Conclusion


 This is end of my first Unity tutorial. I hope you made it here and now you have also this small system working.