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
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.
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 :)
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.
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.
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