feat: Implement multiplayer functionality.

- Server can handle multiple clients simultaneously.
- Client can see other players in the game.
- Server broadcasts the game state to all clients.
- Client receives the game state and renders the other players.
- Server assigns a unique ID to each player.
- Client receives its player ID form the server.
- Server handles client disconnections.. Kinda... Server is ignoring
  SIGPIPE signal.
- Server and client signals are non-blocking.
- Moved player objects off the stack and onto the heap.
This commit is contained in:
Ritchie Cunningham 2025-09-13 19:44:35 +01:00
parent 39a06147c8
commit 5944663017
9 changed files with 203 additions and 42 deletions

View File

@ -7,6 +7,7 @@ enum class MessageType : unsigned char {
PlayerPosition, PlayerPosition,
PlayerState, PlayerState,
GameState, GameState,
PlayerId,
}; };
struct MessageHeader { struct MessageHeader {

View File

@ -5,6 +5,7 @@
#include <sys/socket.h> #include <sys/socket.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
#include <cerrno>
#include "bettola/network/socket.h" #include "bettola/network/socket.h"
#include "bettola/network/net_common.h" #include "bettola/network/net_common.h"
@ -34,11 +35,6 @@ bool Socket::create(void) {
perror("fcntl F_GETFL failed."); perror("fcntl F_GETFL failed.");
return false; return false;
} }
if(fcntl(_sockfd, F_SETFL, flags & ~O_NONBLOCK) == -1) {
perror("fcntl F_SETFL O_NONBLOCK failed");
return false;
}
return true; return true;
} }
@ -74,7 +70,8 @@ Socket* Socket::accept(void) {
socklen_t client_len = sizeof(client_addr); socklen_t client_len = sizeof(client_addr);
int client_sockfd = ::accept(_sockfd, (struct sockaddr*)&client_addr, &client_len); int client_sockfd = ::accept(_sockfd, (struct sockaddr*)&client_addr, &client_len);
if(client_sockfd == -1) { if(client_sockfd == -1) {
perror("Socket accept failed."); if(errno != EWOULDBLOCK && errno != EAGAIN)
perror("Socket accept failed.");
return nullptr; return nullptr;
} }
@ -97,7 +94,7 @@ bool Socket::connect(const std::string& ip_address, unsigned short port) {
return false; return false;
} }
if(::connect(_sockfd, (struct sockaddr*)&_address, sizeof(_address)) == -1) { if(::connect(_sockfd, (struct sockaddr*)&_address, sizeof(_address)) < 0) {
fprintf(stderr, "Socket::connect() failed for sockfd %d with errno %d: %s\n", _sockfd, fprintf(stderr, "Socket::connect() failed for sockfd %d with errno %d: %s\n", _sockfd,
errno, strerror(errno)); errno, strerror(errno));
perror("Connection failed."); perror("Connection failed.");
@ -117,7 +114,7 @@ ssize_t Socket::send(const void* buffer, size_t length) {
ssize_t Socket::recv(void* buffer, size_t length) { ssize_t Socket::recv(void* buffer, size_t length) {
ssize_t bytes_received = ::recv(_sockfd, buffer, length, 0); ssize_t bytes_received = ::recv(_sockfd, buffer, length, 0);
if(bytes_received == -1) { if(bytes_received == -1 && errno != EWOULDBLOCK && errno != EAGAIN) {
perror("Receive failed."); perror("Receive failed.");
} }
return bytes_received; return bytes_received;

View File

@ -1,17 +1,22 @@
#include <GL/glew.h> #include <GL/glew.h>
#include <SDL3/SDL_error.h> #include <SDL3/SDL_error.h>
#include <algorithm>
/* FINE LSP!! I'll play your games!!!! */ /* FINE LSP!! I'll play your games!!!! */
#include <SDL3/SDL_events.h> /* ~HJAPPY?!?!?! */ #include <SDL3/SDL_events.h> /* ~HJAPPY?!?!?! */
#include <SDL3/SDL_stdinc.h> #include <SDL3/SDL_stdinc.h>
#include <SDL3/SDL_timer.h> #include <SDL3/SDL_timer.h>
#include <SDL3/SDL_video.h> #include <SDL3/SDL_video.h>
#include <fcntl.h>
#include <cstddef>
#include <cstdio> #include <cstdio>
#include <string> #include <string>
#include "bettola.h" #include "bettola.h"
#include "bettola/network/socket.h" #include "bettola/network/socket.h"
#include "bettola/network/net_common.h" #include "bettola/network/net_common.h"
#include "math/mat4.h"
#include "network/message.h" #include "network/message.h"
#include "network/game_state_message.h"
#include "network/player_pos_message.h" #include "network/player_pos_message.h"
/* Dacav's resolution ;) */ /* Dacav's resolution ;) */
@ -23,7 +28,8 @@ Bettola::Bettola(void) :
_window(nullptr), _window(nullptr),
_gl_context(nullptr), _gl_context(nullptr),
_vao(0), _vao(0),
_vbo(0) {} _vbo(0),
_our_player_id(0) {}
Bettola::~Bettola(void) { Bettola::~Bettola(void) {
if(_gl_context) { if(_gl_context) {
@ -97,6 +103,28 @@ int Bettola::run(void) {
network_time = 0.0; network_time = 0.0;
} }
if(_our_player_id == 0) {
BettolaLib::Network::MessageHeader id_header;
ssize_t id_bytes_received = _client_socket.recv(&id_header, sizeof(id_header));
if(id_bytes_received > 0 && id_header.type == BettolaLib::Network::MessageType::PlayerId) {
_client_socket.recv(&_our_player_id, sizeof(_our_player_id));
}
}
while(true) {
BettolaLib::Network::MessageHeader header;
ssize_t bytes_received = _client_socket.recv(&header, sizeof(header));
if(bytes_received <= 0) {
break;
}
if(header.type == BettolaLib::Network::MessageType::GameState) {
BettolaLib::Network::GameStateMessage msg;
if(_client_socket.recv(&msg, sizeof(msg)) > 0) {
update_remote_players(msg);
}
}
}
process_events(); process_events();
update(delta_time); update(delta_time);
render(); render();
@ -171,9 +199,50 @@ void Bettola::render(void) {
glDrawArrays(GL_TRIANGLES, 0, 6); glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0); 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); SDL_GL_SwapWindow(_window);
} }
void Bettola::update_remote_players(const BettolaLib::Network::GameStateMessage& msg) {
for(unsigned int i = 0; i < msg.num_players; ++i) {
const auto& player_state = msg.players[i];
auto it = std::find_if(_remote_players.begin(), _remote_players.end(),
[player_state](const RemotePlayer& remote_player) {
return remote_player.get_id() == player_state.player_id;
});
if(it != _remote_players.end()) {
it->set_position(player_state.x, player_state.y);
} else {
/* We don't want to add ourselves as a remote player! */
if(player_state.player_id != _our_player_id) {
_remote_players.emplace_back(player_state.player_id, player_state.x,
player_state.y);
}
}
}
}
bool Bettola::init_sdl(void) { bool Bettola::init_sdl(void) {
if(!SDL_Init(SDL_INIT_VIDEO)) { if(!SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "Failed to iniit SDL! SDL ERROR: %s\n", SDL_GetError()); fprintf(stderr, "Failed to iniit SDL! SDL ERROR: %s\n", SDL_GetError());
@ -257,6 +326,14 @@ bool Bettola::init_client_connection(void) {
return false; return false;
} }
/* Set the socket to non-blocking. */
int flags = fcntl(_client_socket.get_sockfd(), F_GETFL, 0);
if(flags == -1) {
perror("fcntl F_GETFL failed.");
return false;
}
fcntl(_client_socket.get_sockfd(), F_SETFL, flags | O_NONBLOCK);
printf("Bettola Client: Connected to server at 127.0.01.:%hu\n", BettolaLib::Network::DEFAULT_PORT); printf("Bettola Client: Connected to server at 127.0.01.:%hu\n", BettolaLib::Network::DEFAULT_PORT);
return true; return true;
} }

View File

@ -1,11 +1,14 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <vector>
#include "graphics/shader.h" #include "graphics/shader.h"
#include "game/player.h" #include "game/player.h"
#include "game/remote_player.h"
#include "math/mat4.h" #include "math/mat4.h"
#include "bettola/network/socket.h" #include "bettola/network/socket.h"
#include "network/game_state_message.h"
class Bettola { class Bettola {
public: public:
@ -18,6 +21,7 @@ private:
void process_events(void); void process_events(void);
void update(double dt); void update(double dt);
void render(void); void render(void);
void update_remote_players(const BettolaLib::Network::GameStateMessage& msg);
bool init_sdl(void); bool init_sdl(void);
bool init_glew(void); bool init_glew(void);
@ -37,6 +41,7 @@ private:
bool _is_running; bool _is_running;
unsigned int _our_player_id;
SDL_Window* _window; SDL_Window* _window;
SDL_GLContext _gl_context; SDL_GLContext _gl_context;
@ -47,5 +52,6 @@ private:
BettolaMath::Mat4 _projection; BettolaMath::Mat4 _projection;
Player _player; Player _player;
InputState _input; InputState _input;
std::vector<RemotePlayer> _remote_players;
BettolaLib::Network::Socket _client_socket; BettolaLib::Network::Socket _client_socket;
}; };

View File

@ -0,0 +1,8 @@
#include "remote_player.h"
RemotePlayer::RemotePlayer(unsigned int id, float x, float y) :
_id(id), _x(x), _y(y) {}
void RemotePlayer::set_position(float x, float y) {
_x = x; _y = y;
}

17
src/game/remote_player.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
class RemotePlayer {
public:
RemotePlayer(unsigned int id, float x, float y);
void set_position(float x, float y);
unsigned int get_id(void) const { return _id; }
float get_x(void) const { return _x; }
float get_y(void) const { return _y; }
private:
unsigned int _id;
float _x;
float _y;
};

View File

@ -6,27 +6,26 @@
#include "network/message.h" #include "network/message.h"
Player* Game::add_player(BettolaLib::Network::Socket* socket) { Player* Game::add_player(BettolaLib::Network::Socket* socket) {
_players.emplace_back(socket); _players.push_back(new Player(socket));
return &_players.back(); return _players.back();
} }
void Game::remove_player(unsigned int player_id) { void Game::remove_player(unsigned int player_id) {
_players.erase( _players.erase(
std::remove_if(_players.begin(), _players.end(), std::remove_if(_players.begin(), _players.end(),
[player_id](const Player& player) { [player_id](const Player* player) {
/* this is a sh.t way to compare players. */ return player->get_id() == player_id;
return player.get_id() == player_id;
}), }),
_players.end()); _players.end());
} }
void Game::update_player_pos(unsigned int player_id, float x, float y) { void Game::update_player_pos(unsigned int player_id, float x, float y) {
auto it = std::find_if(_players.begin(), _players.end(), auto it = std::find_if(_players.begin(), _players.end(),
[player_id](const Player& player) { [player_id](const Player* player) {
return player.get_id() == player_id; return player->get_id() == player_id;
}); });
if(it != _players.end()) { if(it != _players.end()) {
it->set_position(x, y); (*it)->set_position(x, y);
} }
} }
@ -35,9 +34,9 @@ void Game::broadcast_game_state(void) {
msg.num_players = _players.size(); msg.num_players = _players.size();
for(size_t i = 0; i < _players.size(); ++i) { for(size_t i = 0; i < _players.size(); ++i) {
msg.players[i].player_id = _players[i].get_id(); msg.players[i].player_id = _players[i]->get_id();
msg.players[i].x = _players[i].get_x(); msg.players[i].x = _players[i]->get_x();
msg.players[i].y = _players[i].get_y(); msg.players[i].y = _players[i]->get_y();
} }
BettolaLib::Network::MessageHeader header; BettolaLib::Network::MessageHeader header;
@ -45,9 +44,23 @@ void Game::broadcast_game_state(void) {
header.size = sizeof(msg); header.size = sizeof(msg);
for(const auto& player : _players) { for(const auto& player : _players) {
BettolaLib::Network::Socket& socket = player.get_socket(); BettolaLib::Network::Socket& socket = player->get_socket();
socket.send(&header, sizeof(header)); /* Just quick fix the server crash for now. */
socket.send(&msg, sizeof(msg)); if(socket.send(&header, sizeof(header)) <= 0) {
/* TODO: Probably should handle this error by removing player.. */
}
if(socket.send(&msg, sizeof(msg)) <= 0) {
/* TODO: Probably should handle this error by removing player.. */
}
} }
} }
Player* Game::get_player_by_socket(BettolaLib::Network::Socket* socket) {
auto it = std::find_if(_players.begin(), _players.end(),
[socket](const Player* player) {
return &player->get_socket() == socket;
});
return(it != _players.end() ? *it : nullptr);
}

View File

@ -10,7 +10,8 @@ public:
void remove_player(unsigned int player_id); void remove_player(unsigned int player_id);
void update_player_pos(unsigned int player_id, float x, float y); void update_player_pos(unsigned int player_id, float x, float y);
void broadcast_game_state(void); void broadcast_game_state(void);
Player* get_player_by_socket(BettolaLib::Network::Socket* socket);
private: private:
std::vector<Player> _players; std::vector<Player*> _players;
}; };

View File

@ -2,6 +2,9 @@
#include <cstdio> #include <cstdio>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <vector>
#include <fcntl.h>
#include <signal.h>
#include "bettola/network/socket.h" #include "bettola/network/socket.h"
#include "bettola/network/net_common.h" #include "bettola/network/net_common.h"
@ -11,6 +14,9 @@
int main(void) { int main(void) {
Game game; Game game;
std::vector<BettolaLib::Network::Socket*> client_sockets;
signal(SIGPIPE, SIG_IGN);
printf("=== Bettola Server: Starting ===\n"); printf("=== Bettola Server: Starting ===\n");
@ -36,46 +42,81 @@ int main(void) {
printf("Bettola Server: Listening on port %hu...\n", BettolaLib::Network::DEFAULT_PORT); printf("Bettola Server: Listening on port %hu...\n", BettolaLib::Network::DEFAULT_PORT);
/* Set the server socket to non-blocking. */
int flags = fcntl(server_socket.get_sockfd(), F_GETFL, 0);
if(flags == -1) {
perror("fcntl F_GETFL, failed.");
return 1;
}
fcntl(server_socket.get_sockfd(), F_SETFL, flags | O_NONBLOCK);
auto last_time = std::chrono::high_resolution_clock::now(); auto last_time = std::chrono::high_resolution_clock::now();
/* Main server loop. */ /* Main server loop. */
while(true) { while(true) {
auto current_time = std::chrono::high_resolution_clock::now(); /* Accept new connections. */
auto delta_time = std::chrono::duration_cast<std::chrono::duration<double>>
(current_time-last_time).count();
/* TODO:
* Accept new connections.
* This is blocking.. Will fix later.
*/
BettolaLib::Network::Socket* client_socket = server_socket.accept(); BettolaLib::Network::Socket* client_socket = server_socket.accept();
if(client_socket != nullptr) { if(client_socket != nullptr) {
Player* new_player = game.add_player(client_socket); Player* new_player = game.add_player(client_socket);
printf("Bettola Server: Client connected! Player ID: %u\n", new_player->get_id()); printf("Bettola Server: Client connected! Player ID: %u\n", new_player->get_id());
BettolaLib::Network::MessageHeader header;
header.type = BettolaLib::Network::MessageType::PlayerId;
header.size = sizeof(unsigned int);
client_socket->send(&header, sizeof(header));
unsigned int id = new_player->get_id();
client_socket->send(&id, sizeof(id));
/* Set the client socket to non-blocking. */
int client_flags = fcntl(client_socket->get_sockfd(), F_GETFL, 0);
if(client_flags == -1) {
perror("fcntl F_GETFL failed");
return 1;
}
fcntl(client_socket->get_sockfd(), F_SETFL, client_flags | O_NONBLOCK);
client_sockets.push_back(client_socket);
} }
/* TODO: /* Process messages from clients. */
* Process messages from clients. for(auto it = client_sockets.begin(); it != client_sockets.end();) {
* this is also blocking and only handles one client at a time. fix later BettolaLib::Network::Socket* client = *it;
*/ bool client_disconnected = false;
if(client_socket != nullptr) {
while(true) { while(true) {
BettolaLib::Network::MessageHeader header; BettolaLib::Network::MessageHeader header;
ssize_t bytes_received = client_socket->recv(&header, sizeof(header)); /* Non-blocking. */
ssize_t bytes_received = client->recv(&header, sizeof(header));
if(bytes_received <= 0) { if(bytes_received == 0) {
//game.remove_player(new_player->get_id()); client_disconnected = true;
break;
}
if(bytes_received < 0) {
/* No data to read.. */
break; break;
} }
if(header.type == BettolaLib::Network::MessageType::PlayerPosition) { if(header.type == BettolaLib::Network::MessageType::PlayerPosition) {
BettolaLib::Network::PlayerPosMessage msg; BettolaLib::Network::PlayerPosMessage msg;
bytes_received = client_socket->recv(&msg, sizeof(msg)); bytes_received = client->recv(&msg, sizeof(msg));
if(bytes_received > 0) { if(bytes_received > 0) {
//game.update_player_pos(new_player->get_id(), msg.x, msg.y); Player* player = game.get_player_by_socket(client);
if(player) {
game.update_player_pos(player->get_id(), msg.x, msg.y);
}
} }
} }
} }
if(client_disconnected) {
Player* player = game.get_player_by_socket(client);
if(player) game.remove_player(player->get_id());
it = client_sockets.erase(it);
} else {
++it;
}
} }
/* Broadcase game state. */ /* Broadcase game state. */