FINDING MY WAY
Ok. You’ve listened to me rant about this new game idea I’m obsessed about, but talk is cheap. Here come a few things to look at as a reward for sticking through this post so far.
I’ve only had two development sessions on this game so far, and I think they both went well. In both cases, I spent a full day prior to my night-time dev session thinking on and off about how best to implement a feature, and in each session, I’ve been able to successfully implement at least a basically functional version of said feature in only a small amount of time.
The first of these is a simple movement controller.
The Code:
CharacterMovementController.cs:
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 | using UnityEngine; using System.Collections; public class CharacterMovementController : MonoBehaviour { [SerializeField] //How long it should take to rotate or move the camera private float animationTime = 0.35f; [SerializeField] //The amount to move the player by. Should be equal to the size of each level 'tile' private float unitSize = 3f; [SerializeField] //How much to rotate on each rotation (1 == 360?). This shouldn't change unless allowing diagonal movement private float rotationAmount = 0.25f; [SerializeField] //How high off the ground the player sits. private float playerHeight = 1.75f; [SerializeField] //The easeType to use for the iTween animation. private iTween.EaseType easeType = iTween.EaseType.linear; [SerializeField] //This is used to determine appropriate positioning when climbing/descending uneven terrain. private HeightProbe heightProbePrefab; private Hashtable iTweenArgs; private bool canMove = true; //Initialize itween args void Start() { iTweenArgs = new Hashtable(); iTweenArgs["time"] = animationTime; iTweenArgs["oncompletetarget"] = this.gameObject; iTweenArgs["oncompleteparams"] = true; iTweenArgs["oncomplete"] = "ToggleMovement"; iTweenArgs["easetype"] = easeType; } void Update() { /* Uncomment to allow these values to be manipulated via the inspector at runtime for testing * iTweenArgs["time"] = animationTime; * iTweenArgs["easetype"] = easeType; */ //Move or rotate the player based on input. if (canMove) { if (Input.GetKey(KeyCode.W)) { Move(transform.forward); } else if (Input.GetKey(KeyCode.S)) { Move(-transform.forward); } else if (Input.GetKey(KeyCode.D)) { Move(transform.right); } else if (Input.GetKey(KeyCode.A)) { Move(-transform.right); } else if (Input.GetKey(KeyCode.E)) { Rotate(Vector3.up); } else if (Input.GetKey(KeyCode.Q)) { Rotate(Vector3.down); } } } /* Move the player in the appropriate direction. First initialize newPosition based on direction, then create a HeightProbe to determine the appropriate y value for newPosition. This allows easy vertical movement along both even and uneven terrain. HeightProbe destroys itself after FindFloorHeight() is called. */ private void Move(Vector3 directionVector) { Vector3 newPosition = transform.position + directionVector * unitSize; HeightProbe heightProbe = Instantiate(heightProbePrefab, newPosition, Quaternion.identity) as HeightProbe; newPosition.y = heightProbe.FindFloorHeight() + playerHeight; iTweenArgs["position"] = newPosition; ToggleMovement(false); iTween.MoveTo(this.gameObject, iTweenArgs); } private void Rotate(Vector3 rotationVector) { iTweenArgs["amount"] = rotationVector * rotationAmount; ToggleMovement(false); iTween.RotateBy(this.gameObject, iTweenArgs); } private void ToggleMovement(bool allowMovement) { canMove = allowMovement; } } |
HeightProbe.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | using UnityEngine; using System.Collections; public class HeightProbe : MonoBehaviour { [SerializeField] private float destructionDelay = 0.25f; [SerializeField] // This should be set to only detect the floor in the future private LayerMask layerMask; public float FindFloorHeight() { StartCoroutine(SelfDestruct()); RaycastHit hit; Physics.Raycast(transform.position, -transform.up, out hit, Mathf.Infinity, layerMask); if (hit.transform) { return hit.point.y; } else { return Mathf.NegativeInfinity; } } private IEnumerator SelfDestruct() { yield return new WaitForSeconds(destructionDelay); Destroy(gameObject); } } |
Both of these scripts are super simple and will need some updating, but they give a nice final effect:
Basically, when the user gives input, I spawn a HeightProbe in the appropriate neighboring space, which simply finds the height of the floor/ground in that location. The PlayerMovementController will then add playerHeight to this value and use that as the y position to move to. The reason I did it this way, rather than having a set step height was because I wanted the potential to have my player move on uneven terrain while maintaining the grid-based movement. This will be particularly useful for outdoor areas and will give me greater freedom in my level design.
Right now there are no checks to see if the player is trying to move through a wall, and I rely on iTween to do all of the heavy lifting in animating my player’s rotation and translations (who wants to manually code that stuff? Not me). Obviously I’ll have to do some checking for movement through walls. Also, based on what I’ve read, iTween is pretty inefficient (I do love working with it though), so I think I’ll have to rework the code to use DoTween instead (also free, but apparently performs way better).
Also, I might get rid of the HeightProbe class altogether and put the logic right inside the PlayerMovementController.
Lastly, I don’t like having my input hardcoded, so that’ll have to change. I’ll also want a bit of a mouselook functionality so the player can observe the environment more fully.
And yes, that was a baby dragon :wink:.
What I did in my next development session was implement a bit of pathfinding. Like I said, I thought about this all day, and I came up with my own algorithm. I have no idea what you would call it technically, but in demonstrations I’ve seen, it performs similarly to a concurrent Dijkstra implementation. e is an enemy, p is the player:
I’ve never done any pathfinding before (aside from using Unity’s built in AI stuff), so this is a first for me. And I think I did pretty good coming up with a solution completely by myself. I won’t need a ton of pathfinding in the game (since most enemies will probably only move toward the player if they can see him, or move to a last known location), but I thought this would be fun, and it was.
Not to mention I really like watching my code solve a maze :P. There’s something hypnotizing about these gifs.
The Code:
Pathfinder.cs:
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 | using UnityEngine; using System.Collections; using System.Collections.Generic; public class Pathfinder : MonoBehaviour { [SerializeField] private Color color = Color.red; [SerializeField] PathProbe pathProbePrefab; [SerializeField] private float unitSize = 3f; public float UnitSize { get { return unitSize; } } [SerializeField] private LayerMask layerMask; [SerializeField] private Vector3[] directions; public Vector3[] Directions { get { return directions; } } private List probedCells = new List(); public List ProbedCells { get { return probedCells; } } private List pathToTarget = new List(); public List PathToTarget { get { return pathToTarget; } set { pathToTarget = value; foreach (PathProbe pathProbe in GameObject.FindObjectsOfType()) { if (pathProbe.Master == this) { Destroy(pathProbe.gameObject); } } } } void Start() { InitiatePathFinding(); } private void InitiatePathFinding() { probedCells.Clear(); pathToTarget.Clear(); probedCells.Add(transform.position); foreach (Vector3 direction in directions) { CreateProbeAtNeighboringCell(transform.position, direction); } } void Update() { if (Input.GetKeyDown(KeyCode.F)) { InitiatePathFinding(); } } public PathProbe CreateProbeAtNeighboringCell(Vector3 position, Vector3 direction) { Vector3 cellPosition = position + (direction * unitSize); if (!probedCells.Contains(cellPosition)) { RaycastHit hit; Physics.Raycast(position, direction, out hit, unitSize/*, layerMask*/); Debug.DrawRay(position, direction * unitSize, color); if (!hit.transform) { PathProbe newPathProbe = Instantiate(pathProbePrefab, cellPosition, Quaternion.identity) as PathProbe; newPathProbe.Master = this; newPathProbe.Color = color; probedCells.Add(cellPosition); return newPathProbe; } } return null; } } |
PathProbe.cs:
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 | using UnityEngine; using System.Collections; using System.Collections.Generic; public class PathProbe : MonoBehaviour { private Pathfinder master; public Pathfinder Master { get { return master; } set { master = value; StartCoroutine(PathFind()); } } private Color color = Color.red; public Color Color { set { color = value; } } private List pathPoints = new List(); public List PathPoints { set { pathPoints = value; pathPoints.Add(transform.position); } } private List myProbes = new List(); private IEnumerator PathFind() { if (master) { yield return new WaitForFixedUpdate(); if (master.PathToTarget.Count <= 0) { foreach (Vector3 direction in master.Directions) { PathProbe newPathProbe = master.CreateProbeAtNeighboringCell(transform.position, direction); myProbes.Add(newPathProbe); if (newPathProbe) { newPathProbe.PathPoints = this.pathPoints; } else { RaycastHit hit; Physics.Raycast(transform.position, direction, out hit, master.UnitSize); //Debug.DrawRay(transform.position, direction * master.UnitSize, Color.green); if (hit.transform && hit.transform.tag == "Player") { master.PathToTarget = pathPoints; Debug.Log("Path to player found in " + Time.time + " seconds!"); foreach (PathProbe probe in GameObject.FindObjectsOfType()) { Destroy(probe.gameObject); } } } } } Destroy(this.gameObject); } } } |
Basically, I raycast in all four directions from the enemy’s position, at a distance equal to my grid size. If my rays don’t hit anything at those locations, I spawn a PathProbe at the appropriate place and repeat, adding each cell from which rays were cast to a probedCells list, and omitting those cells from future checks. Because each probe performs iterations at the same time as all of the others, the first path to reach the player is always the shortest, so I didn’t even need to take that into consideration, and as soon as any path is found, I can stop the search.
Currently, my coroutine’s yield is no good and it makes the whole process take a relatively long time (waiting for a new frame before performing another iteration). Essentially, only a single set of cells for each probe can be checked per frame, which can become a problem, particularly in long narrow paths. I need to allow a greater number of checks before yielding to speed up the whole process, but I also have to be careful not to overload it or things tend to crash.
Currently, both of the searches you see above take about 0.75 seconds each. Again, this has nothing to do with complexity, and more to do with how my coroutine operates. In fact, having multiple paths calculated at the same time has had zero impact on frame rate or calculation time so far (though I’ve only tested 3 concurrent paths to date).
However, I really need to rework this as .75 seconds is not acceptable. Presumably, to be safe, I’ll have to recalculate the path each time the enemy moves to a new cell, just in case something has blocked the path or the player has moved. But like I said, I don’t think I’ll need a lot of pathfinding. I think most of it will be linear movement along a line of sight.
Again, I might remove my logic from PathProbe and put it right in Pathfinder to avoid Instantiate calls.
I was also considering using Unity’s NavMeshAgent to calculate a path, and then writing some code to normalize that path to align with my grid (I assume Unity’s pathfinding would be more efficient than anything I’ll write). Another approach I’m considering is generating an array based on my level when it loads, and then traversing the array based on whether the given index is walkable or not. This would save on raycasting at least.
Anyhow, that’s it for now!
Stay tuned for more, including the launch of an official website for Fidelum Games!