Homebrew 2600

Return to Main

Making a Simple 2600 Game

Creating games is not the easiest of undertakings. Doing so in 6502 assembly language is much harder. One technique that is often used is to write pseudo code before writing the real code. With assembly language, however, the pseudo code is pretty similar to what you would write in a higher level language. For that reason, I will often work out the more difficult parts of a problem using a higher level language. As it is also nice to have a game that is playable in a browser, using a language like JavaScript for prototyping a game before writing the real game makes some sense. To aid in doing that, I have created a simple TIA library that lets you work in a similar fashion to what you would do on 2600 hardware.

I have written a simple game that demonstrates how this works. As it is written in JavaScript using the simple TIA simulation it is much easier to understand what is going on. Play Game Demo

The full game code for this game can be found by looking at the source for this page or by downloading my TIALib0_01.zip file.

The initialization section just sets up variables for the game so will not be described here. The main loop of the game starts by calling my rendering routine. As the position of the sprites do not change mid-stream for this game, the current positions of the sprite are set at the top of display.

	scanline.setPlayer0SpriteQuick(1, false, 0, enemyX);
	scanline.setPlayer1SpriteQuick(1, false, 0, playerX);
	scanline.setMissile0Quick(1, false, enemyShotX);
	scanline.setMissile1Quick(1, false, playerShotX);
  

We next run through all the scan lines, using simple logic to determine what to draw. When porting to assembly language, in order to keep the number of cycles down, the display code might break the display into three sections. The first where we know we are drawing the enemy so just fill in it's data and draw the player shot if in that range. The second section would be the middle of the screen and just draw the missiles if visible. The bottom range would always draw the player and the enemy missle if it is visible.

	// loop through the scan-lines
	for (var cntr = 0; cntr < 192; ++cntr) {
		var temp;
		
		// player rendering 
		if ( (cntr >= PLAYER_START_Y) && (cntr <= PLAYER_END_Y) ) {
			temp = Math.floor((cntr - PLAYER_START_Y) / 2)
			if (playerDead) {
				scanline.setPlayer1SpriteData(DEAD_SPRITE[temp]);
			} else {
				scanline.setPlayer1SpriteData(PLAYER_SPRITE[temp]);
			}
		} else {
			scanline.setPlayer1SpriteData(0);
		}
		// enemy rendering
		if ( (cntr >= ENEMY_START_Y) && (cntr <= ENEMY_END_Y) ) {
			temp = Math.floor((cntr - ENEMY_START_Y) / 2)
			if (enemyDead) {
				scanline.setPlayer0SpriteData(DEAD_SPRITE[temp]);
			} else {
				scanline.setPlayer0SpriteData(PLAYER_SPRITE[temp]);
			}
		} else {
			scanline.setPlayer0SpriteData(0);
		}
		
		// bullet 
		var showMissile1 = ((cntr >= playerShotY) && (cntr <= (playerShotY+8)));
		var showMissile0 = ((cntr >= enemyShotY) && (cntr <= (enemyShotY+8)));
		scanline.enableMissiles(showMissile0, showMissile1);

		// api routine to actually draw line to screen
		scanline.renderToCanvas(ctx, 0,cntr*2,4,2);
  

Normally the game logic will be placed both before and after the rendering routines (as there are large blocks of cycles available in both sections) but for simplicity I am having this code as a single block. When actually writing the assembly language this code may end up being split into two chunks if it is too large to run in either the syncing section or the overdraw section.

The game has four states, with the appropriate code being selected (via a switch statement in JavaScript, but using a series of CMP and branching in assembly language.

The waiting for game state simply does nothing until the fire button is pressed.


			case WAITING_FOR_GAME :
				if (firing)
					gameState = STARTING_GAME;
				break;
  

Starting game simply waits for the button pressed above to be released and then sets the game to its starting state.

			case STARTING_GAME :
				if (firing == false) {
					enemyX = PLAYER_X_START;
					enemyShotY = SHOT_INACTIVE_Y;
					playerX = PLAYER_X_START
					playerShotY = SHOT_INACTIVE_Y;
					enemyDead = false;
					playerDead = false;
					gameState = PLAYING_GAME;
				}
				break;
  

The most complex state is playing game. It first adjusts the player position based on the joystick input. If the player's bullet is active, it moves the bullet and checks if it hits the enemy which ends the game, otherwise it checks to see if the player is pressing the fire button in which case it launches the bullet above the player. Once the player work is done, it is time for the enemy logic.

The enemy simply moves toward a target spot. If it has reached that target spot it sees if it can fire and does so if it can otherwise just waits. The Enemy bullet is moved and checked to see if it hits the player in which case it ends the game.

			case PLAYING_GAME:
				//adjust player position based on keys being pressed
				if (movingLeft)
					playerX = Math.max(1, playerX - 1);
				if (movingRight)
					playerX = Math.min(160-8, playerX + 1);

				//  player bullet movement	
				if (playerShotY < SHOT_INACTIVE_Y) {
					playerShotY -= 4;
					if (playerShotY < 0) 
						playerShotY = SHOT_INACTIVE_Y;
					if ((playerShotY >= ENEMY_START_Y) && (playerShotY <= ENEMY_END_Y)
							&& (playerShotX >= enemyX) && (playerShotX < (enemyX+8))) {
						enemyDead = true;
						playerShotY = SHOT_INACTIVE_Y;
						gameState = GAME_OVER;
					}
				//else if player pressing shoot button
				} else if (firing == true) {
					// set player shot to just above player position
					playerShotY = PLAYER_START_Y - 8;
					playerShotX = playerX + 4;
				}
				
				// Enemy AI 
				if (enemyX == enemyTarget) {
					if (enemyShotY >= SHOT_INACTIVE_Y) {
						enemyShotY = ENEMY_END_Y;
						enemyShotX = enemyX + 4;
						enemyTarget = Math.floor(Math.random() * 152);
					}
				} else {
					if (enemyX < enemyTarget)
						++enemyX;
					else
						--enemyX;
				}
				
				// Enemy shot hanling
				if (enemyShotY < SHOT_INACTIVE_Y) {
					enemyShotY += 4;
					if ((enemyShotY >= PLAYER_START_Y) && (enemyShotY <= PLAYER_END_Y) &&
						(enemyShotX > playerX) && (enemyShotX < (playerX+8))) {
						playerDead = true;
						gameState = GAME_OVER;
						enemyShotY = SHOT_INACTIVE_Y;
					}
				}
				break;
  

Finally, game over simply waits until the fire button is released in the case where the player was holding down the fire button when the game ended.

			case GAME_OVER:
				if (firing == false)
					gameState = WAITING_FOR_GAME;
				break;
  

And that is all there is to a simple game. Converting the above game to assembly language will be interesting and I will post additional articles once that has been done.