Blazor GameDev – part 10: the Scene Graph
Hi All! Here we go with part 10 of our Blazor 2d GameDev series. Today we’re going to talk about an extremely important tool that can greatly improve game entities management: the Scene Graph.
This is an example, just to give you an idea of what is going to be the result:
You can also check it in your browser before moving on.
In our last episode, we introduced Finite State Machines. They greatly help cleaning up the mess of procedural code that very often may arise when writing a game. But still, we are adding all our Game Objects to the Game without any kind of relationship. What if we want to model a Solar System?
Or, more formally: what if we want to represent parent-child relationships, with the state of the parent influencing the state of its children.
There’s a long list of different applications we can have for a Scene Graph, for example handling collisions. Imagine a first-person shooter game: when a bullet is fired against an enemy, there’s no need to check for collisions on an arm if the bullet didn’t collide with the whole body bounding box.
Let’s start by defining a very simple SceneGraph class, something like this:
public class SceneGraph { public SceneGraph() { Root = new GameObject(); } public async ValueTask Update(GameContext game) { if (null == Root) return; await Root.Update(game); } public GameObject Root { get; } }
The Root node is a simple empty GameObject, that will act as a “global container” of all our entities. This will be just a starting point for a simple game. It is missing a lot of functionalities (eg. multiple roots, search, removal), but will work for now. Also, keep in mind that in a game you already have the general structure of the entity tree. A different case would be in an editor, but let’s leave it for another time.
The next thing we have to do is update the GameObject class and add parent/child relationship:
public class GameObject { private readonly IList<GameObject> _children; public GameObject() { _children = new List<GameObject>(); } public IEnumerable<GameObject> Children => _children; public GameObject Parent { get; private set; } public void AddChild(GameObject child) { if (!this.Equals(child.Parent)) child.Parent?._children.Remove(child); child.Parent = this; _children.Add(child); } }
We’ll also need to make few changes to its Update() method:
public async ValueTask Update(GameContext game) { foreach (var component in this.Components) await component.Update(game); foreach (var child in _children) await child.Update(game); }
The idea is to first update all the Components and then move to the Children collection. The flow is basically a preorder DFS, which is fine most of the time. Another option would be using a BFS, but that would make the code a bit more complex to follow.
Now in our initialization code all we have to do now is instantiating the Scene Graph instance and start adding nodes to it:
var sceneGraph = new SceneGraph(); var player = new GameObject(); var sceneGraph.Root.AddChild(player); var enemies = new GameObject(); var enemy1 = new GameObject(); enemies.AddChild(enemy1); var enemy2 = new GameObject(); enemies.AddChild(enemy2); var sceneGraph.Root.AddChild(enemies);
Now, in our Solar system game, we want the planets to rotate around the sun and satellites to do the same around planets instead. Using a Scene Graph this becomes extremely easy by updating the TransformComponent class:
public class TransformComponent : BaseComponent { private Transform _local = Transform.Identity(); private Transform _world = Transform.Identity(); public override async ValueTask Update(GameContext game) { _world.Clone(ref _local); if (null != Owner.Parent && Owner.Parent.Components.TryGet<TransformComponent>(out var parentTransform)) _world.Position = _local.Position + parentTransform.World.Position; } public Transform Local => _local; public Transform World => _world; }
We basically inherit the parent’s world position and simply use it as offset to the local position of the current Game Object.
That’s all for today! Next time will see how to improve the asset loading code.
Ciao!