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 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;
}
// @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));
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,6 +1332,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
GameState *gs = s->cur_gs;
// completely reset and destroy all gamestate data
PROFILE_SCOPE("Destroy old gamestate")
{
if (!ser->serializing)
{
// 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));
gs->cur_next_entity = 0; // updated on deserialization
}
}
int cur_next_entity = 0;
if (ser->serializing)
@ -1372,6 +1382,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
}
if (ser->serializing)
{
PROFILE_SCOPE("Serialize entities")
{
bool entities_done = false;
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;
SER_VAR(&entities_done);
}
}
else
{
PROFILE_SCOPE("Deserialize entities")
{
Entity *last_grid = NULL;
while (true)
@ -1462,6 +1477,9 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
last_grid = e;
}
}
PROFILE_SCOPE("Add to free list")
{
for (size_t i = 0; i < gs->cur_next_entity; i++)
{
Entity *e = &gs->entities[i];
@ -1474,6 +1492,8 @@ SerMaybeFailure ser_server_to_client(SerState *ser, ServerToClient *s)
}
}
}
}
}
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->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++;
}
}
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

174
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,83 +1508,83 @@ static void frame(void)
free(decompressed);
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")
{
if (applied_gamestate_packet)
{
uint64_t server_current_tick = tick(&gs);
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;
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
difference = fmax(difference, target_prediction_time);
uint64_t ticks_to_repredict = ticks_should_repredict;
double eps = TIMESTEP * 0.1;
if (predicted_to_time - time(&gs) < target_prediction_time - eps)
if (ticks_should_repredict < healthy_num_ticks_ahead - 1)
{
target_prediction_time_factor = 1.1;
dilating_time_factor = 1.1;
}
else if (predicted_to_time - time(&gs) >
target_prediction_time + eps * 2.0)
else if (ticks_should_repredict > healthy_num_ticks_ahead + 1)
{
target_prediction_time_factor = 0.9;
dilating_time_factor = 0.1;
}
else
{
target_prediction_time_factor = 1.0;
dilating_time_factor = 1.0;
}
// re-predict the inputs
double time_to_repredict = (float)difference;
Log("Repredicting %f\n", time_to_repredict);
// 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();
if (time_to_repredict > 0.0)
{
while (time_to_repredict > TIMESTEP)
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, 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;
}
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;
apply_this_tick_of_input_to_player(tick(&gs));
process(&gs, TIMESTEP);
ticks_to_repredict -= 1;
}
current_time_ahead_of_server = time(&gs) - server_current_time;
}
break;
}
cpVect where_i_am = my_player_pos();
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");
exit(-1);
break;
}
}
}
else if (enet_status == 0)
{
break;
Log("Big reprediction error %llu\n", biggest_frame->tick);
}
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;
}
// 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.
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 = (InputFrame){0};
cur_input_frame.take_over_squad =
-1; // @Robust make this zero initialized
cur_input_frame.take_over_squad = -1; // @Robust make this zero initialized
}
// 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);
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