2529 lines
93 KiB
C
2529 lines
93 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"
|
|
#include "collider.h"
|
|
|
|
struct contact_lookup_entry {
|
|
u64 hash;
|
|
struct entity_handle contact_ent_handle;
|
|
struct contact_lookup_entry *next;
|
|
struct contact_lookup_entry *prev;
|
|
};
|
|
|
|
struct contact_lookup_bucket {
|
|
struct contact_lookup_entry *first;
|
|
struct contact_lookup_entry *last;
|
|
};
|
|
|
|
struct contact_lookup {
|
|
struct arena arena;
|
|
struct contact_lookup_bucket buckets[4096];
|
|
struct contact_lookup_entry *first_free_entry;
|
|
};
|
|
|
|
#if COLLIDER_DEBUG
|
|
/* TODO: Remove this (debugging) */
|
|
struct collision_debug_lookup {
|
|
struct arena arena;
|
|
struct fixed_dict dict;
|
|
};
|
|
#endif
|
|
|
|
GLOBAL struct {
|
|
struct atomic_i32 game_thread_shutdown;
|
|
struct sys_thread game_thread;
|
|
|
|
b32 paused;
|
|
struct sprite_scope *sprite_frame_scope;
|
|
|
|
/* For debugging */
|
|
struct v2 user_cursor;
|
|
|
|
/* TODO: Remove this (testing) */
|
|
b32 extra_spawn;
|
|
b32 should_reset_level;
|
|
|
|
/* Game thread input */
|
|
struct sys_mutex game_cmds_mutex;
|
|
struct arena game_cmds_arena;
|
|
struct entity *root;
|
|
|
|
/* Constants */
|
|
struct math_spring_result contact_softness;
|
|
struct math_spring_result mouse_joint_linear_softness;
|
|
struct math_spring_result mouse_joint_angular_softness;
|
|
f32 mouse_joint_max_force;
|
|
|
|
/* Bookkeeping structures */
|
|
struct contact_lookup contact_lookup;
|
|
#if COLLIDER_DEBUG
|
|
struct collision_debug_lookup collision_debug_lookup;
|
|
#endif
|
|
|
|
/* Ticks */
|
|
struct sys_mutex prev_tick_mutex;
|
|
struct atomic_u64 prev_tick_id;
|
|
struct atomic_u64 prev_tick_continuity_gen;
|
|
struct world prev_tick;
|
|
struct world tick;
|
|
} G = ZI, 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);
|
|
INTERNAL void reset_world(void);
|
|
|
|
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 constants */
|
|
{
|
|
const f32 substep_dt = (1.f / ((f32)GAME_FPS * (f32)GAME_PHYSICS_SUBSTEPS));
|
|
|
|
const f32 contact_frequency = (1.f / substep_dt) / 8.f;
|
|
const f32 contact_damping_ratio = 10;
|
|
G.contact_softness = math_spring(contact_frequency, contact_damping_ratio, substep_dt);
|
|
|
|
const f32 mouse_joint_linear_frequency = 5.0;
|
|
const f32 mouse_joint_linear_damping_ratio = 0.7;
|
|
const f32 mouse_joint_angular_frequency = 0.5f;
|
|
const f32 mouse_joint_angular_damping_ratio = 0.1f;
|
|
G.mouse_joint_max_force = 1000;
|
|
G.mouse_joint_linear_softness = math_spring(mouse_joint_linear_frequency, mouse_joint_linear_damping_ratio, substep_dt);
|
|
G.mouse_joint_angular_softness = math_spring(mouse_joint_angular_frequency, mouse_joint_angular_damping_ratio, substep_dt);
|
|
}
|
|
|
|
/* Initialize game cmd storage */
|
|
G.game_cmds_mutex = sys_mutex_alloc();
|
|
G.game_cmds_arena = arena_alloc(GIGABYTE(64));
|
|
|
|
/* Initialize empty world */
|
|
reset_world();
|
|
|
|
/* Initialize prev tick */
|
|
world_alloc(&G.prev_tick);
|
|
/* FIXME: Make the world struct itself readonly as well */
|
|
arena_set_readonly(&G.prev_tick.entity_store->arena);
|
|
G.prev_tick_mutex = sys_mutex_alloc();
|
|
|
|
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 = ZI;
|
|
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;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Activate
|
|
* ========================== */
|
|
|
|
INTERNAL void activate_now(struct entity *ent)
|
|
{
|
|
entity_enable_prop(ent, ENTITY_PROP_ACTIVE);
|
|
ent->activation_tick = G.tick.tick_id;
|
|
++ent->continuity_gen;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Contact lookup
|
|
* ========================== */
|
|
|
|
INTERNAL void contact_lookup_alloc(struct contact_lookup *l)
|
|
{
|
|
MEMZERO_STRUCT(l);
|
|
l->arena = arena_alloc(GIGABYTE(64));
|
|
}
|
|
|
|
INTERNAL void contact_lookup_release(struct contact_lookup *l)
|
|
{
|
|
arena_release(&l->arena);
|
|
}
|
|
|
|
INTERNAL u64 contact_lookup_hash_from_entities(struct entity_handle h0, struct entity_handle h1)
|
|
{
|
|
struct buffer b0 = BUFFER_FROM_STRUCT(&h0);
|
|
struct buffer b1 = BUFFER_FROM_STRUCT(&h1);
|
|
u64 hash = hash_fnv64(HASH_FNV64_BASIS, b0);
|
|
hash = hash_fnv64(hash, b1);
|
|
return hash;
|
|
}
|
|
|
|
INTERNAL struct contact_lookup_entry *contact_lookup_get(struct contact_lookup *l, u64 hash)
|
|
{
|
|
u64 index = hash % ARRAY_COUNT(l->buckets);
|
|
struct contact_lookup_bucket *bucket = &l->buckets[index];
|
|
struct contact_lookup_entry *res = NULL;
|
|
for (struct contact_lookup_entry *e = bucket->first; e; e = e->next) {
|
|
if (e->hash == hash) {
|
|
res = e;
|
|
break;
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
INTERNAL void contact_lookup_set(struct contact_lookup *l, u64 hash, struct entity_handle handle)
|
|
{
|
|
u64 index = hash % ARRAY_COUNT(l->buckets);
|
|
struct contact_lookup_bucket *bucket = &l->buckets[index];
|
|
|
|
struct contact_lookup_entry *prev = NULL;
|
|
struct contact_lookup_entry **slot = &bucket->first;
|
|
while (*slot) {
|
|
if ((*slot)->hash == hash) {
|
|
break;
|
|
}
|
|
prev = *slot;
|
|
slot = &(*slot)->next;
|
|
}
|
|
|
|
struct contact_lookup_entry *entry = *slot;
|
|
if (entry) {
|
|
/* Set existing entry */
|
|
entry->contact_ent_handle = handle;
|
|
} else {
|
|
/* Allocate entry */
|
|
if (l->first_free_entry) {
|
|
entry = l->first_free_entry;
|
|
l->first_free_entry->prev = NULL;
|
|
l->first_free_entry = entry->next;
|
|
} else {
|
|
entry = arena_push(&l->arena, struct contact_lookup_entry);
|
|
}
|
|
MEMZERO_STRUCT(entry);
|
|
|
|
entry->hash = hash;
|
|
entry->contact_ent_handle = handle;
|
|
if (prev) {
|
|
entry->prev = prev;
|
|
prev->next = entry;
|
|
}
|
|
|
|
bucket->last = entry;
|
|
*slot = entry;
|
|
}
|
|
}
|
|
|
|
INTERNAL void contact_lookup_remove(struct contact_lookup *l, struct contact_lookup_entry *entry)
|
|
{
|
|
struct contact_lookup_bucket *bucket = &l->buckets[entry->hash % ARRAY_COUNT(l->buckets)];
|
|
struct contact_lookup_entry *prev = entry->prev;
|
|
struct contact_lookup_entry *next = entry->next;
|
|
|
|
if (prev) {
|
|
prev->next = next;
|
|
} else {
|
|
bucket->first = next;
|
|
}
|
|
|
|
if (next) {
|
|
next->prev = prev;
|
|
} else {
|
|
bucket->last = prev;
|
|
}
|
|
|
|
if (l->first_free_entry) {
|
|
l->first_free_entry->prev = entry;
|
|
}
|
|
entry->next = l->first_free_entry;
|
|
entry->prev = NULL;
|
|
l->first_free_entry = entry;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Reset
|
|
* ========================== */
|
|
|
|
INTERNAL void reset_world(void)
|
|
{
|
|
if (G.tick.entity_store) {
|
|
/* Release world */
|
|
world_release(&G.tick);
|
|
/* Release bookkeeping */
|
|
#if COLLIDER_DEBUG
|
|
arena_release(&G.collision_debug_lookup.arena);
|
|
#endif
|
|
contact_lookup_release(&G.contact_lookup);
|
|
}
|
|
|
|
/* Create bookkeeping */
|
|
contact_lookup_alloc(&G.contact_lookup);
|
|
#if COLLIDER_DEBUG
|
|
G.collision_debug_lookup.arena = arena_alloc(GIGABYTE(64));
|
|
G.collision_debug_lookup.dict = fixed_dict_init(&G.collision_debug_lookup.arena, 4096);
|
|
#endif
|
|
|
|
/* Re-create world */
|
|
world_alloc(&G.tick);
|
|
G.tick.continuity_gen = atomic_u64_eval(&G.prev_tick_continuity_gen) + 1;
|
|
G.tick.timescale = GAME_TIMESCALE;
|
|
}
|
|
|
|
/* ========================== *
|
|
* Test
|
|
* ========================== */
|
|
|
|
/* TODO: Remove this */
|
|
|
|
INTERNAL void spawn_test_entities(void)
|
|
{
|
|
struct entity *root = entity_from_handle(G.tick.entity_store, G.tick.entity_store->root);
|
|
root->mass_unscaled = F32_INFINITY;
|
|
root->inertia_unscaled = F32_INFINITY;
|
|
|
|
/* Player */
|
|
struct entity *player_ent = entity_nil();
|
|
//if (!G.extra_spawn) {
|
|
{
|
|
|
|
struct entity *e = entity_alloc(root);
|
|
|
|
struct v2 pos = V2(1, -1);
|
|
|
|
//struct v2 size = V2(0.5, 0.5);
|
|
//struct v2 size = V2(0.5, 0.25);
|
|
struct v2 size = V2(1.0, 1.0);
|
|
|
|
//f32 r = PI / 4;
|
|
f32 r = 0;
|
|
|
|
if (!G.extra_spawn) {
|
|
entity_enable_prop(e, ENTITY_PROP_PLAYER_CONTROLLED);
|
|
entity_enable_prop(e, ENTITY_PROP_TEST);
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase"));
|
|
e->mass_unscaled = 10;
|
|
e->inertia_unscaled = 5;
|
|
} else {
|
|
entity_enable_prop(e, ENTITY_PROP_TEST);
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase"));
|
|
e->mass_unscaled = 10;
|
|
e->inertia_unscaled = 5;
|
|
#if 0
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/box.ase"));
|
|
e->mass_unscaled = 100;
|
|
e->inertia_unscaled = 100;
|
|
#endif
|
|
}
|
|
|
|
//e->sprite = sprite_tag_from_path(STR("res/graphics/box_rounded.ase"));
|
|
//e->sprite_span_name = STR("idle.unarmed");
|
|
//e->sprite_span_name = STR("idle.one_handed");
|
|
e->sprite_span_name = STR("idle.two_handed");
|
|
e->sprite_collider_slice = STR("shape");
|
|
|
|
struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
//xf.bx.y = -1.f;
|
|
|
|
entity_set_xform(e, xf);
|
|
|
|
e->linear_ground_friction = 250;
|
|
e->angular_ground_friction = 200;
|
|
|
|
//e->control_force = 500;
|
|
e->control_force = 500;
|
|
//e->control_force_max_speed = 4;
|
|
e->control_torque = 1000;
|
|
e->control_force_max_speed = 4;
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC);
|
|
|
|
player_ent = e;
|
|
}
|
|
|
|
/* Enemy */
|
|
{
|
|
struct entity *e = entity_alloc(root);
|
|
|
|
struct v2 pos = V2(1, -2);
|
|
f32 r = 0;
|
|
struct v2 size = V2(1, 1);
|
|
struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
entity_set_xform(e, xf);
|
|
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/tim.ase"));
|
|
e->sprite_collider_slice = STR("shape");
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC);
|
|
e->mass_unscaled = 10;
|
|
e->inertia_unscaled = 10;
|
|
e->linear_ground_friction = 250;
|
|
e->angular_ground_friction = 200;
|
|
}
|
|
|
|
|
|
/* Box */
|
|
#if 0
|
|
{
|
|
struct entity *e = entity_alloc(root);
|
|
|
|
struct v2 pos = V2(1, -0.5);
|
|
f32 r = 0;
|
|
struct v2 size = V2(0.5, 0.25);
|
|
struct xform xf = XFORM_TRS(.t = pos, .r = r, .s = size);
|
|
entity_set_xform(e, xf);
|
|
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/box.ase"));
|
|
e->sprite_collider_slice = STR("shape");
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_PHYSICAL_DYNAMIC);
|
|
e->mass_unscaled = 10;
|
|
e->inertia_unscaled = 10;
|
|
e->linear_ground_friction = 250;
|
|
e->angular_ground_friction = 5;
|
|
}
|
|
#endif
|
|
|
|
/* Player weapon */
|
|
if (player_ent->valid) {
|
|
struct entity *e = entity_alloc(player_ent);
|
|
e->sprite = sprite_tag_from_path(STR("res/graphics/gun.ase"));
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_ATTACHED);
|
|
e->attach_slice = STR("attach.wep");
|
|
|
|
entity_enable_prop(e, ENTITY_PROP_WEAPON);
|
|
e->trigger_delay = 1.0f / 10.0f;
|
|
|
|
player_ent->equipped = e->handle;
|
|
}
|
|
|
|
/* Camera */
|
|
if (!G.extra_spawn && player_ent->valid) {
|
|
struct entity *e = entity_alloc(root);
|
|
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));
|
|
}
|
|
|
|
G.extra_spawn = true;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================== *
|
|
* TESTING CONTACT CONSTRAINT
|
|
* ========================== */
|
|
|
|
INTERNAL void create_contacts(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
struct entity *root = G.root;
|
|
|
|
for (u64 check0_index = 0; check0_index < store->reserved; ++check0_index) {
|
|
struct entity *check0 = &store->entities[check0_index];
|
|
if (!entity_is_valid_and_active(check0)) continue;
|
|
if (!(entity_has_prop(check0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(check0, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue;
|
|
if (check0->local_collider.count <= 0) continue;
|
|
|
|
struct xform check0_xf = entity_get_xform(check0);
|
|
struct collider_shape check0_collider = check0->local_collider;
|
|
|
|
for (u64 check1_index = 0; check1_index < store->reserved; ++check1_index) {
|
|
struct entity *check1 = &store->entities[check1_index];
|
|
if (check1 == check0) continue;
|
|
if (!entity_is_valid_and_active(check1)) continue;
|
|
if (!(entity_has_prop(check1, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(check1, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue;
|
|
if (check1->local_collider.count <= 0) continue;
|
|
|
|
/* Deterministic entity order based on index */
|
|
struct entity *e0;
|
|
struct entity *e1;
|
|
struct xform e0_xf;
|
|
struct xform e1_xf;
|
|
struct collider_shape e0_collider;
|
|
struct collider_shape e1_collider;
|
|
if (check0_index < check1_index) {
|
|
e0 = check0;
|
|
e1 = check1;
|
|
e0_xf = check0_xf;
|
|
e1_xf = entity_get_xform(check1);
|
|
e0_collider = check0_collider;
|
|
e1_collider = check1->local_collider;
|
|
} else {
|
|
e0 = check1;
|
|
e1 = check0;
|
|
e0_xf = entity_get_xform(check1);
|
|
e1_xf = check0_xf;
|
|
e0_collider = check1->local_collider;
|
|
e1_collider = check0_collider;
|
|
}
|
|
|
|
u64 lookup_hash = contact_lookup_hash_from_entities(e0->handle, e1->handle);
|
|
struct contact_lookup_entry *entry = contact_lookup_get(&G.contact_lookup, lookup_hash);
|
|
|
|
struct entity *constraint_ent = entity_nil();
|
|
if (entry) {
|
|
constraint_ent = entity_from_handle(store, entry->contact_ent_handle);
|
|
if (entity_is_valid_and_active(constraint_ent)) {
|
|
if (constraint_ent->contact_constraint_data.last_iteration >= G.tick.tick_id) {
|
|
/* Already processed constraint this iteration */
|
|
continue;
|
|
} else {
|
|
++constraint_ent->contact_constraint_data.last_iteration;
|
|
}
|
|
} else {
|
|
/* Constraint ent no longer valid, delete entry */
|
|
contact_lookup_remove(&G.contact_lookup, entry);
|
|
entry = NULL;
|
|
}
|
|
}
|
|
|
|
/* Calculate collision */
|
|
struct collider_collision_points_result collider_res = collider_collision_points(&e0_collider, &e1_collider, e0_xf, e1_xf);
|
|
|
|
/* Parts of algorithm are hard-coded to support 2 contact points */
|
|
CT_ASSERT(ARRAY_COUNT(constraint_ent->contact_constraint_data.points) == 2);
|
|
CT_ASSERT(ARRAY_COUNT(collider_res.points) == 2);
|
|
|
|
struct contact_constraint *constraint = NULL;
|
|
if (collider_res.num_points > 0) {
|
|
if (!entity_is_valid_and_active(constraint_ent)) {
|
|
/* Create hit event */
|
|
{
|
|
struct entity *event = entity_alloc(root);
|
|
entity_enable_prop(event, ENTITY_PROP_HIT_EVENT);
|
|
event->hit_event.e0 = e0->handle;
|
|
event->hit_event.e1 = e1->handle;
|
|
event->hit_event.normal = collider_res.normal;
|
|
entity_enable_prop(event, ENTITY_PROP_RELEASE_AT_END_OF_FRAME);
|
|
entity_enable_prop(event, ENTITY_PROP_ACTIVE);
|
|
|
|
/* Calculate point */
|
|
struct v2 point = collider_res.points[0].point;
|
|
if (collider_res.num_points > 1) {
|
|
point = v2_add(point, v2_mul(v2_sub(collider_res.points[1].point, point), 0.5f));
|
|
}
|
|
event->hit_event.point = point;
|
|
|
|
/* Calculate relative velocity */
|
|
struct v2 vrel;
|
|
{
|
|
struct v2 v0 = e0->linear_velocity;
|
|
struct v2 v1 = e1->linear_velocity;
|
|
f32 w0 = e0->angular_velocity;
|
|
f32 w1 = e1->angular_velocity;
|
|
struct v2 vcp0 = v2_sub(point, e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(point, e1_xf.og);
|
|
struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0));
|
|
struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1));
|
|
vrel = v2_sub(vel0, vel1);
|
|
}
|
|
event->hit_event.vrel = vrel;
|
|
}
|
|
|
|
/* Create constraint */
|
|
{
|
|
constraint_ent = entity_alloc(root);
|
|
constraint_ent->contact_constraint_data.e1 = e1->handle;
|
|
constraint_ent->contact_constraint_data.e0 = e0->handle;
|
|
constraint_ent->contact_constraint_data.skip_solve = entity_has_prop(e0, ENTITY_PROP_SENSOR) || entity_has_prop(e1, ENTITY_PROP_SENSOR)
|
|
|| !(entity_has_prop(e0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e1, ENTITY_PROP_PHYSICAL_DYNAMIC));
|
|
entity_enable_prop(constraint_ent, ENTITY_PROP_ACTIVE);
|
|
|
|
/* TODO: Should we recalculate normal as more contact points are added? */
|
|
entity_enable_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT);
|
|
activate_now(constraint_ent);
|
|
ASSERT(!entry); /* Existing entry should never be present here */
|
|
contact_lookup_set(&G.contact_lookup, lookup_hash, constraint_ent->handle);
|
|
}
|
|
}
|
|
constraint = &constraint_ent->contact_constraint_data;
|
|
constraint->normal = collider_res.normal;
|
|
constraint->friction = math_sqrt(e0->friction * e1->friction);
|
|
|
|
/* Delete old contacts that are no longer present */
|
|
for (u32 i = 0; i < constraint->num_points; ++i) {
|
|
struct contact_point *old = &constraint->points[i];
|
|
u32 id = old->id;
|
|
b32 found = false;
|
|
for (u32 j = 0; j < collider_res.num_points; ++j) {
|
|
if (collider_res.points[j].id == id) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
/* Delete contact by replacing with last in array */
|
|
*old = constraint->points[--constraint->num_points];
|
|
--i;
|
|
}
|
|
}
|
|
|
|
/* Update / insert returned contacts */
|
|
for (u32 i = 0; i < collider_res.num_points; ++i) {
|
|
struct collider_collision_point *res_point = &collider_res.points[i];
|
|
struct v2 point = res_point->point;
|
|
f32 sep = res_point->separation;
|
|
u32 id = res_point->id;
|
|
struct contact_point *contact = NULL;
|
|
/* Match */
|
|
for (u32 j = 0; j < constraint->num_points; ++j) {
|
|
struct contact_point *t = &constraint->points[j];
|
|
if (t->id == id) {
|
|
contact = t;
|
|
break;
|
|
}
|
|
}
|
|
if (!contact) {
|
|
/* Insert */
|
|
contact = &constraint->points[constraint->num_points++];
|
|
MEMZERO_STRUCT(contact);
|
|
contact->id = id;
|
|
constraint->softness = G.contact_softness;
|
|
constraint->pushout_velocity = 3.0f;
|
|
}
|
|
|
|
/* Update points & separation */
|
|
contact->point_local_e0 = xform_invert_mul_v2(e0_xf, point);
|
|
contact->point_local_e1 = xform_invert_mul_v2(e1_xf, point);
|
|
contact->starting_separation = sep;
|
|
|
|
#if COLLIDER_DEBUG
|
|
contact->dbg_pt = point;
|
|
#endif
|
|
}
|
|
} else if (constraint_ent->valid) {
|
|
constraint_ent->contact_constraint_data.num_points = 0;
|
|
}
|
|
|
|
/* TODO: Remove this (debugging) */
|
|
#if COLLIDER_DEBUG
|
|
{
|
|
struct string fdkey = STRING_FROM_BUFFER(BUFFER_FROM_STRUCT(&lookup_hash));
|
|
struct entity_handle *dbg_ent_handle = fixed_dict_get(&G.collision_debug_lookup.dict, fdkey);
|
|
if (!dbg_ent_handle) {
|
|
/* FIXME: Handle never released */
|
|
dbg_ent_handle = arena_push_zero(&G.collision_debug_lookup.arena, struct entity_handle);
|
|
}
|
|
|
|
struct entity *dbg_ent = entity_from_handle(store, *dbg_ent_handle);
|
|
|
|
if (!dbg_ent->valid) {
|
|
/* FIXME: Entity never released */
|
|
dbg_ent = entity_alloc(root);
|
|
entity_enable_prop(dbg_ent, ENTITY_PROP_COLLISION_DEBUG);
|
|
*dbg_ent_handle = dbg_ent->handle;
|
|
fixed_dict_set(&G.collision_debug_lookup.arena, &G.collision_debug_lookup.dict, fdkey, dbg_ent_handle);
|
|
}
|
|
|
|
struct collision_debug *dbg = &dbg_ent->collision_debug_data;
|
|
|
|
if (dbg->res.num_points == 0) {
|
|
if (collider_res.num_points > 0) {
|
|
++e0->colliding;
|
|
++e1->colliding;
|
|
}
|
|
} else {
|
|
if (collider_res.num_points == 0) {
|
|
--e0->colliding;
|
|
--e1->colliding;
|
|
}
|
|
}
|
|
dbg->e0 = e0->handle;
|
|
dbg->e1 = e1->handle;
|
|
dbg->res = collider_res;
|
|
|
|
if (constraint) {
|
|
MEMCPY(dbg->points, constraint->points, sizeof(dbg->points));
|
|
dbg->num_points = constraint->num_points;
|
|
} else {
|
|
dbg->num_points = 0;
|
|
}
|
|
|
|
dbg->xf0 = e0_xf;
|
|
dbg->xf1 = e1_xf;
|
|
|
|
/* Update closest points */
|
|
{
|
|
struct collider_closest_points_result closest_points_res = collider_closest_points(&e0_collider, &e1_collider, e0_xf, e1_xf);
|
|
dbg->closest0 = closest_points_res.p0;
|
|
dbg->closest1 = closest_points_res.p1;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void prepare_contacts(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *constraint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(constraint_ent)) continue;
|
|
if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue;
|
|
|
|
struct contact_constraint *constraint = &constraint_ent->contact_constraint_data;
|
|
|
|
u32 num_points = constraint->num_points;
|
|
struct entity *e0 = entity_from_handle(store, constraint->e0);
|
|
struct entity *e1 = entity_from_handle(store, constraint->e1);
|
|
if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) {
|
|
struct v2 normal = constraint->normal;
|
|
struct v2 tangent = v2_perp(normal);
|
|
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
/* TODO: Cache this */
|
|
/* Calculate masses */
|
|
f32 inv_m0;
|
|
f32 inv_m1;
|
|
f32 inv_i0;
|
|
f32 inv_i1;
|
|
{
|
|
f32 scale0 = math_fabs(xform_get_determinant(e0_xf));
|
|
f32 scale1 = math_fabs(xform_get_determinant(e1_xf));
|
|
inv_m0 = 1.f / (e0->mass_unscaled * scale0);
|
|
inv_m1 = 1.f / (e1->mass_unscaled * scale1);
|
|
inv_i0 = 1.f / (e0->inertia_unscaled * scale0);
|
|
inv_i1 = 1.f / (e1->inertia_unscaled * scale1);
|
|
}
|
|
constraint->inv_m0 = inv_m0;
|
|
constraint->inv_m1 = inv_m1;
|
|
constraint->inv_i0 = inv_i0;
|
|
constraint->inv_i1 = inv_i1;
|
|
|
|
if (entity_has_prop(e0, ENTITY_PROP_PHYSICAL_KINEMATIC)) {
|
|
constraint->inv_m0 = 0;
|
|
constraint->inv_i0 = 0;
|
|
}
|
|
if (entity_has_prop(e1, ENTITY_PROP_PHYSICAL_KINEMATIC)) {
|
|
constraint->inv_m1 = 0;
|
|
constraint->inv_i1 = 0;
|
|
}
|
|
|
|
/* Update / insert returned contacts */
|
|
for (u32 i = 0; i < num_points; ++i) {
|
|
struct contact_point *contact = &constraint->points[i];
|
|
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, contact->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, contact->point_local_e1), e1_xf.og);
|
|
|
|
/* Normal mass */
|
|
{
|
|
f32 vcp0_wedge = v2_wedge(vcp0, normal);
|
|
f32 vcp1_wedge = v2_wedge(vcp1, normal);
|
|
f32 k = (inv_m0 + inv_m1) + (inv_i0 * vcp0_wedge * vcp0_wedge) + (inv_i1 * vcp1_wedge * vcp1_wedge);
|
|
contact->inv_normal_mass = k > 0.0f ? 1.0f / k : 0.0f;
|
|
}
|
|
|
|
/* Tangent mass */
|
|
{
|
|
f32 vcp0_wedge = v2_wedge(vcp0, tangent);
|
|
f32 vcp1_wedge = v2_wedge(vcp1, tangent);
|
|
f32 k = (inv_m0 + inv_m1) + (inv_i0 * vcp0_wedge * vcp0_wedge) + (inv_i1 * vcp1_wedge * vcp1_wedge);
|
|
contact->inv_tangent_mass = k > 0.0f ? 1.0f / k : 0.0f;
|
|
}
|
|
|
|
#if !GAME_PHYSICS_ENABLE_WARM_STARTING
|
|
contact->normal_impulse = 0;
|
|
contact->tangent_impulse = 0;
|
|
#endif
|
|
}
|
|
} else {
|
|
/* Mark constraint for removal */
|
|
entity_disable_prop(constraint_ent, ENTITY_PROP_ACTIVE);
|
|
entity_enable_prop(constraint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME);
|
|
/* Remove from lookup */
|
|
u64 hash = contact_lookup_hash_from_entities(constraint->e0, constraint->e1);
|
|
struct contact_lookup_entry *entry = contact_lookup_get(&G.contact_lookup, hash);
|
|
if (entry) {
|
|
contact_lookup_remove(&G.contact_lookup, entry);
|
|
} else {
|
|
ASSERT(false); /* This should always exist */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void warm_start_contacts(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *constraint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(constraint_ent)) continue;
|
|
if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue;
|
|
|
|
struct contact_constraint *constraint = &constraint_ent->contact_constraint_data;
|
|
|
|
u32 num_points = constraint->num_points;
|
|
struct entity *e0 = entity_from_handle(store, constraint->e0);
|
|
struct entity *e1 = entity_from_handle(store, constraint->e1);
|
|
|
|
if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1) && !constraint->skip_solve) {
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
f32 inv_m0 = constraint->inv_m0;
|
|
f32 inv_m1 = constraint->inv_m1;
|
|
f32 inv_i0 = constraint->inv_i0;
|
|
f32 inv_i1 = constraint->inv_i1;
|
|
|
|
struct v2 v0 = e0->linear_velocity;
|
|
struct v2 v1 = e1->linear_velocity;
|
|
f32 w0 = e0->angular_velocity;
|
|
f32 w1 = e1->angular_velocity;
|
|
|
|
/* Warm start */
|
|
struct v2 normal = constraint->normal;
|
|
struct v2 tangent = v2_perp(normal);
|
|
f32 inv_num_points = 1.f / num_points;
|
|
for (u32 i = 0; i < num_points; ++i) {
|
|
struct contact_point *point = &constraint->points[i];
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, point->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, point->point_local_e1), e1_xf.og);
|
|
|
|
struct v2 impulse = v2_add(v2_mul(normal, point->normal_impulse), v2_mul(tangent, point->tangent_impulse));
|
|
impulse = v2_mul(impulse, inv_num_points);
|
|
|
|
v0 = v2_sub(v0, v2_mul(impulse, inv_m0));
|
|
v1 = v2_add(v1, v2_mul(impulse, inv_m1));
|
|
w0 -= v2_wedge(vcp0, impulse) * inv_i0;
|
|
w1 += v2_wedge(vcp1, impulse) * inv_i1;
|
|
}
|
|
|
|
e0->linear_velocity = v0;
|
|
e0->angular_velocity = w0;
|
|
e1->linear_velocity = v1;
|
|
e1->angular_velocity = w1;
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void solve_contacts(f32 dt, b32 apply_bias)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *constraint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(constraint_ent)) continue;
|
|
if (!entity_has_prop(constraint_ent, ENTITY_PROP_CONTACT_CONSTRAINT)) continue;
|
|
|
|
struct contact_constraint *constraint = &constraint_ent->contact_constraint_data;
|
|
|
|
struct entity *e0 = entity_from_handle(store, constraint->e0);
|
|
struct entity *e1 = entity_from_handle(store, constraint->e1);
|
|
|
|
struct v2 v0 = e0->linear_velocity;
|
|
struct v2 v1 = e1->linear_velocity;
|
|
f32 w0 = e0->angular_velocity;
|
|
f32 w1 = e1->angular_velocity;
|
|
|
|
u32 num_points = constraint->num_points;
|
|
if (num_points > 0 && entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1) && !constraint->skip_solve) {
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
f32 inv_m0 = constraint->inv_m0;
|
|
f32 inv_m1 = constraint->inv_m1;
|
|
f32 inv_i0 = constraint->inv_i0;
|
|
f32 inv_i1 = constraint->inv_i1;
|
|
|
|
/* Normal impulse */
|
|
struct v2 normal = constraint->normal;
|
|
for (u32 point_index = 0; point_index < num_points; ++point_index) {
|
|
struct contact_point *point = &constraint->points[point_index];
|
|
struct v2 p0 = xform_mul_v2(e0_xf, point->point_local_e0);
|
|
struct v2 p1 = xform_mul_v2(e1_xf, point->point_local_e1);
|
|
struct v2 vcp0 = v2_sub(p0, e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(p1, e1_xf.og);
|
|
|
|
f32 separation = v2_dot(v2_sub(p1, p0), normal) + point->starting_separation;
|
|
|
|
f32 velocity_bias = 0.0f;
|
|
f32 mass_scale = 1.0f;
|
|
f32 impulse_scale = 0.0f;
|
|
|
|
if (separation > 0.0f) {
|
|
/* Speculative */
|
|
velocity_bias = separation / dt;
|
|
} else if (apply_bias) {
|
|
/* Soft constraint */
|
|
struct math_spring_result softness = constraint->softness;
|
|
f32 pushout_velocity = constraint->pushout_velocity;
|
|
mass_scale = softness.mass_scale;
|
|
impulse_scale = softness.impulse_scale;
|
|
velocity_bias = max_f32(softness.bias_rate * separation, -pushout_velocity);
|
|
}
|
|
|
|
struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0));
|
|
struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1));
|
|
struct v2 vrel = v2_sub(vel0, vel1);
|
|
|
|
f32 k = point->inv_normal_mass;
|
|
|
|
/* (to be applied along n) */
|
|
f32 vn = v2_dot(vrel, normal);
|
|
f32 j = ((k * mass_scale) * (vn - velocity_bias)) - (point->normal_impulse * impulse_scale);
|
|
|
|
f32 old_impulse = point->normal_impulse;
|
|
f32 new_impulse = max_f32(old_impulse + j, 0);
|
|
f32 delta = new_impulse - old_impulse;
|
|
point->normal_impulse = new_impulse;
|
|
|
|
struct v2 impulse = v2_mul(normal, delta);
|
|
v0 = v2_sub(v0, v2_mul(impulse, inv_m0));
|
|
v1 = v2_add(v1, v2_mul(impulse, inv_m1));
|
|
w0 -= v2_wedge(vcp0, impulse) * inv_i0;
|
|
w1 += v2_wedge(vcp1, impulse) * inv_i1;
|
|
}
|
|
|
|
/* Tangent impulse */
|
|
struct v2 tangent = v2_perp(normal);
|
|
for (u32 point_index = 0; point_index < num_points; ++point_index) {
|
|
struct contact_point *point = &constraint->points[point_index];
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, point->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, point->point_local_e1), e1_xf.og);
|
|
|
|
struct v2 vel0 = v2_add(v0, v2_perp_mul(vcp0, w0));
|
|
struct v2 vel1 = v2_add(v1, v2_perp_mul(vcp1, w1));
|
|
struct v2 vrel = v2_sub(vel0, vel1);
|
|
|
|
f32 k = point->inv_tangent_mass;
|
|
|
|
/* (to be applied along t) */
|
|
f32 vt = v2_dot(vrel, tangent);
|
|
f32 j = vt * k;
|
|
|
|
f32 max_friction = constraint->friction * point->normal_impulse;
|
|
f32 old_impulse = point->tangent_impulse;
|
|
f32 new_impulse = clamp_f32(old_impulse + j, -max_friction, max_friction);
|
|
f32 delta = new_impulse - old_impulse;
|
|
point->tangent_impulse = new_impulse;
|
|
|
|
struct v2 impulse = v2_mul(tangent, delta);
|
|
v0 = v2_sub(v0, v2_mul(impulse, inv_m0));
|
|
v1 = v2_add(v1, v2_mul(impulse, inv_m1));
|
|
w0 -= v2_wedge(vcp0, impulse) * inv_i0;
|
|
w1 += v2_wedge(vcp1, impulse) * inv_i1;
|
|
}
|
|
|
|
e0->linear_velocity = v0;
|
|
e0->angular_velocity = w0;
|
|
e1->linear_velocity = v1;
|
|
e1->angular_velocity = w1;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================== *
|
|
* TESTING MOTOR JOINT
|
|
* ========================== */
|
|
|
|
INTERNAL void prepare_motor_joints(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue;
|
|
|
|
struct motor_joint *joint = &joint_ent->motor_joint_data;
|
|
|
|
struct entity *e0 = entity_from_handle(store, joint->e0);
|
|
struct entity *e1 = entity_from_handle(store, joint->e1);
|
|
|
|
if (entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) {
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
/* TODO: Cache this */
|
|
/* Calculate masses */
|
|
f32 inv_m0;
|
|
f32 inv_m1;
|
|
f32 inv_i0;
|
|
f32 inv_i1;
|
|
{
|
|
f32 scale0 = math_fabs(xform_get_determinant(e0_xf));
|
|
f32 scale1 = math_fabs(xform_get_determinant(e1_xf));
|
|
inv_m0 = 1.f / (e0->mass_unscaled * scale0);
|
|
inv_m1 = 1.f / (e1->mass_unscaled * scale1);
|
|
inv_i0 = 1.f / (e0->inertia_unscaled * scale0);
|
|
inv_i1 = 1.f / (e1->inertia_unscaled * scale1);
|
|
}
|
|
joint->inv_m0 = inv_m0;
|
|
joint->inv_m1 = inv_m1;
|
|
joint->inv_i0 = inv_i0;
|
|
joint->inv_i1 = inv_i1;
|
|
|
|
joint->point_local_e0 = V2(0, 0);
|
|
joint->point_local_e1 = V2(0, 0);
|
|
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og);
|
|
|
|
struct xform linear_mass_xf;
|
|
linear_mass_xf.bx.x = inv_m0 + inv_m1 + vcp0.y * vcp0.y * inv_i0 + vcp1.y * vcp1.y * inv_i1;
|
|
linear_mass_xf.bx.y = -vcp0.y * vcp0.x * inv_i0 - vcp1.y * vcp1.x * inv_i1;
|
|
linear_mass_xf.by.x = linear_mass_xf.bx.y;
|
|
linear_mass_xf.by.y = inv_m0 + inv_m1 + vcp0.x * vcp0.x * inv_i0 + vcp1.x * vcp1.x * inv_i1;
|
|
joint->linear_mass_xf = xform_invert(linear_mass_xf);
|
|
|
|
joint->angular_mass = 1.f / (inv_i0 + inv_i1);
|
|
|
|
#if !GAME_PHYSICS_ENABLE_WARM_STARTING
|
|
joint->linear_impulse = V2(0, 0);
|
|
joint->angular_impulse = 0;
|
|
#endif
|
|
} else {
|
|
/* Mark joint for removal */
|
|
entity_disable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME);
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void warm_start_motor_joints(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue;
|
|
|
|
struct motor_joint *joint = &joint_ent->motor_joint_data;
|
|
|
|
struct entity *e0 = entity_from_handle(store, joint->e0);
|
|
struct entity *e1 = entity_from_handle(store, joint->e1);
|
|
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
f32 inv_m0 = joint->inv_m0;
|
|
f32 inv_m1 = joint->inv_m1;
|
|
f32 inv_i0 = joint->inv_i0;
|
|
f32 inv_i1 = joint->inv_i1;
|
|
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og);
|
|
|
|
e0->linear_velocity = v2_sub(e0->linear_velocity, v2_mul(joint->linear_impulse, inv_m0));
|
|
e1->linear_velocity = v2_add(e1->linear_velocity, v2_mul(joint->linear_impulse, inv_m1));
|
|
e0->angular_velocity -= (v2_wedge(vcp0, joint->linear_impulse) + joint->angular_impulse) * inv_i0;
|
|
e1->angular_velocity += (v2_wedge(vcp1, joint->linear_impulse) + joint->angular_impulse) * inv_i1;
|
|
}
|
|
}
|
|
|
|
INTERNAL void solve_motor_joints(f32 dt)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT)) continue;
|
|
|
|
struct motor_joint *joint = &joint_ent->motor_joint_data;
|
|
|
|
struct entity *e0 = entity_from_handle(store, joint->e0);
|
|
struct entity *e1 = entity_from_handle(store, joint->e1);
|
|
|
|
struct xform e0_xf = entity_get_xform(e0);
|
|
struct xform e1_xf = entity_get_xform(e1);
|
|
|
|
f32 inv_m0 = joint->inv_m0;
|
|
f32 inv_m1 = joint->inv_m1;
|
|
f32 inv_i0 = joint->inv_i0;
|
|
f32 inv_i1 = joint->inv_i1;
|
|
|
|
struct v2 v0 = e0->linear_velocity;
|
|
struct v2 v1 = e1->linear_velocity;
|
|
f32 w0 = e0->angular_velocity;
|
|
f32 w1 = e1->angular_velocity;
|
|
|
|
f32 correction_rate = joint->correction_rate / dt;
|
|
|
|
/* Angular constraint */
|
|
{
|
|
f32 max_impulse = joint->max_torque * dt;
|
|
|
|
f32 angular_separation = math_unwind_angle(xform_get_rotation(e1_xf) - xform_get_rotation(e0_xf));
|
|
f32 angular_bias = angular_separation * correction_rate;
|
|
|
|
f32 impulse = -joint->angular_mass * (w1 - w0 + angular_bias);
|
|
|
|
f32 old_impulse = joint->angular_impulse;
|
|
joint->angular_impulse = clamp_f32(joint->angular_impulse + impulse, -max_impulse, max_impulse);
|
|
impulse = joint->angular_impulse - old_impulse;
|
|
|
|
w0 -= impulse * inv_i0;
|
|
w1 += impulse * inv_i1;
|
|
}
|
|
|
|
/* Linear constraint */
|
|
{
|
|
struct v2 vcp0 = v2_sub(xform_mul_v2(e0_xf, joint->point_local_e0), e0_xf.og);
|
|
struct v2 vcp1 = v2_sub(xform_mul_v2(e1_xf, joint->point_local_e1), e1_xf.og);
|
|
|
|
f32 max_impulse = joint->max_force * dt;
|
|
|
|
struct v2 linear_separation = v2_sub(v2_add(e1_xf.og, vcp1), v2_add(e0_xf.og, vcp0));
|
|
struct v2 linear_bias = v2_mul(linear_separation, correction_rate);
|
|
|
|
struct v2 vrel = v2_sub(v2_add(v1, v2_perp_mul(vcp1, w1)), v2_add(v0, v2_perp_mul(vcp0, w0)));
|
|
struct v2 impulse = v2_neg(xform_basis_mul_v2(joint->linear_mass_xf, v2_add(vrel, linear_bias)));
|
|
|
|
struct v2 old_impulse = joint->linear_impulse;
|
|
joint->linear_impulse = v2_clamp_len(v2_add(joint->linear_impulse, impulse), max_impulse);
|
|
impulse = v2_sub(joint->linear_impulse, old_impulse);
|
|
|
|
v0 = v2_sub(v0, v2_mul(impulse, inv_m0));
|
|
v1 = v2_add(v1, v2_mul(impulse, inv_m1));
|
|
w0 -= v2_wedge(vcp0, impulse) * inv_i0;
|
|
w1 += v2_wedge(vcp1, impulse) * inv_i1;
|
|
}
|
|
|
|
e0->linear_velocity = v0;
|
|
e0->angular_velocity = w0;
|
|
e1->linear_velocity = v1;
|
|
e1->angular_velocity = w1;
|
|
}
|
|
}
|
|
|
|
#if 1
|
|
|
|
/* ========================== *
|
|
* TESTING MOUSE JOINT
|
|
* ========================== */
|
|
|
|
INTERNAL void create_mouse_joints(struct game_cmd_array game_cmds)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
struct entity *root = G.root;
|
|
|
|
b32 start_dragging = false;
|
|
b32 stop_dragging = false;
|
|
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;
|
|
switch (cmd.kind) {
|
|
case GAME_CMD_KIND_DRAG_OBJECT:
|
|
{
|
|
if (start) {
|
|
start_dragging = true;
|
|
} else if (stop) {
|
|
stop_dragging = true;
|
|
}
|
|
} break;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
struct v2 cursor = G.user_cursor;
|
|
|
|
struct entity *joint_ent = entity_find_first_match_one(store, ENTITY_PROP_MOUSE_JOINT);
|
|
struct entity *target_ent = entity_from_handle(store, joint_ent->mouse_joint_data.target);
|
|
|
|
if (start_dragging) {
|
|
struct xform mouse_xf = xform_from_pos(cursor);
|
|
struct collider_shape mouse_shape = ZI;
|
|
mouse_shape.points[0] = V2(0, 0);
|
|
mouse_shape.count = 1;
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC)) continue;
|
|
|
|
struct collider_shape ent_collider = ent->local_collider;
|
|
if (ent_collider.count > 0) {
|
|
struct xform ent_xf = entity_get_xform(ent);
|
|
/* TODO: Can just use boolean GJK */
|
|
struct collider_collision_points_result res = collider_collision_points(&ent_collider, &mouse_shape, ent_xf, mouse_xf);
|
|
if (res.num_points > 0) {
|
|
target_ent = ent;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (stop_dragging) {
|
|
target_ent = entity_nil();
|
|
}
|
|
|
|
if (entity_is_valid_and_active(target_ent)) {
|
|
if (!entity_is_valid_and_active(joint_ent)) {
|
|
joint_ent = entity_alloc(root);
|
|
joint_ent->mass_unscaled = F32_INFINITY;
|
|
joint_ent->inertia_unscaled = F32_INFINITY;
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
}
|
|
|
|
struct mouse_joint *joint = &joint_ent->mouse_joint_data;
|
|
|
|
struct xform xf = entity_get_xform(target_ent);
|
|
f32 mass = target_ent->mass_unscaled * math_fabs(xform_get_determinant(xf));
|
|
|
|
if (!entity_handle_eq(joint->target, target_ent->handle)) {
|
|
joint->point_local_start = xform_invert_mul_v2(xf, cursor);
|
|
joint->target = target_ent->handle;
|
|
}
|
|
joint->point_local_end = xform_invert_mul_v2(xf, cursor);
|
|
|
|
joint->linear_softness = G.mouse_joint_linear_softness;
|
|
joint->angular_softness = G.mouse_joint_angular_softness;
|
|
joint->max_force = G.mouse_joint_max_force * mass;
|
|
} else {
|
|
if (entity_is_valid_and_active(joint_ent)) {
|
|
joint_ent->mouse_joint_data.target = entity_nil_handle();
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void prepare_mouse_joints(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue;
|
|
|
|
struct mouse_joint *joint = &joint_ent->mouse_joint_data;
|
|
struct entity *ent = entity_from_handle(store, joint->target);
|
|
if (entity_is_valid_and_active(ent)) {
|
|
struct xform xf = entity_get_xform(ent);
|
|
|
|
/* TODO: Cache this */
|
|
/* Calculate masses */
|
|
f32 inv_m;
|
|
f32 inv_i;
|
|
{
|
|
f32 scale = math_fabs(xform_get_determinant(xf));
|
|
inv_m = 1.f / (ent->mass_unscaled * scale);
|
|
inv_i = 1.f / (ent->inertia_unscaled * scale);
|
|
}
|
|
joint->inv_m = inv_m;
|
|
joint->inv_i = inv_i;
|
|
|
|
struct v2 vcp = v2_sub(xform_mul_v2(xf, joint->point_local_start), xf.og);
|
|
|
|
struct xform linear_mass_xf;
|
|
linear_mass_xf.bx.x = inv_m + inv_i * vcp.y * vcp.y;
|
|
linear_mass_xf.bx.y = -inv_i * vcp.x * vcp.y;
|
|
linear_mass_xf.by.x = linear_mass_xf.bx.y;
|
|
linear_mass_xf.by.y = inv_m + inv_i * vcp.x * vcp.x;
|
|
joint->linear_mass_xf = xform_invert(linear_mass_xf);
|
|
|
|
#if !GAME_PHYSICS_ENABLE_WARM_STARTING
|
|
joint->linear_impulse = V2(0, 0);
|
|
joint->angular_impulse = 0;
|
|
#endif
|
|
} else {
|
|
/* Mark joint for removal */
|
|
entity_disable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME);
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void warm_start_mouse_joints(void)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue;
|
|
|
|
struct mouse_joint *joint = &joint_ent->mouse_joint_data;
|
|
struct entity *ent = entity_from_handle(store, joint->target);
|
|
if (entity_is_valid_and_active(ent)) {
|
|
f32 inv_m = joint->inv_m;
|
|
f32 inv_i = joint->inv_i;
|
|
struct xform xf = entity_get_xform(ent);
|
|
struct v2 vcp = v2_sub(xform_mul_v2(xf, joint->point_local_start), xf.og);
|
|
ent->linear_velocity = v2_add(ent->linear_velocity, v2_mul(joint->linear_impulse, inv_m));
|
|
ent->angular_velocity += (v2_wedge(vcp, joint->linear_impulse) + joint->angular_impulse) * inv_i;
|
|
}
|
|
}
|
|
}
|
|
|
|
INTERNAL void solve_mouse_joints(f32 dt)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *joint_ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(joint_ent)) continue;
|
|
if (!entity_has_prop(joint_ent, ENTITY_PROP_MOUSE_JOINT)) continue;
|
|
|
|
struct mouse_joint *joint = &joint_ent->mouse_joint_data;
|
|
struct entity *ent = entity_from_handle(store, joint->target);
|
|
if (entity_is_valid_and_active(ent)) {
|
|
struct v2 v = ent->linear_velocity;
|
|
f32 w = ent->angular_velocity;
|
|
|
|
f32 inv_m = joint->inv_m;
|
|
f32 inv_i = joint->inv_i;
|
|
|
|
/* Angular impulse */
|
|
{
|
|
struct math_spring_result softness = joint->angular_softness;
|
|
f32 mass_scale = softness.mass_scale;
|
|
f32 impulse_scale = softness.impulse_scale;
|
|
f32 impulse = mass_scale * (-w / inv_i) - impulse_scale * joint->angular_impulse;
|
|
joint->angular_impulse += impulse;
|
|
w += impulse * inv_i;
|
|
}
|
|
|
|
/* Linear impulse */
|
|
{
|
|
f32 max_impulse = joint->max_force / dt;
|
|
|
|
struct xform xf = entity_get_xform(ent);
|
|
|
|
struct v2 point_start = xform_mul_v2(xf, joint->point_local_start);
|
|
struct v2 point_end = xform_mul_v2(xf, joint->point_local_end);
|
|
|
|
struct v2 vcp = v2_sub(point_start, xf.og);
|
|
struct v2 separation = v2_sub(point_start, point_end);
|
|
|
|
struct math_spring_result softness = joint->linear_softness;
|
|
struct v2 bias = v2_mul(separation, softness.bias_rate);
|
|
f32 mass_scale = softness.mass_scale;
|
|
f32 impulse_scale = softness.impulse_scale;
|
|
|
|
#if 0
|
|
struct v2 vel = v2_add(v, v2_perp_mul(vcp, w));
|
|
struct v2 b = v2_mul(xform_basis_mul_v2(xform_invert(joint->linear_mass_xf), v2_add(vel, bias)), mass_scale);
|
|
|
|
struct v2 old_impulse = joint->linear_impulse;
|
|
struct v2 impulse = v2_sub(v2_mul(old_impulse, -impulse_scale), b);
|
|
joint->linear_impulse = v2_clamp_len(v2_add(joint->linear_impulse, impulse), max_impulse);
|
|
impulse = v2_sub(joint->linear_impulse, old_impulse);
|
|
#else
|
|
struct v2 vel = v2_add(v, v2_perp_mul(vcp, w));
|
|
struct v2 b = xform_basis_mul_v2(joint->linear_mass_xf, v2_add(vel, bias));
|
|
|
|
struct v2 impulse;
|
|
impulse.x = -mass_scale * b.x - impulse_scale * joint->linear_impulse.x;
|
|
impulse.y = -mass_scale * b.y - impulse_scale * joint->linear_impulse.y;
|
|
|
|
struct v2 old_impulse = joint->linear_impulse;
|
|
joint->linear_impulse.x += impulse.x;
|
|
joint->linear_impulse.y += impulse.y;
|
|
|
|
joint->linear_impulse = v2_clamp_len(joint->linear_impulse, max_impulse);
|
|
|
|
impulse.x = joint->linear_impulse.x - old_impulse.x;
|
|
impulse.y = joint->linear_impulse.y - old_impulse.y;
|
|
#endif
|
|
|
|
v = v2_add(v, v2_mul(impulse, inv_m));
|
|
w += v2_wedge(vcp, impulse) * inv_i;
|
|
}
|
|
|
|
ent->linear_velocity = v;
|
|
ent->angular_velocity = w;
|
|
}
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
INTERNAL void prepare_mouse_joints(void)
|
|
{
|
|
}
|
|
|
|
INTERNAL void warm_start_mouse_joints(void)
|
|
{
|
|
}
|
|
|
|
INTERNAL void solve_mouse_joints(f32 dt)
|
|
{
|
|
(UNUSED)dt;
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================== *
|
|
* TESTING PHYSICS INTEGRATION
|
|
* ========================== */
|
|
|
|
INTERNAL void integrate_velocities_from_forces(f32 dt)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC) && !entity_has_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC)) continue;
|
|
|
|
struct xform xf = entity_get_xform(ent);
|
|
f32 det_abs = math_fabs(xform_get_determinant(xf));
|
|
f32 mass = ent->mass_unscaled * det_abs;
|
|
f32 inertia = ent->inertia_unscaled * det_abs;
|
|
|
|
/* Determine force & torque acceleration */
|
|
struct v2 force_accel = v2_mul(v2_div(ent->force, mass), dt);
|
|
f32 torque_accel = (ent->torque / inertia) * dt;
|
|
|
|
/* Integrate & clamp */
|
|
ent->linear_velocity = v2_clamp_len(v2_add(ent->linear_velocity, force_accel), GAME_MAX_LINEAR_VELOCITY);
|
|
ent->angular_velocity = clamp_f32(ent->angular_velocity + torque_accel, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY);
|
|
|
|
/* Reset forces */
|
|
ent->force = V2(0, 0);
|
|
ent->torque = 0;
|
|
}
|
|
}
|
|
|
|
INTERNAL void integrate_positions_from_velocities(f32 dt)
|
|
{
|
|
struct entity_store *store = G.tick.entity_store;
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC) && !entity_has_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC)) continue;
|
|
|
|
/* Clamp velocities */
|
|
ent->linear_velocity = v2_clamp_len(ent->linear_velocity, GAME_MAX_LINEAR_VELOCITY);
|
|
ent->angular_velocity = clamp_f32(ent->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY);
|
|
|
|
struct v2 tick_linear_velocity = v2_mul(ent->linear_velocity, dt);
|
|
f32 tick_angular_velocity = ent->angular_velocity * dt;
|
|
|
|
struct xform xf = entity_get_xform(ent);
|
|
xf.og = v2_add(xf.og, tick_linear_velocity);
|
|
xf = xform_basis_rotated_world(xf, tick_angular_velocity);
|
|
entity_set_xform(ent, xf);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================== *
|
|
* TESTING TOI
|
|
* ========================== */
|
|
|
|
/* Takes 2 shapes and their xforms at t=0 and t=1.
|
|
* Returns time of impact in range [0, 1]. */
|
|
INTERNAL f32 toi(struct collider_shape *c0, struct collider_shape *c1,
|
|
struct xform xf0_t0, struct xform xf1_t0,
|
|
struct xform xf0_t1, struct xform xf1_t1,
|
|
f32 tolerance)
|
|
{
|
|
/* Find direction p0 -> p1 at t=0 */
|
|
struct v2 dir;
|
|
struct v2 dir_neg;
|
|
{
|
|
struct collider_closest_points_result closest_points_res = collider_closest_points(c0, c1, xf0_t0, xf1_t0);
|
|
if (closest_points_res.colliding) {
|
|
/* Shapes are penetrating at t=0 */
|
|
return 0;
|
|
}
|
|
dir = v2_norm(v2_sub(closest_points_res.p1, closest_points_res.p0));
|
|
dir_neg = v2_neg(dir);
|
|
}
|
|
|
|
/* Safety check that shapes penetrate at t=1 */
|
|
f32 sep;
|
|
{
|
|
struct v2 p0 = collider_support_point(c0, xf0_t1, dir);
|
|
struct v2 p1 = collider_support_point(c1, xf1_t1, dir_neg);
|
|
sep = v2_dot(dir, v2_sub(p1, p0));
|
|
if (sep > tolerance) {
|
|
/* Shapes are not penetrating at t=1 */
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/* Bisect until distance is within tolerance */
|
|
/* TODO: Implement false position method as well? (should speed up more linear cases) */
|
|
f32 t0 = 0.0;
|
|
f32 t1 = 1.0;
|
|
f32 t = 0.5f;
|
|
while (math_fabs(sep) > tolerance) {
|
|
struct xform xf0 = xform_lerp(xf0_t0, xf0_t1, t);
|
|
struct xform xf1 = xform_lerp(xf1_t0, xf1_t1, t);
|
|
|
|
struct v2 p0 = collider_support_point(c0, xf0, dir);
|
|
struct v2 p1 = collider_support_point(c1, xf1, dir_neg);
|
|
|
|
sep = v2_dot(dir, v2_sub(p1, p0));
|
|
|
|
/* Update bracket */
|
|
if (sep > 0) {
|
|
t0 = t;
|
|
} else {
|
|
t1 = t;
|
|
}
|
|
t = (t1 + t0) / 2.0;
|
|
}
|
|
|
|
return t;
|
|
}
|
|
|
|
INTERNAL f32 determine_earliest_toi(f32 dt, f32 tolerance)
|
|
{
|
|
__prof;
|
|
f32 smallest_t = 1;
|
|
|
|
struct entity_store *store = G.tick.entity_store;
|
|
//struct entity *root = G.root;
|
|
|
|
|
|
for (u64 e0_index = 0; e0_index < store->reserved; ++e0_index) {
|
|
struct entity *e0 = &store->entities[e0_index];
|
|
if (!entity_is_valid_and_active(e0)) continue;
|
|
if (!(entity_has_prop(e0, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e0, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue;
|
|
if (e0->local_collider.count <= 0) continue;
|
|
|
|
struct collider_shape e0_collider = e0->local_collider;
|
|
struct xform e0_xf_t0 = entity_get_xform(e0);
|
|
struct xform e0_xf_t1 = e0_xf_t0;
|
|
{
|
|
/* Calculate xform at t=1 */
|
|
struct v2 linear_velocity = v2_clamp_len(e0->linear_velocity, GAME_MAX_LINEAR_VELOCITY);
|
|
f32 angular_velocity = clamp_f32(e0->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY);
|
|
struct v2 tick_linear_velocity = v2_mul(linear_velocity, dt);
|
|
f32 tick_angular_velocity = angular_velocity * dt;
|
|
e0_xf_t1.og = v2_add(e0_xf_t1.og, tick_linear_velocity);
|
|
e0_xf_t1 = xform_basis_rotated_world(e0_xf_t1, tick_angular_velocity);
|
|
}
|
|
|
|
|
|
/* Start e1 index at e0 index + 1 to prevent redundant checks */
|
|
for (u64 e1_index = e0_index + 1; e1_index < store->reserved; ++e1_index) {
|
|
struct entity *e1 = &store->entities[e1_index];
|
|
if (e1 == e0) continue;
|
|
if (!entity_is_valid_and_active(e1)) continue;
|
|
if (!(entity_has_prop(e1, ENTITY_PROP_PHYSICAL_DYNAMIC) || entity_has_prop(e1, ENTITY_PROP_PHYSICAL_KINEMATIC))) continue;
|
|
if (e1->local_collider.count <= 0) continue;
|
|
|
|
struct collider_shape e1_collider = e1->local_collider;
|
|
struct xform e1_xf_t0 = entity_get_xform(e1);
|
|
struct xform e1_xf_t1 = e1_xf_t0;
|
|
{
|
|
/* Calculate xform at t=1 */
|
|
struct v2 linear_velocity = v2_clamp_len(e1->linear_velocity, GAME_MAX_LINEAR_VELOCITY);
|
|
f32 angular_velocity = clamp_f32(e1->angular_velocity, -GAME_MAX_ANGULAR_VELOCITY, GAME_MAX_ANGULAR_VELOCITY);
|
|
struct v2 tick_linear_velocity = v2_mul(linear_velocity, dt);
|
|
f32 tick_angular_velocity = angular_velocity * dt;
|
|
e1_xf_t1.og = v2_add(e1_xf_t1.og, tick_linear_velocity);
|
|
e1_xf_t1 = xform_basis_rotated_world(e1_xf_t1, tick_angular_velocity);
|
|
}
|
|
|
|
f32 t = toi(&e0_collider, &e1_collider, e0_xf_t0, e1_xf_t0, e0_xf_t1, e1_xf_t1, tolerance);
|
|
if (t != 0 && t < smallest_t) {
|
|
smallest_t = t;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return smallest_t;
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ========================== *
|
|
* Update
|
|
* ========================== */
|
|
|
|
INTERNAL void publish_game_tick(void)
|
|
{
|
|
__prof;
|
|
struct sys_lock lock = sys_mutex_lock_e(&G.prev_tick_mutex);
|
|
arena_set_readwrite(&G.prev_tick.entity_store->arena);
|
|
{
|
|
world_copy_replace(&G.prev_tick, &G.tick);
|
|
}
|
|
arena_set_readonly(&G.prev_tick.entity_store->arena);
|
|
atomic_u64_eval_exchange(&G.prev_tick_id, G.prev_tick.tick_id);
|
|
atomic_u64_eval_exchange(&G.prev_tick_continuity_gen, G.prev_tick.continuity_gen);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
INTERNAL void game_update(struct game_cmd_array game_cmds)
|
|
{
|
|
__prof;
|
|
|
|
struct temp_arena scratch = scratch_begin_no_conflict();
|
|
|
|
/* ========================== *
|
|
* Reset level if necessary
|
|
* ========================== */
|
|
|
|
if (G.should_reset_level) {
|
|
logf_info("Clearing level");
|
|
G.should_reset_level = false;
|
|
G.extra_spawn = false;
|
|
reset_world();
|
|
}
|
|
|
|
/* ========================== *
|
|
* Begin frame
|
|
* ========================== */
|
|
|
|
++G.tick.tick_id;
|
|
G.tick.tick_ts = sys_timestamp();
|
|
G.tick.dt = max_f64(0.0, (1.0 / GAME_FPS) * G.tick.timescale);
|
|
G.tick.time += G.tick.dt;
|
|
|
|
f64 dt = G.tick.dt;
|
|
f64 time = G.tick.time;
|
|
G.sprite_frame_scope = sprite_scope_begin();
|
|
G.root = entity_from_handle(G.tick.entity_store, G.tick.entity_store->root);
|
|
|
|
struct entity *root = G.root;
|
|
struct entity_store *store = G.tick.entity_store;
|
|
struct sprite_scope *sprite_frame_scope = G.sprite_frame_scope;
|
|
|
|
(UNUSED)dt;
|
|
(UNUSED)time;
|
|
|
|
/* ========================== *
|
|
* 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) {
|
|
/* Cursor */
|
|
case GAME_CMD_KIND_CURSOR_MOVE:
|
|
{
|
|
G.user_cursor = cmd.cursor_pos;
|
|
} break;
|
|
|
|
/* Clear level */
|
|
case GAME_CMD_KIND_CLEAR_ALL:
|
|
{
|
|
G.should_reset_level = true;
|
|
} break;
|
|
|
|
/* Spawn test */
|
|
case GAME_CMD_KIND_SPAWN_TEST:
|
|
{
|
|
logf_info("Spawning (test)");
|
|
spawn_test_entities();
|
|
} break;
|
|
default: break;
|
|
};
|
|
}
|
|
|
|
/* ========================== *
|
|
* Activate entities
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
if (!entity_has_prop(ent, ENTITY_PROP_ACTIVE)) {
|
|
u64 atick = ent->activation_tick;
|
|
if (atick != 0 || G.tick.tick_id >= atick) {
|
|
activate_now(ent);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Reset triggered entities
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TRIGGER_NEXT_TICK)) {
|
|
entity_disable_prop(ent, ENTITY_PROP_TRIGGER_NEXT_TICK);
|
|
entity_enable_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK);
|
|
} else if (entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) {
|
|
entity_disable_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update entity from sprite
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (sprite_tag_is_nil(ent->sprite)) continue;
|
|
|
|
/* Update animation */
|
|
|
|
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 + 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 local xform */
|
|
|
|
{
|
|
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_rotated(xf, -rot);
|
|
xf = xform_scaled(xf, sprite_size);
|
|
xf = xform_translated(xf, v2_neg(slice.center));
|
|
ent->sprite_local_xform = xf;
|
|
}
|
|
|
|
/* Update collider from sprite */
|
|
if (ent->sprite_collider_slice.len > 0) {
|
|
struct xform cxf = ent->sprite_local_xform;
|
|
|
|
#if 0
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST)) {
|
|
f32 scale = 0.5;
|
|
cxf = xform_scaled(cxf, V2(scale, scale));
|
|
}
|
|
#endif
|
|
|
|
struct sprite_sheet_slice slice = sprite_sheet_get_slice(sheet, ent->sprite_collider_slice, ent->animation_frame);
|
|
ent->local_collider = collider_from_quad(xform_mul_quad(cxf, quad_from_rect(slice.rect)));
|
|
|
|
#if 1
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST)) {
|
|
//if ((true)) {
|
|
#if 0
|
|
ent->local_collider.points[0] = V2(0, 0);
|
|
ent->local_collider.count = 1;
|
|
ent->local_collider.radius = 0.5;
|
|
#elif 1
|
|
ent->local_collider.points[0] = v2_with_len(V2(0.08, 0.17), 0.15);
|
|
ent->local_collider.points[1] = v2_with_len(V2(-0.07, -0.2), 0.15);
|
|
ent->local_collider.count = 2;
|
|
ent->local_collider.radius = 0.075;
|
|
#elif 1
|
|
#if 0
|
|
/* "Bad" winding order */
|
|
ent->local_collider.points[0] = V2(-0.15, 0.15);
|
|
ent->local_collider.points[1] = V2(0.15, 0.15);
|
|
ent->local_collider.points[2] = V2(0, -0.15);
|
|
#else
|
|
ent->local_collider.points[0] = V2(0, -0.15);
|
|
ent->local_collider.points[1] = V2(0.15, 0.15);
|
|
ent->local_collider.points[2] = V2(-0.15, 0.15);
|
|
#endif
|
|
ent->local_collider.count = 3;
|
|
ent->local_collider.radius = 0.25;
|
|
//ent->local_collider.radius = math_fabs(math_sin(G.tick.time) / 3);
|
|
#else
|
|
//ent->local_collider.radius = 0.5;
|
|
ent->local_collider.radius = 0.25;
|
|
//ent->local_collider.radius = 0.;
|
|
#endif
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update attachments
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_ATTACHED)) continue;
|
|
|
|
struct entity *parent = entity_from_handle(store, ent->parent);
|
|
struct sprite_tag parent_sprite = parent->sprite;
|
|
struct sprite_sheet *parent_sheet = sprite_sheet_from_tag_await(sprite_frame_scope, parent_sprite);
|
|
|
|
struct xform parent_sprite_xf = parent->sprite_local_xform;
|
|
|
|
struct sprite_sheet_slice attach_slice = sprite_sheet_get_slice(parent_sheet, ent->attach_slice, parent->animation_frame);
|
|
struct v2 attach_pos = xform_mul_v2(parent_sprite_xf, attach_slice.center);
|
|
struct v2 attach_dir = xform_basis_mul_v2(parent_sprite_xf, attach_slice.dir);
|
|
|
|
struct xform xf = entity_get_local_xform(ent);
|
|
xf.og = attach_pos;
|
|
xf = xform_basis_with_rotation_world(xf, v2_angle(attach_dir) + PI / 2);
|
|
entity_set_local_xform(ent, xf);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update control from player cmds
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
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;
|
|
|
|
/* 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 = cmd.aim_dir;
|
|
} break;
|
|
|
|
case GAME_CMD_KIND_PLAYER_FIRE:
|
|
{
|
|
if (start) {
|
|
firing = true;
|
|
} else if (stop) {
|
|
firing = false;
|
|
}
|
|
} break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
/* Movement */
|
|
if (v2_len_sq(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
|
|
* ========================== */
|
|
|
|
#if 0
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_TEST)) continue;
|
|
|
|
#if 0
|
|
if (!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;
|
|
}
|
|
|
|
f32 t = (f32)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_rotated_to(xf, r);
|
|
xf = xform_scaled_to(xf, s);
|
|
entity_set_local_xform(ent, xf);
|
|
#else
|
|
f32 t = (f32)time;
|
|
struct v2 og = v2_mul(V2(math_cos(t), math_sin(t)), 3);
|
|
f32 rot = t * PI / 3;
|
|
struct v2 scale = V2(1 + (math_fabs(math_sin(t * 5)) * 3), 1);
|
|
(UNUSED)og;
|
|
(UNUSED)rot;
|
|
(UNUSED)scale;
|
|
|
|
struct xform xf = entity_get_local_xform(ent);
|
|
xf = xform_rotated_to(xf, rot);
|
|
xf = xform_scaled_to(xf, scale);
|
|
entity_set_local_xform(ent, xf);
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
/* ========================== *
|
|
* Trigger equipped
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TRIGGERING_EQUIPPED)) {
|
|
struct entity *eq = entity_from_handle(store, ent->equipped);
|
|
if (entity_is_valid_and_active(eq)) {
|
|
entity_enable_prop(eq, ENTITY_PROP_TRIGGERED_THIS_TICK);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Process triggered entities
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_TRIGGERED_THIS_TICK)) continue;
|
|
if ((time - ent->last_triggered < ent->trigger_delay) && ent->last_triggered != 0) continue;
|
|
|
|
ent->last_triggered = time;
|
|
|
|
/* Fire weapon */
|
|
if (entity_has_prop(ent, ENTITY_PROP_WEAPON)) {
|
|
struct sprite_tag sprite = ent->sprite;
|
|
u32 animation_frame = ent->animation_frame;
|
|
struct sprite_sheet *sheet = sprite_sheet_from_tag_await(sprite_frame_scope, sprite);
|
|
|
|
struct xform sprite_local_xform = ent->sprite_local_xform;
|
|
|
|
struct sprite_sheet_slice out_slice = sprite_sheet_get_slice(sheet, STR("out"), animation_frame);
|
|
struct v2 rel_pos = xform_mul_v2(sprite_local_xform, out_slice.center);
|
|
struct v2 rel_dir = xform_basis_mul_v2(sprite_local_xform, out_slice.dir);
|
|
|
|
{
|
|
/* Spawn bullet */
|
|
struct entity *bullet = entity_alloc(root);
|
|
bullet->sprite = sprite_tag_from_path(STR("res/graphics/bullet.ase"));
|
|
|
|
bullet->bullet_src = ent->handle;
|
|
bullet->bullet_src_pos = rel_pos;
|
|
bullet->bullet_src_dir = rel_dir;
|
|
//bullet->bullet_impulse = 0.1f;
|
|
//bullet->bullet_impulse = 0.25f;
|
|
//bullet->bullet_impulse = 5.f;
|
|
bullet->bullet_impulse = 100000.f;
|
|
bullet->mass_unscaled = 0.04f;
|
|
bullet->inertia_unscaled = 0.00001f;
|
|
bullet->sprite_collider_slice = STR("shape");
|
|
|
|
entity_enable_prop(bullet, ENTITY_PROP_BULLET);
|
|
entity_enable_prop(bullet, ENTITY_PROP_SENSOR);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Create forces from control move
|
|
* ========================== */
|
|
|
|
#if 0
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
struct v2 move = ent->control.move;
|
|
struct v2 force = v2_mul(move, ent->control_force);
|
|
entity_apply_force_to_center(ent, force);
|
|
}
|
|
}
|
|
#else
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
struct entity *joint_ent = entity_from_handle(store, ent->move_joint);
|
|
if (!entity_is_valid_and_active(joint_ent)) {
|
|
joint_ent = entity_alloc(root);
|
|
joint_ent->mass_unscaled = F32_INFINITY;
|
|
joint_ent->inertia_unscaled = F32_INFINITY;
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
ent->move_joint = joint_ent->handle;
|
|
|
|
struct motor_joint_def def = ZI;
|
|
def.e0 = joint_ent->handle; /* Re-using joint entity as e0 */
|
|
def.e1 = ent->handle;
|
|
def.correction_rate = 0;
|
|
def.max_force = ent->control_force;
|
|
def.max_torque = 0;
|
|
joint_ent->motor_joint_data = motor_joint_from_def(def);
|
|
}
|
|
|
|
entity_set_xform(joint_ent, XFORM_IDENT); /* Reset joint ent position */
|
|
joint_ent->linear_velocity = v2_mul(v2_clamp_len(ent->control.move, 1), ent->control_force_max_speed);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* ========================== *
|
|
* Create forces from control focus (aim)
|
|
* ========================== */
|
|
|
|
#if GAME_PLAYER_AIM
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_PLAYER_CONTROLLED)) {
|
|
struct xform xf = entity_get_xform(ent);
|
|
struct xform sprite_xf = xform_mul(xf, ent->sprite_local_xform);
|
|
|
|
/* Retrieve / create aim joint */
|
|
struct entity *joint_ent = entity_from_handle(store, ent->aim_joint);
|
|
if (!entity_is_valid_and_active(joint_ent)) {
|
|
joint_ent = entity_alloc(root);
|
|
joint_ent->mass_unscaled = F32_INFINITY;
|
|
joint_ent->inertia_unscaled = F32_INFINITY;
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_PHYSICAL_KINEMATIC); /* Since we'll be setting velocity */
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
ent->aim_joint = joint_ent->handle;
|
|
|
|
struct motor_joint_def def = ZI;
|
|
def.e0 = joint_ent->handle; /* Re-using joint entity as e0 */
|
|
def.e1 = ent->handle;
|
|
def.correction_rate = 0.1;
|
|
def.max_force = 0;
|
|
def.max_torque = ent->control_torque;
|
|
joint_ent->motor_joint_data = motor_joint_from_def(def);
|
|
}
|
|
|
|
/* Solve for final angle using law of sines */
|
|
f32 new_angle;
|
|
{
|
|
struct v2 ent_pos = xf.og;
|
|
struct v2 focus_pos = v2_add(ent_pos, ent->control.focus);
|
|
|
|
struct v2 sprite_hold_pos;
|
|
struct v2 sprite_hold_dir;
|
|
{
|
|
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("attach.wep"), ent->animation_frame);
|
|
sprite_hold_pos = slice.center;
|
|
sprite_hold_dir = slice.dir;
|
|
}
|
|
|
|
struct v2 hold_dir = xform_basis_mul_v2(sprite_xf, sprite_hold_dir);
|
|
struct v2 hold_pos = xform_mul_v2(sprite_xf, sprite_hold_pos);
|
|
if (v2_eq(hold_pos, 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 */
|
|
sprite_hold_pos = v2_add(sprite_hold_pos, V2(0, -1));
|
|
hold_pos = xform_mul_v2(sprite_xf, sprite_hold_pos);
|
|
}
|
|
|
|
f32 forward_hold_angle_offset;
|
|
{
|
|
struct xform xf_unrotated = xform_basis_with_rotation_world(xf, 0);
|
|
struct v2 hold_pos_unrotated = xform_mul_v2(xf_unrotated, xform_mul_v2(ent->sprite_local_xform, sprite_hold_pos));
|
|
forward_hold_angle_offset = v2_angle_from_dirs(V2(0, -1), v2_sub(hold_pos_unrotated, xf_unrotated.og));
|
|
}
|
|
|
|
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);
|
|
|
|
new_angle = math_unwind_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);
|
|
}
|
|
|
|
f32 new_vel = 0;
|
|
if (!F32_IS_NAN(new_angle)) {
|
|
const f32 angle_error_allowed = 0.001;
|
|
struct xform joint_xf = entity_get_xform(joint_ent);
|
|
f32 diff = math_unwind_angle(new_angle - xform_get_rotation(joint_xf));
|
|
if (math_fabs(diff) > angle_error_allowed) {
|
|
new_vel = diff / dt;
|
|
}
|
|
}
|
|
joint_ent->angular_velocity = new_vel;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* ========================== *
|
|
* Create ground friction force (gravity)
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_PHYSICAL_DYNAMIC)) continue;
|
|
|
|
struct entity *joint_ent = entity_from_handle(store, ent->ground_friction_joint);
|
|
|
|
struct motor_joint_def def = ZI;
|
|
def.e0 = root->handle;
|
|
def.e1 = ent->handle;
|
|
def.correction_rate = 0;
|
|
def.max_force = ent->linear_ground_friction;
|
|
def.max_torque = ent->angular_ground_friction;
|
|
if (joint_ent->motor_joint_data.max_force != def.max_force || joint_ent->motor_joint_data.max_torque != def.max_torque) {
|
|
if (!entity_is_valid_and_active(joint_ent)) {
|
|
joint_ent = entity_alloc(root);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_MOTOR_JOINT);
|
|
entity_enable_prop(joint_ent, ENTITY_PROP_ACTIVE);
|
|
joint_ent->motor_joint_data = motor_joint_from_def(def);
|
|
ent->ground_friction_joint = joint_ent->handle;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Physics
|
|
* ========================== */
|
|
|
|
(UNUSED)create_contacts;
|
|
(UNUSED)prepare_contacts;
|
|
(UNUSED)warm_start_contacts;
|
|
(UNUSED)solve_contacts;
|
|
|
|
(UNUSED)prepare_motor_joints;
|
|
(UNUSED)warm_start_motor_joints;
|
|
(UNUSED)solve_motor_joints;
|
|
|
|
(UNUSED)create_mouse_joints;
|
|
(UNUSED)prepare_mouse_joints;
|
|
(UNUSED)warm_start_mouse_joints;
|
|
(UNUSED)solve_mouse_joints;
|
|
|
|
(UNUSED)integrate_velocities_from_forces;
|
|
(UNUSED)integrate_positions_from_velocities;
|
|
#if 1
|
|
{
|
|
f32 remaining_dt = dt;
|
|
while (remaining_dt > 0) {
|
|
f32 min_toi = 0.000001f;
|
|
f32 tolerance = 0.001f;
|
|
f32 earliest_toi = max_f32(determine_earliest_toi(remaining_dt, tolerance), min_toi);
|
|
f32 step_dt = remaining_dt * earliest_toi;
|
|
|
|
integrate_velocities_from_forces(step_dt);
|
|
create_contacts();
|
|
create_mouse_joints(game_cmds);
|
|
|
|
prepare_contacts();
|
|
prepare_motor_joints();
|
|
prepare_mouse_joints();
|
|
|
|
f32 substep_dt = step_dt / GAME_PHYSICS_SUBSTEPS;
|
|
for (u32 i = 0; i < GAME_PHYSICS_SUBSTEPS; ++i) {
|
|
#if GAME_PHYSICS_ENABLE_WARM_STARTING
|
|
warm_start_contacts();
|
|
warm_start_motor_joints();
|
|
warm_start_mouse_joints();
|
|
#endif
|
|
|
|
#if GAME_PHYSICS_ENABLE_COLLISION
|
|
solve_contacts(substep_dt, true);
|
|
#endif
|
|
solve_motor_joints(substep_dt);
|
|
solve_mouse_joints(substep_dt);
|
|
|
|
integrate_positions_from_velocities(substep_dt);
|
|
|
|
#if GAME_PHYSICS_ENABLE_COLLISION && GAME_PHYSICS_ENABLE_RELAXATION
|
|
solve_contacts(substep_dt, false); /* Relaxation */
|
|
#endif
|
|
}
|
|
remaining_dt -= step_dt;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* ========================== *
|
|
* Respond to hit events
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
if (!entity_has_prop(ent, ENTITY_PROP_HIT_EVENT)) continue;
|
|
|
|
struct hit_event *event = &ent->hit_event;
|
|
|
|
struct entity *e0 = entity_from_handle(store, event->e0);
|
|
struct entity *e1 = entity_from_handle(store, event->e1);
|
|
|
|
if (entity_is_valid_and_active(e0) && entity_is_valid_and_active(e1)) {
|
|
/* Bullet hit entity */
|
|
if (entity_has_prop(e0, ENTITY_PROP_BULLET) || entity_has_prop(e1, ENTITY_PROP_BULLET)) {
|
|
struct entity *bullet = entity_has_prop(e0, ENTITY_PROP_BULLET) ? e0 : e1;
|
|
struct entity *target = e0 == bullet ? e1 : e0;
|
|
|
|
(UNUSED)bullet;
|
|
(UNUSED)target;
|
|
entity_enable_prop(bullet, ENTITY_PROP_RELEASE_AT_END_OF_FRAME);
|
|
|
|
/* Create test blood */
|
|
/* TODO: Remove this */
|
|
{
|
|
struct xform xf = XFORM_TRS(.t = event->point);
|
|
struct entity *decal = entity_alloc(root);
|
|
decal->sprite = sprite_tag_from_path(STR("res/graphics/blood.ase"));
|
|
entity_set_xform(decal, xf);
|
|
|
|
entity_enable_prop(decal, ENTITY_PROP_PHYSICAL_KINEMATIC);
|
|
decal->linear_velocity = v2_mul(v2_norm(event->normal), 0.5f);
|
|
decal->angular_velocity = 1 - (((f32)sys_rand_u32() / (f32)U32_MAX) * 2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Initialize bullet kinematics from sources
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
/* FIXME: Apply src entity velocity to bullet velocity */
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_BULLET) && ent->activation_tick == G.tick.tick_id) {
|
|
struct entity *src = entity_from_handle(store, ent->bullet_src);
|
|
struct xform src_xf = entity_get_xform(src);
|
|
|
|
struct v2 pos = xform_mul_v2(src_xf, ent->bullet_src_pos);
|
|
struct v2 impulse = xform_basis_mul_v2(src_xf, ent->bullet_src_dir);
|
|
impulse = v2_with_len(impulse, ent->bullet_impulse);
|
|
|
|
struct xform xf = XFORM_TRS(.t = pos, .r = v2_angle(impulse) + PI / 2);
|
|
entity_set_xform(ent, xf);
|
|
entity_enable_prop(ent, ENTITY_PROP_PHYSICAL_KINEMATIC);
|
|
|
|
entity_apply_linear_impulse_to_center(ent, impulse);
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Update camera position
|
|
* ========================== */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
/* 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);
|
|
|
|
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_is_zero(camera_size)) {
|
|
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.tick.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
|
|
* ========================== */
|
|
|
|
/* TODO: Sound entities should be created by game thread, but played by the
|
|
* user thread. This is so sounds play at the correct time on the user
|
|
* thread regardless of interp delay. */
|
|
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!entity_is_valid_and_active(ent)) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_TEST_SOUND_EMITTER)) {
|
|
struct mixer_desc desc = ent->sound_desc;
|
|
desc.speed = G.tick.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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ========================== *
|
|
* Release entities
|
|
* ========================== */
|
|
|
|
/* TODO: Breadth first iteration to only release parent entities (since
|
|
* child entities will be released along with parent anyway) */
|
|
|
|
{
|
|
struct temp_arena temp = arena_temp_begin(scratch.arena);
|
|
|
|
struct entity **ents_to_release = arena_dry_push(temp.arena, struct entity *);
|
|
u64 ents_to_release_count = 0;
|
|
for (u64 entity_index = 0; entity_index < store->reserved; ++entity_index) {
|
|
struct entity *ent = &store->entities[entity_index];
|
|
if (!ent->valid) continue;
|
|
|
|
if (entity_has_prop(ent, ENTITY_PROP_RELEASE_AT_END_OF_FRAME)) {
|
|
*arena_push(temp.arena, struct entity *) = ent;
|
|
++ents_to_release_count;
|
|
}
|
|
}
|
|
|
|
for (u64 i = 0; i < ents_to_release_count; ++i) {
|
|
struct entity *ent = ents_to_release[i];
|
|
if (ent->valid) {
|
|
/* Release */
|
|
entity_release(store, ent);
|
|
}
|
|
}
|
|
|
|
arena_temp_end(temp);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Publish tick
|
|
* ========================== */
|
|
|
|
/* Publish tick */
|
|
publish_game_tick();
|
|
__profframe("Game");
|
|
|
|
/* ========================== *
|
|
* 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)
|
|
{
|
|
struct temp_arena scratch = scratch_begin_no_conflict();
|
|
|
|
(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);
|
|
struct temp_arena temp = arena_temp_begin(scratch.arena);
|
|
sleep_frame(last_frame_ts, target_dt);
|
|
last_frame_ts = sys_timestamp();
|
|
{
|
|
struct game_cmd_array game_cmds = pop_cmds(temp.arena);
|
|
if (!G.paused) {
|
|
game_update(game_cmds);
|
|
}
|
|
/* Check for pause / next frame cmds */
|
|
for (u64 i = 0; i < game_cmds.count; ++i) {
|
|
struct game_cmd cmd = game_cmds.cmds[i];
|
|
switch (cmd.kind) {
|
|
case GAME_CMD_KIND_PAUSE: {
|
|
G.paused = !G.paused;
|
|
} break;
|
|
|
|
case GAME_CMD_KIND_STEP: {
|
|
if (G.paused) {
|
|
G.paused = false;
|
|
game_update(game_cmds);
|
|
G.paused = true;
|
|
}
|
|
} break;
|
|
|
|
default: break;
|
|
}
|
|
}
|
|
}
|
|
arena_temp_end(temp);
|
|
}
|
|
|
|
scratch_end(scratch);
|
|
}
|
|
|
|
/* ========================== *
|
|
* Interface
|
|
* ========================== */
|
|
|
|
void game_get_latest_tick(struct world *dest)
|
|
{
|
|
__prof;
|
|
struct sys_lock lock = sys_mutex_lock_s(&G.prev_tick_mutex);
|
|
world_copy_replace(dest, &G.prev_tick);
|
|
sys_mutex_unlock(&lock);
|
|
}
|
|
|
|
u64 game_get_latest_tick_id(void)
|
|
{
|
|
return atomic_u64_eval(&G.prev_tick_id);
|
|
}
|
|
|
|
u64 game_get_latest_tick_continuity_gen(void)
|
|
{
|
|
return atomic_u64_eval(&G.prev_tick_continuity_gen);
|
|
}
|
|
|
|
void game_push_cmds(struct game_cmd_array cmd_array)
|
|
{
|
|
push_cmds(cmd_array);
|
|
}
|