Sunday, June 30, 2013

Tile Maps of Unusual Size

My original tinyMMO had very small zones.  There were several reasons for this.  First, drawing out large tile maps that are interesting is hugely time consuming and I was on a tight deadline.  Second (and more relevant to this post) is that large maps can drag a game's performance down if you don't know how to manage them.

If you want the camera to be centered on the player, each time the player takes a step you need to shift all of the surrounding tiles around them.  If you have a poor tile management strategy, this means all the tiles waaay over there that the player can't possibly see need to be moved as well.  This can bring your processor(s) to their knees.


Is he dancing again?  He'll be the death of us!

Many game engines handle this automatically but I am using CrafyJS, which is more of a framework than an engine.  I like it because its easy to understand and lets me tinker with stuff under the hood.

For the new game I want big, seamless maps where players can explore and feel wonder.  So I need a way to display only what the player can see and forget about everything else.  Lets use a simple example.  A 100x20 tile map with only grass and bushes.


Feel the wonder yet?

Now lets plop a character in there and try walking around.  Runs like molasses on a cold day, right?  The reason is that every frame, 50 times per second, all 2000 tiles need to be moved and redrawn.  Even the ones that are nowhere near your character. Also, at the start all 2000 tiles need to be loaded into memory at once.  Even the ones you will never see.  I want much bigger maps than this so I need a better way.

