Planet
navi homePPSaboutscreenshotsdownloaddevelopmentforum

source: code/trunk/src/libraries/core/Game.cc @ 5929

Last change on this file since 5929 was 5929, checked in by rgrieder, 15 years ago

Merged core5 branch back to the trunk.
Key features include clean level unloading and an extended XML event system.

Two important notes:
Delete your keybindings.ini files! * or you will still get parser errors when loading the key bindings.
Delete build_dir/lib/modules/libgamestates.module! * or orxonox won't start.
Best thing to do is to delete the build folder ;)

  • Property svn:eol-style set to native
File size: 23.6 KB
Line 
1/*
2 *   ORXONOX - the hottest 3D action shooter ever to exist
3 *                    > www.orxonox.net <
4 *
5 *
6 *   License notice:
7 *
8 *   This program is free software; you can redistribute it and/or
9 *   modify it under the terms of the GNU General Public License
10 *   as published by the Free Software Foundation; either version 2
11 *   of the License, or (at your option) any later version.
12 *
13 *   This program is distributed in the hope that it will be useful,
14 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
15 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 *   GNU General Public License for more details.
17 *
18 *   You should have received a copy of the GNU General Public License
19 *   along with this program; if not, write to the Free Software
20 *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
21 *
22 *   Author:
23 *      Reto Grieder
24 *   Co-authors:
25 *      ...
26 *
27 */
28
29/**
30@file
31@brief
32    Implementation of the Game class.
33*/
34
35#include "Game.h"
36
37#include <exception>
38#include <boost/weak_ptr.hpp>
39
40#include "util/Clock.h"
41#include "util/Debug.h"
42#include "util/Exception.h"
43#include "util/ScopeGuard.h"
44#include "util/Sleep.h"
45#include "util/SubString.h"
46#include "CommandLine.h"
47#include "ConsoleCommand.h"
48#include "Core.h"
49#include "CoreIncludes.h"
50#include "ConfigValueIncludes.h"
51#include "GameMode.h"
52#include "GameState.h"
53
54namespace orxonox
55{
56    static void stop_game()
57        { Game::getInstance().stop(); }
58    SetConsoleCommandShortcutExternAlias(stop_game, "exit");
59    static void printFPS()
60        { COUT(0) << Game::getInstance().getAvgFPS() << std::endl; }
61    SetConsoleCommandShortcutExternAlias(printFPS, "printFPS");
62    static void printTickTime()
63        { COUT(0) << Game::getInstance().getAvgTickTime() << std::endl; }
64    SetConsoleCommandShortcutExternAlias(printTickTime, "printTickTime");
65
66    std::map<std::string, GameStateInfo> Game::gameStateDeclarations_s;
67    Game* Game::singletonPtr_s = 0;
68
69
70    /**
71    @brief
72        Represents one node of the game state tree.
73    */
74    struct GameStateTreeNode
75    {
76        std::string name_;
77        weak_ptr<GameStateTreeNode> parent_;
78        std::vector<shared_ptr<GameStateTreeNode> > children_;
79    };
80
81
82    /**
83    @brief
84        Another helper class for the Game singleton: we cannot derive
85        Game from OrxonoxClass because we need to handle the Identifier
86        destruction in the Core destructor.
87    */
88    class GameConfiguration : public OrxonoxClass
89    {
90    public:
91        GameConfiguration()
92        {
93            RegisterRootObject(GameConfiguration);
94            this->setConfigValues();
95        }
96
97        void setConfigValues()
98        {
99            SetConfigValue(statisticsRefreshCycle_, 250000)
100                .description("Sets the time in microseconds interval at which average fps, etc. get updated.");
101            SetConfigValue(statisticsAvgLength_, 1000000)
102                .description("Sets the time in microseconds interval at which average fps, etc. gets calculated.");
103            SetConfigValue(fpsLimit_, 50)
104                .description("Sets the desired framerate (0 for no limit).");
105        }
106
107        unsigned int statisticsRefreshCycle_;
108        unsigned int statisticsAvgLength_;
109        unsigned int fpsLimit_;
110    };
111
112
113    /**
114    @brief
115        Non-initialising constructor.
116    */
117    Game::Game(const std::string& cmdLine)
118        // Destroy factories before the Core!
119        : gsFactoryDestroyer_(Game::GameStateFactory::getFactories(), &std::map<std::string, shared_ptr<GameStateFactory> >::clear)
120    {
121        this->bAbort_ = false;
122        bChangingState_ = false;
123
124#ifdef ORXONOX_PLATFORM_WINDOWS
125        minimumSleepTime_ = 1000/*us*/;
126#else
127        minimumSleepTime_ = 0/*us*/;
128#endif
129
130        // Create an empty root state
131        this->declareGameState<GameState>("GameState", "emptyRootGameState", true, false);
132
133        // Set up a basic clock to keep time
134        this->gameClock_.reset(new Clock());
135
136        // Create the Core
137        this->core_.reset(new Core(cmdLine));
138
139        // After the core has been created, we can safely instantiate the GameStates that don't require graphics
140        for (std::map<std::string, GameStateInfo>::const_iterator it = gameStateDeclarations_s.begin();
141            it != gameStateDeclarations_s.end(); ++it)
142        {
143            if (!it->second.bGraphicsMode)
144                constructedStates_[it->second.stateName] = GameStateFactory::fabricate(it->second);
145        }
146
147        // The empty root state is ALWAYS loaded!
148        this->rootStateNode_ = shared_ptr<GameStateTreeNode>(new GameStateTreeNode());
149        this->rootStateNode_->name_ = "emptyRootGameState";
150        this->loadedTopStateNode_ = this->rootStateNode_;
151        this->loadedStates_.push_back(this->getState(rootStateNode_->name_));
152
153        // Do this after the Core creation!
154        this->configuration_.reset(new GameConfiguration());
155    }
156
157    /**
158    @brief
159        All destruction code is handled by scoped_ptrs and SimpleScopeGuards.
160    */
161    Game::~Game()
162    {
163    }
164
165    /**
166    @brief
167        Main loop of the orxonox game.
168    @note
169        We use the Ogre::Timer to measure time since it uses the most precise
170        method an any platform (however the windows timer lacks time when under
171        heavy kernel load!).
172    */
173    void Game::run()
174    {
175        if (this->requestedStateNodes_.empty())
176            COUT(0) << "Warning: Starting game without requesting GameState. This automatically terminates the program." << std::endl;
177
178        // reset statistics
179        this->statisticsStartTime_ = 0;
180        this->statisticsTickTimes_.clear();
181        this->periodTickTime_ = 0;
182        this->periodTime_ = 0;
183        this->avgFPS_ = 0.0f;
184        this->avgTickTime_ = 0.0f;
185        this->excessSleepTime_ = 0;
186
187        // START GAME
188        // first delta time should be about 0 seconds
189        this->gameClock_->capture();
190        // A first item is required for the fps limiter
191        StatisticsTickInfo tickInfo = {0, 0};
192        statisticsTickTimes_.push_back(tickInfo);
193        while (!this->bAbort_ && (!this->loadedStates_.empty() || this->requestedStateNodes_.size() > 0))
194        {
195            // Generate the dt
196            this->gameClock_->capture();
197
198            // Statistics init
199            StatisticsTickInfo tickInfo = {gameClock_->getMicroseconds(), 0};
200            statisticsTickTimes_.push_back(tickInfo);
201            this->periodTime_ += this->gameClock_->getDeltaTimeMicroseconds();
202
203            // Update the GameState stack if required
204            this->updateGameStateStack();
205
206            // Core preUpdate (doesn't throw)
207            try
208                { this->core_->preUpdate(*this->gameClock_); }
209            catch (...)
210            {
211                COUT(0) << "An exception occurred in the Core preUpdate: " << Exception::handleMessage() << std::endl;
212                COUT(0) << "This should really never happen! Closing the program." << std::endl;
213                this->stop();
214                break;
215            }
216
217            // Update the GameStates bottom up in the stack
218            this->updateGameStates();
219
220            // Core postUpdate (doesn't throw)
221            try
222                { this->core_->postUpdate(*this->gameClock_); }
223            catch (...)
224            {
225                COUT(0) << "An exception occurred in the Core postUpdate: " << Exception::handleMessage() << std::endl;
226                COUT(0) << "This should really never happen! Closing the program." << std::endl;
227                this->stop();
228                break;
229            }
230
231            // Evaluate statistics
232            this->updateStatistics();
233
234            // Limit framerate
235            this->updateFPSLimiter();
236        }
237
238        // UNLOAD all remaining states
239        while (this->loadedStates_.size() > 1)
240            this->unloadState(this->loadedStates_.back()->getName());
241        this->loadedTopStateNode_ = this->rootStateNode_;
242        this->requestedStateNodes_.clear();
243    }
244
245    void Game::updateGameStateStack()
246    {
247        while (this->requestedStateNodes_.size() > 0)
248        {
249            shared_ptr<GameStateTreeNode> requestedStateNode = this->requestedStateNodes_.front();
250            assert(this->loadedTopStateNode_);
251            if (!this->loadedTopStateNode_->parent_.expired() && requestedStateNode == this->loadedTopStateNode_->parent_.lock())
252                this->unloadState(loadedTopStateNode_->name_);
253            else // has to be child
254            {
255                try
256                {
257                    this->loadState(requestedStateNode->name_);
258                }
259                catch (...)
260                {
261                    COUT(1) << "Error: Loading GameState '" << requestedStateNode->name_ << "' failed: " << Exception::handleMessage() << std::endl;
262                    // All scheduled operations have now been rendered inert --> flush them and issue a warning
263                    if (this->requestedStateNodes_.size() > 1)
264                        COUT(4) << "All " << this->requestedStateNodes_.size() - 1 << " scheduled transitions have been ignored." << std::endl;
265                    this->requestedStateNodes_.clear();
266                    break;
267                }
268            }
269            this->loadedTopStateNode_ = requestedStateNode;
270            this->requestedStateNodes_.erase(this->requestedStateNodes_.begin());
271        }
272    }
273
274    void Game::updateGameStates()
275    {
276        // Note: The first element is the empty root state, which doesn't need ticking
277        for (GameStateVector::const_iterator it = this->loadedStates_.begin() + 1;
278            it != this->loadedStates_.end(); ++it)
279        {
280            try
281            {
282                // Add tick time for most of the states
283                uint64_t timeBeforeTick = 0;
284                if ((*it)->getInfo().bIgnoreTickTime)
285                    timeBeforeTick = this->gameClock_->getRealMicroseconds();
286                (*it)->update(*this->gameClock_);
287                if ((*it)->getInfo().bIgnoreTickTime)
288                    this->subtractTickTime(static_cast<int32_t>(this->gameClock_->getRealMicroseconds() - timeBeforeTick));
289            }
290            catch (...)
291            {
292                COUT(1) << "An exception occurred while updating '" << (*it)->getName() << "': " << Exception::handleMessage() << std::endl;
293                COUT(1) << "This should really never happen!" << std::endl;
294                COUT(1) << "Unloading all GameStates depending on the one that crashed." << std::endl;
295                shared_ptr<GameStateTreeNode> current = this->loadedTopStateNode_;
296                while (current->name_ != (*it)->getName() && current)
297                    current = current->parent_.lock();
298                if (current && current->parent_.lock())
299                    this->requestState(current->parent_.lock()->name_);
300                else
301                    this->stop();
302                break;
303            }
304        }
305    }
306
307    void Game::updateStatistics()
308    {
309        // Add the tick time of this frame (rendering time has already been subtracted)
310        uint64_t currentTime = gameClock_->getMicroseconds();
311        uint64_t currentRealTime = gameClock_->getRealMicroseconds();
312        this->statisticsTickTimes_.back().tickLength += currentRealTime - currentTime;
313        this->periodTickTime_ += currentRealTime - currentTime;
314        if (this->periodTime_ > this->configuration_->statisticsRefreshCycle_)
315        {
316            std::list<StatisticsTickInfo>::iterator it = this->statisticsTickTimes_.begin();
317            assert(it != this->statisticsTickTimes_.end());
318            int64_t lastTime = currentTime - this->configuration_->statisticsAvgLength_;
319            if (static_cast<int64_t>(it->tickTime) < lastTime)
320            {
321                do
322                {
323                    assert(this->periodTickTime_ >= it->tickLength);
324                    this->periodTickTime_ -= it->tickLength;
325                    ++it;
326                    assert(it != this->statisticsTickTimes_.end());
327                } while (static_cast<int64_t>(it->tickTime) < lastTime);
328                this->statisticsTickTimes_.erase(this->statisticsTickTimes_.begin(), it);
329            }
330
331            uint32_t framesPerPeriod = this->statisticsTickTimes_.size();
332            this->avgFPS_ = static_cast<float>(framesPerPeriod) / (currentTime - this->statisticsTickTimes_.front().tickTime) * 1000000.0f;
333            this->avgTickTime_ = static_cast<float>(this->periodTickTime_) / framesPerPeriod / 1000.0f;
334
335            this->periodTime_ -= this->configuration_->statisticsRefreshCycle_;
336        }
337    }
338
339    void Game::updateFPSLimiter()
340    {
341        // Why configuration_->fpsLimit_ - 1? No idea, but otherwise the fps rate is always (from 10 to 200!) one frame too high
342        uint32_t nextTime = gameClock_->getMicroseconds() - excessSleepTime_ + static_cast<uint32_t>(1000000.0f / (configuration_->fpsLimit_ - 1));
343        uint64_t currentRealTime = gameClock_->getRealMicroseconds();
344        while (currentRealTime < nextTime - minimumSleepTime_)
345        {
346            usleep(nextTime - currentRealTime);
347            currentRealTime = gameClock_->getRealMicroseconds();
348        }
349        // Integrate excess to avoid steady state error
350        excessSleepTime_ = currentRealTime - nextTime;
351        // Anti windup
352        if (excessSleepTime_ > 50000) // 20ms is about the maximum time Windows would sleep for too long
353            excessSleepTime_ = 50000;
354    }
355
356    void Game::stop()
357    {
358        this->bAbort_ = true;
359    }
360
361    void Game::subtractTickTime(int32_t length)
362    {
363        assert(!this->statisticsTickTimes_.empty());
364        this->statisticsTickTimes_.back().tickLength -= length;
365        this->periodTickTime_ -= length;
366    }
367
368
369    /***** GameState related *****/
370
371    void Game::requestState(const std::string& name)
372    {
373        if (!this->checkState(name))
374        {
375            COUT(2) << "Warning: GameState named '" << name << "' doesn't exist!" << std::endl;
376            return;
377        }
378
379        if (this->bChangingState_)
380        {
381            COUT(2) << "Warning: Requesting GameStates while loading/unloading a GameState is illegal! Ignoring." << std::endl;
382            return;
383        }
384
385        shared_ptr<GameStateTreeNode> lastRequestedNode;
386        if (this->requestedStateNodes_.empty())
387            lastRequestedNode = this->loadedTopStateNode_;
388        else
389            lastRequestedNode = this->requestedStateNodes_.back();
390        if (name == lastRequestedNode->name_)
391        {
392            COUT(2) << "Warning: Requesting the currently active state! Ignoring." << std::endl;
393            return;
394        }
395
396        // Check children first
397        std::vector<shared_ptr<GameStateTreeNode> > requestedNodes;
398        for (unsigned int i = 0; i < lastRequestedNode->children_.size(); ++i)
399        {
400            if (lastRequestedNode->children_[i]->name_ == name)
401            {
402                requestedNodes.push_back(lastRequestedNode->children_[i]);
403                break;
404            }
405        }
406
407        if (requestedNodes.empty())
408        {
409            // Check parent and all its grand parents
410            shared_ptr<GameStateTreeNode> currentNode = lastRequestedNode;
411            while (currentNode != NULL)
412            {
413                if (currentNode->name_ == name)
414                    break;
415                currentNode = currentNode->parent_.lock();
416                requestedNodes.push_back(currentNode);
417            }
418            if (currentNode == NULL)
419                requestedNodes.clear();
420        }
421
422        if (requestedNodes.empty())
423            COUT(1) << "Error: Requested GameState transition is not allowed. Ignoring." << std::endl;
424        else
425            this->requestedStateNodes_.insert(requestedStateNodes_.end(), requestedNodes.begin(), requestedNodes.end());
426    }
427
428    void Game::requestStates(const std::string& names)
429    {
430        SubString tokens(names, ",;", " ");
431        for (unsigned int i = 0; i < tokens.size(); ++i)
432            this->requestState(tokens[i]);
433    }
434
435    void Game::popState()
436    {
437        shared_ptr<GameStateTreeNode> lastRequestedNode;
438        if (this->requestedStateNodes_.empty())
439            lastRequestedNode = this->loadedTopStateNode_;
440        else
441            lastRequestedNode = this->requestedStateNodes_.back();
442        if (lastRequestedNode != this->rootStateNode_)
443            this->requestState(lastRequestedNode->parent_.lock()->name_);
444        else
445            COUT(2) << "Warning: Can't pop the internal dummy root GameState" << std::endl;
446    }
447
448    shared_ptr<GameState> Game::getState(const std::string& name)
449    {
450        GameStateMap::const_iterator it = constructedStates_.find(name);
451        if (it != constructedStates_.end())
452            return it->second;
453        else
454        {
455            std::map<std::string, GameStateInfo>::const_iterator it = gameStateDeclarations_s.find(name);
456            if (it != gameStateDeclarations_s.end())
457                COUT(1) << "Error: GameState '" << name << "' has not yet been loaded." << std::endl;
458            else
459                COUT(1) << "Error: Could not find GameState '" << name << "'." << std::endl;
460            return shared_ptr<GameState>();
461        }
462    }
463
464    void Game::setStateHierarchy(const std::string& str)
465    {
466        // Split string into pieces of the form whitespacesText
467        std::vector<std::pair<std::string, int> > stateStrings;
468        size_t pos = 0;
469        size_t startPos = 0;
470        while (pos < str.size())
471        {
472            int indentation = 0;
473            while(pos < str.size() && str[pos] == ' ')
474                ++indentation, ++pos;
475            startPos = pos;
476            while(pos < str.size() && str[pos] != ' ')
477                ++pos;
478            stateStrings.push_back(std::make_pair(str.substr(startPos, pos - startPos), indentation));
479        }
480        if (stateStrings.empty())
481            ThrowException(GameState, "Emtpy GameState hierarchy provided, terminating.");
482        // Add element with large identation to detect the last with just an iterator
483        stateStrings.push_back(std::make_pair("", -1));
484
485        // Parse elements recursively
486        std::vector<std::pair<std::string, int> >::const_iterator begin = stateStrings.begin();
487        parseStates(begin, this->rootStateNode_);
488    }
489
490    /*** Internal ***/
491
492    void Game::parseStates(std::vector<std::pair<std::string, int> >::const_iterator& it, shared_ptr<GameStateTreeNode> currentNode)
493    {
494        SubString tokens(it->first, ",");
495        std::vector<std::pair<std::string, int> >::const_iterator startIt = it;
496
497        for (unsigned int i = 0; i < tokens.size(); ++i)
498        {
499            it = startIt; // Reset iterator to the beginning of the sub tree
500            if (!this->checkState(tokens[i]))
501                ThrowException(GameState, "GameState with name '" << tokens[i] << "' not found!");
502            if (tokens[i] == this->rootStateNode_->name_)
503                ThrowException(GameState, "You shouldn't use 'emptyRootGameState' in the hierarchy...");
504            shared_ptr<GameStateTreeNode> node(new GameStateTreeNode());
505            node->name_ = tokens[i];
506            node->parent_ = currentNode;
507            currentNode->children_.push_back(node);
508
509            int currentLevel = it->second;
510            ++it;
511            while (it->second != -1)
512            {
513                if (it->second <= currentLevel)
514                    break;
515                else if (it->second == currentLevel + 1)
516                    parseStates(it, node);
517                else
518                    ThrowException(GameState, "Indentation error while parsing the hierarchy.");
519            }
520        }
521    }
522
523    void Game::loadGraphics()
524    {
525        if (!GameMode::showsGraphics())
526        {
527            core_->loadGraphics();
528            Loki::ScopeGuard graphicsUnloader = Loki::MakeObjGuard(*this, &Game::unloadGraphics);
529
530            // Construct all the GameStates that require graphics
531            for (std::map<std::string, GameStateInfo>::const_iterator it = gameStateDeclarations_s.begin();
532                it != gameStateDeclarations_s.end(); ++it)
533            {
534                if (it->second.bGraphicsMode)
535                {
536                    // Game state loading failure is serious --> don't catch
537                    shared_ptr<GameState> gameState = GameStateFactory::fabricate(it->second);
538                    if (!constructedStates_.insert(std::make_pair(
539                        it->second.stateName, gameState)).second)
540                        assert(false); // GameState was already created!
541                }
542            }
543            graphicsUnloader.Dismiss();
544        }
545    }
546
547    void Game::unloadGraphics()
548    {
549        if (GameMode::showsGraphics())
550        {
551            // Destroy all the GameStates that require graphics
552            for (GameStateMap::iterator it = constructedStates_.begin(); it != constructedStates_.end();)
553            {
554                if (it->second->getInfo().bGraphicsMode)
555                    constructedStates_.erase(it++);
556                else
557                    ++it;
558            }
559
560            core_->unloadGraphics();
561        }
562    }
563
564    bool Game::checkState(const std::string& name) const
565    {
566        std::map<std::string, GameStateInfo>::const_iterator it = gameStateDeclarations_s.find(name);
567        if (it == gameStateDeclarations_s.end())
568            return false;
569        else
570            return true;
571    }
572
573    void Game::loadState(const std::string& name)
574    {
575        this->bChangingState_ = true;
576        LOKI_ON_BLOCK_EXIT_OBJ(*this, &Game::resetChangingState);
577
578        // If state requires graphics, load it
579        Loki::ScopeGuard graphicsUnloader = Loki::MakeObjGuard(*this, &Game::unloadGraphics);
580        if (gameStateDeclarations_s[name].bGraphicsMode && !GameMode::showsGraphics())
581            this->loadGraphics();
582        else
583            graphicsUnloader.Dismiss();
584
585        shared_ptr<GameState> state = this->getState(name);
586        state->activate();
587        if (!this->loadedStates_.empty())
588            this->loadedStates_.back()->activity_.topState = false;
589        this->loadedStates_.push_back(state);
590        state->activity_.topState = true;
591
592        graphicsUnloader.Dismiss();
593    }
594
595    void Game::unloadState(const std::string& name)
596    {
597        this->bChangingState_ = true;
598        try
599        {
600            shared_ptr<GameState> state = this->getState(name);
601            state->activity_.topState = false;
602            this->loadedStates_.pop_back();
603            if (!this->loadedStates_.empty())
604                this->loadedStates_.back()->activity_.topState = true;
605            state->deactivate();
606        }
607        catch (...)
608        {
609            COUT(2) << "Warning: Unloading GameState '" << name << "' threw an exception: " << Exception::handleMessage() << std::endl;
610            COUT(2) << "         There might be potential resource leaks involved! To avoid this, improve exception-safety." << std::endl;
611        }
612        // Check if graphics is still required
613        if (!bAbort_)
614        {
615            bool graphicsRequired = false;
616            for (unsigned i = 0; i < loadedStates_.size(); ++i)
617                graphicsRequired |= loadedStates_[i]->getInfo().bGraphicsMode;
618            if (!graphicsRequired)
619                this->unloadGraphics();
620        }
621        this->bChangingState_ = false;
622    }
623
624    /*static*/ std::map<std::string, shared_ptr<Game::GameStateFactory> >& Game::GameStateFactory::getFactories()
625    {
626        static std::map<std::string, shared_ptr<GameStateFactory> > factories;
627        return factories;
628    }
629
630    /*static*/ shared_ptr<GameState> Game::GameStateFactory::fabricate(const GameStateInfo& info)
631    {
632        std::map<std::string, shared_ptr<Game::GameStateFactory> >::const_iterator it = getFactories().find(info.className);
633        assert(it != getFactories().end());
634        return it->second->fabricateInternal(info);
635    }
636}
Note: See TracBrowser for help on using the repository browser.