From db7ce69b0e80d6081e2b5426c9ea43160c9ab8d5 Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Sat, 3 Dec 2022 22:15:07 -0800 Subject: [PATCH] Big netcode refactor to fixed timestep --- buildsettings.h | 1 + flight.rdbg | Bin 1226 -> 1352 bytes gamestate.c | 246 ++++++++++++++++++++++++------------------------ main.c | 224 ++++++++++++++++++++++++++----------------- server.c | 2 +- types.h | 3 +- 6 files changed, 263 insertions(+), 213 deletions(-) diff --git a/buildsettings.h b/buildsettings.h index 45b1aba..69b6c3f 100644 --- a/buildsettings.h +++ b/buildsettings.h @@ -15,6 +15,7 @@ #define UNLOCK_ALL #define INFINITE_RESOURCES #define NO_GRAVITY +#define NO_SUNS #else diff --git a/flight.rdbg b/flight.rdbg index b9fccbd8cbd6366dd562ee2f6db6c7b24de62edf..9ef6c9d29824a413f2deb34f4011647faa69ad8c 100644 GIT binary patch delta 378 zcmYk0y-ve06ot7>kn$rCK?2mN3uQnw3j+)+Ec^@%-4Vuh+}dhVr;aPysjon|8_&W6 zAfAFpfH%NxLt0Cg?|05UNB;6ZgV!;15uB6qJHRLB_|56SWyOA>hAJh4{uIl`Ib2k7 zSjl-T{x|WV5#kS=!R@*#+$%JDWcju3NyBx_I`|81e1{P3H@1*L1gA@@1s+_jtY$gB z+kvgsS-f?4-m>IX*C;~sJe}=Z);!ldk<3UnB3x^w1FOCWt~rr}W(ON&gqjS5Au=I7 zzHJTKw_l9xU}VOLt*zURtSC55jhKmzIy{6cHJ1C O-3aHdUsDr5+}%HTVQ~Eb delta 283 zcmX@Xb&7LBA*0LWL`H+j8<+&yo$^yE^^$GCd~IfaMlB#qm9sdts4TTeFWD2SPKHqz zES{d2n_66wSdu!Kk;#`)10=?to0yrWmuvxKo3SPq6zFA4PGGc#sOL#ZO)5=KDM~Dd zC@^PDtI*5P1S(Zyt1L(@*2|b!7|P4Y00AHxY)vTK8ogwBpdbqn3jlF?v94WiVnuvv zUP)$2W@@o8R4lo)C_XQ>q68vQ$qf`v&d*DUFD@y{%u5I925~1Rv$!#`Oy0<%KY0NQ TD=P<37{oIM@>nL@v2pis_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 diff --git a/main.c b/main.c index 6b60cc4..384f1ff 100644 --- a/main.c +++ b/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)) > diff --git a/server.c b/server.c index 6a8bfb7..09f46b3 100644 --- a/server.c +++ b/server.c @@ -325,7 +325,7 @@ void server(void *info_raw) } } - process(&gs, TIMESTEP, false); + process(&gs, TIMESTEP); total_time -= TIMESTEP; } } diff --git a/types.h b/types.h index d244935..c39c922 100644 --- a/types.h +++ b/types.h @@ -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);