Lets come up with a way to load and show only what is around the player.  We need to break the map up into smaller chunks (I'll call them boxes) and load them one at a time.  I have found that a good box size is one third of the screen width by one third of the screen height.


Nine boxes in a screen.

While we are walking around we need all of the edges to be already loaded and ready for display.


Sixteen surrounding boxes ready for display.

Now whenever we move from one box to another, we need to load a new set of surrounding tiles and unload those that are no longer surrounding.


Out with the old, in with the new.

Since I am lazy, I just mark each box with a flag: loaded or unloaded.  Whenever I detect that the player has moved to a new box, I unload everything 3 tiles away and reload (if loaded flag not set) everything 2 tiles or closer.

There is probably a better way but as I said, lazy.

This is great.  The game is now pretty much only loading what the player can see with just a little bit of overhead.  But there is still a problem.  Creating and destroying tiles is expensive.  When the players hits the edge of a box there is a noticeable hiccup while five new boxes worth of tiles are created from scratch.  And what about the old tiles no longer in use?  Tossed away like used kleenex.  What a waste!


Those bits once proudly roamed the plains.

The old tiles are pretty much the same as the new tiles so lets recycle them!  I'll create a tile cache which is basically just an object with an array defined for each tile identifier.  My tiles ids are '0' and '#' so I have one array defined for each.  Now each time a box is cleared, I push each tile it contained into the corresponding array.  I also set its visibility to false and move it to -10000,-10000 so the framework pretty much ignores it. Each time a new tile is needed, I check the array of its type to see if any are available and pop them off and use them first.

Here is an example running and here is my finished implementation of map manager.


      function MapHandler(initX, initY) {  
           var self = this;  
           var boxStore = {};          // tiles within a map segment  
           var tileStore = {};          // cache of re-usable tiles  
           self.tileCount = 0;  
           var TILESIZE = 64;  
           var MAP_WIDTH = 100;  
           var MAP_HEIGHT = 20;  
           var tilesX = Math.ceil(VIEW_WIDTH / TILESIZE);          // screen width in tiles  
           var tilesY = Math.ceil(VIEW_HEIGHT / TILESIZE);          // screen height in tiles  
           var boxW = Math.ceil(tilesX/3);  
           var boxH = Math.ceil(tilesY/3);  
           console.log('box size ' + boxW + ' x ' + boxH);  
           var oldBox = -1;  
           self.changeLoc = function(newX, newY) {  
                var boxX = Math.floor(newX / (TILESIZE*boxW));  
                var boxY = Math.floor(newY / (TILESIZE*boxH));  
                var curBox = boxX + boxY * Math.ceil(MAP_WIDTH/boxW);  
                if (curBox != oldBox) {  
                     changeBox(curBox);  
                }  
           }  
           var changeBox = function(curBox) {  
                oldBox = curBox;  
                // clear surrounding out-of-frame boxes  
                var y = -3;  
                for (var x=-3;x<=3;x++) {  
                     clearBox(curBox + y * Math.ceil(MAP_WIDTH/boxW) + x);  
                }  
                var x = -3;  
                for (var y=-2;y<=2;y++) {  
                     clearBox(curBox + y * Math.ceil(MAP_WIDTH/boxW) + x);  
                }  
                var x = 3;  
                for (var y=-2;y<=2;y++) {  
                     clearBox(curBox + y * Math.ceil(MAP_WIDTH/boxW) + x);  
                }  
                var y = 3;  
                for (var x=-3;x<=3;x++) {  
                     clearBox(curBox + y * Math.ceil(MAP_WIDTH/boxW) + x);  
                }  
                // fill in surrounding boxes  
                for (var y=-2;y<=2;y++) {  
                     for (var x=-2;x<=2;x++) {  
                          loadBox(curBox + y * Math.ceil(MAP_WIDTH/boxW) + x);  
                     }  
                }  
           };  
           var clearBox = function(boxId) {  
                if (!boxStore[boxId] || boxStore[boxId].loaded == false) {  
                     return;          // not loaded  
                }  
                // don't block  
                setTimeout(function() {  
                     // clear old box tiles  
                     while (boxStore[boxId] && boxStore[boxId].tiles.length) {  
                          var tile = boxStore[boxId].tiles.pop();  
                          tile.visible = false;  
                          tile.attr({ x: -10000, y: -10000 });  
                          tileStore[tile.tmmo_type].push(tile);  
                     }  
                     if (boxStore[boxId]) {  
                          boxStore[boxId].loaded = false;  
                     }  
                }, 10);  
           };  
           var loadBox = function(curBox) {  
                // init box  
                if (!boxStore[curBox]) {  
                     boxStore[curBox] = { loaded: false, tiles: [] };  
                }  
                if (boxStore[curBox].loaded) {  
                     return;     // already loaded  
                }  
                var newX = (curBox % Math.ceil(MAP_WIDTH/boxW)) * boxW;  
                var newY = Math.floor(curBox / Math.ceil(MAP_WIDTH/boxW)) * boxH;  
                for (var y=newY;y < newY + boxH && y < MAP_HEIGHT && y >= 0; y++) {  
                     for (var x=newX;x < newX + boxW && x < MAP_WIDTH && x >= 0; x++) {  
                          var tileId = g_map.charAt(x + y*MAP_WIDTH);  
                          var tile = undefined;  
                          if (!tileStore[tileId]) {  
                               tileStore[tileId] = [];  
                          }  
                          if (tileStore[tileId].length) {  
                               tile = tileStore[tileId].pop();  
                               tile.visible = true;  
                          }  
                          else {  
                               if (tileId == '0') {  
                                    tile = Crafty.e('2D, ' + RENDERING_MODE + ', grass').attr({ z: 1 });  
                               }  
                               else if (tileId == '#') {  
                                    tile = Crafty.e('2D, ' + RENDERING_MODE + ', treetop_tall').attr({ z: (y*TILESIZE) * 10 + 10 });  
                               }  
                               tile.tmmo_type = tileId;  
                               self.tileCount++;  
                               g_game.status.attr({ text: 'tileCount: ' + self.tileCount });  
                          }  
                          tile.attr({ x: x*TILESIZE, y: y*TILESIZE });  
                          boxStore[curBox].tiles.push(tile);  
                     }  
                }  
                boxStore[curBox].loaded = true;  
           };  
           self.changeLoc(initX, initY);  
           return this;  
      }  

Friday, June 28, 2013

tinyMMO - a New Beginning

Now it is 2013.  My day job is now mostly front-end web app programming so my JavaScript kung-fu has gotten quite strong.  Now feels like a good time to once again take up this (hopeless?) quest to make a small, fun, good looking browser MMORPG.  But here is the downside.  I work full time at a startup, I have kids, and I live on a farm.  Not exactly a lifestyle that can tolerate hours of daily hacking.

Add some gray hairs and this is me.

So I will need some help.  First off is artwork.  Luckily I participated in the Liberated Pixel Cup, a competition run by OpenGameArt last year.   So I know there is a bunch of good quality RPG artwork available there.  


Pictured: me saving literally 100's of hours doodling

Second, this new version will use node.js on the backend.  While I have toyed with node, I am far from adept at using it.  That is where Michael will come in.  Michael is an all-around server and backend expert and has graciously offered to help out on the project.  This will fill a big void in my skillset and already Michael has created a PaaS architecture for us to use.  I know, I barely understand that sentence too.


Wait, then I attackclone the grit repo pushmerge?

Next, this game needs a storyline (ie. a reason to be).  I know from past experience that coming up with an interesting world and filling in all the necessary events and dialog is a massive undertaking.  That is why I am really happy to welcome Sarah to the team.  Sarah is a creative writing student and RPG aficionado.  She has offered to spend some of her precious summer break helping craft a world for us.

Shown: some of Sarah's awesomeness

This still leaves some holes in the team, but I feel we can make a really good start now.  I will be making updates whenever I feel we have something cool to show which I hope will be once a week or so.

Here is to hope for the hopeless!

tinyMMO the Second

In early 2012, it had been 2 years since I released tinyMMO the First.  Nobody was playing it anymore and changes to the Google App Engine architecture had broken much of the functionality.  In those two years I had entered a few game jams and I felt that my skills were now up to the task of making a proper game out of it.

Unfortunately, after moving to Sweden I had forgotten to switch my power supply from 110 to 220.  What happens when you forget something like that?


This.  This happens.

All of the source code went poof.  Not to be deterred, I forged ahead.  I could still download the client source from the web and also the maps.  App Engine now offered something called the Channel API for streaming content to clients.  Its basically an implementation of Comet, or long polling.  Not great but much better than HTTP for latency.  I put together a new server and got most things working again.


It lives!

I worked, but I was starting to run into some serious limitations of App Engine.  No threading and no web sockets meant I was not going to be able to make the game that I really wanted.  And then this happened:


Same but better.  Much, much better.

It was time for some soul-searching.  I had to admit to myself that these guys had just accomplished exactly what I was going for.  I wanted to be the first clean, fun little browser MMO.  The keyword is first.  I was no longer going to be first.  It may seem like a small setback but to petty little me it was the death of my enthusiasm for the project.  So ended development of tinyMMO the Second.

Thursday, June 27, 2013

tinyMMO the First

At the end of 2009, I was reeling from my latest failure to complete an overambitious game project.  Even a pep-talk from Notch wasn't enough to get me over the finish line.  While sobbing quietly and perusing TIGSource, I came across an upcoming game creation contest where all of the artwork would be created for you.  It was called Assemblee.  Since I have the artistic ability of a four-year-old I thought this contest would be perfect for me.

This was also about the time that Javascript had gained a reputation as a not-altogether-useless language among programmers.  I had not used Javascript much beyond simple user input forms stuff and so I thought why not combine learning a language with writing a fun game?  But what type of game should I make?  Why, an MMORPG of course.

At this point we will pause to allow you to finish laughing.  An MMO?  In one month?  In your spare time?  On an untried platform?  In a language you barely know? 

Pictured: me


I was in over my head.  My first hopes sprouted when I tried a simple test using a new-fangled (to me) technology called Ajax to send and receive data without reloading the page.  With it, I was able to send position data between two clients in real-time over HTTP!  My dots could see each other move!

All games start as dots


So I had the hard part of making an MMO (the networking) working after only one day.  At this pace I would have a working game in no time, right?  Right?  Answer: this is not right.

There is an exhausting list of hurdles to overcome when building an MMO and I won't bore you by listing them all here.  By dumb luck and willingness to burn the goodwill of my wife an kids (sorry guys) I would sit up late every night after work and successfully solve one problem.  Player inventory, map loading, camera panning, zone boundaries, player chat, mob AI, and so on each day I would cross one more off the list.  My saving grace was that I solved the biggest problems facing MMOs, namely latency and synchronization, by punting.  I set my latency target at 2 seconds (note: I didn't say milliseconds) and designed the game around that.  This does not result in an action-thriller. 


