Pathfinding in Unity3D Continued: Moving

on unity3d

posts/pathfinding_in_unity3d_continued_moving/FFT_Battle.jpg

In my last blog post I talked about how to find a path from a start position to an end position. Now let us use that path to move the entity to that end position.

Following The Path

First thing you need is a new component, this will be responsible for moving our entity around based on the given path. It is important for this to be a component because it needs to be updated every frame. The entity will slowly inch its way to the next tile and on to the next until it finally reaches the end position.

Now there are a couple ways of doing this, I felt like it was easiest to add the Walk component when needed and remove it when finished. It looks like this.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

[RequireComponent (typeof (TileAlign))]
public class Walk : MonoBehaviour {

    private Stack<Vector3> _path;

    private Action<GameObject> _OnComplete;
    private Animator _animator;

    private bool _finished;

    private float _startTime;
    private float _journeyLength;
    public float speed = 5.5f;
    public bool faceDirection = true;

    private Vector3 _startPos;
    private Vector3 _endPos;

    private Entity entity;

    void Awake(){
        entity = GetComponent<Entity>();
        _animator = entity.animator;
    }

    public void FollowPath(Stack<Vector3> path, Action<GameObject> OnComplete){
        this._OnComplete = OnComplete;
        // pop first point cause its my current position
        path.Pop();

        _animator.SetFloat("Speed", speed);
        this._path = path;
        _finished = true;
    }

    private void FinishWalk(){
        _animator.SetFloat("Speed", 0);

        if(_OnComplete != null){
            _OnComplete(this.gameObject);
            _OnComplete = null;
        }

        _path = null;

        Destroy(this);
    }

    private void OnVisit(Vector3 tilePos){
        // TODO: trigger global event
    }

    private void NextDirection(){
        _startTime = Time.time;
        Vector3 newDirection = _path.Pop();

        Vector3 endPoint = MathUtil.TileToPoint(newDirection);

        _startPos = transform.localPosition;
        _endPos = endPoint;

        if(faceDirection){
            entity.LookAtTile(newDirection);
        }

        _journeyLength = Vector3.Distance(_startPos, _endPos);
    }

    public void Update(){

        if(_finished){

            OnVisit(MathUtil.PointToTile(transform.localPosition));

            if(_path.Count == 0){
                FinishWalk();
                return;
            } else {
                NextDirection();
            }
        }

        float distCovered = (Time.time - _startTime) * speed;
        float fracJourney = distCovered / _journeyLength;

        Vector3 pos = Vector3.Lerp(_startPos, _endPos, fracJourney);

        pos.y = GetComponent<TileAlign>().GetTileHeight();

        transform.localPosition = pos;

        _finished = fracJourney >= 1;
    }
}

This component accepts a path and a completion method. It simply grabs tiles, one at a time from the path and slowly moves the entity in a straight line from the last tile to the next tile using Unitys built in method Vector3.Lerp.

On every update call, it checks to see if the entity has reached the next tile, if so it will attempt to find the next tile to move to. Or if it reached the final tile, finishes its walk and calls the completion method.

If the entity has not reached a tile yet, it will continue moving along in a straight line. Remember the entity is walking and animating in this time.

private void NextDirection(){
    _startTime = Time.time;
    Vector3 newDirection = _path.Pop();

    Vector3 endPoint = MathUtil.TileToPoint(newDirection);

    _startPos = transform.localPosition;
    _endPos = endPoint;

    if(faceDirection){
        entity.LookAtTile(newDirection);
    }

    _journeyLength = Vector3.Distance(_startPos, _endPos);
}

The NextDirection method takes the last element out of the path and converts its tile position into world space so a correct distance value can be found. This just multiplies each number by the tile size.

float distCovered = (Time.time - _startTime) * speed;
float fracJourney = distCovered / _journeyLength;

Vector3 pos = Vector3.Lerp(_startPos, _endPos, fracJourney);

This distance is then put into the Vector3.Lerp method. This method basically slowly increments in value based on time passed. You can read more about it here.

_animator.SetFloat("Speed", speed);

Before the entity is first moved, a value Speed is set on the animator object. This signals that the entity is moving and the walking animation is played. I think this will be refactored out in the future, it is unrelated to the Walk component.

A method OnVisit is called on every new tile entered. It will trigger a global event, like I talked about in Unity Event Manager. This event can then be used to trigger things on the outside like, cutscenes, animations and AI. It can even be used to check for a hidden mine and explode.

Aligning to Tile

You might have noticed a component called TileAlign being used in the update method. As the entity moves around it will enter tiles with various heights. This method provides an easy way of figuring out the height of the tile directly below the entity. That value can then be used to move the entity up or down so it is correctly standing on it.

public class TileAlign : MonoBehaviour {

    private Collider _tileCollider;

    public float GetTileHeight(){
        // lazy define to ensure its defined
        if(_tileCollider == null){
            _tileCollider = App.Instance.tileCollider;
        }

        RaycastHit hit;
        // transform from local to world and offset to middle of model
        float halfTile = App.tileSize / 2;
        Vector3 origin = transform.TransformPoint(halfTile, 10, halfTile);
        Vector3 down = transform.TransformDirection(Vector3.down);

        Ray ray = new Ray(origin, down);
        if(_tileCollider.collider.Raycast(ray, out hit, 30f)){
            return hit.point.y;
        }

        return 0;
    }

    public void AlignToTile(){
        Vector3 pos = transform.localPosition;

        pos.y = GetTileHeight();

        transform.localPosition = pos;
    }
}

This uses a ray cast that shoots directly below the entity and hits the tileColldier, which is specifically created to handle collisions on the map. A ray cast works wonders because it works in every situation, even on uneven tiles, like slops. This still work even if the entity is on the corner of the tile, because a ray cast is sent from the entities current position.

If a hit was made, its y value is then taken and returned as the tile height.

Usage

_preset = new MoveSearchPreset();
_preset.pathFindProps.startPos = currentEntity.tilePos;
_preset.pathFindProps.endPos = tilePos;

Stack<Vector3> path = pathFind.Search(_preset.pathFindProps);

Walk walk = currentEntity.gameObject.AddComponent<Walk>();
walk.FollowPath(path, OnWalkComplete);

The Walk component is used like so. First a path is found using the path finding algorithm I talked about in the last blog post. Second a walk component is added to the target which you want to move through the path. Finally the method FollowPath is called with the path and callback method.

The callback method is needed because this is an asynchronous process. When the walking is complete, the Walk component can be removed.

Next Time

In the next blog post I will go through how to highlight individual tiles like in Final Fanytasy Tactics.