Hexagonal Turn Based
Example scene which demonstrates turn-based movement and custom traversal rules.
Overview
This example scene shows turn-based movement on a hexagonal grid. It does not use the normal movement scripts, but instead handles movement for all characters in a single script.
Turn-based movement can, of course, be done with the included movement scripts as well.
The scene presents a simple puzzle game. You can click on the orange cones to show their movement range, and then you can move them by clicking on a separate tile. If you move an orange cone to a purple hexagon, you will get the option to toggle a set of green "door" hexagons.
The goal of the puzzle is to get one orange cone to the purple hexagon in the red area to the right of the screen.
Graph setup
A hexagonal grid graph is used in this scene. It is configured by creating a GridGraph, and then changing the Shape dropdown to Hexagonal.
For hexagonal graphs, there are two different measurements that one can use to size them. In this scene we set the width (distance between opposing sides) of the hexagons to 1 world unit, but it is also possible to use the diameter (distance between opposing vertices).
If you are building a 2D game with a Tilemap component, you can align the graph to your tilemap directly. See Pathfinding on tilemaps.
You may notice that the hexagonal graph looks like a squished rectangle. This is because a hexagonal graph is internally actually just a grid graph which has been squished and connected in a different way. For some games, you may get some wasted nodes outside your level, but this is usually nothing to worry about, performance-wise.
To align all the hexagonal meshes in the scene with the graph, a script called SnapToNode is used. This runs even in edit mode, and will snap the transform to the closest node every time the transform is moved.
void Update () {
This makes it easy to move around prefabs in the scene view, and have them automatically snap to the closest hexagon node.
if (transform.hasChanged && AstarPath.active != null) {
var node = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node;
if (node != null) {
transform.position = (Vector3)node.position;
transform.hasChanged = false;
}
}
}
Purple triggers
The purple hexagons in the scene are triggered using Unity's built-in OnTriggerEnter function. This is handled by the HexagonTrigger class. In this method, the script checks if the object is an agent, and if so, if the unit has this node as its target node, or if it is merely passing through. If the checks pass, an animation is played.
void OnTriggerEnter (Collider coll) {
The animation will either reveal a button, or for the final one, show a victory message. The buttons that are revealed can toggle the traversability of the green hexagons. If a button is clicked, it calls TurnBasedDoor.Toggle on the relevant green hexagons. In turn, this will play an animation, and use the SingleNodeBlocker component to block or unblock the node underneath the green hexagon.
var unit = coll.GetComponentInParent<TurnBasedAI>();
var node = AstarPath.active.GetNearest(transform.position).node;
// Check if it is an agent and the agent is headed for this node
if (unit != null && unit.targetNode == node) {
visible = true;
anim.CrossFade("show", 0.1f);
}
}
Utilities for turn-based games For more info about how the SingleNodeBlocker component works.
Agent movement
Movement in this example scene is a bit different from the other ones, in that it does not use one of the built-in movement scripts. Instead, it uses a custom script, TurnBasedManager to handle all movement. This is not a requirement for a turn-based game, but it illustrates an alternative approach to movement.
This manager does a few things:
It listens for mouse clicks, and selects or moves agents depending on what was clicked.
If an agent is selected, it shows all nodes it can move to within one turn.
When an agent moves, it animates the agent's position along a smoothed version of the path.
It triggers the SingleNodeBlocker on each agent when they move, so that the other agent cannot move to the same hexagon.
Movement animation
To move the agents, the TurnBasedManager.MoveAlongPath method is used. It uses a catmull-rom spline to smoothly interpolate between the points in the path.
static IEnumerator MoveAlongPath (TurnBasedAI unit, ABPath path, float speed) {
if (path.error || path.vectorPath.Count == 0)
throw new System.ArgumentException("Cannot follow an empty path");
// Very simple movement, just interpolate using a catmull-rom spline
float distanceAlongSegment = 0;
for (int i = 0; i < path.vectorPath.Count - 1; i++) {
var p0 = path.vectorPath[Mathf.Max(i-1, 0)];
// Start of current segment
var p1 = path.vectorPath[i];
// End of current segment
var p2 = path.vectorPath[i+1];
var p3 = path.vectorPath[Mathf.Min(i+2, path.vectorPath.Count-1)];
// Approximate the length of the spline
var segmentLength = Vector3.Distance(p1, p2);
// Move the agent forward each frame, until we reach the end of the segment
while (distanceAlongSegment < segmentLength) {
// Use a Catmull-rom spline to smooth the path. See https://en.wikipedia.org/wiki/Cubic_Hermite_spline#Catmull%E2%80%93Rom_spline
var interpolatedPoint = AstarSplines.CatmullRom(p0, p1, p2, p3, distanceAlongSegment / segmentLength);
unit.transform.position = interpolatedPoint;
yield return null;
distanceAlongSegment += Time.deltaTime * speed;
}
distanceAlongSegment -= segmentLength;
}
// Move the agent to the final point in the path
unit.transform.position = path.vectorPath[path.vectorPath.Count - 1];
}
Movement range
To show the movement range, a ConstantPath is used. This is a special path type which outputs all nodes that can be reached up to a given maximum path cost. Therefore, it is perfect for generating all nodes that an agent can reach in one turn.
Take a look at the Path Types example scene for more info about the ConstantPath type.
This is handled by the TurnBasedManager.GeneratePossibleMoves method. It synchronously (for simplicity) calculates a ConstantPath, iterates through all nodes in the path, and instantiates a prefab on each node's position.
void GeneratePossibleMoves (TurnBasedAI unit) {
var path = ConstantPath.Construct(unit.transform.position, unit.movementPoints * 1000 + 1);
path.traversalProvider = unit.traversalProvider;
// Schedule the path for calculation
AstarPath.StartPath(path);
// Force the path request to complete immediately
// This assumes the graph is small enough that
// this will not cause any lag
path.BlockUntilCalculated();
foreach (var node in path.allNodes) {
if (node != path.startNode) {
// Create a new node prefab to indicate a node that can be reached
// NOTE: If you are going to use this in a real game, you might want to
// use an object pool to avoid instantiating new GameObjects all the time
var go = GameObject.Instantiate(nodePrefab, (Vector3)node.position, Quaternion.identity) as GameObject;
possibleMoves.Add(go);
go.GetComponent<Astar3DButton>().node = node;
}
}
}