Big netcode refactor to fixed timestep

main
Cameron Murphy Reikes 2 years ago
parent 720b1f23ed
commit db7ce69b0e

@ -15,6 +15,7 @@
#define UNLOCK_ALL #define UNLOCK_ALL
#define INFINITE_RESOURCES #define INFINITE_RESOURCES
#define NO_GRAVITY #define NO_GRAVITY
#define NO_SUNS
#else #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 // 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 // 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" // ONLY connected horizontally and vertically.
// 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
#define MAX_SEPARATE_GRIDS 8 #define MAX_SEPARATE_GRIDS 8
EntityID separate_grids[MAX_SEPARATE_GRIDS] = {0}; 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 processed_boxes = 0;
int biggest_separate_grid_index = 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 // process all boxes into separate, but correctly connected, grids
while (processed_boxes < num_boxes) while (processed_boxes < num_boxes)
@ -544,6 +541,8 @@ static void grid_correct_for_holes(GameState *gs, struct Entity *grid)
assert(unprocessed->is_box); assert(unprocessed->is_box);
box_remove_from_boxes(gs, unprocessed); // no longer in the boxes list of the grid 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, // flood fill from this unprocessed box, adding each result to cur_separate_grid_index,
// removing each block from the grid // removing each block from the grid
// https://en.wikipedia.org/wiki/Flood_fill // 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++; cur_separate_grid_size++;
processed_boxes++; 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); cpVect cur_local_pos = entity_shape_pos(N);
const cpVect dirs[] = { const cpVect dirs[] = {
(cpVect){ (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; biggest_separate_grid_index = cur_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; cur = next;
} }
// @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)))); cpBodySetVelocity(new_grid->body, cpBodyGetVelocityAtWorldPoint(grid->body, (grid_com(new_grid))));
cpBodySetAngularVelocity(new_grid->body, entity_angular_velocity(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 // can't zero out gs data because the entity memory arena is reused
// on deserialization // 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) if (gs->entities[i].exists)
{
entity_destroy(gs, &gs->entities[i]); entity_destroy(gs, &gs->entities[i]);
gs->entities[i] = (Entity){0};
}
} }
cpSpaceFree(gs->space); cpSpaceFree(gs->space);
gs->space = NULL; 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; gs->cur_next_entity = 0;
} }
// center of mass, not the literal position // center of mass, not the literal position
@ -1325,6 +1332,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
GameState *gs = s->cur_gs; GameState *gs = s->cur_gs;
// completely reset and destroy all gamestate data // completely reset and destroy all gamestate data
PROFILE_SCOPE("Destroy old gamestate")
{
if (!ser->serializing) if (!ser->serializing)
{ {
// avoid a memset here very expensive. que rico! // avoid a memset here very expensive. que rico!
@ -1332,6 +1341,7 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities)); initialize(gs, gs->entities, gs->max_entities * sizeof(*gs->entities));
gs->cur_next_entity = 0; // updated on deserialization gs->cur_next_entity = 0; // updated on deserialization
} }
}
int cur_next_entity = 0; int cur_next_entity = 0;
if (ser->serializing) if (ser->serializing)
@ -1372,6 +1382,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
} }
if (ser->serializing) if (ser->serializing)
{
PROFILE_SCOPE("Serialize entities")
{ {
bool entities_done = false; bool entities_done = false;
for (size_t i = 0; i < gs->cur_next_entity; i++) for (size_t i = 0; i < gs->cur_next_entity; i++)
@ -1426,7 +1438,10 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
entities_done = true; entities_done = true;
SER_VAR(&entities_done); SER_VAR(&entities_done);
} }
}
else else
{
PROFILE_SCOPE("Deserialize entities")
{ {
Entity *last_grid = NULL; Entity *last_grid = NULL;
while (true) while (true)
@ -1462,6 +1477,9 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
last_grid = e; last_grid = e;
} }
} }
PROFILE_SCOPE("Add to free list")
{
for (size_t i = 0; i < gs->cur_next_entity; i++) for (size_t i = 0; i < gs->cur_next_entity; i++)
{ {
Entity *e = &gs->entities[i]; Entity *e = &gs->entities[i];
@ -1474,6 +1492,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
} }
} }
} }
}
}
return ser_ok; return ser_ok;
} }
@ -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->body != NULL);
assert(grid_to_transplant_to->is_grid); 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 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_target = grid_world_to_local(grid_to_transplant_to, from_target);
cpVect local_facing = local_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)); 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") PROFILE_SCOPE("Gameplay processing")
{ {
assert(gs->space != NULL); 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++; gs->tick++;
}
}
else
{
assert(gs->subframe_time == 0.0);
gs->tick++;
}
}
PROFILE_SCOPE("sun gravity") PROFILE_SCOPE("sun gravity")
{ {
@ -2452,7 +2452,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (!e->exists) if (!e->exists)
continue; continue;
PROFILE_SCOPE("instant death ") // PROFILE_SCOPE("instant death")
{ {
cpFloat dist_from_center = cpvlengthsq((entity_pos(e))); 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)) if (e->body != NULL && dist_from_center > (INSTANT_DEATH_DISTANCE_FROM_CENTER * INSTANT_DEATH_DISTANCE_FROM_CENTER))
@ -2485,7 +2485,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
} }
// sun processing for this current entity // sun processing for this current entity
#if 0 #ifndef NO_SUNS
PROFILE_SCOPE("this entity sun processing") PROFILE_SCOPE("this entity sun processing")
{ {
SUNS_ITER(gs) SUNS_ITER(gs)
@ -2557,7 +2557,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (e->is_box) if (e->is_box)
{ {
PROFILE_SCOPE("Box processing") // PROFILE_SCOPE("Box processing")
{ {
if (e->is_platonic) if (e->is_platonic)
{ {
@ -2646,7 +2646,7 @@ void process(struct GameState *gs, double dt, bool is_subframe)
if (e->is_grid) if (e->is_grid)
{ {
PROFILE_SCOPE("Grid processing") // PROFILE_SCOPE("Grid processing")
{ {
Entity *grid = e; Entity *grid = e;
// calculate how much energy solar panels provide // calculate how much energy solar panels provide

174
main.c

@ -58,6 +58,8 @@ static bool fullscreened = false;
static bool picking_new_boxtype = false; static bool picking_new_boxtype = false;
static bool build_pressed = 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; static bool interact_pressed = false;
#define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1) #define MAX_MOUSEBUTTON (SAPP_MOUSEBUTTON_MIDDLE + 1)
static bool mousedown[MAX_MOUSEBUTTON] = {0}; static bool mousedown[MAX_MOUSEBUTTON] = {0};
@ -86,8 +88,6 @@ static ENetPeer *peer;
static double zoom_target = 300.0; static double zoom_target = 300.0;
static double zoom = 300.0; static double zoom = 300.0;
static enum Squad take_over_squad = (enum Squad) - 1; // -1 means not taking over any squad 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 // images
static sg_image image_itemframe; static sg_image image_itemframe;
@ -1330,6 +1330,19 @@ static void ui(bool draw, double dt, double width, double height)
sgp_pop_transform(); 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) static void draw_dots(cpVect camera_pos, double gap)
{ {
set_color_values(1.0, 1.0, 1.0, 1.0); 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) static cpVect get_global_hand_pos(cpVect world_mouse_pos, bool *hand_at_arms_length)
{ {
if (myentity() == NULL) if (myentity() == NULL)
@ -1395,7 +1425,7 @@ static void frame(void)
double width = (float)sapp_width(), height = (float)sapp_height(); double width = (float)sapp_width(), height = (float)sapp_height();
double ratio = width / height; double ratio = width / height;
double exec_time = sapp_frame_count() * sapp_frame_duration(); double exec_time = sapp_frame_count() * sapp_frame_duration();
double dt = (float)sapp_frame_duration(); double dt = sapp_frame_duration();
// pressed input management // pressed input management
{ {
@ -1422,6 +1452,9 @@ static void frame(void)
PROFILE_SCOPE("networking") PROFILE_SCOPE("networking")
{ {
ENetEvent event; 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) while (true)
{ {
int enet_status = enet_host_service(client, &event, 0); int enet_status = enet_host_service(client, &event, 0);
@ -1448,7 +1481,6 @@ static void frame(void)
assert(LZO1X_MEM_DECOMPRESS == 0); assert(LZO1X_MEM_DECOMPRESS == 0);
ma_mutex_lock(&play_packets_mutex); ma_mutex_lock(&play_packets_mutex);
double predicted_to_time = time(&gs);
ServerToClient msg = (ServerToClient){ ServerToClient msg = (ServerToClient){
.cur_gs = &gs, .cur_gs = &gs,
.audio_playback_buffer = &packets_to_play, .audio_playback_buffer = &packets_to_play,
@ -1462,6 +1494,7 @@ static void frame(void)
{ {
server_to_client_deserialize(&msg, decompressed, server_to_client_deserialize(&msg, decompressed,
decompressed_max_len, false); decompressed_max_len, false);
applied_gamestate_packet = true;
} }
my_player_index = msg.your_player; my_player_index = msg.your_player;
} }
@ -1475,83 +1508,83 @@ static void frame(void)
free(decompressed); free(decompressed);
enet_packet_destroy(event.packet); enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
fprintf(stderr, "Disconnected from server\n");
exit(-1);
break;
}
}
}
else if (enet_status == 0)
{
break;
}
else if (enet_status < 0)
{
fprintf(stderr, "Error receiving enet events: %d\n", enet_status);
break;
}
}
// only repredict inputs on the most recent server authoritative packet
PROFILE_SCOPE("Repredicting inputs") PROFILE_SCOPE("Repredicting inputs")
{ {
if (applied_gamestate_packet)
{
uint64_t server_current_tick = tick(&gs);
double server_current_time = time(&gs); uint64_t ticks_should_repredict = predicted_to_tick - server_current_tick;
double difference = predicted_to_time - server_current_time;
double target_prediction_time = uint64_t healthy_num_ticks_ahead = (uint64_t)ceil((((double)peer->roundTripTime) / 1000.0) / TIMESTEP) + 6;
(((double)peer->roundTripTime) / 1000.0) + TIMESTEP * 6.0;
// keeps it stable even though causes jumps occasionally // keeps it stable even though causes jumps occasionally
difference = fmax(difference, target_prediction_time); uint64_t ticks_to_repredict = ticks_should_repredict;
double eps = TIMESTEP * 0.1; if (ticks_should_repredict < healthy_num_ticks_ahead - 1)
if (predicted_to_time - time(&gs) < target_prediction_time - eps)
{ {
target_prediction_time_factor = 1.1; dilating_time_factor = 1.1;
} }
else if (predicted_to_time - time(&gs) > else if (ticks_should_repredict > healthy_num_ticks_ahead + 1)
target_prediction_time + eps * 2.0)
{ {
target_prediction_time_factor = 0.9; dilating_time_factor = 0.1;
} }
else else
{ {
target_prediction_time_factor = 1.0; dilating_time_factor = 1.0;
} }
// re-predict the inputs // snap in dire cases
double time_to_repredict = (float)difference; if (ticks_should_repredict < healthy_num_ticks_ahead - TICKS_BEHIND_DO_SNAP)
Log("Repredicting %f\n", time_to_repredict); {
Log("Snapping\n");
ticks_to_repredict = healthy_num_ticks_ahead;
}
uint64_t start_prediction_time = stm_now(); uint64_t start_prediction_time = stm_now();
if (time_to_repredict > 0.0) while (ticks_to_repredict > 0)
{
while (time_to_repredict > TIMESTEP)
{ {
if (stm_ms(stm_diff(stm_now(), start_prediction_time)) > MAX_MS_SPENT_REPREDICTING) 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); Log("Reprediction took longer than %f milliseconds, needs to repredict %llu more ticks\n", MAX_MS_SPENT_REPREDICTING, ticks_to_repredict);
break; break;
} }
QUEUE_ITER(&input_queue, cur_header) apply_this_tick_of_input_to_player(tick(&gs));
{ process(&gs, TIMESTEP);
InputFrame *cur = (InputFrame *)cur_header->data; ticks_to_repredict -= 1;
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; cpVect where_i_am = my_player_pos();
}
break;
}
case ENET_EVENT_TYPE_DISCONNECT: 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)
{ {
fprintf(stderr, "Disconnected from server\n"); Log("Big reprediction error %llu\n", biggest_frame->tick);
exit(-1);
break;
}
}
}
else if (enet_status == 0)
{
break;
} }
else if (enet_status < 0)
{
fprintf(stderr, "Error receiving enet events: %d\n", enet_status);
break;
} }
} }
} }
@ -1647,6 +1680,14 @@ static void frame(void)
cur_input_frame.build_rotation = cur_editing_rotation; cur_input_frame.build_rotation = cur_editing_rotation;
} }
// in client side prediction, only process the latest input in the queue, not
// the one currently constructing.
time_to_process += dt * dilating_time_factor;
cpVect before = my_player_pos();
do
{
// "commit" the input. each input must be on a successive tick. // "commit" the input. each input must be on a successive tick.
if (tick(&gs) > last_input_committed_tick) if (tick(&gs) > last_input_committed_tick)
{ {
@ -1669,16 +1710,23 @@ static void frame(void)
cur_input_frame; // for the client side prediction! cur_input_frame; // for the client side prediction!
cur_input_frame = (InputFrame){0}; cur_input_frame = (InputFrame){0};
cur_input_frame.take_over_squad = cur_input_frame.take_over_squad = -1; // @Robust make this zero initialized
-1; // @Robust make this zero initialized
} }
// in client side prediction, only process the latest in the queue, not if (time_to_process >= TIMESTEP)
// the one currently constructing. {
static double prediction_time_factor = 1.0; uint64_t tick_to_predict = tick(&gs);
prediction_time_factor = lerp(prediction_time_factor, apply_this_tick_of_input_to_player(tick_to_predict);
target_prediction_time_factor, dt * 3.0); process(&gs, TIMESTEP);
process(&gs, dt * prediction_time_factor, true); 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; static int64_t last_sent_input_time = 0;
if (stm_sec(stm_diff(stm_now(), last_sent_input_time)) > 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; total_time -= TIMESTEP;
} }
} }

@ -76,6 +76,7 @@
#define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS * 0.8f) #define VOIP_DISTANCE_WHEN_CANT_HEAR (VISION_RADIUS * 0.8f)
// multiplayer // 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 MAX_MS_SPENT_REPREDICTING 30.0f
#define TIME_BETWEEN_SEND_GAMESTATE (1.0f / 20.0f) #define TIME_BETWEEN_SEND_GAMESTATE (1.0f / 20.0f)
#define TIME_BETWEEN_INPUT_PACKETS (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); void process_fixed_timestep(GameState *gs);
// if is subframe, doesn't always increment the tick. When enough // if is subframe, doesn't always increment the tick. When enough
// subframe time has been processed, increments the tick // 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 *)); Entity *closest_box_to_point_in_radius(struct GameState *gs, cpVect point, double radius, bool (*filter_func)(Entity *));
uint64_t tick(struct GameState *gs); uint64_t tick(struct GameState *gs);
double time(GameState *gs); double time(GameState *gs);

Loading…
Cancel
Save