Replaced the previous correction models with a full "Reconciliation by Replaying Inputs system".
446 lines
13 KiB
C++
446 lines
13 KiB
C++
#include <GL/glew.h>
|
|
#include <SDL3/SDL_error.h>
|
|
#include <algorithm>
|
|
/* FINE LSP!! I'll play your games!!!! */
|
|
#include <SDL3/SDL_events.h> /* ~HJAPPY?!?!?! */
|
|
#include <SDL3/SDL_stdinc.h>
|
|
#include <SDL3/SDL_timer.h>
|
|
#include <SDL3/SDL_video.h>
|
|
#include <arpa/inet.h>
|
|
#include <fcntl.h>
|
|
#include <netinet/in.h>
|
|
#include <cstdlib>
|
|
#include <set>
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
#include "bettola.h"
|
|
#include "bettola/network/net_common.h"
|
|
#include "game/remote_player.h"
|
|
#include "math/mat4.h"
|
|
#include "network/message.h"
|
|
#include "network/game_state_message.h"
|
|
#include "network/player_input_message.h"
|
|
|
|
/* Dacav's resolution ;) */
|
|
const int SCREEN_WIDTH = 800;
|
|
const int SCREEN_HEIGHT = 600;
|
|
|
|
Bettola::Bettola(void) :
|
|
_is_running(false),
|
|
_window(nullptr),
|
|
_gl_context(nullptr),
|
|
_vao(0),
|
|
_vbo(0),
|
|
_our_player_id(0),
|
|
_input_sequence_number(0) {
|
|
|
|
memset(&_server_addr, 0, sizeof(_server_addr));
|
|
}
|
|
|
|
Bettola::~Bettola(void) {
|
|
if(_gl_context) {
|
|
SDL_GL_DestroyContext(_gl_context);
|
|
}
|
|
|
|
/* Explicitly close client socket. */
|
|
_tcp_socket.close();
|
|
_udp_socket.close();
|
|
|
|
if(_window) {
|
|
SDL_DestroyWindow(_window);
|
|
}
|
|
|
|
SDL_Quit();
|
|
|
|
if(_gl_context) {
|
|
/* Should only be deleted if it was successfully created
|
|
* and if _gl_context is valid.
|
|
*/
|
|
glDeleteVertexArrays(1, &_vao);
|
|
glDeleteBuffers(1, &_vbo);
|
|
}
|
|
}
|
|
|
|
int Bettola::run(void) {
|
|
if(!init_sdl()) return -1;
|
|
if(!create_window()) return -1;
|
|
if(!create_gl_context()) return -1;
|
|
if(!init_client_connection()) return -1;
|
|
if(!init_glew()) return -1;
|
|
|
|
if(!_shader.load_from_files("assets/shaders/simple.vert",
|
|
"assets/shaders/simple.frag")) {
|
|
return -1;
|
|
}
|
|
|
|
_projection = BettolaMath::Mat4::orthographic(0.0f, 800.0f, 600.0f, 0.0f, -1.0f, 1.0f);
|
|
_shader.use();
|
|
unsigned int proj_loc = glGetUniformLocation(_shader.get_id(), "projection");
|
|
glUniformMatrix4fv(proj_loc, 1, GL_FALSE, _projection.get_ptr());
|
|
|
|
setup_quad();
|
|
|
|
glViewport(0,0,SCREEN_WIDTH, SCREEN_HEIGHT);
|
|
|
|
_is_running = true;
|
|
|
|
Uint64 perf_freq = SDL_GetPerformanceFrequency();
|
|
Uint64 last_count = SDL_GetPerformanceCounter();
|
|
double delta_time = 0;
|
|
|
|
while(_is_running) {
|
|
Uint64 current_counter = SDL_GetPerformanceCounter();
|
|
delta_time = (double)(current_counter-last_count) / (double)perf_freq;
|
|
last_count = current_counter;
|
|
|
|
process_events();
|
|
process_network();
|
|
update(delta_time);
|
|
render();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void Bettola::process_events(void) {
|
|
SDL_Event event;
|
|
while(SDL_PollEvent(&event)) {
|
|
if(event.type == SDL_EVENT_QUIT) {
|
|
_is_running = false;
|
|
} else if(event.type == SDL_EVENT_KEY_DOWN) {
|
|
switch(event.key.key) {
|
|
case SDLK_W: _input.up = true; break;
|
|
case SDLK_S: _input.down = true; break;
|
|
case SDLK_A: _input.left = true; break;
|
|
case SDLK_D: _input.right = true; break;
|
|
}
|
|
} else if(event.type == SDL_EVENT_KEY_UP) {
|
|
switch(event.key.key) {
|
|
case SDLK_W: _input.up = false; break;
|
|
case SDLK_S: _input.down = false; break;
|
|
case SDLK_A: _input.left = false; break;
|
|
case SDLK_D: _input.right = false; break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bettola::process_network(void) {
|
|
fd_set read_fds;
|
|
FD_ZERO(&read_fds);
|
|
|
|
int tcp_fd = _tcp_socket.get_sockfd();
|
|
int udp_fd = _udp_socket.get_sockfd();
|
|
|
|
FD_SET(tcp_fd, &read_fds);
|
|
FD_SET(udp_fd, &read_fds);
|
|
|
|
int max_fd = std::max(tcp_fd, udp_fd);
|
|
|
|
struct timeval tv;
|
|
tv.tv_sec = 0;
|
|
tv.tv_usec = 0; /* Non-blocking. */
|
|
|
|
int activity = select(max_fd+1, &read_fds, nullptr, nullptr, &tv);
|
|
|
|
if(activity > 0) {
|
|
if(FD_ISSET(tcp_fd, &read_fds)) {
|
|
/* We only expect player ID assignment for now. */
|
|
if(_our_player_id == 0) {
|
|
BettolaLib::Network::MessageHeader id_header;
|
|
ssize_t id_bytes_received = _tcp_socket.recv(&id_header, sizeof(id_header));
|
|
if(id_bytes_received > 0 && id_header.type == BettolaLib::Network::MessageType::PlayerId) {
|
|
_tcp_socket.recv(&_our_player_id, sizeof(_our_player_id));
|
|
printf("Bettola: Assigned player ID: %u\n", _our_player_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(FD_ISSET(udp_fd, &read_fds)) {
|
|
process_udp_messages();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bettola::process_udp_messages(void) {
|
|
char buffer[1024];
|
|
sockaddr_in from_addr;
|
|
ssize_t id_bytes_received;
|
|
|
|
while((id_bytes_received = _udp_socket.recv_from(buffer,sizeof(buffer),from_addr))>0) {
|
|
if(id_bytes_received < sizeof(BettolaLib::Network::MessageHeader)) {
|
|
continue;
|
|
}
|
|
|
|
BettolaLib::Network::MessageHeader header;
|
|
memcpy(&header, buffer, sizeof(header));
|
|
memcpy(&header, buffer, sizeof(header));
|
|
|
|
const char* payload = buffer + sizeof(BettolaLib::Network::MessageHeader);
|
|
size_t payload_size = id_bytes_received-sizeof(BettolaLib::Network::MessageHeader);
|
|
|
|
if(header.type == BettolaLib::Network::MessageType::GameState) {
|
|
if(payload_size >= sizeof(BettolaLib::Network::GameStateMessage)) {
|
|
BettolaLib::Network::GameStateMessage msg;
|
|
memcpy(&msg, payload, sizeof(msg));
|
|
process_game_state(msg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bettola::update(double dt) {
|
|
float dir_x = 0.0f;
|
|
float dir_y = 0.0f;
|
|
if(_input.up) dir_y -= 1.0f;
|
|
if(_input.down) dir_y += 1.0f;
|
|
if(_input.left) dir_x -= 1.0f;
|
|
if(_input.right) dir_x += 1.0f;
|
|
_player.set_velocity_direction(dir_x, dir_y);
|
|
|
|
_player.update(dt);
|
|
|
|
for(auto& remote_player : _remote_players) {
|
|
remote_player.update(dt);
|
|
}
|
|
|
|
if(_our_player_id > 0) {
|
|
/* 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::PlayerInput;
|
|
header.size = sizeof(input_msg);
|
|
|
|
char buffer[sizeof(header) + sizeof(input_msg)];
|
|
memcpy(buffer, &header, sizeof(header));
|
|
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];
|
|
static double time_since_title_update = 0.0;
|
|
|
|
time_since_title_update += dt;
|
|
if(time_since_title_update > 0.25) {
|
|
snprintf(window_title, 256, "Bettola - FPS %.2f", 1.0/dt);
|
|
SDL_SetWindowTitle(_window, window_title);
|
|
time_since_title_update = 0.0;
|
|
}
|
|
}
|
|
|
|
void Bettola::render(void) {
|
|
glClearColor(0.1f, 0.1f, 0.3f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
_shader.use();
|
|
|
|
BettolaMath::Mat4 trans_matrix = BettolaMath::Mat4::translation(_player.get_x(),
|
|
_player.get_y(), 0.0f);
|
|
|
|
BettolaMath::Mat4 scale_matrix = BettolaMath::Mat4::scale(_player.get_width(),
|
|
_player.get_height(), 1.0f);
|
|
|
|
BettolaMath::Mat4 model= trans_matrix.multiply(scale_matrix);
|
|
|
|
unsigned int model_loc = glGetUniformLocation(_shader.get_id(), "model");
|
|
glUniformMatrix4fv(model_loc, 1, GL_FALSE, model.get_ptr());
|
|
|
|
glBindVertexArray(_vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
|
glBindVertexArray(0);
|
|
|
|
/* Render remote players. */
|
|
for(const auto& remote_player : _remote_players) {
|
|
BettolaMath::Mat4 remote_trans_matrix =
|
|
BettolaMath::Mat4::translation(remote_player.get_x(),
|
|
remote_player.get_y(),
|
|
0.0f);
|
|
|
|
BettolaMath::Mat4 remote_scale_matrix =
|
|
/* Assuming remote players have same size as local player. */
|
|
BettolaMath::Mat4::scale(50.0f, 50.0f, 1.0f);
|
|
|
|
BettolaMath::Mat4 remote_model = remote_trans_matrix.multiply(remote_scale_matrix);
|
|
|
|
glUniformMatrix4fv(model_loc, 1, GL_FALSE, remote_model.get_ptr());
|
|
|
|
glBindVertexArray(_vao);
|
|
glDrawArrays(GL_TRIANGLES, 0, 6);
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
SDL_GL_SwapWindow(_window);
|
|
}
|
|
|
|
void Bettola::process_game_state(const BettolaLib::Network::GameStateMessage& msg) {
|
|
/* Track remote players each update. */
|
|
std::set<unsigned int> players_in_message;
|
|
|
|
for(unsigned int i = 0; i < msg.num_players; ++i) {
|
|
const auto& player_state = msg.players[i];
|
|
|
|
if(player_state.player_id == _our_player_id) {
|
|
/* This is our player. Reconcile. */
|
|
_player.set_position(player_state.x, 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.. */
|
|
players_in_message.insert(player_state.player_id);
|
|
auto it = std::find_if(_remote_players.begin(), _remote_players.end(),
|
|
[&](const RemotePlayer& p) {
|
|
return p.get_id() == player_state.player_id;
|
|
});
|
|
|
|
if(it != _remote_players.end()) {
|
|
/* Found 'em! update their target pos. */
|
|
it->set_target_position(player_state.x, player_state.y);
|
|
} else {
|
|
/* They are new, add them to our list. */
|
|
_remote_players.emplace_back(player_state.player_id, player_state.x, player_state.y);
|
|
}
|
|
}
|
|
}
|
|
_remote_players.erase(
|
|
std::remove_if(_remote_players.begin(), _remote_players.end(),
|
|
[&](const RemotePlayer& p) {
|
|
return players_in_message.find(p.get_id()) == players_in_message.end();
|
|
}),
|
|
|
|
_remote_players.end());
|
|
}
|
|
|
|
bool Bettola::init_sdl(void) {
|
|
if(!SDL_Init(SDL_INIT_VIDEO)) {
|
|
fprintf(stderr, "Failed to iniit SDL! SDL ERROR: %s\n", SDL_GetError());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Bettola::init_glew(void) {
|
|
glewExperimental = GL_TRUE;
|
|
GLenum glew_error = glewInit();
|
|
if(glew_error != GLEW_OK) {
|
|
fprintf(stderr, "Failed to init GLEW! %s\n", glewGetErrorString(glew_error));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Bettola::create_window(void) {
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
|
|
|
/* Don't you dare f.ckin fail to open!!!!! */
|
|
_window = SDL_CreateWindow("Bettola Makes No Sense!", SCREEN_WIDTH, SCREEN_HEIGHT,
|
|
SDL_WINDOW_OPENGL);
|
|
|
|
if(!_window) {
|
|
fprintf(stderr, "Failed to create window! SDL_ERROR: %s\n", SDL_GetError());
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Bettola::create_gl_context(void) {
|
|
_gl_context = SDL_GL_CreateContext(_window);
|
|
if(!_gl_context) {
|
|
fprintf(stderr, "Failed to create OpenGL context! SDL_ERROR: %s\n", SDL_GetError());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Bettola::setup_quad(void) {
|
|
float vertices[] = {
|
|
/* Should be a quad??!?? */
|
|
-0.5f, -0.5f, 0.0f,
|
|
0.5f, -0.5f, 0.0f,
|
|
0.5f, 0.5f, 0.0f,
|
|
0.5f, 0.5f, 0.0f,
|
|
-0.5f, 0.5f, 0.0f,
|
|
-0.5f, -0.5f, 0.0f,
|
|
};
|
|
|
|
glGenVertexArrays(1, &_vao);
|
|
glGenBuffers(1, &_vbo);
|
|
|
|
glBindVertexArray(_vao);
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, _vbo);
|
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|
|
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
|
|
glEnableVertexAttribArray(0);
|
|
|
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
|
glBindVertexArray(0);
|
|
}
|
|
|
|
bool Bettola::init_client_connection(void) {
|
|
SDL_Delay(1000); /* Wait a second before trying to connect. */
|
|
if(!_tcp_socket.create()) {
|
|
printf("Bettola client: Failed to create socket.\n");
|
|
return false;
|
|
}
|
|
|
|
if(!_tcp_socket.connect("127.0.0.1", BettolaLib::Network::DEFAULT_PORT)) {
|
|
perror("Bettola Client: Failed to connect to server.\n");
|
|
return false;
|
|
}
|
|
|
|
if(!_udp_socket.create()) {
|
|
printf("Bettola Client: Failed to create UDP socket.\n");
|
|
return false;
|
|
}
|
|
|
|
/* Setup server address for UDP. */
|
|
_server_addr.sin_family = AF_INET;
|
|
_server_addr.sin_port = htons(BettolaLib::Network::DEFAULT_PORT);
|
|
inet_pton(AF_INET, "127.0.0.1", &_server_addr.sin_addr);
|
|
|
|
/* Set the socket to non-blocking. */
|
|
int flags = fcntl(_tcp_socket.get_sockfd(), F_GETFL, 0);
|
|
if(flags == -1) {
|
|
perror("fcntl F_GETFL failed.");
|
|
return false;
|
|
}
|
|
fcntl(_tcp_socket.get_sockfd(), F_SETFL, flags | O_NONBLOCK);
|
|
fcntl(_udp_socket.get_sockfd(), F_SETFL, flags | O_NONBLOCK);
|
|
|
|
printf("Bettola Client: Connected to server at 127.0.01.:%hu\n", BettolaLib::Network::DEFAULT_PORT);
|
|
return true;
|
|
}
|
|
|