How do you structure multiple game screens?
Chris Katko

You start a game. You get a menu system. You move in and out of forms. Then you start the game. Now you're on the level select / campaign screen and things are moving around. Then you click go, you play a round of C&C or whatever and beat the level. You go back to the level select, and play another.

How do you typically go about structuring that stuff? Is there a single game loop that changes states based on what "screen" you're in? Or is there multiple game loops for each screen, and a master controller above them?

Lastly, how do you go about de-coupling and designing interfaces between those sections? Clearly, you don't want a hodge-podge mess of everything touching everything. Without good encapsulation, it would be a nightmare, so how do you go about architecting that?

MiquelFire

For XNA, Microsoft had this package or something in which you define scenes. Each scene would be the screen on your post. You would have an active list, and you add remove scenes as needed, so the main menu would be a scene, the level select would be a scene, etc. I don't remember the details, but I think the class structure would have functions that the main loop could call when the scene needed to do something.

The way they handled it however, a scene could be transparent, so the top most scene would have focus, but other scenes could do updates. So the pause menu could be a scene, but the game could still animate stuff behind the pause menu.

jmasterx

Echoing this, I use a base Scene class, and all Scenes inherit this.

But I can only run one scene.

Then I have a SceneManager that is responsible for scene switching, and calling things like input, logic, and render() on the scene.

When a scene is done, it sends a message to the scene manager and on the next frame, the scene manager calls the scene end methods, cleanup is performed and the new scene is loaded.

The reason you don't want to kill the scene right away is because things will crash hard if you do.

Here is the full source of my scene manager. It does a little more than described but the idea is the same.

