Tag: Tile Based

RPG Game Dev Weekly #2

Well, so far not so great for getting weekly posts on this! It’s probably better for me to aim to do these periodically and summarize the last week or so of work. Fewer commitments that way and it’s a bit more realistic for me to achieve. With that said, let’s dive into it!

Entity Filtering in an RPG

Our RPG has had some notion of entity filtering for a long time, but to understand the current state of filtering, it’s important to understand the two major sets of entities and components we have:

  • Game Objects + Behaviors: Everything in our RPG game world is represented as a “game object” and the properties/capabilities are captured by components called “behaviors”
  • Definitions + Generator Components: The content for our game is represented by “definitions” and the “generator components” are how we transform this information into “game objects” and “behaviors”

Because I heavily focused on our loot generation system early on, the focus for filtering was really around being able to select loot from “drop tables” (which have item definitions on them and associated with enchantment definitions). Early on it was a goal to ensure that loot generation and other game systems could have extremely complex situations handled, so we needed to be able to do things like generate loot given arbitrary game state. Additionally, this system needed to be fully extensible to support any game state we wanted to add in the future.

In what might seem to be an extremely contrived example, assume we want to generate item X when:

  • The player is level 50+
  • The current weather on the map is raining
  • The current time of day in the game world is night
  • The player has completed quest A, B, and C
  • The player is holding item Y
  • We are generating loot for killing enemy N

These conditions represent a filter that we need to execute, but I never realized until recently that the implementation of this is actually something I’ve started calling “bidirectional filtering”. This means that we’re running a query with a filter over a set of drop tables, but we provide along some state and the drop tables are filtering backwards on this state. Sound complicated? It was, especially before explicitly thinking of it as bidirectional filtering!

It can be more easily digested when thinking of it this way:

Provided Game State:

  • Current weather
  • Current time of day
  • Player (level and equipment in this example)
  • Quest status

Required Filter from Game:

  • Drop table ID matching enemy N

Using the above information, we select all drop tables matching the required filter from the game. Essentially, any drop table ID for enemy N. For each of the drop tables that match this specific ID, we then run THEIR filters the opposite direction on the game state! If a drop table had no specific filters on it, it would match right away. However, if it had a weather filter on it, it would need to match the provided game state weather.

Okay so that’s bidirectional filtering in our RPG, but this is what ALREADY existed! So what did we need? We actually just needed single direction filtering. Literally just standard querying of game objects. Half the battle of what was already in place!

If you’ve played games like Path of Exile or even Last Epoch, you’ll see indicators in the game that there are tags associated with different things (like skills, items, enchantments). If in our game we want to be able to query all items in a player’s inventory that are of type “axe” or all skills associated with “fire”, we need some way to filter our entities. And in this case, there’s no need for the entities to filter backwards like in bidirectional filtering!

We expanded our filtering system to support this now, which unlocks:

  • Skills that require certain item types
  • Skill prerequisites that aren’t just a skill ID
  • Advanced skill targeting
  • Enchantments that boost skill stats (damage, levels, etc…) that match a particular filter
  • Crafting / Alchemy (i.e. filtering on ingredients)
  • Item socket patterns (this was already in game, but we can do this declaratively instead of writing custom-coded plugins)
  • Restricting applying poisons or consumables to certain item types
  • … this list goes on and on!

RPG Movement Overhaul

This block of work is what chewed up most of my RPG development time the past few days. I was frustrated with my initial and seemingly failed implementation of A* for path finding. For anyone that’s gone through popular computer science algorithms, A* is a pretty straight forward algorithm for finding shortest possible paths. It’s really popular in games because it’s extremely efficient! But my take on it just wasn’t cutting it for our RPG, and it would often report that no paths were possible (which just wasn’t true)!

Our RPG has an explore mode and a combat mode, sort of like Pokemon. However, the combat is tactical, so it’s sort of like Final Fantasy Tactics with respect to that. Being a 2D tile-based game, having tile-restricted movement makes a lot of sense. However, early on I felt that having free-form movement in the explore mode felt really fluid and fun. There was no great reason our explore mode needed to be tile restricted. However, making this movement system decision complicated all of our tile-based movement by trying to re-use the same code.

So I did a thing that I find feels counter-intuitive as a programmer normally… I duplicated a bunch of code, one for supporting tile-based movement and the other for freeform. No more “common code” for this. From here, I found I could actually start correcting each movement system and tailoring it to do the right thing. I was hitting this blockage before because these really were two fully separate implementations of class that can support movement.

From there, I worked through my path-finding issues. I built out some visuals that can determine where the player can move, and used this to validate what I believed I was seeing in my unit tests. 2D stuff is easier to understand visually! The following is an example that shows a snag that I hit where our logic to check “can we move to this position” was incorrectly calculating the distance to that point. If the player has only 3 moves to make, why does it think it can get to the tile labeled 4?

The answer? It was doing birds-eye distance checks back to where the player was standing. Not going to work for our RPG! Instead, I was able to re-use part of the A* algorithm to calculate the distance as the algorithm progresses through neighboring nodes. I also needed to update A* to report the total distance in addition to the positions on the path. The result was consistent calculations between our valid walk points and the actual pathfinding algorithm. The next snapshot shows a small update to the visuals:

