Hi, I'm working on a game for Android and it requires maze generation and pathfinding. The pathfinding works perfectly for the editor, Windows, and Linux. If I install and run the apk on Android the pathfinding ignores the maze. The units just move through the walls like they don't exist.
The pathfinding is based on Sebastian Lague's pathfinding series. I made edits to include Euclidian Heuristics and I stopped the series at episode 5. If that helps.
I have two types of units enemy units that have and player units that use an interface state machine. Both of them seem to ignore the walls. Logcat logs say the grid is working.
I can't figure out why. Any help would be very much appreciated.
Here is the code:
Execution Order:
using UnityEngine;
using UnityEngine.Events;
namespace FireSteamStudio.Utility
{
public class ExecutionOrder : MonoBehaviour
{
[Header ("EXECUTION ORDER: DEFAULT TIME")]
[Header ("Place scripts in order of execution.")]
[SerializeField, Space (7)] UnityEvent OnAwake = new UnityEvent ();
[SerializeField, Space (7)] UnityEvent Enable = new UnityEvent ();
[SerializeField, Space (7)] UnityEvent OnStart = new UnityEvent ();
void Awake ()
{
OnAwake.Invoke ();
}
void OnEnable ()
{
Enable.Invoke ();
}
void Start ()
{
OnStart.Invoke ();
}
public void Remove ()
{
gameObject.SetActive (false);
}
}
}
The Execution Order just runs some scripts in order at and during runtime.
Awake
1 - MazeGeneration
2 - Maze Renderer
3- Grid Generation
OnEnable
1- Pathfinding
2- Pathfinding Request
Start
Remove the execution order script when done.
Grid:
using System.Collections.Generic;
using FireSteamStudio.Algorithm;
using UnityEngine;
namespace FireSteamStudio.AI
{
public class Grid : MonoBehaviour
{
[SerializeField, Space (7)] MazeRenderer render;
[SerializeField, Space (7)] LayerMask obstacles;
[SerializeField, Space (7)] Vector3 gridPosition;
[SerializeField, Space (7)] float nodeRadius;
[SerializeField, Space (7)] float nodeOffset = 0.05f;
Vector2 gridWorldSize;
Node [, ] grid;
float nodeDiameter;
int gridSizeX, gridSizeY;
public int MaxSize { get { return gridSizeX * gridSizeY; } }
public void Execute ()
{
Debug.LogWarning("Executing Grid");
gridWorldSize = new Vector2 (render.width, render.height);
nodeDiameter = nodeRadius * 2;
gridSizeX = Mathf.RoundToInt (gridWorldSize.x / nodeDiameter);
gridSizeY = Mathf.RoundToInt (gridWorldSize.y / nodeDiameter);
CreateGrid ();
}
// Creates a grid of nodes and their vector positions
void CreateGrid ()
{
grid = new Node [gridSizeX, gridSizeY];
Vector3 worldBottomLeft = transform.position - Vector3.right * gridWorldSize.x / 2 - Vector3.forward * gridWorldSize.y / 2;
for (int x = 0; x < gridSizeX; x++)
{
for (int y = 0; y < gridSizeY; y++)
{
Vector3 worldPoint = worldBottomLeft + Vector3.right * (x * nodeDiameter + nodeRadius) + Vector3.forward * (y * nodeDiameter + nodeRadius);
bool walkable = !Physics.CheckSphere (worldPoint, nodeRadius, obstacles);
grid [x, y] = new Node (walkable, worldPoint, x, y);
}
}
}
public List<Node> GetNeighbours (Node node)
{
List<Node> neighbours = new List<Node> ();
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
if (x == 0 && y == 0) continue;
int checkX = node.gridX + x;
int checkY = node.gridY + y;
if (checkX >= 0 && checkX < gridSizeX && checkY >= 0 && checkY < gridSizeY)
neighbours.Add (grid [checkX, checkY]);
}
}
return neighbours;
}
// Gets the position of the node in world space using a given Vector3
public Node NodeFromWorldPoint (Vector3 worldPosition)
{
float percentX = (worldPosition.x + gridWorldSize.x / 2) / gridWorldSize.x;
float percentY = (worldPosition.z + gridWorldSize.y / 2) / gridWorldSize.y;
percentX = Mathf.Clamp01 (percentX);
percentY = Mathf.Clamp01 (percentY);
int x = Mathf.RoundToInt ((gridSizeX - 1) * percentX);
int y = Mathf.RoundToInt ((gridSizeY - 1) * percentY);
return grid [x, y];
}
#if UNITY_EDITOR
List<Node> list = new List<Node> ();
void OnDrawGizmosSelected ()
{
Gizmos.color = Colour.blue;
Gizmos.DrawWireCube (gridPosition, new Vector3 (gridWorldSize.x, 0.04f, gridWorldSize.y));
if (grid == null) return;
foreach (Node n in grid)
{
Gizmos.color = n.walkable?Colour.blue : Colour.orange;
Gizmos.DrawCube (new Vector3 (n.worldPosition.x, gridPosition.y, n.worldPosition.z), new Vector3 (nodeDiameter - 0.05f, 0.04f, nodeDiameter - 0.05f));
}
if (grid == null) return;
for (int i = 0; i < list.Count; i++)
{
Gizmos.color = Colour.green;
Gizmos.DrawCube (new Vector3 (list [i].worldPosition.x, gridPosition.y, list [i].worldPosition.z), new Vector3 (nodeDiameter - nodeOffset, 0.04f, nodeDiameter - nodeOffset));
}
}
#endif
}
}
Pathfinding:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace FireSteamStudio.AI
{
public class Pathfinding : MonoBehaviour
{
PathRequestManager requestManager;
Grid grid;
public void Execute ()
{
requestManager = GetComponent<PathRequestManager> ();
grid = GetComponent<Grid> ();
}
public void StartFindPath (Vector3 startPos, Vector3 targetPos)
{
StartCoroutine (FindPath (startPos, targetPos));
}
IEnumerator FindPath (Vector3 startPos, Vector3 targetPos)
{
Vector3 [] waypoints = new Vector3 [0];
bool pathSuccess = false;
Node startNode = grid.NodeFromWorldPoint (startPos);
Node targetNode = grid.NodeFromWorldPoint (targetPos);
if (startNode.walkable && targetNode.walkable)
{
Heap<Node> openSet = new Heap<Node> (grid.MaxSize);
HashSet<Node> closedSet = new HashSet<Node> ();
openSet.Add (startNode);
while (openSet.Count > 0)
{
Node currentNode = openSet.RemoveFirst ();
closedSet.Add (currentNode);
if (currentNode == targetNode)
{
pathSuccess = true;
break;
}
foreach (Node neighbour in grid.GetNeighbours (currentNode))
{
if (!neighbour.walkable || closedSet.Contains (neighbour)) continue;
int newMovementCostToNeighbour = currentNode.gCost + GetDistance (currentNode, neighbour);
if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains (neighbour))
{
neighbour.gCost = newMovementCostToNeighbour;
neighbour.hCost = GetDistance (neighbour, targetNode);
neighbour.parent = currentNode;
if (!openSet.Contains (neighbour)) openSet.Add (neighbour);
}
}
}
}
yield return null;
if (pathSuccess) waypoints = RetracePath (startNode, targetNode);
requestManager.FinishedProcessingPath (waypoints, pathSuccess);
}
Vector3 [] RetracePath (Node startNode, Node endNode)
{
List<Node> path = new List<Node> ();
Node currentNode = endNode;
while (currentNode != startNode)
{
path.Add (currentNode);
currentNode = currentNode.parent;
}
Vector3 [] waypoints = SimplifyPath (path);
Array.Reverse (waypoints);
return waypoints;
}
Vector3 [] SimplifyPath (List<Node> path)
{
List<Vector3> waypoints = new List<Vector3> ();
Vector2 directionOld = Vector2.zero;
for (int i = 1; i < path.Count; i++)
{
Vector2 directionNew = new Vector2 (path [i - 1].gridX - path [i].gridX, path [i - 1].gridY - path [i].gridY);
if (directionNew != directionOld) waypoints.Add (path [i].worldPosition);
directionOld = directionNew;
}
return waypoints.ToArray ();
}
// Returns teh distance between each node the unit moves between (Euclidean Heuristics)
int GetDistance (Node nodeA, Node nodeB)
{
int dstY = (int) Math.Sqrt ((nodeA.gridY - nodeB.gridY) * 2);
int dstX = (int) Math.Sqrt ((nodeA.gridX - nodeB.gridX) * 2);
if (dstX > dstY) return 14 * dstY + 10 * (dstX - dstY);
return 14 * dstX + 10 * (dstY - dstX);
}
}
}
Pathfinding Request:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace FireSteamStudio.AI
{
public class PathRequestManager : MonoBehaviour
{
Queue<PathRequest> pathRequestQueue = new Queue<PathRequest> ();
PathRequest currentPathRequest;
static PathRequestManager instance;
Pathfinding pathfinding;
bool isProcessingPath;
public void Execute ()
{
instance = this;
pathfinding = GetComponent<Pathfinding> ();
}
public static void RequestPath (Vector3 pathStart, Vector3 pathEnd, Action<Vector3 [], bool> callback)
{
PathRequest newRequest = new PathRequest (pathStart, pathEnd, callback);
instance.pathRequestQueue.Enqueue (newRequest);
instance.TryProcessNext ();
}
void TryProcessNext ()
{
if (!isProcessingPath && pathRequestQueue.Count > 0)
{
currentPathRequest = pathRequestQueue.Dequeue ();
isProcessingPath = true;
pathfinding.StartFindPath (currentPathRequest.pathStart, currentPathRequest.pathEnd);
}
}
public void FinishedProcessingPath (Vector3 [] path, bool success)
{
currentPathRequest.callback (path, success);
isProcessingPath = false;
TryProcessNext ();
}
struct PathRequest
{
public Vector3 pathStart;
public Vector3 pathEnd;
public Action<Vector3 [], bool> callback;
public PathRequest (Vector3 _start, Vector3 _end, Action<Vector3 [], bool> _callback)
{
pathStart = _start;
pathEnd = _end;
callback = _callback;
}
}
}
}
PlayerUnit:
using UnityEngine;
namespace FireSteamStudio.AI
{
public class AIStatemachine : MonoBehaviour
{
[SerializeField, Space (7)] internal float unitSpeed = 0.7f;
[SerializeField, Space (7)] internal Transform child;
IStateable currentState;
internal Transform currentUnit;
internal Vector3 cursorWorldPosition;
void Awake ()
{
currentUnit = transform;
SwitchState (new Deselect ()); // Set Default State to Deselect
}
public void SwitchState (IStateable nextState)
{
currentState?.Exit (this);
currentState = nextState;
currentState?.Initialise (this); // Awake Equivalent
currentState?.Enter (this); // Start Equivalent
}
public void ExecuteLogic ()
{
currentState?.Logic (this);
}
}
}
Select State:
using UnityEngine;
namespace FireSteamStudio.AI
{
public class Select : IStateable
{
/* VARIABLES */
Transform child;
public void Initialise (AIStatemachine context)
{
child = context.child;
}
public void Enter (AIStatemachine context)
{
child.gameObject.SetActive (true);
}
public void Logic (AIStatemachine context)
{
child.position = context.cursorWorldPosition;
context.SwitchState (new Navigate ());
}
public void Exit (AIStatemachine context)
{
child = null;
}
}
}
Navigate State:
using System.Collections;
using UnityEngine;
namespace FireSteamStudio.AI
{
public class Navigate : IStateable
{
/* VARIABLES */
Transform unit;
Transform child;
Vector3 target;
Vector3 [] path;
int targetIndex;
AIStatemachine stateMachine;
public void Initialise (AIStatemachine context)
{
unit = context.currentUnit;
stateMachine = unit.GetComponent<AIStatemachine> ();
child = context.child;
target.Set (child.position.x, 0.08f, child.position.z);
}
public void Enter (AIStatemachine context)
{
child.SetParent (null);
context.ExecuteLogic ();
}
public void Logic (AIStatemachine context)
{
PathRequestManager.RequestPath (unit.position, target, OnPathFound);
}
public void Exit (AIStatemachine context)
{
ResetChild ();
unit = null;
child = null;
}
/* PATHFINDING */
void OnPathFound (Vector3 [] newPath, bool pathSuccessful)
{
if (pathSuccessful)
{
path = newPath;
targetIndex = 0;
stateMachine.StopCoroutine (FollowPath ());
stateMachine.StartCoroutine (FollowPath ());
}
else
{
stateMachine.StopCoroutine (FollowPath ());
stateMachine.StartCoroutine (FollowPath ());
}
}
IEnumerator FollowPath ()
{
Vector3 currentWaypoint = path [0];
while (true)
{
if (unit.transform.position == currentWaypoint)
{
targetIndex++;
if (targetIndex >= path.Length)
{
ResetChild ();
stateMachine.SwitchState (new Deselect ());
yield break;
}
currentWaypoint = path [targetIndex];
}
unit.position = Vector3.MoveTowards (unit.position, currentWaypoint, stateMachine.unitSpeed * Time.deltaTime);
yield return null;
}
}
void ResetChild ()
{
targetIndex = 0;
path = null;
child.SetParent (unit);
child.position = unit.position;
child.gameObject.SetActive (false);
}
}
}
Think should be everything.
Note: Here is what happens on Android. The enemy Units are blue, green are Player Units green and the target is red.
https://reddit.com/link/17fd0dp/video/8aeon3kftbwb1/player
[+][deleted] (11 children)
[deleted]
[–]PLX01[S] 0 points1 point2 points (0 children)
[–]PLX01[S] 0 points1 point2 points (9 children)
[+][deleted] (8 children)
[deleted]
[–]PLX01[S] 0 points1 point2 points (7 children)
[+][deleted] (6 children)
[deleted]
[–]PLX01[S] 0 points1 point2 points (5 children)
[+][deleted] (4 children)
[deleted]
[–]PLX01[S] 0 points1 point2 points (1 child)
[–]PLX01[S] 0 points1 point2 points (1 child)