#include #include #include #include #include #include #include "game_client.h" #include "bettola/game/player_base.h" #include "game/remote_player.h" #include "bettola/math/vec3.h" #include "bettola/network/chunk_message.h" #include "bettola/network/game_state_message.h" #include "bettola/network/message.h" GameClient::GameClient(void) : _our_player_id(0), _input_sequence_number(0) { memset(&_server_addr, 0, sizeof(_server_addr)); } bool GameClient::connect(const char* host, unsigned short port) { if(!_tcp_socket.create() || !_tcp_socket.connect(host,port)) { perror("BettolaClient: Failed to connect TCP socket.\n"); return false; } if(!_udp_socket.create()) { perror("BettolaClient: Failed to create UDP socket.\n"); return false; } /* Setup server address. */ _server_addr.sin_family = AF_INET; _server_addr.sin_port = htons(port); inet_pton(AF_INET, host, &_server_addr.sin_addr); /* Set the socket to non-blocking. */ int tcp_flags = fcntl(_tcp_socket.get_sockfd(), F_GETFL, 0); fcntl(_tcp_socket.get_sockfd(), F_SETFL, tcp_flags | O_NONBLOCK); int udp_flags = fcntl(_udp_socket.get_sockfd(), F_GETFL, 0); fcntl(_udp_socket.get_sockfd(), F_SETFL, udp_flags | O_NONBLOCK); printf("BettolaClient: Connected to server at %s:%hu\n", host, port); return true; } void GameClient::process_network_messages(void) { fd_set read_fds; FD_ZERO(&read_fds); int tcp_fd = _tcp_socket.get_sockfd(); int udp_fd = _udp_socket.get_sockfd(); FD_SET(tcp_fd, &read_fds); FD_SET(udp_fd, &read_fds); int max_fd = std::max(tcp_fd, udp_fd); struct timeval tv = {0,0}; /* Non-blocking. */ if(select(max_fd+1, &read_fds, nullptr, nullptr, &tv) > 0) { if(FD_ISSET(tcp_fd, &read_fds)) { /* Drain all data from the TCP socket. */ while(true) { BettolaLib::Network::MessageHeader header; ssize_t header_bytes = _tcp_socket.recv(&header, sizeof(header)); if(header_bytes <= 0) { /* No more data, or an error? */ break; } if(header.type == BettolaLib::Network::MessageType::PlayerId) { if(header.size == sizeof(unsigned int)) { _tcp_socket.recv(&_our_player_id, sizeof(_our_player_id)); printf("BettolaClient: Assigned player ID %u\n", _our_player_id); } } else if(header.type == BettolaLib::Network::MessageType::ChunkData) { if(header.size == sizeof(BettolaLib::Network::ChunkMessage)) { BettolaLib::Network::ChunkMessage msg; _tcp_socket.recv(&msg, sizeof(msg)); _world.add_chunk(msg.chunk_x, msg.chunk_z, msg.heightmap, msg.detailmap); } } else { /* Discard message payloads we don't understand to prevent de-sync. */ char discard_buffer[4096]; if(header.size > 0 && header.size < sizeof(discard_buffer)) { _tcp_socket.recv(discard_buffer, header.size); } } } } if(FD_ISSET(udp_fd, &read_fds)) { _process_udp_messages(); } } } void GameClient::_process_udp_messages(void) { char buffer[1024]; sockaddr_in from_addr; ssize_t bytes_received; while((bytes_received = _udp_socket.recv_from(buffer,sizeof(buffer),from_addr))>0) { if(bytes_received < sizeof(BettolaLib::Network::MessageHeader)) continue; BettolaLib::Network::MessageHeader header; memcpy(&header, buffer, sizeof(header)); const char* payload = buffer + sizeof(BettolaLib::Network::MessageHeader); size_t payload_size = bytes_received-sizeof(BettolaLib::Network::MessageHeader); if(header.type == BettolaLib::Network::MessageType::GameState) { if(payload_size >= sizeof(BettolaLib::Network::GameStateMessage)) { BettolaLib::Network::GameStateMessage msg; memcpy(&msg, payload, sizeof(msg)); _process_game_state(msg); } } } } void GameClient::_process_game_state(const BettolaLib::Network::GameStateMessage& msg) { /* Track remote players each update. */ std::set players_in_message; for(unsigned int i = 0; i < msg.num_players; ++i) { const auto& ps = msg.players[i]; if(ps.player_id == _our_player_id) { /* This is our player. Reconcile. */ _player.set_position({ps.x, ps.y, ps.z}); /* Remove all inputs from our history that the server has processed. */ while(!_pending_inputs.empty() && _pending_inputs.front().sequence_number <= ps.last_processed_sequence) { _pending_inputs.pop_front(); } /* Now, re-apply any remaining inputs that the server hasn't * seen yet. This'll bring the client up to date with most recent inputs. */ for(const auto& input_msg : _pending_inputs) { PlayerBase::InputState input_state = { input_msg.up, input_msg.down, input_msg.left, input_msg.right }; BettolaMath::Vec3 cam_front = { input_msg.cam_front_x, input_msg.cam_front_y, input_msg.cam_front_z }; _player.set_velocity_direction(input_state, cam_front); _player.update(input_msg.dt); } } else { /* Remote player. Find if we already know about them.. */ players_in_message.insert(ps.player_id); auto it = std::find_if(_remote_players.begin(), _remote_players.end(), [&](const RemotePlayer&p) {return p.get_id() == ps.player_id; }); if(it != _remote_players.end()) { /* Found 'em! update their target pos. */ it->set_target_position(ps.x, ps.y, ps.z, ps.yaw); } else { /* They are new, add them to our list. */ _remote_players.emplace_back(ps.player_id, ps.x, ps.y, ps.z); } } } _remote_players.erase(std::remove_if(_remote_players.begin(), _remote_players.end(), [&](const RemotePlayer& p) { return players_in_message.find(p.get_id()) == players_in_message.end(); }), _remote_players.end()); } void GameClient::send_input(PlayerBase::InputState& input, const Camera& camera, float dt) { if(_our_player_id > 0) { /* Sent our current input state to the server. */ BettolaLib::Network::PlayerInputMessage input_msg; input_msg.player_id = _our_player_id; input_msg.sequence_number = ++_input_sequence_number; input_msg.up = input.up; input_msg.down = input.down; input_msg.left = input.left; input_msg.right = input.right; input_msg.dt = dt; input_msg.yaw = camera.get_yaw(); const auto& cam_front = camera.get_front(); input_msg.cam_front_x = cam_front.x; input_msg.cam_front_y = cam_front.y; input_msg.cam_front_z = cam_front.z; BettolaLib::Network::MessageHeader header; header.type = BettolaLib::Network::MessageType::PlayerInput; header.size = sizeof(input_msg); char buffer[sizeof(header) + sizeof(input_msg)]; memcpy(buffer, &header, sizeof(header)); memcpy(buffer+sizeof(header), &input_msg, sizeof(input_msg)); _udp_socket.send_to(buffer, sizeof(buffer), _server_addr); _pending_inputs.push_back(input_msg); /* Store for reconciliation. */ } } void GameClient::update_players(float dt) { _player.update(dt); float terrain_height = _world.get_height(_player.get_position().x, _player.get_position().z); _player.apply_gravity_and_collision(terrain_height); for(auto& p : _remote_players) { p.update(dt); } } const World& GameClient::get_world(void) const { return _world; }