[Change] Switched to an input-based reconcilation

Replaced the previous correction models with a full "Reconciliation by
Replaying Inputs system".
This commit is contained in:
Ritchie Cunningham 2025-09-14 04:25:10 +01:00
parent 8d59e79f8a
commit f653c34baf
10 changed files with 97 additions and 69 deletions

View File

@ -4,7 +4,7 @@ namespace BettolaLib {
namespace Network {
enum class MessageType : unsigned char {
PlayerPosition,
PlayerInput,
PlayerState,
GameState,
PlayerId,

View File

@ -5,10 +5,14 @@
namespace BettolaLib {
namespace Network {
struct PlayerPosMessage {
struct PlayerInputMessage {
unsigned int player_id;
float x;
float y;
unsigned int sequence_number;
bool up;
bool down;
bool left;
bool right;
float dt;
};
} /* namespace Network. */

View File

@ -9,6 +9,7 @@ struct PlayerStateMessage {
unsigned int player_id;
float x;
float y;
unsigned int last_processed_sequence;
};
} /* namespace Network. */

View File

@ -20,7 +20,7 @@
#include "math/mat4.h"
#include "network/message.h"
#include "network/game_state_message.h"
#include "network/player_pos_message.h"
#include "network/player_input_message.h"
/* Dacav's resolution ;) */
const int SCREEN_WIDTH = 800;
@ -33,9 +33,7 @@ Bettola::Bettola(void) :
_vao(0),
_vbo(0),
_our_player_id(0),
_needs_correction(false),
_correction_target_x(0.0f),
_correction_target_y(0.0f) {
_input_sequence_number(0) {
memset(&_server_addr, 0, sizeof(_server_addr));
}
@ -203,41 +201,33 @@ void Bettola::update(double dt) {
_player.update(dt);
if(_needs_correction) {
const float interp_speed = 20.0f; /* We'll use a slightly higher speed for self-correction. */
float current_x = _player.get_x();
float current_y = _player.get_y();
_player.set_position(current_x + (_correction_target_x - current_x) * interp_speed * dt,
current_y + (_correction_target_y - current_y) * interp_speed * dt);
/* If we are very close.. Stop correcting. */
if(std::abs(current_x - _correction_target_x) < 0.1f &&
std::abs(current_y - _correction_target_y) < 0.1f) {
_needs_correction = false;
}
}
for(auto& remote_player : _remote_players) {
remote_player.update(dt);
}
if(_our_player_id > 0) {
/* Send player position to server via UDP.. */
/* Sent our current input state to the server. */
BettolaLib::Network::PlayerInputMessage input_msg;
input_msg.player_id = _our_player_id;
input_msg.sequence_number = ++_input_sequence_number;
input_msg.up = _input.up;
input_msg.down = _input.down;
input_msg.left = _input.left;
input_msg.right = _input.right;
input_msg.dt = dt;
BettolaLib::Network::MessageHeader header;
header.type = BettolaLib::Network::MessageType::PlayerPosition;
header.size = sizeof(BettolaLib::Network::PlayerPosMessage);
header.type = BettolaLib::Network::MessageType::PlayerInput;
header.size = sizeof(input_msg);
BettolaLib::Network::PlayerPosMessage msg;
msg.player_id = _our_player_id;
msg.x = _our_player_id;
msg.x = _player.get_x();
msg.y = _player.get_y();
char buffer[sizeof(header) + sizeof(msg)];
char buffer[sizeof(header) + sizeof(input_msg)];
memcpy(buffer, &header, sizeof(header));
memcpy(buffer + sizeof(header), &msg, sizeof(msg));
memcpy(buffer+sizeof(header), &input_msg, sizeof(input_msg));
_udp_socket.send_to(buffer, sizeof(buffer), _server_addr);
/* Store for reconciliation. */
_pending_inputs.push_back(input_msg);
}
static char window_title[256];
@ -303,22 +293,24 @@ void Bettola::process_game_state(const BettolaLib::Network::GameStateMessage& ms
const auto& player_state = msg.players[i];
if(player_state.player_id == _our_player_id) {
/*
* This is our player. Reconcile predicted state with the
* server's authoritative state.
*/
printf(" -> Processing remote player %u at (%.2f, %.2f)\n", player_state.player_id,
player_state.x, player_state.y);
/* This is us! Reconcile our predicted state. */
float dx = _player.get_x() - player_state.x;
float dy = _player.get_y() - player_state.y;
if((dx*dx+dy*dy) > 0.0001f) { /* If distance is not negligibale. */
/* Simple correction, snap to server position. */
//_player.set_position(player_state.x, player_state.y);
/* This is our player. Reconcile. */
_player.set_position(player_state.x, player_state.y);
_needs_correction = true;
_correction_target_x = player_state.x;
_correction_target_y = player_state.y;
/* Remove all inputs from our history that the server has processed. */
while(!_pending_inputs.empty() &&
_pending_inputs.front().sequence_number <= player_state.last_processed_sequence) {
_pending_inputs.pop_front();
}
/* Now, re-apply any remaining inputs that the server hasn't
* seen yet. This'll bring the client up to date with most recent inputs.
*/
for(const auto& input : _pending_inputs) {
float dir_x = (input.right ? 1.0f : 0.0f) - (input.left ? 1.0f : 0.0f);
float dir_y = (input.down ? 1.0f : 0.0f) - (input.up ? 1.0f : 0.0f);
_player.set_velocity_direction(dir_x, dir_y);
_player.update(input.dt);
}
} else {
/* Remote player. Find if we already know about them.. */

View File

@ -1,6 +1,7 @@
#pragma once
#include <netinet/in.h>
#include <deque>
#include <SDL3/SDL.h>
#include <vector>
@ -9,6 +10,7 @@
#include "game/remote_player.h"
#include "math/mat4.h"
#include "network/tcpsocket.h"
#include "network/player_input_message.h"
#include "network/udpsocket.h"
namespace BettolaLib { namespace Network { struct GameStateMessage; } }
@ -61,7 +63,6 @@ private:
BettolaLib::Network::UDPSocket _udp_socket;
sockaddr_in _server_addr;
bool _needs_correction;
float _correction_target_x;
float _correction_target_y;
unsigned int _input_sequence_number;
std::deque<BettolaLib::Network::PlayerInputMessage> _pending_inputs;
};

View File

@ -4,7 +4,7 @@
#include "game.h"
#include "network/game_state_message.h"
#include "network/player_pos_message.h"
#include "network/player_input_message.h"
#include "network/tcpsocket.h"
#include "network/message.h"
#include "network/udpsocket.h"
@ -27,12 +27,18 @@ void Game::remove_player(unsigned int player_id) {
_players.end());
}
void Game::handle_udp_message(const BettolaLib::Network::MessageHeader& header,
const char* buffer, const sockaddr_in& from_addr) {
void Game::process_udp_message(const char* buffer, size_t size, const sockaddr_in& from_addr) {
if(size < sizeof(BettolaLib::Network::MessageHeader)) return;
if(header.type == BettolaLib::Network::MessageType::PlayerPosition) {
BettolaLib::Network::PlayerPosMessage msg;
memcpy(&msg, buffer, sizeof(msg));
BettolaLib::Network::MessageHeader header;
memcpy(&header, buffer, sizeof(header));
if(header.type == BettolaLib::Network::MessageType::PlayerInput) {
if(size < sizeof(BettolaLib::Network::MessageHeader)
+ sizeof(BettolaLib::Network::PlayerInputMessage)) return;
BettolaLib::Network::PlayerInputMessage msg;
memcpy(&msg, buffer + sizeof(header), sizeof(msg));
Player* player = get_player_by_id(msg.player_id);
if(player) {
@ -40,7 +46,11 @@ void Game::handle_udp_message(const BettolaLib::Network::MessageHeader& header,
printf("Bettola Server: Associated UDP address for player %u\n", msg.player_id);
player->set_udp_addr(from_addr);
}
player->set_position(msg.x, msg.y);
player->set_last_processed_sequence(msg.sequence_number);
float dir_x = (msg.right ? 1.0f : 0.0f) - (msg.left ? 1.0f : 0.0f);
float dir_y = (msg.down ? 1.0f : 0.0f) - (msg.up ? 1.0f : 0.0f);
player->set_velocity_direction(dir_x, dir_y);
player->update(msg.dt);
}
}
}
@ -54,6 +64,7 @@ void Game::broadcast_game_state(BettolaLib::Network::UDPSocket& udp_socket) {
msg.players[i].player_id = _players[i]->get_id();
msg.players[i].x = _players[i]->get_x();
msg.players[i].y = _players[i]->get_y();
msg.players[i].last_processed_sequence = _players[i]->get_last_processed_sequence();
}
BettolaLib::Network::MessageHeader header;

View File

@ -12,8 +12,7 @@ public:
Player* add_player(BettolaLib::Network::TCPSocket* socket);
void remove_player(unsigned int player_id);
void handle_udp_message(const BettolaLib::Network::MessageHeader& header,
const char* buffer, const sockaddr_in& from_addr);
void process_udp_message(const char* buffer, size_t size, const sockaddr_in& from_addr);
void broadcast_game_state(BettolaLib::Network::UDPSocket& udp_socket);
Player* get_player_by_socket(const BettolaLib::Network::TCPSocket* socket);

View File

@ -1,4 +1,5 @@
#include <string.h>
#include <math.h>
#include "player.h"
#include "network/tcpsocket.h"
@ -14,7 +15,25 @@ Player::Player(BettolaLib::Network::TCPSocket* socket) :
_next_player_id = 1;
}
_id = _next_player_id++;
_x = 0.0f; _y = 0.0f;
_x = 0.0f; _y = 0.0f;
_vx = 0.0f; _vy = 0.0f;
_speed = 200.0f; /* Must match client! */
_last_processed_sequence = 0;
memset(&_udp_addr, 0, sizeof(_udp_addr));
}
void Player::update(double dt) {
_x += _vx * dt;
_y += _vy * dt;
}
void Player::set_velocity_direction(float dir_x, float dir_y) {
if(dir_x == 0.0f && dir_y == 0.0f) {
_vx = _vy = 0.0f;
} else {
float mag = sqrt(dir_x * dir_x + dir_y * dir_y);
_vx = (dir_x / mag) * _speed;
_vy = (dir_y / mag) * _speed;
}
}

View File

@ -8,6 +8,8 @@ class Player {
public:
Player(BettolaLib::Network::TCPSocket* socket);
void update(double dt);
void set_velocity_direction(float dir_x, float dir_y);
void set_position(float x, float y) { _x = x; _y = y; }
void set_udp_addr(const sockaddr_in& addr) { _udp_addr = addr; _has_udp_addr = true; }
@ -18,6 +20,8 @@ public:
BettolaLib::Network::TCPSocket& get_socket(void) const { return *_socket; }
const sockaddr_in& get_udp_addr(void) const { return _udp_addr; }
bool has_udp_addr(void) const { return _has_udp_addr; }
void set_last_processed_sequence(unsigned int sequence) { _last_processed_sequence = sequence; }
unsigned int get_last_processed_sequence(void) const { return _last_processed_sequence; }
private:
static unsigned int _next_player_id;
@ -28,4 +32,7 @@ private:
BettolaLib::Network::TCPSocket* _socket;
sockaddr_in _udp_addr;
bool _has_udp_addr;
unsigned int _last_processed_sequence;
float _vx, _vy;
float _speed;
};

View File

@ -12,7 +12,7 @@
#include "bettola/network/udpsocket.h"
#include "bettola/network/net_common.h"
#include "bettola/network/message.h"
#include "bettola/network/player_pos_message.h"
#include "bettola/network/player_input_message.h"
#include "game/game.h"
int main(void) {
@ -126,13 +126,7 @@ int main(void) {
ssize_t bytes_received;
while((bytes_received = udp_socket.recv_from(buffer, sizeof(buffer), from_addr)) > 0) {
if(bytes_received >= sizeof(BettolaLib::Network::MessageHeader)) {
BettolaLib::Network::MessageHeader header;
memcpy(&header, buffer, sizeof(header));
if(bytes_received >= sizeof(BettolaLib::Network::MessageHeader) + header.size) {
game.handle_udp_message(header, buffer+sizeof(BettolaLib::Network::MessageHeader), from_addr);
}
}
game.process_udp_message(buffer, bytes_received, from_addr);
}
}