1

I am building a tile engine in C++. What is the most efficient way to store the logical properties of the individual tiles in the game? I understand the rendering side of the program, but I am having trouble taking a simple id number and turning it into an actual set of properties ( like whether or not a tile is walkable or flammable or can trigger an event, etc. )

One idea is to have a tile object that has the potential to be any kind of tile, and turns on certain boolean "switches" based on the type ( note that the following is mostly just pseudocode and not meant to actually compile):

class Tile
{
private:
    int m_type;
    bool m_walkable;
    // etc...

public:
    Tile( int type ) : m_type( type)
    {
        if( type == 0 )
        {
            m_walkable = true;

        } else if( type == 1 ) {
              m_walkable = false;
        }
        // etc etc would probably be a switch
        // statement but you get the idea
    }
};

Personally, I do not like this idea; I think it would be much more elegant for each type of tile to have its own data structure. I imagine using some kind of inheritance based system but I just can't seem to put it all together. Fundamentally, I think it should look something like this:

enum class TileType
{
     TILE_TYPE null, // 0
     TILE_TYPE floor, // 1
     TILE_TYPE wall, // 2
     // etc ...
};

class BTile
{
private:
    // Location and dimensions of tile
    int m_xOffset;
    int m_yOffset;
    int m_width;
    int m_height;

    // Type of tile, initialized to 0 for base class
    TileType m_type;

public:
    // ...
};

class Floor : public BTile
{
private:
    TileType = 1;
    bool walkable = true;

    // etc...
};

class Wall : public BTile
{
private:
    TileType = 2;
    bool walkable = false;
};

Something like this would feel much more organized and flexible, while also allowing me to plug Floor and Wall objects into any kind of function expecting a Tile object. The problem is that I just cannot seem to put this all together in a way that actually works - for example, how can I provide a specific tile type with the tile it is associated with? If I am reading a text file into my program for example, how can I get from 001 to Tile->Floor? Any advice or input on this subject would be greatly appreciated.

mskfisher
  • 3,291
  • 4
  • 35
  • 48

3 Answers3

1

Try using a factory method.

The simplest way to do this is to use a switch.

Tile* createTile(TileType tileType) {
    switch(tileType) {
    case TileType.floor: return new Floor;
    case TileType.wall: return new Wall;
    }
    return nullptr;
}

This is usually not recommended as you have to update the factory each time you add a new type.

Alternatively you could use some trick to register the types to the factory. One approach is the one described here. There are a lot more strategies.

Tiberiu Maran
  • 1,983
  • 16
  • 23
0

Why are you reinventing OO? Objects already have a type, no need for TileType. You may want to read up on virtual functions.

MSalters
  • 173,980
  • 10
  • 155
  • 350
0

There are several approaches here, depending on what exactly you want the tiles in your game to do. You can go the object oriented way, by having distinct classes for the different tile types that you have, or you can go simpler and just have a bitset that represent the different abilities your tiles will have.

This choice will depend on what you want to do.

Bitset only

Oftentimes, the bitset-only variant is enough. To do that you'll need something along those lines:

You most probably want a set of flags which will represent different abilities of your tiles (e.g IsWalkable, IsWater, etc). Something similar to this:

struct TileFlag
{
    bool m_IsWalkable : 1;
    bool m_IsWater : 1;
    //other flags you might need
};

With this in mind, your Tiles would be something along those lines (Texture and Texture manager are there just for the example):

struct Tile
{
    void Render();

    void Serialize(const boost::property_tree::ptree& tileData)
    {
        m_Flags.m_IsWalkable = tileData.get<bool>("walkable", false);
        m_Flags.m_IsWater = tileData.get<bool>("water", false);
        std::string texturePath = tileData.get<std::string>("texturePath", "");
        m_TileTexture = TextureManager::GetOrLoad(texturePath);
    }

    TileFlags m_Flags;
    std::shared_ptr<Texture> m_TileTexture;
};

You would need some kind of registry, where you contain all of your tiles, so that your levels can reference the tiles. This registry can be as simple as an std::map.