And at this point, it was really just about showing/hiding/updating the valid walk highlighting at the right times! So I figured I’d whip up a little video to show progress on that:

What’s Next In Our RPG?

We’ve been making some awesome progress on our RPG. We’re starting to look into random dungeon generation, and I’ll probably try to find something more architectural to tackle. I find I burn out fast when I spend too long on things that feel strictly visual or like I’m polishing things. In this case, the movement part of combat is critical so it needed to get done. But reflecting on all of the changes there was nothing fundamentally changed in our game to accomplish this.

Additionally, I’m going to start doing a better job of adding more content. I’ll be adding in item affixes for item generation, prefixes/suffixes item name creation, and ideas for items that can drop. I don’t think we fully have the components required to define all the items I’d like to make, so I’ll start just by recording my thoughts in a consistent format.

Overall, we’re moving in the right direction! But there’s no denying making an RPG is a ton of work!


TileMap – How To Get ALL The Tiles

If you’re building a 2D game in Unity3D, odds are you’ve come across the TileMap component. The TileMap is a powerful tool that allows you to create a grid of tiles that you can render your tiles with instead of hand-placing individual game objects with sprites. It has a host of built in functionality that you might otherwise find yourself manually writing, like mapping coordinates to particular cells on a map. And what’s even cooler about using a TileMap? You don’t need to handroll your own editor to paint tiles! I think I’d pass on having to do that. But have you found yourself in a situation where you want to get all of the painted tiles on a TileMap? You may have found it’s not quite as obvious as you’d have hoped!

What We Have To Work With On A TileMap

As I mentioned, a TileMap can do a lot for you. In particular, I found myself wanting to get all of the cells that have a Sprite associated with them and what that Sprite actually is. We can use the built in method GetSprite for this:

var cellPosition = new Vector3Int(x, y, 0);
var sprite = tilemap.GetSprite(cellPosition);

Simple enough. But how do we know which coordinates to use for x and y? Is it good enough to just go from 0 to a really high number on the x and y axes with two loops and hope it’s good enough? We can do better than that! The TileMap class has a handy property on it called cellBounds. This gives us an integer rectangle that defines the bounds of all of the TileMap cells that have been modified:

tilemap.cellBounds

If you have found you’re doing a lot of editing on a TileMap, you may be erasing sections to focus on another area. If that’s common, you may benefit from calling CompressBounds() to shrink this back down to the currently assigned cells:

tilemap.CompressBounds()

And with all of these, we can approach how we might tie them all together to get what we need!

Be Careful With TileMap.cellBounds And Starting From Zero!

If you have a keen eye, you’ve probably realized that we want to use two loops to go through the cell bounds on the x and y axes to get the cells we’re interested in on the TileMap. You’re spot on! But there’s something we should be careful about here!

It’s an easy mistake to make because it’s how we commonly write loops:

for (int x = 0; x < bounds.max.x; x++)
{
    for (int y = 0; y < bounds.max.y; y++)
    {
        // do stuff
    }
}

But what’s wrong with this? The starting value! You need to be careful when working in 2D space like in Unity. There’s nothing that actually guarantees your Tiles have been drawn starting from position zero on the TileMap and only going in a positive direction. In fact, in my game I had no tiles on the positive y axis! So a simple change to make sure you don’t mess up is use the MINIMUM as your starting point:

for (int x = bounds.min.x; x < bounds.max.x; x++)
{
    for (int y = bounds.min.y; y < bounds.max.y; y++)
    {
        // do stuff
    }
}

Simple as that!

Wrapping It All Up

You’re probably looking for a full-blown code snippet by now. Fair enough. Here’s what I whipped up:

public static class TileMapExtensionMethods
{
    public static IEnumerable GetAllTiles(this Tilemap tilemap)
    {
        // note: optionally call tilemap.CompressBounds() some time prior to this
        var bounds = tilemap.cellBounds;

        // loop over the bounds (from min to max) on both axes
        for (int x = bounds.min.x; x < bounds.max.x; x++)
        {
            for (int y = bounds.min.y; y < bounds.max.y; y++)
            {
                var cellPosition = new Vector3Int(x, y, 0);

                // get the sprite and tile object at the specified location
                var sprite = tilemap.GetSprite(cellPosition);
                var tile = tilemap.GetTile(cellPosition);

                // this is a sanity check that i've included to ensure we're only
                // looking at populated tiles. you can change this up!
                if (tile == null && sprite == null)
                {
                    continue;
                }

                // create a data-transfer-object to yield back
                var tileData = new TileData(x, y, sprite, tile);
                yield return tileData;             
            }
        }
    }
}

public sealed class TileData
{
    public TileData(
        int x,
        int y,
        Sprite sprite,
        TileBase tile)
    {
        X = x;
        Y = y;
        Sprite = sprite;
        Tile = tile;
    }
    
    public int X { get; }

    public int Y { get; }

    public Sprite Sprite { get; }

    public TileBase Tile { get; }
}

The above example provides a nice extension method to get you back a TileData instance for all the populated cells on your TileMap!


  • Copyright © 1996-2010 Dev Leader. All rights reserved.
    Jarrah theme by Templates Next | Powered by WordPress