/** * @file physics.c * @brief Implements the physics subsystem. */ #include #include #include #include #include #include #include #include akerr_ErrorContext *akgl_physics_null_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor"); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_null_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, a1, AKERR_NULLPOINTER, "a1"); FAIL_ZERO_RETURN(errctx, a2, AKERR_NULLPOINTER, "a2"); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_null_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor"); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_init_null(akgl_PhysicsBackend *self) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); self->gravity = akgl_physics_null_gravity; self->collide = akgl_physics_null_collide; self->move = akgl_physics_null_move; self->simulate = akgl_physics_simulate; // Set for the same reason as the arcade backend: the null backend still // goes through akgl_physics_simulate, which still computes a dt and still // commits velocity to position through akgl_physics_null_move. self->gravity_time = SDL_GetTicksNS(); self->max_timestep = AKGL_PHYSICS_DEFAULT_MAX_TIMESTEP; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_arcade_gravity(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor"); if ( self->gravity_x != 0 ) { // Assume the X origin is - (screen left) actor->ex -= (self->gravity_x * dt); } if ( self->gravity_y != 0 ) { // Assume Y origin is + (down screen) actor->ey += (self->gravity_y * dt); } if ( self->gravity_z != 0 ) { // Assume Z origin is - (behind the camera) actor->ez -= (self->gravity_z * dt); } SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_arcade_collide(akgl_PhysicsBackend *self, akgl_Actor *a1, akgl_Actor *a2) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_RETURN(errctx, AKERR_API, "Not implemented"); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_arcade_move(akgl_PhysicsBackend *self, akgl_Actor *actor, float32_t dt) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, actor, AKERR_NULLPOINTER, "actor"); actor->x += actor->vx * dt; actor->y += actor->vy * dt; actor->z += actor->vz * dt; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_init_arcade(akgl_PhysicsBackend *self) { akgl_String *tmp; PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); PASS(errctx, akgl_heap_next_string(&tmp)); self->gravity = akgl_physics_arcade_gravity; self->collide = akgl_physics_arcade_collide; self->move = akgl_physics_arcade_move; self->simulate = akgl_physics_simulate; // The epoch the first step's dt is measured from. Nothing set it, so it // stayed at whatever the backend's storage held -- zero, for the default // backend in BSS -- and the first akgl_physics_simulate() measured dt as // the entire time since SDL started. A host that spends a quarter of a // second loading before its first frame got a quarter-second step: 101 // pixels of fall where a 60 Hz frame is 0.44. self->gravity_time = SDL_GetTicksNS(); ATTEMPT { CATCH(errctx, akgl_get_property("physics.gravity.x", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->gravity_x)); CATCH(errctx, akgl_get_property("physics.gravity.y", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->gravity_y)); CATCH(errctx, akgl_get_property("physics.gravity.z", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->gravity_z)); CATCH(errctx, akgl_get_property("physics.drag.x", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->drag_x)); CATCH(errctx, akgl_get_property("physics.drag.y", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->drag_y)); CATCH(errctx, akgl_get_property("physics.drag.z", &tmp, "0.0")); CATCH(errctx, aksl_atof(tmp->data, &self->drag_z)); CATCH(errctx, akgl_get_property("physics.max_timestep", &tmp, "0.05")); CATCH(errctx, aksl_atof(tmp->data, &self->max_timestep)); } CLEANUP { IGNORE(akgl_heap_release_string(tmp)); } PROCESS(errctx) { } FINISH(errctx, true); SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_simulate(akgl_PhysicsBackend *self, akgl_Iterator *opflags) { PREPARE_ERROR(errctx); akgl_Iterator defflags = { .flags = 0, .layerid = 0 }; SDL_Time curtime = 0; float32_t dt = 0; float32_t overshoot = 0.0f; float32_t thrustscale = 0.0f; akgl_Actor *actor = NULL; FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, self->move, AKERR_NULLPOINTER, "self->move"); // Reading the elapsed time requires self, so it cannot be hoisted above // the null check. curtime = SDL_GetTicksNS(); dt = (float32_t)(curtime - self->gravity_time) / (float32_t)AKGL_TIME_ONESEC_NS; // Bound the step. Everything below multiplies by dt, so one enormous dt // moves every actor by however far its velocity carries it over the whole // stall -- a quarter-second load is a hundred pixels of fall under // platformer gravity, straight through whatever was underneath. Advancing // the world in slow motion through a hitch is the trade to make here. // // A zero or negative max_timestep disables the bound, for a caller who // would rather have the real elapsed time and handle it themselves. if ( (self->max_timestep > 0.0) && (dt > (float32_t)self->max_timestep) ) { dt = (float32_t)self->max_timestep; } // A clock that went backwards is not a step. SDL_GetTicksNS is monotonic, // but gravity_time is a public field and a caller can put anything in it. if ( dt < 0.0f ) { dt = 0.0f; } if ( opflags == NULL ) { opflags = &defflags; } for ( int i = 0; i < AKGL_MAX_HEAP_ACTOR; i++ ) { actor = &akgl_heap_actors[i]; if ( actor->refcount == 0 ) { continue; } if ( actor->parent != NULL ) { // Children don't move independently of their parents, they just have an offset actor->x = actor->parent->x + actor->vx; actor->y = actor->parent->y + actor->vy; actor->z = actor->parent->z + actor->vz; continue; } else if ( actor->basechar == NULL ) { continue; } if ( AKGL_BITMASK_HAS(opflags->flags, AKGL_ITERATOR_OP_LAYERMASK) ) { if ( actor->layer != opflags->layerid ) { continue; } } // thrust is a function of acceleration on a given axis if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_LEFT) || AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_RIGHT) ) { actor->tx += actor->ax * dt; } if ( AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_UP) || AKGL_BITMASK_HAS(actor->state, AKGL_ACTOR_STATE_MOVING_DOWN) ) { actor->ty += actor->ay * dt; } // Cap the thrust *vector* against the ellipse the character's per-axis // top speeds describe, rather than each axis against its own cap. // // Capping the axes independently lets the corner of the box through: an // actor holding two directions at once got both caps at once and // travelled their diagonal, which measured 41% faster than either alone. // Scaling to the ellipse keeps a character whose horizontal and vertical // speeds differ moving at the ratio it asked for, and makes them equal // where the speeds are. // // An axis with a top speed of zero cannot be thrust along at all, which // is what the old per-axis clamp did with it, and it stays out of the // magnitude entirely -- dividing by it would not end well. overshoot = 0.0f; if ( actor->sx != 0.0f ) { overshoot += (actor->tx / actor->sx) * (actor->tx / actor->sx); } else { actor->tx = 0.0f; } if ( actor->sy != 0.0f ) { overshoot += (actor->ty / actor->sy) * (actor->ty / actor->sy); } else { actor->ty = 0.0f; } if ( actor->sz != 0.0f ) { overshoot += (actor->tz / actor->sz) * (actor->tz / actor->sz); } else { actor->tz = 0.0f; } if ( overshoot > 1.0f ) { thrustscale = 1.0f / sqrtf(overshoot); actor->tx *= thrustscale; actor->ty *= thrustscale; actor->tz *= thrustscale; } ATTEMPT { CATCH(errctx, actor->movementlogicfunc(actor, dt)); PASS(errctx, self->gravity(self, actor, dt)); // Counteract velocity with atmospheric drag if ( self->drag_x != 0 ) { actor->ex -= actor->ex * self->drag_x * dt; } if ( self->drag_y != 0 ) { actor->ey -= actor->ey * self->drag_y * dt; } if ( self->drag_z != 0 ) { actor->ez -= actor->ez * self->drag_z * dt; } actor->vx = actor->ex + actor->tx; actor->vy = actor->ey + actor->ty; actor->vz = actor->ez + actor->tz; PASS(errctx, self->move(self, actor, dt)); } CLEANUP { } PROCESS(errctx) { } HANDLE(errctx, AKGL_ERR_LOGICINTERRUPT) { // noop } FINISH(errctx, true); } self->gravity_time = curtime; SUCCEED_RETURN(errctx); } akerr_ErrorContext *akgl_physics_factory(akgl_PhysicsBackend *self, akgl_String *type) { PREPARE_ERROR(errctx); FAIL_ZERO_RETURN(errctx, self, AKERR_NULLPOINTER, "self"); FAIL_ZERO_RETURN(errctx, type, AKERR_NULLPOINTER, "type"); if ( strncmp(type->data, "null", 4) == 0) { PASS(errctx, akgl_physics_init_null(self)); SUCCEED_RETURN(errctx); } if ( strncmp(type->data, "arcade", 6) == 0) { PASS(errctx, akgl_physics_init_arcade(self)); SUCCEED_RETURN(errctx); } FAIL_RETURN(errctx, AKERR_KEY, "Invalid physics engine %s", type->data); }