More exciting than my game.

Players move like they are swimming in tar.  But at the end of the month, I had an honest-to-goodness little MMORPG

In all her glory

It has zones, mobs, loot, player classes, boss mobs, NPCs, spells, scripted AI, inventory, leveling, melee, player equipment, chat, two teams, player cooperation and even PvP!

Was it a good game?  Not really.  But it was a complete game which is as much as I could have realistically hoped for.  I spent many evenings after releasing it playing with random players.  What a feeling to see people playing, chatting, and being entertained in a little world that I created!

The game quickly waned in popularity and unfortunately changes to Google App Engine have now broken much of the functionality.  A burned up hard-drive containing all the server source-code means there is no fixing it.  In any case, for a very brief time I could look at all that I had made and it was good!  (well, to me anyway)

A special thanks to all of the great artists who made the graphics and sound for Assemblee.  Especially dbb and oryx.

In the beginning...

Hi, I am Jonas.  I am a programmer and this blog is about my (hopeless?) quest to create a small-scale MMORPG that looks good and entertains players for more than 10 minutes.

I have been a hobbyist game programmer for an embarrassingly long time considering I don't have much to show for it.  In the last few years I have been entering more game jams to force myself to actually finish the games I start.  As anyone who has finished a game knows: the difference between 90% done and actually, totally, ship-it, hands-off done is an enormous chasm that can swallow your enthusiasm whole.

My game, tinyMMO, is currently at about 5% done.  It will be a long road...