Tuesday, July 3, 2012

EPI #8: Maps, part 2

So I wanted to write a class that would take interpret an XML map specification and draw it to the screen.  Well, it didn't make much sense to me to dive right into without any context, so I sat down with a pen and a ruler and drew another flowchart.  This flowchart goes into detail about the steps the MGEET needs to go through before it draws a map:


I made this a couple of days before I wrote the last post, so some things don't line up.  In particular the "manifest.xml" I mention here is actually the "global.xml" I talked about last post.

I implemented everything up to "Is a scenario specified?"  Then I hard-coded a scenario into my config.ini and pretended that "Is a scenario specified?" pointed straight to "Draw map".  I'm not really concerned about the title screen right now because I have bigger and more exciting things to do, like have things move around on the map and get into fights with each other.

config.ini is a simple flat file containing one "key = value" statement per line.  These are then parsed into a dictionary called engine_settings.  Currently, I have the keys resolution_x, resolution_y, bg_r, bg_g, bg_b, scenario_dir, and last_savegame implemented, although I will certainly add more keys as they become necessary.  Users aren't meant to modify config.ini directly.  Instead, I'll have an interface to changing config.ini from within the application.

This afternoon, as I was still without power, I went to Starbucks, ordered a Grande Decaf Iced Mocha Latte (henceforth "GDIML") because I figured I needed to patronize them in exchange for free internet and free power, and started coding.  Five hours of banging my head against PyGTK documentation and tutorials later, I had a class that would draw a map correctly, and a driver to make it go:

This is ugly because I tried to do Art.
Astute readers will have noticed that this is the same map I described in the XML example I gave last post.  Thrilling, I know.  Implementation wise, it makes neat use of object inheritance-- I wrote a MapWidget class which extends the gtk.DrawingArea class provided by PyGTK.  MapWidget basically combines gtk.DrawingArea with the Map class I wrote about earlier.  There's also a fair amount of coordinate geometry that goes into centering the map in the window, and figuring out which tiles are appropriate to draw based on where the player character is and how much window space the application has available to it.  I'll attach scans of my notes in a jump at the end of this post.

While I was implementing MapWidget, several modifications to the Map and MapTile classes became necessary.  In a nutshell, the classes now look like this:

  • Map
    • tileset - dictionary
    • tiles_resolution - OrderedPair
    • default - OrderedPair
    • actors - list
    • map - integer matrix
    • specials - dictionary
    • bg - (red, green, blue) tuple
  • MapTile
    • art_filename - string
    • passability - integer
    • pixel_buffer - gtk.gdk.Pixbuf
    • resolution - OrderedPair

Each Map keeps track of its tiles in its tileset dictionary, which is indexed by tile id.  The actual map matrix consists only of integers-- each tile can be accessed as Map.tileset[map[x][y]].  This technique, known as deduplication, saves a lot of memory as long as the tileset is small compared to the total number of tiles in the map (and this condition should pretty much always hold true).  I'm not really sure why both Maps and MapTiles keep track of their resolutions.  This is redundant, and I'll fix it when I feel like fixing little things.

It would be nice still to dynamically redraw the map every time the window is resized, and also maybe add a zoom feature.

Replacing my bad mspaint tile art with results from Google image searches for sand and stone textures has an immediate positive impact on the map's appearance:

Looking better!
A little creativity with the tileset definition goes a long way.  For my next trick, I'll use more than two tiles to create a beach:

The very essence of summer!
It's primitive, I know, but I hope I'm demonstrating that the returns scale with the amount of effort artists put in to creating environments.

With MapWidget more or less implemented, it's a good time to mention some pros and cons of the strictly tile-based system I've adopted.
  • Pros 
    • It's easy to implement.
  • Cons
    • It's fugly.
    • Character movement is unsmooth.
    • Tile-to-position ratio is approximately 1:1.
    • XML Maps are hard to manage.
    • One MapEntity per tile.
Tile-to-position ratio is ratio of tiles on the map to unique positions a character can be in.

A slightly better approach would be to define and draw maps using tiles, but rather than express MapEntity (look at EPI #6 if you don't remember these) locations in terms of MapTile coordinates, express them as pixel coordinates.  Then, if we give each MapEntity a "speed" (the number of pixels it moves at once), we solve many of the aforementioned cons:
  • Character movement becomes much smoother, since they move fractions of a tile at a time.
  • Tile-to-position ratio skyrockets-- (tile_res_x * tile_res_y)/speed:1.  With a 32x32 tiles and a speed of 2, this approach takes us from 1:1 to 512:1!
  • The increase in tile-to-position ratio also helps with managing XML Maps.  The above example essentially allows a designer to replace a 32x32 grid of tiles with a single tile!
  • Large tiles become more attractive-- they're bigger, more detailed, and you need to keep track of fewer of them to create a whole map.
The only real downside to this approach is that it's a little bit harder to implement.  And I do mean "little bit". It's really not that bad.  I'll probably take this approach as I start to worry about drawing MapEntitys later this week.

As promised, see below the break for the coordinate geometry notes I made by candlelight.  I know you all are probably very excited for this.








No comments:

Post a Comment