Hexagonal grid: Moving animated character in the grid

Introduction

First of all, before we start I would like to mention that this is not a stand-alone article but another addition to the “Hexagonal grid” article series and here we will be continuing from the point we left off with “Path-finding using A* algorithm” tutorial. Also, as we will be working with vectors in this tutorial, you should at least know what they are and how vector subtraction and addition works.

Moving the character

Obviously the first thing to do is to find some animated character model. If you don’t have any and don’t know how to make one, you can simply import standard unity package called “Character controller” (…\Unity\Editor\Standard Packages). There you should find “3rd person controller”. Drop the prefab to your project, adjust model scale to fit nicely in the center of your hex tile and remove “Third Person Controller” and “Third Person Camera” scripts. Finally, those who are using their own models should add “Character Controller” (Component -> Physics -> Character Controller) (3rd person controller prefab already comes with character controller) and everyone should adjust the script’s public parameters. Now, you can just position the character on the left upper corner where the tile with (0, 0) coordinates should be manually using the editor or just create a new prefab, use it to instantiate public GameObject PlayerChar variable in the GridManager class and in the “Start” method use some code similar to the following one to place it in the right position:

    GameObject PC = (GameObject)Instantiate(PlayerChar);
    PC.transform.position = new Vector3(-2.283f, 0.637f, 2.25f);

Ok, now that we have our character model set up, let’s take a look at the actual script which we’ll be using control character movement.

public class CharacterMovement: MonoBehaviour
{
    //speed in meters per second
    public float speed = 0.0025F;
    public float rotationSpeed = 0.004F;
    //distance between character and tile position when we assume we reached it and start looking for the next. Explained in detail later on
    public static float MinNextTileDist = 0.07f;

    private CharacterController controller;
    public static CharacterMovement instance = null;
    //position of the tile we are heading to
    Vector3 curTilePos;
    Tile curTile;
    List<Tile> path;
    public bool IsMoving { get; private set; }
    Transform myTransform;

    void Awake()
    {
        //singleton pattern here is used just for the sake of simplicity. Messenger (http://goo.gl/3Okkh) should be used in cases when this script is attached to more than one character
        instance = this;
        IsMoving = false;
    }

    void Start()
    {
        controller = this.GetComponent<CharacterController>();
        //all the animations by default should loop
        animation.wrapMode = WrapMode.Loop;
        //caching the transform for better performance
        myTransform = transform;
    }

    //gets tile position in world space
    Vector3 calcTilePos(Tile tile)
    {
        //y / 2 is added to convert coordinates from straight axis coordinate system to squiggly axis system
        Vector2 tileGridPos = new Vector2(tile.X + tile.Y / 2, tile.Y);
        Vector3 tilePos = GridManager.instance.calcWorldCoord(tileGridPos);
        //y coordinate is disregarded
        tilePos.y = myTransform.position.y;
        return tilePos;
    }

    //method argument is a list of tiles we got from the path finding algorithm
    public void StartMoving(List<Tile> path)
    {
        if (path.Count == 0)
            return;
        //the first tile we need to reach is actually in the end of the list just before the one the character is currently on
        curTile = path[path.Count - 2];
        curTilePos = calcTilePos(curTile);
        IsMoving = true;
        this.path = path;
    }

    //Method used to switch destination and origin tiles after the destination is reached
    void switchOriginAndDestinationTiles()
    {
        GridManager GM = GridManager.instance;
        Material originMaterial = GM.originTileTB.renderer.material;
        GM.originTileTB.renderer.material = GM.destTileTB.defaultMaterial;
        GM.originTileTB = GM.destTileTB;
        GM.originTileTB.renderer.material = originMaterial;
        GM.destTileTB = null;
        GM.generateAndShowPath();
    }

    void Update()
    {
        if (!IsMoving)
            return;
//code continued after the break

Looking at the following code line you might be wondering why can’t we just check if the distance is zero and why are we using sqrMagnitude instead of just magnitude. The answer is that we can’t just check if the distance is zero as it might never become zero because our movement and rotation speeds might not be low enough and the reason to use sqrMagnitude instead of just magnitude is simply because calculating squared magnitude is much faster.

        //if the distance between the character and the center of the next tile is short enough
        if ((curTilePos - myTransform.position).sqrMagnitude < MinNextTileDist * MinNextTileDist)
        {
            //if we reached the destination tile
            if (path.IndexOf(curTile) == 0)
            {
                IsMoving = false;
                animation.CrossFade("idle");
                switchOriginAndDestinationTiles();
                return;
            }
            //curTile becomes the next one
            curTile = path[path.IndexOf(curTile) - 1];
            curTilePos = calcTilePos(curTile);
        }

        MoveTowards(curTilePos);
    }

