General structure and design of the Acclimate Engine [Part 2]

Published April 03, 2024
Advertisement

Welcome back. In last weeks entry, we looked at the base-layout of the Acclimate Engine's resource-system. This week, I'll be highlighting the runtime-part of the system - the diagram from last week still applys, so I won't be posting it again.

Runtime

Now to recap, the games resources are handled via a GameResource, which consists of Extension-classes, which are both defined by the engine, or a plugin/user. For the runtime-side of things, it looks quite similar - a GameInstance-class is created, consisting of a number of GameSystems, The methods that these systems can extend, give them access to hook into the runtime-process. Let's look at an example:

class SoundGameSystem final :
	public game::GameSystem<SoundGameSystem>
{
public:
	SoundGameSystem(void) noexcept;
	~SoundGameSystem(void);

	void PlayBackgroundMusic(const audio::File& file, float volume);

	static void OnRegister(sys::StringView strFilter);
	[[nodiscard]] static game::GameSystemDeclaration GenerateDeclaration(void) noexcept;

private:
	void Init(const game::GameContext& game) override;
	void Start(void) override;
	
	void Update(DeltaTime dt) override;

	audio::GameSystem* m_pSystem;
	sys::Pointer<audio::BaseEmitter> m_pBackgroundEmitter;
};

This is the sound-system, responsible for playing the background-music for 2D games. The methods you have available are similar to what a MonoBehaviour could do in Unity - you get callbacks for Initialization, when the Game is starting, as well as updating. One key difference, again, is the access to other systems. This SoundGameSystem is only an extension, specific to 2D games, so it doesn't contain all the logic required for audio. Instead, it uses the engines core audio::GameSystem to create a game-specific audio-emitter. Similar to Extensions, this is done via the GameContext, which let's game-systems access other systems of eigther game or engine scope:

void SoundGameSystem::Init(const game::GameContext& game)
{
	m_pSystem = &audio::GameSystem::Get(game);
}

One new thing is the ability to register calls for the scripting-system. The visual script-system actually abstracts away the access to those systems, so if we want to be able to play a specific music-piece via script, we first register like this:

void SoundGameSystem::OnRegister(sys::StringView strFilter)
{
    event::registerBytecodeBindFunction(&SoundGameSystem::PlayBackgroundMusic, strFilter, L"PlayBackgroundMusic(file, volume=1.0f)");
}

And then we can use the corresponding node:

Practical benefits & applications

Now, let's talk about some of the real practical benefits of this system. Similar to the Resources/Extensions, the Runtime-Systems are not singletons or global classes. They can be instantiated, by creating a GameInstance, which is entirely detached from all other GameInstances. But why, you might ask, would one want more than one of these? There is actually a lot of cases where this could make sense:

Editor/Tooling

Since we can create individual GameInstances, we can use those in the editor to create diverse tools. For example, our Teleporter-system offers a visual picker, that let's you link a target teleporter, by allowing inspection of the target-scene:

This is an extra window that comes up, and allows inspecting scenes other than the one that is currently loaded

This tool is opened aside the normal editor-view, and can simulate it's own version of the game-world, via it's own GameInstance. GameInstances can be rendered separately as well, so we can create a GameView, and set our pickers' GameInstance, and boom, we now got two different versions of the game running. And there is a lot of tools like this - we have a render-previewer, save-game-viewer, etc… all are pretty simple, because the code is simply:

void ScenePreviewController::SetGame(const game::GameResource& game)
{
	m_pGame = m_ctx.base.game.games.CreateRuntime(game, m_ctx.base);
	// do stuff with it
}
Preview of a render-graph

I'll probably show a bit more once we get into scene-managment. But the applications are just very widespread. Even something like PlayMode in the editor can easily be setup with this system. Instead of having to persist the state before entering PlayMode, and having to apply it again, we can simply create a new GameRuntime, set it as our active runtime for the editor, and once we are done with PlayMode, we destroy it, and set the old one.

Applications for the games

For the games itself, the applications are a bit more limited, but they are still there. For example, my current games save-system shows a preview of the state of the save:

Now, normally, you'd need to create a separate representation of that data to be loaded and shown. However, since we can just create distinct instances of the game-world, we can use this system to read the data of the actual save:

// created once at start
m_pRuntime = m_game.pBase->game.games.CreateRuntime(m_game.pGame->GetGame(), m_game);


// load the save to be previewed
auto& saveSystem = game::SaveStateGameSystem::Get(*m_pRuntime);
saveSystem.LoadDirect(strFullFile, false, false);
				
// access the player-stats to get the level				

const auto& stats = PlayerStats::Get(runtime);
const auto level = std::min(uint8_t(99), uint8_t(stats.GetCurrentLevel()));

Let's go through it real quick. We create one GameRuntime to be able to hold the saves game-world state. Then, later, we can fetch that GameRuntimes SaveStateGameSystem - savegames are also implemented via systems - and load the save into that. Then, we can access the stats from the actual systems the are used to implement them. Again, this save is loaded in separation, so this code can be executed alongside the running game with no issues.

Actually, since it's just a 2D game with fast load-times, we also just render the save-preview directly from that GameRuntime. However, for games where fully loading a scene would take too long, we could explicitely save this as a texture, and still use the stat-query system. The loading-process is segmented to where we can just load the base data, not all the state required for rendering. This should still be fast enough for pretty much any game.

Well, it's a neat little trick and keeps things more simple for actually implementing the game and it's save. It's not really groundshattering like the editor-parts, but it also doesn't require anything that is not being present already, so making use of it is nice.

Limitations

The main limitation here is that I currently don't support being able to execute multiple GameRuntimes in parallel. This is mostly due to some particular optimizations, in regards to data that the script-system accesses. Static variables for example are placed in the current code-segment and accessed in relation to the current instruction-pointer. We would need at least 1 additional indirections, if multi-runtime execution were to be executed - and that's also true in a few other places. It could be supported without too many problems, though practically I just don't have a need right now, so I don't bother. Being able to put GameRuntimes in a state where they can be inspected and rendered is enough for all my use-cases.

Wrapping it up

Alright, now we have both resources+runtimes discussed. Next time, I'll show more of the scene-managment, which uses a similar system, but with it's own unique use-cases.

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement