Welcome! Before I get into things, if you don’t already know me, a quick intro: I’m Dave, founder of DIE SOFT, and I do the programming, art, and animation for the upcoming game Little Nemo and the Guardians of Slumberland. Before I dig into all the technical details, let me show and then describe to you exactly what I’m going to be talking about in this post:
Nemo’s Little Buddy, Panda Tuxpin, can be seen here using pathfinding to follow along with the player
I’m going to be breaking down how I got Nemo’s Little Buddies to follow the player around, talking about things from an abstract, high level overview with no code samples and attempting to give a general layout of my solution from end-to-end. That means I’ll be talking about creating a pathfinding graph, executing pathfinding, moving the buddies along the found path, working around pathfinding limitations, and how all these systems tie together.
If you finish reading and are curious about specific implementation details, I’m happy to chat about it! Come find me on our Discord server (we’ve got a #game-dev channel in there and I love to talk about this stuff with people).
Little Nemo and the Guardians of Slumberland is a 2D platformer-centric metroidvania (meaning it plays more like Super Mario Bros., but has a world and upgrades more like Super Metroid), and one of the items you can collect are Little Buddies, small stuffed animals or otherwise anthropomorphized toys who will follow you around the world and provide some sort of passive benefit (for example, Lampet is a light source, B.O.B. detects secrets, and My Alien Monster can eat enemies). This sort of feature is often implemented by allowing the “familiar” to no-clip through terrain and just float near the player, but I wanted these buddies to be extra cute and fun so having them run and jump alongside Nemo felt right.
Familiars in “Symphony of the Night” don’t interact with the terrain and simply float near the player.
The player character in Little Nemo has a variety of mobility options available (increasingly so as they unlock new abilities) and the Little Buddies can only run and jump, so our solution is to have them use pathfinding to follow along with Nemo. So let’s break down what kind of work needs to happen to accomplish this…
As for the technical context of what I’m describing, I’m writing all of this in C# for my Unity project, but because I’m talking about things at a high level of abstraction, you could apply these ideas in other languages and game engines. The important thing to keep in mind here is simply that we’re working in a 2D world which is composed of tiles that conform to a grid. Whether you’re using Unity’s or Godot’s tilemaps to build your world shouldn’t matter.
Our pathfinding solution breaks down into two major areas: building the world graph, and then using that graph to do pathfinding and path following. I’ll quickly outline these steps before getting into the details of them.
The pathfinding algorithm we’re working with is going to need a graph to work from (tldr; for those unfamiliar: a graph is a collection of nodes and edges that connect those nodes together). Essentially what we need to do is codify our understanding of the world’s terrain into a graph that describes how our entities can move through it. This is the hardest and most complex part of this entire solution, so it’s important to appreciate exactly what we’re trying to achieve.
In my case, Slumberland is mostly composed of tilemaps that follow a uniform grid, so it’s easy to get a sense of what nodes we need in our graph: we’ll simply start with assuming every tile in the world will have some corresponding node in the graph. We then also need to connect these nodes with edges that describe how we traverse from one node to another. It will be important that all of our edges are one-way; consider falling for example, which we expect to only work in a downward direction.
A debug view of the world graph overlaid on a small portion of the world terrain. Each node has different colors that convey what kind of node it is, and the lines between them are the color-coded edges.
Despite “pathfinding” being in the title of this post, it’s actually one of the least interesting parts of all of this. We’re using the A* algorithm to do pathfinding, and there are plenty of articles out there describing how to leverage A*, but I’m more interested in discussing how this step gets integrated into the whole process. To use a pathfinding algorithm like this, we need to supply the algorithm with our graph, as well as which nodes from within that graph are our starting and goal nodes. The resulting output from this pathfinding algorithm will be a path (or lack thereof if no valid path from start to goal exists).
The path is essentially just an ordered sequence of edges that your entity can follow to go from node to node, eventually reaching the goal node.
You can see here how Lampet’s path is a collection of edges from the world graph. The crosshairs show which node Lampet is currently heading towards. Green is a walking edge, cyan is a jumping edge, and red is a falling edge.
Once we have a path, we need to tell our entity how to follow it. The implementation details will heavily depend on the structure of your game logic. Generally speaking though, you’ll want to keep track of which segment of the path the entity is currently traversing, describe to the entity how to follow that segment of the path, and then also watch for when we’ve reached the end of that segment and can move on to the next (or if there is no next segment, we’ve reached our goal).
In this context, a path segment roughly corresponds to a graph edge, but will contain other details as well, so it’s good to think of the path segment as its own concept with its own name, rather than just calling it an edge.
My Alien Monster follows along, running, jumping, and passing through platforms as the path dictates.
Before we get into the details of these steps, I think it will be helpful to help anchor your understanding of how the graphs are used by the pathfinding logic with a quick example. This will hopefully better help you understand how nodes and edges are used by the pathfinding logic.
In this example, we’ll just have Panda Tuxpin trying to get to the Star, and we’ll examine each step along the path.
At the start here, things are pretty simple. We’re at a Surface node, and it has a WalkTo edge to the neighboring Surface node to our right. That moves us closer to the goal and walking from one surface to another is the easiest thing to do, so that’s what Panda Tuxpin will do here.
At this node however, things get a little trickier. We can walk to our neighboring Surface node to the right, but it is also a HazardSurface. Since we want to avoid Hazards if possible, this node is fairly expensive to traverse, so let’s keep looking at other edges heading out from this node.
Surface nodes will also have JumpTo nodes to most nearby Surface nodes, and although jumping also has a cost associated with it, it’s less expensive than getting hurt by the hazard. Unfortunately, there is not a JumpTo node that jumps us straight to the Goal because that pillar of Solid tiles is blocking us, but we do have a JumpTo node to the top of that pillar. That’s going to be the best way to get closer to the goal for the lowest cost, so that’s what Panda Tuxpin would do next.
Now that we’re up here, there’s no neighboring Surface node for us to walk to to get closer to the goal, but we do have an edge that allows us to fall directly onto the goal. Falling is relatively inexpensive so that is the sensible next step Panda Tuxpin would take and would reach the goal.
Now that I’ve laid out a general picture of what we’re doing, let’s get into the finer details about each step.
I glossed over quite a bit in describing the graphs above, so I’m going to dig into more detail about each component of the graph.
Now that we understand a bit more about the details of what this graph looks like, let’s dig into how we build that graph.
This is what our sequence will look like as we populate the graph in the steps below.
Query everything that contributes to world geography. In my case, this is all of our tilemaps that make up the world, as well as any collideable prefabs. I made an interface, IGraphNodeProvider, that they all conform to. I can simply query it with the method NodeType NodeTypeAt(int2 gridPos) and that tilemap or prefab is responsible for describing what NodeType is created in each location. So for instance, my collideable tilemaps will respond with NodeType.Solid for any grid position where they have a tile painted in.
We will iterate over every position in the bounds that we want graphed out and ask every IGraphNodeProvider what type of NodeType they provide at that location. This comes back to why it was important that we use a bit field enum for our NodeType. Since we’re querying from multiple sources, we can simply bitwise OR them all together to get our final NodeType (eg. if both the main collider tilemap, and the hazards tilemap have a tile at a specific location, after querying them both, we’ll know that location is both solid and a hazard). At each of these positions create a node using the NodeType we got by combining input from all of our IGraphNodeProviders.
Here is a gizmo showing the node types overlayed on our world. Solid nodes are black PassThru are blue, and Hazards are red. Notice the monkey bar tiles don’t have nodes because the Buddies do not make use of them so they are not relevant to their pathfinding.
All of our nodes describe the contents of the world now, but now we need to do a second pass to add more NodeTypes, based on the nodes that we populated the graph with in the first step. For instance, you will probably want to go through and add NodeType.Surface to any node that is above a node which is NodeType.Solid and which itself is not solid. These secondary nodes will be essential because these will usually be the nodes that our entity actually passes through and is concerned with.
Here you can see the same nodes as above, but with all of the additional nodes we’ve added on top of that such as different Surface types and ledges.
Right now we have nodes for every tile, even the empty ones, so we can simplify our graph by deleting any nodes with the None/Empty NodeType. They can be safely removed because we know they won’t be relevant to pathfinding other than that we know we can pass through them. It also drastically speeds up the time it takes to generate the edges of our graph, and results in fewer edges which also speeds up pathfinding (which is logic that you will definitely need to do at runtime).
This is where you’ll have a lot of very specific logic based on your needs. Generally what I’m trying to do is allow the Buddies to walk horizontally, fall off of ledges, fall through pass through platforms, and jump from any node to any other that we expect they can reach. It’s a good bit of code (about 350 lines of C#), so I will walk through a quick overview of what I’m doing:
While iterating over every node in the graph:
And now finally we have the same context as shown above, but with all of the edges drawn in as well. Each of these lines represents how a Little Buddy could possibly get from one node to another.
I’m glossing over some fairly interesting details here. The desmos graphs I linked above were presented with very little context, so if you wanna know more about how exactly I’m using these formulas to determine if a Buddy can get from one node to another by jumping or falling in my specific use cases, come chat with me in Discord.
How your entities follow a path is going to be very specific to your circumstances, so I think what I want to do is talk about how I handled this for my use case from a high level overview. For each entity that has a path which needs to be followed, we run this logic each frame:
This loop essentially boils down to:
We’ve gone into detail about how we generate the path, use it for pathfinding, and then how to use those paths to move our entities. But that’s not the whole story, we need to tie all these things together. So I’m going to give a quick look at what all that looks like for my Little Buddy pathfinding, and then also some additional things we do to help smooth over failure scenarios for the pathfinding.
Overview of Important ECS Components and Steps We Use
And with that, you have a fairly exhaustive birds-eye-view of what all of the Little Buddy pathfinding looks like from end-to-end. As a quick recap we talked about:
Ultimately I’m really happy with this system. There are a few minor things still I want to clean up, but it’s really fun to have your favorite Little Buddy following along with you as you make your way through Slumberland. It was ultimately more work than I originally anticipated, but that is mostly due to my first approach at a solution having a major design flaw that was ultimately not fixable (I won’t get into that here since this is already so long).
One thing that was a bit surprising was how these Little Buddies remind me of Tails in Sonic 2, even though this wasn’t actually a source of inspiration. If you’re unfamiliar, Sonic 2 introduced the character Tails, who always followed along with you like a faux player 2 of sorts. It’s mostly just superficial, but it’s a really nice touch that adds a bit of fun to the game, much in the same way these Buddies do.
The CPU-controlled Tails character following along with the player in Sonic 2
The Little Buddies were conceived of as a potent upgrade item that players could be excited to collect, while also forcing them to make meaningful choices about which Buddy they currently want to take with them to Slumberland. But I suspect that ultimately these Buddies will end up being more important as a source of flavor rather than gameplay impact thanks to the success of this pathfinding. We shall see if players agree once the game releases.
If you made it all the way through, thank you so much for reading all of this. It was quite a long one, but I wanted to try to tackle the whole end-to-end pathfinding solution because I felt like I had trouble getting examples like this from other developer’s implementations of anything similar, so I hope seeing how I accomplished this will be helpful if you’re ever doing something at all similar. And if you have any feedback about all of this, please let me know. I’m curious if these engineering-centric deep dives are interesting enough that I should do more in the future.
I’ll leave you with this quick clip of B.O.B. following along and then briefly hopping onto Nemo’s back for a short ride
And one final plug for the Discord: if you wanna ask about some of the finer details of all of this (I wrote my own A* implementation for this if you wanna talk about that for instance), feel free to hop into the Little Nemo Discord server where I’m happy to go on about all of this at length if prompted. Thank you! 😊
-Dave