ungroup/ungroup_game

View on GitHub
src/client/systems/ClientGameController.cpp

Summary

Maintainability
Test Coverage
#include <cmath>
#include <iostream>
#include <numeric>
#include <thread>

#include <SFML/Graphics.hpp>

#include "../../common/events/CollisionEvent.hpp"
#include "../../common/events/EventController.hpp"
#include "../../common/physics/VectorUtil.hpp"
#include "../../common/util/InputDef.hpp"
#include "../../common/util/StateDef.hpp"
#include "../../common/util/game_settings.hpp"
#include "ClientGameController.hpp"

ClientGameController::ClientGameController(bool is_headless, bool is_bot, BotStrategy strategy,
                                           const std::string& server_ip, uint32_t server_tcp_port,
                                           LevelKey level_key) :
    m_headless(is_headless),
    m_isBot(is_bot), m_strategy(strategy), m_serverIP(server_ip), GameController(level_key),
    m_networkingClient(new NetworkingClient(m_serverIP, server_tcp_port)) {
}

ClientGameController::~ClientGameController() {
}

void ClientGameController::start() {
    m_gameStateCore.status = GameStatus::not_started;
    registerClient();
    postRegisterInit();
    while (m_window.isOpen()) {
        step();
        draw();
        if (m_headless) {
            // unfortunately, the game will buffer up a ton of
            // non-relevant moves, unless we slow it down. previously
            // the draw was slowing it.
            std::this_thread::sleep_for(std::chrono::milliseconds(50));
        }
    }
}

void ClientGameController::registerClient() {
    std::cout << "Registering client with server..." << std::endl;
    m_playerId = m_networkingClient->registerClientAndFetchPlayerId();
}

void ClientGameController::postRegisterInit() {
    m_window.create(
        sf::VideoMode(RenderingDef::WINDOW_RESOLUTION.x, RenderingDef::WINDOW_RESOLUTION.y),
        "Ungroup", sf::Style::Close);
    m_renderingController = std::unique_ptr<RenderingController>(new RenderingController(
        m_window, *m_gameObjectController, *m_gameObjectStore, m_playerId, m_headless));
    m_inputController = std::unique_ptr<InputController>(new InputController(INPUT_KEYS));
}

void ClientGameController::incrementTick() {
    m_networkingClient->incrementTick();
}

void ClientGameController::draw() {
    m_renderingController->draw();
}

InputDef::PlayerInputs ClientGameController::getPlayerInputs() {
    return m_inputController->getPlayerInputs(m_playerId);
}

std::pair<InputDef::ReliableInput, InputDef::UnreliableInput>
ClientGameController::getBotMove(uint32_t bot_player_id, BotStrategy strategy) {
    return m_bot.getMove(bot_player_id, strategy, *m_gameObjectController);
}

void ClientGameController::preUpdate() {
    auto inputs = m_inputController->collectInputs(m_window);

    switch (m_gameStateCore.status) {
        case GameStatus::not_started: {
            // Keep fetching game state to check if game status changed from not_started
            std::map<uint32_t, GameState> game_state_buffer =
                m_networkingClient->getGameStateBuffer();
            if (game_state_buffer.size() == GAME_SETTINGS.CLIENT_GAME_STATE_BUFFER_SIZE) {
                m_gameStateCore = game_state_buffer.begin()->second.core;
            }
            break;
        }
        case GameStatus::playing: {
            if (m_isBot) {
                // Override user input with bot input
                inputs = getBotMove(m_playerId, m_strategy);
            }
            sendInputs(inputs);
            break;
        }
        case GameStatus::game_over: {
            // noop
            break;
        }
    }

    m_renderingController->preUpdate();
}

void ClientGameController::update(const InputDef::PlayerInputs& pi, sf::Int32 delta_ms) {
    switch (m_gameStateCore.status) {
        case GameStatus::not_started: {
            // noop
            break;
        }
        case GameStatus::playing: {
            updateGameState(pi, delta_ms);
            break;
        }
        case GameStatus::game_over: {
            // noop
            break;
        }
    }

    m_renderingController->update(delta_ms);
    m_tickDeltaMetric.update();
    m_behindGameStateMetric.update();
    m_aheadGameStateMetric.update();
    m_noGameStateMetric.update();
    m_interpolateGameStateMetric.update();
    m_stallGameStateMetric.update();
}

void ClientGameController::postUpdate(sf::Int32 update_time) {
    UIData ui_data = {
        .game_steps_per_second = m_gameStepMetric.getRate(sf::seconds(1)),
        .game_updates_per_second = m_gameUpdateMetric.getRate(sf::seconds(1)),
        .server_state_per_second = m_networkingClient->getServerGameStateRate(sf::seconds(1)),
        .tick_delta_average = m_tickDeltaMetric.getAverage(),
        .behind_game_state_rate = m_behindGameStateMetric.getRate(sf::seconds(1)),
        .ahead_game_state_rate = m_aheadGameStateMetric.getRate(sf::seconds(1)),
        .no_game_state_rate = m_noGameStateMetric.getRate(sf::seconds(1)),
        .interpolate_distance_average = m_interpolateGameStateMetric.getAverage(),
        .stall_distance_average = m_stallGameStateMetric.getAverage(),
        .resources = m_gameObjectController->getPlayerResources(m_playerId),
        .resource_goals = m_gameObjectController->getPlayerController()
                              .getPlayer(m_playerId)
                              ->getWinCondition()
                              .getResourceCountsToWin(),
        .game_status = m_gameStateCore.status,
        .winner_player_id = m_gameStateCore.winner_player_id,
        .tick = getTick(),
        .tick_duration_ms = GAME_SETTINGS.MIN_TIME_STEP_SEC * 1000,
    };
    m_renderingController->postUpdate(update_time, ui_data);
}

