/** * @file actors.c * @brief Everything level1.tmj places that is not the player. * * None of these are created here. The map's object layer creates all seven * actors, registers them under the names Tiled gave them, and binds each one to * the character its `character` property names -- so this file's whole job is to * find them again by name and give them behaviour. * * Two of the three behaviours raise #AKGL_ERR_LOGICINTERRUPT, which is the one * status in libakgl that is not a failure. Raised from a `movementlogicfunc` it * means "skip the rest of this tick for me": no gravity, no drag, no move for * that actor this step, and akgl_physics_simulate carries on to the next one. * That is exactly what an actor that has just placed itself wants. */ #include #include #include #include #include "sidescroller.h" /** @brief The blob's collision box, offset into its 32x32 frame. */ static SDL_FRect ss_blob_body = { .x = 8.0f, .y = 0.0f, .w = 16.0f, .h = 32.0f }; /** * @brief Per-actor data for everything in this file, one slot per actor. * * A fixed table rather than an allocation, for the same reason the library has * pools rather than a `malloc`: the level places a known number of things, and a * game that cannot run out of memory at runtime is one fewer failure mode. */ static ss_ActorData ss_hazard_data[SS_HAZARD_COUNT]; /** * @brief Look an actor up by the name its Tiled object carried. * * A miss here means the map and the code disagree about what the level * contains, which is worth failing on rather than working around. */ static akerr_ErrorContext *find_actor(char *name, akgl_Actor **dest) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, name, AKERR_NULLPOINTER, "name"); FAIL_ZERO_RETURN(errctx, dest, AKERR_NULLPOINTER, "dest"); *dest = SDL_GetPointerProperty(AKGL_REGISTRY_ACTOR, name, NULL); FAIL_ZERO_RETURN(errctx, *dest, AKERR_KEY, "The map placed no actor called %s", name); SUCCEED_RETURN(errctx); } /** * @brief A `movementlogicfunc` for something that does not move at all. * * The coins need this. Their character declares no speed and no acceleration, so * they cannot thrust -- but gravity is not thrust. `akgl_physics_arcade_gravity` * accumulates into `ey` for every actor it is handed, so a coin left to the * default logic falls out of the level along with everything else. * * Raising #AKGL_ERR_LOGICINTERRUPT is how an actor opts out of the rest of its * step. It is not an error and nothing logs it. */ static akerr_ErrorContext *ss_static_movement(akgl_Actor *obj, float32_t dt) { PREPARE_ERROR(errctx); (void)dt; FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s does not simulate", (char *)obj->name); } /** * @brief The blob: walk, and turn around at a wall or a ledge. * * This one stays inside the physics step -- it walks on the ground under the * map's gravity like the player does, and the library resolves it the same way. * All this hook decides is which way to face next. */ static akerr_ErrorContext *ss_blob_movement(akgl_Actor *obj, float32_t dt) { ss_ActorData *data = NULL; SDL_FRect ahead; float32_t probe_x = 0.0f; float32_t probe_y = 0.0f; float32_t step = 0.0f; bool floor_ahead = false; bool wall_ahead = false; bool grounded = false; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_ZERO_RETURN(errctx, obj->actorData, AKERR_NULLPOINTER, "obj->actorData"); data = (ss_ActorData *)obj->actorData; AKGL_BITMASK_DEL(obj->state, (AKGL_ACTOR_STATE_FACE_ALL | AKGL_ACTOR_STATE_MOVING_ALL)); if ( data->facing < 0.0f ) { AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_LEFT | AKGL_ACTOR_STATE_MOVING_LEFT)); } else { AKGL_BITMASK_ADD(obj->state, (AKGL_ACTOR_STATE_FACE_RIGHT | AKGL_ACTOR_STATE_MOVING_RIGHT)); } /* Signs the acceleration from the movement bits just set. */ PASS(errctx, akgl_actor_logic_movement(obj, dt)); step = 1.0f; if ( data->facing < 0.0f ) { step = -1.0f; } /* Three probes, all taken from where the last step left the blob. The blob * turns on a wall or a ledge, and a contact from the resolver answers * neither question: it says something pushed back, not which side of the * blob it was on, and it says nothing at all about a floor that is missing. */ PASS(errctx, akgl_collision_shape_bounds(&obj->shape, (obj->x + step), obj->y, &ahead)); PASS(errctx, akgl_collision_box_blocked(&ss_collision, &ahead, AKGL_COLLISION_LAYER_STATIC, &wall_ahead)); PASS(errctx, ss_grounded(&obj->shape, obj->x, obj->y, &grounded)); /* One pixel past the leading edge of the box and one pixel below its feet: * no floor there means the next step walks off. */ probe_y = obj->y + ss_blob_body.y + ss_blob_body.h + 1.0f; if ( data->facing < 0.0f ) { probe_x = obj->x + ss_blob_body.x - 1.0f; } else { probe_x = obj->x + ss_blob_body.x + ss_blob_body.w + 1.0f; } PASS(errctx, akgl_collision_solid_at(&ss_collision, probe_x, probe_y, &floor_ahead)); if ( (wall_ahead == true) || ((grounded == true) && (floor_ahead == false)) ) { data->facing = -data->facing; /* The library's response removes the component of `tx` going into a * wall, but nothing removes it on the ledge path -- and without this the * blob keeps its old momentum through the turn. */ obj->tx = 0.0f; } SUCCEED_RETURN(errctx); } /** * @brief The moth: a figure-eight around where the map put it. * * It flies, so it wants no gravity -- and the map's physics has gravity, because * the player needs it. Rather than fighting the backend the moth writes its own * position and then opts out of the rest of the step, which is the second thing * #AKGL_ERR_LOGICINTERRUPT is for. */ static akerr_ErrorContext *ss_moth_movement(akgl_Actor *obj, float32_t dt) { ss_ActorData *data = NULL; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, obj, AKERR_NULLPOINTER, "obj"); FAIL_ZERO_RETURN(errctx, obj->actorData, AKERR_NULLPOINTER, "obj->actorData"); data = (ss_ActorData *)obj->actorData; data->phase += dt; obj->x = data->home_x + (sinf(data->phase) * 64.0f); obj->y = data->home_y + (sinf(data->phase * 2.0f) * 24.0f); AKGL_BITMASK_DEL(obj->state, AKGL_ACTOR_STATE_FACE_ALL); if ( cosf(data->phase) < 0.0f ) { AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_LEFT); } else { AKGL_BITMASK_ADD(obj->state, AKGL_ACTOR_STATE_FACE_RIGHT); } FAIL_RETURN(errctx, AKGL_ERR_LOGICINTERRUPT, "%s flies itself", (char *)obj->name); } akerr_ErrorContext *ss_actors_bind(void) { char name[32]; int count = 0; int i = 0; PREPARE_ERROR(errctx); for ( i = 0; i < SS_COIN_COUNT; i++ ) { PASS(errctx, aksl_snprintf(&count, (char *)&name, sizeof(name), "coin%d", (i + 1))); PASS(errctx, find_actor((char *)&name, &ss_game.coins[i])); ss_game.coins[i]->movementlogicfunc = &ss_static_movement; } PASS(errctx, find_actor("blob1", &ss_game.hazards[0])); PASS(errctx, akgl_collision_shape_box(&ss_game.hazards[0]->shape, &ss_blob_body, 0.0f)); ss_game.hazards[0]->shape_override = true; PASS(errctx, akgl_collision_settle(&ss_collision, &ss_game.hazards[0]->shape, &ss_game.hazards[0]->x, &ss_game.hazards[0]->y, 0)); ss_hazard_data[0].home_x = ss_game.hazards[0]->x; ss_hazard_data[0].home_y = ss_game.hazards[0]->y; ss_hazard_data[0].facing = -1.0f; ss_game.hazards[0]->actorData = (void *)&ss_hazard_data[0]; ss_game.hazards[0]->movementlogicfunc = &ss_blob_movement; PASS(errctx, find_actor("moth1", &ss_game.hazards[1])); ss_hazard_data[1].home_x = ss_game.hazards[1]->x; ss_hazard_data[1].home_y = ss_game.hazards[1]->y; ss_hazard_data[1].facing = 1.0f; ss_game.hazards[1]->actorData = (void *)&ss_hazard_data[1]; ss_game.hazards[1]->movementlogicfunc = &ss_moth_movement; SUCCEED_RETURN(errctx); }