tutorial:tilemap

Result

This tutorial will create an application that can dynamically create tilemaps and use texture atlases. We'll also explore using tilemaps for rendering fonts. The source can be found here. There is also a pure sceGu implementation there.

The starting code will overall be the same as the end of the Sprite tutorial excluding sprites and the camera. We also change the orthographic mode to be slightly different coordinates for ease of use, and the textures to the two atlases default.png and terrain.png

#include "../../common/callbacks.h"
#include "../../common/common-gl.h"
#include <gu2gl.h>
#include <string.h>
#include <malloc.h>
#include <math.h>
 
// PSP Module Info
PSP_MODULE_INFO("Tilemap Sample", 0, 1, 1);
PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU);
 
// Global variables
int running = 1;
static unsigned int __attribute__((aligned(16))) list[262144];
 
typedef struct {
    void* data;
    u16* indices;
    u32 index_count;
} Mesh;
 
Mesh* create_mesh(u32 vcount, u32 index_count) {
    Mesh* mesh = malloc(sizeof(Mesh));
    if(mesh == NULL)
        return NULL;
 
    mesh->data = memalign(16, sizeof(Vertex) * vcount);
    if(mesh->data == NULL) {
        free(mesh);
        return NULL;
    }
    mesh->indices = memalign(16, sizeof(u16) * index_count);
    if(mesh->indices == NULL) {
        free(mesh->data);
        free(mesh);
        return NULL;
    }
 
    mesh->index_count = index_count;
 
    return mesh;
}
 
void draw_mesh(Mesh* mesh) {
    glDrawElements(GL_TRIANGLES, GL_INDEX_16BIT | GL_TEXTURE_32BITF | GL_COLOR_8888 | GL_VERTEX_32BITF | GL_TRANSFORM_3D, mesh->index_count, mesh->indices, mesh->data);
}
 
void destroy_mesh(Mesh* mesh) {
    free(mesh->data);
    free(mesh->indices);
    free(mesh);
}
 
Vertex create_vert(float u, float v, unsigned int color, float x, float y, float z) {
    Vertex vert = {
        .u = u,
        .v = v,
        .color = color,
        .x = x,
        .y = y,
        .z = z
    };
 
    return vert;
}
 
 
int main() {
    // Boilerplate
    SetupCallbacks();
 
    // Initialize Graphics
    guglInit(list);
 
    // Initialize Matrices
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 480, 0.0f, 272.0f, -10.0f, 10.0f);
 
    glMatrixMode(GL_VIEW);
    glLoadIdentity();
 
    glMatrixMode(GL_MODEL);
    glLoadIdentity();
 
    Texture* texture = load_texture("terrain.png", GL_FALSE, GL_TRUE);
    Texture* texture2 = load_texture("default.png", GL_FALSE, GL_TRUE);
 
    //Main program loop
    while(running){
        guglStartFrame(list, GL_FALSE);
 
        // We're doing a 2D, Textured render 
        glDisable(GL_DEPTH_TEST);
 
        // Blending
        glBlendFunc(GU_ADD, GU_SRC_ALPHA, GU_ONE_MINUS_SRC_ALPHA, 0, 0);
        glEnable(GL_BLEND);
 
        //Clear background to Bjack
        glClearColor(0xFF000000);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
 
 
        guglSwapBuffers(GL_TRUE, GL_FALSE);
    }
 
    // Terminate Graphics
    guglTerm();
 
    // Exit Game
    sceKernelExitGame();
    return 0;
}

Instead of loading perhaps hundreds or thousands of textures all as different individual textures which would destroy your memory organization, resulting in massive memory fragmentation, cache misses, and more – developers decided to create texture atlases, which were just collections of different in-game tiles placed together on one texture. While there are many algorithms to map these and unmap these, we'll be taking a simple tile-based approach. In any atlas, there are an arbitrary number of textures (n) in a fixed grid of (m * p). With this, we can then determine the texture coordinates to any texture (t) indexed in the array given the size of the grid.

An individual texture atlas is incredibly simple data structure.

typedef struct {
    float w, h;
} TextureAtlas;

To retrieve the uv coordinates of any individual texture in a friendly order, we'll create a method:

void get_uv_index(TextureAtlas* atlas, float* buf, int idx) {
    int row = idx / (int)atlas->w;
    int column = idx % (int)atlas->h;
 
    float sizeX = 1.f / ((float)atlas->w);
    float sizeY = 1.f / ((float)atlas->h);
 
    float y = (float)row * sizeY;
    float x = (float)column * sizeX;
    float h = y + sizeY;
    float w = x + sizeX;
 
    buf[0] = x;
    buf[1] = h;
 
    buf[2] = x;
    buf[3] = y;
 
    buf[4] = w;
    buf[5] = y;
 
    buf[6] = w;
    buf[7] = h;
}

