Musings of a Tilemap Junky
There's been a recent influx of programming related topics, and a lot of talk about OOP, so I thought I'd just share what I did today, which is say that "tiles are done like this, presto bango fix up the placeholder code and then move on." Yeah, I sort of had some placeholders in for different map stuff that I wanted to get rid of. The truth is that 2D tilemaps are very simple, like stupid simple. I've thrown together at least different versions over the years for different things. Usually just for fun when I'm prototyping an idea and want to see how it works with a whole graphics scene. Sure they are very basic, but more than enough for something like Pac Man or Breakout. Anyway, the point is that tilemaps are not hard by any means, and thinking you have to use a specialized 2D tile engine library is a little bonkers if you ask me. Anyway.
So back to to the point: If it is so easy why did I have all the placeholders in the first place? Which as it turns out is a totally good question by itself. So today I had the day off and just went ahead and solved the problem once and for all.
Well, I wanted it to support a bunch of features, I mean, RPG's are very complex. I hadn't ever made one before and I didn't want to screw anything up. When you think about what might need to have it can seem like it's complicated: You might have tiles that don't do anything, null tiles, static tiles, animated tiles, dynamically moving tiles, tiles that change into other tiles (eg; like combo->next). So when you start thinking about it from a code perspective you usually come up with something like this:
Code:
class Tile /*maybe : public MapEntity?*/
{
public:
virtual ~Tile() {}
virtual void Update() = 0;
virtual void Render() = 0;
virtual Rect GetCollisionRect() return Rect::Empty; }
//etc...
private:
int m_id; //reverse lookup
TileSet* m_parent; //needed for rendering
Region region;
//other stuff
};
class AnimatedTile
{
public:
void Update() { m_tileAnimation.Animate(); }
void Render { m_tileAnimation.RenderTile(this, this->GetParentTileSet()); }
//etc...
};
//ad nauseum...
Which basically solves every problem because now tiles can do everything we need them to do. ...right? ..wait. ..no. ...yes. ..? I mean it's grade A 80% lean choice OOP so... no. This what happens when you read c++ tutorials online people! What this actually is is an Entity system in disguise, and absolutely not a tilemap system. The whole point of tiles is they have a grid. If there is no grid, then you definitely shouldn't even have tiles at all. Plus this code is like 1000 bugs all waiting to strike, and another downside is that it is slow and very time consuming when all is said and done. I definitely do not want to spend 5 years getting it to work right with everything else in the game when they are added.
So I thought about what the actual problem is I came up with this: Tiles have static properties and also dynamic properties, but, tiles don't actually exist. That is, there is no such thing as a single tile at all. Tiles are part of a larger set, grouped by their image and perhaps animation--all tiles with the same id need to stay in sync, for example, and if the don't then you have problems--by layers, and groupings, and maybe even resembling larger objects. Tile groupings can even be scattered throughout the map, so you'd want to treat far away tiles similarly to nearby tiles instead of putting them to sleep like entities.
In the end I just ended up with the simplest solution. Which is mutable TileData, and immutable properties of each tile. Which is just this, minus some unfinished things:
Code:
enum TileAnimationType ENUM_TYPE(u8)
{
TileAnimationType_None,
TileAnimationType_Loop,
TileAnimationType_PingPong,
TileAnimationType_OneTime
};
enum TileAnimationFlags ENUM_TYPE(u8)
{
TileAnimationFlags_FlipX = 0x01,
TileAnimationFlags_FlipY = 0x02,
TileAnimationFlags_Reverse = 0x80,
};
struct RPG_API Tile
{
uint16 id; // depricated. (currently used in saving)
uint16 _padding;
/// The starting top-most rectangle position (in pixels) in the texture.
uint16 sourceX;
/// The starting left-most rectangle position (in pixels) in the texture.
uint16 sourceY;
/// The texture coordinates when rendering the tile.
Rectf uv;
/// The frame counter.
uint16 frameCounter;
/// The animation delay of each frame. (In fixed 16:1 point)
uint16 animationDelay;
/// The current animation frame.
uint8 currentFrame;
/// The maximum number of animation frames.
uint8 numFrames;
/// The type of animation.
uint8 animationType;
/// Animation flags. (Used internally)
uint8 animationFlags;
// aggregate
/// Updates the tile animation.
/// The tiles' texture information must be supplied along with the size of the tile.
void UpdateAnimation(float textureWidth, float textureHeight, uint32 tileWidth, uint32 tileHeight);
/// Sets the current animation frame and updates rendering data if needed.
/// Does not reset the frame counter.
void SetCurrentFrame(float textureWidth, float textureHeight, uint32 tileWidth, uint32 tileHeight, u8 frame);
FORCEINLINE bool IsAnimated() const { return numFrames > 1; }
static Tile Default;
};
CE_MAKE_TRAIT(Tile, is_pod);
And that's it, minus additional properties for gameplay mechanics. So simple, right? It may not solve every single problem you think might come up by itself, but it doesn't cause any either. Plus it can only be managed by components with specific knowledge about what it is they have to do with just straight forward, simple data. Speed goes way up and complexity way down. For example, when a map layer is told to render tiles it can cull and go through all the tiles in view at once, and simple throw all that data into an array, which renders the entire tile layer to the GPU in a single draw call. Can't get any faster than that. Actually, most layers will even share the same tileset so in that case every layer on screen can even be batched together... but that's really not necessary.
[edit] In case of possible name confusion, the tilemaps work like this:
-- Tileset->Tile array[]
-- TileMap->TileLayers[each layer holds a tileset pointer]->TileLayerCell array[]->also references a tile.
(so in ZC terms, the term 'tile' is like a 'combo', and 'tiles' are physical files like tilesheets or /png.)
[editmore] Ugh.. too tired to fix any grammer issues. That's grampers job.