Files
akbasic/examples/galaga/player.c

408 lines
12 KiB
C
Raw Normal View History

/**
* @file player.c
* @brief The player's ship, its shots, and every collision in the game.
*
* Bullets and collision are C forever -- they are engine, not behavior. The
* per-frame budget for the script is spent on the forty things that think;
* nothing here thinks, it just moves and intersects.
*/
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <akstdlib.h>
#include <akgl/actor.h>
#include <akgl/controller.h>
#include <akgl/game.h>
#include <akgl/heap.h>
#include <akgl/registry.h>
#include <akgl/util.h>
#include "galaga.h"
/* Points per kill, indexed by enemy kind. */
static const int KILL_SCORE[GALAGA_ENEMY_KINDS] = {
/* bee butterfly boss */
50, 80, 150
};
static uint32_t PSHOT_SERIAL = 0;
/* -------------------------------------------------------------- hitboxes --- */
/*
* Actor x/y is a sprite's top-left corner. Every box is inset from the
* artwork's rectangle, because the PNGs carry transparent margin and wing
* tips that should not kill anybody.
*/
static void player_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x + 12.0f;
dest->y = actor->y + 8.0f;
dest->w = 75.0f;
dest->h = 60.0f;
}
static void enemy_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x + 8.0f;
dest->y = actor->y + 8.0f;
dest->w = 78.0f;
dest->h = 68.0f;
}
static void shot_box(akgl_Actor *actor, SDL_FRect *dest)
{
dest->x = actor->x;
dest->y = actor->y;
dest->w = 9.0f;
dest->h = 54.0f;
}
/* ---------------------------------------------------------- player shots --- */
/**
* @brief Kill one enemy: score it, blow it up, free its slot.
*/
static akerr_ErrorContext *kill_enemy(int index)
{
akgl_Actor *actor = NULL;
galaga_Enemy *enemy = NULL;
PREPARE_ERROR(errctx);
FAIL_NONZERO_RETURN(errctx, (index < 0 || index >= GALAGA_MAX_ENEMIES),
AKERR_OUTOFBOUNDS, "enemy index %d", index);
actor = galaga_enemy_actors[index];
FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "enemy %d is already gone", index);
enemy = &galaga_enemies[index];
galaga_game.score += KILL_SCORE[enemy->kind];
galaga_game.kills[enemy->kind] += 1;
PASS(errctx, galaga_boom_spawn(actor->x + 20.0f, actor->y + 20.0f));
PASS(errctx, akgl_heap_release_actor(actor));
galaga_enemy_actors[index] = NULL;
SUCCEED_RETURN(errctx);
}
/**
* @brief Move a player shot and test it against every live enemy.
*
* The classic O(shots x enemies) sweep: at most 2 x 40 rectangle tests a
* frame, which is noise. A hit costs the enemy a point of hp; the boss's
* second point is the script's business to survive, not this file's.
*/
static akerr_ErrorContext *player_shot_update(akgl_Actor *obj)
{
SDL_FRect mine;
SDL_FRect theirs;
bool hit = false;
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
obj->y -= 900.0f * galaga_game.dt;
if ( obj->y < -60.0f ) {
galaga_game.player_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(obj));
SUCCEED_RETURN(errctx);
}
shot_box(obj, &mine);
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] == NULL ) {
continue;
}
enemy_box(galaga_enemy_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( !hit ) {
continue;
}
galaga_enemies[i].hp -= 1;
if ( galaga_enemies[i].hp <= 0 ) {
PASS(errctx, kill_enemy(i));
}
galaga_game.player_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(obj));
SUCCEED_RETURN(errctx);
}
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *player_fire(akgl_Actor *player)
{
akgl_Actor *shot = NULL;
char name[32];
int count = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
PSHOT_SERIAL += 1;
PASS(errctx, aksl_snprintf(&count, name, sizeof(name), "pshot%u", PSHOT_SERIAL));
PASS(errctx, akgl_heap_next_actor(&shot));
PASS(errctx, akgl_actor_initialize(shot, name));
PASS(errctx, akgl_actor_set_character(shot, "galaga_playershot"));
/* AFTER initialize: it resets all seven hooks. */
shot->updatefunc = &player_shot_update;
shot->movement_controls_face = false;
shot->state = AKGL_ACTOR_STATE_ALIVE;
shot->visible = true;
shot->x = player->x + 45.0f;
shot->y = player->y - 44.0f;
galaga_game.player_shots_live += 1;
galaga_game.fire_cooldown = 0.22f;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------- player hit --- */
static akerr_ErrorContext *player_hit(akgl_Actor *player)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, player, AKERR_NULLPOINTER, "player");
galaga_game.lives -= 1;
galaga_game.respawn_timer = 2.0f;
PASS(errctx, galaga_boom_spawn(player->x + 25.0f, player->y + 10.0f));
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
SUCCEED_RETURN(errctx);
}
/**
* @brief The player's own update hook: motion, fire, and what can kill it.
*/
static akerr_ErrorContext *player_update(akgl_Actor *obj)
{
SDL_FRect mine;
SDL_FRect theirs;
bool hit = false;
float dx = 0.0f;
int i = 0;
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj");
dx = 0.0f;
if ( galaga_game.moveleft ) {
dx -= GALAGA_PLAYER_SPEED;
}
if ( galaga_game.moveright ) {
dx += GALAGA_PLAYER_SPEED;
}
obj->x += dx * galaga_game.dt;
if ( obj->x < GALAGA_PLAYER_MARGIN ) {
obj->x = GALAGA_PLAYER_MARGIN;
}
if ( obj->x > (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f ) {
obj->x = (float)GALAGA_VIEW_WIDTH - GALAGA_PLAYER_MARGIN - 99.0f;
}
galaga_game.fire_cooldown -= galaga_game.dt;
if ( galaga_game.firing
&& galaga_game.fire_cooldown <= 0.0f
&& galaga_game.player_shots_live < GALAGA_MAX_PLAYER_SHOTS
&& galaga_game.screen == GALAGA_SCREEN_PLAY ) {
PASS(errctx, player_fire(obj));
}
/*
* Respawn grace: two seconds of blinking invulnerability. The blink is
* the `visible` flag, which is deliberate hiding -- the actor still
* updates, it just is not drawn on the off frames.
*/
if ( galaga_game.respawn_timer > 0.0f ) {
galaga_game.respawn_timer -= galaga_game.dt;
obj->visible = ((galaga_game.frame / 6) % 2) == 0;
SUCCEED_RETURN(errctx);
}
obj->visible = true;
player_box(obj, &mine);
/* Enemy shots. */
for ( i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) {
if ( akgl_heap_actors[i].refcount == 0 ) {
continue;
}
if ( strncmp(akgl_heap_actors[i].name, "eshot", 5) != 0 ) {
continue;
}
shot_box(&akgl_heap_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( hit ) {
galaga_game.enemy_shots_live -= 1;
PASS(errctx, akgl_heap_release_actor(&akgl_heap_actors[i]));
PASS(errctx, player_hit(obj));
SUCCEED_RETURN(errctx);
}
}
/* Diving enemies. The formation never reaches this low, so testing all
* forty is the same answer as testing the divers, without a state read. */
for ( i = 0; i < GALAGA_MAX_ENEMIES; i++ ) {
if ( galaga_enemy_actors[i] == NULL ) {
continue;
}
enemy_box(galaga_enemy_actors[i], &theirs);
PASS(errctx, akgl_collide_rectangles(&mine, &theirs, &hit));
if ( hit ) {
PASS(errctx, kill_enemy(i));
PASS(errctx, player_hit(obj));
SUCCEED_RETURN(errctx);
}
}
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- controls --- */
static akerr_ErrorContext *left_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveleft = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *left_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveleft = false;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *right_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveright = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *right_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.moveright = false;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *fire_on(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.firing = true;
SUCCEED_RETURN(errctx);
}
static akerr_ErrorContext *fire_off(akgl_Actor *obj, SDL_Event *event)
{
PREPARE_ERROR(errctx);
(void)obj; (void)event;
galaga_game.firing = false;
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *galaga_player_controls(void)
{
akgl_Control control;
PREPARE_ERROR(errctx);
memset(&control, 0, sizeof(control));
control.event_on = SDL_EVENT_KEY_DOWN;
control.event_off = SDL_EVENT_KEY_UP;
control.key = SDLK_LEFT;
control.handler_on = &left_on;
control.handler_off = &left_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
control.key = SDLK_RIGHT;
control.handler_on = &right_on;
control.handler_off = &right_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
control.key = SDLK_SPACE;
control.handler_on = &fire_on;
control.handler_off = &fire_off;
PASS(errctx, akgl_controller_pushmap(0, &control));
akgl_controlmaps[0].target = galaga_game.player;
SUCCEED_RETURN(errctx);
}
/* ----------------------------------------------------------------- spawn --- */
akerr_ErrorContext *galaga_player_spawn(void)
{
akgl_Actor *player = NULL;
PREPARE_ERROR(errctx);
PASS(errctx, akgl_heap_next_actor(&player));
PASS(errctx, akgl_actor_initialize(player, "player"));
PASS(errctx, akgl_actor_set_character(player, "galaga_player"));
/* AFTER initialize: it resets all seven hooks. */
player->updatefunc = &player_update;
player->movement_controls_face = false;
player->state = AKGL_ACTOR_STATE_ALIVE;
player->visible = true;
player->x = (float)GALAGA_VIEW_WIDTH / 2.0f - 50.0f;
player->y = GALAGA_PLAYER_Y;
galaga_game.player = player;
SUCCEED_RETURN(errctx);
}
/* -------------------------------------------------------------- autoplay --- */
/**
* @brief Send one synthetic key event through the controller.
*
* Through akgl_controller_handle_event(), never the handlers directly: the
* point of autoplay is to exercise the same path a keyboard does.
*/
static akerr_ErrorContext *synth_key(SDL_Keycode key, bool down)
{
SDL_Event event;
PREPARE_ERROR(errctx);
memset(&event, 0, sizeof(event));
event.type = (down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP);
event.key.key = key;
PASS(errctx, akgl_controller_handle_event((void *)&akgl_game.state, &event));
SUCCEED_RETURN(errctx);
}
/**
* @brief The scripted pilot for headless runs: hold fire, sweep the floor.
*/
akerr_ErrorContext *galaga_player_autoplay(int frame)
{
int phase = 0;
PREPARE_ERROR(errctx);
/* Hold fire until the wave has mostly assembled: shooting the entry
* stream point-blank empties the formation before it exists, which makes
* both the game and its figure worse. */
if ( frame == 300 ) {
PASS(errctx, synth_key(SDLK_SPACE, true));
}
phase = frame % 240;
if ( phase == 30 ) {
PASS(errctx, synth_key(SDLK_LEFT, true));
} else if ( phase == 90 ) {
PASS(errctx, synth_key(SDLK_LEFT, false));
PASS(errctx, synth_key(SDLK_RIGHT, true));
} else if ( phase == 210 ) {
PASS(errctx, synth_key(SDLK_RIGHT, false));
}
SUCCEED_RETURN(errctx);
}