The method takes in a TextureAtlas, a float buffer to output the UVs to, and the index you want. It then fills the buffer. The h and y components are flipped here for the end result.

A tilemap is simply a collection of tiles in space. They're batched together, built, and then rendered together to save on performance. Each individual call to draw that you use will take CPU time and resources over the bus, so sending the data all at once is a far better solution. For a tilemap, we need to know its position and scale, the width and height of the tile array, a texture atlas and its accompanying texture, the actual array of tiles, and the mesh for it to be baked into. An individual tile for us will just have an X and Y position, alongside of the index into the atlas.

typedef struct {
    int x, y;
    int tex_idx;
} Tile;
 
typedef struct {
    float x, y;
    float scale_x, scale_y;
    int w, h;
    TextureAtlas atlas;
    Texture* texture;
    Tile* tiles;
    Mesh* mesh;
} Tilemap;

To create the tile map, we'll have to do a similar thing to the sprite and pre-allocate a bunch of data for it.

Tilemap* create_tilemap(TextureAtlas atlas, Texture* texture, int sizex, int sizey) {
    Tilemap* tilemap = (Tilemap*)malloc(sizeof(Tilemap));
    if(tilemap == NULL)
        return NULL;
 
    tilemap->tiles = (Tile*)malloc(sizeof(Tile) * sizex * sizey);
    if(tilemap->tiles == NULL){
        free(tilemap);
        return NULL;
    }
 
    tilemap->mesh = create_mesh(sizex * sizey * 4, sizex * sizey * 6);
    if(tilemap->mesh == NULL){
        free(tilemap->tiles);
        free(tilemap);
    }
 
    memset(tilemap->mesh->data, 0, sizeof(Vertex) * sizex * sizey);
    memset(tilemap->mesh->indices, 0, sizeof(u16) * sizex * sizey);
    memset(tilemap->tiles, 0, sizeof(Tile) * sizex * sizey);
 
    tilemap->atlas = atlas;
    tilemap->texture = texture;
    tilemap->x = 0;
    tilemap->y = 0;
    tilemap->w = sizex;
    tilemap->h = sizey;
    tilemap->mesh->index_count = tilemap->w * tilemap->h * 6;
    tilemap->scale_x = 16.0f;
    tilemap->scale_y = 16.0f;
 
    return tilemap;
}

First we create the tilemap object itself, then the tile array which is sizex * sizey bytes. The indexing is left up to the end user to decide – we have no explicit method to set a given tile. The tile mesh is then created with the appropriate number of vertices (4) and indices (6). We then perform a memset on all of the memory allocated to the mesh information – this can be ignored, but we do it for the sake of correctness – if you don't memset and then don't set all tiles you may get very random buggy-looking tilemaps. We then set the individual data to default values for the user and send it back.

We'll then create a method to destroy the map.

void destroy_tilemap(Tilemap* tilemap) {
    destroy_mesh(tilemap->mesh);
    free(tilemap->tiles);
    free(tilemap);
}

And finally we'll make a method to draw the map. This is basically the exact same as for sprite.

void draw_tilemap(Tilemap* tilemap) {
    glMatrixMode(GL_MODEL);
    glLoadIdentity();
 
    ScePspFVector3 v = {
        .x = tilemap->x,
        .y = tilemap->y,
        .z = 0.0f,
    };
 
    gluTranslate(&v);
 
    ScePspFVector3 v1 = {
        .x = tilemap->scale_x,
        .y = tilemap->scale_y,
        .z = 0.0f,
    };
 
    gluScale(&v1);
 
    bind_texture(tilemap->texture);
    draw_mesh(tilemap->mesh);
}

This by far is the most complicated part about a tilemap, but also what gives it the performance associated. We have to iterate through every tile, grab the texture UV data, make the vertices in the map, and then set the indices. If you've been following closely with sprites, you'll understand that this is doing essentially the exact same thing.