    void MoveTowards(Vector3 position)
    {
        //mevement direction
        Vector3 dir = position - myTransform.position;

        // Rotate towards the target
        myTransform.rotation = Quaternion.Slerp(myTransform.rotation,
            Quaternion.LookRotation(dir), rotationSpeed * Time.deltaTime);

        Vector3 forwardDir = myTransform.forward;
        forwardDir = forwardDir * speed;
//continued after the break

Just a short explanation what speedModifier represents here. Dot product is calculated by multiplying vector lengths by cos of the angle between them. Both vectors used there are unit vectors so basically the result of the dot product here is just cosθ. When the character is almost facing straight at the center of the next tile (the result of cosθ is more than 0.95) we start moving with a bit lower speed (forwardDir *= speedModifier) because the character is still rotating while moving. Just keep in mind that depending on your rotation speed you might need to change 0.95 to something better suited for your situation.

        float speedModifier = Vector3.Dot(dir.normalized, myTransform.forward);
        forwardDir *= speedModifier;
        if (speedModifier > 0.95f)
        {
            controller.SimpleMove(forwardDir);
            if (!animation["walk"].enabled)
                animation.CrossFade("walk");
        }
        else if (!animation["idle"].enabled)
            animation.CrossFade("idle");
    }
}

We now have CharacterMovement class with public startMoving method but we don’t use it anywhere yet. Let’s modify GridManager class to make origin tile the tile with (0,0) coordinates on game start and call startMoving method when path is generated.

//Methods in GridManager class to be modified
    void createGrid()
    {
        Vector2 gridSize = calcGridSize();
        GameObject hexGridGO = new GameObject("HexGrid");
        //board is used to store tile locations
        Dictionary<Point, Tile> board = new Dictionary<Point, Tile>();

        for (float y = 0; y < gridSize.y; y++)
        {
            float sizeX = gridSize.x;
            //if the offset row sticks up, reduce the number of hexes in a row
            if (y % 2 != 0 && (gridSize.x + 0.5) * hexWidth > groundWidth)
                sizeX--;
            for (float x = 0; x < sizeX; x++)
            {
                GameObject hex = (GameObject)Instantiate(Hex);
                Vector2 gridPos = new Vector2(x, y);
                hex.transform.position = calcWorldCoord(gridPos);
                hex.transform.parent = hexGridGO.transform;
                var tb = (TileBehaviour)hex.GetComponent("TileBehaviour");
                //y / 2 is subtracted from x because we are using straight axis coordinate system
                tb.tile = new Tile((int)x - (int)(y / 2), (int)y);
                board.Add(tb.tile.Location, tb.tile);
                //Mark originTile as the tile with (0,0) coordinates
                if (x == 0 && y == 0)
                {
                    tb.renderer.material = tb.OpaqueMaterial;
                    Color red = Color.red;
                    red.a = 158f / 255f;
                    tb.renderer.material.color = red;
                    originTileTB = tb;
                }
            }
        }
        //variable to indicate if all rows have the same number of hexes in them
        //this is checked by comparing width of the first hex row plus half of the hexWidth with groundWidth
        bool equalLineLengths = (gridSize.x + 0.5) * hexWidth <= groundWidth;
        //Neighboring tile coordinates of all the tiles are calculated
        foreach(Tile tile in board.Values)
            tile.FindNeighbours(board, gridSize, equalLineLengths);
    }

    public void generateAndShowPath()
    {
        //Don't do anything if origin or destination is not defined yet
        if (originTileTB == null || destTileTB == null)
        {
            DrawPath(new List<Tile>());
            return;
        }
        //We assume that the distance between any two adjacent tiles is 1
        //If you want to have some mountains, rivers, dirt roads or something else which might slow down the player you should replace the function with something that suits better your needs
        Func<Tile, Tile, double> distance = (node1, node2) => 1;

        var path = PathFinder.FindPath(originTileTB.tile, destTileTB.tile, 
            distance, calcDistance);
        DrawPath(path);
        CharacterMovement.instance.StartMoving(path.ToList());
    }

Finally we reach the point where the only thing left to do is attaching CharacterMovement script to your character prefab, adjust speed, rotationSpeed and MinNextTileDist public variables to suit your needs and click play 🙂

Advertisements

