概要
Waypointに到達するごとに次のWaypointを取得するこで経路を決定します。以下のものが必要になります。
- Waypoints:ゴーストが移動できる全座標のリスト。
- Tile:Waypoint同士のつながりを表すクラス。
- TileUtility:ゴーストの現在地からTileを取得するクラス。
- AAI:TileUtilityを使ってゴーストの現在地からTileを取得し、移動方向を決定する抽象クラス。これを継承してゴーストごとのAIを作成します。
- 各ゴーストのAI:追跡対象を取得するクラス。
処理の流れは以下の通りです。
- TileUtilityを使ってゴーストの現在地からTileを取得します。
- TileからAIを使って移動先のWaypointを決定します。
Waypointsの取得
ゴーストが移動できる全座標を手作業で座標を書き出してもよいのですが、迷路のColliderMapの作成と同じ要領でWaypointsMapを作成し、MazeUtilityを使いWaypointsを取得します(MazeUtilityについては下記参照)。
nullsuke.com
MazeオブジェクトにWaypointsMapをセットし、ついでに迷路の幅と高さも設定します。
TileUtilityの作成
Mazeオブジェクトが読み込まれたときにCreateTiles関数でTileのリスト作成し、それをもとにTileUtilityを作成します。
public class Tile
{
public Tile Left, Right, Up, Down;
public Vector2 Waypoint { get; private set; }
public int X { get; private set; }
public int Y { get; private set; }
public bool IsOccupied { get; set; } = true;
public bool IsIntersection { get; set; }
public int AdjacentCount { get; set; }
public Tile(int x, int y)
{
X = x;
Y = y;
Waypoint = new Vector2(x, y);
Left = Right = Up = Down = null;
}
}
public class TileUtility
{
private readonly List<Tile> tiles;
private readonly int maxX;
private readonly int maxY;
private readonly int minX;
private readonly int minY;
public TileUtility(List<Tile> tiles)
{
this.tiles = tiles;
maxX = tiles.Max(t => t.X);
maxY = tiles.Max(t => t.Y);
minX = tiles.Min(t => t.X);
minY = tiles.Min(t => t.Y);
}
public Tile GetTile(Vector2 pos)
{
var p = RoundAwayFromZero(pos);
if (p.x < minX) p.x = minX;
if (p.y < minY) p.y = minY;
if (p.x > maxX) p.x = maxX;
if (p.y > maxY) p.y = maxY;
var tile = tiles.Find(t => t.Position == p);
if (tile == null) throw new Exception(p.ToString() + " tile not found");
return tile;
}
private Vector2Int RoundAwayFromZero(Vector2 v)
{
int x = (int)(v.x + 0.5f);
int y = (int)(v.y + 0.5f);
return new Vector2Int(x, y);
}
}
private List<Tile> CreateTiles(Texture2D waypointsMap)
{
var tiles = new List<Tile>();
for (int y = 0; y < size.y; y++)
{
for (int x = 0; x < size.x + 1; x++)
{
var tile = new Tile(x, y);
tiles.Add(tile);
}
}
var wayrects = mazeUtility.GetRectangles(waypointsMap);
tiles.ForEach(t =>
{
if (wayrects.Exists(w => t.Waypoint == w.Center2 / 8f))
{
t.IsOccupied = false;
}
});
SetWarpPoint(tiles);
foreach (var t in tiles.Where(t => !t.IsOccupied))
{
var up = tiles.Find(t2 => t2.Waypoint == t.Waypoint + Vector2.up);
if (up != null && !up.IsOccupied)
{
t.Up = up;
t.AdjacentCount++;
}
var right = tiles.Find(t2 => t2.Waypoint == t.Waypoint + Vector2.right);
if (right != null && !right.IsOccupied)
{
t.Right = right;
t.AdjacentCount++;
}
var down = tiles.Find(t2 => t2.Waypoint == t.Waypoint + Vector2.down);
if (down != null && !down.IsOccupied)
{
t.Down = down;
t.AdjacentCount++;
}
var left = tiles.Find(t2 => t2.Waypoint == t.Waypoint + Vector2.left);
if (left != null && !left.IsOccupied)
{
t.Left = left;
t.AdjacentCount++;
}
}
tiles.ForEach(t =>
{
if (t.AdjacentCount > 2) t.IsIntersection = true;
});
return tiles;
}
private void SetWarpPoint(List<Tile> tiles)
{
foreach(var w in GetComponentsInChildren<WarpPoint>())
{
var p = w.transform.localPosition;
var tile = new Tile((int)p.x, (int)p.y);
tile.IsOccupied = false;
tiles.Add(tile);
}
}
private MazeUtility mazeUtility;
public TileUtility TileUtility { get; private set; }
private void Awake()
{
mazeUtility = GetComponent<MazeUtility>();
TileUtility = new TileUtility(CreateTiles(waypointsMap));
}
AAIの作成
処理の流れは以下の通りです。
- GetNextTile関数で進行方向にあるTile(NextTile)を取得します。
- NextTileが存在して、かつ交差点でない場合、そのTileの位置をWaypointとします。
- NextTileが存在せず、かつ交差点でない場合、現在のタイルから進めるTileを探して、Waypointとします。
- NextTileが存在せず、かつ交差点の場合ゴーストの状態によって方法が異なります。
- 追跡状態:GetTarget関数で追跡対象のTileを取得し、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。
- 恐慌状態:現在のTileに隣接するTileからランダムでWaypointを決定します。
- 帰還状態:巡回開始地点を目的地とし、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。
- 死亡状態:ゴーストの巣付近を目的地とし、そこまでの直線距離が最も短くなるTileの位置をWaypointとします。
パックマンなどの追跡対象への距離を単純な直線距離で計算しているため、場合によって遠回りしています(マヌケ)。気になる人は距離の求め方を修正するとよいと思います。
public abstract class AAI : MonoBehaviour
{
protected Pacman pacman;
protected TileUtility tileUtility;
public void Initialize(Pacman pacman, TileUtility tileUtility)
{
this.pacman = pacman;
this.tileUtility = tileUtility;
}
public Vector2 GetWaypoint(Vector2 dir, State? state = null, Vector2? tp = null)
{
var currentPos = (Vector2)transform.localPosition;
var currentTile = tileUtility.GetTile(currentPos);
var nextTile = GetNextTile(dir, currentPos);
var waypoint = currentPos;
if (nextTile.IsOccupied || currentTile.IsIntersection)
{
if (nextTile.IsOccupied && !currentTile.IsIntersection)
{
if (dir.x != 0)
{
if (currentTile.Up != null) waypoint = currentTile.Up.Waypoint;
if (currentTile.Down != null) waypoint = currentTile.Down.Waypoint;
if (currentTile.Up == null && currentTile.Down == null)
{
if (dir.x > 0) waypoint = currentTile.Left.Waypoint;
else waypoint = currentTile.Right.Waypoint;
}
}
if (dir.y != 0)
{
if (currentTile.Right != null) waypoint = currentTile.Right.Waypoint;
if (currentTile.Left != null) waypoint = currentTile.Left.Waypoint;
if (currentTile.Right == null && currentTile.Left == null)
{
if (dir.y > 0) waypoint = currentTile.Down.Waypoint;
else waypoint = currentTile.Up.Waypoint;
}
}
}
if (currentTile.IsIntersection)
{
switch (state)
{
case State.Chase:
tp = GetTargetTile().Waypoint;
waypoint = GetWaypointAtIntersection(dir, currentTile, tp.Value);
break;
case State.Scare:
waypoint = GetWaypointAtIntersection(dir, currentTile);
break;
case null:
waypoint = GetWaypointAtIntersection(dir, currentTile, tp.Value);
break;
default:
break;
}
}
return waypoint;
}
else
{
return nextTile.Waypoint;
}
}
protected abstract Tile GetTargetTile();
protected float GetSqrDistance(Vector2 a, Vector2 b)
{
return Mathf.Pow(a.x - b.x, 2) + Mathf.Pow(a.y - b.y, 2);
}
private Tile GetNextTile(Vector2 dir, Vector2 pos)
{
Tile nextTile = null;
if (dir.x > 0)
{
nextTile = tileUtility.GetTile(pos + Vector2.right);
}
if (dir.x < 0)
{
nextTile = tileUtility.GetTile(pos + Vector2.left);
}
if (dir.y > 0)
{
nextTile = tileUtility.GetTile(pos + Vector2.up);
}
if (dir.y < 0)
{
nextTile = tileUtility.GetTile(pos + Vector2.down);
}
return nextTile;
}
private Vector2 GetWaypointAtIntersection(Vector2 dir, Tile currentTile, Vector2 tp)
{
var waypoint = currentTile.Waypoint;
float up, right, down, left;
up = right = down = left = Mathf.Infinity;
if (currentTile.Up != null && !(dir.y < 0))
{
up = GetSqrDistance(tp, currentTile.Up.Waypoint);
}
if (currentTile.Right != null && !(dir.x < 0))
{
right = GetSqrDistance(tp, currentTile.Right.Waypoint);
}
if (currentTile.Down != null && !(dir.y > 0))
{
down = GetSqrDistance(tp, currentTile.Down.Waypoint);
}
if (currentTile.Left != null && !(dir.x > 0))
{
left = GetSqrDistance(tp, currentTile.Left.Waypoint);
}
var min = Mathf.Min(up, right, down, left);
if (min == up) waypoint = currentTile.Up.Waypoint;
else if (min == right) waypoint = currentTile.Right.Waypoint;
else if (min == down) waypoint = currentTile.Down.Waypoint;
else if (min == left) waypoint = currentTile.Left.Waypoint;
if (waypoint == currentTile.Waypoint) throw new Exception("no way");
return waypoint;
}
private Vector2 GetWaypointAtIntersection(Vector2 dir, Tile currentTile)
{
var available = new List<Tile>();
if (currentTile.Up != null && !(dir.y < 0))
{
available.Add(currentTile.Up);
}
if (currentTile.Right != null && !(dir.x < 0))
{
available.Add(currentTile.Right);
}
if (currentTile.Down != null && !(dir.y > 0))
{
available.Add(currentTile.Down);
}
if (currentTile.Left != null && !(dir.x > 0))
{
available.Add(currentTile.Left);
}
var m = available.Count;
var r = UnityEngine.Random.Range(0, m);
return available[r].Waypoint;
}
}
各ゴーストのAIの作成
AAIを継承してゴーストごとのAIを作成します。追跡対象がいるTileを取得するGetTargetTileを実装します。
各ゴーストの追跡時の仕様については下記を参照してください。
nullsuke.com
public class AkabeiAI : AAI
{
protected override Tile GetTargetTile()
{
return tileUtility.GetTile(pacman.transform.localPosition);
}
}
public class PinkyAI : AAI
{
protected override Tile GetTargetTile()
{
var targetPos = (Vector2)pacman.transform.localPosition +
pacman.Direction * 4;
var tile = tileUtility.GetTile(targetPos);
return tile;
}
}
public class AosukeAI : AAI
{
private Akabei akabei;
public void Initialize(Pacman pacman, TileUtility tileUtility,
Akabei akabei)
{
this.akabei = akabei;
base.Initialize(pacman, tileUtility);
}
protected override Tile GetTargetTile()
{
var forwardPos = (Vector2)pacman.transform.localPosition +
pacman.Direction * 2;
var ambushVector = forwardPos -
(Vector2)akabei.transform.localPosition;
var targetPos = forwardPos + ambushVector;
var tile = tileUtility.GetTile(targetPos);
return tile;
}
}
public class GuzutaAI : AAI
{
protected override Tile GetTargetTile()
{
var limit = 64;
var dis = GetSqrDistance(pacman.transform.localPosition,
transform.localPosition);
Vector2 targetPos = Vector2.zero;
if (dis >= limit) targetPos = pacman.transform.localPosition;
return tileUtility.GetTile(targetPos);
}
}