What is covered:
- Parsing some basic XML without an XML unit
- Loading multiple layers
- Basic usage of ZenGL to draw the map
- Object Orientation
What is not covered:
- Advanced ZenGL usage (sprite engine, etc.)
- Scrolling maps
- Efficient maps
- Advanced map loading (Objects, layer names, other XML attributes)
A quick list of the tools that we'll use: Lazarus .9.30, FPC 2.4.2, ZenGL .2.1, Tiled QT .6
Final Result:
Getting Started
This tutorial isn't really meant to be a "Let's do this together, right now!" tutorial, but is more of an extensive commenting of the example code I'm going to provide. If you want to brave following along, see the bottom for a basic idea of how to get ZenGL to work with Lazarus, and look at demo05 from ZenGL to see how you can compose a basic rendering, initializing, etc. program. Really, I recommend you simply open the example project I made and follow along that code with this article.
So, what do we need?
Code:
uses zglHeader, SysUtils {for some string handling}, Classes {for TStringList} ;
Objects
Code:
type TTile = class public index: integer; end; TTileMap = class public tile: array of TTile; end;
Variables
Code:
var f: zglPFont; tex: zglPTexture; mapW, mapH: integer; layer: array of TTileMap;
Initialization
This is going to be the last section of the source file (in case you're following along).
Code:
begin zglLoad(libZenGL); randomize(); zgl_Reg(SYS_LOAD, @Init); zgl_Reg(SYS_DRAW, @Draw); zgl_Enable(APP_USE_UTF8); wnd_SetCaption('Tiled CSV Example'); wnd_ShowCursor(TRUE); scr_SetOptions(640, 480, REFRESH_MAXIMUM, FALSE, FALSE); zgl_Init(); end.
procedure Init
We told ZenGL to call the Init procedure when it loads, and here is what we have for it:
Code:
procedure Init; begin f := font_LoadFromFile('font.zfi'); tex := tex_LoadFromFile('tiles.png', $FF000000, TEX_DEFAULT_2D); tex_SetFrameSize(tex, 32, 32); loadMap('map_csv.tmx'); end;
Code:
tex_SetFrameSize(tex, 32, 32);
Just in case
Code:
procedure debug(output: string); begin WriteLn(output); end;
The Map Loading (loadMap)
As you might expect, this one's a doozy. First, the entirety of the function:
Code:
procedure loadMap(fname: string); var mapfile: TextFile; s, s2, beginning: string; llen, i: integer; csv: TStringList; begin AssignFile(mapfile, fname); Reset(mapfile); while(not(EOF(mapfile))) do begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); beginning := LeftStr(s, 5); if(beginning = '<map ') then begin // Do some basic XML parsing / hacking to get dimensions of map // Sets s2 = the string starting from the width s2 := Copy(s, Pos('width', s)+7, 10000); // Final parsing, copying to the double quote, we now have a number mapW := StrToInt(Copy(s2, 1, Pos('"', s2)-1)); // Sets s2 = the string starting from the width s2 := Copy(s, Pos('height', s)+8, 10000); // Final parsing, copying to the double quote, we now have a number mapH := StrToInt(Copy(s2, 1, Pos('"', s2)-1)); debug('Map dimensions: '+IntToStr(mapW)+'x'+IntToStr(mapH)); end else if(beginning = '<laye') then begin // Wee, we have a new tile layer full of delicious CSV tile data // Initialize objects and arrays to the map dimensions llen := Length(layer); // Going to be using this a lot, so make a var SetLength(layer, llen+1); layer[llen] := TTileMap.Create; SetLength(layer[llen].tile, mapW * mapH); for i := 0 to mapW * mapH do begin layer[llen].tile[i] := TTile.Create; end; debug('layer '+IntToStr(llen)+' objects initialized'); // Read until we hit the CSV data while(not(s = '<data encoding="csv">')) do // This is the last line before begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); end; csv := TStringList.Create; s2 := ''; // Read CSV data until no more while(not(s = '</data>')) do begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); s2 := Concat(s2, s); end; s2 := Copy(s2, 1, Length(s2)-7); // </data> would otherwise be appended debug(s2); // CSV split into a TStringList csv.StrictDelimiter := true; csv.Delimiter := ','; csv.DelimitedText := s2; debug('-----'); // Tile data populated for i := 0 to csv.Count-1 do begin layer[llen].tile[i].index := StrToInt(csv[i]); end; debug(''); end; end; CloseFile(mapfile); end;
Code:
AssignFile(mapfile, fname); Reset(mapfile); while(not(EOF(mapfile))) do begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); beginning := LeftStr(s, 5);
Parsing Map Dimensions
Code:
if(beginning = '<map ') then begin // Do some basic XML parsing / hacking to get dimensions of map // Sets s2 = the string starting from the width s2 := Copy(s, Pos('width', s)+7, 10000); // Final parsing, copying to the double quote, we now have a number mapW := StrToInt(Copy(s2, 1, Pos('"', s2)-1)); // Sets s2 = the string starting from the width s2 := Copy(s, Pos('height', s)+8, 10000); // Final parsing, copying to the double quote, we now have a number mapH := StrToInt(Copy(s2, 1, Pos('"', s2)-1)); debug('Map dimensions: '+IntToStr(mapW)+'x'+IntToStr(mapH)); end else
Code:
<map version="1.0" orientation="orthogonal" width="20" height="15" tilewidth="32" tileheight="32">
Code:
// Sets s2 = the string starting from the width s2 := Copy(s, Pos('width', s)+7, 10000);
Code:
// Final parsing, copying to the double quote, we now have a number mapW := StrToInt(Copy(s2, 1, Pos('"', s2)-1));
Side note: If you notice, checking for a <map> tag is done every time a line is read. Currently, Tiled stores 1 map per file, although its not inconceivable to concatenate files yourself. I have no idea if this code will work well in those situations, but you're a programmer! (hopefully)
Dealing with new layers
Code:
if(beginning = '<laye') then begin // Wee, we have a new tile layer full of delicious CSV tile data // Initialize objects and arrays to the map dimensions llen := Length(layer); // Going to be using this a lot, so make a var SetLength(layer, llen+1); layer[llen] := TTileMap.Create; SetLength(layer[llen].tile, mapW * mapH); for i := 0 to mapW * mapH do begin layer[llen].tile[i] := TTile.Create; end; debug('layer '+IntToStr(llen)+' objects initialized');
Code:
llen := Length(layer); // Going to be using this a lot, so make a var SetLength(layer, llen+1); layer[llen] := TTileMap.Create; SetLength(layer[llen].tile, mapW * mapH);
Code:
for i := 0 to mapW * mapH do begin layer[llen].tile[i] := TTile.Create; end;
Almost to the CSV!
Code:
// Read until we hit the CSV data while(not(s = '<data encoding="csv">')) do // This is the last line before begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); end;
Loading the CSV into a single string
Code:
csv := TStringList.Create; s2 := ''; // Read CSV data until no more while(not(s = '</data>')) do begin ReadLn(mapfile, s); s := LowerCase(Trim(s)); s2 := Concat(s2, s); end; s2 := Copy(s2, 1, Length(s2)-7); // </data> would otherwise be appended
Splitting up the CSV data
Code:
// CSV split into a TStringList csv.StrictDelimiter := true; csv.Delimiter := ','; csv.DelimitedText := s2; debug('-----');
Putting that data in our objects and finishing up
Code:
// Tile data populated for i := 0 to csv.Count-1 do begin layer[llen].tile[i].index := StrToInt(csv[i]); end;
(There's some more code that ends that procedure)
Code:
debug(''); end; end; CloseFile(mapfile); end;
Drawing that Map
We just spent forever making that map load itself into a bunch of objects, and now it's off to draw them. Remember, we told ZenGL to use the Draw; procedure for drawing, so here it is:
Code:
procedure Draw; var i, i2, i3, c, c2: integer; begin batch2d_Begin(); c := 0; c2 := 0; for i := 0 to mapH-1 do begin for i2 := 0 to mapW-1 do begin for i3 := 0 to Length(layer)-1 do begin if(not(layer[i3].tile[c].index = 0)) then begin // tex, x, y, w, h, angle, index asprite2d_Draw(tex, i2*32, i*32, 32, 32, 0, layer[i3].tile[c].index); Inc(c2); end; end; Inc(c); end; end; text_Draw(f, 0, 0, 'fps: ' + u_IntToStr(zgl_Get(RENDER_FPS))); batch2d_End(); end;
Code:
for i3 := 0 to Length(layer)-1 do begin if(not(layer[i3].tile[c].index = 0)) then begin // tex, x, y, w, h, angle, index asprite2d_Draw(tex, i2*32, i*32, 32, 32, 0, layer[i3].tile[c].index); Inc(c2); end; end; Inc(c);
The general structure of these loops is:
We're on a row -> Go through each column in this row -> draw every tile in this row,column (in every layer), switch to next column, etc, -> next row, etc.
Tiled's nice file structure is what allows us to use so little space to draw so many tiles. It puts the lowest leveled layers first in the file, which is extremely handy because this is also how we want to draw the layers: from lowest to highest (for proper z order)
End of drawing
Code:
text_Draw(f, 0, 0, 'fps: ' + u_IntToStr(zgl_Get(RENDER_FPS))); text_draw(f, 0, 26, 'tiles: '+ u_IntToStr(c2)); batch2d_End(); end;
Comparison of Tiled and our program:
You're Done, see attachment for code and such
Using the Example Code Provided
I created the example project with Lazarus, but it should be easy enough to use Delphi or FPC only. The entire example is contained in one file, the .lpr file.
Assuming you're using Lazarus, be sure to also download ZenGL from http://zengl.org. Once downloaded and extracted, in Lazarus go to Project -> Project Options. Click on "Compiler Options", and in "Other Units -Fu", change the line to point to the path you extracted ZenGL to the headers directory. Example: ZenGL_Base_Install/headers/ or C:/ZenGL/headers/
If you're on Windows, the example should now be able to run. If you're on a different OS, see the ZenGL wiki for compilation instructions. Note that this tutorial uses dynamic linking, because static linking is a little tricky to get working properly, and I wanted the simplest experience. Once this library is compiled, copy it to the bin directory.
About the Code and I
This code is kind of hackish in parts, notably the XML parsing. I'm currently a University student doing this in my spare time, for use in my own personal projects, so this does not matter. While I did try to implement some future-proofing into the loading code, if you're doing this for an important commercial project I would highly suggest just implementing an XML unit to parse everything -- it is the only way to be 100% sure if the TMX file format changes, you will still hopefully have access to your layer data. It also makes much cleaner and more readable code But alas, I will never feed my projects a different TMX structure, nor do I care about readability in this procedure (for loading the maps), and I do care about executable size, which can be driven up by some XML units. Sure, it's not a drastic amount in the long run, but why have it if you don't need it?
Final Notes
- Sorry if I screwed up between CSV and CVS, happens.
- If you move the Tiled maps, you'll have to hand edit them to point to the new location of the tileset image or Tiled won't load the map.
- Sorry I'm kind of inconsistent with capitalization. Ex, I use camel case for Pascal and FPC procedures, but not for keywords, variables, or my procedures.
- Again, this should work just fine on Win/Mac/Lin Delphi/Lazarus, provided you have the proper know-how.
- I plan to do examples for XML and base64 (non z-lib) versions of TMX files in the future
- I've been busy with a bunch of stuff lately :|
- ZenGL doesn't require a lot to get working! As such, it's very easy to just pick apart the Tiled loading code.
- The code and my silly tileset are licensed for the public domain. Do what you want
- I don't utilize anything fancy with ZenGL. Partly because I was lazy, and partly because I didn't want to confuse the over all concepts. ZenGL offers more managed ways to draw a tilemap, instead of drawing every individual tile yourself.
- Tiled and ZenGL are both awesome and open source. If you currently use neither, check them out.
- Even if you don't use Tiled (or ZenGL) in your projects, you can easily rip this tutorial and example code apart for its CSV parsing
- Making a full tileset is extremely time consuming, so I didn't do it. You can easily tell where there could be more tiles to make it look better, this wasn't meant to be beautiful.
- Oh how I wish we had a Pascal highlighter..... ... .. .
- Sorry about the in-post horrible indentation. I couldn't be bothered at the moment to reformat it from the source file. I'll probably get around to fixing it later.
vBulletin Message