#SelectExpand
1#include "Game/Engine/SceneManager.hpp" 2#include "Game/Scene/InitialScene.hpp" 3#include "Game/Scene/GameScene.hpp" 4#include "Game/Scene/LobbyScene.hpp" 5#include "Game/Scene/LoginScene.hpp" 6#include "Game/Utility/StringUtil.hpp" 7 8namespace cge 9{ 10 SceneManager::SceneManager( DeviceManager* settings, DynamicUIManager* dynamicUImanager ) 11 : m_devices(settings),m_gameIsRunning(true), m_nextScene(LOGIN), 12 m_currentScene(NULL),m_needsRedraw(true),m_frameRate(60.0f), 13 m_dynamicUI(dynamicUImanager), m_currentSceneType(NO_SCENE), 14 m_needsResize(false),m_newScreenHeight(0),m_newScreenWidth(0), 15 m_transitioning(false),m_transitionOpacity(1.0f),m_transitionRate(0.05f), m_needsTransition(false), 16 m_guiImageManager("res/gui/gui_image_mapping.conf","res/gui/") 17 { 18 queue = al_create_event_queue(); 19 m_gameTimer = al_create_timer(1.0 / m_frameRate); 20 registerEventSources(); 21 //Instance the first scene 22 processMessages(); 23 Log::write("Scene Manager","Scene Manager Started"); 24 25 m_g.setBuffering(settings->getDisplay()->getMaxTextureSize() >= 2048); 26 } 27 28 SceneManager::~SceneManager(void) 29 { 30 //delete the current scene 31 if(m_currentScene) 32 { 33 m_currentScene->sceneEnd(); 34 delete m_currentScene; 35 m_currentScene = NULL; 36 } 37 38 al_destroy_timer(m_gameTimer); 39 al_destroy_event_queue(queue); 40 Log::write("Scene Manager","Scene Manager Destroyed"); 41 42 } 43 44 void SceneManager::run() 45 { 46 m_devices->setSceneMessenger(this); 47 48 al_start_timer(m_gameTimer); 49 50 //is the event handled? 51 bool handled = false; 52 ALLEGRO_EVENT next; 53 int numTicks = 0; 54 55 //main loop 56 while(m_gameIsRunning) 57 { 58 59 handled = false; 60 al_wait_for_event(queue,&evt); 61 62 bool hasNext = al_peek_next_event(queue,&next); 63 if(hasNext && next.type == ALLEGRO_EVENT_TIMER) 64 { 65 al_drop_next_event(queue); 66 } 67 //render the scene 68 if(m_needsRedraw && al_is_event_queue_empty(queue)) 69 { 70 m_g.begin(); 71 m_currentScene->render(); 72 if(m_needsTransition) 73 { 74 m_needsTransition = false; 75 m_transitioning = true; 76 } 77 if(m_transitioning) 78 m_g.drawTintedSprite(m_g.getBuffer(), 79 Color(m_transitionOpacity,m_transitionOpacity,m_transitionOpacity,m_transitionOpacity),0,0,0); 80 m_g.end(); 81 82 m_needsRedraw = false; 83 } 84 85 defaultBeginEventHandler(&evt); 86 m_currentScene->processEvent(&evt,handled); 87 88 //do default behavior if event was not handled by the scene 89 if (!handled) 90 { 91 defaultEndEventHandler(&evt); 92 } 93 processMessages(); 94 } 95 } 96 97 void SceneManager::registerEventSources() 98 { 99 al_register_event_source(queue, al_get_keyboard_event_source()); 100 al_register_event_source(queue, al_get_mouse_event_source()); 101 al_register_event_source(queue, al_get_timer_event_source(m_gameTimer)); 102 al_register_event_source(queue, al_get_display_event_source(m_devices->getDisplay()->getContext())); 103 Log::write("Scene Manager","Event sources registered"); 104 105 } 106 107 void SceneManager::sendSceneChangeMessage( SceneEnum scene ) 108 { 109 m_nextScene = scene; 110 } 111 112 void SceneManager::sendQuitMessage() 113 { 114 m_gameIsRunning = false; 115 } 116 117 void SceneManager::processMessages() 118 { 119 changeScene(); 120 } 121 122 void SceneManager::changeScene() 123 { 124 if(m_nextScene != NO_SCENE) 125 { 126 127 //delete the current scene 128 if(m_currentScene) 129 { 130 if(m_g.isBuffering()) 131 { 132 startTransition(); 133 } 134 135 m_currentScene->sceneEnd(); 136 m_currentScene->deinitBaseScene(); 137 delete m_currentScene; 138 m_currentScene = NULL; 139 Log::write("Scene Manager","Scene Ended"); 140 } 141 142 m_currentSceneType = m_nextScene; 143 144 //instance the requested scene 145 switch(m_nextScene) 146 { 147 case SceneManagerMessage::GAME: 148 m_currentScene = new GameScene(); 149 Log::write("Scene Manager","Changed to Game Scene"); 150 break; 151 case SceneManagerMessage::LOBBY: 152 m_currentScene = new LobbyScene(); 153 Log::write("Scene Manager","Changed to Lobby Scene"); 154 break; 155 case SceneManagerMessage::LOGIN: 156 m_currentScene = new LoginScene(); 157 Log::write("Scene Manager","Changed to Login Scene"); 158 break; 159 } 160 161 m_nextScene = NO_SCENE; 162 163 //set some class pointers 164 m_currentScene->setGraphics(&m_g); 165 m_currentScene->setDeviceManager(m_devices); 166 m_currentScene->setDynamicUIManager(m_dynamicUI); 167 m_currentScene->setGuiImageManager(&m_guiImageManager); 168 m_currentScene->setSceneMessenger(this); 169 m_currentScene->setGameTimer(m_gameTimer); 170 m_currentScene->initBaseScene(); 171 m_currentScene->sceneBegin(); 172 m_currentScene->resizeEvent( 173 m_devices->getDisplay()->getResolution().getX(), 174 m_devices->getDisplay()->getResolution().getY()); 175 m_currentScene->initialResizeCompleted(); 176 m_currentScene->sceneLogic(); 177 m_currentScene->logic(); 178 179 180 } 181 } 182 183 void SceneManager::defaultEndEventHandler( ALLEGRO_EVENT* evt ) 184 { 185 switch(evt->type) 186 { 187 case ALLEGRO_EVENT_DISPLAY_CLOSE: 188 189 sendQuitMessage(); 190 break; 191 default: 192 break; 193 } 194 } 195 196 void SceneManager::defaultBeginEventHandler( ALLEGRO_EVENT*evt ) 197 { 198 199 m_currentScene->processGuiInputEvent(evt); 200 201 if(evt->type == ALLEGRO_EVENT_TIMER && evt->timer.source == m_gameTimer) 202 { 203 if(m_transitioning) 204 { 205 m_transitionOpacity -= m_transitionRate; 206 207 if(m_transitionOpacity <= 0) 208 { 209 m_transitionOpacity = 0; 210 m_transitioning = false; 211 } 212 } 213 m_needsRedraw = true; 214 m_devices->getAudioManager()->logic(); 215 m_currentScene->sceneLogic(); 216 m_currentScene->processGuiLogic(); 217 m_currentScene->logic(); 218 m_devices->getNetClient()->tick(); 219 } 220 else if(evt->type == ALLEGRO_EVENT_DISPLAY_RESIZE) 221 { 222 m_needsResize = true; 223 m_newScreenWidth = evt->display.width; 224 m_newScreenHeight = evt->display.height; 225 sendResizeMessage(m_newScreenWidth,m_newScreenHeight); 226 } 227 else if(m_needsResize) 228 { 229 m_needsResize = false; 230 231 } 232 } 233 234 void SceneManager::sendResizeMessage( int w, int h ) 235 { 236 //this is a bug in Allegro 237 if(w == 0 && h == 0) 238 { 239 return; 240 } 241 242 al_acknowledge_resize(m_devices->getDisplay()->getContext()); 243 m_g.resizeBuffer(w,h); 244 //stop transition 245 m_transitioning = false; 246 m_transitionOpacity = 0.0f; 247 m_currentScene->processGuiResizeEvent(); 248 m_currentScene->resizeEvent( 249 w,h); 250 Log::write("Scene Manager","Resize Event: Width:" + StringUtil::toString(w) + 251 " Height:" + StringUtil::toString(h)); 252 } 253 254 SceneManagerMessage::SceneEnum SceneManager::getCurrentScene() const 255 { 256 return m_currentSceneType; 257 } 258 259 bool SceneManager::willSceneChange() const 260 { 261 return m_nextScene != NO_SCENE; 262 } 263 264 void SceneManager::startTransition() 265 { 266 m_g.setTargetToBuffer(); 267 m_g.begin(); 268 m_currentScene->render(); 269 m_g.setTargetToBackbuffer(); 270 m_transitioning = false; 271 m_needsTransition = true; 272 m_transitionOpacity = 1.0f; 273 } 274 275}

