Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

22 Jan 2012

My First Real PIX-6T4 Game: Sixty4Racer

.NET   Visual Studio   Netduino   Micro+Framework   Electronics   MCU  

After assembling my Netduino-powered PIX-6T4, I wanted to go and write a simple game. This post describes the construction of that game, including all source code.

Concept

When you have 64 monochrome red pixels, you need to keep the graphics simple. I decided on a game inspired by the classic Atari River Raid game. This is essentially a vertical scrolling game where you need to dodge obstacles with your boat. Variations included things like Spy Hunter on the C64 and many many others. Most of those games also involved shooting and enemies, but that's a but more complex than you can reasonably do on this board. I won't enable moving walls like Laser Gates, but I'll leave things open enough not to make it impossible to do that in the future.

The game had to be small enough that I could figure out the API, and then design, code, and blog about it in a single evening after my kids went to bed The PIX-6T4 is fun, but I have way too many projects on my backlog to be able to devote any significant time to it (here's a taste: a ShapeOko CNC mill, an AVR MIDI->CV Converter, the final touches on the MIDI Thru Box, several MFOS Synth Modules, Several Gadgeteer Board Concepts, a Win8 XAML book, chapters to review in my Silverlight 5 book, and much much more). In fact, that was one of the big selling points of this device: simple gameplay and quick to develop for. Combined with the great library Fabien designed, and my past experience with Netduino and, more specifically, C#, and this should be an evening project.

GamePlay

A simple dodge'em racing game. Levels get progressively longer and faster until you crash. And you will crash. And burn. And die.

The joystick lets you control moving your single pixel vehicle either left or right. You can't move in any other direction.

Progression

Most good games increase in difficulty as you progress through the game. I decided that for this one, both the number of screens in the level and the overall speed would both increase as you progress through the game.

Goals

The goal is simple: not die. The longer you last, the more levels you'll make it through, and the more you'll be able to brag to your friends.

Screen Design

Back in the 80s, in 7th grade, I used to design single-color sprites for the commodore 64. The sprites themselves were 3 bytes wide, with each pixel represented as a single bit in the byte. I used to define them on graph paper, but alas, the notebooks I filled with sprites and BASIC listings have long since disappeared.

I used to create my own fonts (programmable characters) as well. Those were done in a similar way, but on 8x8 graphs, eight bytes total for each character. Some pretty amazing games were created just with character graphics (the amazing Below the Root on the C64, as I recall, was one of them. You can see it played here.). That model is what we have to work with on the PIX-6T4, but showing the equivalent of one character at a time.

Here's an image from the Commodore 64 Programmer's Reference Manual

spacer

You can see there how the letter A is formed by the bit patterns for the eight bytes. I'm going to use a similar approach for the level design here. I'll create the "blocks" that make up the play area and then chain them together to create a playable game field.

Here here are the initial screens I created, using notepad. The only criteria I had was to make sure the middle two spots were open at the start and end of each screen. Zeroes are safe areas, ones are walls/shore/hard-deadly-smashy-things.

spacer

The more variation in screens you add, the more challenging and interesting the game can be. So, I'll be sure to leave the design open enough to allow for other screens to be easily added.

I don't need to limit myself to 8 bytes high, but I did anyway, as that will let you do things like use 8x8 pixel font editors to design the screens. In fact, here are the same levels (with a couple slight modifications), plus a bunch more, done using the pixel font editor. I got a little carried away :)

spacer

The first entry is the start screen, the second is the end screen. All the ones after that are random ones which can be shown at any point during play. There are several "break" screens in there. That is, screens that aren't particularly difficult. And then there are a number of hair-pullers, and ones with dead-ends too :) Black is wall, white is passable space.

Thanks to Fabien for pointing out this tool in the source code. It's so much easier to visualize the design when using the font editor, plus you get to export the bytes automatically. Speaking of export, here's what the tool generated for me:


// Font: ScreensSource.pf

unsigned char font[2048] =
{
0xC7, 0xC3, 0x83, 0x83, 0x81, 0x00, 0x00, 0x00, // Char 000 (.)
0x00, 0x00, 0x81, 0x81, 0xC3, 0xC3, 0xE7, 0xE7, // Char 001 (.)
0xE3, 0xCF, 0x87, 0xCF, 0x81, 0xF3, 0xC3, 0x87, // Char 002 (.)
0x87, 0xC1, 0xF1, 0xF9, 0xE1, 0x87, 0xC3, 0x81, // Char 003 (.)
0x81, 0xE7, 0x81, 0x99, 0x99, 0x81, 0xE7, 0xC3, // Char 004 (.)
0xE7, 0xE7, 0xF1, 0xC1, 0xCF, 0xE3, 0xF7, 0xE7, // Char 005 (.)
0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, // Char 006 (.)
0xE3, 0xF9, 0xFC, 0xFE, 0xFC, 0xF9, 0xF3, 0xE7, // Char 007 (.)
0xE7, 0xE7, 0xEF, 0xE7, 0xF7, 0xE7, 0xEF, 0xE7, // Char 008 (.)
0xC3, 0xFB, 0xE1, 0xEF, 0xC1, 0xFB, 0x83, 0xE7, // Char 009 (.)
0xE7, 0xC1, 0x81, 0xF9, 0xC0, 0x81, 0x9F, 0x87, // Char 010 (.)
0xE7, 0xC3, 0x00, 0x7E, 0x00, 0xE7, 0xE7, 0x81, // Char 011 (.)
0xE7, 0x81, 0x18, 0x7E, 0x3C, 0x99, 0xC3, 0xE7, // Char 012 (.)
0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x18, 0x00, // Char 013 (.)
0xC3, 0x89, 0x24, 0xB5, 0xA5, 0x2C, 0x81, 0xC3, // Char 014 (.)
0x86, 0x3C, 0x5A, 0x66, 0x3C, 0x28, 0x82, 0xC6, // Char 015 (.)
0x81, 0x00, 0x66, 0x66, 0x00, 0x99, 0x81, 0xA5, // Char 016 (.)
0xE7, 0xC3, 0xDF, 0x83, 0xF7, 0xA1, 0x8D, 0xE7, // Char 017 (.)
0xC3, 0x81, 0x00, 0x18, 0x18, 0x00, 0x81, 0xC3, // Char 018 (.)
0xE7, 0xC3, 0x99, 0x38, 0x1C, 0x99, 0xC3, 0xE7, // Char 019 (.)
0x04, 0x49, 0x02, 0x10, 0x4A, 0x00, 0x44, 0x20, // Char 020 (.)
0xA7, 0x2D, 0x6A, 0x2D, 0x26, 0x55, 0x56, 0x27, // Char 021 (.)
0xE7, 0xF7, 0xE7, 0xF7, 0xC3, 0xDB, 0xC3, 0xE7, // Char 022 (.)
0xE3, 0xCB, 0x9B, 0x3B, 0x89, 0x3D, 0x9D, 0xC5, // Char 023 (.)
0xE7, 0xC3, 0x99, 0xBD, 0x99, 0xC3, 0xE7, 0xE7, // Char 024 (.)
0xC3, 0xCF, 0xC3, 0xF3, 0xC7, 0xE3, 0xCF, 0xC7, // Char 025 (.)
0x65, 0xB6, 0x55, 0xB6, 0x65, 0xAA, 0x6D, 0xA7, // Char 026 (.)
0xE7, 0xC3, 0xE3, 0xF1, 0xF8, 0xF3, 0xC7, 0xE7, // Char 027 (.)
0xE4, 0xF4, 0xF4, 0xF4, 0xE4, 0x69, 0x0B, 0xA7, // Char 028 (.)
0x00, 0x18, 0x66, 0x5A, 0x5A, 0x66, 0x18, 0x00, // Char 029 (.)
0xA5, 0x91, 0x93, 0x97, 0x93, 0xCB, 0xE3, 0xE7, // Char 030 (.)
0x00, 0xDB, 0xC3, 0xE7, 0xE7, 0xC3, 0x99, 0x00, // Char 031 (.)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Char 032 ( )
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Char 033 (!)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Char 034 (")
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Char 035 (#)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Char 036 ($)
...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Char 255 (.)
};

I then just needed to prune it to remove the stuff I wasn't using, and make it work in my app. The font designer certainly took a lot of the work out of it, though. The end-result can be seen in the code listing for the Screens class.

With that in place, it was time to actually start creating the game.

First Iteration: Creating the scrolling playfield

I called my project PeteBrown.Sixty4Racer. Just as in the previous post, I copied over the Program.cs file from another project and used that as the start. Please read my previous post to see what references you need and whatnot.

The first class I created was the one that manages the creation of the screens.

The Screens Class

The Screens class is responsible for storing all the known screens, and then assembling them into a level when requested. It knows how large the screens are, and how large a single level's full set of screens is.

using System;
using Microsoft.SPOT;
using netduino.helpers.Imaging;

namespace PeteBrown.Sixty4Racer
{
class Screens
{
// Font: ScreensSource.pf
private const int ScreenCount = 32;
private const int ScreenHeight = 8;
private readonly byte[] _screenBytes = new byte[ScreenCount * ScreenHeight]
{
0xC7, 0xC3, 0x83, 0x83, 0x81, 0x00, 0x00, 0x00, // Start Screen
0x00, 0x00, 0x81, 0x81, 0xC3, 0xC3, 0xE7, 0xE7, // Stop Screen
0xE3, 0xCF, 0x87, 0xCF, 0x81, 0xF3, 0xC3, 0x87, // Char 002 (.)
0x87, 0xC1, 0xF1, 0xF9, 0xE1, 0x87, 0xC3, 0x81, // Char 003 (.)
0x81, 0xE7, 0x81, 0x99, 0x99, 0x81, 0xE7, 0xC3, // Char 004 (.)
0xE7, 0xE7, 0xF1, 0xC1, 0xCF, 0xE3, 0xF7, 0xE7, // Char 005 (.)
0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, 0xE7, // Char 006 (.)
0xE3, 0xF9, 0xFC, 0xFE, 0xFC, 0xF9, 0xF3, 0xE7, // Char 007 (.)
0xE7, 0xE7, 0xEF, 0xE7, 0xF7, 0xE7, 0xEF, 0xE7, // Char 008 (.)
0xC3, 0xFB, 0xE1, 0xEF, 0xC1, 0xFB, 0x83, 0xE7, // Char 009 (.)
0xE7, 0xC1, 0x81, 0xF9, 0xC0, 0x81, 0x9F, 0x87, // Char 010 (.)
0xE7, 0xC3, 0x00, 0x7E, 0x00, 0xE7, 0xE7, 0x81, // Char 011 (.)
0xE7, 0x81, 0x18, 0x7E, 0x3C, 0x99, 0xC3, 0xE7, // Char 012 (.)
0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x3C, 0x18, 0x00, // Char 013 (.)
0xC3, 0x89, 0x24, 0xB5, 0xA5, 0x2C, 0x81, 0xC3, // Char 014 (.)
0x86, 0x3C, 0x5A, 0x66, 0x3C, 0x28, 0x82, 0xC6, // Char 015 (.)
0x81, 0x00, 0x66, 0x66, 0x00, 0x99, 0x81, 0xA5, // Char 016 (.)
0xE7, 0xC3, 0xDF, 0x83, 0xF7, 0xA1, 0x8D, 0xE7, // Char 017 (.)
0xC3, 0x81, 0x00, 0x18, 0x18, 0x00, 0x81, 0xC3, // Char 018 (.)
0xE7, 0xC3, 0x99, 0x38, 0x1C, 0x99, 0xC3, 0xE7, // Char 019 (.)
0x04, 0x49, 0x02, 0x10, 0x4A, 0x00, 0x44, 0x20, // Char 020 (.)
0xA7, 0x2D, 0x6A, 0x2D, 0x26, 0x55, 0x56, 0x27, // Char 021 (.)
0xE7, 0xF7, 0xE7, 0xF7, 0xC3, 0xDB, 0xC3, 0xE7, // Char 022 (.)
0xE3, 0xCB, 0x9B, 0x3B, 0x89, 0x3D, 0x9D, 0xC5, // Char 023 (.)
0xE7, 0xC3, 0x99, 0xBD, 0x99, 0xC3, 0xE7, 0xE7, // Char 024 (.)
0xC3, 0xCF, 0xC3, 0xF3, 0xC7, 0xE3, 0xCF, 0xC7, // Char 025 (.)
0x65, 0xB6, 0x55, 0xB6, 0x65, 0xAA, 0x6D, 0xA7, // Char 026 (.)
0xE7, 0xC3, 0xE3, 0xF1, 0xF8, 0xF3, 0xC7, 0xE7, // Char 027 (.)
0xE4, 0xF4, 0xF4, 0xF4, 0xE4, 0x69, 0x0B, 0xA7, // Char 028 (.)
0x00, 0x18, 0x66, 0x5A, 0x5A, 0x66, 0x18, 0x00, // Char 029 (.)
0xA5, 0x91, 0x93, 0x97, 0x93, 0xCB, 0xE3, 0xE7, // Char 030 (.)
0x00, 0xDB, 0xC3, 0xE7, 0xE7, 0xC3, 0x99, 0x00 // Char 031 (.)
};

private RacerGame _parentGame;

public Screens(RacerGame parentGame)
{
_parentGame = parentGame;
}


public int GetLevelScreenCount(int level)
{
return level * 6 + 2;
}

public int GetLevelPixelHeight(int level)
{
return GetLevelScreenCount(level) * ScreenHeight;
}

public Composition GetLevelComposition(int level)
{
int levelScreenCount = GetLevelScreenCount(level);

byte[] bytes = new byte[ScreenHeight * levelScreenCount];

int b = 0;
int startIndex = 0;

for (int i = 0; i < levelScreenCount; i++)
{
if (i == 0)
{
// finish screen. This is the second screen in the array
startIndex = ScreenHeight;
}
else if (i == levelScreenCount - 1)
{
// start screen. First screen in the array
startIndex = 0;
}
else
{
// regular random screen
int selectedScreen = _parentGame.Random.Next(ScreenCount - 2) + 2;

startIndex = ScreenHeight * selectedScreen;
}

for (int a = startIndex; a < startIndex + ScreenHeight; a++, b++)
{
bytes[b] = _screenBytes[a];
}
}

return new Composition(bytes, 8, levelScreenCount * ScreenHeight);
}

}
}

The class has the primary function GetLevelComposition which builds the background bitmap used to populate all the little red LEDs. The Composition class is one of the netduino helper classes in the PIX-6T4 library.

The next class is the main game class.

The RacerGame class

The RacerGame class is responsible for all the game-specific logic. It handles scrolling the level, displaying messages, and (eventually), moving the player around the screen and handling collision detection. In this first iteration, all it does is display the level number and then scroll the level at the appropriate speed. The first level may seem to scroll painfully slow, but trust me, throw a player pixel in there and you'll change your mind.

using System;
using Microsoft.SPOT;
using netduino.helpers.Fun;
using netduino.helpers.Imaging;
using System.Threading;

namespace PeteBrown.Sixty4Racer
{
class RacerGame : Game
{
private Screens _screens;

public RacerGame(ConsoleHardwareConfig config)
: base(config)
{
_screens = new Screens(this);

DisplayDelay = 25;
}

protected override void OnGameEnd()
{
}


protected override void OnGameStart()
{
base.OnGameStart();
}


private int _currentLevel = 0;
private float _currentlevelSpeedIncrement = 0f;
private float _exactScrollPosition = 0.0f;

private int CurrentWorldLine
{
get { return (int)_exactScrollPosition; }
}

private void InitializeLevel()
{
World = _screens.GetLevelComposition(_currentLevel);
_exactScrollPosition = _screens.GetLevelPixelHeight(_currentLevel) - 1;
_currentlevelSpeedIncrement = 0.02f * (float)_currentLevel; // tweak this to change speed
}

private void CompleteLevel()
{
// TODO: Play some music

Thread.Sleep(1500);
ScrollMessage(" Level Up!");
}

private void IntroduceLevel()
{
// show the level number
Hardware.Matrix.Display(SmallChars.ToBitmap(_currentLevel / 10, _currentLevel % 10));
Thread.Sleep(1000);
}

private bool _firstTime = true;
public override void Loop()
{
if (CurrentWorldLine == 0 || _firstTime)
{
if (!_firstTime)
{
CompleteLevel();
}

// Next Level
_currentLevel++;
InitializeLevel();
IntroduceLevel();

_firstTime = false;
}
else
{
_exactScrollPosition -= _currentlevelSpeedIncrement;
}

// draw the frame
Hardware.Matrix.Display(World.GetFrame(0, CurrentWorldLine));
}
}
}

The RacerGame class knows a level is complete when the top row of pixels is the first row of pixels in the level. Remember, since we're scrolling the screen down, we're moving from the bottom to the top of the rows of pixels. Get to the top, and the level is done.

Minor But Fatal Bitmap Class Bug

The GetFrame method in the version of the source code I used has an incorrect check. Where it says if (x >= 0 && x + FrameSize < Width) it should say if (x >= 0 && x + FrameSize <= Width). Notice the <= instead of <. The incorrect check means that any bitmaps that are exactly 8 pixels wide, like the one here, simply won't display. This bug may be fixed in the source you get, but you'll want to double-check. I'm just glad I had the source to refer to (and fix!). Isn't OSS great? :)

When the level is complete, you get the scrolling "Level Up!" message. Click the joystick button and you're good to try the next level.

Now is a good time to run the game and see what it looks like. You should see a vertically scrolling playfield, but no player just yet.

Second Iteration: Adding in the player

The PIX-6T4 libraries have built-in the concept of a PlayerMissile. This is a single pixel on the playfield. It may move, so it has X and Y speed. You can show or hide it, so it has Visibility. And most importantly, it has collision detection with other PlayerMissile instances. For our game, we're not going to use that, since we're looking for collision detection with the background. So, a little manual detection is in order.

Bitmap Class Bugs

While coding this game, I found a few more bugs in the Bitmap class. I've alerted Fabien, so you should see an updated set of source code soon. The first bug is that the GetPixel and SetPixel methods do some incorrect bounds checking up front. Y is checked against width rather than height, and the opposite happens with X. We're going to use GetPixel in a moment, so fixing this is important.

Here's the updated RacerGame class

using System;
using Microsoft.SPOT;
using netduino.helpers.Fun;
using netduino.helpers.Imaging;
using System.Threading;

namespace PeteBrown.Sixty4Racer
{
class RacerGame : Game
{
private Screens _screens;
private PlayerMissile _ship;

public RacerGame(ConsoleHardwareConfig config)
: base(config)
{
_screens = new Screens(this);

DisplayDelay = 25;
}

protected override void OnGameEnd()
{
ScrollMessage(" Game Over");
}


protected override void OnGameStart()
{
base.OnGameStart();

_ship = new PlayerMissile()
{
Name = "ship",
IsEnemy = false,
X = 3,
IsVisible = true,
VerticalSpeed = 0,
};

ScrollMessage(" Sixty4Racer!");
}


private int _currentLevel = 0;
private float _currentlevelSpeedIncrement = 0f;
private const float BaseShipSpeed = 0.25f; // speed ship moves across the screen
private float _exactScrollPosition = 0.0f;

private int CurrentWorldLine
{
get { return (int)_exactScrollPosition; }
}

private void InitializeLevel()
{
World = _screens.GetLevelComposition(_currentLevel);
World.AddMissile(_ship);

_exactScrollPosition = _screens.GetLevelPixelHeight(_currentLevel) - 1;
_currentlevelSpeedIncrement = 0.02f * (float)_currentLevel; // tweak this to change speed

_ship.Y = CurrentWorldLine + 7; // always be on bottom line
_ship.X = 3;
}

private void CompleteLevel()
{
// TODO: Play some music

Thread.Sleep(1500);
ScrollMessage(" Level Up!");
}

private void IntroduceLevel()
{
// show the level number
Hardware.Matrix.Display(SmallChars.ToBitmap(_currentLevel / 10, _currentLevel % 10));
Thread.Sleep(1000);
}

private bool CheckForCollision()
{
return World.Background.GetPixel(_ship.X, _ship.Y);
}

private bool _firstTime = true;
public override void Loop()
{
_ship.HorizontalSpeed = (float)Hardware.JoystickLeft.XDirection * BaseShipSpeed;
_ship.Move();

if (_ship.X < 0) _ship.X = 0;
if (_ship.X > 7) _ship.X = 7;

if (CurrentWorldLine == 0 || _firstTime)
{
if (!_firstTime)
{
CompleteLevel();
}

// Next Level
_currentLevel++;
InitializeLevel();
IntroduceLevel();

_firstTime = false;
}
else
{
_exactScrollPosition -= _currentlevelSpeedIncrement;

_ship.Y = CurrentWorldLine + 7; // always be on bottom line
}

// draw the frame
Hardware.Matrix.Display(World.GetFrame(0, CurrentWorldLine));

if (CheckForCollision())
{
// game over
Stop();
}
}
}
}

This version of the source adds in the player pixel

I represent the "This Game is Too Damn Hard" Party

At this point, after adding the player and collision detection, I realized just how hard this game is! If you want to make it easier, I suggest editing the levels to make them a little more open, like a minimum of two pixels wide for any path, and no sudden moves left or right. You may even want to segment the level array into easy/medium/hard, and then change the mix of screens from level to level.

With all that in place, now is another great time to try out the game. It has all the main functionality at this point; anything else is just polish.

Third Iteration: Polishing

The first thing I realized was that it was really hard to make out the player pixel in the sea of red. That's to be expected on a monochrome display at 8x8 resolution. The approach I came up with to make it a bit easier is to simply flicker the player pixel. Each time the game loop executes, I toggle the visibility of the ship PlayerMissile to give it a nice seizure-inducing flicker.

public override void Loop()
{
_ship.HorizontalSpeed = (float)Hardware.JoystickLeft.XDirection * BaseShipSpeed;
_ship.Move();

// make the ship blink so we can see it
_ship.IsVisible = !_ship.IsVisible;

if (_ship.X < 0) _ship.X = 0;
if (_ship.X > 7) _ship.X = 7;

if (CurrentWorldLine == 0 || _firstTime)
{
if (!_firstTime)
{
CompleteLevel();
}

// Next Level
_currentLevel++;
InitializeLevel();
IntroduceLevel();

_firstTime = false;
}
else
{
_exactScrollPosition -= _currentlevelSpeedIncrement;

_ship.Y = CurrentWorldLine + 7; // always be on bottom line
}

// draw the frame
Hardware.Matrix.Display(World.GetFrame(0, CurrentWorldLine));

if (CheckForCollision())
{
// game over
Stop();
}
}

Sound Effects

The next thing this needed was some sound effects. When you move left or right a full pixel, it would be helpful to play a little blip. Maybe that's annoying? Nah! Let's do it.

This is really easy to do. I simply keep track of the last position the ship moved to. If the new position is different from the old one, I call the Beep function to make a noise. The actual implementation here results in an extra blip on startup, but I'm cool with that :)

private void Blip()
{
Beep(200, 30);
}

private bool _firstTime = true;
private int _oldShipX = 0;
public override void Loop()
{
_ship.HorizontalSpeed = (float)Hardware.JoystickLeft.XDirection * BaseShipSpeed;
_ship.Move();

if (_ship.X != _oldShipX)
{
Blip();
_oldShipX = _ship.X;
}


// make the ship blink so we can see it
_ship.IsVisible = !_ship.IsVisible;

...

}

Next, we need to do a little bit of exploding when you hit the wall. And yes, you will hit the wall.

The Explosion Sprite

The final thing was to add a little bit of an explosion when the player hits the wall. I designed the sprite data using the same PixelFont editor.

spacer

I then copied just those 8 x 8 bytes of data into the initializer for the sprite class. Here's the Sprite class itself (without the initializer code).

using System;
using Microsoft.SPOT;

namespace PeteBrown.Sixty4Racer
{
class Sprite
{
private readonly byte[] _frames;
private const int Width = 8;
private const int Height = 8;
private int _frameCount;

public Sprite (byte[] frames)
{
_frames = frames;

_frameCount = _frames.Length / Height;
}

public int FrameCount
{
get { return _frameCount; }
}

public int CurrentFrame
{
get { return _currentFrame; }
}

public void Reset()
{
_currentFrame = 0;
}

private int _currentFrame = 0;
public byte[] GetNextFrame()
{
return GetFrame(_currentFrame++);
}

public byte[] GetFrame(int index)
{
if (index >= FrameCount)
throw new IndexOutOfRangeException("Index must be less than " + FrameCount);

byte[] b = new byte[Height];

for (int i = 0; i < Height; i++)
{
b[i] = _frames[index * Height + i];
}

return b;
}
}
}

(The initialization code will be in the next listing.)

Next, I added in a little music. How about some Sad Trombone when you die? Sounds good to me. By looking at the Pac Man music example, and the source code for the RttlSong class, I was able to figure out how to build a string of notes to play the sad trombone sound asynchronously while the explosion happens. Rub it in!

Sprite _explosion = new Sprite(new byte[]
{
0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x18, 0x24, 0x24, 0x18, 0x00, 0x00,
0x08, 0x3C, 0x72, 0x5B, 0xDE, 0x62, 0x3C, 0x08,
0x3C, 0xD2, 0xAD, 0xFE, 0x5F, 0xF5, 0x56, 0x28,
0x00, 0x3C, 0x56, 0x7A, 0x6E, 0x72, 0x3C, 0x00,
0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00,
0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x08, 0x10, 0x00, 0x00, 0x00
});

protected override void OnGameEnd()
{
var song = new RttlSong("SadTrombone:d=4,o=4,b=40:32d4,32c#4,32c4,4b3");
var thread = song.Play(Hardware.Speaker, true);

for (int i = 0; i < _explosion.FrameCount; i++)
{
Hardware.Matrix.Display(_explosion.GetNextFrame());
Thread.Sleep(200);
}

ScrollMessage(" Game Over");
}

You can go further, of course, but I'm going to wrap it up at that.

Final Steps

The final things to do are to create the manifest file and bitmap which will be used on the SD card. I'll need to check with Fabien to see what the exact format of the .bin file is, but I suspect it's just the 8 bytes of data formatted like all the other bitmap data in this application. I'm also not sure if he has a nice little app to write that data out, or convert from a bitmap, or something else. I ended up just using a hex editor to recreate the pattern from one of the images I created in the font editor.

spacer

The assembly itself needs to be loaded from the SD card as a .pe file, as Fabien explains in his blog post on dynamically loading assemblies from an SD card. Be sure to comment out the #define dev before your final compile. Make sure you take the .pet file from the LE (little endian) folder, not the BE (big endian) folder. The files look identical other than byte order, but the BE version will not work.

I then created a cartridge.txt manifest file for my game. The contents of that consist of a single line:


assembly:file=PeteBrown.Sixty4Racer.pe;name=PeteBrown.Sixty4Racer;version=1.0.0.0;class=PeteBrown.Sixty4Racer.Program;method=Run

They must be in a folder with the same name as the root file name for the image. So, these go in a PeteBrown.Sixty4Racer folder.

Finally, don't forget to reflash the PIX-6T4 with the main ConsoleBootLoader application from your solution.

After that, pop the SD card into the PIX-6T4 and have a blast!

What You Can Do

This came is completely free and open source. While I'd love credit for the initial work, it's not a requirement. Go ahead and do whatever you'd like with the source and have a blast :)

Here's a video of the game in action.

PIX-6T4 Netduino Mini Game
Share |

Source Code and Related Media

Download /media/82945/petebrown.sixty4racer.zip
posted by Pete Brown on Sunday, January 22, 2012
filed under: .NET   Visual Studio   Netduino   Micro+Framework   Electronics   MCU  

1 comment for “My First Real PIX-6T4 Game: Sixty4Racer”

  1. spacer Reply
    Paul Kohlersays:
    Thursday, January 26, 2012 at 10:16:52 PM
    Now that brings back some memories!!

    Interesting post Pete :-)

Comment on this Post

Remember me

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.