Big netcode refactor to fixed timestep

main
parent 720b1f23ed
commit db7ce69b0e

@ -15,6 +15,7 @@
#define UNLOCK_ALL
#define INFINITE_RESOURCES
#define NO_GRAVITY
#define NO_SUNS
#else

Binary file not shown.

@ -521,10 +521,7 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
// could be a gap between boxes in the grid, separate into multiple grids
// goal: create list of "real grids" from this grid that have boxes which are
// ONLY connected horizontally and vertically. whichever one of these "real grids"
// has the most blocks stays the current grid, so
// if a player is inhabiting this ship it stays that ship.
// The other "real grids" are allocated as new grids
// ONLY connected horizontally and vertically.
#define MAX_SEPARATE_GRIDS 8
EntityID separate_grids[MAX_SEPARATE_GRIDS] = {0};
@ -533,7 +530,7 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
int processed_boxes = 0;
int biggest_separate_grid_index = 0;
int biggest_separate_grid_length = 0;
uint32_t biggest_separate_grid_length = 0;
// process all boxes into separate, but correctly connected, grids
while (processed_boxes < num_boxes)
@ -544,6 +541,8 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
assert(unprocessed->is_box);
box_remove_from_boxes(gs, unprocessed); // no longer in the boxes list of the grid
uint32_t biggest_box_index = 0;
// flood fill from this unprocessed box, adding each result to cur_separate_grid_index,
// removing each block from the grid
// https://en.wikipedia.org/wiki/Flood_fill
@ -565,6 +564,11 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
cur_separate_grid_size++;
processed_boxes++;
if (get_id(gs, N).index > biggest_box_index)
{
biggest_box_index = get_id(gs, N).index;
}
cpVect cur_local_pos = entity_shape_pos(N);
const cpVect dirs[] = {
(cpVect){
@ -619,9 +623,9 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
}
}
if (cur_separate_grid_size > biggest_separate_grid_length)
if (biggest_box_index > biggest_separate_grid_length)
{
biggest_separate_grid_length = cur_separate_grid_size;
biggest_separate_grid_length = biggest_box_index;
biggest_separate_grid_index = cur_separate_grid_index;
}
cur_separate_grid_index++;
@ -659,8 +663,14 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
cur = next;
}
cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, (grid_com(new_grid))));
cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(grid));
// @Robust do the momentum stuff properly here so no matter which grid stays as the current grid,
// the *SAME RESULT* happens. VERY IMPORTANT for client side prediction to match what the server says.
// Tried to use something consistent on the server and client like current entity index but DID NOT WORK
if (sepgrid_i != biggest_separate_grid_index)
{
cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, (grid_com(new_grid))));
cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(grid) / fmax(1.0, cpvdist(entity_pos(new_grid), entity_pos(grid))));
}
}
}
@ -746,19 +756,16 @@ void destroy(GameState *gs)
{
// can't zero out gs data because the entity memory arena is reused
// on deserialization
for (size_t i = 0; i < gs->max_entities; i++)
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
if (gs->entities[i].exists)
{
entity_destroy(gs, &gs->entities[i]);
gs->entities[i] = (Entity){0};
}
}
cpSpaceFree(gs->space);
gs->space = NULL;
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
if (gs->entities[i].exists)
gs->entities[i] = (Entity){0};
}
gs->cur_next_entity = 0;
}
// center of mass, not the literal position
@ -1325,12 +1332,15 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
GameState *gs = s->cur_gs;
// completely reset and destroy all gamestate data
if (!ser->serializing)
PROFILE_SCOPE("Destroy old gamestate")
{
// avoid a memset here very expensive. que rico!
destroy(gs);
initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities));
gs->cur_next_entity = 0; // updated on deserialization
if (!ser->serializing)
{
// avoid a memset here very expensive. que rico!
destroy(gs);
initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities));
gs->cur_next_entity = 0; // updated on deserialization
}
}
int cur_next_entity = 0;
@ -1373,104 +1383,114 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
if (ser->serializing)
{
bool entities_done = false;
for (size_t i = 0; i < gs->cur_next_entity; i++)
PROFILE_SCOPE("Serialize entities")
{
Entity *e = &gs->entities[i];
bool entities_done = false;
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
Entity *e = &gs->entities[i];
#define DONT_SEND_BECAUSE_CLOAKED(entity) (!ser->save_or_load_from_disk && ser->for_player != NULL && is_cloaked(gs, entity, ser->for_player))
#define SER_ENTITY() \
SER_VAR(&entities_done); \
SER_VAR(&i); \
SER_MAYBE_RETURN(ser_entity(ser, gs, e))
if (e->exists && !(ser->save_or_load_from_disk && e->no_save_to_disk) && !DONT_SEND_BECAUSE_CLOAKED(e))
{
if (!e->is_box && !e->is_grid)
{
SER_ENTITY();
}
if (e->is_grid)
if (e->exists && !(ser->save_or_load_from_disk && e->no_save_to_disk) && !DONT_SEND_BECAUSE_CLOAKED(e))
{
bool serialized_grid_yet = false;
// serialize boxes always after bodies, so that by the time the boxes
// are loaded in the parent body is loaded in and can be referenced.
BOXES_ITER(gs, cur_box, e)
if (!e->is_box && !e->is_grid)
{
bool this_box_in_range = ser->save_or_load_from_disk;
this_box_in_range |= ser->for_player == NULL;
this_box_in_range |= (ser->for_player != NULL && cpvdistsq(entity_pos(ser->for_player), entity_pos(cur_box)) < VISION_RADIUS * VISION_RADIUS); // only in vision radius
if (DONT_SEND_BECAUSE_CLOAKED(cur_box))
this_box_in_range = false;
if (cur_box->always_visible)
this_box_in_range = true;
if (this_box_in_range)
SER_ENTITY();
}
if (e->is_grid)
{
bool serialized_grid_yet = false;
// serialize boxes always after bodies, so that by the time the boxes
// are loaded in the parent body is loaded in and can be referenced.
BOXES_ITER(gs, cur_box, e)
{
if (!serialized_grid_yet)
bool this_box_in_range = ser->save_or_load_from_disk;
this_box_in_range |= ser->for_player == NULL;
this_box_in_range |= (ser->for_player != NULL && cpvdistsq(entity_pos(ser->for_player), entity_pos(cur_box)) < VISION_RADIUS * VISION_RADIUS); // only in vision radius
if (DONT_SEND_BECAUSE_CLOAKED(cur_box))
this_box_in_range = false;
if (cur_box->always_visible)
this_box_in_range = true;
if (this_box_in_range)
{
serialized_grid_yet = true;
SER_ENTITY();
}
if (!serialized_grid_yet)
{
serialized_grid_yet = true;
SER_ENTITY();
}
// serialize this box
EntityID cur_id = get_id(gs, cur_box);
SER_ASSERT(cur_id.index < gs->max_entities);
SER_VAR(&entities_done);
size_t the_index = (size_t)cur_id.index; // super critical. Type of &i is size_t. @Robust add debug info in serialization for what size the expected type is, maybe string nameof the type
SER_VAR_NAME(&the_index, "&i");
SER_MAYBE_RETURN(ser_entity(ser, gs, cur_box));
// serialize this box
EntityID cur_id = get_id(gs, cur_box);
SER_ASSERT(cur_id.index < gs->max_entities);
SER_VAR(&entities_done);
size_t the_index = (size_t)cur_id.index; // super critical. Type of &i is size_t. @Robust add debug info in serialization for what size the expected type is, maybe string nameof the type
SER_VAR_NAME(&the_index, "&i");
SER_MAYBE_RETURN(ser_entity(ser, gs, cur_box));
}
}
}
}
}
#undef SER_ENTITY
}
entities_done = true;
SER_VAR(&entities_done);
}
entities_done = true;
SER_VAR(&entities_done);
}
else
{
Entity *last_grid = NULL;
while (true)
PROFILE_SCOPE("Deserialize entities")
{
bool entities_done = false;
SER_VAR(&entities_done);
if (entities_done)
break;
size_t next_index;
SER_VAR_NAME(&next_index, "&i");
SER_ASSERT(next_index < gs->max_entities);
SER_ASSERT(next_index >= 0);
Entity *e = &gs->entities[next_index];
e->exists = true;
// unsigned int possible_next_index = (unsigned int)(next_index + 2); // plus two because player entity refers to itself on deserialization
unsigned int possible_next_index = (unsigned int)(next_index + 1);
gs->cur_next_entity = gs->cur_next_entity < possible_next_index ? possible_next_index : gs->cur_next_entity;
SER_MAYBE_RETURN(ser_entity(ser, gs, e));
if (e->is_box)
Entity *last_grid = NULL;
while (true)
{
SER_ASSERT(last_grid != NULL);
SER_ASSERT(get_entity(gs, e->shape_parent_entity) != NULL);
SER_ASSERT(last_grid == get_entity(gs, e->shape_parent_entity));
e->prev_box = (EntityID){0};
e->next_box = (EntityID){0};
box_add_to_boxes(gs, last_grid, e);
}
bool entities_done = false;
SER_VAR(&entities_done);
if (entities_done)
break;
size_t next_index;
SER_VAR_NAME(&next_index, "&i");
SER_ASSERT(next_index < gs->max_entities);
SER_ASSERT(next_index >= 0);
Entity *e = &gs->entities[next_index];
e->exists = true;
// unsigned int possible_next_index = (unsigned int)(next_index + 2); // plus two because player entity refers to itself on deserialization
unsigned int possible_next_index = (unsigned int)(next_index + 1);
gs->cur_next_entity = gs->cur_next_entity < possible_next_index ? possible_next_index : gs->cur_next_entity;
SER_MAYBE_RETURN(ser_entity(ser, gs, e));
if (e->is_grid)
{
e->boxes = (EntityID){0};
last_grid = e;
if (e->is_box)
{
SER_ASSERT(last_grid != NULL);
SER_ASSERT(get_entity(gs, e->shape_parent_entity) != NULL);
SER_ASSERT(last_grid == get_entity(gs, e->shape_parent_entity));
e->prev_box = (EntityID){0};
e->next_box = (EntityID){0};
box_add_to_boxes(gs, last_grid, e);
}
if (e->is_grid)
{
e->boxes = (EntityID){0};
last_grid = e;
}
}
}
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
Entity *e = &gs->entities[i];
if (!e->exists)
PROFILE_SCOPE("Add to free list")
{
if (e->generation == 0)
e->generation = 1; // 0 generation reference is invalid, means null
e->next_free_entity = gs->free_list;
gs->free_list = get_id(gs, e);
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
Entity *e = &gs->entities[i];
if (!e->exists)
{
if (e->generation == 0)
e->generation = 1; // 0 generation reference is invalid, means null
e->next_free_entity = gs->free_list;
gs->free_list = get_id(gs, e);
}
}
}
}
}
@ -1768,9 +1788,6 @@ enum CompassRotation facing_vector_to_compass(Entity *grid_to_transplant_to, Ent
assert(grid_to_transplant_to->body != NULL);
assert(grid_to_transplant_to->is_grid);
cpVect local_to_from = grid_world_to_local(grid_facing_vector_from, cpvadd(entity_pos(grid_facing_vector_from), facing_vector));
Log("local %f %f\n", local_to_from.x, local_to_from.y);
cpVect from_target = cpvadd(entity_pos(grid_to_transplant_to), facing_vector);
cpVect local_target = grid_world_to_local(grid_to_transplant_to, from_target);
cpVect local_facing = local_target;
@ -2118,30 +2135,13 @@ void exit_seat(GameState *gs, Entity *seat_in, Entity *p)
cpBodySetVelocity(p->body, cpBodyGetVelocity(box_grid(seat_in)->body));
}
void process(struct GameState *gs, double dt, bool is_subframe)
void process(struct GameState *gs, double dt)
{
PROFILE_SCOPE("Gameplay processing")
{
assert(gs->space != NULL);
PROFILE_SCOPE("subframe stuff")
{
if (is_subframe)
{
gs->subframe_time += dt;
while (gs->subframe_time > TIMESTEP)
{
gs->subframe_time -= TIMESTEP;
gs->tick++;
}
}
else
{
assert(gs->subframe_time == 0.0);
gs->tick++;
}
}
gs->tick++;
PROFILE_SCOPE("sun gravity")
{
@ -2452,7 +2452,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (!e->exists)
continue;
PROFILE_SCOPE("instant death ")
// PROFILE_SCOPE("instant death")
{
cpFloat dist_from_center = cpvlengthsq((entity_pos(e)));
if (e->body != NULL && dist_from_center > (INSTANT_DEATH_DISTANCE_FROM_CENTER * INSTANT_DEATH_DISTANCE_FROM_CENTER))
@ -2484,8 +2484,8 @@ void process(struct GameState *gs, double dt, bool is_subframe)
}
}
// sun processing for this current entity
#if 0
// sun processing for this current entity
#ifndef NO_SUNS
PROFILE_SCOPE("this entity sun processing")
{
SUNS_ITER(gs)
@ -2517,7 +2517,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
}
}
}
#endif
#endif
if (e->is_explosion)
{
@ -2557,7 +2557,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (e->is_box)
{
PROFILE_SCOPE("Box processing")
// PROFILE_SCOPE("Box processing")
{
if (e->is_platonic)
{
@ -2646,7 +2646,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (e->is_grid)
{
PROFILE_SCOPE("Grid processing")
// PROFILE_SCOPE("Grid processing")
{
Entity *grid = e;
// calculate how much energy solar panels provide

224
main.c

@ -58,6 +58,8 @@ static bool fullscreened = false;
static bool picking_new_boxtype = false;
static bool build_pressed = false;
static double dilating_time_factor = 1.0;
static double time_to_process = 0.0;
static bool interact_pressed = false;
#define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1)
static bool mousedown[MAX_MOUSEBUTTON] = {0};
@ -86,8 +88,6 @@ static ENetPeer *peer;
static double zoom_target = 300.0;
static double zoom = 300.0;
static enum Squad take_over_squad = (enum Squad) - 1; // -1 means not taking over any squad
static double target_prediction_time_factor = 1.0;
static double current_time_ahead_of_server = 0.0;
// images
static sg_image image_itemframe;
@ -1330,6 +1330,19 @@ static void ui(bool draw, double dt, double width, double height)
sgp_pop_transform();
}
// returns zero vector if no player
static cpVect my_player_pos()
{
if (myentity() != NULL)
{
return entity_pos(myentity());
}
else
{
return (cpVect){0};
}
}
static void draw_dots(cpVect camera_pos, double gap)
{
set_color_values(1.0, 1.0, 1.0, 1.0);
@ -1368,6 +1381,23 @@ static void draw_dots(cpVect camera_pos, double gap)
}
}
void apply_this_tick_of_input_to_player(uint64_t tick_to_search_for)
{
InputFrame *to_apply = NULL;
QUEUE_ITER(&input_queue, cur_header)
{
InputFrame *cur = (InputFrame *)cur_header->data;
if (cur->tick == tick(&gs))
{
to_apply = cur;
break;
}
}
if (to_apply != NULL && myplayer() != NULL)
{
myplayer()->input = *to_apply;
}
}
static cpVect get_global_hand_pos(cpVect world_mouse_pos, bool *hand_at_arms_length)
{
if (myentity() == NULL)
@ -1395,7 +1425,7 @@ static void frame(void)
double width = (float)sapp_width(), height = (float)sapp_height();
double ratio = width / height;
double exec_time = sapp_frame_count() * sapp_frame_duration();
double dt = (float)sapp_frame_duration();
double dt = sapp_frame_duration();
// pressed input management
{
@ -1422,6 +1452,9 @@ static void frame(void)
PROFILE_SCOPE("networking")
{
ENetEvent event;
uint64_t predicted_to_tick = tick(&gs); // modified on deserialization of game state
cpVect where_i_thought_id_be = my_player_pos();
bool applied_gamestate_packet = false;
while (true)
{
int enet_status = enet_host_service(client, &event, 0);
@ -1448,7 +1481,6 @@ static void frame(void)
assert(LZO1X_MEM_DECOMPRESS == 0);
ma_mutex_lock(&play_packets_mutex);
double predicted_to_time = time(&gs);
ServerToClient msg = (ServerToClient){
.cur_gs = &gs,
.audio_playback_buffer = &packets_to_play,
@ -1462,6 +1494,7 @@ static void frame(void)
{
server_to_client_deserialize(&msg, decompressed,
decompressed_max_len, false);
applied_gamestate_packet = true;
}
my_player_index = msg.your_player;
}
@ -1475,64 +1508,6 @@ static void frame(void)
free(decompressed);
enet_packet_destroy(event.packet);
PROFILE_SCOPE("Repredicting inputs")
{
double server_current_time = time(&gs);
double difference = predicted_to_time - server_current_time;
double target_prediction_time =
(((double)peer->roundTripTime) / 1000.0) + TIMESTEP * 6.0;
// keeps it stable even though causes jumps occasionally
difference = fmax(difference, target_prediction_time);
double eps = TIMESTEP * 0.1;
if (predicted_to_time - time(&gs) < target_prediction_time - eps)
{
target_prediction_time_factor = 1.1;
}
else if (predicted_to_time - time(&gs) >
target_prediction_time + eps * 2.0)
{
target_prediction_time_factor = 0.9;
}
else
{
target_prediction_time_factor = 1.0;
}
// re-predict the inputs
double time_to_repredict = (float)difference;
Log("Repredicting %f\n", time_to_repredict);
uint64_t start_prediction_time = stm_now();
if (time_to_repredict > 0.0)
{
while (time_to_repredict > TIMESTEP)
{
if (stm_ms(stm_diff(stm_now(), start_prediction_time)) > MAX_MS_SPENT_REPREDICTING)
{
Log("Reprediction took longer than %f milliseconds, could only predict %f\n", MAX_MS_SPENT_REPREDICTING, time_to_repredict);
break;
}
QUEUE_ITER(&input_queue, cur_header)
{
InputFrame *cur = (InputFrame *)cur_header->data;
if (cur->tick == tick(&gs))
{
myplayer()->input = *cur;
break;
}
}
process(&gs, TIMESTEP, false);
time_to_repredict -= TIMESTEP;
}
process(&gs, time_to_repredict, true);
time_to_repredict = 0.0;
}
current_time_ahead_of_server = time(&gs) - server_current_time;
}
break;
}
@ -1554,6 +1529,64 @@ static void frame(void)
break;
}
}
// only repredict inputs on the most recent server authoritative packet
PROFILE_SCOPE("Repredicting inputs")
{
if (applied_gamestate_packet)
{
uint64_t server_current_tick = tick(&gs);
uint64_t ticks_should_repredict = predicted_to_tick - server_current_tick;
uint64_t healthy_num_ticks_ahead = (uint64_t)ceil((((double)peer->roundTripTime) / 1000.0) / TIMESTEP) + 6;
// keeps it stable even though causes jumps occasionally
uint64_t ticks_to_repredict = ticks_should_repredict;
if (ticks_should_repredict < healthy_num_ticks_ahead - 1)
{
dilating_time_factor = 1.1;
}
else if (ticks_should_repredict > healthy_num_ticks_ahead + 1)
{
dilating_time_factor = 0.1;
}
else
{
dilating_time_factor = 1.0;
}
// snap in dire cases
if (ticks_should_repredict < healthy_num_ticks_ahead - TICKS_BEHIND_DO_SNAP)
{
Log("Snapping\n");
ticks_to_repredict = healthy_num_ticks_ahead;
}
uint64_t start_prediction_time = stm_now();
while (ticks_to_repredict > 0)
{
if (stm_ms(stm_diff(stm_now(), start_prediction_time)) > MAX_MS_SPENT_REPREDICTING)
{
Log("Reprediction took longer than %f milliseconds, needs to repredict %llu more ticks\n", MAX_MS_SPENT_REPREDICTING, ticks_to_repredict);
break;
}
apply_this_tick_of_input_to_player(tick(&gs));
process(&gs, TIMESTEP);
ticks_to_repredict -= 1;
}
cpVect where_i_am = my_player_pos();
double reprediction_error = cpvdist(where_i_am, where_i_thought_id_be);
InputFrame *biggest_frame = (InputFrame *)queue_most_recent_element(&input_queue);
if (reprediction_error >= 0.1)
{
Log("Big reprediction error %llu\n", biggest_frame->tick);
}
}
}
}
// gameplay
@ -1647,38 +1680,53 @@ static void frame(void)
cur_input_frame.build_rotation = cur_editing_rotation;
}
// "commit" the input. each input must be on a successive tick.
if (tick(&gs) > last_input_committed_tick)
{
cur_input_frame.tick = tick(&gs);
last_input_committed_tick = tick(&gs);
// in client side prediction, only process the latest input in the queue, not
// the one currently constructing.
InputFrame *to_push_to = queue_push_element(&input_queue);
if (to_push_to == NULL)
time_to_process += dt * dilating_time_factor;
cpVect before = my_player_pos();
do
{
// "commit" the input. each input must be on a successive tick.
if (tick(&gs) > last_input_committed_tick)
{
InputFrame *to_discard = queue_pop_element(&input_queue);
(void)to_discard;
to_push_to = queue_push_element(&input_queue);
assert(to_push_to != NULL);
}
cur_input_frame.tick = tick(&gs);
last_input_committed_tick = tick(&gs);
*to_push_to = cur_input_frame;
InputFrame *to_push_to = queue_push_element(&input_queue);
if (to_push_to == NULL)
{
InputFrame *to_discard = queue_pop_element(&input_queue);
(void)to_discard;
to_push_to = queue_push_element(&input_queue);
assert(to_push_to != NULL);
}
if (myplayer() != NULL)
myplayer()->input =
cur_input_frame; // for the client side prediction!
*to_push_to = cur_input_frame;
cur_input_frame = (InputFrame){0};
cur_input_frame.take_over_squad =
-1; // @Robust make this zero initialized
}
if (myplayer() != NULL)
myplayer()->input =
cur_input_frame; // for the client side prediction!
// in client side prediction, only process the latest in the queue, not
// the one currently constructing.
static double prediction_time_factor = 1.0;
prediction_time_factor = lerp(prediction_time_factor,
target_prediction_time_factor, dt * 3.0);
process(&gs, dt * prediction_time_factor, true);
cur_input_frame = (InputFrame){0};
cur_input_frame.take_over_squad = -1; // @Robust make this zero initialized
}
if (time_to_process >= TIMESTEP)
{
uint64_t tick_to_predict = tick(&gs);
apply_this_tick_of_input_to_player(tick_to_predict);
process(&gs, TIMESTEP);
time_to_process -= TIMESTEP;
}
} while (time_to_process >= TIMESTEP);
cpVect after = my_player_pos();
// use theses variables to suss out reprediction errors, enables you to
// breakpoint on when they happen
(void)before;
(void)after;
static int64_t last_sent_input_time = 0;
if (stm_sec(stm_diff(stm_now(), last_sent_input_time)) >

@ -325,7 +325,7 @@ void server(void *info_raw)
}
}
process(&gs, TIMESTEP, false);
process(&gs, TIMESTEP);
total_time -= TIMESTEP;
}
}

@ -76,6 +76,7 @@
#define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS * 0.8f)
// multiplayer
#define TICKS_BEHIND_DO_SNAP 6 // when this many ticks behind, instead of dilating time SNAP to the healthy ticks ahead
#define MAX_MS_SPENT_REPREDICTING 30.0f
#define TIME_BETWEEN_SEND_GAMESTATE (1.0f / 20.0f)
#define TIME_BETWEEN_INPUT_PACKETS (1.0f / 20.0f)
@ -422,7 +423,7 @@ void destroy(struct GameState *gs);
void process_fixed_timestep(GameState *gs);
// if is subframe, doesn't always increment the tick. When enough
// subframe time has been processed, increments the tick
void process(struct GameState *gs, double dt, bool is_subframe); // does in place
void process(struct GameState *gs, double dt); // does in place
Entity *closest_box_to_point_in_radius(struct GameState *gs, cpVect point, double radius, bool (*filter_func)(Entity *));
uint64_t tick(struct GameState *gs);
double time(GameState *gs);

Loading…
Cancel
Save