Scene Management
Go to the following links for the source code for fully working examples of managed and unmanaged scenes.
When it comes to games, how you manage your scenes can impact many aspects of your game. It can improve performance by only loading what is necessary for that section of your game, keeping your code organized, and making debugging more effortless. This guide will cover how to manage scenes in your game.
Velaptor comes with a scene manager, which performs routine tasks for you.
Here is a list of the tasks the scene manager handles:
- Life cycle method for loading content
- Life cycle method for unloading content
- Life cycle method for updating
- Life cycle method for rendering
- Scene transition
- Texture batching
What does this mean? Regarding the life cycle methods, this means that the standard loading and unloading of content,
updating, and rendering are invoked for you by Velaptor. You can use these methods if your scene inherits from the SceneBase
class.
Also, SceneBase
handles batching for you. What this means is that in your Render()
methods, you don't have to worry about calling
the IBatcher.Begin()
and IBatcher.End()
methods.
You can also build a custom scene manager for more control. Velaptor does not stand in your way!
This guide will not show you how to manage scenes yourself. That is a more advanced topic. If you are interested, Velaptor is open source! You can see how we built the scene manager and how it works by looking at the source code.
Let's start by creating our first scene.
Step 1: Project setup
Before we start, make sure you have a project set up.
1.1: Setup project
To get started, create a new Velaptor application.
Refer to the Project Setup guide for more info.
1.2: Add content
Follow the Adding Content guide to add the content below to your project. You can download the images below by clicking them and the audio player by clicking the three dots.
Absolutely! Feel free to use any content you want.
Remember, the content has to be a .png
file for images and a .mp3
or .ogg
file for audio.
Step 2: Create Scene A
We will be creating two scenes. One will be named SceneA
, and the other will be named SceneB
. If we want to demonstrate how to navigate
between scenes, we need more than one scene. We will create SceneA
first.
2.1: Create the class
We can take advantage of scenes with various lifecycle methods, similar to the Window
class. These methods
are automatically called by the scene manager to load and unload content, update the scene, and render the scene.
Create a new class named SceneA
and inherit from the abstract
SceneBase
class.
public class SceneA : SceneBase
{
}
2.2: Create override methods
Now, we can take advantage of the lifecycle methods by overriding some of them. Create the following methods in the
SceneA
class.
public class SceneA : SceneBase
{
public override void LoadContent()
{
// You load content in here
base.LoadContent();
}
public override void UnloadContent()
{
// You unload content in here
base.UnloadContent();
}
public override void Update(FrameTime frameTime)
{
// You update the scene here
base.Update(frameTime);
}
public override void Render()
{
// You render the scene here
base.Render();
}
}
2.3: Create loaders and renderers
We must load, use, render, and unload our scene content with our loaders and renderers. We need to create the loaders and renderers as class fields, which will be used throughout the scene's lifetime to load and render the content.
2.3a: Create class fields
public class SceneA: SceneBase
{
private readonly ILoader<ITexture> textureLoader;
private readonly ILoader<IFont> fontLoader; // Used to load the font for rendering the instructions
private readonly ITextureRenderer textureRenderer;
private readonly IFontRenderer fontRenderer; // Used to render the instructions
...
}
2.3b: Set loaders and renderers
Create a constructor and add the following code to set the loaders and renderers.
public SceneA()
{
this.textureLoader = ContentLoaderFactory.CreateTextureLoader();
this.fontLoader = ContentLoaderFactory.CreateFontLoader();
this.textureRenderer = RendererFactory.CreateTextureRenderer();
this.fontRenderer = RendererFactory.CreateFontRenderer();
}
2.4: Load the scene content
Now that we have a way to load content, we can use the loaders to load the content.
2.4a: Texture and font class fields
Add the following class fields to hold the texture and font.
public class SceneA : SceneBase
{
private readonly ILoader<ITexture> textureLoader;
private readonly ILoader<IFont> fontLoader; // Used to load the font for rendering the instructions
private readonly ITextureRenderer textureRenderer;
private readonly IFontRenderer fontRenderer; // Used to render the instructions
private ITexture? logoTexture;
private IFont? font;
...
}
2.4b: Load the content
Now, we can load the content using the LoadContent()
method. The loaders load the texture and font.
public override void LoadContent()
{
this.logoTexture = this.textureLoader.Load("kd-logo");
this.font = this.fontLoader.Load("TimesNewRoman-Regular", 12);
base.LoadContent();
}
Remember, if you have decided to use your content, make sure that the name of the content loaded matches the name of your content file without the extension.
2.5: Unloading the content
Depending on your game, you may or may not need to unload the content. In most cases, if you move to another scene,
you no longer need the content. To free up resources, you can unload the content using the UnloadContent()
method.
To do this, use the loaders to unload your content using the UnloadContent()
method, as shown below.
public override void UnloadContent()
{
this.textureLoader.Unload(this.logoTexture);
this.fontLoader.Unload(this.font);
base.UnloadContent();
}
2.6: Add something interesting
To add something interesting in the scene to see the Update()
and Render()
methods in action, we can use the mouse input
to have the texture follow the mouse as you move it around the screen.
Create class fields to get the mouse's state and remember its position.
We will use the IAppInput<MouseState> mouse
field to get the state of the mouse on every frame.
public class SceneA : SceneBase
{
private readonly ILoader<ITexture> textureLoader;
private readonly ILoader<IFont> fontLoader; // Used to load the font for rendering the instructions
private readonly ITextureRenderer textureRenderer;
private readonly IFontRenderer fontRenderer; // Used to render the instructions
private readonly IAppInput<MouseState> mouse;
private ITexture? logoTexture;
private PointF logoPosition;
private IFont? font;
}
2.6a: Create mouse object
Go into the constructor and use the HardwareFactory
to set the mouse input class field.
public SceneA()
{
this.textureLoader = ContentLoaderFactory.CreateTextureLoader();
this.fontLoader = ContentLoaderFactory.CreateFontLoader();
this.textureRenderer = RendererFactory.CreateTextureRenderer();
this.fontRenderer = RendererFactory.CreateFontRenderer();
this.mouse = HardwareFactory.GetMouse();
}
2.6b: Setting mouse position
Awesome! Now that we have a way to get the state of the mouse. We can now go into the Update()
method and add some
code to get the current position of the mouse.
public override void Update(FrameTime frameTime)
{
var currentMouseState = this.mouse.GetState();
this.logoPosition = mouseState.GetPosition();
base.Update(frameTime);
}
Almost there!
The final piece is to render the texture at the mouse's position on every frame. When you move the mouse, the position of the mouse is remembered and then used to render the texture at that position.
Remember, in most game frameworks and engines, the Update()
is called first, and then the Render()
is called.
Velaptor is no different .
The Update()
method will be called in the same frame as the Render()
method.
2.6c: Render the texture
Add the following code to the Render()
method.
public override void Render()
{
// Convert the `PointF` to a `Vector2`
var logoPos = new Vector2(this.logoPosition.X, this.logoPosition.Y);
// Render the image
this.textureRenderer.Render(this.logoTexture, logoPos);
// Render the text
this.fontRenderer.Render(this.font, Instructions, new Vector2(WindowWidth / 2f, 20));
base.Render();
}
2.7: A little extra safety
Before rendering the texture, check if the content is null before using it to add some extra safety. Not only does this satisfy any warnings for null references, but it will also verify that we remembered to load the content. If we did not load the content, we would get a null reference exception, and then we could fix the problem.
Add the following null check code to the beginning of the Render()
method.
public override void Render()
{
ArgumentNullException.ThrowIfNull(this.logoTexture);
ArgumentNullException.ThrowIfNull(this.font);
// Convert the `PointF` to a `Vector2`
var logoPos = new Vector2(this.logoPosition.X, this.logoPosition.Y);
// Render the image
this.textureRenderer.Render(this.logoTexture, logoPos);
base.Render();
}
2.8: Add some instructions
Next, we will add some simple instructions to the scene and render the instruction text at the top of the window to let the user know what to do.
2.8a: Add constants
Add some class field constants to hold the window's width and the instruction text.
public class SceneA : SceneBase
{
private const int WindowWidth = 1000;
private const string Instructions = "Left & right arrow keys to navigate to scenes.";
private readonly ILoader<ITexture> textureLoader;
private readonly ILoader<IFont> fontLoader; // Used to load the font for rendering the instructions
private readonly ITextureRenderer textureRenderer;
private readonly IFontRenderer fontRenderer; // Used to render the instructions
private readonly IAppInput<MouseState> mouse;
private ITexture? logoTexture;
private PointF logoPosition;
private IFont? font;
}
2.8b: Render the instructions
Add some code to render the instructions at the scene's top.
public override void Render()
{
ArgumentNullException.ThrowIfNull(this.logoTexture);
ArgumentNullException.ThrowIfNull(this.font);
// Convert the `PointF` to a `Vector2`
var logoPos = new Vector2(this.logoPosition.X, this.logoPosition.Y);
// Render the image
this.textureRenderer.Render(this.logoTexture, logoPos);
// Render the text
this.fontRenderer.Render(this.font, Instructions, new Vector2(WindowWidth / 2f, 20));
base.Render();
}
That is it for the first scene!!. We have created our first scene! Now we can move on to creating the second scene so we can demonstrate how to navigate from scene to scene.
Step 3: Create Scene B
The second scene is almost identical to the first. The only differences will be the image, the instruction text, and the mouse behavior. For the behavior, instead of following the mouse, we will simply set the image to the position where the mouse is clicked and then play a sound.
3.1: Copy the other scene
To make things easier, since SceneB
will be almost identical to SceneA
, copy the SceneA
class and rename it to SceneB
.
Also, rename the constructor from SceneA
to SceneB
.
After this is complete, we will make the necessary changes. Do not worry; we will show you what should be removed
and added.
Perform the rename as shown below.
public class SceneA : SceneBase
public class SceneB : SceneBase
{
...
public SceneA()
public SceneB()
{
...
}
}
3.2: Add class fields
Here, we will add some additional class fields. You can use the class fields to hold the audio, which makes a sound every time the user clicks the mouse button and updates the instructions.
public class SceneB : SceneBase
{
private const int WindowWidth = 1000;
private const int WindowHeight = 1000;
private const string Instructions = "Left & right arrow keys to navigate to scenes.\nClick anywhere in the window.";
private readonly ILoader<ITexture> textureLoader;
private readonly ILoader<IFont> fontLoader;
private readonly ILoader<IAudio> audioLoader;
private readonly ITextureRenderer textureRenderer;
private readonly IFontRenderer fontRenderer;
private readonly IAppInput<MouseState> mouse;
private ITexture? logoTexture;
private PointF logoPosition;
private IFont? font;
private IAudio? audio;
private MouseState prevMouseState;
...
}
3.3: Create audio loader
SceneB
uses audio as part of the behavior, which means we must create an audio loader.
public SceneB()
{
this.textureLoader = ContentLoaderFactory.CreateTextureLoader();
this.fontLoader = ContentLoaderFactory.CreateFontLoader();
this.audioLoader = ContentLoaderFactory.CreateAudioLoader();
this.textureRenderer = RendererFactory.CreateTextureRenderer();
this.fontRenderer = RendererFactory.CreateFontRenderer();
this.mouse = HardwareFactory.GetMouse();
}
3.4: Update load content
Now that we have the additional audio loader, we can update our LoadContent()
method.
public override void LoadContent()
{
this.logoTexture = this.textureLoader.Load("kd-logo");
this.logoTexture = this.textureLoader.Load("velaptor-mascot");
this.font = this.fontLoader.Load("TimesNewRoman-Regular", 12);
this.audio = this.audioLoader.Load("mario-jump", AudioBuffer.Full);
// Set the default location of the texture to the center of the window
this.logoPosition = new Point(WindowWidth / 2, WindowHeight / 2);
base.LoadContent();
}
3.5: Update unload content
This scene has an additional type of content. Update the UnloadContent()
method to unload the audio content.
public override void UnloadContent()
{
this.textureLoader.Unload(this.logoTexture);
this.fontLoader.Unload(this.font);
this.audioLoader.Unload(this.audio);
base.UnloadContent();
}
3.6: Change the behavior
The most significant change in SceneB
is the behavior.
The texture's position will be set, and a sound will be played wherever the user clicks the mouse.
public override void Update(FrameTime frameTime)
{
var currentMouseState = this.mouse.GetState();
this.logoPosition = currentMouseState.GetPosition();
ArgumentNullException.ThrowIfNull(this.audio);
var currentMouseState = this.mouse.GetState();
// If the left mouse button was fully clicked
if (currentMouseState.IsLeftButtonUp() && this.prevMouseState.IsLeftButtonDown())
{
this.audio.Play();
this.logoPosition = currentMouseState.GetPosition();
}
this.prevMouseState = currentMouseState;
base.Update(frameTime);
}
3.7: Adjust text position
We will update the Render()
method to render the instructions.
Since SceneB
has larger instruction text, we need to position the text farther down so it is not rendered off the screen.
public override void Render()
{
ArgumentNullException.ThrowIfNull(this.logoTexture);
ArgumentNullException.ThrowIfNull(this.font);
// Convert the `PointF` to a `Vector2`
var logoPos = new Vector2(this.logoPosition.X, this.logoPosition.Y);
// Render the image
this.textureRenderer.Render(this.logoTexture, logoPos);
// Render the text
this.fontRenderer.Render(this.font, Instructions, new Vector2(WindowWidth / 2f, 20));
this.fontRenderer.Render(this.font, Instructions, new Vector2(WindowWidth / 2f, 30));
base.Render();
}
Step 4: Managed Scenes
Let's get into it!
There are two ways to manage scenes. You can either use the built-in scene manager or manage the scenes yourself. Why would you want to manage the scenes yourself? You may want more control over how you manage scenes, or the built-in scene manager does not have a particular feature you need.
Flexibility is important in many kinds of software, but it is even more important in games.
4.1: Update the game class
In the Game
constructor, you will instantiate the scenes and add them to the manager.
You should have created the Game
class in step Step 1.1.
Set the title and the window size in the constructor.
public Game()
{
Title = "Managed Scenes";
Width = 1000;
Height = 1000;
}
4.2: Keyboard scene navigation
We will be using the arrow keyboard keys to navigate between the scenes. Keyboard input is something you should be familiar with.
4.2a: Keyboard input fields
Add the 2 class fields and create a keyboard object.
public class Game : Window
{
private readonly IAppInput<KeyboardState> keyboard;
private KeyboardState prevKeyState;
...
}
4.2b: Create keyboard input object
In the constructor, create the keyboard object.
public Game()
{
Title = "Managed Scenes";
Width = 1000;
Height = 1000;
this.keyboard = HardwareFactory.GetKeyboard();
}
4.3: Create the scenes
Now, we can create instances of the scenes we made earlier in the guide and add them to the scene manager.
4.3a: Create scene instances
public Game()
{
Title = "Managed Scenes";
Width = 1000;
Height = 1000;
this.keyboard = HardwareFactory.GetKeyboard();
var sceneA = new SceneA { Name = "Scene A", };
var sceneB = new SceneB { Name = "Scene B", };
}
4.3b: Add scenes to the manager
public Game()
{
Title = "Managed Scenes";
Width = 1000;
Height = 1000;
this.keyboard = HardwareFactory.GetKeyboard();
var sceneA = new SceneA { Name = "Scene A", };
var sceneB = new SceneB { Name = "Scene B", };
SceneManager.AddScene(sceneA, setToActive: true);
SceneManager.AddScene(sceneB);
}
4.4: Scene navigation behavior
Now, add some logic to the Update()
method. It will allow the user to navigate between the scenes if the left or
right arrow keys are pressed.
protected override void OnUpdate(FrameTime frameTime)
{
var currentKeyState = this.keyboard.GetState();
// If the user has pressed the left or right arrow keys
if (currentKeyState.IsKeyDown(KeyCode.Right) && this.prevKeyState.IsKeyUp(KeyCode.Right))
{
SceneManager.NextScene();
}
else if (currentKeyState.IsKeyDown(KeyCode.Left) && this.prevKeyState.IsKeyUp(KeyCode.Left))
{
SceneManager.PreviousScene();
}
this.prevKeyState = currentKeyState;
base.OnUpdate(frameTime);
}
That is it! You have created a game with two scenes and can navigate between them using the arrow keys. As you can see, the code for managing the scenes is straightforward and minimal. The scene manager handles all the content loading, unloading, updating, and rendering.
Step 5: Run it!
Run the application! You should see the two scenes shown below.