From 59446630177cb29242423fc5a5f38538d6c70868 Mon Sep 17 00:00:00 2001 From: Ritchie Cunningham Date: Sat, 13 Sep 2025 19:44:35 +0100 Subject: [PATCH] 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. --- libbettola/include/bettola/network/message.h | 1 + libbettola/src/bettola/network/socket.cpp | 13 ++-- src/bettola.cpp | 79 +++++++++++++++++++- src/bettola.h | 6 ++ src/game/remote_player.cpp | 8 ++ src/game/remote_player.h | 17 +++++ srv/game/game.cpp | 41 ++++++---- srv/game/game.h | 3 +- srv/main.cpp | 77 ++++++++++++++----- 9 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 src/game/remote_player.cpp create mode 100644 src/game/remote_player.h diff --git a/libbettola/include/bettola/network/message.h b/libbettola/include/bettola/network/message.h index 32787a6..f07563e 100644 --- a/libbettola/include/bettola/network/message.h +++ b/libbettola/include/bettola/network/message.h @@ -7,6 +7,7 @@ enum class MessageType : unsigned char { PlayerPosition, PlayerState, GameState, + PlayerId, }; struct MessageHeader { diff --git a/libbettola/src/bettola/network/socket.cpp b/libbettola/src/bettola/network/socket.cpp index 40fe666..8f6976d 100644 --- a/libbettola/src/bettola/network/socket.cpp +++ b/libbettola/src/bettola/network/socket.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "bettola/network/socket.h" #include "bettola/network/net_common.h" @@ -34,11 +35,6 @@ bool Socket::create(void) { perror("fcntl F_GETFL failed."); return false; } - if(fcntl(_sockfd, F_SETFL, flags & ~O_NONBLOCK) == -1) { - perror("fcntl F_SETFL O_NONBLOCK failed"); - return false; - } - return true; } @@ -74,7 +70,8 @@ Socket* Socket::accept(void) { socklen_t client_len = sizeof(client_addr); int client_sockfd = ::accept(_sockfd, (struct sockaddr*)&client_addr, &client_len); if(client_sockfd == -1) { - perror("Socket accept failed."); + if(errno != EWOULDBLOCK && errno != EAGAIN) + perror("Socket accept failed."); return nullptr; } @@ -97,7 +94,7 @@ bool Socket::connect(const std::string& ip_address, unsigned short port) { 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, errno, strerror(errno)); 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 bytes_received = ::recv(_sockfd, buffer, length, 0); - if(bytes_received == -1) { + if(bytes_received == -1 && errno != EWOULDBLOCK && errno != EAGAIN) { perror("Receive failed."); } return bytes_received; diff --git a/src/bettola.cpp b/src/bettola.cpp index d1d393c..7a471d5 100644 --- a/src/bettola.cpp +++ b/src/bettola.cpp @@ -1,17 +1,22 @@ #include #include +#include /* FINE LSP!! I'll play your games!!!! */ #include /* ~HJAPPY?!?!?! */ #include #include #include +#include +#include #include #include #include "bettola.h" #include "bettola/network/socket.h" #include "bettola/network/net_common.h" +#include "math/mat4.h" #include "network/message.h" +#include "network/game_state_message.h" #include "network/player_pos_message.h" /* Dacav's resolution ;) */ @@ -23,7 +28,8 @@ Bettola::Bettola(void) : _window(nullptr), _gl_context(nullptr), _vao(0), - _vbo(0) {} + _vbo(0), + _our_player_id(0) {} Bettola::~Bettola(void) { if(_gl_context) { @@ -97,6 +103,28 @@ int Bettola::run(void) { 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(); update(delta_time); render(); @@ -171,9 +199,50 @@ void Bettola::render(void) { 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::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) { if(!SDL_Init(SDL_INIT_VIDEO)) { fprintf(stderr, "Failed to iniit SDL! SDL ERROR: %s\n", SDL_GetError()); @@ -257,6 +326,14 @@ bool Bettola::init_client_connection(void) { 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); return true; } diff --git a/src/bettola.h b/src/bettola.h index 12c5e4a..17817ea 100644 --- a/src/bettola.h +++ b/src/bettola.h @@ -1,11 +1,14 @@ #pragma once #include +#include #include "graphics/shader.h" #include "game/player.h" +#include "game/remote_player.h" #include "math/mat4.h" #include "bettola/network/socket.h" +#include "network/game_state_message.h" class Bettola { public: @@ -18,6 +21,7 @@ private: void process_events(void); void update(double dt); void render(void); + void update_remote_players(const BettolaLib::Network::GameStateMessage& msg); bool init_sdl(void); bool init_glew(void); @@ -37,6 +41,7 @@ private: bool _is_running; + unsigned int _our_player_id; SDL_Window* _window; SDL_GLContext _gl_context; @@ -47,5 +52,6 @@ private: BettolaMath::Mat4 _projection; Player _player; InputState _input; + std::vector _remote_players; BettolaLib::Network::Socket _client_socket; }; diff --git a/src/game/remote_player.cpp b/src/game/remote_player.cpp new file mode 100644 index 0000000..de76e56 --- /dev/null +++ b/src/game/remote_player.cpp @@ -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; +} diff --git a/src/game/remote_player.h b/src/game/remote_player.h new file mode 100644 index 0000000..f33490d --- /dev/null +++ b/src/game/remote_player.h @@ -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; +}; diff --git a/srv/game/game.cpp b/srv/game/game.cpp index a0f5f9d..5fb17ea 100644 --- a/srv/game/game.cpp +++ b/srv/game/game.cpp @@ -6,27 +6,26 @@ #include "network/message.h" Player* Game::add_player(BettolaLib::Network::Socket* socket) { - _players.emplace_back(socket); - return &_players.back(); + _players.push_back(new Player(socket)); + return _players.back(); } void Game::remove_player(unsigned int player_id) { _players.erase( std::remove_if(_players.begin(), _players.end(), - [player_id](const Player& player) { - /* this is a sh.t way to compare players. */ - return player.get_id() == player_id; + [player_id](const Player* player) { + return player->get_id() == player_id; }), _players.end()); } void Game::update_player_pos(unsigned int player_id, float x, float y) { auto it = std::find_if(_players.begin(), _players.end(), - [player_id](const Player& player) { - return player.get_id() == player_id; + [player_id](const Player* player) { + return player->get_id() == player_id; }); 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(); for(size_t i = 0; i < _players.size(); ++i) { - 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].player_id = _players[i]->get_id(); + msg.players[i].x = _players[i]->get_x(); + msg.players[i].y = _players[i]->get_y(); } BettolaLib::Network::MessageHeader header; @@ -45,9 +44,23 @@ void Game::broadcast_game_state(void) { header.size = sizeof(msg); for(const auto& player : _players) { - BettolaLib::Network::Socket& socket = player.get_socket(); - socket.send(&header, sizeof(header)); - socket.send(&msg, sizeof(msg)); + BettolaLib::Network::Socket& socket = player->get_socket(); + /* Just quick fix the server crash for now. */ + 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); +} + diff --git a/srv/game/game.h b/srv/game/game.h index 3c9f820..d2464bb 100644 --- a/srv/game/game.h +++ b/srv/game/game.h @@ -10,7 +10,8 @@ public: void remove_player(unsigned int player_id); void update_player_pos(unsigned int player_id, float x, float y); void broadcast_game_state(void); + Player* get_player_by_socket(BettolaLib::Network::Socket* socket); private: - std::vector _players; + std::vector _players; }; diff --git a/srv/main.cpp b/srv/main.cpp index 10916fd..998ecc2 100644 --- a/srv/main.cpp +++ b/srv/main.cpp @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include #include "bettola/network/socket.h" #include "bettola/network/net_common.h" @@ -11,6 +14,9 @@ int main(void) { Game game; + std::vector client_sockets; + + signal(SIGPIPE, SIG_IGN); printf("=== Bettola Server: Starting ===\n"); @@ -36,46 +42,81 @@ int main(void) { 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(); /* Main server loop. */ while(true) { - auto current_time = std::chrono::high_resolution_clock::now(); - auto delta_time = std::chrono::duration_cast> - (current_time-last_time).count(); - - /* TODO: - * Accept new connections. - * This is blocking.. Will fix later. - */ + /* Accept new connections. */ BettolaLib::Network::Socket* client_socket = server_socket.accept(); if(client_socket != nullptr) { Player* new_player = game.add_player(client_socket); 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. - * this is also blocking and only handles one client at a time. fix later - */ - if(client_socket != nullptr) { + /* Process messages from clients. */ + for(auto it = client_sockets.begin(); it != client_sockets.end();) { + BettolaLib::Network::Socket* client = *it; + bool client_disconnected = false; + while(true) { 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) { - //game.remove_player(new_player->get_id()); + if(bytes_received == 0) { + client_disconnected = true; + break; + } + + if(bytes_received < 0) { + /* No data to read.. */ break; } if(header.type == BettolaLib::Network::MessageType::PlayerPosition) { BettolaLib::Network::PlayerPosMessage msg; - bytes_received = client_socket->recv(&msg, sizeof(msg)); + bytes_received = client->recv(&msg, sizeof(msg)); 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. */