/* * Copyright (C) 2017-2019 Christopher J. Howard * * This file is part of Antkeeper Source Code. * * Antkeeper Source Code is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Antkeeper Source Code is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Antkeeper Source Code. If not, see . */ #include "game.hpp" #include "resources/string-table.hpp" #include "states/game-state.hpp" #include "states/sandbox-state.hpp" #include "filesystem.hpp" #include "timestamp.hpp" #include "ui/ui.hpp" #include "graphics/ui-render-pass.hpp" #include "graphics/shadow-map-render-pass.hpp" #include "graphics/clear-render-pass.hpp" #include "graphics/sky-render-pass.hpp" #include "graphics/lighting-render-pass.hpp" #include "graphics/silhouette-render-pass.hpp" #include "graphics/final-render-pass.hpp" #include "resources/resource-manager.hpp" #include "resources/text-file.hpp" #include "game/camera-rig.hpp" #include "game/lens.hpp" #include "game/forceps.hpp" #include "game/brush.hpp" #include "entity/component-manager.hpp" #include "entity/components/transform-component.hpp" #include "entity/components/model-component.hpp" #include "entity/components/terrain-patch-component.hpp" #include "entity/entity-manager.hpp" #include "entity/entity-template.hpp" #include "entity/system-manager.hpp" #include "entity/systems/sound-system.hpp" #include "entity/systems/collision-system.hpp" #include "entity/systems/render-system.hpp" #include "entity/systems/camera-system.hpp" #include "entity/systems/tool-system.hpp" #include "entity/systems/locomotion-system.hpp" #include "entity/systems/behavior-system.hpp" #include "entity/systems/steering-system.hpp" #include "entity/systems/particle-system.hpp" #include "entity/systems/terrain-system.hpp" #include "configuration.hpp" #include "parameter-dict.hpp" #include "stb/stb_image_write.h" #include "menu.hpp" #include #include #include #include #include #include #include "debug/command-interpreter.hpp" #include "debug/logger.hpp" #include "debug/ansi-escape-codes.hpp" Game::Game(int argc, char* argv[]): currentState(nullptr), window(nullptr) { // Determine application name std::string applicationName; #if defined(_WIN32) applicationName = "Antkeeper"; #else applicationName = "antkeeper"; #endif // Form resource paths dataPath = getDataPath(applicationName) + "data/"; configPath = getConfigPath(applicationName); controlsPath = configPath + "controls/"; // Create nonexistent config directories std::vector configPaths; configPaths.push_back(configPath); configPaths.push_back(controlsPath); for (const std::string& path: configPaths) { if (!pathExists(path)) { createDirectory(path); } } // Setup resource manager resourceManager = new ResourceManager(); // Include resource search paths in order of priority resourceManager->include(controlsPath); resourceManager->include(configPath); resourceManager->include(dataPath); // Subscribe the game to scheduled function events eventDispatcher.subscribe(this); toggleFullscreenDisabled = false; sandboxState = new SandboxState(this); } Game::~Game() { if (window) { windowManager->destroyWindow(window); } } void Game::changeState(GameState* state) { if (currentState != nullptr) { currentState->exit(); } currentState = state; if (currentState != nullptr) { currentState->enter(); } } std::string Game::getString(const std::string& name, std::optional languageIndex) const { std::string value; if (languageIndex == std::nullopt) { languageIndex = currentLanguageIndex; } auto it = stringTableIndex.find(name); if (it != stringTableIndex.end()) { value = (*stringTable)[it->second][(*languageIndex) + 2]; if (value.empty()) { value = std::string("# EMPTY STRING: ") + name + std::string(" #"); } } else { value = std::string("# MISSING STRING: ") + name + std::string(" #"); } return value; } void Game::changeLanguage(std::size_t nextLanguageIndex) { // Get names of fonts std::string menuFontFilename = getString("menu-font-filename"); // Unload fonts delete menuFont; resourceManager->unload(menuFontFilename); // Change current language index currentLanguageIndex = nextLanguageIndex; // Reload fonts loadFonts(); // Set window title window->setTitle(getString("title").c_str()); // Repopulate UI element strings restringUI(); // Resize the UI resizeUI(w, h); // Reselect menu item if (currentMenuItem) { menuSelectorSlideAnimation.stop(); selectMenuItem(menuItemIndex, false); uiRootElement->update(); currentMenu->getContainer()->resetTweens(); } // Save settings language = getString("language-code"); saveSettings(); } void Game::nextLanguage() { changeLanguage((currentLanguageIndex + 1) % languageCount); } void Game::openMenu(Menu* menu, int selectedItemIndex) { if (currentMenu) { closeCurrentMenu(); } currentMenu = menu; uiRootElement->addChild(currentMenu->getContainer()); currentMenu->getContainer()->addChild(menuSelectorImage); currentMenu->getContainer()->setTintColor(Vector4(1.0f)); for (MenuItem* item: *currentMenu->getItems()) { item->getContainer()->setTintColor(menuItemInactiveColor); } selectMenuItem(selectedItemIndex, false); uiRootElement->update(); currentMenu->getContainer()->resetTweens(); } void Game::closeCurrentMenu() { uiRootElement->removeChild(currentMenu->getContainer()); currentMenu->getContainer()->removeChild(menuSelectorImage); currentMenu->getContainer()->setTintColor(Vector4(1.0f)); for (MenuItem* item: *currentMenu->getItems()) { item->getContainer()->setTintColor(menuItemInactiveColor); } currentMenu = nullptr; currentMenuItem = nullptr; menuItemIndex = -1; menuFadeAnimation.stop(); menuSelectorSlideAnimation.stop(); menuItemSelectAnimation.stop(); menuItemDeselectAnimation.stop(); previousMenu = currentMenu; currentMenu = nullptr; } void Game::selectMenuItem(int index, bool tween) { bool reselected = false; if (index != menuItemIndex) { if (menuItemSelectAnimation.isPlaying()) { menuItemSelectAnimation.stop(); currentMenuItem->getContainer()->setTintColor(menuItemActiveColor); } if (menuItemDeselectAnimation.isPlaying()) { menuItemDeselectAnimation.stop(); previousMenuItem->getContainer()->setTintColor(menuItemInactiveColor); } // Save previous menu item previousMenuItem = currentMenuItem; // Determine current menu item menuItemIndex = index; currentMenuItem = (*(currentMenu->getItems()))[index]; } else { reselected = true; } // Determine target position of menu item selector Vector2 itemTranslation = currentMenuItem->getContainer()->getPosition() - currentMenu->getContainer()->getPosition(); Vector2 itemDimensions = currentMenuItem->getContainer()->getDimensions(); float spacing = fontSizePX; Vector2 translation; translation.x = itemTranslation.x - menuSelectorImage->getDimensions().x - spacing; translation.y = itemTranslation.y + itemDimensions.y * 0.5f - menuSelectorImage->getDimensions().y * 0.5; // Create tween animations if (!reselected && tween && previousMenuItem != nullptr) { float tweenDuration = 0.2f; Vector2 oldTranslation = menuSelectorImage->getTranslation(); Vector2 newTranslation = translation; // Slide animation { menuSelectorSlideClip.removeChannels(); AnimationChannel* channel = menuSelectorSlideClip.addChannel(0); channel->insertKeyframe(0.0f, oldTranslation.y); channel->insertKeyframe(tweenDuration, newTranslation.y); menuSelectorSlideAnimation.setTimeFrame(menuSelectorSlideClip.getTimeFrame()); menuSelectorSlideAnimation.rewind(); menuSelectorSlideAnimation.play(); } // Color animations { menuItemSelectClip.removeChannels(); AnimationChannel* channel = menuItemSelectClip.addChannel(0); channel->insertKeyframe(0.0f, menuItemInactiveColor); channel->insertKeyframe(tweenDuration, menuItemActiveColor); menuItemSelectAnimation.setTimeFrame(menuItemSelectClip.getTimeFrame()); menuItemSelectAnimation.rewind(); menuItemSelectAnimation.play(); if (previousMenuItem) { menuItemDeselectClip.removeChannels(); channel = menuItemDeselectClip.addChannel(0); channel->insertKeyframe(0.0f, menuItemActiveColor); channel->insertKeyframe(tweenDuration, menuItemInactiveColor); menuItemDeselectAnimation.setTimeFrame(menuItemDeselectClip.getTimeFrame()); menuItemDeselectAnimation.rewind(); menuItemDeselectAnimation.play(); } } menuSelectorImage->setTranslation(Vector2(newTranslation.x, oldTranslation.y)); } else if (!tween) { menuSelectorImage->setTranslation(translation); currentMenuItem->getContainer()->setTintColor(menuItemActiveColor); if (previousMenuItem) { previousMenuItem->getContainer()->setTintColor(menuItemInactiveColor); } } } void Game::selectNextMenuItem() { int index = (menuItemIndex + 1) % currentMenu->getItems()->size(); selectMenuItem(index, true); } void Game::selectPreviousMenuItem() { int index = (menuItemIndex + (currentMenu->getItems()->size() - 1)) % currentMenu->getItems()->size(); selectMenuItem(index, true); } void Game::activateMenuItem() { currentMenuItem->activate(); } void Game::activateLastMenuItem() { if (currentMenu) { (*currentMenu->getItems())[currentMenu->getItems()->size() - 1]->activate(); } } void Game::toggleFullscreen() { if (!toggleFullscreenDisabled) { fullscreen = !(*fullscreen); window->setFullscreen(*fullscreen); if (!(*fullscreen)) { const Display* display = deviceManager->getDisplays()->front(); int displayWidth = std::get<0>(display->getDimensions()); int displayHeight = std::get<1>(display->getDimensions()); w = (*windowResolution)[0]; h = (*windowResolution)[1]; window->setDimensions(w, h); window->setPosition((*windowPosition)[0], (*windowPosition)[1]); } restringUI(); // Disable fullscreen toggles for 500ms toggleFullscreenDisabled = true; ScheduledFunctionEvent event; event.caller = static_cast(this); event.function = [this]() { toggleFullscreenDisabled = false; }; eventDispatcher.schedule(event, time + 0.5f); // Save settings saveSettings(); } } void Game::toggleVSync() { vsync = !(*vsync); window->setVSync(*vsync); restringUI(); // Save settings saveSettings(); } void Game::setUpdateRate(double frequency) { stepScheduler.setStepFrequency(frequency); } void Game::disableNonSystemControls() { controls.setCallbacksEnabled(false); systemControls.setCallbacksEnabled(true); } void Game::setup() { setupDebugging(); loadSettings(); setupLocalization(); setupWindow(); setupGraphics(); setupControls(); setupUI(); setupGameplay(); screenshotQueued = false; paused = false; // Load model resources try { lensModel = resourceManager->load("lens.mdl"); forcepsModel = resourceManager->load("forceps.mdl"); brushModel = resourceManager->load("brush.mdl"); smokeMaterial = resourceManager->load("smoke.mtl"); } catch (const std::exception& e) { logger->error("Failed to load one or more models: \"" + std::string(e.what()) + "\"\n"); close(EXIT_FAILURE); } time = 0.0f; // Tools currentTool = nullptr; lens = new Lens(lensModel, &animator); lens->setOrbitCam(orbitCam); worldScene->addObject(lens->getModelInstance()); worldScene->addObject(lens->getSpotlight()); lens->setSunDirection(-sunlightCamera.getForward()); // Forceps forceps = new Forceps(forcepsModel, &animator); forceps->setOrbitCam(orbitCam); worldScene->addObject(forceps->getModelInstance()); // Brush brush = new Brush(brushModel, &animator); brush->setOrbitCam(orbitCam); worldScene->addObject(brush->getModelInstance()); // Initialize component manager componentManager = new ComponentManager(); // Initialize entity manager entityManager = new EntityManager(componentManager); // Initialize systems soundSystem = new SoundSystem(componentManager); collisionSystem = new CollisionSystem(componentManager); cameraSystem = new CameraSystem(componentManager); renderSystem = new RenderSystem(componentManager, worldScene); toolSystem = new ToolSystem(componentManager); toolSystem->setPickingCamera(&camera); toolSystem->setPickingViewport(Vector4(0, 0, w, h)); eventDispatcher.subscribe(toolSystem); behaviorSystem = new BehaviorSystem(componentManager); steeringSystem = new SteeringSystem(componentManager); locomotionSystem = new LocomotionSystem(componentManager); terrainSystem = new TerrainSystem(componentManager); terrainSystem->setPatchSize(500.0f); particleSystem = new ParticleSystem(componentManager); particleSystem->resize(1000); particleSystem->setMaterial(smokeMaterial); particleSystem->setDirection(Vector3(0, 1, 0)); lens->setParticleSystem(particleSystem); particleSystem->getBillboardBatch()->setAlignment(&camera, BillboardAlignmentMode::SPHERICAL); worldScene->addObject(particleSystem->getBillboardBatch()); // Initialize system manager systemManager = new SystemManager(); systemManager->addSystem(soundSystem); systemManager->addSystem(behaviorSystem); systemManager->addSystem(steeringSystem); systemManager->addSystem(locomotionSystem); systemManager->addSystem(collisionSystem); systemManager->addSystem(toolSystem); systemManager->addSystem(terrainSystem); systemManager->addSystem(particleSystem); systemManager->addSystem(cameraSystem); systemManager->addSystem(renderSystem); // Load navmesh TriangleMesh* navmesh = resourceManager->load("sidewalk.mesh"); int highResolutionDiameter = 3; int mediumResolutionDiameter = highResolutionDiameter + 2; int lowResolutionDiameter = 20; float lowResolutionRadius = static_cast(lowResolutionDiameter) / 2.0f; float mediumResolutionRadius = static_cast(mediumResolutionDiameter) / 2.0f; float highResolutionRadius = static_cast(highResolutionDiameter) / 2.0f; for (int i = 0; i < lowResolutionDiameter; ++i) { for (int j = 0; j < lowResolutionDiameter; ++j) { EntityID patch; int x = i - lowResolutionDiameter / 2; int z = j - lowResolutionDiameter / 2; if (std::abs(x) < highResolutionRadius && std::abs(z) < highResolutionRadius) { patch = createInstanceOf("terrain-patch-high-resolution"); } else if (std::abs(x) < mediumResolutionRadius && std::abs(z) < mediumResolutionRadius) { patch = createInstanceOf("terrain-patch-medium-resolution"); } else { patch = createInstanceOf("terrain-patch-low-resolution"); } setTerrainPatchPosition(patch, {x, z}); } } // Setup state machine states languageSelectState = { std::bind(&Game::enterLanguageSelectState, this), std::bind(&Game::exitLanguageSelectState, this) }; splashState = { std::bind(&Game::enterSplashState, this), std::bind(&Game::exitSplashState, this) }; loadingState = { std::bind(&Game::enterLoadingState, this), std::bind(&Game::exitLoadingState, this) }; titleState = { std::bind(&Game::enterTitleState, this), std::bind(&Game::exitTitleState, this) }; playState = { std::bind(&Game::enterPlayState, this), std::bind(&Game::exitPlayState, this) }; // Initialize state machine if (firstRun) { StateMachine::changeState(&languageSelectState); } else { #if defined(DEBUG) StateMachine::changeState(&titleState); #else StateMachine::changeState(&splashState); #endif } changeState(sandboxState); } void Game::update(float t, float dt) { this->time = t; // Execute current state if (currentState != nullptr) { currentState->execute(); } // Update systems systemManager->update(t, dt); // Update animations animator.animate(dt); if (fpsLabel->isVisible()) { std::stringstream stream; stream.precision(2); stream << std::fixed << (performanceSampler.getMeanFrameDuration() * 1000.0f); fpsLabel->setText(stream.str()); } uiRootElement->update(); } void Game::input() { controls.update(); } void Game::render() { // Perform sub-frame interpolation on UI elements uiRootElement->interpolate(stepScheduler.getScheduledSubsteps()); // Update and batch UI elements uiBatcher->batch(uiBatch, uiRootElement); // Perform sub-frame interpolation particles particleSystem->getBillboardBatch()->interpolate(stepScheduler.getScheduledSubsteps()); particleSystem->getBillboardBatch()->batch(); // Render scene renderer.render(*worldScene); renderer.render(*uiScene); if (screenshotQueued) { screenshot(); screenshotQueued = false; } // Swap window framebuffers window->swapBuffers(); } void Game::exit() {} void Game::handleEvent(const WindowResizedEvent& event) { w = event.width; h = event.height; if (*fullscreen) { logger->log("Resized fullscreen window to " + std::to_string(w) + "x" + std::to_string(h)); fullscreenResolution = {event.width, event.height}; } else { logger->log("Resized window to " + std::to_string(w) + "x" + std::to_string(h)); windowResolution = {event.width, event.height}; } // Save resolution settings saveSettings(); defaultRenderTarget.width = event.width; defaultRenderTarget.height = event.height; glViewport(0, 0, event.width, event.height); camera.setPerspective(glm::radians(40.0f), static_cast(w) / static_cast(h), 0.1, 100.0f); toolSystem->setPickingViewport(Vector4(0, 0, w, h)); resizeUI(event.width, event.height); skipSplash(); } void Game::handleEvent(const GamepadConnectedEvent& event) { // Unmap all controls inputRouter->reset(); // Reload control profile loadControlProfile(*controlProfileName); } void Game::handleEvent(const GamepadDisconnectedEvent& event) {} void Game::handleEvent(const ScheduledFunctionEvent& event) { if (event.caller == static_cast(this)) { event.function(); } } void Game::setupDebugging() { // Setup logging { logger = new Logger(); // Style log format std::string logPrefix = std::string(); std::string logPostfix = "\n"; std::string warningPrefix = "Warning: "; std::string warningPostfix = std::string(); std::string errorPrefix = "Error: "; std::string errorPostfix = std::string(); std::string successPrefix = std::string(); std::string successPostfix = std::string(); // Enable colored messages on debug builds #if defined(DEBUG) warningPrefix = std::string(ANSI_CODE_BOLD) + std::string(ANSI_CODE_YELLOW) + std::string("Warning: ") + std::string(ANSI_CODE_RESET) + std::string(ANSI_CODE_YELLOW); warningPostfix.append(ANSI_CODE_RESET); errorPrefix = std::string(ANSI_CODE_BOLD) + std::string(ANSI_CODE_RED) + std::string("Error: ") + std::string(ANSI_CODE_RESET) + std::string(ANSI_CODE_RED); errorPostfix.append(ANSI_CODE_RESET); successPrefix.insert(0, ANSI_CODE_GREEN); successPostfix.append(ANSI_CODE_RESET); #endif logger->setLogPrefix(logPrefix); logger->setLogPostfix(logPostfix); logger->setWarningPrefix(warningPrefix); logger->setWarningPostfix(warningPostfix); logger->setErrorPrefix(errorPrefix); logger->setErrorPostfix(errorPostfix); logger->setSuccessPrefix(successPrefix); logger->setSuccessPostfix(successPostfix); // Redirect logger output to log file on release builds #if !defined(DEBUG) std::string logFilename = configPath + "log.txt"; logFileStream.open(logFilename.c_str()); logger->redirect(&logFileStream); #endif } // Create CLI cli = new CommandInterpreter(); // Register CLI commands std::function setCommand = std::bind(&CommandInterpreter::set, cli, std::placeholders::_1, std::placeholders::_2); std::function unsetCommand = std::bind(&CommandInterpreter::unset, cli, std::placeholders::_1); std::function exitCommand = std::bind(std::exit, EXIT_SUCCESS); std::function setScaleCommand = [this](int id, float x, float y, float z) { setScale(id, {x, y, z}); }; std::function setTranslationCommand = [this](int id, float x, float y, float z) { setTranslation(id, {x, y, z}); }; std::function createInstanceCommand = std::bind(&Game::createInstance, this); std::function createNamedInstanceCommand = std::bind(&Game::createNamedInstance, this, std::placeholders::_1); std::function createInstanceOfCommand = std::bind(&Game::createInstanceOf, this, std::placeholders::_1); std::function createNamedInstanceOfCommand = std::bind(&Game::createNamedInstanceOf, this, std::placeholders::_1, std::placeholders::_2); std::function toggleWireframeCommand = [this](float width){ lightingPass->setWireframeLineWidth(width); }; std::function helpCommand = [this]() { auto& helpStrings = cli->help(); for (auto it = helpStrings.begin(); it != helpStrings.end(); ++it) { if (it->second.empty()) { continue; } std::cout << it->second << std::endl; } }; std::function variablesCommand = [this]() { auto& variables = cli->variables(); for (auto it = variables.begin(); it != variables.end(); ++it) { std::cout << it->first << "=\"" << it->second << "\"" << std::endl; } }; std::string exitHelp = "exit"; std::string setHelp = "set "; std::string unsetHelp = "unset "; std::string createInstanceHelp = "createinstance