584 lines
18 KiB
C
584 lines
18 KiB
C
#include "game.h"
|
|
#include "app.h"
|
|
#include "sys.h"
|
|
#include "util.h"
|
|
#include "entity.h"
|
|
#include "sheet.h"
|
|
#include "tick.h"
|
|
#include "sound.h"
|
|
#include "mixer.h"
|
|
#include "math.h"
|
|
#include "scratch.h"
|
|
|
|
#define GAME_FPS 30
|
|
|
|
GLOBAL struct {
|
|
b32 shutdown;
|
|
struct sys_thread game_thread;
|
|
|
|
f64 timescale;
|
|
struct entity *free_entity_head;
|
|
struct tick tick;
|
|
|
|
/* Game thread input */
|
|
struct sys_mutex game_cmds_mutex;
|
|
struct arena game_cmds_arena;
|
|
|
|
/* Game thread output */
|
|
struct sys_mutex published_tick_mutex;
|
|
struct tick published_tick;
|
|
} L = { 0 } DEBUG_LVAR(L_game);
|
|
|
|
/* ========================== *
|
|
* Entity allocation
|
|
* ========================== */
|
|
|
|
INTERNAL struct entity *entity_alloc(void)
|
|
{
|
|
struct entity *entity = NULL;
|
|
if (L.free_entity_head) {
|
|
/* Reuse from free list */
|
|
entity = L.free_entity_head;
|
|
L.free_entity_head = entity->next_free;
|
|
*entity = (struct entity) {
|
|
.handle = entity->handle
|
|
};
|
|
} else {
|
|
/* Make new */
|
|
if (L.tick.entities_count >= MAX_ENTITIES) {
|
|
sys_panic(STR("MAX_ENTITIES reached"));
|
|
}
|
|
u64 idx = L.tick.entities_count++;
|
|
entity = &L.tick.entities[idx];
|
|
*entity = (struct entity) {
|
|
.handle = { .gen = 1, .idx = idx }
|
|
};
|
|
}
|
|
return entity;
|
|
}
|
|
|
|
#if 0
|
|
INTERNAL void entity_release(struct entity *entity)
|
|
{
|
|
entity->next_free = L.free_entity_head;
|
|
L.free_entity_head = entity;
|
|
*entity = (struct entity) {
|
|
.gen = entity->gen + 1
|
|
};
|
|
}
|
|
#endif
|
|
|
|
/* Returns a valid entity or nil entity. Always safe to read result, need to check to write. */
|
|
INTERNAL struct entity *entity_from_handle(struct entity_handle eh)
|
|
{
|
|
if (eh.idx < L.tick.entities_count) {
|
|
struct entity *entity = &L.tick.entities[eh.idx];
|
|
if (entity->handle.gen == eh.gen) {
|
|
return entity;
|
|
}
|
|
}
|
|
return entity_nil();
|
|
}
|
|
|
|
INTERNAL void entity_tree_attach(struct entity *parent, struct entity *child)
|
|
{
|
|
struct entity *first_child = entity_from_handle(parent->first);
|
|
struct entity *last_child = entity_from_handle(parent->last);
|
|
|
|
child->prev = last_child->handle;
|
|
child->parent = parent->handle;
|
|
|
|
if (last_child->valid) {
|
|
last_child->next = child->handle;
|
|
}
|
|
parent->last = child->handle;
|
|
|
|
if (!first_child->valid) {
|
|
parent->first = child->handle;
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Game cmd
|
|
* ========================== */
|
|
|
|
INTERNAL void push_cmds(struct game_cmd_array cmd_array)
|
|
{
|
|
sys_mutex_lock(&L.game_cmds_mutex);
|
|
{
|
|
for (u64 i = 0; i < cmd_array.count; ++i) {
|
|
struct game_cmd *write_cmd = arena_push(&L.game_cmds_arena, struct game_cmd);
|
|
*write_cmd = cmd_array.cmds[i];
|
|
}
|
|
}
|
|
sys_mutex_unlock(&L.game_cmds_mutex);
|
|
}
|
|
|
|
INTERNAL struct game_cmd_array pop_cmds(struct arena *arena)
|
|
{
|
|
struct game_cmd_array array = { 0 };
|
|
if (L.game_cmds_arena.pos > 0) {
|
|
sys_mutex_lock(&L.game_cmds_mutex);
|
|
{
|
|
struct buffer game_cmds_buff = arena_to_buffer(&L.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(&L.game_cmds_arena);
|
|
}
|
|
sys_mutex_unlock(&L.game_cmds_mutex);
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update
|
|
* ========================== */
|
|
|
|
INTERNAL void publish_game_tick(void)
|
|
{
|
|
__prof;
|
|
sys_mutex_lock(&L.published_tick_mutex);
|
|
L.tick.published_ts = sys_timestamp();
|
|
tick_cpy(&L.published_tick, &L.tick);
|
|
sys_mutex_unlock(&L.published_tick_mutex);
|
|
}
|
|
|
|
INTERNAL void game_update(void)
|
|
{
|
|
__prof;
|
|
|
|
struct temp_arena scratch = scratch_begin_no_conflict();
|
|
|
|
++L.tick.id;
|
|
L.tick.dt = max_f64(0.0, (1.0 / GAME_FPS) * L.timescale);
|
|
L.tick.time += L.tick.dt;
|
|
|
|
/* TODO: remove this (testing) */
|
|
/* Initialize entities */
|
|
static b32 run = 0;
|
|
if (!run) {
|
|
run = 1;
|
|
(UNUSED)entity_tree_attach;
|
|
|
|
/* Player ent */
|
|
struct entity *player_ent;
|
|
{
|
|
struct v2 pos = V2(0, 0);
|
|
struct v2 size = V2(1, 1);
|
|
f32 r = 0;
|
|
|
|
struct entity *e = entity_alloc();
|
|
e->valid = true;
|
|
e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
|
|
struct string sprite_name = STR("res/graphics/tim.ase");
|
|
struct string sprite_tag_name = STR("UNARMED");
|
|
struct sheet *sheet = sheet_load(sprite_name);
|
|
f32 meters_width = sheet->frame_size.x / PIXELS_PER_UNIT;
|
|
f32 meters_height = sheet->frame_size.y / PIXELS_PER_UNIT;
|
|
|
|
struct v2 sprite_pos = V2(0, 0);
|
|
f32 sprite_rot = 0;
|
|
struct v2 sprite_size = V2(meters_width, meters_height);
|
|
|
|
struct v2 sprite_pivot;
|
|
{
|
|
struct v2 sprite_pivot_norm = V2(0, 0.5); /* Pivot x & y are each normalized about sprite dimensions. <0, 0> is center, <1, 1> is bottom right corner, etc. */
|
|
struct v2 half_size = v2_mul(sprite_size, 0.5f);
|
|
sprite_pivot = v2_mul_v2(sprite_pivot_norm, half_size);
|
|
}
|
|
|
|
struct xform sprite_xf = XFORM_POS(sprite_pos);
|
|
sprite_xf = xform_rotate(sprite_xf, sprite_rot);
|
|
sprite_xf = xform_translate(sprite_xf, v2_neg(sprite_pivot));
|
|
sprite_xf = xform_scale(sprite_xf, sprite_size);
|
|
e->sprite_xform = sprite_xf;
|
|
|
|
e->sprite_name = sprite_name;
|
|
e->sprite_tag_name = sprite_tag_name;
|
|
e->sprite_tint = COLOR_WHITE;
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED);
|
|
e->player_max_speed = 5.f;
|
|
e->player_acceleration = 15.0f;
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_ANIMATING);
|
|
e->animation_looping = true;
|
|
|
|
player_ent = e;
|
|
|
|
//entity_enable_prop(e, ENTITY_PROP_TEST);
|
|
//entity_enable_prop(e, ENTITY_PROP_TEST_FOLLOW_MOUSE);
|
|
}
|
|
|
|
/* Child ent */
|
|
{
|
|
struct v2 pos = V2(0, 0.25);
|
|
struct v2 size = V2(1, 1);
|
|
f32 r = 0;
|
|
|
|
struct entity *e = entity_alloc();
|
|
e->valid = true;
|
|
e->rel_xform = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
|
|
struct string sprite_name = STR("res/graphics/tim.ase");
|
|
struct string sprite_tag_name = STR("UNARMED");
|
|
struct sheet *sheet = sheet_load(sprite_name);
|
|
f32 meters_width = sheet->frame_size.x / PIXELS_PER_UNIT;
|
|
f32 meters_height = sheet->frame_size.y / PIXELS_PER_UNIT;
|
|
|
|
struct v2 sprite_pos = V2(0, 0);
|
|
f32 sprite_rot = 0;
|
|
struct v2 sprite_size = V2(meters_width, meters_height);
|
|
|
|
struct v2 sprite_pivot;
|
|
{
|
|
struct v2 sprite_pivot_norm = V2(0, 0.5); /* Pivot x & y are each normalized about sprite dimensions. <0, 0> is center, <1, 1> is bottom right corner, etc. */
|
|
struct v2 half_size = v2_mul(sprite_size, 0.5f);
|
|
sprite_pivot = v2_mul_v2(sprite_pivot_norm, half_size);
|
|
}
|
|
|
|
struct xform sprite_xf = XFORM_POS(sprite_pos);
|
|
sprite_xf = xform_rotate(sprite_xf, sprite_rot);
|
|
sprite_xf = xform_translate(sprite_xf, v2_neg(sprite_pivot));
|
|
sprite_xf = xform_scale(sprite_xf, sprite_size);
|
|
e->sprite_xform = sprite_xf;
|
|
|
|
e->sprite_name = sprite_name;
|
|
e->sprite_tag_name = sprite_tag_name;
|
|
e->sprite_tint = RGBA_F(0.3, 0.3, 0.3, 0.3);
|
|
|
|
//entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED);
|
|
//e->player_max_speed = 5.f;
|
|
//e->player_acceleration = 15.0f;
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_ANIMATING);
|
|
e->animation_looping = true;
|
|
|
|
//entity_enable_prop(e, ENTITY_PROP_TEST);
|
|
//entity_enable_prop(e, ENTITY_PROP_TEST_FOLLOW_MOUSE);
|
|
|
|
entity_tree_attach(player_ent, e);
|
|
}
|
|
|
|
/* Camera ent */
|
|
{
|
|
struct entity *e = entity_alloc();
|
|
e->valid = true;
|
|
e->rel_xform = XFORM_IDENT;
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_CAMERA);
|
|
e->camera_active = true;
|
|
e->camera_follow = player_ent->handle;
|
|
e->camera_zoom = 1;
|
|
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Process game cmds
|
|
* ========================== */
|
|
|
|
L.tick.player_move_dir = V2(0, 0);
|
|
|
|
struct game_cmd_array game_cmds = pop_cmds(scratch.arena);
|
|
for (u64 i = 0; i < game_cmds.count; ++i) {
|
|
struct game_cmd cmd = game_cmds.cmds[i];
|
|
|
|
switch (cmd.kind) {
|
|
/* Movement */
|
|
case GAME_CMD_KIND_PLAYER_MOVE: {
|
|
struct v2 dir = cmd.dir;
|
|
|
|
L.tick.player_move_dir = v2_add(L.tick.player_move_dir, dir);
|
|
} break;
|
|
|
|
/* Focus */
|
|
case GAME_CMD_KIND_PLAYER_FOCUS: {
|
|
L.tick.player_focus = cmd.pos;
|
|
} break;
|
|
|
|
default: break;
|
|
};
|
|
}
|
|
|
|
if (v2_len(L.tick.player_move_dir) > 1.f) {
|
|
/* Clamp movement magnitude */
|
|
L.tick.player_move_dir = v2_norm(L.tick.player_move_dir);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/* ========================== *
|
|
* Update entities pre-physics
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) {
|
|
struct entity *ent = &L.tick.entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* ========================== *
|
|
* Initialize
|
|
* ========================== */
|
|
|
|
/* ENTITY_PROP_TEST */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST) && !ent->test_initialized) {
|
|
ent->test_initialized = true;
|
|
ent->test_start_rel_xform = ent->rel_xform;
|
|
ent->test_start_sprite_xform = ent->sprite_xform;
|
|
}
|
|
|
|
/* ENTITY_PROP_ANIMATING */
|
|
if (entity_has_prop(ent, ENTITY_PROP_ANIMATING) && ent->animation_start_time == 0) {
|
|
ent->animation_start_time = L.tick.time;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update animation
|
|
* ========================== */
|
|
|
|
if (ent->animation_start_time > 0) {
|
|
/* Stop animation if past duration and not looping */
|
|
if (!ent->animation_looping) {
|
|
f64 time_in_anim = L.tick.time - ent->animation_start_time;
|
|
|
|
struct sheet *sheet = sheet_load(ent->sprite_name);
|
|
if (sheet) {
|
|
struct sheet_tag tag = sheet_get_tag(sheet, ent->sprite_tag_name);
|
|
|
|
struct sheet_frame frame = { 0 };
|
|
u64 frame_index = tag.start;
|
|
while (time_in_anim > 0) {
|
|
frame = sheet_get_frame(sheet, frame_index);
|
|
if (frame_index > tag.end) {
|
|
entity_disable_prop(ent, ENTITY_PROP_ANIMATING);
|
|
ent->animation_start_time = 0;
|
|
break;
|
|
}
|
|
time_in_anim -= frame.duration;
|
|
++frame_index;
|
|
}
|
|
} else {
|
|
entity_disable_prop(ent, ENTITY_PROP_ANIMATING);
|
|
ent->animation_start_time = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Test
|
|
* ========================== */
|
|
|
|
/* ENTITY_PROP_TEST */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST)) {
|
|
f32 t = ((f32)L.tick.time);
|
|
f32 r = t * 2.f;
|
|
f32 s = 1 + (math_fabs(math_sin(t * 5)) * 3);
|
|
(UNUSED)r;
|
|
(UNUSED)s;
|
|
|
|
|
|
ent->rel_xform = xform_with_rotation(ent->rel_xform, r);
|
|
ent->rel_xform = xform_with_scale(ent->rel_xform, V2(s, 1));
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update entity physics
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) {
|
|
struct entity *ent = &L.tick.entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
if (ent->parent.gen) continue; /* Only update parent entities */
|
|
|
|
/* ========================== *
|
|
* 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(L.tick.player_move_dir, 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)L.tick.dt;
|
|
|
|
/* Apply acceleration to velocity */
|
|
struct v2 a = v2_mul(ent->acceleration, dt);
|
|
ent->velocity = v2_add(ent->velocity, a);
|
|
|
|
/* Apply velocity to position */
|
|
ent->rel_xform.og = v2_add(ent->rel_xform.og, v2_mul(ent->velocity, dt));
|
|
}
|
|
|
|
/* ========================== *
|
|
* Player look direction
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
struct v2 ent_pos = ent->rel_xform.og;
|
|
struct v2 look_pos = L.tick.player_focus;
|
|
f32 r = v2_angle_to_point(ent_pos, look_pos) + PI / 2;
|
|
ent->rel_xform = xform_with_rotation(ent->rel_xform, r);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update position from mouse
|
|
* ========================== */
|
|
|
|
/* ENTITY_PROP_TEST_FOLLOW_MOUSE */
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST_FOLLOW_MOUSE)) {
|
|
ent->rel_xform.og = L.tick.player_focus;
|
|
ent->test_start_rel_xform.og = L.tick.player_focus;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Calculate xforms
|
|
* ========================== */
|
|
|
|
ent->world_xform = ent->rel_xform;
|
|
|
|
struct entity *child = entity_from_handle(ent->first);
|
|
struct xform parent_xform = ent->world_xform;
|
|
while (child->valid) {
|
|
child->world_xform = xform_mul(parent_xform, child->rel_xform);
|
|
|
|
/* Depth first iteration */
|
|
if (child->first.gen) {
|
|
/* Next child */
|
|
parent_xform = child->world_xform;
|
|
child = entity_from_handle(child->first);
|
|
} else if (child->next.gen) {
|
|
/* Next sibling */
|
|
child = entity_from_handle(child->next);
|
|
} else if (child->parent.gen && entity_from_handle(child->parent)->next.gen) {
|
|
/* Next parent sibling */
|
|
struct entity *parent = entity_from_handle(child->parent);
|
|
struct entity *grandparent = entity_from_handle(parent->parent);
|
|
parent_xform = grandparent->world_xform;
|
|
child = entity_from_handle(parent->next);
|
|
} else {
|
|
child = entity_nil();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update entities post-physics
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < ARRAY_COUNT(L.tick.entities); ++entity_index) {
|
|
struct entity *ent = &L.tick.entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
/* ========================== *
|
|
* Update camera position
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_CAMERA)) {
|
|
struct entity *follow = entity_from_handle(ent->camera_follow);
|
|
|
|
ent->world_xform = follow->world_xform;
|
|
ent->world_xform = xform_with_rotation(ent->world_xform, 0);
|
|
ent->world_xform = xform_with_scale(ent->world_xform, V2(1, 1));
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update sound emitters
|
|
* ========================== */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST_SOUND_EMITTER)) {
|
|
struct mixer_desc desc = ent->sound_desc;
|
|
desc.speed = L.timescale;
|
|
desc.pos = ent->world_xform.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 */
|
|
publish_game_tick();
|
|
__profframe("Game");
|
|
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Startup
|
|
* ========================== */
|
|
|
|
INTERNAL void game_thread_entry_point(void *arg)
|
|
{
|
|
(UNUSED)arg;
|
|
sys_timestamp_t last_frame_ts = 0;
|
|
f64 target_dt = GAME_FPS > 0 ? (1.0 / GAME_FPS) : 0;
|
|
while (!L.shutdown) {
|
|
__profscope(game_update_w_sleep);
|
|
sleep_frame(last_frame_ts, target_dt);
|
|
last_frame_ts = sys_timestamp();
|
|
game_update();
|
|
}
|
|
}
|
|
|
|
void game_startup(void)
|
|
{
|
|
/* Initialize game cmd storage */
|
|
L.game_cmds_mutex = sys_mutex_alloc();
|
|
L.game_cmds_arena = arena_alloc(GIGABYTE(64));
|
|
|
|
/* Initialize tick storage */
|
|
L.published_tick_mutex = sys_mutex_alloc();
|
|
|
|
L.timescale = 1.0;
|
|
L.game_thread = sys_thread_init(&game_thread_entry_point, NULL, STR("[P2] Game thread"));
|
|
}
|
|
|
|
void game_shutdown(void)
|
|
{
|
|
L.shutdown = true;
|
|
sys_thread_join(&L.game_thread);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Interface
|
|
* ========================== */
|
|
|
|
void game_get_latest_tick(struct tick *dest)
|
|
{
|
|
sys_mutex_lock(&L.published_tick_mutex);
|
|
tick_cpy(dest, &L.published_tick);
|
|
sys_mutex_unlock(&L.published_tick_mutex);
|
|
}
|
|
|
|
u64 game_get_latest_tick_id(void)
|
|
{
|
|
return L.published_tick.id;
|
|
}
|
|
|
|
void game_push_cmds(struct game_cmd_array cmd_array)
|
|
{
|
|
push_cmds(cmd_array);
|
|
}
|