The holidays are over, the game refactored.

| No Comments | No TrackBacks

A lot of time since has passed since my last post, and it's late too. The main reason for the silence is the holidays, very little actually work transpired in respects to my thesis during that time, and this is the first week with significant progress since then, and the post is late as I had to finish up the final touches on them today. There has been a great deal of changes, and some interesting considerations, so this post is going to be a long one. It's the first one where I actually felt the need to use the feature in this blog to have a "read more" link on the front page. :)

Since the last post the entire game has been refactored, so before going into detail about changes, I'll sum up the main changes. Before the refactoring, the game was structured like your basic game, with every game character located on the pixel grid of the screen and moving every frame (so, 30 times per second), with movements being in pixels per frame and collisions being based on the actual character sprites. Now it has been split into two parts, one "game" part that handles the screen and keyboard input, and one simulation part that handles the position of the characters, the AI and the logic of the game (collisions and such). The simulation part (or at least its logic) will be shared with the ADATE system and will be translated into ADATE-ML to be used with it, so it made sense to split the code into two bits like this.

A story of two worlds

GameDiagram.png To facilitate splitting the game in two, I created a flowchart diagram of sorts to help brainstorm how to separate the game into two parts, which is included in this post for your perusal. It doesn't exactly represent the final product as some modifications needed to be done while coding, but it's close enough for government work. In addition the code is available on github for the specially interested.

The main idea of the split was to drastically minimize the amount of information shared between the two parts, as you see by the two "interface methods" on the border between them, which represent the only points of information exchange. There is the simtick method, which tells the simulation to run a new simulation cycle and returns the new state of the world (represented only by a collection of positions, whether the game is over, and whether it was won or lost), and there is the setCatMove method which is a bit of hack to allow passing in the current direction to move based on the keyboard input if the current cat AI is the one that allows keyboard control. This minimal interaction surface and the simplicity of the data that is passed through, means that it's almost trivial to exchange the current simulation world with, say, one implemented in SML/ADATE-ML.

Another big benefit of this split is that it allows you to have two different representations of the world. In the current implementation there exists the "simulation world", where the characters are represented as squares 1.5 units in size positioned in a small field that is 16 by 16 units in size, and they can only move in four directions (up, down, left, right, naturally). In addition, there is the "game world" where the characters are positioned on the screen scaled by (currently) 30 pixels per simulation units, giving you a 480 by 480 pixels screen size, and they might be positioned between two simulation world positions to allow for more fluid animation.

The two worlds also runs at different speeds. The simulation world runs, currently, at 5 cycles (or ticks) per second, and the game world runs 6 times faster, to give you the standard 30 frames per second of games. To prevent jerky motion from the fact that characters only update their position once every six frames, we try to animate the characters in the game world. This is done by interpolating between the previous and current position of the character in the simulation over the span of six frames, making them look like they're moving between them.

Entering the simulation world

The simulation world is a simple place, with very few moving bits. The bits that do move are separated into their own method called updateState (as shown in the diagram by the dashed arrows pointing towards the simulation state, representing write access). The flow is simple (again, refer to diagram for 200% of your daily allotment of arrows pointing to shapes), the characters each get a chance to run their AI code, which decides which direction they move in next out of the four possible, the state of the simulation is updated with these movements, making sure that everything stays within the world, and everything is checked for collisions. After this, either the game is declared over (a collision occured between the cat and anything else in the world) and it's marked as either win or loss (depending on what the collision was with), or nothing really happens. Either way, the simulation returns the new state of the world to whatever called it.

To facilitate this localized protection of the state, the characters themselves, along with their AI code, are not allowed to alter the state of the simulation, and the state is only updated after every character has made their decision. This makes sure that every character receives the same information, and that no AI code can "cheat" by moving others around, even by accident. If the state was updated each time a character had made an AI decision, the characters that come later would have an unfair advantage of knowing where the character is at in the "future", so any evasive action becomes useless. And not allowing AI code to cheat prevents ADATE from generating code that is a bit TOO human like, resorting to cheats to win. :)

Retreat to the game world

The game world is a bit different. The first, and most obvious, difference is that it has to deal with "the real world", not a made up virtual world, so it has to deal with pixels and keyboards, and most scary of all, people. The diagram makes it seem like this part is very simple, and for the most part it is, but dealing with the difference between the game speed and the simulation speed introduces a bit of gnarly code in the game's updateState method.

It has to deal with the game in terms of "rounds" of ticks, where there currently are 6 game ticks per simulation tick, and for each game tick that is not also a simulation tick, the game has to interpolate between the previous position of a character and the target of the character (as given by the simulation state). It also has to deal with the very last simulation tick specially, where the game is over, so that it finishes the last round of ticks (moves the characters to their final locations), and then stops animating.

Handling the keyboard intelligently is another tricky point, you can't just make the key handling code transmit a movement direction to the simulated cat on every keydown. It has to handle multiple keypresses in an intelligent fashion. The way I solved this was that every time a key is pressed down, it is added to the front of a buffer (a sort of stack, if you will), pushing any other keys currently there backwards. Now, whenever a key is released, that buffer is searched, and that key is removed from the buffer. This means that if the currently active key (the last key pressed) is released, it will mark the previously pressed key active, but if you release an inactive key, no appreciable changes happen. This means that if you're holding down the right key, then press the up key to start moving upwards, that works fine and you can then release the up key to return to moving right, but if you release the right key, nothing happens and you keep moving up, and further releasing the up button returns you to a standstill. This is, I believe, the best way to handle key presses when you're only allowed one movement direction.

Other than these points there is scant little difference in flow from the simulation world. The game is initialized with an instance of the simulation, set up with the right cat and dog AIs, and enters the game loop. Within, the game polls for events, such as keyboard presses, and handles this by sending the information to the simulation world. Then the game runs the logic by running a simulation tick if necessary, and updating the position of every character in the game (interpolating between positions if needed). The game state is then drawn to the screen and the game goes to sleep until the next frame is needed, to make sure we only draw 30 frames per second. Rinse and repeat, easy peasy.

Conclusion

This is where I'd usually showcase the game again to show how much it's changed, but the truth is, it looks pretty much exactly the same. It plays a bit differently, due to it now being constrained by the simulation world where everything only moves in four directions and can only change directions five times a second, so it is no longer possible to move diagonally, and your character will not respond "instantly" like it did with the previous version.

And this is as it should be, a refactoring should leave the product the same, mostly, and I'm pretty pleased with the result. Next up is starting on my ADATE specification and the ML translation, and getting into the actual research bits of it. :)

No TrackBacks

TrackBack URL: http://ircubic.net/cgi-bin/mt/mt-tb.cgi/10

Leave a comment

About me

I am Daniel E. Bruce, a Python and .NET coder.

Currently working on Renraku OS, in addition to some personal Python web projects, using both Django and Pylons.

More info:

About this Entry

This page contains a single entry by Daniel E. Bruce published on January 12, 2012 9:08 PM.

Game is feature complete was the previous entry in this blog.

Find recent content on the main index or look in the archives to find all content.

January 2012

Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        
OpenID accepted here Learn more about OpenID
Powered by Movable Type 5.01