Movement and Scatter
Unlike prior labs, you will only be working on a single class (GhostAI
) for this entire lab. Look at GhostAI.h and GhostAI.cpp to see what the starting code provides.
Notably, there is an enum
for the AI’s state, and member variables for the following path nodes:
mTargetNode
is the goal that the ghost is tracking towards currentlymPrevNode
is the last node the ghost intersected with prior to the current positionmNextNode
is the next node the ghost is currently travelling towards
There’s also a debug drawing function that is used to visualize the current move the ghost is attempting.
Note that GhostAI.h does not have all the member data and functions you’ll need. These instructions mostly will not tell you when we think you should make a new function or new variables. It’s up to you to figure out when it makes sense to split things up into functions.
A well-implemented version of the GhostAI
should involve adding several new helper functions and several new pieces of member data. If you try to just put everything into Update
, you’re going to end up with messy code that’s difficult to debug.
As discussed in lecture, ghosts in Pac-Man have four different states:
- Scatter – The ghost paths from its current position to its home node (its “scatter node”). If it reaches the scatter node, it will continue circling around the scatter node until the state changes.
- Chase – The ghost paths to a designated target node, which is typically somehow relative to Pac-Man’s position. The four different ghosts each have different target node behavior.
- Frightened – The ghost turns blue and picks a random node to turn to at every intersection
- Dead – The ghost turns into eyes and paths back to the home pen area, at which point it will come back to life.
One rule of the ghost movement is they usually aren’t allowed to turn around. For example, if a ghost was at node #1 and then travels to node #2, it can’t decide to just turn around again and head back to node #1. At each intersection, the ghost must either go forward, turn left, or turn right. (There is one exception to this rule – when a ghost initially becomes frightened, it will reverse direction).
The way the actual movement works for the different states is the same. Every time the ghost reaches an intersection, it will pick the neighboring node (other than the previous node) which is closest to its goal (or mTargetNode
). The only thing that’s different is each state has different logic for selecting the goal. This means that once you get Scatter working correctly, the other states are significantly less work to implement.
GhostAI::Start
For now, the Start
function needs to:
- Set the position of the owner to the position of
startNode
- Set
mState
toScatter
- Set
mPrevNode
tonullptr
- Set
mNextNode
tostartNode
Deciding on the Next Move
At a high level, the logic of moving the ghost is that every frame Update
will move the ghost little bit in the direction towards the next node. Eventually, the ghost will Intersect
with the next node, which means they’ve reached it. At this point, the logic needs to decide where the ghost should go next. Once it selects where to go, you’ll update mPrevNode
and mNextNode
to represent this next move.
In Update
, add a check to see whether the ghost intersects with the next node. If they do, you will need to:
- Set the ghost’s position to
mNextNode
’s position - Check if you need to change the state (for now, you won’t need this function as you’re only doing the Scatter state)
- Update the target node as needed
- Update
mPrevNode
andmNextNode
for the next move the ghost will make
We strongly recommend making steps #3 and #4 separate functions to keep your logic clean.
For step #3, for now, only worry about the Scatter state. In the Scatter state, the mTargetNode
should always be mGhost->GetScatterNode()
.
For step #4, here are some tips:
- Keep in mind that you will call this function when the ghost is currently at
mNextNode
- You want to select a node that is adjacent to
mNextNode
which is closest tomTargetNode
, while accounting for the fact that you aren’t allowed to pick the following nodes:mPrevNode
- Any
PathNode
withGetType()
ofPathNode::Ghost
- Any
PathNode
withGetType()
ofPathNode::Tunnel
- Remember
PathNode
class has astd::vector
of adjacent nodes calledmAdjacent
- Once you select the node you want the ghost to travel to, you now need to update
mPrevNode
andmNextNode
to be correct for the next move:mPrevNode
becomes what was previouslymNextNode
mNextNode
becomes the node that you selected for the next move
Because you’ve set it up in GhostAI::Start
that mNextNode
is the current position of the ghost, this means that the very first time Update
gets called, it will trigger the logic that the ghost is at “next node.” This should mean that the code for steps 1-4 above will trigger. If done properly, the red ghost should have a target node of the top left corner (represented by a square), and you should see a line from the red ghost to the node to the left of it, which looks like this:
Moving the Ghost
To move the ghost, first add a member variable Vector2
to track the current direction of movement of the ghost. Then, add a member function that calculates the direction as the vector from mPrevNode
to mNextNode
.
Then, after you update mPrevNode
/mNextNode
(in the step #4 function from the previous section), call your function to update the direction.
Finally, at the start of Update
, before the intersection test, update the ghost’s position based on the movement direction, speed, and delta time. The speed is:
- 90 pixels/second in Scatter and Chase
- 65 pixels/second in Frightened
- 125 pixels/second in Dead
If everything is correct, you should now have the red ghost move towards the node to its left. When it reaches that node, it’ll continue on to the subsequent moves until it reaches the top left corner, at which point the ghost will circle the node. It should look like this:
You may notice that the route Blinky chooses to the corner isn’t the optimal path. The path would be shorter if the first move had Blinky move up instead of to the left. This is because the ghost is making a greedy decision at each intersection rather than calculating the full optimal path in advance. However, this behavior replicates the original Pac-Man game which does not guarantee the ghosts make optimal pathing decisions.
Enabling the Second Ghost
To enable the second ghost, in Game.h change the GHOST_COUNT
const to 2.
You should now also have the pink ghost (Pinky) travel towards the top right corner and circle around it:
Changing Animations
There are four different ghost animations for basic movement: "up"
, "down"
, "right"
, and "left"
. These animations just change it so the ghost’s eyes face towards the direction they’re moving in.
Make it so the ghost uses the correct animation based on their movement direction. You can just change the animation at the end of your function that calculates the correct movement direction.
Verify the animations switch properly as the ghosts change directions:
Enabling the Third and Fourth Ghost
To enable the third and fourth ghost, change GHOST_COUNT
in Game.h to 4. If you run this, your game will crash.
Tracing up the call stack and looking at the local variables, can you figure out why it’s crashing? Answer:
The reason this happens is because when selecting the node for the next move, we disallow path nodes with type PathNode::Ghost
or PathNode::Tunnel
. However, in the case of the third and fourth ghosts, they begin inside the ghost “pen” where the only neighboring nodes are ghost nodes. Thus, no valid node is found for the next move which leads to this problem.
To fix this issue, after the first attempt to select the node for the next move, do a check to see if the selected node is null. If it is, then try to select the best node for the next move again, but this time allow nodes of type PathNode::Ghost
. This means that instead of disallowing ghost nodes entirely, we first try our best to select a non-ghost node for the next move, and only if none of those are available do we allow picking a ghost node.
After this, you need to check if the selected node is still null, which can happen in one specific scenario when the orange ghost dies. In this third and final attempt to select the next move, allow it to pick from any neighbor (even the previous node).
Confirm that all four ghosts now path to and circle around their respective corners:
Once you’ve pushed this code, you’re ready to move on to part 2.