To avoid having to create many Arduino mini-projects, we will organize our code by isolating each demonstration example in an examples
folder located at the root of the project, by an example-##.h
file that we will just have to include into the main my-stunning-game.ino
sketch. For example, let’s start with an example-00.h
file:
Don’t forget to create an empty file examples.ino
to be able to open the files contained in the examples
folder with the code editor of the Arduino IDE.
The main sketch is therefore reduced to the following single precompilation directive:
my-stunning-game.ino
#include "examples/example-00.h"
And the source code must now be written in the example-00.h
file:
examples/example-00.h
#include <Gamebuino-Meta.h>
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
}
With each new source code example-01.h
, example-02.h
, etc., you will only need to update the main sketch with the correct #include
precompilation directive.
drawImage()
method The gb.display
API exposes specialized methods that make it easy to display images on the META screen. The term method is used here because it refers to functions that are defined within an object-oriented model (the Image
class). If you are not familiar with the nuance, it doesn’t matter, methods are primarily functions in the semantics of the C++ language. Among these methods, one of them will be of particular interest to us here. It is the drawImage()
method, which exposes 3 different signatures:
void drawImage(int16_t x, int16_t y, Image& img);
void drawImage(int16_t x, int16_t y, Image& img, int16_t w2, int16_t h2);
void drawImage(int16_t x, int16_t y, Image& img, int16_t x2, int16_t y2, int16_t w2, int16_t h2);
You can see that they all accept a parameter which is a reference to an instance of the Image
class. And we saw earlier how to create such an instance by providing the constructor of the Image
class with an array of data of type uint16_t
or uint8_t
depending on the configured display mode.
For example, with the default display mode :
config-gamebuino.h
#define DISPLAY_MODE DISPLAY_MODE_RGB565
This is how to display our animated sprite on the screen using the first signature:
examples/example-01.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.drawImage(36, 28, avatar);
}
Here, the assets file is included by following the relative path from the location of the example-01.h
file to the location of the rgb565.h
file:
#include "../assets/rgb565.h"
We then create an instance of the class Image
, named avatar
, by passing to the constructor the SPRITE_DATA
array, which defines all the frames of our sprite:
Image avatar(SPRITE_DATA);
Then we display the sprite in the center of the screen:
gb.display.drawImage(36, 28, avatar);
Why at coordinates (36,28)?
In general, it is not recommended to express coordinates in raw form when the positioning is the result of a calculation related to the screen size. It is better to write things like this:
examples/example-01.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.drawImage(
.5*(SCREEN_WIDTH - AVATAR_WIDTH), // x
.5*(SCREEN_HEIGHT - AVATAR_HEIGHT), // y
avatar // image
);
}
Remember that:
SPRITE_DATA[0]
is the frame_width
of the spriteSPRITE_DATA[1]
is the frame_height
of the spriteTheir values are expressed with the uint16_t
type because it is the common type for all data stored in the SPRITE_DATA
array, but they can also be stored in constants of type uint8_t
since they are here lower than 256:
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
These constants are absolutely not necessary here, but they make the code easier to read.
Remember also that we set frame_loop
to 4
in SPRITE_DATA
, which means that the animation is automatically set to 8 fps here:
Now let’s look at the second signature of the drawImage()
method:
void drawImage(int16_t x, int16_t y, Image& img, int16_t w2, int16_t h2);
It introduces two new arguments that allow to stretch the original image on a rectangle of width w2
and height h2
. In other words, this version allows you to enlarge or shrink the image according to its horizontal and/or vertical dimensions. For example, if you want to enlarge the image uniformly by a factor of 3, simply multiply its original width and height by 3:
examples/example-02.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
It is quite possible to apply different stretching factors to the width and height of the image. But in this case, the image will necessarily be distorted, since the ratio width / height of the original image will not be respected. On the other hand, if you look closely at the signature of the drawImage()
method, you can see that the w2
and h2
arguments can be negative. And this is very interesting, because it means that we can not only stretch the image but also reverse it. For example, if we multiply the initial width of the image by the -1
factor, we get a horizontal reversal:
examples/example-03.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.drawImage(
.5*(SCREEN_WIDTH - AVATAR_WIDTH), // x
.5*(SCREEN_HEIGHT - AVATAR_HEIGHT), // y
avatar, // image
- AVATAR_WIDTH, // reversed horizontally
AVATAR_HEIGHT // only
);
}
You can imagine that this subtlety will be very useful to handle the movement of the avatar. We won’t need to modify the sprite to add images that move to the left. It will be enough to apply a horizontal reversal, and the trick will be done.
We still need to examine the last signature of the drawImage()
method:
void drawImage(int16_t x, int16_t y, Image& img, int16_t x2, int16_t y2, int16_t w2, int16_t h2);
This one will allow us to crop the display area of the image by specifying the coordinates of the upper left corner and the dimensions of the cropping rectangle. For example, let’s imagine that we want to erase the head of our avatar:
examples/example-04.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
uint8_t y = 4; // just below the head
uint8_t h = AVATAR_HEIGHT - y;
gb.display.drawImage(
.5*(SCREEN_WIDTH - AVATAR_WIDTH), // x
.5*(SCREEN_HEIGHT - h), // y
avatar, // image
0, // x2 = x
y, // y2 = y+4
AVATAR_WIDTH, // w2 = w
h // h2 = h-4
);
}
You can see that the possibilities are multiple with these 3 variants of the drawImage()
method.
Now let’s switch to an indexed color display mode:
config-gamebuino.h
#define DISPLAY_MODE DISPLAY_MODE_INDEX_HALFRES
And let’s see what happens when we display our sprite:
examples/example-05.h
#include <Gamebuino-Meta.h>
#include "../assets/indexed.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
However, we have integrated the right asset file:
#include "../assets/indexed.h"
Yes, but?… We must have forgotten something…
Remember, by default the Gamebuino palette is applied to the indexed color display modes. So don’t forget to apply your color palette to the gb.display
image during initialization:
gb.display.setPalette(PALETTE);
Okay, let’s put this into action right now:
examples/example-05.h
#include <Gamebuino-Meta.h>
#include "../assets/indexed.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
gb.display.setPalette(PALETTE);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
This time, everything is fine:
Don’t forget that the color palette applied to the gb.display
image is global. Therefore all the images you display with the drawImage()
method will necessarily share this unique palette. However, it is possible to modify this palette with the setPalette()
method at any time, but the repercussions will be propagated to all the images displayed.
It is quite different in the case of the RGB565 display mode. And I propose to examine some of the tricks that come with it.
If the color palette is common to all assets in the case of indexed color display modes, it is possible to dissociate them for each asset, and to handle them independently of the others, in the case of DISPLAY_MODE_RGB565
. Let’s go back to this mode:
config-gamebuino.h
#define DISPLAY_MODE DISPLAY_MODE_RGB565
But let’s continue to use the assets as they have been defined for the indexed color display modes by the assets/indexed.h
file.
The graphic buffer of gb.display
can store any color in the RGB565 space, but it is possible to transfer an image in indexed colors with the drawImage()
method. To do this, simply apply the desired palette just before transferring.
Let’s suppose we want to change the skin color of our avatar. We could apply the following changes to the sprite’s palette:
0x6
: 0xEE2F
⇒ 0x7B2A
0x7
: 0xFF36
⇒ 0xBCAF
examples/example-06.h
#include <Gamebuino-Meta.h>
#include "../assets/indexed.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Color palette[16];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
memcpy(palette, PALETTE, 16*sizeof(Color));
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.setPalette(palette);
palette[0x6] = (Color)0x7b2a;
palette[0x7] = (Color)0xbcaf;
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
During initialization we copy the original PALETTE
of the sprite into a palette array that we can modify during the execution of the program:
memcpy(palette, PALETTE, 16*sizeof(Color));
Then, in the loop()
function, just before displaying the sprite with drawImage()
, we assign the dynamic palette to gb.display
, which we can then modify freely:
gb.display.setPalette(palette);
palette[0x6] = (Color)0x7b2a;
palette[0x7] = (Color)0xbcaf;
You can see that you can do some pretty funny things very easily, starting from the same colormap, but applying changes to the palette just before transferring the image to gb.display
.
For example, we might want to dress the sprite with different colors:
examples/example-07.h
#include <Gamebuino-Meta.h>
#include "../assets/indexed.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t NB_TINTS = 6;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Color tint[] = { PINK, LIGHTBLUE, LIGHTGREEN, YELLOW, ORANGE, RED };
Color palette[16];
Image avatar(SPRITE_DATA);
void updatePalette(Color color) {
uint16_t c = (uint16_t)color;
uint8_t r1 = .8f * (c >> 11);
uint8_t g1 = .8f * ((c >> 5) & 0x3f);
uint8_t b1 = .8f * (c & 0x1f);
uint8_t r2 = .8f * r1;
uint8_t g2 = .8f * g1;
uint8_t b2 = .8f * b1;
palette[0xb] = (Color)((r2 << 11) | (g2 << 5) | b2);
palette[0xc] = (Color)((r1 << 11) | (g1 << 5) | b1);
palette[0xd] = color;
}
void setup() {
gb.begin();
gb.setFrameRate(32);
memcpy(palette, PALETTE, 16*sizeof(Color));
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.setPalette(palette);
for (uint8_t i=0; i<NB_TINTS; ++i) {
updatePalette(tint[i]);
gb.display.drawImage(
(i+1)*SCREEN_WIDTH/(NB_TINTS+1) - .5*AVATAR_WIDTH,
.5*(SCREEN_HEIGHT - AVATAR_HEIGHT),
avatar
);
}
}
The principle is always the same: assign the dynamic palette to gb.display
, then modify it before transferring the sprite with drawImage()
.
You can even assign a new color to the palette, which changes with each display cycle. For example, to make the sprite look like a cyborg, we can do the following:
examples/example-08.h
#include <Gamebuino-Meta.h>
#include "../assets/indexed.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Color palette[16];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
memcpy(palette, PALETTE, 16*sizeof(Color));
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
gb.display.setPalette(palette);
palette[0x0] = (Color)((uint8_t)(0x1f*(1+sin(4*PI*(gb.frameCount & 0x3f)/0x3f))) << 5);
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
One could also apply cyclic permutations on the color palette. This is exactly the technique used by talented artists like Mark Ferrari to animate beautiful pixel-art scenes, using only cycles of colors defined on a palette.
The possibilities are huge, even on a small screen like the META. Now you have the keys in hand… Trust your imagination.
We have just reviewed the different ways to display our sprite on the screen with the drawImage()
method. We didn’t worry at all about handling the animation, since it is implemented automatically thanks to the frame_loop
property that we have defined in the SPRITE_DATA
array. Until now, it had the value of 4
, which translated into an animation at the frequency of 8 fps.
If you want to double this frequency to 16 fps and give the impression that our avatar is running, simply divide frame_loop
by 2
:
assets/rgb565.h
#pragma once
const uint16_t SPRITE_DATA[] = {
// metadata
8, // frame width
8, // frame height
4, // frames
2, // frame loop <-- 4/2 = 2
0xf81f, // transparent color
0, // 16-bits color mode
// colormap
And let’s see what we get with that:
examples/example-09.h
#include <Gamebuino-Meta.h>
#include "../assets/rgb565.h"
const uint8_t SCREEN_WIDTH = 80;
const uint8_t SCREEN_HEIGHT = 64;
const uint8_t AVATAR_WIDTH = SPRITE_DATA[0];
const uint8_t AVATAR_HEIGHT = SPRITE_DATA[1];
Image avatar(SPRITE_DATA);
void setup() {
gb.begin();
gb.setFrameRate(32);
}
void loop() {
gb.waitForUpdate();
gb.display.clear();
uint8_t aw = 3*AVATAR_WIDTH;
uint8_t ah = 3*AVATAR_HEIGHT;
gb.display.drawImage(
.5*(SCREEN_WIDTH - aw), // x
.5*(SCREEN_HEIGHT - ah), // y
avatar, // image
aw, // x-stretched
ah // y-stretched
);
}
This feature is quite useful for cyclic animations where you don’t need any control. Just let gb.display
handle the animation automatically at the chosen frequency. This can be useful for example for assets like a torch whose flame would flicker over time. We wouldn’t have to worry explicitly about its animation in the main control loop of the game. gb.display
would handle it automatically for us.
But in the case of our avatar, it is the user’s interactions with the console buttons that should govern the sprite animations. We must therefore have total control over these animations. This is of course possible and we will see it later. But before that, I suggest that we examine how to set up the scenery of the game using the images in our tileset. See you in the next chapter for that.