Hexagonal grid: Path-finding using A* algorithm :: 게임제작[SSISO Community]
 
SSISO 카페 SSISO Source SSISO 구직 SSISO 쇼핑몰 SSISO 맛집
추천검색어 : JUnit   Log4j   ajax   spring   struts   struts-config.xml   Synchronized   책정보   Ajax 마스터하기   우측부분

게임제작
[1]
등록일:2018-10-18 14:32:05 (0%)
작성자:
제목:Hexagonal grid: Path-finding using A* algorithm

Hexagonal grid: Path-finding using A* algorithm

Preparations

As usual let’s start with things you should know before following this guide. Firstly, as this entry is continuation of “Hexagonal grid” article series, you should know about the things explained in Generating the grid article. Also, even I will explain some of the implementation details, you should know how A* search algorithm works. Next you should read “Hexagon grids: coordinate systems and distance calculations” article by Chris Schetter to know what I mean by squiggly axis and straight axis coordinate systems. Finally, you should read “Path Finding Using A* in C# 3.0” article series by Eric Lippert because we’ll be using his generic path finding implementation. Oh, and one more thing I would like to mention before we move on with the tutorial: this tutorial will be in many ways similar to Patrick Lea’s “Unity, hexagons and path-finding” with few key differences – I will use straight axis coordinate system, because of which things like distance calculations and neighbor tile coordinate calculations will be different, also I will use slightly different ways to represent pathfindng results, determine origin and destination tiles and I will take into account ground dimensions.

The model layer

If some of you are not familiar with “model layer” terminology, model layer groups classes which are usually used for data representation. Classes here should be simple enough to understand without any explanation for any average C# coder.

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
 
//struct holding x and y coordinates
public struct Point
{
    public int X, Y;
 
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
//abstract class implemented by Tile class
public abstract class GridObject
{
    public Point Location;
    public int X { get { return Location.X; } }
    public int Y { get { return Location.Y; } }
 
    public GridObject(Point location)
    {
        Location = location;
    }
 
    public GridObject(int x, int y): this(new Point(x, y)) {}
 
    public override string ToString()
    {
        return string.Format("({0}, {1})", X, Y);
    }
}
1
2
3
4
5
6
using System.Collections;
//interface that should be implemented by grid nodes used in E. Lippert's generic path finding implementation
public interface IHasNeighbours<N>
{
    IEnumerable<N> Neighbours { get; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Collections;
using System;
using System.Linq;
using UnityEngine;
//Basic skeleton of Tile class which will be used as grid node
public class Tile: GridObject, IHasNeighbours<Tile>
{
    public bool Passable;
 
    public Tile(int x, int y)
        : base(x, y)
    {
        Passable = true;
    }
 
    public IEnumerable AllNeighbours { get; set; }
    public IEnumerable Neighbours
    {
        get { return AllNeighbours.Where(o => o.Passable); }
    }
}

The last class is not complete and we will return to it a bit later.

Generic A* algorithm implementation

For generic A* implementation we’ll be using the code kindly provided by Eric Lippert and I will only add few comments to the actual path finding algorithm implementation and none to the data classes which are easy to understand once written but might not be so easy to come up with.

1
2
3
4
5
6
7
8
9
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
 
public class Path: IEnumerable
{
//this comment should be replaced by code from http://goo.gl/oJ6Hd
}

 

1
2
3
4
5
6
7
8
using System;
using System.Collections.Generic;
using System.Linq;
 
class PriorityQueue
{
//this comment should be replaced by code from http://goo.gl/ugzP6
}
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
using System;
using System.Collections.Generic;
using System.Linq;
 
public static class PathFinder
{
    //distance f-ion should return distance between two adjacent nodes
    //estimate should return distance between any node and destination node
    public static Path<Node> FindPath<Node>(
        Node start,
        Node destination,
        Func distance,
        Func estimate)
        where Node: IHasNeighbours
    {
        //set of already checked nodes
        var closed = new HashSet();
        //queued nodes in open set
        var queue = new PriorityQueue>();
        queue.Enqueue(0, new Path(start));
 
        while (!queue.IsEmpty)
        {
            var path = queue.Dequeue();
 
            if (closed.Contains(path.LastStep))
                continue;
            if (path.LastStep.Equals(destination))
                return path;
 
            closed.Add(path.LastStep);
 
            foreach (Node n in path.LastStep.Neighbours)
            {
                double d = distance(path.LastStep, n);
                //new step added without modifying current path
                var newPath = path.AddStep(n, d);
                queue.Enqueue(newPath.TotalCost + estimate(n), newPath);
            }
        }
 
        return null;
    }
}

Interacting with the grid

The result of the previous tutorial is nothing more than just a plain grid represantation which can not be interacted with in any way. This time let’s make it look better and react to some mouse actions. For that to happen we will need to introduce new TileBehavior class which will be used with hex tile prefab and modify our GridManager class from the last section in Generating the grid tutorial plus add a couple new hex tile materials to our project. The first new material should use white texture with some other color representing hex tile outline and the second one should be completely transparent except hex tile outlines plus the shader used in both cases should be “Transparent/Diffuse”. If you are using hex tile model which I linked to in the previous tutorial, you can find textures for it here. After you have created two new materials, let’s modify a bit our GridManager 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class GridManager: MonoBehaviour
{
    //selectedTile stores the tile mouse cursor is hovering on
    public Tile selectedTile = null;
    //TB of the tile which is the start of the path
    public TileBehaviour originTileTB = null;
    //TB of the tile which is the end of the path
    public TileBehaviour destTileTB = null;
 
    public static GridManager instance = null;
 
    void Awake()
    {
        instance = this;
    }
    //some of the omitted code from the original goes here
    void createGrid()
    {
        Vector2 gridSize = calcGridSize();
        GameObject hexGridGO = new GameObject("HexGrid");
 
        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;
                //TileBehabiour object is retrieved
                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);
            }
        }
    }
    //following method will be implemented a bit later
    public void generateAndShowPath() {}
//the rest of the omitted code goes here

We’ll come back to GridManager once again once we’ll be showing the path but for now, let’s leave it and take a look at our TileBehaviour 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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using UnityEngine;
using System.Collections;
 
public class TileBehaviour: MonoBehaviour
{
    public Tile tile;
    //After attaching this script to hex tile prefab don't forget to initialize following materials with the ones we created earlier
    public Material OpaqueMaterial;
    public Material defaultMaterial;
    //Slightly transparent orange
    Color orange = new Color(255f / 255f, 127f / 255f, 0, 127f/255f);
 
