Lesson 12 - Tetris in MonoGame: Game Scene Management
In the previous lesson, Tetris in MonoGame: Level Features, we improved the block rotation and added a ghost block into the level. Now the Tetris game can be considered as complete. You can, of course, add another mechanics to it, such as power-ups, game modes, etc.
Today we're going to focus on game scenes, one of which could be, for example, the game menu.
Game Scenes
We know that MonoGame isn't an engine, but a framework. Therefore, MonoGame itself, apart from components, doesn't provide any way to manage and switch between game scenes. By a game scene is meant some separate part of the game, such as the level, the main menu, the score table, the credits, and so on. The main menu would certainly have a different logic than the Tetris level. We also need to be able to switch between those game scenes and store their state.
There are, of course, many ways to do that. The most silly way would be to write the whole game code into a single file and introduce many states in it (the menu state, the game state, etc.). The final file would probably look very messy. Since we know how to use game components, we'll definitely do so. Sometimes you can see projects that have components for each individual scene. These components are then being switched (e.g. we switch from the Menu component to the Level component). This may be sufficient for small games, but not for larger projects. The problem is that we'd be limited to have just one component per scene. Then we couldn't have, for example, the clouds, level, and other parts of a complex project as separated components.
The solution we're going to show here defines a game scene as a set of components. All components are part of the game project, and after switching the current scene, only those that are used in that particular scene are enabled, while all the other components are disabled.
Let's add a GameScene
class to the project. Its instances will
represent individual game scenes. We'll make the class public
and
add two private fields in it. The first one will be a collection of components
that the scene consists of, and the second one will be the
RobotrisGame
instance:
private List<GameComponent> components; private RobotrisGame robotrisGame;
To use the GameComponent
type, we need to add the following
using
statement:
using Microsoft.Xna.Framework;
We'll add a public AddComponent()
method that stores the passed
component in the private components
collection. The passed
component is also stored in the game's Components
collection, but
it can only be added when it's not already there. Remember that some scenes may
use the same components. The method will look like this:
public void AddComponent(GameComponent component) { components.Add(component); if (!robotrisGame.Components.Contains(component)) robotrisGame.Components.Add(component); }
Because the collection is private, adding components must be done using this
method, which ensures that the components will be added into the game's
Components
as well.
In the class constructor, we'll pass the game instance as a parameter, just
as we do in other components and game objects. Also, we'll use the
params
keyword, which allows us to enter multiple
GameComponent
instances as other parameters. We'll loop through
them and add them using our method.
public GameScreen(RobotrisGame robotrisGame, params GameComponent[] components) { this.robotrisGame = robotrisGame; this.components = new List<GameComponent>(); foreach (GameComponent component in components) { AddComponent(component); } }
As the last method we'll add ReturnComponents()
, which will
return the components used by the game scene as an array:
public GameComponent[] ReturnComponents() { return components.ToArray(); }
To make it complete, you can add your own method that'll remove a component. It may be handy in larger projects, but for our needs it's not necessary.
Menu
To have something to test on, we'll add MenuComponent
into our
game project (add it in the Components/
folder, but keep
the namespace only as Robotris
). It'll be another
DrawableComponent
. We've shown how to add one in the Dividing
a MonoGame Project into Components lesson. Just to be sure, here's the class
code:
public class MenuComponent : Microsoft.Xna.Framework.DrawableGameComponent { private RobotrisGame robotrisGame; public MenuComponent(RobotrisGame robotrisGame) : base(robotrisGame) { this.robotrisGame = robotrisGame; } public override void Initialize() { base.Initialize(); } protected override void LoadContent() { base.LoadContent(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public override void Draw(GameTime gameTime) { base.Draw(gameTime); } }
We'll leave the component blank for now.
Managing Game Scenes
We'll put the game scene management into the RobotrisGame
class.
It makes sense and is easily accessible from all components. Another option
would be to create a separate game scene manager.
Le's go to RobotrisGame.cs
where we'll add two public game
scenes as class fields. These will be the menu and level scenes:
public GameScene menuScene, levelScene;
We'll create the menu component in Initialize()
, next to the
other components:
MenuComponent menu = new MenuComponent(this);
We'll completely remove the part adding components to the game's
Components
collection. That's because the game scene already does
that for us. Instead, we'll instantiate the game scenes:
menuScene = new GameScene(this, clouds, menu); levelScene = new GameScene(this, clouds, level);
We can see that the same Cloud component instance can be used in multiple scenes.
We'll add a private method for enabling and disabling the scenes, taking the
component and the enabled state (true
/false
) as its
parameters. MonoGame's GameComponent
has the Enabled
property that enables/disables its Update()
method execution. If we
set it to false
, the component stops working. If the component is
of the DrawableGameComponent
type (which is in most cases), we also
have to set the Visible
property, which specifies whether the
Draw()
method is being executed and thus whether the component is
rendered.
private void ChangeComponentState(GameComponent component, bool enabled) { component.Enabled = enabled; if (component is DrawableGameComponent) ((DrawableGameComponent)component).Visible = enabled; }
If we disable the component this way, it won't be rendered nor updated, but it'll still exist and keeps its state until it's enabled again. This may sometimes be very useful (e.g. for switching between different locations, minigames, starting a level from the menu, etc.).
Let's go back to Initialize()
. We'll loop through all the game
components and disable them, right after creating the scenes instances:
foreach (GameComponent component in Components) { ChangeComponentState(component, false); }
Finally, we'll add the scene switching method itself. It'll be public and take the scene we want to switch to as its parameter.
public void SwitchScene(GameScene scene) { }
First, we'll get the components used by the scene:
GameComponent[] usedComponents = scene.ReturnComponents();
Then we'll check all components in the array to see whether they're used, and change their state accordingly.
foreach (GameComponent component in Components) { bool isUsed = usedComponents.Contains(component); ChangeComponentState(component, isUsed); }
We'll also update the previous keyboard state in the method, because switching the components causes it to skip updating:
previousKeyboardState = keyboardState;
We'll then switch to the current scene at the end of the
LoadContent()
method (so after everything has been loaded):
SwitchScene(menuScene);
Now when we run the game, we should see the menu scene, which is currently just clouds.
Let's move to the Update()
method of MenuComponent
,
and add a piece of code that'll start the game after pressing the
Enter key:
if (robotrisGame.NewKey(Keys.Enter))
robotrisGame.SwitchScene(robotrisGame.levelScene);
Now let's try our scene. We can see we got the switching functionality.
In the next lesson, Tetris in MonoGame: Game Menu, we'll focus on the game menu
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 35x (15.99 MB)
Application includes source codes