This is replaced by swfobjct.
Jul 04 2008

Separating Game Logic and the User Interface: Part 1

Chess Pieces

Keeping your user interface from getting tangled up with your game logic is one of the greatest gifts you can give yourself.

In this guest article, Andrew Pellerano gives us an introduction into the art of separating business logic and UI code.

Keeping your user interface from getting tangled up with your game logic is one of the greatest gifts you can give yourself. Instead of lecturing you any more than that, I’m just going to bombard you with examples.

Event listeners are so particular

Today we will pretend that we are game programmers working on a new game. We are creating a chess game and this game needs to be able to pause when the user clicks a button. That’s easy enough, let’s just start writing!

private function onPauseClick( a_event: MouseEvent ): void 
{ 
    m_gameTimer.pause(); 
    m_musicPlayer.pause(); 
    m_playerInput.pause(); 
    //players can't look at a paused game, that's cheating! 
    m_gameBoard.visible = false; 
    m_pauseOverlay.visible = true; 
}

That works great. However, a few days later we decide that our game needs a how-to-play screen that tells players legal chess moves. When the user brings up that screen, the game should also pause. Okay, so…

private function onShowTutorialClick( a_event: MouseEvent ): void 
{ 
    this.onPauseClick(); 
    m_tutorialPanel.visible = true; 
}

Oh, man, it wants that event… Well, we don’t need really use it…

this.onPauseClick( null );

There, that’ll do. Or will it?

The plot thickens

Let’s take a look at another one of our event listeners. This one highlights the square that the player clicked on.

private function onGameBoardClick( a_event: MouseEvent ): void 
{ 
    var col: int = a_event.localX / TILE_WIDTH; 
    var row: int = a_event.localY / TILE_HEIGHT; 

    var tile: Sprite = getTileAt( col, row ); 
    tile.graphics.beginFill( 0xFF0000 ); 
    tile.graphics.drawRect( col * TILE_WIDTH, row * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT ); 
    tile.graphics.endFill(); 
}

We’ve decided that when the opponent AI is taking its turn, we should highlight the square of the piece it is moving, to make it clearer for the player trying to follow the AI’s moves. Well, we’ve got something that already does tile highlighting so let’s just use that.

this.onGameBoardClick();

There’s that pesky event again.

this.onGameBoardClick( null );

Uh oh, it didn’t work this time! We’re getting a null error at runtime from the first line of the function, because it wants that event to have a localX value. Let’s try…

private function aiThink(): void 
{ 
    var enemyCol: int = 2; 
    var enemyRow: int = 1; 
    this.onGameBoardclick( new MouseEvent( MouseEvent.CLICK, true, false, enemyCol * TILE_WIDTH, enemy_row * TILE_HEIGHT ) ); 
}

I bet you’d need to check the documentation to find out if that line was error free or not. Did I just simulate a mouse click for the AI? This is getting ridiculous. Let’s back up, because we broke the cardinal rule of writing user interface code. UI code should never mingle with game logic code! If you looked back at the examples you’d see that we broke this rule almost 100% of the time.

Let’s be proper

private function onPauseClick( a_event: MouseEvent ): void
{
    pause();
}
    
private function onShowTutorialClick( a_event: MouseEvent ): void
{
    pause();
    m_tutorialPanel.visible = true;
}
    
private function onGameBoardClick( a_event: MouseEvent ): void
{
    var col: int = a_event.localX / TILE_WIDTH;
    var row: int = a_event.localY / TILE_HEIGHT;
    highlightTileAt( col, row );
}
    
private function aiThink(): void
{
    var enemyCol: int = 2;
    var enemyRow: int = 1;
    highlightTileAt( enemyCol, enemyRow );
}
    
public function pause(): void
{
    m_gameTimer.pause();
    m_musicPlayer.pause();
    m_playerInput.pause();
    
    //players can't look at a paused game, that's cheating!
    m_gameBoard.visible = false;
    m_pauseOverlay.visible = true;
}
    
public function highlightTileAt( a_col: int, a_row: int ): void
{
    var tile: Sprite = getTileAt( a_col, a_row );
    tile.graphics.beginFill( 0xFF0000 );
    tile.graphics.drawRect( a_col * TILE_WIDTH, a_row * TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT );
    tile.graphics.endFill();
}

As you can see from the example above, all our functions can now be placed into two categories. The first category is functions that deal with UI stuff like events and mouse click locations. The second category is functions that deal with game logic like pausing timers, hiding the screen, and highlighting tiles.

That’s all there is to learn in regards to separating the UI and game logic. It really is that simple. If you apply these techniques you’ll find that your games are much faster to write and easier to debug. Your clothes will also come out of the washer with less stains, your fruits will stay fresher longer, and you’ll have a much easier time learning how to play accordian. Depending on how much of that you believed, stay tuned for the second article in this series where I’ll either teach you more flash UI best practices or swindle you out of millions!

Related Posts:

Comments

No comments yet.

Leave a comment

You must be logged in to post a comment.