Moving objects and a sort of brain - February 02, 2019 by Steve
When we left off in Part 2, we had successfully generated a maze. Now it's time to populate it. We'll want to put in a representation of the player. The player will need to run around and pick up offerings, and take them to the altar. Then the Minotaur will also be in the maze, wandering around. Three of these items are pretty simple to set up, the player, the offerings, and the altar. The Minotaur will need some smarts in order to navigate the maze.
Since I was planning on adding in better graphics later in the weekend, I started with using some of the built in sprites to represent the objects. A green circle for the player, orange hexagon for the Minotaur, blue triangle for the offerings, and a yellow diamond for the altar. I could always change the sprites used later, and in fact could even animate the sprites using the built in tools from Unity.
I started with the player object, with my green circle sprite in hand, I attached it to a game object. Then created
the ubiquitous PlayerController script which would handle movement. A Rigidbody2D component attached to the player
object would provide the physics for movement, and a CircleCollider2D would let the player bump into walls and other things.
The script for movement itself is pretty simple and is the entirety of the FixedUpdate() method:
void FixedUpdate()
{
if (!GameController.instance.isWaitingForNewLevel())
{
float moveHorizontal = Input.GetAxis("Horizontal");
float moveVertical = Input.GetAxis("Vertical");
Vector2 movement = new Vector2(moveHorizontal, moveVertical);
rb2d.AddForce(movement * speed);
}
}
I simply grab the horizontal and vertical inputs, create a Vector2 scaled by a speed value, and add it as a force
to the attached RigidBody2D component. In the Start() method, I grab a reference to the RigidBody2D and store it in
the rb2d variable for ease of use/performance reasons.
That's it. The player can now move around the maze, with walls stopping the player. The GameController uses a PlacePlayer() method
in the MazeController to place the player at the starting position for the maze every time we start a new maze. The only MazeController
that's been implemented just puts the player in one corner of the altar room that gets carved out of the center of the maze.
Before tackling the Minotaur, and its need for a brain, I implemented the altar, and the offerings. This let me make a very easy game of "run around the maze and collect things" that had no limitations on doing so.
The offering is a game object with our blue triangle sprite and a CircleCollider2D set as a trigger so our player can run into it and
pick it up. The Offering MonoBehavior on the object is one of the simplest in the game. It has two values that can be set in the editor,
an integer value, and a boolean flag moving. The flag never gets used, but the idea was eventually I could add in different types
of offerings, such as one that might also wander slowly around the maze. (Think a chicken or goat or something...) The
offerings are generated over time and placed in the maze by the GameController which keeps track of the offerings,
and limiting how many are available at any time. To generate the offering, we make the offering a prefab, and the GameControler
instantiates an instance of the offering and places it in an open space in the maze via the MazeController. This is why when
we walked the maze in Part 2, we kept a list of the open spaces. Now we can just pick randomly from
that list.
The script for the offering implements OnTriggerEnter2D() which checks if the thing that triggered it is the player, and if so
makes the player pick it up if they are not carrying another offering. I decided that it would be more challenging if the player could
only pick up one offering at a time. The script looks like this:
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player")) // we hit an offering, so pick it up if we can
{
PlayerController pc = other.GetComponent<PlayerController>();
if (!pc.handsFull)
{
pc.pickup(this);
}
}
}
The player script takes a note of the value, turns on an indicator (a child object that is a smaller version of the blue triangle centered on the player)
and flashes an alert telling the player to take the offering to the altar. It then destroys the offering.
// in PlayerController
public bool pickup(Offering offering)
{
if (!handsFull)
{
handsFull = true;
carriedValue = offering.value;
offeringIndicator.color = offering.GetComponentInParent<SpriteRenderer>().color;
offeringIndicator.enabled = true;
Destroy(offering.gameObject);
showAlert();
return true;
}
else
{
return false;
}
}
The method returns a true/false because I was just going to blindly call it and do something with the return, but
decided to just have the method trigger the Destroy itself.
The OnDestroy() method for the offering does one thing before being destroyed, it tells the GameController to remove
this offering from those available in the maze, which in turn starts the timer for generating a new offering to replace it.
Now that the player can pick up an offering, we need to make the altar so the player can drop it off. The altar is set up the
same way as an offering, with a CircleCollider2D as a trigger, and a single script attached. Two different objects can
run into the altar, the player or the minotaur, so when the collider is triggered we look to see which it is. At the moment
we only have a player, so we ask the PlayerController if the player is holding something (via the handsFull flag), and if so
we add the carriedValue to the altar's value, set a flag to indicate there are offerings at the altar,
update some UI text to indicate how many offerings are at the altar, and reset the player so it isn't carrying anything anymore.
If the Minotaur triggers the altar, we add the value of the offerings at the altar to the player's points (via the GameController),
reset the altar's value to zero, and set the flag indicating there is an offering at the altar to false. One thing the script does is
check to see if the activeState of the Minotaur is MinotaurController.Activity.Eat before adding in the points/clearing the altar.
The Minotaur's brain is implemented as a state machine, and if it isn't eating, we don't want the altar to do anything when the Minotaur runs
past. (More on that in the next section.)
The Minotaur is the star of the game, and the most complex piece. The supposed intelligence of the game can be broken down into two pieces. One being the 'simple' ability of the Minotaur to move from Point A to Point B in the maze without getting stuck on a wall. The other being what determines the Point B which is controlled by a state machine. Basically the Minotaur has a fixed number of 'states of mind':
Let's take a look at the first problem of navigating the maze. Unity has a navigation system built in but the version I was using didn't do so well for 2D games. I should probably revisit Unity's built in system, but for the Ludum Dare, I went with the A* Pathfinding Project. I had never used it before, and there is a bit of a learning curve. I was able to get something working, but I feel like I'm either doing something wrong, or just need to tune things more.
That basic idea of the A* Pathfinding Project is that it will generate a graph of the maze (in our case) and given a 'target object' other objects will find a path that avoids walls using the A* algorithm. The A* algorithm, isn't hard to code up, and I briefly thought about just implementing it myself. The difficult part is integrating it with Unity, and the 2D meshes, and the objects, etc. So I made the wise decision to let someone else do the heavy lifting so to speak.
The steps I took to get the A* Pathfinding Project to work for me were:
First we create an empty GameObject and add the Astar Path script to it. This component has all the settings for the
A* system on it and looks like:

