I planed to create a game as a gift and use the really great Meggy Jr RGB platform for it. See the picture above, it’s a great game machine powered by the ATmega328P (buy it here). You can use the Arduino IDE to program it.
After assembling the device, I played a little bit around with the provided library — but was a bit disappointed by the library. This is a game device after all, but the library lack in my opinion some important things a game library need.
So I wrote a new library from scratch, using the provided schema and documentation (you can see the printed out schema in the background of the picture). Sometimes its better to start from scratch and learn from the mistakes of other libraries.
Today I release the first version of the library at GitHub here: https://github.com/LuckyResistor/LRMeggyJr. It has a proper API documentation and a few examples. I release this early, but hope to improve the documentation and add examples while I develop the actual game.
Download the release v1.2 as a ZIP
In the next sections I will write about the library version 1.0 in more detail.
The Features
- Small memory footprint.
- User customizable application frame rate (15, 30, 60, 120 FPS)
- Display with back buffer
- Display synchronization
- Synchronization with load meter to show your loop performance.
- Easy color handling using a
Color
class. - Advanced pixel operations like scrolling and fade.
- Comfortable and safe button handling
- Sound player with notes, speeds and effects.
Small Memory Footprint
Obviously I plan to use most of the memory for the game itself, therefore the library should be as small as possible. The most problematic part is the display buffer and the back buffer. You actually have to store the RGB values of all LEDs twice. The old library used one byte for each color, which is a waste. So I packed two colors in each byte, which used half of the memory. Using an additional back buffer doubled the required memory amount again. So I actually could put two display buffers in the same space where the old library put one.
Let us compare an empty sketch compiling using the two libraries.
My Library | Old Library | |
---|---|---|
Sketch Size | 2,956 bytes (9%) | 1,146 bytes (3%) |
Global variables | 233 bytes (11%) | 211 bytes (10%) |
My library uses only 22 bytes more SRAM, but almost 2 kbyte more flash memory. This is because of tables and optimized code I use to have a good performance. I tried to move as much as possible in the flash memory, because there is 32 kbyte of it, and only 2 kbyte SRAM. I hope you will safe much more memory, because my library provides high level APIs which should require less code for the game.
Game Style Main Loop
Games usually use a loop similar to the one Arduino provides, but this loop is synchronized with the display frame rate. Therefore I provide a similar mechanism and also a back buffer which avoids flickering of LEDs if the draw routine goes to the limit.
Your main loop will just look like this:
void loop() { const uint32_t frame = meg.frameSync(); meg.clearPixels(); // optional // your drawing code and logic here }
The variable “frame” will always contain the current displayed frame which you can use for animation timing. You can optionally clear the display at start of the loop. Because of the back buffer this is no problem. The frameSync
method will always wait until the display buffer is copied into the back buffer and the continue. Everything you do in the loop is always in sync and you will get smooth, flicker free animations.
You and not forced to use the frameSync()
method, you can use my library in the same way you used the old library – just there is no flicker ;-).
You can choose the application frame rate in the setup method:
void setup() { meg.setup(FrameRate120); }
The default is 30 frames per second, which should be fine for most games. But you can use 15, 30, 60 and 120 frames per second. If you choose a faster frame rate, there is less time for your drawing code and logic.
You can always monitor the performance of you code. Simple replace frameSync()
with frameSyncShowLoad()
and the extra LEDs on the top of the display will show you the actual timing of your code.
........ = 0% X....... = 12% XX...... XXXXXXX. = 87% XXXXXXXX = 100% .XXXXXXX = >100% ..XXXXXX = >200% ...XXXXX = >300% ....XXXX = >400%
Obviously, you code should always be in the range from 0-100%, and better stay below 90%.
Easy Color Handling
The old library had an easy interface where you used predefined colors to prepare a matrix and copy it at one to the display matrix. Using a back buffer this is not necessary anymore. You can use RGB colors directly, or use the predefined colors from the Color
class. As I understand, the RGB display changed over time and so changed the colors. My predefined colors just match version 1.3 of Meggy Jr, because I have no older versions here to compare.
meg.setPixel(0, 0, Color::black()); meg.setPixel(1, 0, Color::red()); meg.setPixel(2, 0, Color::orange()); meg.setPixel(3, 0, Color::yellow()); meg.setPixel(4, 0, Color::green()); meg.setPixel(5, 0, Color::blue()); meg.setPixel(6, 0, Color::violet()); meg.setPixel(7, 0, Color::white()); meg.setPixel(1, 2, Color::darkRed()); meg.setPixel(2, 2, Color::darkOrange()); meg.setPixel(3, 2, Color::darkYellow()); meg.setPixel(4, 2, Color::darkGreen()); meg.setPixel(5, 2, Color::darkBlue()); meg.setPixel(6, 2, Color::darkViolet()); meg.setPixel(7, 2, Color::gray()); meg.setPixel(7, 4, Color::maximum()); // Keep a own color const Color myColor(4, 2, 2); // Use my own color meg.setPixel(0, 7, myColor);
Colors only need two bytes of memory. You can create your own color tables without wasting much memory. I will probably add some program memory options later, to store the colors in program memory.
Advanced Pixel Methods
There are the well known methods to draw pixels and to get the color of an existing pixel. If there is a method ending in “S” this means, the input parameter are checked. “set” methods ignore wrong parameters, and “get” methods return default values. If there is no “S” method, the method should be safe if wrong values are passed to it.
void clearPixels(); void setPixel(uint8_t x, uint8_t y, const Color &color); void setPixelS(uint8_t x, uint8_t y, const Color &color); Color getPixel(uint8_t x, uint8_t y) const; Color getPixelS(uint8_t x, uint8_t y) const;
I added a method to fill blocks with a color. They will get much faster in the future. I had not the time to optimize these methods.
void fillRect(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const Color &color); void fillRectS(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const Color &color);
There is an interesting method to scroll the whole display in any direction. For diagonals you need two calls. This scrolling methods are really fast. There is no harm in calling them.
enum ScrollDirection : uint8_t { ScrollUp, ScrollDown, ScrollLeft, ScrollRight }; void scrollPixel(ScrollDirection scrollDirection);
There is also a method to fade the whole display to dark. You can create interesting effects with it:
void fadePixel();
Handling the Extra LEDs
There are the 8 extra LEDs on top of the display. I added a few methods to make handling of this LEDs as simple as possible. A set bit will light up the LED.
void setExtraLeds(uint8_t bits); uint8_t getExtraLeds() const; void enableExtraLed(uint8_t index); void disableExtraLed(uint8_t index); bool isExtraLedEnabled(uint8_t index) const;
Handling Buttons
Handling button presses is critical for each game. So I added a full set of method to make things as simple as possible. The isXXXButtonDown()
methods are working without frame synchronization and should be accurate, because the state is only checked for each frame. The isXXXButtonPressed()
and isXXXButtonReleased()
make only sense if you are using frameSync()
.
enum ButtonMask : uint8_t { ButtonA = B00000010, ButtonB = B00000001, ButtonUp = B00000100, ButtonDown = B00001000, ButtonLeft = B00010000, ButtonRight = B00100000 }; bool isAButtonPressed() const; bool isAButtonDown() const; bool isAButtonReleased() const; bool isBButtonPressed() const; bool isBButtonDown() const; bool isBButtonReleased() const; bool isUpButtonPressed() const; bool isUpButtonDown() const; bool isUpButtonReleased() const; bool isDownButtonPressed() const; bool isDownButtonDown() const; bool isDownButtonReleased() const; bool isLeftButtonPressed() const; bool isLeftButtonDown() const; bool isLeftButtonReleased() const; bool isRightButtonPressed() const; bool isRightButtonDown() const; bool isRightButtonReleased() const; uint8_t getCurrentButtonState() const; uint8_t getLastButtonState() const;
The isXXXButtonPressed()
methods report true
, if the button was not pressed in the previous frame, but now pressed down. The isXXXButtonReleased()
methods report true
if the button was pressed, but released for this frame.
getCurrentButtonState()
returns the current button bit mask for this frame, and getLastButtonState
reports the bit mask for the last frame. You can use both methods to make your own comparison logic. They don’t work on the port, so there is no flickering.
A Comfortable Sound Player
What is a game without sound. The library plays sounds, effects and melodies of any length in the background. You neither have to calculate the frequencies nor play individual notes. You can simple define your sound or effect in program memory, start it and are done. The sound will automatically play. You could even implement background music for your game.
I also added a method to check the current played note. So you can visualize the played music if you like.
The API is very simple:
void playSound(const SoundToken* sound); void stopSound(); uint8_t getPlayedNote() const;
Two important things:
- Sound must be declared in program memory
- Every sound must end with
SoundEnd
You can play the famous piece Für Elise like this:
#include <LRMeggyJr.h> using namespace lr; const SoundToken PROGMEM melody1[] = { PlaySpeed90, NoteE4, Play16, NoteDs4, Play16, // --: NoteE4, Play16, NoteDs4, Play16, NoteE4, Play16, NoteH3, Play16, NoteD4, Play16, NoteC4, Play16, // -- NoteA3, Play8, Pause16, NoteC3, Play16, NoteE3, Play16, NoteA3, Play16, // -- NoteH3, Play8, Pause16, NoteE3, Play16, NoteGs3, Play16, NoteH3, Play16, // -- NoteC4, Play8, Pause16, NoteE3, Play16, NoteE4, Play16, NoteDs4, Play16, // -- NoteE4, Play16, NoteDs4, Play16, NoteE4, Play16, NoteH3, Play16, NoteD4, Play16, NoteC4, Play16, // -- NoteA3, Play8, Pause16, NoteC3, Play16, NoteE3, Play16, NoteA3, Play16, // -- NoteH3, Play8, Pause16, NoteD3, Play16, NoteC4, Play16, NoteH3, Play16, // -- NoteA3, Play4, SoundEnd // NEVER forget the end token! }; void setup() { meg.setup(); } void loop() { const uint32_t frame = meg.frameSync(); if (meg.isAButtonPressed()) { meg.playSound(melody1); } }
You see, the core of the sound definition is a simple array of SoundToken
. Note the important PROGMEM
which will put the whole thing into the flash memory. Here a shortened list of available tokens:
enum SoundToken : uint8_t { SoundEnd = 0x00, // End of a sound definition. NoteA0 = 0x01, NoteAs0 = 0x02, NoteH0 = 0x03, NoteC1 = 0x04, NoteCs1 = 0x05, NoteD1 = 0x06, NoteDs1 = 0x07, NoteE1 = 0x08, // ... all notes up to ... NoteFs7 = 0x52, NoteG7 = 0x53, NoteGs7 = 0x54, Play1 = 0x80, // Play the note for 1/1 Play2 = 0x81, // Play the note for 1/2 Play4 = 0x82, // Play the note for 1/4 Play8 = 0x83, // Play the note for 1/8 Play16 = 0x84, // Play the note for 1/16 Play32 = 0x85, // Play the note for 1/32 Play64 = 0x86, // Play the note for 1/64 Pause1 = 0xa0, // Pause for 1/1 Pause2 = 0xa1, // Pause for 1/2 Pause4 = 0xa2, // Pause for 1/4 Pause8 = 0xa3, // Pause for 1/8 Pause16 = 0xa4, // Pause for 1/16 Pause32 = 0xa5, // Pause for 1/32 Pause64 = 0xa6, // Pause for 1/64 PlaySpeed50 = 0xb0, // Play at ~50 bpm PlaySpeed60 = 0xb1, // Play at ~60 bpm // ... many play speeds ... PlaySpeed180 = 0xbd, // Play at ~180 bpm PlaySpeed200 = 0xbe, // Play at ~200 bpm (for effects) PlaySpeed350 = 0xbf, // Play at ~350 bpm (for effects) NoteShiftOff = 0xc0, // Stop any shifting. NoteShiftUp1 = 0xc1, // Shift every note up at speed 1 NoteShiftUp2 = 0xc2, // Shift every note up at speed 2 // ... more shift speeds NoteShiftDown1 = 0xc9, // Shift every note down at speed 1 NoteShiftDown2 = 0xca, // Shift every note down at speed 2 // ... more shift speeds };
Playing sound is very easy. You can use all tokens of this list in any order. But the sound will always stop if it encounters the SoundEnd
token. To play a tone, you first choose the note:
NoteA4, SoundEnd
Now you can use as many PlayX
tokens as you like to play the note. Play1
means playing the note for a whole bar, Play2
for a half bar, and so in. It is also called a full note, or a half note, a quarter note etc. Higher values are shorter notes.
NoteA4, Play4, NoteC4, Play8, SoundEnd
To make a pause, use one of the PauseX
tokens which are working from timing like the PlayX
tokens.
NoteA4, Play4, Pause4, NoteC4, Play8, SoundEnd
To create things like a 3/4 note, just combine two PlayX
tokens:
NoteA4, Play2, Play4, Pause4, SoundEnd
You can also choose at the begin or at any place in the sound a new speed, using the PlaySpeedXX
tokens.
Some Sound Effects
Using the NoteShiftXX
tokens will not actually shift the note up and down in different speeds. You can create interesting effects with these tokens.
Conclusion
The new library for Meggy Jr RGB should provide a solid base for game development for this great device. I hope it will give a litte boost to this platform and people owning a Meggy Jr will hopefully have a second look and even write a new game for it.
It is a fresh start, but there will be hopefully more soon, about things like fonts and sprites.
I thank you for your attention and have lots of fun using this library.
Best,
Lucky Resistor