    void changeColor(Color color)
    {
        //If transparency is not set already, set it to default value
        if (color.a == 1)
            color.a = 130f / 255f;
        renderer.material = OpaqueMaterial;
        renderer.material.color = color;
    }
 
    //IMPORTANT: for methods like OnMouseEnter, OnMouseExit and so on to work, collider (Component -> Physics -> Mesh Collider) should be attached to the prefab
    void OnMouseEnter()
    {
        GridManager.instance.selectedTile = tile;
        //when mouse is over some tile, the tile is passable and the current tile is neither destination nor origin tile, change color to orange
        if (tile.Passable && this != GridManager.instance.destTileTB
            && this != GridManager.instance.originTileTB)
        {
            changeColor(orange);
        }
    }
 
    //changes back to fully transparent material when mouse cursor is no longer hovering over the tile
    void OnMouseExit()
    {
        GridManager.instance.selectedTile = null;
        if (tile.Passable && this != GridManager.instance.destTileTB
            && this != GridManager.instance.originTileTB)
        {
            this.renderer.material = defaultMaterial;
            this.renderer.material.color = Color.white;
        }
    }
    //called every frame when mouse cursor is on this tile
    void OnMouseOver()
    {
        //if player right-clicks on the tile, toggle passable variable and change the color accordingly
        if (Input.GetMouseButtonUp(1))
        {
            if (this == GridManager.instance.destTileTB ||
                this == GridManager.instance.originTileTB)
                return;
            tile.Passable = !tile.Passable;
            if (!tile.Passable)
                changeColor(Color.gray);
            else
                changeColor(orange);
 
            GridManager.instance.generateAndShowPath();
        }
        //if user left-clicks the tile
        if (Input.GetMouseButtonUp(0))
        {
            tile.Passable = true;
 
            TileBehaviour originTileTB = GridManager.instance.originTileTB;
            //if user clicks on origin tile or origin tile is not assigned yet
            if (this == originTileTB || originTileTB == null)
                originTileChanged();
            else
                destTileChanged();
 
            GridManager.instance.generateAndShowPath();
        }
    }
 
    void originTileChanged()
    {
        var originTileTB = GridManager.instance.originTileTB;
        //deselect origin tile if user clicks on current origin tile
        if (this == originTileTB)
        {
            GridManager.instance.originTileTB = null;
            renderer.material = defaultMaterial;
            return;
        }
        //if origin tile is not specified already mark this tile as origin
        GridManager.instance.originTileTB = this;
        changeColor(Color.red);
    }
 