Thomas Fjellstrom

I'd probably do something similar to the above. Though the limit of only being able to run a single "scene" at once is a bit arbitrary though it saves a bit on work, and allows certain assumptions. I'd probably have an active "stack". So you could have the game running, but then call up the settings screen which would overlay the game, and not kill the game scene.

Or you can get fancier and have an actual scene graph, and have the game view just be a regular view in your graph.

pkrcel

I've seen the "state stack" model applied in some game remakes aroudn the web, but I can't say I'm a fan of it.

What jmsterx supplied is more along the lines fo how I was doing it, albeit I never managed to get through.

Thomas Fjellstrom
pkrcel said:

I've seen the "state stack" model applied in some game remakes aroudn the web, but I can't say I'm a fan of it.

And is that the fault of the underlying concept, or the implementation?

jmasterx

What I'm currently doing could technically work with multiple scenes in a stack.

Much like how it works in iPhone programming, I allow for a single 'RootViewController' and that thing could be a NavigationVC that keeps a stack of scenes in itself.

But at that point, might as well have that in the first place via SceneManager.

The way I did it works fine for me, but a scene graph sounds way cooler.

When I started my game project 4 years ago, I had no idea how to do scenes, and what you see there is what I came up with without researching anything. I might do it differently now if I started over though.

The design forced me to create a class called ClientShared where I keep data structures of the state of the lobby, chat etc while the player is in a game so that I don't have to send the client the full state of the lobby when they return.

pkrcel

And is that the fault of the underlying concept, or the implementation?

Ah more about the concept itself, implementaiton mileage may vary....and of cours eI am not experienced enuff to go over what's basicalli my impression.

Sure enough, I'd prefer for example a graph like you suggest which in my mind might handle better "scene concurrency" and "realtime passing by".

Of course if I think about games of the past (I am nostalgic ;D ) , a scene is actually a "screen" and you VERY RARELY get more than one at a time, and maybe the most common case regardless of game "age" is that you basically have more often than not just two states: game and menu .

In such situations I guess popping and pushing states might make sense and be actually easy to implement and maintain, though when I see a moltitude of "screens" and behind the scenes the engine is preparing->pushing->showing->processing->popping states, I find it difficult to follow along the code, which in my mind makes maintenance (by a third party, which I usually am :-/ ) a pain.

I am aware that I haven't said anything really valuable on the technical point of view :P

beoran

I would normally use a single game loop combined with a state machine for keeping track of the different game states such as start menu, settings, play, pause, etc. If needed the state machine can have a stack to keep track of "previous" states and return to them. For the detailed state, I maintain the GUI state, the engine state and the game state separately in different structs or in the scripting engine.

Thomas Fjellstrom
beoran said:

I would normally use a single game loop combined with a state machine for keeping track of the different game states such as start menu, settings, play, pause, etc. If needed the state machine can have a stack to keep track of "previous" states and return to them.

IMO that's the same as a regular old scene state. Just a bit inside out.

Audric

State machine is not adequate if the same screen can be reached from several ways, ex:
- main menu -> options screen -> (back to) main menu
- (ingame) -> pause menu -> options screen -> (back to) pause menu

A lot of game menu systems can be nicely represented by stack:
main menu (user clicks new game, push "difficulty select" state)
difficulty select (user clicks "easy", pop 1 and push game state)
game (user presses ESC, push "pause" state)
pause (user selects options, push "options" state)
options (user presses esc : pop 1)
pause (user selects "end game" : push "confirm quit")
confirm quit (user selects OK: pop 2)
main menu

beoran

Yes, in such a case a stack is needed, combined with a state machine to keep track of the actual state and to forbid disallowed state transitions. :)

Thomas Fjellstrom

Which ends up being about the same as a scene stack :D

Ariesnl

You can also have "in game" screens and "out game" screens.
In the first case the game is still running realtime. And the screen could be a translucent overlay. In the case of a out game screen you would want to pause the game logic. You couls still draw the game scène and draw the out game screen as a translucent overlay, still showing the paused game. Which looks cool
8-)

Thread #614771. Printed from Allegro.cc