void build_tilemap(Tilemap* tilemap) {
    for(int i = 0; i < tilemap->w * tilemap->h; i++){
        float buf[8];
        get_uv_index(&tilemap->atlas, buf, tilemap->tiles[i].tex_idx);
 
        float tx = (float)tilemap->tiles[i].x;
        float ty = (float)tilemap->tiles[i].y;
        float tw = tx + 1.0f;
        float th = ty + 1.0f;
 
        ((Vertex*)tilemap->mesh->data)[i * 4 + 0] = create_vert(buf[0], buf[1], 0xFFFFFFFF, tx, ty, 0.0f);
        ((Vertex*)tilemap->mesh->data)[i * 4 + 1] = create_vert(buf[2], buf[3], 0xFFFFFFFF, tx, th, 0.0f);
        ((Vertex*)tilemap->mesh->data)[i * 4 + 2] = create_vert(buf[4], buf[5], 0xFFFFFFFF, tw, th, 0.0f);
        ((Vertex*)tilemap->mesh->data)[i * 4 + 3] = create_vert(buf[6], buf[7], 0xFFFFFFFF, tw, ty, 0.0f);
 
        tilemap->mesh->indices[i * 6 + 0] = (i * 4) + 0;
        tilemap->mesh->indices[i * 6 + 1] = (i * 4) + 1;
        tilemap->mesh->indices[i * 6 + 2] = (i * 4) + 2;
        tilemap->mesh->indices[i * 6 + 3] = (i * 4) + 2;
        tilemap->mesh->indices[i * 6 + 4] = (i * 4) + 3;
        tilemap->mesh->indices[i * 6 + 5] = (i * 4) + 0;
    }
    sceKernelDcacheWritebackInvalidateAll();
}

In this code we create a for loop which iterates through every single tile in the array. We then create a buffer and submit the atlas, the buffer to write to, and the index of the tile at the current spot. We then calculate the current tileX and tileY positions from the current tile, alongside of the full width and height for those tiles. We then create the vertices – the index here is the current tile index count times 4 plus the offset. For each vertex, we create it according to the winding order, and this code translates pretty well to the sprites we made earlier. The indices aren't much harder, but we do have to realize that the vertices are multiplied by 4, whereas the indices index is multiplied by 6 instead.

Now, we can create code using our tilemaps. We'll start off by creating an atlas and a tilemap in our main under the texture. We'll set the position so that the atlas gets centered. In this case, we're making an 8 x 8 atlas, so the mid point (240) minus half the width (64) results in our position of 176.

TextureAtlas atlas = {.w = 16, .h = 16};
Tilemap* tilemap = create_tilemap(atlas, texture, 8, 8);
tilemap->x = 176;
tilemap->y = 136;

Then we need to set this to some random data – in this case, it's just as simple as creating a loop and calling build_tilemap()

   for(int y = 0; y < 8; y++) {
        for(int x = 0; x < 8; x++) {
            Tile tile = {
                .x = x,
                .y = y,
                .tex_idx = x + y * 8
            };
            tilemap->tiles[x + y * 8] = tile;
        }
    }
    build_tilemap(tilemap);

We'll then insert the draw_tilemap(tilemap) after the clear in our loop. Don't forget to destroy the map when you're done!

With that you should see an array of textures from CrossCraft appear on your screen!

Sometimes you want to draw fonts to the screen! To do this, we can use tilemaps in order to store arrays of characters. Since there's 256 characters representable by a char, a 16×16 atlas can carry a font! This is how many games operate to make text without diving into glyphs, TTF, OTF, and other file formats, and the monotony that produces. In this case, we're going to use our font map (default.png) to render.

We'll create a method to facilitate that:

void draw_text(Tilemap* t, const char* str){
    int len = strlen(str);
 
    for(int i = 0; i < len; i++){
        char c = str[i];
 
        Tile tile = {
            .x = i % t->w,
            .y = i / t->w,
            .tex_idx = c
        };
 
        t->tiles[i] = tile;
    }
}

This code is rather simple. We get the length of the string using C's strlen() – then we iterate for each character in the string. We get the characters into a char, then set up a tile. This math makes the tile wrap line by line – but you can also just have x = i. We then set the tiles in order equal to the one we just generated. Using this, we can then draw a string pretty easily.

    Tilemap* tilemap2 = create_tilemap(atlas, texture2, 16, 16);
    tilemap2->x = 144;
    tilemap2->y = 16;
    draw_text(tilemap2, "Hello World!");
    build_tilemap(tilemap2);

Place the draw command in your loop and you'll (hopefully) see the Hello World! printed to your screen.

Congratulations, you've now made a tilemap that can be used in your applications, including using it for text and maps! This marks the end of this tutorial series, and we'll later create an advanced series for more stuff like lighting, shadows, rendering to textures, and more!

  • tutorial/tilemap.txt
  • Last modified: 2022/10/06 04:07
  • by iridescence