Skip to main content Link Search Menu Expand Document (external link)

Theme: auto

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 currently
  • mPrevNode is the last node the ghost intersected with prior to the current position
  • mNextNode 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 to Scatter
  • Set mPrevNode to nullptr
  • Set mNextNode to startNode

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:

  1. Set the ghost’s position to mNextNode’s position
  2. Check if you need to change the state (for now, you won’t need this function as you’re only doing the Scatter state)
  3. Update the target node as needed
  4. Update mPrevNode and mNextNode 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 to mTargetNode, while accounting for the fact that you aren’t allowed to pick the following nodes:
    • mPrevNode
    • Any PathNode with GetType() of PathNode::Ghost
    • Any PathNode with GetType() of PathNode::Tunnel
  • Remember PathNode class has a std::vector of adjacent nodes called mAdjacent
  • Once you select the node you want the ghost to travel to, you now need to update mPrevNode and mNextNode to be correct for the next move:
    • mPrevNode becomes what was previously mNextNode
    • 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:Red ghost picks node to the left

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:Two ghosts scattering

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:Ghost movement animations

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.