708 lines
24 KiB
C
708 lines
24 KiB
C
#include "game.h"
|
|
#include "sys.h"
|
|
#include "util.h"
|
|
#include "world.h"
|
|
#include "sprite.h"
|
|
#include "sound.h"
|
|
#include "mixer.h"
|
|
#include "math.h"
|
|
#include "scratch.h"
|
|
#include "atomic.h"
|
|
#include "app.h"
|
|
#include "log.h"
|
|
|
|
GLOBAL struct {
|
|
struct atomic_i32 game_thread_shutdown;
|
|
struct sys_thread game_thread;
|
|
|
|
/* Double buffered game state */
|
|
struct world prev_world; /* Last tick */
|
|
struct world world;
|
|
|
|
/* Game thread input */
|
|
struct sys_mutex game_cmds_mutex;
|
|
struct arena game_cmds_arena;
|
|
|
|
/* Game thread output */
|
|
struct sys_mutex published_tick_mutex;
|
|
struct world published_tick;
|
|
struct atomic_u64 published_tick_id;
|
|
} G = { 0 }, DEBUG_ALIAS(G, G_game);
|
|
|
|
/* ========================== *
|
|
* Startup
|
|
* ========================== */
|
|
|
|
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(game_shutdown);
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(game_thread_entry_point, arg);
|
|
|
|
struct game_startup_receipt game_startup(struct mixer_startup_receipt *mixer_sr,
|
|
struct sprite_startup_receipt *sheet_sr,
|
|
struct sound_startup_receipt *sound_sr)
|
|
{
|
|
(UNUSED)mixer_sr;
|
|
(UNUSED)sheet_sr;
|
|
(UNUSED)sound_sr;
|
|
|
|
/* Initialize game cmd storage */
|
|
G.game_cmds_mutex = sys_mutex_alloc();
|
|
G.game_cmds_arena = arena_alloc(GIGABYTE(64));
|
|
|
|
/* Initialize world */
|
|
world_alloc(&G.world);
|
|
|
|
/* Initialize tick transmission */
|
|
world_alloc(&G.published_tick);
|
|
arena_set_readonly(&G.published_tick.entity_store->arena);
|
|
G.published_tick_mutex = sys_mutex_alloc();
|
|
|
|
G.world.timescale = GAME_TIMESCALE;
|
|
G.game_thread = sys_thread_alloc(&game_thread_entry_point, NULL, STR("[P2] Game thread"));
|
|
app_register_exit_callback(&game_shutdown);
|
|
|
|
return (struct game_startup_receipt) { 0 };
|
|
}
|
|
|
|
INTERNAL APP_EXIT_CALLBACK_FUNC_DEF(game_shutdown)
|
|
{
|
|
__prof;
|
|
atomic_i32_eval_exchange(&G.game_thread_shutdown, true);
|
|
sys_thread_wait_release(&G.game_thread);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Game cmd
|
|
* ========================== */
|
|
|
|
INTERNAL void push_cmds(struct game_cmd_array cmd_array)
|
|
{
|
|
struct sys_lock lock = sys_mutex_lock_e(&G.game_cmds_mutex);
|
|
struct game_cmd *cmds = arena_push_array(&G.game_cmds_arena, struct game_cmd, cmd_array.count);
|
|
MEMCPY(cmds, cmd_array.cmds, cmd_array.count * sizeof(*cmds));
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
INTERNAL struct game_cmd_array pop_cmds(struct arena *arena)
|
|
{
|
|
struct game_cmd_array array = { 0 };
|
|
if (G.game_cmds_arena.pos > 0) {
|
|
struct sys_lock lock = sys_mutex_lock_e(&G.game_cmds_mutex);
|
|
struct buffer game_cmds_buff = arena_to_buffer(&G.game_cmds_arena);
|
|
arena_align(arena, alignof(struct game_cmd));
|
|
array.cmds = (struct game_cmd *)arena_push_array(arena, u8, game_cmds_buff.size);
|
|
array.count = game_cmds_buff.size / sizeof(struct game_cmd);
|
|
MEMCPY(array.cmds, game_cmds_buff.data, game_cmds_buff.size);
|
|
arena_reset(&G.game_cmds_arena);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Test
|
|
* ========================== */
|
|
|
|
/* TODO: Remove this */
|
|
|
|
INTERNAL void spawn_test_entities(void)
|
|
{
|
|
/* Player ent */
|
|
struct entity *player_ent;
|
|
{
|
|
struct v2 pos = V2(1, 1);
|
|
struct v2 size = V2(1, 1);
|
|
f32 r = 0;
|
|
|
|
struct entity *e = entity_alloc_top(G.world.entity_store);
|
|
entity_set_xform(e, XFORM_TRS(.t = pos, .r = r, .s = size));
|
|
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase"));
|
|
e->sprite_span_name = STR("idle.unarmed");
|
|
//e->sprite_span_name = STR("idle.one_handed");
|
|
//e->sprite_span_name = STR("idle.two_handed");
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED);
|
|
e->player_max_speed = 4.f;
|
|
e->player_acceleration = 20.0f;
|
|
e->control.focus = V2(0, -1);
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_ANIMATING);
|
|
|
|
player_ent = e;
|
|
|
|
//entity_enable_prop(e, ENTITY_PROP_TEST);
|
|
}
|
|
|
|
/* Child 1 */
|
|
{
|
|
struct v2 pos = V2(1, 0);
|
|
struct v2 size = V2(1, 1);
|
|
f32 r = PI / 4;
|
|
|
|
struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
|
|
struct entity *e = entity_alloc_top(G.world.entity_store);
|
|
entity_set_local_xform(e, xf);
|
|
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/gun.ase"));
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_ANIMATING);
|
|
entity_enable_prop(e, ENTITY_PROP_WEAPON);
|
|
|
|
player_ent->equipped = e->handle;
|
|
}
|
|
|
|
/* Camera ent */
|
|
{
|
|
struct entity *e = entity_alloc_top(G.world.entity_store);
|
|
entity_set_xform(e, XFORM_IDENT);
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_CAMERA);
|
|
entity_enable_prop(e, ENTITY_PROP_CAMERA_ACTIVE);
|
|
e->camera_follow = player_ent->handle;
|
|
|
|
f32 width = (f32)DEFAULT_CAMERA_WIDTH;
|
|
f32 height = (f32)DEFAULT_CAMERA_HEIGHT;
|
|
e->camera_quad_xform = XFORM_TRS(.s = V2(width, height));
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update
|
|
* ========================== */
|
|
|
|
INTERNAL void publish_game_tick(void)
|
|
{
|
|
__prof;
|
|
struct sys_lock lock = sys_mutex_lock_e(&G.published_tick_mutex);
|
|
arena_set_readwrite(&G.published_tick.entity_store->arena);
|
|
{
|
|
world_copy_replace(&G.published_tick, &G.world);
|
|
}
|
|
arena_set_readonly(&G.published_tick.entity_store->arena);
|
|
atomic_u64_eval_exchange(&G.published_tick_id, G.published_tick.tick_id);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
INTERNAL void game_update(void)
|
|
{
|
|
__prof;
|
|
|
|
struct temp_arena scratch = scratch_begin_no_conflict();
|
|
|
|
/* ========================== *
|
|
* Begin frame
|
|
* ========================== */
|
|
|
|
++G.world.tick_id;
|
|
G.world.tick_ts = sys_timestamp();
|
|
G.world.dt = max_f64(0.0, (1.0 / GAME_FPS) * G.world.timescale);
|
|
G.world.time += G.world.dt;
|
|
|
|
struct game_cmd_array game_cmds = pop_cmds(scratch.arena);
|
|
struct entity_store *store = G.world.entity_store;
|
|
struct entity *root = entity_from_handle(store, store->root);
|
|
struct sprite_scope *sprite_frame_scope = sprite_scope_begin();
|
|
|
|
/* ========================== *
|
|
* Spawn test entities
|
|
* ========================== */
|
|
|
|
/* TODO: remove this (testing) */
|
|
/* Initialize entities */
|
|
static b32 run = 0;
|
|
if (!run) {
|
|
run = 1;
|
|
spawn_test_entities();
|
|
}
|
|
|
|
/* ========================== *
|
|
* Process global game cmds
|
|
* ========================== */
|
|
|
|
for (u64 cmd_index = 0; cmd_index < game_cmds.count; ++cmd_index) {
|
|
struct game_cmd cmd = game_cmds.cmds[cmd_index];
|
|
|
|
switch (cmd.kind) {
|
|
/* Clear level */
|
|
case GAME_CMD_KIND_CLEAR_ALL: {
|
|
logf_info("Clearing level");
|
|
for (u64 i = 0; i < store->count; ++i) {
|
|
struct entity *ent = &store->entities[i];
|
|
if (ent->valid && !ent->is_root) {
|
|
entity_release(store, ent);
|
|
}
|
|
}
|
|
} break;
|
|
|
|
/* Spawn test */
|
|
case GAME_CMD_KIND_SPAWN_TEST: {
|
|
logf_info("Spawning (test)");
|
|
spawn_test_entities();
|
|
} break;
|
|
|
|
default: break;
|
|
};
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update sprite from animation
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
if (sprite_tag_is_nil(ent->sprite)) continue;
|
|
|
|
/* ========================== *
|
|
* Update sprite animation
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_ANIMATING)) {
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, ent->sprite);
|
|
struct sprite_sheet_span span = sprite_sheet_get_span(sheet, ent->sprite_span_name);
|
|
|
|
f64 time_in_frame = ent->animation_time_in_frame + G.world.dt;
|
|
u64 frame_index = ent->animation_frame;
|
|
if (frame_index < span.start || frame_index > span.end) {
|
|
frame_index = span.start;
|
|
}
|
|
|
|
struct sprite_sheet_frame frame = sprite_sheet_get_frame(sheet, frame_index);
|
|
while (time_in_frame > frame.duration) {
|
|
time_in_frame -= frame.duration;
|
|
++frame_index;
|
|
if (frame_index > span.end) {
|
|
/* Loop animation */
|
|
frame_index = span.start;
|
|
}
|
|
frame = sprite_sheet_get_frame(sheet, frame_index);
|
|
}
|
|
|
|
ent->animation_time_in_frame = time_in_frame;
|
|
ent->animation_frame = frame_index;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update sprite xform
|
|
* ========================== */
|
|
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, ent->sprite);
|
|
struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, STR("pivot"), ent->animation_frame);
|
|
struct v2 sprite_size = v2_div(sheet->frame_size, (f32)PIXELS_PER_UNIT);
|
|
|
|
struct v2 dir = v2_mul_v2(sprite_size, slice.dir);
|
|
f32 rot = v2_angle(dir) + PI / 2;
|
|
|
|
struct xform xf = XFORM_IDENT;
|
|
xf = xform_rotate(xf, -rot);
|
|
xf = xform_scale(xf, sprite_size);
|
|
xf = xform_translate(xf, v2_neg(slice.center));
|
|
ent->sprite_local_xform = xf;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update entities from cmds
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* ========================== *
|
|
* Initialize test
|
|
* ========================== */
|
|
|
|
/* ENTITY_PROP_TEST */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST) && !ent->test_initialized) {
|
|
ent->test_initialized = true;
|
|
ent->test_start_local_xform = entity_get_local_xform(ent);
|
|
ent->test_start_sprite_xform = ent->sprite_local_xform;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update control from player cmds
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
/* Process cmds */
|
|
struct v2 move = ent->control.move;
|
|
struct v2 focus = ent->control.focus;
|
|
b32 firing = entity_has_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED);
|
|
|
|
for (u64 i = 0; i < game_cmds.count; ++i) {
|
|
struct game_cmd cmd = game_cmds.cmds[i];
|
|
b32 start = cmd.state == GAME_CMD_STATE_START;
|
|
b32 stop = cmd.state == GAME_CMD_STATE_STOP;
|
|
(UNUSED)start;
|
|
(UNUSED)stop;
|
|
|
|
/* TODO: Combine movement from multiple inputs? E.G. a sudden
|
|
* start and immediate stop cmd should still move the player a
|
|
* tad. */
|
|
switch (cmd.kind) {
|
|
case GAME_CMD_KIND_PLAYER_MOVE: {
|
|
move = cmd.move_dir;
|
|
focus = v2_sub(cmd.aim_pos, entity_get_xform(ent).og);
|
|
} break;
|
|
|
|
case GAME_CMD_KIND_PLAYER_FIRE: {
|
|
if (start) {
|
|
firing = true;
|
|
} else if (stop) {
|
|
firing = false;
|
|
}
|
|
} break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
/* Movement */
|
|
if (v2_len_squared(move) > 1) {
|
|
/* Cap movement vector magnitude at 1 */
|
|
move = v2_norm(move);
|
|
}
|
|
ent->control.move = move;
|
|
ent->control.focus = focus;
|
|
|
|
/* Firing */
|
|
if (firing) {
|
|
entity_enable_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED);
|
|
} else {
|
|
entity_disable_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Test
|
|
* ========================== */
|
|
|
|
/* ENTITY_PROP_TEST */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST)) {
|
|
f32 t = ((f32)G.world.time);
|
|
struct v2 og = v2_mul(V2(math_cos(t), math_sin(t)), 3);
|
|
f32 r = t * 3;
|
|
struct v2 s = V2(1 + (math_fabs(math_sin(t * 5)) * 3), 1);
|
|
(UNUSED)og;
|
|
(UNUSED)r;
|
|
(UNUSED)s;
|
|
|
|
og = v2_add(og, ent->test_start_local_xform.og);
|
|
r += xform_get_rotation(ent->test_start_local_xform);
|
|
s = v2_add(s, xform_get_scale(ent->test_start_local_xform));
|
|
|
|
|
|
struct xform xf = entity_get_local_xform(ent);
|
|
xf.og = og;
|
|
xf= xform_with_rotation(xf, r);
|
|
xf= xform_with_scale(xf, s);
|
|
entity_set_local_xform(ent, xf);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Activate triggers
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* Trigger equipped */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED)) {
|
|
struct entity *eq = entity_from_handle(store, ent->equipped);
|
|
if (eq->valid) {
|
|
entity_enable_prop(eq, ENTITY_PROP_TRIGGERED_THIS_TICK);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
/* ========================== *
|
|
* Simulate entity physics
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* ========================== *
|
|
* Weapon firing
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_WEAPON)) {
|
|
if (entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) {
|
|
ent->sprite_tint = RGBA_32_F(1, 0, 0, 1);
|
|
} else {
|
|
ent->sprite_tint = RGBA_32_F(1, 1, 1, 1);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Player angle
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
struct xform xf = entity_get_xform(ent);
|
|
|
|
/* Solve for final angle using law of sines */
|
|
f32 final_xf_angle;
|
|
{
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, ent->sprite);
|
|
struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, STR("hold"), ent->animation_frame);
|
|
|
|
f32 forward_hold_angle_offset;
|
|
{
|
|
struct xform xf_unrotated = xform_with_rotation(xf, 0);
|
|
struct v2 hold_pos_unrotated = xform_mul_v2(xf_unrotated, xform_mul_v2(ent->sprite_local_xform, slice.center));
|
|
forward_hold_angle_offset = v2_angle_from_dirs(V2(0, -1), v2_sub(hold_pos_unrotated, xf_unrotated.og));
|
|
}
|
|
|
|
struct v2 ent_pos = xf.og;
|
|
struct v2 focus_pos = v2_add(ent_pos, ent->control.focus);
|
|
|
|
struct v2 hold_pos;
|
|
{
|
|
struct xform hold_pos_xf = xform_translate(ent->sprite_local_xform, slice.center);
|
|
hold_pos_xf = xform_mul(xf, hold_pos_xf);
|
|
|
|
struct v2 hold_pos_xf_dir = xform_basis_mul_v2(hold_pos_xf, slice.dir);
|
|
hold_pos_xf = xform_with_rotation(hold_pos_xf, v2_angle(hold_pos_xf_dir) + PI / 2);
|
|
|
|
if (v2_eq(hold_pos_xf.og, ent_pos)) {
|
|
/* If hold pos is same as origin (E.G if pivot is being used as hold pos), then move hold pos forward a tad to avoid issue */
|
|
hold_pos_xf = xform_translate(hold_pos_xf, V2(0, -1));
|
|
}
|
|
hold_pos = hold_pos_xf.og;
|
|
}
|
|
|
|
struct v2 hold_dir = xform_basis_mul_v2(xf, xform_basis_mul_v2(ent->sprite_local_xform, slice.dir));
|
|
struct v2 hold_ent_dir = v2_sub(ent_pos, hold_pos);
|
|
struct v2 focus_ent_dir = v2_sub(ent_pos, focus_pos);
|
|
|
|
f32 hold_ent_len = v2_len(hold_ent_dir);
|
|
f32 focus_ent_len = v2_len(focus_ent_dir);
|
|
|
|
f32 final_hold_angle_btw_ent_and_focus = v2_angle_from_dirs(hold_ent_dir, hold_dir);
|
|
f32 final_focus_angle_btw_ent_and_hold = math_asin((math_sin(final_hold_angle_btw_ent_and_focus) * hold_ent_len) / focus_ent_len);
|
|
f32 final_ent_angle_btw_focus_and_hold = PI - (final_focus_angle_btw_ent_and_hold + final_hold_angle_btw_ent_and_focus);
|
|
|
|
final_xf_angle = v2_angle_from_dirs(V2(0, -1), v2_sub(focus_pos, ent_pos)) + final_ent_angle_btw_focus_and_hold - forward_hold_angle_offset;
|
|
}
|
|
|
|
if (!F32_IS_NAN(final_xf_angle)) {
|
|
entity_set_xform(ent, xform_with_rotation(xf, final_xf_angle));
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Player movement
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
f32 max_speed = ent->player_max_speed;
|
|
f32 acceleration_rate = ent->player_acceleration;
|
|
acceleration_rate = clamp_f32(acceleration_rate, 0, GAME_FPS); /* Can't integrate acceleration rate higher than FPS */
|
|
struct v2 target_velocity = v2_mul(ent->control.move, max_speed);
|
|
struct v2 target_acceleration = v2_sub(target_velocity, ent->velocity);
|
|
ent->acceleration = v2_mul(target_acceleration, acceleration_rate);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Integrate acceleration & velocity
|
|
* ========================== */
|
|
|
|
{
|
|
f32 dt = (f32)G.world.dt;
|
|
|
|
/* Apply acceleration to velocity */
|
|
struct v2 a = v2_mul(ent->acceleration, dt);
|
|
ent->velocity = v2_add(ent->velocity, a);
|
|
|
|
/* Apply velocity to position */
|
|
struct xform xf = entity_get_xform(ent);
|
|
xf.og = v2_add(xf.og, v2_mul(ent->velocity, dt));
|
|
entity_set_xform(ent, xf);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update entities post-simulation
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* ========================== *
|
|
* Update camera position
|
|
* ========================== */
|
|
|
|
/* Camera follow */
|
|
if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) {
|
|
struct entity *follow = entity_from_handle(store, ent->camera_follow);
|
|
|
|
struct xform xf = entity_get_xform(ent);
|
|
|
|
if (entity_has_prop(follow, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
f32 aspect_ratio = 1.0;
|
|
{
|
|
struct xform quad_xf = xform_mul(entity_get_xform(ent), ent->camera_quad_xform);
|
|
struct v2 camera_size = xform_get_scale(quad_xf);
|
|
if (!v2_eq(camera_size, V2(0, 0))) {
|
|
aspect_ratio = camera_size.x / camera_size.y;
|
|
}
|
|
}
|
|
f32 ratio_y = 0.33f;
|
|
f32 ratio_x = ratio_y / aspect_ratio;
|
|
struct v2 camera_focus_dir = v2_mul_v2(follow->control.focus, V2(ratio_x, ratio_y));
|
|
struct v2 camera_focus_pos = v2_add(entity_get_xform(follow).og, camera_focus_dir);
|
|
ent->camera_xform_target = xf;
|
|
ent->camera_xform_target.og = camera_focus_pos;
|
|
}
|
|
|
|
/* Lerp camera */
|
|
if (ent->camera_applied_lerp_continuity_gen_plus_one == ent->camera_lerp_continuity_gen + 1) {
|
|
f32 t = 1 - math_pow(2.f, -20.f * (f32)G.world.dt);
|
|
xf = xform_lerp(xf, ent->camera_xform_target, t);
|
|
} else {
|
|
/* Skip lerp */
|
|
xf = ent->camera_xform_target;
|
|
}
|
|
ent->camera_applied_lerp_continuity_gen_plus_one = ent->camera_lerp_continuity_gen + 1;
|
|
entity_set_xform(ent, xf);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update sound emitters
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST_SOUND_EMITTER)) {
|
|
struct mixer_desc desc = ent->sound_desc;
|
|
desc.speed = G.world.timescale;
|
|
|
|
desc.pos = entity_get_xform(ent).og;
|
|
struct sound *sound = sound_load_async(ent->sound_name, 0);
|
|
b32 played = ent->sound_handle.gen != 0;
|
|
if (sound) {
|
|
if (!played) {
|
|
ent->sound_handle = mixer_play_ex(sound, desc);
|
|
} else {
|
|
mixer_track_set(ent->sound_handle, desc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Publish tick
|
|
* ========================== */
|
|
|
|
/* Update cached global xforms to ensure they're accurate in published tick */
|
|
{
|
|
struct temp_arena temp = arena_temp_begin(scratch.arena);
|
|
|
|
struct stack_node {
|
|
struct entity *entity;
|
|
struct xform parent_global_xform;
|
|
};
|
|
|
|
/* Depth first iteration */
|
|
*arena_push(temp.arena, struct stack_node) = (struct stack_node) { .entity = root, .parent_global_xform = XFORM_IDENT };
|
|
u64 stack_count = 1;
|
|
while (stack_count > 0) {
|
|
/* Pull from top of stack */
|
|
struct stack_node node;
|
|
arena_pop(temp.arena, struct stack_node, &node);
|
|
--stack_count;
|
|
|
|
struct entity *child = node.entity;
|
|
|
|
/* Calculate world xform */
|
|
struct xform xf;
|
|
if (child->cached_global_xform_dirty) {
|
|
xf = xform_mul(node.parent_global_xform, child->local_xform);
|
|
child->cached_global_xform = xf;
|
|
child->cached_global_xform_dirty = false;
|
|
} else {
|
|
xf = child->cached_global_xform;
|
|
}
|
|
|
|
/* Append sub-children to stack */
|
|
struct entity *subchild = entity_from_handle(store, child->last);
|
|
while (subchild->valid) {
|
|
*arena_push(temp.arena, struct stack_node) = (struct stack_node) { .entity = subchild, .parent_global_xform = xf };
|
|
++stack_count;
|
|
subchild = entity_from_handle(store, subchild->prev);
|
|
}
|
|
}
|
|
|
|
arena_temp_end(temp);
|
|
}
|
|
|
|
/* Publish tick */
|
|
publish_game_tick();
|
|
__profframe("Game");
|
|
|
|
/* ========================== *
|
|
* Reset triggered entities
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->count; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) {
|
|
entity_disable_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* End frame cache scopes
|
|
* ========================== */
|
|
|
|
sprite_scope_end(sprite_frame_scope);
|
|
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Game thread
|
|
* ========================== */
|
|
|
|
INTERNAL SYS_THREAD_ENTRY_POINT_FUNC_DEF(game_thread_entry_point, arg)
|
|
{
|
|
(UNUSED)arg;
|
|
sys_timestamp_t last_frame_ts = 0;
|
|
f64 target_dt = GAME_FPS > (0) ? (1.0 / GAME_FPS) : 0;
|
|
while (!atomic_i32_eval(&G.game_thread_shutdown)) {
|
|
__profscope(game_update_w_sleep);
|
|
sleep_frame(last_frame_ts, target_dt);
|
|
last_frame_ts = sys_timestamp();
|
|
game_update();
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Interface
|
|
* ========================== */
|
|
|
|
void game_get_latest_tick(struct world *dest)
|
|
{
|
|
__prof;
|
|
struct sys_lock lock = sys_mutex_lock_e(&G.published_tick_mutex);
|
|
world_copy_replace(dest, &G.published_tick);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
u64 game_get_latest_tick_id(void)
|
|
{
|
|
return atomic_u64_eval(&G.published_tick_id);
|
|
}
|
|
|
|
void game_push_cmds(struct game_cmd_array cmd_array)
|
|
{
|
|
push_cmds(cmd_array);
|
|
}
|