Some example code:

struct TileRegistry
{
    void LoadTiles(const boost::property_tree::ptree& tiles)
    {
        for (boost::property_tree::ptree::value_type& tileType : tiles.get_child("tileTypes"))
        {
            std::unique_ptr<Tile> newTile = std::make_unique<Tile>();
            newTile->Serialize(tileType.second);
            m_Tiles[tileType.first] = std::move(newTile);
        }
    }
    Tile* FindTile(const std::string& tileType)
    {
        Tile* result = nullptr;
        auto search = m_Tiles.find(tileType);
        if (search != m_Tiles.end()) {
            result = search->second.get();
        } 
        return result;
    }
    std::map<std::string, std::unique_ptr<Tile>> m_Tiles;
};

Then, when you load your levels, you just search for the Tile Type in the TileRegistry, and you'll get a pointer to your Tile.

OOP Style object inheritance

This approach would borrow a lot from the previous one, with the biggest difference being in how you are going to create your tiles. You are going to need some kind of Factory, as @artcorpse mentions.

If you want to go a bit more generic, you can do some automation magic with a few macros:

struct TileFactory
{
    static std::map<std::string, CreateFunctionPtr> m_FactoryFunctors;
    std::unique_ptr<ITile> CreateTile(const std::string& tileType) 
    {
        std::unique_ptr<ITile> result;
        auto search = m_FactoryFunctors.find(tileType);
        if (search != m_FactoryFunctors.end()) {
            auto creationFunctionPtr = search->second;
            result = creationFunctionPtr(); //Notice the function invocation here
        } 
        return result;
    }
};

template<typename T>
struct TileRegistrator
{
    TileRegistrator(const std::string& tileTypeName){
        TileFactory::m_FactoryFunctors[tileTypeName] = &T::CreateTile;
    }
};

#define DECLARE_TILE_TYPE(TileType) \
static std::unique_ptr<ITile> CreateTile() { return std::make_unique<TileType>();} \
static const TileRegistrator<TileType> s_Registrator;

#define DEFINE_TILE_TYPE(TileType) \
const TileRegistrator<TileType> TileType::s_Registrator = {#TileType};

And how you use those macros:

struct ITile
{
    virtual ~ITile() = default; //Don't forget a virtual destructor when you have object which can be accessed by pointer to Base!
    virtual bool IsWalkable() const = 0;
    virtual bool IsSailable() const = 0;
    virtual void Serialize(const boost::property_tree::ptree& tileData) = 0;
};

In your .h files, e.g. OceanTile.h:

struct OceanTile : public ITile
{
    DECLARE_TILE_TYPE(OceanTile);
    bool IsWalkable() const override;
    bool IsSailable() const override;
    void Serialize(const boost::property_tree::ptree& tileData) override;
    int m_WavesIntensity{0};
};

In your .cpp files, e.g. OceanTile.cpp:

DEFINE_TILE_TYPE(OceanTile)

bool OceanTile::IsWalkable() const { 
    return false;
}

bool OceanTile::IsSailable() const { 
    return true;
}
void OceanTile::Serialize(const boost::property_tree::ptree& tileData) {
    m_WavesIntensity = tileData.get<int>("WavesIntensity", 0);
}

And creating a new tile object, asuming you know its type as a string (e.g. coming from a data file is very simple:

void LoadTiles(const boost::property_tree::ptree& levelData)
{
    for (boost::property_tree::ptree::value_type& tile : levelData.get_child("levelTiles"))
    {
        std::unique_ptr<ITile> newTile = TileFactory::CreateTile(tile->first);
        newTile->Serialize(tile.second);
        //Do whatever you want to do with your Tile - maybe store it in some vector of all tiles for the level or something
    }
}

Disclaimer: I have not compiled or tested any of the above code, but hopefully it can give you an idea on how to get started. There might be any number of bugs hiding there.

I encourage you to start with the Bitset only option, as this is enough for a lot of different types of games, and is much simpler to work with and reason about.

Hope this gives you some start :)

divinas
  • 1,787
  • 12
  • 12