/* * ORXONOX - the hottest 3D action shooter ever to exist * > www.orxonox.net < * * * License notice: * * This program 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 2 * of the License, or (at your option) any later version. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Author: * Reto Grieder * Co-authors: * ... * */ /** @file @brief Implementation of the Game class. */ #include "Game.h" #include #include #include #include "util/Clock.h" #include "util/Output.h" #include "util/Exception.h" #include "util/Sleep.h" #include "util/SubString.h" #include "CommandLineParser.h" #include "Core.h" #include "CoreIncludes.h" #include "ConfigValueIncludes.h" #include "GameMode.h" #include "GameState.h" #include "GraphicsManager.h" #include "GUIManager.h" #include "command/ConsoleCommand.h" namespace orxonox { static void stop_game() { Game::getInstance().stop(); } SetConsoleCommand("exit", &stop_game); static void printFPS() { orxout(message) << Game::getInstance().getAvgFPS() << endl; } SetConsoleCommand("Stats", "printFPS", &printFPS); static void printTickTime() { orxout(message) << Game::getInstance().getAvgTickTime() << endl; } SetConsoleCommand("Stats", "printTickTime", &printTickTime); std::map Game::gameStateDeclarations_s; Game* Game::singletonPtr_s = 0; //! Represents one node of the game state tree. struct GameStateTreeNode { std::string name_; weak_ptr parent_; std::vector > children_; }; Game::Game(const std::string& cmdLine) : gameClock_(NULL) , core_(NULL) , bChangingState_(false) , bAbort_(false) , destructionHelper_(this) { orxout(internal_status) << "initializing Game object..." << endl; #ifdef ORXONOX_PLATFORM_WINDOWS minimumSleepTime_ = 1000/*us*/; #else minimumSleepTime_ = 0/*us*/; #endif // reset statistics this->statisticsStartTime_ = 0; this->statisticsTickTimes_.clear(); this->periodTickTime_ = 0; this->periodTime_ = 0; this->avgFPS_ = 0.0f; this->avgTickTime_ = 0.0f; this->excessSleepTime_ = 0; // Create an empty root state this->declareGameState("GameState", "emptyRootGameState", true, false); // Set up a basic clock to keep time this->gameClock_ = new Clock(); // Create the Core orxout(internal_info) << "creating Core object:" << endl; this->core_ = new Core(cmdLine); // Do this after the Core creation! RegisterRootObject(Game); this->setConfigValues(); // After the core has been created, we can safely instantiate the GameStates that don't require graphics for (std::map::const_iterator it = gameStateDeclarations_s.begin(); it != gameStateDeclarations_s.end(); ++it) { if (!it->second.bGraphicsMode) constructedStates_[it->second.stateName] = GameStateFactory::fabricate(it->second); } // The empty root state is ALWAYS loaded! this->rootStateNode_ = shared_ptr(new GameStateTreeNode()); this->rootStateNode_->name_ = "emptyRootGameState"; this->loadedTopStateNode_ = this->rootStateNode_; this->loadedStates_.push_back(this->getState(rootStateNode_->name_)); orxout(internal_status) << "finished initializing Game object" << endl; } void Game::destroy() { orxout(internal_status) << "destroying Game object..." << endl; // Remove us from the object lists again to avoid problems when destroying them this->unregisterObject(); assert(loadedStates_.size() <= 1); // Just empty root GameState // Destroy all GameStates (shared_ptrs take care of actual destruction) constructedStates_.clear(); GameStateFactory::getFactories().clear(); safeObjectDelete(&core_); safeObjectDelete(&gameClock_); orxout(internal_status) << "finished destroying Game object..." << endl; } void Game::setConfigValues() { SetConfigValue(statisticsRefreshCycle_, 250000) .description("Sets the time in microseconds interval at which average fps, etc. get updated."); SetConfigValue(statisticsAvgLength_, 1000000) .description("Sets the time in microseconds interval at which average fps, etc. gets calculated."); SetConfigValueExternal(fpsLimit_, "GraphicsSettings", "fpsLimit", 50) .description("Sets the desired frame rate (0 for no limit)."); } /** @brief Main loop of the orxonox game. @note We use the Ogre::Timer to measure time since it uses the most precise method an any platform (however the windows timer lacks time when under heavy kernel load!). */ void Game::run() { if (this->requestedStateNodes_.empty()) orxout(user_error) << "Starting game without requesting GameState. This automatically terminates the program." << endl; // Update the GameState stack if required. We do this already here to have a properly initialized game before entering the main loop this->updateGameStateStack(); orxout(user_status) << "Game loaded" << endl; orxout(internal_status) << "--------------------------------------------------" << endl; orxout(internal_status) << "starting main loop..." << endl; // START GAME // first delta time should be about 0 seconds this->gameClock_->capture(); // A first item is required for the fps limiter StatisticsTickInfo tickInfo = {0, 0}; statisticsTickTimes_.push_back(tickInfo); while (!this->bAbort_ && (!this->loadedStates_.empty() || this->requestedStateNodes_.size() > 0)) { // Generate the dt this->gameClock_->capture(); // Statistics init StatisticsTickInfo tickInfo = {gameClock_->getMicroseconds(), 0}; statisticsTickTimes_.push_back(tickInfo); this->periodTime_ += this->gameClock_->getDeltaTimeMicroseconds(); // Update the GameState stack if required this->updateGameStateStack(); // Core preUpdate try { this->core_->preUpdate(*this->gameClock_); } catch (...) { orxout(user_error) << "An exception occurred in the Core preUpdate: " << Exception::handleMessage() << endl; orxout(user_error) << "This should really never happen! Closing the program." << endl; this->stop(); break; } // Update the GameStates bottom up in the stack this->updateGameStates(); // Core postUpdate try { this->core_->postUpdate(*this->gameClock_); } catch (...) { orxout(user_error) << "An exception occurred in the Core postUpdate: " << Exception::handleMessage() << endl; orxout(user_error) << "This should really never happen! Closing the program." << endl; this->stop(); break; } // Evaluate statistics this->updateStatistics(); // Limit frame rate static bool hasVSync = GameMode::showsGraphics() && GraphicsManager::getInstance().hasVSyncEnabled(); // can be static since changes of VSync currently require a restart if (this->fpsLimit_ > 0 && !hasVSync) this->updateFPSLimiter(); } orxout(internal_status) << "finished main loop" << endl; orxout(internal_status) << "--------------------------------------------------" << endl; // UNLOAD all remaining states while (this->loadedStates_.size() > 1) this->unloadState(this->loadedStates_.back()->getName()); this->loadedTopStateNode_ = this->rootStateNode_; this->requestedStateNodes_.clear(); } void Game::updateGameStateStack() { while (this->requestedStateNodes_.size() > 0) { shared_ptr requestedStateNode = this->requestedStateNodes_.front(); assert(this->loadedTopStateNode_); if (!this->loadedTopStateNode_->parent_.expired() && requestedStateNode == this->loadedTopStateNode_->parent_.lock()) this->unloadState(loadedTopStateNode_->name_); else // has to be child { try { this->loadState(requestedStateNode->name_); } catch (...) { orxout(user_error) << "Loading GameState '" << requestedStateNode->name_ << "' failed: " << Exception::handleMessage() << endl; // All scheduled operations have now been rendered inert --> flush them and issue a warning if (this->requestedStateNodes_.size() > 1) orxout(internal_info) << "All " << this->requestedStateNodes_.size() - 1 << " scheduled transitions have been ignored." << endl; this->requestedStateNodes_.clear(); break; } } this->loadedTopStateNode_ = requestedStateNode; this->requestedStateNodes_.erase(this->requestedStateNodes_.begin()); } } void Game::updateGameStates() { // Note: The first element is the empty root state, which doesn't need ticking for (GameStateVector::const_iterator it = this->loadedStates_.begin() + 1; it != this->loadedStates_.end(); ++it) { try { // Add tick time for most of the states uint64_t timeBeforeTick = 0; if ((*it)->getInfo().bIgnoreTickTime) timeBeforeTick = this->gameClock_->getRealMicroseconds(); (*it)->update(*this->gameClock_); if ((*it)->getInfo().bIgnoreTickTime) this->subtractTickTime(static_cast(this->gameClock_->getRealMicroseconds() - timeBeforeTick)); } catch (...) { orxout(user_error) << "An exception occurred while updating '" << (*it)->getName() << "': " << Exception::handleMessage() << endl; orxout(user_error) << "This should really never happen!" << endl; orxout(user_error) << "Unloading all GameStates depending on the one that crashed." << endl; shared_ptr current = this->loadedTopStateNode_; while (current->name_ != (*it)->getName() && current) current = current->parent_.lock(); if (current && current->parent_.lock()) this->requestState(current->parent_.lock()->name_); else this->stop(); break; } } } void Game::updateStatistics() { // Add the tick time of this frame (rendering time has already been subtracted) uint64_t currentTime = gameClock_->getMicroseconds(); uint64_t currentRealTime = gameClock_->getRealMicroseconds(); this->statisticsTickTimes_.back().tickLength += (uint32_t)(currentRealTime - currentTime); this->periodTickTime_ += (uint32_t)(currentRealTime - currentTime); if (this->periodTime_ > this->statisticsRefreshCycle_) { std::list::iterator it = this->statisticsTickTimes_.begin(); assert(it != this->statisticsTickTimes_.end()); int64_t lastTime = currentTime - this->statisticsAvgLength_; if (static_cast(it->tickTime) < lastTime) { do { assert(this->periodTickTime_ >= it->tickLength); this->periodTickTime_ -= it->tickLength; ++it; assert(it != this->statisticsTickTimes_.end()); } while (static_cast(it->tickTime) < lastTime); this->statisticsTickTimes_.erase(this->statisticsTickTimes_.begin(), it); } uint32_t framesPerPeriod = this->statisticsTickTimes_.size(); // Why minus 1? No idea, but otherwise the fps rate is always (from 10 to 200!) one frame too low this->avgFPS_ = -1 + static_cast(framesPerPeriod) / (currentTime - this->statisticsTickTimes_.front().tickTime) * 1000000.0f; this->avgTickTime_ = static_cast(this->periodTickTime_) / framesPerPeriod / 1000.0f; this->periodTime_ -= this->statisticsRefreshCycle_; } } void Game::updateFPSLimiter() { uint64_t nextTime = gameClock_->getMicroseconds() - excessSleepTime_ + static_cast(1000000.0f / fpsLimit_); uint64_t currentRealTime = gameClock_->getRealMicroseconds(); while (currentRealTime < nextTime - minimumSleepTime_) { usleep((unsigned long)(nextTime - currentRealTime)); currentRealTime = gameClock_->getRealMicroseconds(); } // Integrate excess to avoid steady state error excessSleepTime_ = (int)(currentRealTime - nextTime); // Anti windup if (excessSleepTime_ > 50000) // 20ms is about the maximum time Windows would sleep for too long excessSleepTime_ = 50000; } void Game::stop() { orxout(user_status) << "Exit" << endl; this->bAbort_ = true; } void Game::subtractTickTime(int32_t length) { assert(!this->statisticsTickTimes_.empty()); this->statisticsTickTimes_.back().tickLength -= length; this->periodTickTime_ -= length; } /***** GameState related *****/ void Game::requestState(const std::string& name) { if (!this->checkState(name)) { orxout(user_warning) << "GameState named '" << name << "' doesn't exist!" << endl; return; } if (this->bChangingState_) { orxout(user_warning) << "Requesting GameStates while loading/unloading a GameState is illegal! Ignoring." << endl; return; } shared_ptr lastRequestedNode; if (this->requestedStateNodes_.empty()) lastRequestedNode = this->loadedTopStateNode_; else lastRequestedNode = this->requestedStateNodes_.back(); if (name == lastRequestedNode->name_) { orxout(user_warning) << "Requesting the currently active state! Ignoring." << endl; return; } // Check children first std::vector > requestedNodes; for (unsigned int i = 0; i < lastRequestedNode->children_.size(); ++i) { if (lastRequestedNode->children_[i]->name_ == name) { requestedNodes.push_back(lastRequestedNode->children_[i]); break; } } if (requestedNodes.empty()) { // Check parent and all its grand parents shared_ptr currentNode = lastRequestedNode; while (currentNode != NULL) { if (currentNode->name_ == name) break; currentNode = currentNode->parent_.lock(); requestedNodes.push_back(currentNode); } if (currentNode == NULL) requestedNodes.clear(); } if (requestedNodes.empty()) orxout(user_error) << "Requested GameState transition is not allowed. Ignoring." << endl; else this->requestedStateNodes_.insert(requestedStateNodes_.end(), requestedNodes.begin(), requestedNodes.end()); } void Game::requestStates(const std::string& names) { SubString tokens(names, ",;", " "); for (unsigned int i = 0; i < tokens.size(); ++i) this->requestState(tokens[i]); } void Game::popState() { shared_ptr lastRequestedNode; if (this->requestedStateNodes_.empty()) lastRequestedNode = this->loadedTopStateNode_; else lastRequestedNode = this->requestedStateNodes_.back(); if (lastRequestedNode != this->rootStateNode_) this->requestState(lastRequestedNode->parent_.lock()->name_); else orxout(internal_warning) << "Can't pop the internal dummy root GameState" << endl; } shared_ptr Game::getState(const std::string& name) { GameStateMap::const_iterator it = constructedStates_.find(name); if (it != constructedStates_.end()) return it->second; else { std::map::const_iterator it = gameStateDeclarations_s.find(name); if (it != gameStateDeclarations_s.end()) orxout(internal_error) << "GameState '" << name << "' has not yet been loaded." << endl; else orxout(internal_error) << "Could not find GameState '" << name << "'." << endl; return shared_ptr(); } } void Game::setStateHierarchy(const std::string& str) { // Split string into pieces of the form whitespacesText std::vector > stateStrings; size_t pos = 0; size_t startPos = 0; while (pos < str.size()) { int indentation = 0; while (pos < str.size() && str[pos] == ' ') ++indentation, ++pos; startPos = pos; while (pos < str.size() && str[pos] != ' ') ++pos; stateStrings.push_back(std::make_pair(str.substr(startPos, pos - startPos), indentation)); } if (stateStrings.empty()) ThrowException(GameState, "Emtpy GameState hierarchy provided, terminating."); // Add element with large identation to detect the last with just an iterator stateStrings.push_back(std::make_pair(std::string(), -1)); // Parse elements recursively std::vector >::const_iterator begin = stateStrings.begin(); parseStates(begin, this->rootStateNode_); } /*** Internal ***/ void Game::parseStates(std::vector >::const_iterator& it, shared_ptr currentNode) { SubString tokens(it->first, ","); std::vector >::const_iterator startIt = it; for (unsigned int i = 0; i < tokens.size(); ++i) { it = startIt; // Reset iterator to the beginning of the sub tree if (!this->checkState(tokens[i])) ThrowException(GameState, "GameState with name '" << tokens[i] << "' not found!"); if (tokens[i] == this->rootStateNode_->name_) ThrowException(GameState, "You shouldn't use 'emptyRootGameState' in the hierarchy..."); shared_ptr node(new GameStateTreeNode()); node->name_ = tokens[i]; node->parent_ = currentNode; currentNode->children_.push_back(node); int currentLevel = it->second; ++it; while (it->second != -1) { if (it->second <= currentLevel) break; else if (it->second == currentLevel + 1) parseStates(it, node); else ThrowException(GameState, "Indentation error while parsing the hierarchy."); } } } void Game::loadGraphics() { if (!GameMode::showsGraphics()) { orxout(user_status) << "Loading graphics" << endl; orxout(internal_info) << "loading graphics in Game" << endl; core_->loadGraphics(); Loki::ScopeGuard graphicsUnloader = Loki::MakeObjGuard(*this, &Game::unloadGraphics); // Construct all the GameStates that require graphics for (std::map::const_iterator it = gameStateDeclarations_s.begin(); it != gameStateDeclarations_s.end(); ++it) { if (it->second.bGraphicsMode) { // Game state loading failure is serious --> don't catch shared_ptr gameState = GameStateFactory::fabricate(it->second); if (!constructedStates_.insert(std::make_pair( it->second.stateName, gameState)).second) assert(false); // GameState was already created! } } graphicsUnloader.Dismiss(); orxout(internal_info) << "finished loading graphics in Game" << endl; } } void Game::unloadGraphics() { if (GameMode::showsGraphics()) { orxout(user_status) << "Unloading graphics" << endl; orxout(internal_info) << "unloading graphics in Game" << endl; // Destroy all the GameStates that require graphics for (GameStateMap::iterator it = constructedStates_.begin(); it != constructedStates_.end();) { if (it->second->getInfo().bGraphicsMode) constructedStates_.erase(it++); else ++it; } core_->unloadGraphics(); } } bool Game::checkState(const std::string& name) const { std::map::const_iterator it = gameStateDeclarations_s.find(name); if (it == gameStateDeclarations_s.end()) return false; else return true; } void Game::loadState(const std::string& name) { orxout(internal_status) << "loading state '" << name << "'" << endl; this->bChangingState_ = true; LOKI_ON_BLOCK_EXIT_OBJ(*this, &Game::resetChangingState); (void)LOKI_ANONYMOUS_VARIABLE(scopeGuard); // If state requires graphics, load it Loki::ScopeGuard graphicsUnloader = Loki::MakeObjGuard(*this, &Game::unloadGraphics); if (gameStateDeclarations_s[name].bGraphicsMode && !GameMode::showsGraphics()) this->loadGraphics(); else graphicsUnloader.Dismiss(); shared_ptr state = this->getState(name); state->activateInternal(); if (!this->loadedStates_.empty()) this->loadedStates_.back()->activity_.topState = false; this->loadedStates_.push_back(state); state->activity_.topState = true; graphicsUnloader.Dismiss(); } void Game::unloadState(const std::string& name) { orxout(internal_status) << "unloading state '" << name << "'" << endl; this->bChangingState_ = true; try { shared_ptr state = this->getState(name); state->activity_.topState = false; this->loadedStates_.pop_back(); if (!this->loadedStates_.empty()) this->loadedStates_.back()->activity_.topState = true; state->deactivateInternal(); } catch (...) { orxout(internal_warning) << "Unloading GameState '" << name << "' threw an exception: " << Exception::handleMessage() << endl; orxout(internal_warning) << "There might be potential resource leaks involved! To avoid this, improve exception-safety." << endl; } // Check if graphics is still required if (!bAbort_) { bool graphicsRequired = false; for (unsigned i = 0; i < loadedStates_.size(); ++i) graphicsRequired |= loadedStates_[i]->getInfo().bGraphicsMode; if (!graphicsRequired) this->unloadGraphics(); } this->bChangingState_ = false; } /*static*/ std::map >& Game::GameStateFactory::getFactories() { static std::map > factories; return factories; } /*static*/ shared_ptr Game::GameStateFactory::fabricate(const GameStateInfo& info) { std::map >::const_iterator it = getFactories().find(info.className); assert(it != getFactories().end()); return it->second->fabricateInternal(info); } }