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:
parent
39a06147c8
commit
5944663017
@ -7,6 +7,7 @@ enum class MessageType : unsigned char {
|
||||
PlayerPosition,
|
||||
PlayerState,
|
||||
GameState,
|
||||
PlayerId,
|
||||
};
|
||||
|
||||
struct MessageHeader {
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <cerrno>
|
||||
|
||||
#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;
|
||||
|
||||
@ -1,17 +1,22 @@
|
||||
#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 <fcntl.h>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <vector>
|
||||
|
||||
#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<RemotePlayer> _remote_players;
|
||||
BettolaLib::Network::Socket _client_socket;
|
||||
};
|
||||
|
||||
8
src/game/remote_player.cpp
Normal file
8
src/game/remote_player.cpp
Normal 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
17
src/game/remote_player.h
Normal 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;
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<Player> _players;
|
||||
std::vector<Player*> _players;
|
||||
};
|
||||
|
||||
77
srv/main.cpp
77
srv/main.cpp
@ -2,6 +2,9 @@
|
||||
#include <cstdio>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
|
||||
#include "bettola/network/socket.h"
|
||||
#include "bettola/network/net_common.h"
|
||||
@ -11,6 +14,9 @@
|
||||
|
||||
int main(void) {
|
||||
Game game;
|
||||
std::vector<BettolaLib::Network::Socket*> 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<std::chrono::duration<double>>
|
||||
(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. */
|
||||
|
||||
Loading…
Reference in New Issue
Block a user