Adding a Simulation system
Don't worry if you don't fully understand all the terms from the previous tutorial, you'll gain familiarity as we get more hands-on.
In this tutorial, we're going to add a system to the Server's Simulation. Our system is going to be pretty simple: every few seconds, it's going to replace the floor sprite that each entity is standing on with a sprite of our choosing. This will let us dip our toes into modifying the tile map, and interacting with the entity data!
1. Creating the new system's files
The easiest way to start a new system is to copy an existing one. In our project, we can see all the Server Simulation systems by looking in the Source/Server/Simulation directory. Take this opportunity to look through the existing systems and see what they all do.
TeleportSystem
happens to be the closest to what we're going to build, so copy+paste TeleportSystem.h and TeleportSystem.cpp to duplicate them, then name the new files TestSystem.h and TestSystem.cpp.
Any time we add files, we need to tell the build system that the new files exist. Open CMakeLists.txt in the same Simulation directory, and add the files to the list:
target_sources(Server
PRIVATE
(other files...)
Private/TestSystem.cpp
PUBLIC
(other files...)
Public/TestSystem.h
)
2. Adding logic to the new system
Starting in TestSystem.h, we're going to rename the class and rip out the stuff we don't need. We'll keep the World
dependency since we need it to get the entity data and modify the tile map, and the Timer
so we can make sure our logic only runs once every few seconds:
#pragma once
#include "Timer.h"
namespace AM
{
namespace Server
{
// Forward declaration
class World;
class TestSystem
{
public:
TestSystem(World& inWorld);
void runCode();
private:
World& world;
Timer updateTimer;
};
} // End namespace Server
} // End namespace AM
In TestSystem.cpp, we're going to set everything up, then I'll explain what's going on:
#include "TestSystem.h"
#include "World.h"
namespace AM
{
namespace Server
{
TestSystem::TestSystem(World& inWorld)
: world{inWorld}
, updateTimer{}
{
}
void TestSystem::runCode()
{
}
} // End namespace Server
} // End namespace AM
At the top, we've included the headers for our TestSystem
and our World
dependency (we'll talk more about dependencies later).
Below the includes, we've replaced all mention of TeleportSystem
with TestSystem
, renamed our function to runCode()
, and removed the contents of the function.
Now, let's talk about what this system is actually going to do. Every 5 seconds, it's going to iterate through every entity, determine which tile they're standing on, and replace that tile's floor sprite. Here's what that looks like, take some time to get familiar with it:
void TestSystem::runCode()
{
// If the timer has been running for 5 seconds.
if (updateTimer.getTime() >= 5) {
// Iterate through every entity that has a Position component.
auto view{world.registry.view<Position>()};
for (entt::entity entity : view) {
// Get this entity's position component.
Position& position{view.get<Position>(entity)};
// Replace the floor sprite (layer 0) of the tile that the entity
// is standing on.
// Note: We need a sprite to use, so I randomly chose the sprite with
// ID == 1.
TilePosition tilePosition{position.asTilePosition()};
std::size_t layerIndex{0};
int spriteNumericID{1};
world.tileMap.setTileSpriteLayer(tilePosition.x, tilePosition.y,
layerIndex, spriteNumericID);
}
// Reset the timer.
updateTimer.reset();
}
}
This is a cool example because we get to use both the entity registry and the tile map (we use the registry to get all entities that have a Position
component, then we use their position to update the tile map). These two classes are the main ways that we can interact with the world, and will certainly be useful in any future systems that you build!
If you want to learn more, you can look up TileMapBase.h, read the entt documentation, or look around at how our other systems use these classes.
From this example, we can learn some things about the engine:
- The world map is tile-based.
- Each tile contains multiple sprites, organized into layers.
- Layer 0 typically contains the floor sprite, (since it's the lowest layer).
- Sprites have a numeric ID (they also have string IDs that we more commonly use, but that will be covered by a different tutorial).
- Calling
setTileSpriteLayer()
was enough to get update messages sent to the clients (we didn't need to explicitly send a map update to everyone). This is the case for tile map updates and entity movement, but other things may require your system to send its own messages. We'll cover that in a later tutorial.
3. Adding the system to an extension class
We've built our new system, but it currently isn't being called anywhere. If you recall, the "module extension classes" are what let us tell the engine to run our code. Let's add our new system to the extension class. Open SimulationExtension.h (still in the Source/Server/Simulation directory).
At the top of this file, you'll see a place where we include the existing systems. Add our system to the list:
#include "ISimulationExtension.h"
#include "SimulationExDependencies.h"
(other systems...)
#include "TestSystem.h"
At the bottom, you'll see a place where we add the systems as members of the SimulationExtension class. Add our system there, too:
private:
(other systems...)
TestSystem testSystem;
In the middle of this file, you'll see all of the hooks—functions where systems can be called. Read through the comments if you'd like, then open SimulationExtension.cpp.
Our new system isn't very dependent on where it's placed within the simulation loop, so we're just going to add it to beforeAll()
. We're also going to add our new system to the constructor's initializer list:
SimulationExtension::SimulationExtension(SimulationExDependencies deps)
(other systems...)
, testSystem{deps.world}
{
}
void SimulationExtension::beforeAll() {
testSystem.runCode();
}
Adding testSystem.runCode()
to beforeAll()
means that it will now be called once per Simulation tick.
Note that we're passing deps.world
to TestSystem
here. The World
class is just one of the dependencies that the engine provides us with. To see what else is available, you can look at SimulationExDependencies.h (be sure to look at the Server version, Client also has a SimulationExDependencies).
Each module has an ExDependencies class like this. When you're thinking up how to implement something, check out these headers and drill down deeper to see what tools are at your disposal!
4. Running our new system
Now, we just build the project and run it as before (launch the server, then the client). If all goes well, you should see a sprite appearing under your character every 5 seconds!
At this point, you're expected to be a bit confused about how exactly beforeAll()
fits into the rest of the engine logic. Don't worry, we'll cover that in the next tutorial!