void ClientGameController::updateGameState(const InputDef::PlayerInputs& pi, sf::Int32 delta_ms) {

    std::map<uint32_t, GameState> game_state_buffer = m_networkingClient->getGameStateBuffer();

    if (game_state_buffer.empty()) {
        return;
    }

    uint32_t client_tick = getTick();
    uint32_t smallest_server_tick = game_state_buffer.begin()->first;
    const GameState& smallest_server_game_state = game_state_buffer.begin()->second;

    const GameState& largest_server_game_state = game_state_buffer.rbegin()->second;
    if (largest_server_game_state.core.status == GameStatus::game_over) {
        m_gameStateCore = largest_server_game_state.core;
        return;
    }

    if (client_tick < smallest_server_tick) {
        behind(client_tick, smallest_server_tick, smallest_server_game_state);
        m_behindGameStateMetric.pushCount();
    } else {
        ahead(client_tick, smallest_server_tick, game_state_buffer);
        m_aheadGameStateMetric.pushCount();
    }
    m_tickDeltaMetric.pushCount(std::abs(static_cast<int>(smallest_server_tick - client_tick)));
}

/**
 * If client is too far behind smallest server game state in the buffer, jump to server
 * game state, otherwise interpolate the game state of the next tick.
 */
void ClientGameController::behind(uint32_t client_tick, uint32_t smallest_server_tick,
                                  const GameState& smallest_server_game_state) {
    uint32_t next_tick;
    if (smallest_server_tick - client_tick > GAME_SETTINGS.BEHIND_TICK_DELTA_THRESHOLD) {
        next_tick = smallest_server_tick - GAME_SETTINGS.BEHIND_TICK_DELTA_THRESHOLD;

    } else {
        next_tick = client_tick + 1;
    }
    interpolateGameState(client_tick, next_tick, smallest_server_tick, smallest_server_game_state);
}

/**
 * If client is ahead of the smallest server game state in the buffer, stay at the same tick but
 * interpolate to the next available game state in the buffer. If there are none available just keep
 * the game state the same. Note that the tick stays the same in this case so that we fall back
 * behind to being behind the smallest server game state in the buffer.
 */
void ClientGameController::ahead(uint32_t client_tick, uint32_t smallest_server_tick,
                                 std::map<uint32_t, GameState>& game_state_buffer) {
    std::map<uint32_t, GameState>::iterator game_state_buffer_it = game_state_buffer.begin();
    ++game_state_buffer_it;
    while (game_state_buffer_it != game_state_buffer.end()) {
        uint32_t next_server_tick = game_state_buffer_it->first;
        if (client_tick < next_server_tick) {
            const GameState& next_server_game_state = game_state_buffer_it->second;
            uint32_t next_tick = client_tick;
            // Keep the same tick but interpolate to the next available server game state.
            interpolateGameState(client_tick - 1, next_tick, next_server_tick,
                                 next_server_game_state);
            m_stallGameStateMetric.pushCount(client_tick - smallest_server_tick);
            return;
        }
        ++game_state_buffer_it;
    }

    // If we reach here then we don't have any game states to interpolate to. Keep the same tick and
    // game state.
    m_stallGameStateMetric.pushCount(client_tick - smallest_server_tick);
    return;
}

void ClientGameController::interpolateGameState(uint32_t start_tick, uint32_t to_tick,
                                                uint32_t end_tick, const GameState& game_state) {
    if (to_tick < start_tick || end_tick < to_tick) {
        throw std::runtime_error("Can't interpolate to the past.");
    }

    if (to_tick == end_tick) {
        m_gameObjectController->applyGameStateObject(game_state.object);
        m_gameStateCore = game_state.core;
    } else {
        float a =
            static_cast<float>(to_tick - start_tick) / static_cast<float>(end_tick - start_tick);
        m_gameStateCore = game_state.core;
        sf::Int32 delta_ms = MIN_TIME_STEP * (to_tick - start_tick);
        m_gameObjectController->interpolateGameStateObject(game_state.object, a, delta_ms);
    }
    applyGameStateEvents(game_state.event);
    setTick(to_tick);
    m_interpolateGameStateMetric.pushCount(end_tick - to_tick);
}

void ClientGameController::applyGameStateEvents(const GameStateEvent& game_state_event) {
    for (const auto& collision : game_state_event.collisions) {
        EventController::getInstance().forceQueueEvent(
            std::move(std::unique_ptr<CollisionEvent>(new CollisionEvent(collision))));
    }
    EventController::getInstance().forceProcessEvents();
}

void ClientGameController::sendInputs(
    std::pair<InputDef::ReliableInput, InputDef::UnreliableInput> inputs) {
    if (!inputs.first.allFalse()) {
        m_networkingClient->pushReliableInput(inputs.first);
    }
    if (!inputs.second.allFalse()) {
        m_networkingClient->pushUnreliableInput(inputs.second);
    }
}

uint32_t ClientGameController::getTick() {
    return m_networkingClient->getTick();
}

void ClientGameController::setTick(uint32_t tick) {
    m_networkingClient->setTick(tick);
}