Then every time we generate a new maze, we simply call the following after the maze is generated:
AstarPath.active.UpdateGraphs(mazeController.wallHolder.GetComponent<CompositeCollider2D>().bounds);
AstarPath.active.FlushGraphUpdates();
AstarPath.active.Scan();
In my implementation, the Awake() event in the GameController calls the refreshMap() method which contains the
UpdateGraphs() and FlushGraphUpdates(), and then calls the Scan() method. Subsequent levels only end up calling the
first two calls, and not the Scan(). I'm not 100% sure this is the correct way of doing things, but it seems to work.
This tells the AstarPath system to update its graphs with the bounds of the collider on the wall holder. Remember that
while each wall tile has a BoxCollider2D on it, the wallHolder object has a CompositeCollider2D which combines them all
into one. So we're passing in the boundaries of the full maze, not the actual collider itself. Internally, the AstarPath is finding
all the walls and generating the graph representation of the maze.
Now all that we need to do is add a few Astar scripts to the Minotaur, and set a target for the path finding.

There are four components that I've added.
The AI Path Component is the main movement script. It calculates a path from the Minotaur
to the target, and moves the Minotaur along that path. The AI Destination Setter sets the target for the AI Path to a game object.
The Funnel Modifier simplifies the path a bit. The Funnel Modifier may also be why
the Minotaur sometimes gets snagged on a corner. Then the Seeker is what ties everything together,
handling the path finding calls, and modifiers, etc.
After adding these to the Minotaur, we can simply set a GameObject placed in the maze (the same way we place offerings) as the target,
and the Minotaur will go. In the Update() method we can check to see if we got to the target, and set a new one as needed
// in Start()
destinationSetter = GetComponent<AIDestinationSetter>();
ai = GetComponent<IAstarAI>();
WanderTarget = new GameObject();
WanderTarget.transform.position = maze.getRandomMazePoint();
destinationSetter.target = WanderTarget.transform;
// in Update()
if(!ai.pathPending && (ai.reachedDestination || !ai.hasPath)))
{
WanderTarget.transform.position = maze.getRandomMazePoint();
destinationSetter.target = WanderTarget.transform;
}
Now that the Minotaur is wandering, we can do some interesting things. I'll save the state machine for the next post,
but here is a simplified version of things. First we create an enum for the various states the Minotaur can be in, and
when the game starts we set set an activeState variable to Wander. We can easily set up a collider detection so that if the Minotaur hits the player
we end the game. When the Minotaur hits the altar and is in the Wander state, he doesn't do anything, just keeps moving on.
In the Minotaur's Update(), we can use a switch statement based on activeState. If the Minotaur is in Wander, we do as before randomly continue wandering. If not,
at this point, we just don't do anything in the Update() as it's all handled by the path finding and colliders. We check
if the Minotaur shouldEat() and if the player has put an offering on the altar, the Minotaur's activeState gets set to Eat and the path finding target is set to the altar.
When the trigger collider on the altar is tripped, we do the points thing, and set the Minotaur back to Wander, which on the next Update()
will set a new wander target for the path finding.
So here's a simplified version of the Minotaur controller:
using UnityEngine;
using Pathfinding;
public class MinotaurController : MonoBehaviour {
public enum Activity
{
Waiting,
Wander,
Eat
}
public Activity activeState = Activity.Waiting;
public GameObject WanderTarget;
public GameObject altar;
public GameObject player;
private AltarController _altarController;
public MazeController maze;
private AIDestinationSetter destinationSetter;
private IAstarAI ai;
private float baseSpeed = 0.5f;
void Start ()
{
destinationSetter = GetComponent<AIDestinationSetter>();
ai = GetComponent<IAstarAI>();
ai.maxSpeed = baseSpeed;
WanderTarget = new GameObject();
WanderTarget.transform.position = transform.position;
destinationSetter.target = WanderTarget.transform;
}
private bool shouldEat(Activity currentState)
{
if (_altarController.hasOffering)
{
if (currentState != Activity.Eat)
{
Debug.Log("Nom Nom Nom!");
activeState = Activity.Eat;
destinationSetter.target = altar.transform;
}
return true;
}
return false;
}
void Update()
{
switch (activeState)
{
case Activity.Waiting: // We're still initializing, once everything is set, we'll start wandering.
if (altar && player && maze && !GameController.instance.isWaitingForNewLevel() && GameController.instance.isRunning())
{
_altarController = altar.GetComponent<AltarController>();
activeState = Activity.Wander;
}
break;
case Activity.Wander:
if (!shouldEat(activeState) &&
(!ai.pathPending && (ai.reachedDestination || !ai.hasPath)))
{
WanderTarget.transform.position = maze.getRandomMazePoint();
destinationSetter.target = WanderTarget.transform;
}
break;
case Activity.Eat:
if (!shouldEat(activeState))
{
// If we've finished eating (i.e. got to the altar
// and removed offerings.... go back to Wander
activeState = Activity.Wander;
WanderTarget.transform.position = maze.getRandomMazePoint();
destinationSetter.target = WanderTarget.transform;
}
break;
}
And with that the Minotaur wanders the maze and eats offerings left at the altar. The game at this point is 'playable' although it isn't very challenging. But at this point, if you don't run into the Minotaur, you get points for every offering the Minotaur eats off the altar.
In Part 4 I'll give the Minotaur more than two states of mind, and add some additional challenges as well as actual game levels and an ending.