    void destTileChanged()
    {
        var destTile = GridManager.instance.destTileTB;
        //deselect destination tile if user clicks on current destination tile
        if (this == destTile)
        {
            GridManager.instance.destTileTB = null;
            renderer.material.color = orange;
            return;
        }
        //if there was other tile marked as destination, change its material to default (fully transparent) one
        if (destTile != null)
            destTile.renderer.material = defaultMaterial;
        GridManager.instance.destTileTB = this;
        changeColor(Color.blue);
    }
}

Now, after you attach collider (Component -> Physics -> Mesh collider) and TileBehaviour script, instantiate opaque and default materials with the materials we created earlier and finally start the game you should be able to select origin and destination tiles plus tile color should change but nothing more will happen. We need to actually generate the path and show it.

Generating and showing the path

Finally we come to the last part of this tutorial where we actually generate and show the path. To do just that we’ll need to modify our GridManager once again.

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//Some part of the code covered before is omitted
    //Line should be initialised to some 3d object that can fit nicely in the center of a hex tile and will be used to indicate the path. For example, it can be just a simple small sphere with some material attached to it. Initialise the variable using inspector pane.
    public GameObject Line;
    //List to hold "Lines" indicating the path
    List<GameObject> path;
 
    void Start()
    {
        setSizes();
        createGrid();
        generateAndShowPath();
    }
 
    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);
            }
        }
        //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);
    }
    //Distance between destination tile and some other tile in the grid
    double calcDistance(Tile tile)
    {
        Tile destTile = destTileTB.tile;
        //Formula used here can be found in Chris Schetter's article
        float deltaX = Mathf.Abs(destTile.X - tile.X);
        float deltaY = Mathf.Abs(destTile.Y - tile.Y);
        int z1 = -(tile.X + tile.Y);
        int z2 = -(destTile.X + destTile.Y);
        float deltaZ = Mathf.Abs(z2 - z1);
 
        return Mathf.Max(deltaX, deltaY, deltaZ);
    }
 
    private void DrawPath(IEnumerable<Tile> path)
    {
        if (this.path == null)
            this.path = new List<GameObject>();
        //Destroy game objects which used to indicate the path
        this.path.ForEach(Destroy);
        this.path.Clear();
 
        //Lines game object is used to hold all the "Line" game objects indicating the path
        GameObject lines = GameObject.Find("Lines");
        if (lines == null)
            lines = new GameObject("Lines");
        foreach (Tile tile in path)
        {
            var line = (GameObject)Instantiate(Line);
            //calcWorldCoord method uses squiggly axis coordinates so we add y / 2 to convert x coordinate from straight axis coordinate system
            Vector2 gridPos = new Vector2(tile.X + tile.Y / 2, tile.Y);
            line.transform.position = calcWorldCoord(gridPos);
            this.path.Add(line);
            line.transform.parent = lines.transform;
        }
    }
 
    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);
    }
//Part of the code omitted

The last piece of the puzzle – FindNeighbours method implementation in the Tile 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Tile: GridObject, IHasNeighbours<Tile>
{
//Some previously covered code omitted
 
    //change of coordinates when moving in any direction
    public static List<Point> NeighbourShift
    {
        get
        {
            return new List<Point>
                {
                    new Point(0, 1),
                    new Point(1, 0),
                    new Point(1, -1),
                    new Point(0, -1),
                    new Point(-1, 0),
                    new Point(-1, 1),
                };
        }
    }
 
    public void FindNeighbours(Dictionary<Point, Tile> Board,
        Vector2 BoardSize, bool EqualLineLengths)
    {
        List<Tile> neighbours = new List<Tile>();
 
        foreach (Point point in NeighbourShift)
        {
            int neighbourX = X + point.X;
            int neighbourY = Y + point.Y;
            //x coordinate offset specific to straight axis coordinates
            int xOffset = neighbourY / 2;
 
            //If every second hexagon row has less hexagons than the first one, just skip the last one when we come to it
            if (neighbourY % 2 != 0 && !EqualLineLengths &&
                neighbourX + xOffset == BoardSize.x - 1)
                continue;
            //Check to determine if currently processed coordinate is still inside the board limits
            if (neighbourX >= 0 - xOffset &&
                neighbourX < (int)BoardSize.x - xOffset &&
                neighbourY >= 0 && neighbourY < (int)BoardSize.y)
                neighbours.Add(Board[new Point(neighbourX, neighbourY)]);
        }
 
        AllNeighbours = neighbours;
    }
}

At long last, initialize Line game object defined in GridManager class with some 3d object that fits inside your hexagon tile and push “play” to see everything’s working as expected ????

Small update: in case something doesn’t work for you, take a look at a working example code at github.

[본문링크] Hexagonal grid: Path-finding using A* algorithm
[1]
코멘트(이글의 트랙백 주소:/cafe/tb_receive.php?no=34885
작성자
비밀번호

 

SSISOCommunity

[이전]

Copyright byCopyright ⓒ2005, SSISO Community All Rights Reserved.