Lesson 5 - Dividing a MonoGame Project into Components
In the previous lesson, Sounds, Music, Keyboard, and Mouse Input in MonoGame, we showed how to play sounds and process keyboard and mouse input.
Today, we're going to talk about the game architecture, and how to divide it into several separate components. After today's lesson we'll have everything prepared to start programming the Tetris game logic.
I think it's obvious that the game will have multiple scenes. Those will be the menu, the level itself, the credits screen with the game authors, the online high score table and so on. Each scene will then consist of other components, such as the falling blocks, the game area, or the endless clouds we programmed in the previous lessons.
If we put the whole game code into a single class, it'd be very messy. There are several ways to decompose the game code into multiple classes. During the entire course we'll take a look at all of them to see their pros and cons. In the course we'll also create a simple component-object architecture.
Game Components
One of the ways to decompose a game into several parts is called game components. These are already available for us as part of the MonoGame framework. Most tutorials don't talk about them very much, although it's a fairly good way to keep things nice and neat.
The game component is the most independent part of the game with as least
dependencies as possible. Its internal structure resembles the game itself, more
specifically, the Game
class. It has the same methods, such as
LoadContent()
, Initialize()
, Update()
and
so on. We can think of it as a game in our game. The component should be
independent as much as possible. This condition is met by our endless clouds, so
let's move it into a component.
Let's remove all the unnecessary experiments we created in the previous
lessons from the game code, especially the mouse routines and some of the
rendering. Specifically, let's remove mouseState
,
robotRectangle
, and everything associated with it, the text
rendering and the Enter key behavior. We'll keep our keyboard and
music methods, as well as the moving clouds and the other two backgrounds. We'll
keep the entire game Content
.
To the Robotris
project, add a new component and name it
CloudsComponent
(right-click the Robotris
project in
Solution Explorer -> Add -> New Item -> Class):
Visual Studio has generated a class which we'll inherit from
Microsoft.Xna.Framework.GameComponent
. The instance of our game
(more precisely of it's ancestor, the Game
class) is passed in the
constructor.
The component will look like this:
public class CloudsComponent : Microsoft.Xna.Framework.GameComponent { public CloudsComponent(Game game) : base(game) { } }
The LoadContent()
, Initialize()
, and
Update()
methods are inherited and can be overridden.
Unfortunately, there's no template for creating components in MonoGame, so we have to create the component manually like this.
Renderable Components
A GameComponent
cannot be rendered. Although such components may
be useful in our game as well, we need the rendering for the clouds. Luckily,
MonoGame offers one more component type, namely
DrawableGameComponent
. We'll change our GameComponent
to this type in the class header:
public class CloudsComponent : Microsoft.Xna.Framework.DrawableGameComponent
We need to pass the common game data to the component somehow. We can do this
by modifying the constructor parameter type to our game class. We just need to
change it from Game
to RobotrisGame
. Next, we'll add a
private robotrisGame
field of the RobotrisGame
type
into the component, and store the instance passed in the constructor to that
field. The modified component constructor will look like this:
public CloudsComponent(RobotrisGame robotrisGame) : base(robotrisGame) { this.robotrisGame = robotrisGame; }
Now let's add the LoadContent()
, Initialize()
,
Update()
, and Draw()
methods to the component. Just
type public override
... to make Visual Studio offer you a list of
methods that we can override:
The component structure is ready. Since we're going to create more similar components in the future, showing it's source code here will become handy:
public class CloudsComponent : Microsoft.Xna.Framework.DrawableGameComponent { private RobotrisGame robotrisGame; public CloudsComponent(RobotrisGame robotrisGame) : base(hra) { 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); } }
When you'll use this code to create another component (which we'll be doing in a moment), be sure to change the class name and the constructor name as well.
The class isn't documented, feel free to add comments in it. Now we'll move
all the clouds code from our game class into the component. It should be simple,
we just have to access Content
in LoadContent()
via
robotrisGame.Content
. In the same way we'll need to access
spriteBatch
via robotrisGame.spriteBatch
inside
Draw()
. We don't have it public, so we'll modify it in the game
class:
public BetterSpriteBatch spriteBatch;
Inside the component's Draw()
method, remember to add both the
Begin()
and End()
method calls, just like in the
game's Draw()
method!
Today we write a lot of code. Your code should be similar to something like this:
public class CloudsComponent : Microsoft.Xna.Framework.DrawableGameComponent { private int change; private int direction; private Texture2D clouds; private Vector2 position; private RobotrisGame robotrisGame; public CloudsComponent(RobotrixGame robotrisGame) : base(robotrisGame) { this.robotrisGame = robotrisGame; } public override void Initialize() { position = new Vector2(0, 0); change = 0; direction = 1; base.Initialize(); } protected override void LoadContent() { clouds = robotrisGame.Content.Load<Texture2D>(@"Sprites\spr_clouds"); base.LoadContent(); } public override void Update(GameTime gameTime) { // moving clouds to the left position.X--; // return to the start position when out of screen if (position.X < -(clouds.Width)) position.X = 0; // clouds color change according to the direction change += direction; if (change >= 96) direction = -1; if (change <= 0) direction = 1; base.Update(gameTime); } public override void Draw(GameTime gameTime) { robotrisGame.spriteBatch.Begin(); Color color = new Color(128 + change, 255 - change, 128 + change); for (int i = 0; i < 6; i++) robotrisGame.spriteBatch.Draw(clouds, new Vector2(position.X + i * clouds.Width, 0), color * 0.8f); robotrisGame.spriteBatch.End(); base.Draw(gameTime); } }
We'll finish it later. Now we'll add another renderable component to our
game. We'll name it LevelComponent
, and it'll contain the gameplay
logic itself. We haven't implemented that yet, but we'll at least render the
robot background. Surely you can do it. The steps are similar as for the clouds
- move the background rendering from the game class into this component.
The result will look like this:
public class LevelComponent : Microsoft.Xna.Framework.DrawableGameComponent { private RobotrisGame robotrisGame; private Texture2D background; public LevelComponent(RobotrisGame robotrisGame) : base(robotrisGame) { this.robotrisGame = robotrisGame; } public override void Initialize() { base.Initialize(); } protected override void LoadContent() { background = robotrisGame.Content.Load<Texture2D>(@"Sprites\background_level"); base.LoadContent(); } public override void Update(GameTime gameTime) { base.Update(gameTime); } public override void Draw(GameTime gameTime) { robotrisGame.spriteBatch.Begin(); robotrisGame.spriteBatch.Draw(background, new Vector2(0, 0), Color.White); robotrisGame.spriteBatch.End(); base.Draw(gameTime); } }
Using Components in the Game
What's left in the game class is now only the rendering of the block
background in Draw()
and the keyboard state processing in
Update()
. Now let's move to Initialize()
and create
instances of our components, to which we pass the game class instance in their
constructors:
CloudsComponent clouds = new CloudsComponent(this); LevelComponent level = new LevelComponent(this);
Now comes the magic! The game has the Components
property, which
is the collection (List
, to be more precise) of the components the
game is made of. We'll add our components into this collection in the order we
want them to be updated and rendered:
Components.Add(clouds); Components.Add(level);
Let's run it. See that the game runs as well as before. The components are
updated all by themselves and we don't have to do any extra work. The game code
is clear and very short. The components contain only what is relevant to them.
Everything works because of calling base.Initialize()
,
base.Update()
, and other in the game methods which results in
calling these methods in all the active game components as well.
Just a few final aesthetic changes - let's rename the Game1.cs
file to RobotrisGame.cs
. Also, since there will be more components,
we'll create a folder for them (right-click on Robotris -> Add -> New
Folder) and name it Components/
. Now move both of our components in
it.
We've reached the milestone from which we can finally start to implement the Tetris logic
We'll take a look at it in the next lesson, Tetris in MonoGame: Block.
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 51x (15.06 MB)
Application includes source codes