25 Responses to Hexagonal grid: Moving animated character in the grid

  1. Alex says:

    Hello,
    I find your tutorials very helpful and interesting. Thanks for sharing. I am just starting with Unity now and any good tutorial is vital.

    It would be great if you could enable an RSS feed for your blog so I could sign up to get the in the reader.

    • hakimio says:

      You are welcome.
      RSS is enabled, it’s just that I don’t show the link to RSS feed. Most of web browsers nowadays have buttons to get the link to RSS feed, if your browser doesn’t support that, here is the url.

      PS: if you find any problems with the tutorials, feel free to post a comment.

  2. Tommy says:

    This seems like a really great tutorial. I’ve been trying to follow it so far, but I’ve run into some problems. I’ve been getting “The type or namespace name `Func’ could not be found. Are you missing a using directive or an assembly reference?” for Func and Node. I can’t say I’m a c# expert so I don’t know too much about using Unity with Func type, but Unity doesn’t seem to recognize it

  3. Geo says:

    Will you be continuing this series of articles?… If not, can you point to a resource that is similar?… It was excellent, but I’m looking for the logic/prog-state layer info… Thank you,
    G

    • hakimio says:

      No, I won’t be continuing. You can find source code of my project which has most of the things, described in the “About the game” blog post, implemented here. You can use the source code as you wish, but the pathfinder used in “Level1” scene requires Pro version of Unity3D and I don’t own any of the 3d models used in the project.

      • Geo says:

        Since I’m just starting, would you please explain why the pathfinder used in “Level1″ scene requires Pro version of Unity3D?

        & thank you for taking the time to respond, and do the tutorials that you did… All the best ~ G

      • hakimio says:

        Built-in navmesh generator and pathfinding is pro only features -> http://docs.unity3d.com/Documentation/Manual/NavmeshandPathfinding.html

      • litelus says:

        It’s incredibly nice of you to let people use your source code as they wish.
        I’ve checked it out and your project might give me a big jump start in my tbs.
        Is there any way i can buy you a beer/pizza/month of unity pro if that’s the case ?

      • hakimio says:

        Glad you found it useful 🙂
        Good luck with your project 🙂

        PS: the source code can be used in any project (even a commercial one), but some of the other assets (3D & 2D graphics, fonts, sounds, etc) might not be free. Also pathfinding used in one of the scenes for real-time character path calculation is only available in a non-free Unity version.

  4. Vinh says:

    Just wanted to say thanks, found this resource very helpful.

  5. Josh says:

    Very handy tutorial. This information is the jump start I have been looking for to get into Unity. Thanks.

  6. Fuhans Puji Saputra says:

    Hai, why my character is not moving?

    Here is the link of the image:

    Anyway, thank you for the tutorial, it is help me a lot!

  7. mike says:

    Am I correct in thinking that if using this for multiple objects, but only one object at a time, the messenger class is not needed? Just have an object reference of “selectedObject” or something and instead of charactermovement.instance.startmoving can just do selectedObject.movementScript.startmoving?

  8. mike says:

    I’m having a problem with this. Quaternion.lookrotation is returning a seemingly random result. when direction is (5,0,0), lookrotation is randomly returning (0,0.7,0,0.7) and as a result the object moves off into the y direction, when y is the only direction it should never move =/. I’ve tried researching it but had no luck. Do you have any idea? Thanks in advance.

    • tomas says:

      How are you calculating the direction? Is it a difference of target position and current position like in the example? Are you using slerp to get current rotation?

      • mike says:

        Yeah it’s identical to the example above, i can only assume it’s something to do with the quaternion.lookrotation up vector, but blindly experimenting with it has given no results.

      • mike says:

        to clarify, the vector3 dir = (5,0,0), quaternion.lookrotation(dir) is returning (0, 0.7, 0, 0.7)

    • tomas says:

      i don’t know what Quaternion should be for that direction, but I could suggest to make sure y value of current and target positions is the same and try out my project which I host on github.

      • mike says:

        may i ask what your compass is set to for this example and the github? ie, is positive Z up the screen, positive x to the right and negative Y going into the screen?

      • hakimio says:

        I don’t have unity3d installed in the computer I am using right now but as far as I remember some of the axis might have been switched. Is there any problems building the project with latest unity?

  9. Chridder says:

    Hi, thanks for the great tutorial…
    I have an issue with the latest changes in your description:
    GridManager->generateAndShowPath()
    Statement
    “CharacterMovement.instance.StartMoving(path.ToList());”

    I get the error: “Path” does not contain any definition for “ToList” and also no enhancement method “ToList”…

    Does anyone know how to fix it?
    I tried several things but w/o any results.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: