Optimizing Isometric Tiles
David Collins

I just yesterday decided to switch from using Allegro 4 to 5.
This game I'm working on works with 5 now, but just like with 4, I am having terrible performance problems while trying to draw the isometric tiles. Kinda defeats the purpose of me switching... I was hoping the hardware acceleration would improve it.

What I'm trying to do is draw a fairly large number of tiles... probably comparable to as many as you'd see in your average session of Roller Coaster Tycoon.

I have tile culling in place, but it's still very slow when the map fills the screen. When the camera is on the edges, it's better.

Right now the game is VERY simple. More of a skeleton. So maybe I made some kind of error somewhere.

My INIT function:

#SelectExpand
1int init() //start up necessary allegro tools and other objects 2{ 3 al_init(); //allegro itself 4 5 al_set_new_display_flags(ALLEGRO_FULLSCREEN); 6 7 int dsk_w = 1360, dsk_h =768; //my monitor's native res 8 //get_desktop_resolution(&dsk_w,&dsk_h); //something left over from AL4 9 display = al_create_display(dsk_w, dsk_h); //create display 10 11 12 al_init_image_addon(); //bitmaps, pngs, jpegs etc 13 al_init_font_addon(); //for fonts, just bitmap ones 14 al_init_primitives_addon(); //rectangles, circles, triangles etc 15 al_init_ttf_addon(); // true type fonts (scalable vector fonts) 16 al_install_mouse(); //the mouse 17 al_install_keyboard(); //the keyboard 18 al_install_audio(); //for audio, WAVs, 19 al_init_acodec_addon(); //audio codecs like OGG and FLAC 20 21 22 srand(time(NULL)); 23 24 font_default = al_load_font("font/calibri.ttf", 14, 0); 25 26 return 0; 27}

This is the main part of the screen refreshing function, very basic:

This object's draw event is called once each frame. It draw the world tiles:
Note that BMP["filename"] is the resource map that retrieves a pointer to a loaded bitmap, and loads it if it hasn't already been loaded. It's not in use in the loop.

#SelectExpand
1Session::Session() 2{ 3 Grid_Tiles = Grid<ALLEGRO_BITMAP*>(128,128,NULL); 4 5 camX=500; 6 camY=-200; 7 8 int seed=rand(); 9 10 ALLEGRO_BITMAP* tiletest1 = BMP["tiletest1.png"]; 11 ALLEGRO_BITMAP* tiletest2 = BMP["tiletest2.png"]; 12 13 14 for(int a=0; a<Grid_Tiles.w; a++) 15 for(int b=0; b<Grid_Tiles.h; b++) 16 { 17 if (perlin(a, b, 3, seed)>0.4) 18 { 19 Grid_Tiles.set(a,b,tiletest1); 20 } 21 else 22 Grid_Tiles.set(a,b,tiletest2); 23 } 24 25} 26 27void Session::draw() 28{ 29 30 if (key[ALLEGRO_KEY_UP]) camY-=3; 31 if (key[ALLEGRO_KEY_DOWN]) camY+=3; 32 if (key[ALLEGRO_KEY_RIGHT]) camX+=6; 33 if (key[ALLEGRO_KEY_LEFT]) camX-=6; 34 35 selectX=-1; 36 selectY=-1; 37 38 int drawX, drawY; 39 40 int viewX1=std::max(0,(((camX)/(TILE_W-1))+(camY/TILE_H))-3); 41 int viewX2=std::min(Grid_Tiles.w,((((camX)+SCREEN_W)/(TILE_W-1))+((camY)+SCREEN_H)/TILE_H)+5); 42 int viewY1=std::min(Grid_Tiles.h-1,((((camX)+SCREEN_W)/(TILE_W-1))-(camY)/TILE_H)+5); 43 int viewY2=std::max(0,(((camX)/(TILE_W))-(((camY)+SCREEN_H)/TILE_H))-3); 44 45 //ALLEGRO_BITMAP* selectorback = BMP["tileselector_back.png"]; 46 //ALLEGRO_BITMAP* selectorfront = BMP["tileselector_front.png"]; 47 48 49 for(int a=viewX1; a<viewX2; a++) 50 { 51 for(int b=viewY1; b>=viewY2; b--) 52 { 53 drawX=(b*(int)(TILE_W/2-1))+(a*(int)(TILE_W/2-1))-camX; 54 if (drawX<-TILE_W || drawX>SCREEN_W) 55 continue; 56 57 drawY=(a*(int)(TILE_H/2))-(b*(int)(TILE_H/2))-camY; 58 if(drawY<-200 || drawY>SCREEN_H) 59 continue; 60 61 62 63 //TODO: Set selectX and selectY based on where the mouse is pointing 64 65 //if (selectX==a && selectY==b) 66 // al_draw_bitmap(selectorback, drawX, drawY, 0); 67 68 al_draw_bitmap(Grid_Tiles.get(a,b), drawX, drawY, 0); 69 70 if (Grid_Actors->get(a,b)!=NULL) 71 { 72 Grid_Actors->get(a,b)->draw(drawX, drawY); 73 } 74 75 /*if (selectX==a && selectY==b) 76 { 77 al_draw_bitmap(selectorfront, drawX, drawY, 0); 78 al_draw_textf(font_default,al_map_rgb(255,150,30),drawX-20,drawY-10,0, "(%d,%d)",a,b); 79 }*/ 80 } 81 } 82 83}

Here is the Bitmap resource handler. Maybe I'm loading them incorrectly?

#SelectExpand
1#include "bitmaps.h" 2#include <allegro5/allegro_native_dialog.h> 3#include <allegro5/allegro_image.h> 4 5 6ALLEGRO_BITMAP* BitmapHandler::operator[](std::string filename) 7{ 8 9 ALLEGRO_BITMAP* result = my_map[filename]; 10 11 if (!result) //file hasn't been loaded yet 12 { 13 result=load(filename); 14 } 15 if (!result) //load error 16 { 17 my_map.erase(filename); 18 } 19 20 return result; 21} 22 23ALLEGRO_BITMAP* BitmapHandler::load(std::string filename) 24{ 25 ALLEGRO_BITMAP* result = al_load_bitmap((char*)("gfx/"+filename).c_str()); 26 27 if (!result) 28 al_show_native_message_box(NULL,"ERROR", "Image Could Not Load",(char*)filename.c_str(),NULL, ALLEGRO_MESSAGEBOX_ERROR); 29 else 30 my_map[filename] = result; 31 32 return result; 33} 34 35//THIS IS NOT USED ANYWHERE YET: 36void BitmapHandler::unload(std::string filename) 37{ 38 39 al_destroy_bitmap(my_map[filename]); 40 my_map.erase(filename); 41} 42 43 44//THIS IS NOT USED ANYWHERE YET: 45void BitmapHandler::reload(std::string filename) 46{ 47 unload(filename); 48 my_map[filename]=load(filename); 49} 50 51void BitmapHandler::clear() 52{ 53 std::map<std::string, ALLEGRO_BITMAP*>::iterator it; 54 for (it = my_map.begin(); it != my_map.end(); ++it ) 55 { 56 al_destroy_bitmap((*it).second); 57 } 58 my_map.clear(); 59}

And the Grid class that I created. [Needed this to have a structure that returns a default value on an out-of-bounds get()]

#SelectExpand
1#pragma once 2 3#include <vector> 4 5template<class T> 6class Grid 7{ 8 private: 9 std::vector<T> field; 10 T default_value; 11 12 public: 13 int w,h,size; 14 15 Grid() 16 { 17 w=0; 18 h=0; 19 size=0; 20 default_value=0; 21 } 22 23 Grid(int W, int H, T def) 24 { 25 w=W; 26 h=H; 27 size=w*h; 28 default_value=def; 29 for ( int i = 0; i < size; i++ ) 30 { 31 field.push_back(default_value); 32 } 33 } 34 35 T get(int x, int y) 36 { 37 if (x<0 || y<0 || x>=w || y>=h) return default_value; 38 return field[x + y*w]; 39 } 40 41 void set(int x, int y, T val) 42 { 43 if (x<0 || y<0 || x>=w || y>=h) return; 44 field[x + y*w] = val; 45 } 46 47 void fill(T val) 48 { 49 for(int a=0; a<size; a++) 50 { 51 field[a] = val; 52 } 53 } 54};

I'll get yelled at for this, but here's my "global.h". To the main CPP, it declares and initializes global variables. Outside the main.cpp, it declares them as external.

#SelectExpand
1#pragma once 2 3#include <vector> 4#include <algorithm> 5 6#include <allegro5/allegro.h> 7#include <allegro5/allegro_font.h> 8#include <allegro5/allegro_audio.h> 9#include <allegro5/allegro_acodec.h> 10#include <allegro5/allegro_ttf.h> 11#include <allegro5/allegro_image.h> 12#include <allegro5/allegro_primitives.h> 13#include <allegro5/allegro_native_dialog.h> 14 15#include "game.h" 16#include "gameobject.h" 17#include "clarionmath.h" 18#include "grid.h" 19#include "bitmaps.h" 20 21#ifndef MAINCPP 22#define GLOB(X,V) extern X; 23#else 24#define GLOB(X,V) X V; 25#undef MAINCPP 26#endif 27 28 29///GLOBAL DEFINES 30#define SCREEN_W al_get_display_width(display) 31#define SCREEN_H al_get_display_height(display) 32#define SCREEN_BITMAP al_get_backbuffer(display) 33 34///ENUMS 35namespace ROOM 36{ 37enum ENUM 38{ 39 MENU, 40 GAME 41}; 42} 43 44namespace DIR 45{ 46enum ENUM 47{ 48 NORTH, 49 EAST, 50 SOUTH, 51 WEST, 52 UP, 53 DOWN 54}; 55} 56 57 58enum MOUSE_BUTTONS 59{ 60 MBLEFT=0, 61 MBRIGHT=1, 62 MBMIDDLE=2 63}; 64 65 66///GLOBAL VARS 67//remember to use GLOB(Declaration,Definition) to define global variables and avoid linker errors 68 69GLOB(Game* Clarion,) 70 71GLOB(const int MASK,=16711935) 72 73GLOB(unsigned int LAST_CLOCK,=clock()) 74GLOB(double FPS,=1) 75GLOB(int TARGET_FPS,=60) 76 77//dimensions of various things 78GLOB(int TILE_W,=50) 79GLOB(int TILE_H,=25) 80GLOB(int UI_W,=200) 81GLOB(int UI_H,=120) 82 83//resource holders (hold maps of resources) 84GLOB(BitmapHandler BMP,) 85 86//keyboard data 87GLOB(ALLEGRO_KEYBOARD_STATE KBSTATE,) 88GLOB(bool key[ALLEGRO_KEY_MAX],) 89GLOB(bool key_was[ALLEGRO_KEY_MAX],) 90GLOB(bool key_pressed[ALLEGRO_KEY_MAX],) 91GLOB(bool key_released[ALLEGRO_KEY_MAX],) 92 93//mouse data 94GLOB(ALLEGRO_MOUSE_STATE MOUSE,) 95GLOB(bool mouse_was[3],) 96GLOB(bool mouse_pressed[3],) 97GLOB(bool mouse_released[3],) 98 99//fonts 100GLOB(ALLEGRO_FONT* font_default,) 101 102//display 103GLOB(ALLEGRO_DISPLAY* display,) 104 105 106 107 108///FUNCTIONS 109int init(); //start up necessary allegro tools and other objects 110 111void update_input(); 112 113/*void soundplay(ALLEGRO_SAMPLES* S); 114void soundloop(ALLEGRO_SAMPLES* S);*/ 115 116void refresh_screen(); //flips screen 117 118//draw a UI frame using a small picture as the base 119void draw_frame(ALLEGRO_BITMAP* frame, int x, int y, int w, int h, bool inner=0); 120 121int color_compare(ALLEGRO_COLOR c1, ALLEGRO_COLOR c2);

Trent Gamblin

tl;dr all of it. But I have one tip anyway. Using individual bitmaps is going to be slow on OpenGL or D3D. What you should do is put your tiles in one sheet (if possible, or 2, 3 sheets if not). Then draw everything in one swoop. Something like this:

al_hold_bitmap_drawing(true);
// loop drawing sub bitmaps must be the same parent
al_hold_bitmap_drawing(false);

Like that. Then there are very few OpenGL state changes.

If you need to keep your tiles in individual files on disk, then load and draw them onto a single large bitmap then destroy them. That big bitmap is called a texture atlas. <shameless plug>If you want something that can easily create atlases from loaded bitmaps (the atlases are static, once you create them they can't be changed and that's the main shortcoming), I have an atlas library here: http://www.nooskewl.com/content/open-source. </shameless plug>

David Collins

Well, so far there are only TWO kinds of tiles, so I'm not sure if that's the cause. Since all the tiles are 64x64 I shouldn't have a problem making this atlas you speak of.

Can you explain exactly what

al_hold_bitmap_drawing(true);
// loop drawing sub bitmaps must be the same parent
al_hold_bitmap_drawing(false);

does?

Are you saying that each individual tile should be a sub bitmap of the atlas?

I'm going to try this out anyhow. Thanks for the tip.

Trent Gamblin

Two tiles can be just as bad as 1000. What that code does is start collecting vertices in a buffer, each call to al_draw_bitmap is batched up and when you finally call al_hold_bitmap_drawing(false) it only has to bind that one texture (the atlas) once, and then just passes the gpu a bunch of vertex (triangle) data. The really slow part is switching textures and using an atlas avoids that. If you have just two textures you still have to switch over and over between them. Atlases are very very common in games using OpenGL/D3D and that's why.

David Collins

Amazing!

I made a TileHandler, similar to the above BitmapHandler, except it stores sub-bitmaps to a 1024 by 1024 atlas bitmap. You use TILE["tile.png"]; and it places that tile in the next available spot on the atlas if it hasn't already been loaded. It returns the pointer to the sub-bitmap.

After using this instead of the bitmap handler to store the tiles, and adding the al_hold_bitmap_drawing(); function in the world drawing loop, my framerate is through the roof.

Thanks a ton for the help.

EDIT: So, this begs the question. How do I get around my hard limit of 256 64x64 tiles?

I'm thinking I should just make new tile file loads loop back to the start of the atlas (0,0) if it's full, right? Tricky.

Trent Gamblin

It depends how many tiles you need per level. If you need no more than 256, reset your atlas each level. If you need more, you'll need several sheets and a modified drawing algorithm. The drawing algo would look something like this:

for each layer
  for each sheet
    hold drawing true
      loop drawing all tiles on sheet X
    release hold

Thread #609898. Printed from Allegro.cc