├── .gitignore ├── LICENSE ├── README.md ├── client.cpp ├── client.hpp ├── command.hpp ├── fragment_shader.glsl ├── keyboard.hpp ├── makefile ├── mask.obj ├── misc.hpp ├── network.hpp ├── player.hpp ├── server.cpp ├── server.hpp ├── ui.hpp ├── ui_sdl.hpp ├── ui_sdl_gl.hpp ├── vertex_shader.glsl └── world.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | client 2 | server -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robin Arnesson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # game-networking 2 | 3 | A simple game-like networking program that uses client-side prediction and entity interpolation. The project is based on Valves description of basic networking for the Source engine, read [here](https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking). 4 | 5 | Watch demo: 6 | [http://youtu.be/52AQqD_kKPY](http://youtu.be/52AQqD_kKPY) 7 | 8 | ### Start server 9 | Run in terminal: 10 | ``` 11 | ./server 1024 12 | ``` 13 | ### Start client(s) 14 | Run in terminal: 15 | ``` 16 | ./client localhost 1024 3d 17 | ``` 18 | ### Controls 19 | ##### 3d-controls: 20 | * WASD + mouse look 21 | * Space: Move up 22 | * Left ctrl: Move down 23 | 24 | ##### 2d-controls: 25 | * Use arrow keys 26 | 27 | ##### Other: 28 | * F1: Toggle debug mode 29 | * F2: Toggle prediction and interpolation 30 | * Escape: Quit 31 | -------------------------------------------------------------------------------- /client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "client.hpp" 3 | #include "ui_sdl.hpp" 4 | #include "ui_sdl_gl.hpp" 5 | #include "misc.hpp" 6 | 7 | int main(int argc, char const *argv[]) { 8 | if (argc < 4 || !misc::is_number(argv[2]) || (strcmp(argv[3], "2d") && strcmp(argv[3], "3d"))) { 9 | std::cout << "Usage: " << argv[0] << " 2d|3d" << std::endl << std::endl; 10 | std::cout << "2d-controls: " << std::endl; 11 | std::cout << " Move around with the arrow keys" << std::endl; 12 | std::cout << "3d-controls: " << std::endl; 13 | std::cout << " W: Move forward" << std::endl; 14 | std::cout << " S: Move backward" << std::endl; 15 | std::cout << " A: Step left" << std::endl; 16 | std::cout << " D: Step right" << std::endl; 17 | std::cout << " Space: Move up" << std::endl; 18 | std::cout << " Left ctrl: Move down" << std::endl; 19 | std::cout << " Mouse look" << std::endl; 20 | std::cout << "Other: " << std::endl; 21 | std::cout << " F1: Toggle debug mode" << std::endl; 22 | std::cout << " F2: Toggle prediction and interpolation" << std::endl; 23 | std::cout << " Escape: Quit" << std::endl; 24 | 25 | return 1; 26 | } 27 | 28 | std::string title(std::string("client program, ") + argv[1] + ":" + argv[2]); 29 | 30 | try { 31 | ui* interface; 32 | 33 | if (!strcmp(argv[3], "3d")) 34 | interface = new ui_sdl_gl(title); // 3d 35 | else 36 | interface = new ui_sdl(title); // 2d 37 | 38 | client(argv[1], argv[2], *interface).join_game(); 39 | 40 | delete interface; 41 | } catch (std::exception& e) { 42 | std::cerr << "exception: " << e.what() << std::endl; 43 | 44 | return 2; 45 | } 46 | 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /client.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CLIENT_HPP_ 2 | #define CLIENT_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "keyboard.hpp" 12 | #include "misc.hpp" 13 | #include "network.hpp" 14 | #include "player.hpp" 15 | #include "ui.hpp" 16 | #include "world.hpp" 17 | 18 | class client { 19 | public: 20 | static const int MAIN_LOOP_SLEEP_MS = 15; 21 | static const int INTERPOLATION_TIME_MS = 300; 22 | 23 | client(std::string host, std::string port, ui& interface) 24 | : player_id_(0), 25 | game_time_ms_(0), 26 | io_service_(), 27 | socket_(io_service_), 28 | resolver_(io_service_), 29 | endpoint_iterator_(resolver_.resolve({ host, port })), 30 | host_(host), 31 | port_(port), 32 | exit_program_(false), 33 | predict_and_interpolate_(true), 34 | debug_(false), 35 | interface_(interface) 36 | { 37 | start_server_connect(); 38 | io_service_thread_ = std::thread([this](){ io_service_.run(); }); 39 | INFO("client started"); 40 | } 41 | 42 | ~client() { 43 | INFO("stopping client"); 44 | io_service_.stop(); 45 | io_service_thread_.join(); 46 | } 47 | 48 | void join_game() { 49 | INFO("joining game"); 50 | 51 | // wait for server to accept join request and send world snapshot 52 | while (!game_ready() && !exit_program_) 53 | misc::sleep_ms(200); 54 | 55 | main_loop(); 56 | } 57 | 58 | private: 59 | void main_loop() { 60 | command command; 61 | int command_id = 1; 62 | uint64_t frame_time_ms = 0; 63 | uint64_t start_ms, stop_ms; 64 | 65 | // main loop 66 | while (!exit_program_) { 67 | start_ms = misc::get_time_ms(); 68 | 69 | // update interface event queue 70 | interface_.poll_events(); 71 | 72 | // check for quit 73 | if (interface_.check_event_quit()) 74 | break; 75 | 76 | // update debug state 77 | if (interface_.check_event_button_released(keyboard::button::f1)) { 78 | debug_ = !debug_; 79 | INFO("debug: " << debug_); 80 | } 81 | 82 | // update prediction and interpolation state 83 | if (interface_.check_event_button_released(keyboard::button::f2)) { 84 | predict_and_interpolate_ = !predict_and_interpolate_; 85 | INFO("predict and interpolate: " << predict_and_interpolate_); 86 | } 87 | 88 | // build move command 89 | command.id = command_id++; 90 | command.buttons = interface_.get_pressed_buttons(); 91 | interface_.get_delta_angles(command.horz_delta_angel, command.vert_delta_angel); 92 | command.duration_ms = frame_time_ms; 93 | 94 | // check for quit 95 | if (command.buttons & keyboard::button::quit) 96 | break; 97 | 98 | world_mutex_.lock(); 99 | commands_mutex_.lock(); 100 | 101 | if (command.buttons || fabs(command.horz_delta_angel + command.vert_delta_angel) > 0.0) { 102 | // handle client-side prediction 103 | if (predict_and_interpolate_) { 104 | // save command for next world update 105 | commands_.push_back(command); 106 | 107 | // process command (predict) 108 | world_.run_command(command, player_id_); 109 | } 110 | 111 | // send command to server 112 | network::write_object(command::CLASS_ID, command, socket_); 113 | } 114 | 115 | // handle entity interpolation 116 | if (predict_and_interpolate_) { 117 | uint64_t render_time = get_interpolation_time_point_ms(); 118 | 119 | boost::optional from, to; 120 | get_snapshots_adjacent_to_time_point(from, to, render_time); 121 | 122 | if (from && to) 123 | interpolate(from, to, render_time); 124 | } 125 | 126 | interface_.draw_clear(); 127 | 128 | // draw smoothed world 129 | interface_.draw_world(world_, player_id_, false); 130 | 131 | // draw actual world 132 | if (debug_) 133 | interface_.draw_world(world_snapshots_.back().snapshot, player_id_, true); 134 | 135 | interface_.draw_update(); 136 | 137 | commands_mutex_.unlock(); 138 | world_mutex_.unlock(); 139 | 140 | stop_ms = misc::get_time_ms(); 141 | frame_time_ms = MAIN_LOOP_SLEEP_MS + stop_ms - start_ms; 142 | game_time_ms_ += frame_time_ms; 143 | 144 | misc::sleep_ms(MAIN_LOOP_SLEEP_MS); 145 | } 146 | } 147 | 148 | void process_message() { 149 | switch (network::get_class_id(read_buffer_)) { 150 | case network::world_snapshot::CLASS_ID: 151 | process_world_update(); 152 | break; 153 | case network::server_accept::CLASS_ID: 154 | process_join_accept(); 155 | break; 156 | case network::server_deny::CLASS_ID: 157 | process_join_deny(); 158 | break; 159 | } 160 | } 161 | 162 | void process_world_update() { 163 | world_mutex_.lock(); 164 | 165 | // decide if to create new snapshot or overwrite last 166 | if (!world_snapshots_.size() || world_snapshots_.back().client_time_ms != game_time_ms_) 167 | world_snapshots_.emplace_back(world()); // create new 168 | 169 | // put snapshot last 170 | network::deserialize(world_snapshots_.back(), read_buffer_); 171 | world_snapshots_.back().client_time_ms = game_time_ms_; 172 | 173 | // clean up 174 | remove_old_world_snapshots(); 175 | 176 | // update world 177 | world_ = world(world_snapshots_.back().snapshot); 178 | 179 | world_mutex_.unlock(); 180 | 181 | // handle client-side prediction 182 | if (predict_and_interpolate_ && game_ready()) { 183 | world_mutex_.lock(); 184 | commands_mutex_.lock(); 185 | 186 | // get command id for last processed command on server 187 | boost::optional p = world_.get_player(player_id_); 188 | int last_command_id = p.get().get_last_command_id(); 189 | 190 | // remove commands older than last command 191 | auto i = commands_.begin(); 192 | while (i != commands_.end()) { 193 | if (i->id <= last_command_id) 194 | i = commands_.erase(i); 195 | else 196 | i++; 197 | } 198 | 199 | // 200 | // TODO: correct player position and clear commands, if needed 201 | // 202 | 203 | // run remaining commands in updated world 204 | for (auto& c : commands_) 205 | world_.run_command(c, player_id_); 206 | 207 | commands_mutex_.unlock(); 208 | world_mutex_.unlock(); 209 | } 210 | } 211 | 212 | void process_join_accept() { 213 | network::server_accept m; 214 | network::deserialize(m, read_buffer_); 215 | player_id_ = m.player_id; 216 | INFO("joined game, player_id: " << std::to_string(player_id_)); 217 | } 218 | 219 | void process_join_deny() { 220 | network::server_deny m; 221 | network::deserialize(m, read_buffer_); 222 | INFO("join rejected, reason: " << m.reason); 223 | signal_exit(); 224 | } 225 | 226 | void make_join_request() { 227 | network::join_request m; 228 | m.player_color_AABBGGRR = misc::generate_color_AABBGGRR(); 229 | DEBUG("player color: " << std::hex << std::setfill('0') << m.player_color_AABBGGRR); 230 | network::write_object(network::join_request::CLASS_ID, m, socket_); 231 | INFO("join request sent"); 232 | } 233 | 234 | bool game_ready() { 235 | return join_request_accepted() && player_added_to_world(); 236 | } 237 | 238 | bool join_request_accepted() { 239 | return player_id_; 240 | } 241 | 242 | bool player_added_to_world() { 243 | if (world_mutex_.try_lock()) { 244 | bool exists = world_snapshots_.size() > 0 245 | && world_snapshots_.front().snapshot.player_exists(player_id_); 246 | world_mutex_.unlock(); 247 | return exists; 248 | } 249 | 250 | return false; 251 | } 252 | 253 | uint64_t get_interpolation_time_point_ms() { 254 | uint64_t time_point = 0; 255 | 256 | if (game_time_ms_ > INTERPOLATION_TIME_MS) 257 | time_point = game_time_ms_ - INTERPOLATION_TIME_MS; 258 | 259 | return time_point; 260 | } 261 | 262 | void get_snapshots_adjacent_to_time_point(boost::optional& from, 263 | boost::optional& to, uint64_t time_point) { 264 | for (auto& s : world_snapshots_) { 265 | if (s.client_time_ms > time_point) { 266 | to = s; 267 | break; 268 | } 269 | from = s; 270 | } 271 | } 272 | 273 | void interpolate(boost::optional& from, 274 | boost::optional& to, uint64_t time_point) { 275 | // 276 | // TODO: put interpolation code in class world and player 277 | // 278 | 279 | // get interpolation time fraction 280 | double fraction = get_time_fraction(from.get().client_time_ms, 281 | to.get().client_time_ms, time_point); 282 | 283 | for (player& p_from : from.get().snapshot.get_players()) { 284 | for (player& p_to : to.get().snapshot.get_players()) { 285 | // player exists in both snapshots and is not client player, interpolate 286 | if (p_from.get_id() == p_to.get_id() && p_to.get_id() != player_id_) { 287 | // get player from world that will be rendered 288 | boost::optional p_real = world_.get_player(p_to.get_id()); 289 | if (p_real) { 290 | // calc interpolated player values 291 | double new_x = (p_to.get_x() - p_from.get_x()) * fraction + p_from.get_x(); 292 | double new_y = (p_to.get_y() - p_from.get_y()) * fraction + p_from.get_y(); 293 | double new_z = (p_to.get_z() - p_from.get_z()) * fraction + p_from.get_z(); 294 | double new_angel = (p_to.get_horz_angel() - p_from.get_horz_angel()) 295 | * fraction + p_from.get_horz_angel(); 296 | 297 | // assign values to player 298 | p_real.get().set_x(new_x); 299 | p_real.get().set_y(new_y); 300 | p_real.get().set_z(new_z); 301 | p_real.get().set_horz_angel(new_angel); 302 | } 303 | } 304 | } 305 | } 306 | } 307 | 308 | double get_time_fraction(uint64_t start_ms, uint64_t stop_ms, uint64_t between_ms) { 309 | return static_cast(between_ms - start_ms) / static_cast(stop_ms - start_ms); 310 | } 311 | 312 | void remove_old_world_snapshots() { 313 | bool remove_next = false; 314 | uint64_t render_time = get_interpolation_time_point_ms(); 315 | auto i = world_snapshots_.end(); 316 | 317 | while (i != world_snapshots_.begin()) { 318 | if (remove_next) 319 | i = world_snapshots_.erase(i); 320 | else if (i->client_time_ms <= render_time && i->client_time_ms > INTERPOLATION_TIME_MS) 321 | remove_next = true; 322 | i--; 323 | } 324 | } 325 | 326 | void start_server_connect() { 327 | boost::asio::async_connect(socket_, endpoint_iterator_, 328 | boost::bind(&client::handle_server_connect, this,boost::asio::placeholders::error)); 329 | } 330 | 331 | void handle_server_connect(const boost::system::error_code& error) { 332 | if (!error) { 333 | INFO("connected to server"); 334 | make_join_request(); 335 | start_read_header(); 336 | } else { 337 | signal_exit(); 338 | DEBUG("async_connect(): " << error.message()); 339 | } 340 | } 341 | 342 | void start_read_header() { 343 | read_buffer_.clear(); 344 | read_buffer_.resize(network::HEADER_SIZE); 345 | 346 | boost::asio::async_read(socket_, 347 | boost::asio::buffer(read_buffer_, network::HEADER_SIZE), 348 | boost::bind(&client::handle_read_header, this, boost::asio::placeholders::error)); 349 | } 350 | 351 | void handle_read_header(const boost::system::error_code& error) { 352 | if (!error) { 353 | start_read_body(network::get_body_size(read_buffer_)); 354 | } else { 355 | signal_exit(); 356 | DEBUG("async_read(): " << error.message()); 357 | } 358 | } 359 | 360 | void start_read_body(size_t body_size) { 361 | read_buffer_.clear(); 362 | read_buffer_.resize(body_size); 363 | 364 | boost::asio::async_read(socket_, 365 | boost::asio::buffer(read_buffer_, body_size), 366 | boost::bind(&client::handle_read_body, this, boost::asio::placeholders::error)); 367 | } 368 | 369 | void handle_read_body(const boost::system::error_code& error) { 370 | if (!error) { 371 | process_message(); 372 | start_read_header(); 373 | } else { 374 | signal_exit(); 375 | DEBUG("async_read(): " << error.message()); 376 | } 377 | } 378 | 379 | void signal_exit() { 380 | socket_.close(); 381 | exit_program_ = true; 382 | } 383 | 384 | // game 385 | world world_; 386 | std::list world_snapshots_; 387 | std::mutex world_mutex_; 388 | std::list commands_; 389 | std::mutex commands_mutex_; 390 | std::atomic player_id_; 391 | std::atomic game_time_ms_; 392 | 393 | // network 394 | std::vector read_buffer_; 395 | boost::asio::io_service io_service_; 396 | boost::asio::ip::tcp::socket socket_; 397 | boost::asio::ip::tcp::resolver resolver_; 398 | boost::asio::ip::tcp::resolver::iterator endpoint_iterator_; 399 | std::thread io_service_thread_; 400 | std::string host_; 401 | std::string port_; 402 | 403 | // other 404 | std::atomic exit_program_; 405 | std::atomic predict_and_interpolate_; 406 | std::atomic debug_; 407 | ui& interface_; 408 | }; 409 | 410 | #endif // CLIENT_HPP_ 411 | -------------------------------------------------------------------------------- /command.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_HPP_ 2 | #define COMMAND_HPP_ 3 | 4 | #include 5 | #include 6 | 7 | class command { 8 | public: 9 | static const uint8_t CLASS_ID = 7; 10 | 11 | int id; 12 | int buttons; // keyboard buttons pressed (keyboard::button) 13 | float horz_delta_angel; 14 | float vert_delta_angel; 15 | int duration_ms; // frame time 16 | 17 | private: 18 | friend class boost::serialization::access; 19 | 20 | template 21 | void serialize(Archive& ar, const unsigned int version) { 22 | ar & id; 23 | ar & buttons; 24 | ar & horz_delta_angel; 25 | ar & vert_delta_angel; 26 | ar & duration_ms; 27 | } 28 | }; 29 | 30 | #endif // COMMAND_HPP_ 31 | -------------------------------------------------------------------------------- /fragment_shader.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec4 normal; 4 | 5 | out vec4 fragment_color; 6 | 7 | uniform vec4 color; 8 | 9 | void main(void) { 10 | vec4 light_position = vec4(5.0, 5.0, 0.0, 0.0); 11 | vec3 diffuse = vec3(0.5, 0.5, 0.5) * max(dot(normalize(normal), normalize(light_position)), 0.0); 12 | 13 | fragment_color = color + vec4(diffuse, 0.0); 14 | } 15 | -------------------------------------------------------------------------------- /keyboard.hpp: -------------------------------------------------------------------------------- 1 | #ifndef KEYBOARD_HPP_ 2 | #define KEYBOARD_HPP_ 3 | 4 | namespace keyboard { 5 | enum button { 6 | up = (1 << 0), 7 | down = (1 << 1), 8 | forward = (1 << 2), 9 | backward = (1 << 3), 10 | left = (1 << 4), 11 | right = (1 << 5), 12 | step_left = (1 << 6), 13 | step_right = (1 << 7), 14 | quit = (1 << 8), 15 | f1 = (1 << 9), 16 | f2 = (1 << 10), 17 | end = (1 << 11) 18 | }; 19 | } 20 | 21 | #endif // KEYBOARD_HPP_ 22 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | CC = g++ 2 | CFLAGS = -Wall -pedantic -std=c++11 3 | 4 | all: client server 5 | 6 | server: 7 | $(CC) server.cpp -o server $(CFLAGS) -D _DEBUG=0 -D _INFO=1 -lboost_serialization -lboost_system -lpthread 8 | 9 | client: 10 | $(CC) client.cpp -o client $(CFLAGS) -D _DEBUG=0 -D _INFO=1 -D GLM_FORCE_RADIANS -lboost_serialization -lboost_system -lpthread -lGL -lGLEW -lSDL2 -lGLU -lSDL2_gfx -lSDL2_image 11 | 12 | clean: clean_server clean_client 13 | 14 | clean_server: 15 | rm -f server 16 | 17 | clean_client: 18 | rm -f client 19 | -------------------------------------------------------------------------------- /misc.hpp: -------------------------------------------------------------------------------- 1 | #ifndef MISC_HPP_ 2 | #define MISC_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define DEBUG(_m_) do { \ 12 | if (_DEBUG) { \ 13 | std::cerr \ 14 | << misc::get_datetime() << " " \ 15 | << __FILE__ << ":" << __LINE__ << " " \ 16 | << __FUNCTION__ << "() | " << _m_ \ 17 | << std::endl; \ 18 | } \ 19 | } while (0) 20 | 21 | #define INFO(_m_) do { \ 22 | if (_INFO) { \ 23 | std::cout \ 24 | << misc::get_datetime() << " | " << _m_ \ 25 | << std::endl; \ 26 | std::cout.flush(); \ 27 | } \ 28 | } while (0) 29 | 30 | namespace misc { 31 | bool is_number(const std::string& s) { 32 | return !s.empty() && std::find_if(s.begin(), 33 | s.end(), [](char c) { return !std::isdigit(c); }) == s.end(); 34 | } 35 | 36 | void sleep_ms(int sleep_time_ms) { 37 | std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time_ms)); 38 | } 39 | 40 | uint64_t get_time_ms() { 41 | return std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1); 42 | } 43 | 44 | uint32_t generate_color_AABBGGRR() { 45 | std::random_device rd; 46 | std::mt19937_64 gen(rd()); 47 | std::uniform_int_distribution dis(0x80, 0xFF); 48 | 49 | return (0xff << 24) | (dis(gen) << 16) | (dis(gen) << 8) | dis(gen); 50 | } 51 | 52 | std::string get_datetime() { 53 | char buffer[32]; 54 | time_t rawtime; 55 | time(&rawtime); 56 | strftime(buffer, 32, "%F %I:%M:%S", localtime(&rawtime)); 57 | 58 | return std::string(buffer); 59 | } 60 | 61 | std::string get_file_content(const char* file) { 62 | std::ifstream ifs(file); 63 | 64 | return std::string( 65 | (std::istreambuf_iterator(ifs)), 66 | (std::istreambuf_iterator())); 67 | } 68 | } 69 | 70 | #endif // MISC_HPP_ 71 | -------------------------------------------------------------------------------- /network.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NETWORK_HPP_ 2 | #define NETWORK_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "world.hpp" 9 | 10 | namespace network { 11 | const int HEADER_SIZE = 8; // bytes 12 | const int CLASS_ID_SIZE = 3; // bytes 13 | 14 | /////////////////////////////////////////////////////////////////// 15 | 16 | class world_snapshot { 17 | public: 18 | static const uint8_t CLASS_ID = 10; 19 | 20 | world snapshot; 21 | uint64_t server_time_ms; 22 | uint64_t client_time_ms; 23 | 24 | world_snapshot(world world) 25 | : snapshot(world), 26 | server_time_ms(0), 27 | client_time_ms(0) 28 | { 29 | } 30 | 31 | private: 32 | friend class boost::serialization::access; 33 | 34 | template 35 | void serialize(Archive & ar, const unsigned int version) { 36 | ar & snapshot; 37 | ar & server_time_ms; 38 | ar & client_time_ms; 39 | } 40 | }; 41 | 42 | /////////////////////////////////////////////////////////////////// 43 | 44 | class connection { 45 | public: 46 | boost::asio::ip::tcp::socket socket; 47 | std::vector read_buffer; 48 | uint8_t player_id; 49 | 50 | connection(boost::asio::io_service& io_service) 51 | : socket(io_service), 52 | player_id(0) 53 | { 54 | } 55 | }; 56 | 57 | using connection_ptr = std::shared_ptr; 58 | 59 | /////////////////////////////////////////////////////////////////// 60 | 61 | class join_request { 62 | public: 63 | static const uint8_t CLASS_ID = 4; 64 | 65 | uint32_t player_color_AABBGGRR; 66 | 67 | private: 68 | friend class boost::serialization::access; 69 | 70 | template 71 | void serialize(Archive & ar, const unsigned int version) { 72 | ar & player_color_AABBGGRR; 73 | } 74 | }; 75 | 76 | /////////////////////////////////////////////////////////////////// 77 | 78 | class server_accept { 79 | public: 80 | static const uint8_t CLASS_ID = 5; 81 | 82 | uint8_t player_id; 83 | 84 | private: 85 | friend class boost::serialization::access; 86 | 87 | template 88 | void serialize(Archive & ar, const unsigned int version) { 89 | ar & player_id; 90 | } 91 | }; 92 | 93 | /////////////////////////////////////////////////////////////////// 94 | 95 | class server_deny { 96 | public: 97 | static const uint8_t CLASS_ID = 6; 98 | 99 | std::string reason; 100 | 101 | private: 102 | friend class boost::serialization::access; 103 | 104 | template 105 | void serialize(Archive & ar, const unsigned int version) { 106 | ar & reason; 107 | } 108 | }; 109 | 110 | /////////////////////////////////////////////////////////////////// 111 | 112 | template 113 | void deserialize(T& object, const std::vector& body_data) { 114 | std::string archive_data(body_data.begin() + CLASS_ID_SIZE, body_data.end()); 115 | std::istringstream archive_stream(archive_data); 116 | boost::archive::text_iarchive archive(archive_stream); 117 | archive >> object; 118 | } 119 | 120 | template 121 | void build_message(std::vector& message, uint8_t class_id, const T& object) { 122 | // get object data (serialize) 123 | std::ostringstream archive_stream; 124 | boost::archive::text_oarchive archive(archive_stream); 125 | archive << object; 126 | const std::string& object_str = archive_stream.str(); 127 | 128 | // set object data size 129 | std::ostringstream header_stream; 130 | header_stream << std::setw(HEADER_SIZE) 131 | << std::to_string(object_str.size() + CLASS_ID_SIZE); 132 | const std::string& header_str = header_stream.str(); 133 | 134 | // set class id 135 | std::ostringstream class_id_stream; 136 | class_id_stream << std::setw(CLASS_ID_SIZE) << std::to_string(class_id); 137 | const std::string& class_id_str = class_id_stream.str(); 138 | 139 | // set message header 140 | message.insert(message.end(), header_str.begin(), header_str.end()); 141 | 142 | // set message body 143 | message.insert(message.end(), class_id_str.begin(), class_id_str.end()); 144 | message.insert(message.end(), object_str.begin(), object_str.end()); 145 | } 146 | 147 | void write_data(const std::vector& data, boost::asio::ip::tcp::socket& socket) { 148 | boost::asio::async_write(socket, 149 | boost::asio::buffer(data, data.size()), 150 | [](boost::system::error_code, std::size_t){ /* do nothing */ }); 151 | } 152 | 153 | template 154 | void write_object(uint8_t class_id, const T& object, boost::asio::ip::tcp::socket& socket) { 155 | std::vector data; 156 | build_message(data, class_id, object); 157 | write_data(data, socket); 158 | } 159 | 160 | int get_number(const std::vector& data, size_t start, size_t size) { 161 | try { 162 | return std::stoi(std::string(data.begin() + start, data.begin() + size)); 163 | } catch (std::exception& e) { 164 | return 0; 165 | } 166 | } 167 | 168 | uint8_t get_class_id(const std::vector& body_data) { 169 | return get_number(body_data, 0, CLASS_ID_SIZE); 170 | } 171 | 172 | int get_body_size(const std::vector& header_data) { 173 | return get_number(header_data, 0, HEADER_SIZE); 174 | } 175 | } 176 | 177 | #endif // NETWORK_HPP_ 178 | -------------------------------------------------------------------------------- /player.hpp: -------------------------------------------------------------------------------- 1 | #ifndef PLAYER_HPP_ 2 | #define PLAYER_HPP_ 3 | 4 | #include 5 | #include 6 | #include "command.hpp" 7 | #include "keyboard.hpp" 8 | 9 | class player { 10 | public: 11 | static const uint8_t CLASS_ID = 1; 12 | 13 | static const int DEFAULT_MOVE_SPEED = 2; // m/s 14 | static const int DEFAULT_TURN_SPEED = 3; // rad/s 15 | 16 | player() 17 | : id_(0), 18 | color_(0xffffffff), // 0xAABBGGRR 19 | x_(0.0), 20 | y_(0.0), 21 | z_(0.0), 22 | horz_angel_(0.0), 23 | vert_angel_(0.0), 24 | last_command_id_(0) 25 | { 26 | } 27 | 28 | uint8_t get_id() const { 29 | return id_; 30 | } 31 | 32 | void set_id(uint8_t id) { 33 | id_ = id; 34 | } 35 | 36 | uint32_t get_color_AABBGGRR() const { 37 | return color_; 38 | } 39 | 40 | void set_color_AABBGGRR(uint32_t color) { 41 | color_ = color; 42 | } 43 | 44 | float get_x() const { 45 | return x_; 46 | } 47 | 48 | void set_x(float x) { 49 | x_ = x; 50 | } 51 | 52 | float get_y() const { 53 | return y_; 54 | } 55 | 56 | void set_y(float y) { 57 | y_ = y; 58 | } 59 | 60 | float get_z() const { 61 | return z_; 62 | } 63 | 64 | void set_z(float z) { 65 | z_ = z; 66 | } 67 | 68 | float get_horz_angel() const { 69 | return horz_angel_; 70 | } 71 | 72 | void set_horz_angel(float angel) { 73 | horz_angel_ = angel; 74 | } 75 | 76 | float get_vert_angel() const { 77 | return vert_angel_; 78 | } 79 | 80 | void set_vert_angel(float angel) { 81 | vert_angel_ = angel; 82 | } 83 | 84 | int get_last_command_id() const { 85 | return last_command_id_; 86 | } 87 | 88 | void set_last_command_id(int command_id) { 89 | last_command_id_ = command_id; 90 | } 91 | 92 | void run_command(const command& cmd) { 93 | // remember this command 94 | last_command_id_ = cmd.id; 95 | 96 | // update angels 97 | horz_angel_ += cmd.horz_delta_angel; 98 | vert_angel_ += cmd.vert_delta_angel; 99 | 100 | // handle action turn left and turn right 101 | if ((cmd.buttons & keyboard::button::left) || (cmd.buttons & keyboard::button::right)) { 102 | float add_angel = cmd.duration_ms * DEFAULT_TURN_SPEED / 1000.0; 103 | 104 | horz_angel_ += (cmd.buttons & keyboard::button::left) ? add_angel : -add_angel; 105 | } 106 | 107 | // if no more actions except turn left and right then return 108 | if (cmd.buttons == (keyboard::button::left | keyboard::button::right)) 109 | return; 110 | 111 | float add_x, add_z; 112 | float base_distance = cmd.duration_ms * DEFAULT_MOVE_SPEED / 1000.0; 113 | 114 | // handle action move forward and move backward 115 | if ((cmd.buttons & keyboard::button::forward) || (cmd.buttons & keyboard::button::backward)) { 116 | add_x = base_distance * std::cos(horz_angel_); 117 | add_z = base_distance * -std::sin(horz_angel_); 118 | 119 | x_ += (cmd.buttons & keyboard::button::forward) ? add_x : -add_x; 120 | z_ += (cmd.buttons & keyboard::button::forward) ? add_z : -add_z; 121 | } 122 | 123 | // handle action strafe left and strafe right 124 | if ((cmd.buttons & keyboard::button::step_left) || 125 | (cmd.buttons & keyboard::button::step_right)) { 126 | add_x = base_distance * std::cos(horz_angel_ - M_PI / 2); 127 | add_z = base_distance * -std::sin(horz_angel_ - M_PI / 2); 128 | 129 | x_ += (cmd.buttons & keyboard::button::step_right) ? add_x : -add_x; 130 | z_ += (cmd.buttons & keyboard::button::step_right) ? add_z : -add_z; 131 | } 132 | 133 | // handle action move up 134 | if ((cmd.buttons & keyboard::button::up)) 135 | y_ += base_distance; 136 | 137 | // handle action move down 138 | if ((cmd.buttons & keyboard::button::down)) 139 | y_ -= base_distance; 140 | } 141 | 142 | private: 143 | friend class boost::serialization::access; 144 | 145 | template 146 | void serialize(Archive& ar, const unsigned int version) { 147 | ar & id_; 148 | ar & color_; 149 | ar & x_ & y_ & z_; 150 | ar & horz_angel_ & vert_angel_; 151 | ar & last_command_id_; 152 | } 153 | 154 | uint8_t id_; 155 | uint32_t color_; 156 | float x_, y_, z_; 157 | float horz_angel_, vert_angel_; 158 | int last_command_id_; 159 | }; 160 | 161 | #endif // PLAYER_HPP_ 162 | -------------------------------------------------------------------------------- /server.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "server.hpp" 4 | #include "misc.hpp" 5 | 6 | int main(int argc, char const *argv[]) { 7 | if (argc < 2 || !misc::is_number(argv[1])) { 8 | std::cout << "Usage: " << argv[0] << " " << std::endl; 9 | 10 | return 1; 11 | } 12 | 13 | try { 14 | server s(std::stoi(argv[1])); 15 | std::cin.get(); // exit on key pressed 16 | } catch (std::exception& e) { 17 | std::cerr << "exception: " << e.what() << std::endl; 18 | 19 | return 2; 20 | } 21 | 22 | return 0; 23 | } 24 | -------------------------------------------------------------------------------- /server.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_HPP_ 2 | #define SERVER_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "misc.hpp" 9 | #include "network.hpp" 10 | #include "world.hpp" 11 | 12 | class server { 13 | public: 14 | static const int CLIENT_UPDATE_INTERVAL_MS = 50; 15 | 16 | server(int port) 17 | : game_time_ms_(0), 18 | io_service_(), 19 | endpoint_(boost::asio::ip::tcp::v4(), port), 20 | acceptor_(io_service_, endpoint_), 21 | timer_(io_service_) 22 | { 23 | start_socket_acceptor(); 24 | start_client_updater(); 25 | io_service_thread_ = std::thread([this](){ io_service_.run(); }); 26 | INFO("server started"); 27 | } 28 | 29 | ~server() { 30 | INFO("stopping server"); 31 | io_service_.stop(); 32 | io_service_thread_.join(); 33 | } 34 | 35 | private: 36 | void process_message(network::connection_ptr connection) { 37 | switch (network::get_class_id(connection->read_buffer)) { 38 | case network::join_request::CLASS_ID: 39 | process_join_request(connection); 40 | break; 41 | case command::CLASS_ID: 42 | process_command(connection); 43 | break; 44 | } 45 | } 46 | 47 | void process_join_request(network::connection_ptr connection) { 48 | network::join_request m; 49 | network::deserialize(m, connection->read_buffer); 50 | 51 | player p; 52 | p.set_color_AABBGGRR(m.player_color_AABBGGRR); 53 | 54 | if (world_.add_player(p)) { 55 | connection->player_id = p.get_id(); 56 | 57 | // confirm join 58 | network::server_accept accept; 59 | accept.player_id = p.get_id(); 60 | network::write_object(network::server_accept::CLASS_ID, accept, connection->socket); 61 | 62 | INFO("player joined, id: " << std::to_string(p.get_id())); 63 | } else { 64 | // reject join 65 | network::server_deny deny; 66 | deny.reason = "player limit reached"; 67 | network::write_object(network::server_deny::CLASS_ID, deny, connection->socket); 68 | 69 | INFO("player rejected, reason: " << deny.reason); 70 | } 71 | } 72 | 73 | void process_command(network::connection_ptr connection) { 74 | command c; 75 | network::deserialize(c, connection->read_buffer); 76 | 77 | // 78 | // TODO: validate command 79 | // 80 | 81 | // simulate 82 | world_.run_command(c, connection->player_id); 83 | } 84 | 85 | void update_clients() { 86 | game_time_ms_ += CLIENT_UPDATE_INTERVAL_MS; 87 | 88 | if (!connections_.size()) 89 | return; 90 | 91 | // build world snapshot 92 | std::vector data; 93 | network::world_snapshot s(world_); 94 | s.server_time_ms = game_time_ms_; 95 | network::build_message(data, network::world_snapshot::CLASS_ID, s); 96 | 97 | // send to all clients 98 | for (auto& c : connections_) 99 | network::write_data(data, c->socket); 100 | } 101 | 102 | void start_socket_acceptor() { 103 | network::connection_ptr c(new network::connection(io_service_)); 104 | acceptor_.async_accept(c->socket, 105 | boost::bind(&server::handle_socket_accept, this, c, 106 | boost::asio::placeholders::error)); 107 | } 108 | 109 | void handle_socket_accept(network::connection_ptr connection, 110 | const boost::system::error_code& error) { 111 | if (!error) { 112 | connections_.push_back(connection); 113 | start_read_header(connection); 114 | INFO("new client connected"); 115 | } else { 116 | DEBUG("async_accept(): " << error.message()); 117 | } 118 | 119 | start_socket_acceptor(); 120 | } 121 | 122 | void start_client_updater() { 123 | timer_.expires_from_now(boost::posix_time::milliseconds(CLIENT_UPDATE_INTERVAL_MS)); 124 | timer_.async_wait( 125 | boost::bind(&server::handle_client_update, this, boost::asio::placeholders::error)); 126 | } 127 | 128 | void handle_client_update(const boost::system::error_code& error) { 129 | if (!error) { 130 | update_clients(); 131 | start_client_updater(); 132 | } else { 133 | DEBUG("async_wait(): " << error.message()); 134 | } 135 | } 136 | 137 | void start_read_header(network::connection_ptr connection) { 138 | connection->read_buffer.clear(); 139 | connection->read_buffer.resize(network::HEADER_SIZE); 140 | 141 | boost::asio::async_read(connection->socket, 142 | boost::asio::buffer(connection->read_buffer, network::HEADER_SIZE), 143 | boost::bind(&server::handle_read_header, this, connection, 144 | boost::asio::placeholders::error)); 145 | } 146 | 147 | void handle_read_header(network::connection_ptr connection, 148 | const boost::system::error_code& error) { 149 | if (!error) { 150 | start_read_body(connection, network::get_body_size(connection->read_buffer)); 151 | } else { 152 | close_connection(connection); 153 | DEBUG("async_read(): " << error.message()); 154 | } 155 | } 156 | 157 | void start_read_body(network::connection_ptr connection, size_t read_size) { 158 | connection->read_buffer.clear(); 159 | connection->read_buffer.resize(read_size); 160 | 161 | boost::asio::async_read(connection->socket, 162 | boost::asio::buffer(connection->read_buffer, read_size), 163 | boost::bind(&server::handle_read_body, this, connection, 164 | boost::asio::placeholders::error)); 165 | } 166 | 167 | void handle_read_body(network::connection_ptr connection, 168 | const boost::system::error_code& error) { 169 | if (!error) { 170 | process_message(connection); 171 | start_read_header(connection); 172 | } else { 173 | close_connection(connection); 174 | DEBUG("async_read(): " << error.message()); 175 | } 176 | } 177 | 178 | void close_connection(network::connection_ptr connection) { 179 | remove_connection_from_list(connection); 180 | world_.remove_player(connection->player_id); 181 | connection->socket.close(); 182 | INFO("client disconnected"); 183 | } 184 | 185 | void remove_connection_from_list(network::connection_ptr connection) { 186 | auto i = connections_.begin(); 187 | 188 | while (i != connections_.end()) { 189 | if (i->get() == connection.get()) { 190 | connections_.erase(i); 191 | break; 192 | } 193 | i++; 194 | } 195 | } 196 | 197 | // game 198 | world world_; 199 | uint64_t game_time_ms_; 200 | 201 | // network 202 | boost::asio::io_service io_service_; 203 | boost::asio::ip::tcp::endpoint endpoint_; 204 | boost::asio::ip::tcp::acceptor acceptor_; 205 | boost::asio::deadline_timer timer_; 206 | std::list connections_; 207 | 208 | // other 209 | std::thread io_service_thread_; 210 | }; 211 | 212 | #endif // SERVER_HPP_ 213 | -------------------------------------------------------------------------------- /ui.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UI_HPP_ 2 | #define UI_HPP_ 3 | 4 | #include 5 | 6 | class ui { 7 | public: 8 | virtual ~ui() { } 9 | 10 | virtual void poll_events() = 0; 11 | 12 | virtual bool check_event_quit() = 0; 13 | 14 | virtual bool check_event_button_released(keyboard::button button) = 0; 15 | 16 | virtual int get_pressed_buttons() = 0; 17 | 18 | virtual void get_delta_angles(float& horizontal, float& vertical) = 0; 19 | 20 | virtual void draw_clear() = 0; 21 | 22 | virtual void draw_world(world& world, uint8_t perspective_player_id, bool draw_phantoms) = 0; 23 | 24 | virtual void draw_update() = 0; 25 | }; 26 | 27 | #endif // UI_HPP_ 28 | -------------------------------------------------------------------------------- /ui_sdl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UI_SDL_HPP_ 2 | #define UI_SDL_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include "glm/gtc/matrix_transform.hpp" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "keyboard.hpp" 13 | #include "world.hpp" 14 | 15 | class ui_sdl : public ui { 16 | public: 17 | static const int WINDOW_WIDTH = 500; 18 | static const int WINDOW_HEIGHT = 500; 19 | 20 | ui_sdl(std::string title) { 21 | if (SDL_Init(SDL_INIT_VIDEO) != 0) 22 | throw std::runtime_error(std::string("error, SDL_Init(): ") + SDL_GetError()); 23 | 24 | window_ = SDL_CreateWindow( 25 | title.c_str(), 26 | SDL_WINDOWPOS_CENTERED, 27 | SDL_WINDOWPOS_CENTERED, 28 | WINDOW_WIDTH, 29 | WINDOW_HEIGHT, 30 | SDL_WINDOW_SHOWN); 31 | 32 | if (!window_) 33 | throw std::runtime_error(std::string("error, SDL_CreateWindow(): ") + SDL_GetError()); 34 | 35 | renderer_ = SDL_CreateRenderer(window_, 0, SDL_RENDERER_ACCELERATED); 36 | 37 | if (!renderer_) 38 | throw std::runtime_error(std::string("error, SDL_CreateRenderer(): ") + SDL_GetError()); 39 | } 40 | 41 | ~ui_sdl() { 42 | SDL_DestroyWindow(window_); 43 | SDL_DestroyRenderer(renderer_); 44 | SDL_Quit(); 45 | } 46 | 47 | void poll_events() { 48 | events_.resize(64); 49 | 50 | int events_returned = SDL_PeepEvents( 51 | &events_.front(), 52 | events_.capacity(), 53 | SDL_GETEVENT, 54 | SDL_FIRSTEVENT, 55 | SDL_LASTEVENT); 56 | 57 | if (events_returned == -1) { 58 | INFO("error, SDL_PeepEvents(): " << SDL_GetError()); 59 | events_returned = 0; 60 | } 61 | 62 | events_.resize(events_returned); 63 | } 64 | 65 | bool check_event_quit() { 66 | for (auto& e : events_) 67 | if (e.type == SDL_QUIT) 68 | return true; 69 | 70 | return false; 71 | } 72 | 73 | bool check_event_button_released(keyboard::button button) { 74 | for (auto& e : events_) { 75 | if (e.type != SDL_KEYUP) 76 | continue; 77 | 78 | int code = e.key.keysym.scancode; 79 | 80 | for (int i = 1; i != keyboard::button::end; i <<= 1) { 81 | std::vector codes = get_sdl_scancodes(i); 82 | 83 | for (auto &c : codes) 84 | if (code == c && i == button) 85 | return true; 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | 92 | int get_pressed_buttons() { 93 | SDL_PumpEvents(); 94 | const Uint8* state = SDL_GetKeyboardState(nullptr); 95 | 96 | int pressed = 0; 97 | 98 | for (int i = 1; i != keyboard::button::end; i <<= 1) { 99 | std::vector codes = get_sdl_scancodes(i); 100 | 101 | for (auto& c : codes) 102 | if (state[c]) 103 | pressed |= i; 104 | } 105 | 106 | return pressed; 107 | } 108 | 109 | void get_delta_angles(float& horizontal, float& vertical) { 110 | horizontal = vertical = 0.0; 111 | } 112 | 113 | void draw_clear() { 114 | int clear_color = 0x202020ff; // 0xRRGGBBAA 115 | 116 | SDL_SetRenderDrawColor( 117 | renderer_, 118 | (clear_color >> 24) & 0xff, 119 | (clear_color >> 16) & 0xff, 120 | (clear_color >> 8) & 0xff, 121 | clear_color & 0xff); 122 | 123 | SDL_RenderClear(renderer_); 124 | } 125 | 126 | void draw_world(world& world, uint8_t perspective_player_id, bool draw_phantoms) { 127 | // draw each player 128 | for (player& p : world.get_players()) { 129 | float a = p.get_horz_angel(); 130 | 131 | float temp[] = { 132 | std::cos(a), -std::sin(a), 133 | std::sin(a), std::cos(a)}; 134 | 135 | glm::mat2 rotation_matrix = glm::make_mat2(temp); 136 | 137 | // set triangle edges 138 | glm::vec2 c1 = rotation_matrix * glm::vec2(-15, -15); 139 | glm::vec2 c2 = rotation_matrix * glm::vec2(24, 0); 140 | glm::vec2 c3 = rotation_matrix * glm::vec2(-15, 15); 141 | 142 | int center_horz = WINDOW_WIDTH / 2; 143 | int center_vert = WINDOW_HEIGHT / 2; 144 | 145 | if (draw_phantoms) { 146 | trigonColor(renderer_, 147 | center_horz + c1.x + p.get_x() * 50, center_vert + c1.y + p.get_z() * 50, 148 | center_horz + c2.x + p.get_x() * 50, center_vert + c2.y + p.get_z() * 50, 149 | center_horz + c3.x + p.get_x() * 50, center_vert + c3.y + p.get_z() * 50, 150 | p.get_color_AABBGGRR()); 151 | } else { 152 | filledTrigonColor(renderer_, 153 | center_horz + c1.x + p.get_x() * 50, center_vert + c1.y + p.get_z() * 50, 154 | center_horz + c2.x + p.get_x() * 50, center_vert + c2.y + p.get_z() * 50, 155 | center_horz + c3.x + p.get_x() * 50, center_vert + c3.y + p.get_z() * 50, 156 | p.get_color_AABBGGRR()); 157 | } 158 | } 159 | } 160 | 161 | void draw_update() { 162 | SDL_RenderPresent(renderer_); 163 | } 164 | 165 | private: 166 | std::vector get_sdl_scancodes(int button) { 167 | switch (button) { 168 | case keyboard::button::forward: return std::vector { SDL_SCANCODE_UP }; 169 | case keyboard::button::backward: return std::vector { SDL_SCANCODE_DOWN }; 170 | case keyboard::button::left: return std::vector { SDL_SCANCODE_LEFT }; 171 | case keyboard::button::right: return std::vector { SDL_SCANCODE_RIGHT }; 172 | case keyboard::button::quit: return std::vector { SDL_SCANCODE_ESCAPE }; 173 | case keyboard::button::f1: return std::vector { SDL_SCANCODE_F1 }; 174 | case keyboard::button::f2: return std::vector { SDL_SCANCODE_F2 }; 175 | } 176 | 177 | return std::vector(); 178 | } 179 | 180 | // sdl 181 | SDL_Window* window_; 182 | SDL_Renderer* renderer_; 183 | std::vector events_; 184 | }; 185 | 186 | #endif // UI_SDL_HPP_ 187 | -------------------------------------------------------------------------------- /ui_sdl_gl.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UI_SDL_GL_HPP_ 2 | #define UI_SDL_GL_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "glm/gtc/matrix_transform.hpp" 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include "keyboard.hpp" 20 | #include "misc.hpp" 21 | #include "world.hpp" 22 | 23 | class ui_sdl_gl : public ui { 24 | public: 25 | static const int WINDOW_WIDTH = 1280; 26 | static const int WINDOW_HEIGHT = 800; 27 | 28 | ui_sdl_gl(std::string title) 29 | : geometry_mesh_size_(0), 30 | angles_last_read_ms_(misc::get_time_ms()) 31 | { 32 | // 33 | // init SDL 34 | // 35 | 36 | if (SDL_Init(SDL_INIT_VIDEO) != 0) 37 | throw std::runtime_error(std::string("error, SDL_Init(): ") + SDL_GetError()); 38 | 39 | window_ = SDL_CreateWindow( 40 | title.c_str(), 41 | SDL_WINDOWPOS_CENTERED, 42 | SDL_WINDOWPOS_CENTERED, 43 | WINDOW_WIDTH, 44 | WINDOW_HEIGHT, 45 | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN); 46 | 47 | if (!window_) 48 | throw std::runtime_error(std::string("error, SDL_CreateWindow(): ") + SDL_GetError()); 49 | 50 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); 51 | SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); 52 | SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); 53 | 54 | context_ = SDL_GL_CreateContext(window_); 55 | 56 | if (!context_) 57 | throw std::runtime_error(std::string("error, SDL_GL_CreateContext(): ") + SDL_GetError()); 58 | 59 | // 60 | // init glew 61 | // 62 | 63 | glewExperimental = true; 64 | 65 | if (glewInit() != GLEW_OK) 66 | throw std::runtime_error(std::string("error, glewInit() failed")); 67 | 68 | // 69 | // init opengl and shader program 70 | // 71 | 72 | program_id_ = create_shader_program("vertex_shader.glsl", "fragment_shader.glsl"); 73 | 74 | if (!program_id_) 75 | throw std::runtime_error(std::string("error when building shader program")); 76 | 77 | glUseProgram(program_id_); 78 | glEnable(GL_DEPTH_TEST); 79 | glEnable(GL_BLEND); 80 | glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); 81 | glCullFace(GL_BACK); 82 | glClearColor(0.1, 0.1, 0.1, 0); 83 | 84 | uni_model_id_ = glGetUniformLocation(program_id_, "model"); 85 | uni_view_id_ = glGetUniformLocation(program_id_, "view"); 86 | uni_projection_id_ = glGetUniformLocation(program_id_, "projection"); 87 | uni_color_id_ = glGetUniformLocation(program_id_, "color"); 88 | 89 | projection_matrix_ = 90 | glm::perspective(70.0f, (float)WINDOW_WIDTH / (float)WINDOW_HEIGHT, 0.1f, 100.0f); 91 | 92 | glUniformMatrix4fv(uni_projection_id_, 1, GL_FALSE, &projection_matrix_[0][0]); 93 | 94 | // 95 | // get geometry data 96 | // 97 | 98 | std::vector model_vertices, model_normals; 99 | 100 | if(!get_geometry_data_from_file("mask.obj", model_vertices, model_normals)) 101 | throw std::runtime_error(std::string("error when loading model geometry")); 102 | 103 | geometry_mesh_size_ = model_vertices.size(); 104 | 105 | // 106 | // upload geometry 107 | // 108 | 109 | // create vertex array object 110 | GLuint vao; 111 | glGenVertexArrays(1, &vao); 112 | glBindVertexArray(vao); 113 | 114 | // create vertex buffer objects 115 | GLuint vbo[2]; 116 | glGenBuffers(2, vbo); 117 | 118 | // put vertices in first buffer 119 | glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); 120 | glBufferData(GL_ARRAY_BUFFER, model_vertices.size() * sizeof(GLfloat), &model_vertices[0], 121 | GL_STATIC_DRAW); 122 | glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); 123 | glEnableVertexAttribArray(0); 124 | 125 | // put normals in second buffer 126 | glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); 127 | glBufferData(GL_ARRAY_BUFFER, model_normals.size() * sizeof(GLfloat), &model_normals[0], 128 | GL_STATIC_DRAW); 129 | glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, 0); 130 | glEnableVertexAttribArray(1); 131 | } 132 | 133 | ~ui_sdl_gl() { 134 | SDL_DestroyWindow(window_); 135 | SDL_GL_DeleteContext(context_); 136 | SDL_Quit(); 137 | } 138 | 139 | virtual void poll_events() { 140 | events_.resize(64); 141 | 142 | int events_returned = SDL_PeepEvents( 143 | &events_.front(), 144 | events_.capacity(), 145 | SDL_GETEVENT, 146 | SDL_FIRSTEVENT, 147 | SDL_LASTEVENT); 148 | 149 | if (events_returned == -1) { 150 | INFO("error, SDL_PeepEvents(): " << SDL_GetError()); 151 | events_returned = 0; 152 | } 153 | 154 | events_.resize(events_returned); 155 | 156 | // show/hide cursor 157 | for (auto& e : events_) { 158 | if (e.type != SDL_WINDOWEVENT) 159 | continue; 160 | 161 | if (e.window.event == SDL_WINDOWEVENT_FOCUS_LOST) 162 | SDL_ShowCursor(SDL_TRUE); 163 | 164 | if (e.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) 165 | SDL_ShowCursor(SDL_FALSE); 166 | } 167 | } 168 | 169 | virtual bool check_event_quit() { 170 | for (auto& e : events_) 171 | if (e.type == SDL_QUIT) 172 | return true; 173 | 174 | return false; 175 | } 176 | 177 | virtual bool check_event_button_released(keyboard::button button) { 178 | for (auto& e : events_) { 179 | if (e.type != SDL_KEYUP) 180 | continue; 181 | 182 | int code = e.key.keysym.scancode; 183 | 184 | for (int i = 1; i != keyboard::button::end; i <<= 1) { 185 | std::vector codes = get_sdl_scancodes(i); 186 | 187 | for (auto &c : codes) 188 | if (code == c && i == button) 189 | return true; 190 | } 191 | } 192 | 193 | return false; 194 | } 195 | 196 | virtual int get_pressed_buttons() { 197 | SDL_PumpEvents(); 198 | const Uint8* state = SDL_GetKeyboardState(nullptr); 199 | 200 | int pressed = 0; 201 | 202 | for (int i = 1; i != keyboard::button::end; i <<= 1) { 203 | std::vector codes = get_sdl_scancodes(i); 204 | 205 | for (auto& c : codes) 206 | if (state[c]) 207 | pressed |= i; 208 | } 209 | 210 | return pressed; 211 | } 212 | 213 | virtual void get_delta_angles(float& horizontal, float& vertical) { 214 | // if window is not in focus 215 | if (!(SDL_GetWindowFlags(window_) & SDL_WINDOW_MOUSE_FOCUS) || 216 | !(SDL_GetWindowFlags(window_) & SDL_WINDOW_INPUT_FOCUS)) { 217 | horizontal = vertical = 0.0; 218 | angles_last_read_ms_ = misc::get_time_ms(); 219 | 220 | return; 221 | } 222 | 223 | int x, y; 224 | SDL_GetMouseState(&x, &y); 225 | 226 | float delta_time = (float) (misc::get_time_ms() - angles_last_read_ms_) / 1000; 227 | horizontal = 0.05 * delta_time * float(WINDOW_WIDTH / 2 - x); 228 | vertical = 0.05 * delta_time * float(WINDOW_HEIGHT / 2 - y); 229 | 230 | SDL_WarpMouseInWindow(window_, WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2); 231 | angles_last_read_ms_ = misc::get_time_ms(); 232 | } 233 | 234 | virtual void draw_clear() { 235 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 236 | } 237 | 238 | virtual void draw_world(world& world, uint8_t perspective_player_id, bool draw_phantoms) { 239 | // get player to render scene for 240 | boost::optional cam_player = world.get_player(perspective_player_id); 241 | 242 | glm::vec3 cam_position = glm::vec3( 243 | cam_player.get().get_x(), 244 | cam_player.get().get_y(), 245 | cam_player.get().get_z()); 246 | 247 | glm::vec3 cam_direction( 248 | cos(cam_player.get().get_vert_angel()) * cos(cam_player.get().get_horz_angel()), 249 | sin(cam_player.get().get_vert_angel()), 250 | -(cos(cam_player.get().get_vert_angel()) * sin(cam_player.get().get_horz_angel()))); 251 | 252 | // set view matrix 253 | glm::mat4 view = glm::lookAt(cam_position, cam_position + cam_direction, glm::vec3(0, 1, 0)); 254 | 255 | glUniformMatrix4fv(uni_view_id_, 1, GL_FALSE, &view[0][0]); 256 | 257 | // draw rest of players 258 | for (player& p : world.get_players()) { 259 | if (p.get_id() == perspective_player_id) 260 | continue; 261 | 262 | // set player color 263 | glm::vec4 color = glm::vec4( 264 | (p.get_color_AABBGGRR() & 0xff) / 255.f, 265 | ((p.get_color_AABBGGRR() >> 8) & 0xff) / 255.f, 266 | ((p.get_color_AABBGGRR() >> 16) & 0xff) / 255.f, 267 | (draw_phantoms) ? 0.2 : 1.0); 268 | 269 | glUniform4fv(uni_color_id_, 1, &color[0]); 270 | 271 | // set model matrix 272 | glm::mat4 model = 273 | glm::translate(glm::mat4(1.0), glm::vec3(p.get_x(), p.get_y(), p.get_z())) * 274 | glm::rotate(glm::mat4(1.0), p.get_horz_angel(), glm::vec3(0, 1, 0)) * 275 | glm::rotate(glm::mat4(1.0), p.get_vert_angel(), glm::vec3(0, 0, 1)); 276 | 277 | glUniformMatrix4fv(uni_model_id_, 1, GL_FALSE, &model[0][0]); 278 | 279 | // draw player 280 | glDrawArrays(GL_TRIANGLES, 0, geometry_mesh_size_ / 2); 281 | } 282 | } 283 | 284 | virtual void draw_update() { 285 | SDL_GL_SwapWindow(window_); 286 | } 287 | 288 | private: 289 | std::vector get_sdl_scancodes(int button) { 290 | switch (button) { 291 | case keyboard::button::up: return std::vector { SDL_SCANCODE_SPACE }; 292 | case keyboard::button::down: return std::vector { SDL_SCANCODE_LCTRL }; 293 | case keyboard::button::forward: return std::vector { SDL_SCANCODE_UP, 294 | SDL_SCANCODE_W }; 295 | case keyboard::button::backward: return std::vector { SDL_SCANCODE_DOWN, 296 | SDL_SCANCODE_S }; 297 | case keyboard::button::left: return std::vector { SDL_SCANCODE_LEFT }; 298 | case keyboard::button::right: return std::vector { SDL_SCANCODE_RIGHT }; 299 | case keyboard::button::step_left: return std::vector { SDL_SCANCODE_A }; 300 | case keyboard::button::step_right: return std::vector { SDL_SCANCODE_D }; 301 | case keyboard::button::quit: return std::vector { SDL_SCANCODE_ESCAPE }; 302 | case keyboard::button::f1: return std::vector { SDL_SCANCODE_F1 }; 303 | case keyboard::button::f2: return std::vector { SDL_SCANCODE_F2 }; 304 | } 305 | 306 | return std::vector(); 307 | } 308 | 309 | bool get_geometry_data_from_file(const char* file, std::vector& vertices, 310 | std::vector& normals) { 311 | std::ifstream file_stream(file); 312 | 313 | if (!file_stream.is_open()) { 314 | INFO("error, could not open file: " << file); 315 | return false; 316 | } 317 | 318 | std::vector vertex_lines, normal_lines; 319 | 320 | while (file_stream.good()) { 321 | std::string line; 322 | getline(file_stream, line); 323 | 324 | // read all strings holding vertices and normals 325 | if (line[0] == 'v') { 326 | if (line[1] == 'n') 327 | normal_lines.push_back(line); 328 | else 329 | vertex_lines.push_back(line); 330 | 331 | continue; 332 | } 333 | 334 | // read strings holding faces and put there values into argument vectors 335 | if (line[0] == 'f') { 336 | std::vector edges; 337 | boost::split(edges, line, boost::is_any_of(" ")); // split "f 1//1 2//2 4//3" 338 | 339 | for (size_t i = 1; i < edges.size(); i++) { 340 | std::vector indices; 341 | boost::split(indices, edges[i], boost::is_any_of("/")); // split "1//1" 342 | 343 | GLfloat temp; 344 | 345 | std::istringstream vertex_stream(vertex_lines[stoi(indices[0]) - 1]); 346 | vertex_stream.ignore(32, ' '); // skip "v" 347 | while (vertex_stream >> temp) 348 | vertices.push_back(temp); 349 | 350 | std::istringstream normal_stream(normal_lines[stoi(indices[2]) - 1]); 351 | normal_stream.ignore(32, ' '); // skip "vn" 352 | while (normal_stream >> temp) 353 | normals.push_back(temp); 354 | } 355 | } 356 | } 357 | 358 | file_stream.close(); 359 | 360 | return true; 361 | } 362 | 363 | GLuint create_shader_program(const char* vertex_file_path, const char* fragment_file_path) { 364 | std::string vertex_shader_code = misc::get_file_content(vertex_file_path); 365 | if (vertex_shader_code.empty()) { 366 | INFO("error, got no data from: " << vertex_file_path); 367 | return 0; 368 | } 369 | 370 | std::string fragment_shader_code = misc::get_file_content(fragment_file_path); 371 | if (fragment_shader_code.empty()) { 372 | INFO("error, got no data from: " << fragment_file_path); 373 | return 0; 374 | } 375 | 376 | GLuint vertex_shader_id = glCreateShader(GL_VERTEX_SHADER); 377 | GLuint fragment_shader_id = glCreateShader(GL_FRAGMENT_SHADER); 378 | 379 | GLint result = GL_FALSE; 380 | int info_log_length; 381 | 382 | // compile vertex shader 383 | char const* vertex_source_ptr = vertex_shader_code.c_str(); 384 | glShaderSource(vertex_shader_id, 1, &vertex_source_ptr , NULL); 385 | glCompileShader(vertex_shader_id); 386 | 387 | // check vertex shader 388 | glGetShaderiv(vertex_shader_id, GL_COMPILE_STATUS, &result); 389 | if (result != GL_TRUE) { 390 | glGetShaderiv(vertex_shader_id, GL_INFO_LOG_LENGTH, &info_log_length); 391 | std::vector msg(info_log_length); 392 | glGetShaderInfoLog(vertex_shader_id, info_log_length, NULL, &msg[0]); 393 | INFO("error, compiling vertex shader: " << &msg[0]); 394 | return 0; 395 | } 396 | 397 | // compile fragment shader 398 | char const* fragment_source_ptr = fragment_shader_code.c_str(); 399 | glShaderSource(fragment_shader_id, 1, &fragment_source_ptr , NULL); 400 | glCompileShader(fragment_shader_id); 401 | 402 | // check fragment shader 403 | glGetShaderiv(fragment_shader_id, GL_COMPILE_STATUS, &result); 404 | if (result != GL_TRUE) { 405 | glGetShaderiv(fragment_shader_id, GL_INFO_LOG_LENGTH, &info_log_length); 406 | std::vector msg(info_log_length); 407 | glGetShaderInfoLog(fragment_shader_id, info_log_length, NULL, &msg[0]); 408 | INFO("error, compiling fragment shader: " << &msg[0]); 409 | glDeleteShader(vertex_shader_id); 410 | return 0; 411 | } 412 | 413 | // link program 414 | GLuint program_id = glCreateProgram(); 415 | glAttachShader(program_id, vertex_shader_id); 416 | glAttachShader(program_id, fragment_shader_id); 417 | 418 | glLinkProgram(program_id); 419 | 420 | glDeleteShader(vertex_shader_id); 421 | glDeleteShader(fragment_shader_id); 422 | 423 | // check program 424 | glGetProgramiv(program_id, GL_LINK_STATUS, &result); 425 | if (result != GL_TRUE) { 426 | glGetProgramiv(program_id, GL_INFO_LOG_LENGTH, &info_log_length); 427 | std::vector msg(std::max(info_log_length, int(1))); 428 | glGetProgramInfoLog(program_id, info_log_length, NULL, &msg[0]); 429 | INFO("error, linking shaders: " << &msg[0]); 430 | return 0; 431 | } 432 | 433 | return program_id; 434 | } 435 | 436 | // sdl 437 | SDL_Window* window_; 438 | SDL_GLContext context_; 439 | std::vector events_; 440 | 441 | // gl 442 | GLuint program_id_; 443 | GLuint uni_model_id_; 444 | GLuint uni_view_id_; 445 | GLuint uni_projection_id_; 446 | GLuint uni_color_id_; 447 | 448 | // other 449 | size_t geometry_mesh_size_; 450 | uint64_t angles_last_read_ms_; 451 | glm::mat4 projection_matrix_; 452 | }; 453 | 454 | #endif // UI_SDL_GL_HPP_ 455 | -------------------------------------------------------------------------------- /vertex_shader.glsl: -------------------------------------------------------------------------------- 1 | #version 330 2 | 3 | in vec3 in_position; 4 | in vec3 in_normal; 5 | 6 | out vec4 normal; 7 | 8 | uniform mat4 model; 9 | uniform mat4 view; 10 | uniform mat4 projection; 11 | 12 | void main(void) { 13 | normal = model * vec4(in_normal, 0.0); 14 | 15 | gl_Position = projection * view * model * vec4(in_position, 1.0); 16 | } 17 | -------------------------------------------------------------------------------- /world.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WORLD_HPP_ 2 | #define WORLD_HPP_ 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "command.hpp" 11 | #include "player.hpp" 12 | 13 | class world { 14 | public: 15 | static const uint8_t CLASS_ID = 2; 16 | 17 | static const int WIDTH = 5; // meter 18 | static const int HEIGTH = 5; // meter 19 | 20 | bool add_player(player& player) { 21 | uint8_t player_id = generate_player_id(); 22 | 23 | if (!player_id) 24 | return false; 25 | 26 | player.set_id(player_id); 27 | assign_random_position(player); 28 | players_.push_back(player); 29 | 30 | return true; 31 | } 32 | 33 | void remove_player(uint8_t player_id) { 34 | auto i = players_.begin(); 35 | 36 | while (i != players_.end()) { 37 | if (i->get_id() == player_id) { 38 | players_.erase(i); 39 | break; 40 | } 41 | i++; 42 | } 43 | } 44 | 45 | boost::optional get_player(uint8_t player_id) { 46 | boost::optional opt_player; 47 | 48 | for (auto& p : players_) { 49 | if (p.get_id() == player_id) { 50 | opt_player = p; 51 | break; 52 | } 53 | } 54 | 55 | return opt_player; 56 | } 57 | 58 | std::vector& get_players() { 59 | return players_; 60 | } 61 | 62 | bool player_exists(uint8_t player_id) const { 63 | return !player_id_is_free(player_id); 64 | } 65 | 66 | void run_command(const command& cmd, uint8_t player_id) { 67 | boost::optional opt_p = get_player(player_id); 68 | 69 | if (opt_p) 70 | opt_p.get().run_command(cmd); 71 | } 72 | 73 | private: 74 | friend class boost::serialization::access; 75 | 76 | template 77 | void serialize(Archive& ar, const unsigned int version) { 78 | ar & players_; 79 | } 80 | 81 | bool player_id_is_free(uint8_t id) const { 82 | for (const player& p : players_) 83 | if (p.get_id() == id) 84 | return false; 85 | 86 | return true; 87 | } 88 | 89 | void assign_random_position(player& player) { 90 | std::random_device rd; 91 | std::mt19937_64 gen(rd()); 92 | 93 | std::uniform_int_distribution dis1(-WIDTH, WIDTH); 94 | player.set_x(dis1(gen)); 95 | 96 | std::uniform_int_distribution dis2(-HEIGTH, HEIGTH); 97 | player.set_z(dis2(gen)); 98 | } 99 | 100 | uint8_t generate_player_id() const { 101 | uint8_t new_id = 1; 102 | 103 | while (new_id) { 104 | if (player_id_is_free(new_id)) 105 | return new_id; 106 | new_id++; 107 | } 108 | 109 | return 0; 110 | } 111 | 112 | std::vector players_; 113 | }; 114 | 115 | #endif // WORLD_HPP_ 116 | --------------------------------------------------------------------------------