Nintendo 64 Part 10: Animation, Controller Input, and Crash Screens
Missing from my program is a real update loop and controller input. I’ll add these next.
Basic Update Loop
I’ll start by just making a bunch of balls that bounce around on the screen, like a screensaver. I add three new functions:
static void game_init(void);
static void game_update(void);
static Gfx *game_render(Gfx *dl);
The game_render
function just takes a pointer to a display list
buffer as a parameter, writes to the display list, and then returns the
updated pointer.
Here’s what it looks like.
Hardware Incompatibility
When I tested on hardware, the Nintendo 64 locked up. After trying out a few different builds saved from old versions of the game, I noticed that there was a vertical black stripe near the right edge of the screen. It turns out that the framebuffer was not aligned!
Here is the fix:
static u16 framebuffers[2][SCREEN_WIDTH * SCREEN_HEIGHT]
__attribute__((aligned(16)));
Here is a video of the program running on a Nintendo 64 system, with a CRT that I pulled out of storage:
Framerate Independence
Right now, I just call update()
once per frame and move
objects by a fixed amount.
However, this is undesirable in a finished game, because the actual
frame rate may be differ.
There are two reasons the frame rate may change:
- In a complex scene, the system may not be able to finish updating before the deadline.
- On a PAL system, the refresh rate is 50 Hz, and on an NTSC system, refresh rate is 29.97 Hz.
If you’re looking for a game that experiences both problems, then Sonic the Hedgehog (1991) for Sega Genesis / Sega Mega Drive is a great example. The gameplay and all the music is plays 17% slower in PAL regions (YouTube: Sonic 1 - 50Hz vs 60Hz). The game would also slow down noticeably if you lost a lot of rings, which was not a deliberate slow-motion effect, but just a slowdown caused by too many sprites on the screen.
I know some of the judges for the game jam have PAL systems, so I should at least make sure my game works well at 50Hz.
The simplest way to do it is to pass a time delta into your game update function, and use the correct units everywhere in your calculations. Here are the changes:
static void game_update(uint32_t delta_time) {
float dt = (float)delta_time * (1.0f / (float)OS_CPU_COUNTER);
// Clamp to 100ms, in case something gets out of hand.
if (dt > 0.1f) {
dt = 0.1f;
}
for (int i = 0; i < NUM_BALLS; i++) {
struct ball *restrict b = &balls[i];
b->x += b->vx * dt;
b->y += b->vy * dt;
...
}
}
// Main loop
static void main(void) {
...
uint32_t prev_time = 0;
bool first_frame = true;
for (;;) {
uint32_t cur_time = osGetTime();
if (!first_frame) {
game_update(cur_time - prev_time);
} else {
first_frame = false;
}
prev_time = cur_time;
...
}
}
Controller Input
Reading from controllers turns out to be fairly simple, but it does take the serial interface (SI) some time to read the controller inputs. The manual says it takes around 2 milliseconds, so I definitely don’t want to do it synchronously in the main loop.
You initialize controllers with osContInit
, which like
everything else, requires an event queue:
u8 cont_mask;
OSContStatus cont_status[MAXCONTROLLERS];
// Set up controller system.
osCreateMesgQueue(&cont_message_queue, &cont_message_buffer, 1);
osSetEventMesg(OS_EVENT_SI, &cont_message_queue, NULL);
osContInit(&cont_message_queue, &cont_mask, cont_status);
// Scan for first controller.
uint32_t controller_index = 0;
bool has_controller = false;
for (int i = 0; i < MAXCONTROLLERS; i++) {
if ((cont_mask & (1u << i)) != 0 && cont_status[i].errno == 0 &&
(cont_status[i].type & CONT_TYPE_MASK) == CONT_TYPE_NORMAL) {
controller_index = i;
has_controller = true;
break;
}
}
The function fills in a mask with the low four bits indicating whether that controller is present. I am just interested in reading input from the first controller, so I iterate through the list until one controller is found, and use that one.
For now, I just want to check that it works, so I make it so that the background color changes randomly when you press A:
// Initialize to zero for first frame.
OSContPad cont_pad[MAXCONTROLLERS] = {};
bool is_pressed = false;
// Main loop.
for (;;) {
if (has_controller) {
// Randomize color if A button is pressed.
bool was_pressed = is_pressed;
is_pressed =
(cont_pad[controller_index].button & A_BUTTON) != 0;
if (!was_pressed && is_pressed) {
color = rand_next(&game_rand);
}
}
...
// Read controller at end of loop.
osViSwapBuffer(framebuffers[which_framebuffer]);
osContStartReadData(&cont_message_queue);
...
osRecvMesg(&retrace_message_queue, NULL, OS_MESG_BLOCK);
osRecvMesg(&cont_message_queue, NULL, OS_MESG_BLOCK);
}
I’m issuing the command to read the controller state at the same time that I swap buffers, but I’m not sure that this is the right way to do it.
Transparent Text Sprites
You can in the previous screenshots that the text sprites are each white on a black background. The background should be clear, so I can draw text on top of other things on the screen. It took me a while to figure out how to do this, but it turns out to be fairly easy once you know how the RDP pipeline works. There are three relevant parts:
- The RDP must be run in 1-cycle mode, not copy mode. Most of the RDP pipeline is disabled in copy mode.
- Set the color combiner stage to use the
DECALRGBA
mode. This causes the color combiner to pass through the texture input with alpha. Since we are in 1-cycle mode, we pass in the same mode twice. - Set the rendering mode to
XLU_SURF
(translucent surface).
gDPSetCycleType(dl++, G_CYC_1CYCLE);
gDPSetCombineMode(dl++, G_CC_DECALRGBA, G_CC_DECALRGBA);
gDPSetRenderMode(dl++, G_RM_XLU_SURF, G_RM_XLU_SURF2);
It’s also easy to change the text color. We just assign a primitive color in our display list and change the color combiner settings to make it multiply the sprites by the primitive color:
// Set color to orange.
gDPSetPrimColor(dl++, 0, 0, 255, 128, 0, 255);
// Multiply texture by primitive color.
gDPSetCombineMode(dl++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
Here is what that looks like:
That’s ugly, but I can make it look better later.
Crashing the Game
I want to be able to stop the game and display debugging information on
the screen.
I define a fatal_error
function which will do this: I pass
in same text, like for printf
.
It stops the game and displays that text on top with a short message
explaining that the game has crashed.
This will mostly be used for debugging the game.
// Show a "fatal error" screen.
noreturn void fatal_error(const char *msg, ...)
__attribute__((format(printf, 1, 2)));
The way it works is by stealing the idle thread, increasing its priority over all user threads, and using it to display the text on the screen.
extern u8 _idle_thread_stack[];
struct fatal_ctx {
const char *msg;
va_list ap;
};
static void fatal_thread(void *arg);
noreturn void fatal_error(const char *msg, ...) {
// Stop and destroy the idle thread so we can reuse it.
osStopThread(&idle_thread);
osDestroyThread(&idle_thread);
// Capture the command-line arguments.
va_list ap;
va_start(ap, msg);
struct fatal_ctx ctx = {.msg = msg, .ap = ap};
// Create a new, high-priority idle thread.
osCreateThread(&idle_thread, 1, fatal_thread, &ctx,
_idle_thread_stack, OS_PRIORITY_APPMAX);
osStartThread(&idle_thread);
osStopThread(NULL);
__builtin_unreachable();
}
The thread itself mostly just forces a specific video mode and a specific buffer, and then periodically draws the text into that buffer. It periodically draw texts into the buffer because some old RCP tasks may also be writing into the buffer.
static void fatal_thread(void *arg) {
struct fatal_ctx *ctx = arg;
const char *msg = ctx->msg;
va_list ap = ctx->ap;
format_error_message(msg, ap);
uint16_t *fb = framebuffers[0];
osViSetMode(&osViModeNtscLpn1);
osViSetSpecialFeatures(OS_VI_GAMMA_OFF);
osViSwapBuffer(fb);
for (;;) {
draw_error_message();
OSTime start = osGetTime();
while (osGetTime() - start < OS_CPU_COUNTER / 10) {}
}
}
I can test this out by crashing the main thread after 100 frames and printing out some information:
int frame_count = 0;
for (;;) {
frame_count++;
if (frame_count == 100) {
fatal_error("Framebuffers = %p\n", framebuffers);
}
...
}
This works as expected. After 100 frames, the game freezes and displays the error message.
There is very naturally a chunk of extra code, since I implemented my own
alternative to sprintf
—a very simple version which is only
150 lines of code or so.