Hello Graphics and Keypad
Now that you have a stable VBlank loop, the next step is drawing a visible shape and moving it.
This page pairs two tiny demos that share the same consteval circle sprite:
demo_hello_graphics.cpp: draw the sprite in the centre.demo_hello_keypad.cpp: move the same sprite with the D-pad.
Part 1: draw a shape
#include <gba/bios>
#include <gba/color>
#include <gba/interrupt>
#include <gba/shapes>
#include <gba/video>
#include <cstring>
using namespace gba::shapes;
using gba::operator""_clr;
namespace {
constexpr auto spr_ball = sprite_16x16(circle(8.0, 8.0, 7.0));
} // namespace
int main() {
gba::irq_handler = {};
gba::reg_dispstat = {.enable_irq_vblank = true};
gba::reg_ie = {.vblank = true};
gba::reg_ime = true;
gba::reg_dispcnt = {
.video_mode = 0,
.linear_obj_tilemap = true,
.enable_obj = true,
};
gba::pal_bg_mem[0] = "#102040"_clr;
gba::pal_obj_bank[0][1] = "white"_clr;
auto* objDst = gba::memory_map(gba::mem_vram_obj);
std::memcpy(objDst, spr_ball.data(), spr_ball.size());
const auto tileIdx = gba::tile_index(objDst);
auto obj = spr_ball.obj(tileIdx);
obj.x = (240 - 16) / 2;
obj.y = (160 - 16) / 2;
obj.palette_index = 0;
gba::obj_mem[0] = obj;
for (int i = 1; i < 128; ++i) {
gba::obj_mem[i] = {.disable = true};
}
while (true) {
gba::VBlankIntrWait();
}
}
What is happening?
- The setup is the same as Hello VBlank: initialise interrupts and wait on
gba::VBlankIntrWait()in the main loop. sprite_16x16(circle(...))creates the sprite tile data at compile time (consteval).- We copy that tile data into OBJ VRAM, then place it with
obj_mem[0]. - The display runs in Mode 0 with objects enabled (
.enable_obj = true). - Colours use
_clrliterals for readability ("#102040"_clr,"white"_clr).

Part 2: move it with keypad
#include <gba/color>
#include <gba/interrupt>
#include <gba/keyinput>
#include <gba/shapes>
#include <gba/video>
#include <algorithm>
#include <cstring>
using namespace gba::shapes;
using gba::operator""_clr;
namespace {
constexpr int screen_width = 240;
constexpr int screen_height = 160;
constexpr int sprite_size = 16;
constexpr auto spr_ball = sprite_16x16(circle(8.0, 8.0, 7.0));
int clamp(int value, int lo, int hi) {
if (value < lo) {
return lo;
}
if (value > hi) {
return hi;
}
return value;
}
} // namespace
int main() {
gba::irq_handler = {};
gba::reg_dispstat = {.enable_irq_vblank = true};
gba::reg_ie = {.vblank = true};
gba::reg_ime = true;
gba::reg_dispcnt = {
.video_mode = 0,
.linear_obj_tilemap = true,
.enable_obj = true,
};
gba::pal_bg_mem[0] = "#102040"_clr;
gba::pal_obj_bank[0][1] = "white"_clr;
auto* objDst = gba::memory_map(gba::mem_vram_obj);
std::memcpy(objDst, spr_ball.data(), spr_ball.size());
const auto tileIdx = gba::tile_index(objDst);
auto obj = spr_ball.obj(tileIdx);
obj.palette_index = 0;
int spriteX = (screen_width - sprite_size) / 2;
int spriteY = (screen_height - sprite_size) / 2;
obj.x = static_cast<unsigned short>(spriteX);
obj.y = static_cast<unsigned short>(spriteY);
gba::obj_mem[0] = obj;
gba::object disabled{.disable = true};
std::fill(std::begin(gba::obj_mem) + 1, std::end(gba::obj_mem), disabled);
gba::keypad keys;
while (true) {
gba::VBlankIntrWait();
keys = gba::reg_keyinput;
spriteX += keys.xaxis();
spriteY += keys.i_yaxis();
spriteX = clamp(spriteX, 0, screen_width - sprite_size);
spriteY = clamp(spriteY, 0, screen_height - sprite_size);
obj.x = static_cast<unsigned short>(spriteX);
obj.y = static_cast<unsigned short>(spriteY);
gba::obj_mem[0] = obj;
}
}
keys.xaxis()handles left/right.keys.i_yaxis()handles up/down in screen-space coordinates.- Position is clamped to keep the sprite inside the 240x160 screen.
Next step
Continue to Hello Audio to trigger a PSG jingle on button press.