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

Published April 14, 2024
Advertisement

Welcome back. Last time, we looked at the runtime-extensibility of the Acclimate Engine (https://www.gamedev.net/blogs/entry/2277862-general-structure-and-design-of-the-acclimate-engine-part-2/.​ Now we are going to see, how this all applies to scene-managment.

The basics

In general, scene-managment is also implemented using the aformentioend Resource/Runtime-system. A SceneGameExtentsion us provided to allow us to handle scene-loading, and a SceneGameSystem to handle execution and scene-switching (funny names, I know). Using both hooks into the main game processing, we then do the same for scenes, by splitting it into a SceneResource and SceneRuntime:

SceneResource

A SceneResource also consists of multiplex SceneExtensions, so it's very similar to a GameResource. However, it's applications are a bit more varied. In practice, only one GameResource will exist - after all, we only run a single game - but multiplex SceneResource will exist, one for each scene. This gives the ability to handle certain things a bit better than other engines setups. Let's look at one such example, audio.

Now I'm currently making a 2D-game, whose game-world is divided into many Scenes. Each Scene has it's own default background music set. In Unity, we'd have to add a MonoBehaviour to the scene, to be able to set this, and Unreal would be very similar. However, in my own engine, I can define a SceneExtension to hold that property:

/****************************
* SoundSceneExtention
****************************/


// yes, I didn't know how to spell Extension back when I wrote all this...
class SoundSceneExtention final :
	public game::SceneAttributeExtention<SoundSceneExtention>
{
public:
	[[nodiscard]] bool ChangeMusicOnEnter(void) const noexcept;
	[[nodiscard]] const audio::File* GetBackgroundMusic(void) const noexcept;

	[[nodiscard]] static Declaration GenerateDeclaration(void) noexcept
	{
		return 
		{
			L"SceneAudio",
			{
				{ L"ChangeMusicOnEnter", core::bindAttribute(&SoundSceneExtention::m_changeMusicOnEnter) },
				{ L"BackgroundMusic", core::bindAttribute(&SoundSceneExtention::m_pBackgroundMusic) },
			}
		};
	}
private:

	bool m_changeMusicOnEnter;
	sys::SharedPtr<audio::File> m_pBackgroundMusic;
};

Aside from the file, we also need a bool to define if the music should be changed here (as there are scenes that simply do not update the currently played music). And this time, there is nothing left out-this is the whole class, minus the one line of implementation in each of the getters. The effect of defining such an Extension, is that we can now set both fields when inspecting the scene:

This doesn't require the scene to be loaded at all, and allows multi-editing, which is really neat compared to having a BackgroundAudioManager lying in every scene.

SceneRuntime

Each Scene has one Resource, but like with game, multiple Runtimes can exist for each scene - in fact, multiple active GameRuntimes can each have their own variants of scenes Runtime. While I'm in general not a fan of much global data or singleton; it should be remarked that such a system is only possible due to relying on as little global state as possible, so that each instance of a SceneRuntime accesses only the variants of other Runtimes that belong to itself and it's parent. But, as mentioned in the last articles, due to contexts and a simple and fast system for getting those variants, it's not complicated at all.

Ok, now SceneRuntime. Those allow us to hook into the scenes runtime-processing via SceneSystem. Start, Update, all those things. For our background-music handling, we cannot really have the audio-emitter placed in a SceneSystem though, as going to another scene with the same BGM should keep it playing. Thus, we already have a GameSystem defined to handle that part, however the SoundSceneSystem is then responsible for triggering which audio play, depending on what we've set:

void SoundSceneSystem::Init(const game::SceneContext& ctx)
{
	m_pSystem = &SoundGameSystem::Get(ctx);
	m_pExtention = &SoundSceneExtention::Get(ctx);

	m_pBackgroundMusic = m_pExtention->GetBackgroundMusic();
}

void SoundSceneSystem::Start(void)
{
	if (m_pExtention->ChangeMusicOnEnter())
		m_pSystem->PlayBackgroundMusic(m_pBackgroundMusic, 1.0f);
}

While SoundGameSystem has more methods, in order to allow us to change the background-music via scripts.

Practical benefits

Gameplay-programming

One very neat application, that wouldn't base as easy in the U-engines, is handling transitions between scenes. Each teleporter between scenes has a small animation of fixed length. When you first hit a teleporter, and you leave to a scene with a different music, a fade-out of exactly that length should be triggered. To do that, we need to know what music the target-scene will be playing.

In the Rpg-Maker, I used to set this for each teleporter manually - puke - and in the big Us, you'd probably need some script to pre-process what each StupidSceneAudioManager had set, to be able to access from other scenes. Here, we just get this information for free:

In our Teleporter-base class script, we can access which scene we are teleporting to when triggering the screen-fade, and then execute a method to check if we also need music to change. The method is implemented in SoundGameSystem:

bool SoundGameSystem::SceneRequiresMusicChange_Ev(game::Scene& scene) const noexcept
{
	const auto& extension = SoundSceneExtention::Get(scene);

	if (extension.ChangeMusicOnEnter())
	{
		const auto* pFile = SoundSceneSystem::EvaluateBackgroundMusic(extension, game::SceneGameSystem::Get(m_ctx).GetActiveLayers(scene));
		if (m_pBackgroundEmitter && pFile != &m_pBackgroundEmitter->GetFile())
			return true;
	}

	return false;
} 

A SceneExtension with all it's attributes exists on a scene even before the scene is fully loaded, we call this state “PreLoaded”. We currently simply do this when a scene is discovered (in editor), or after creating all SceneRuntimes (in build), as the operations are quite cheap, especially in build (since no complicated data needs to be processed then).
Thus we can check if the music changes when entering the scene; then compare to what is currently playing to see if we need the transition.

Loadtimes

On other nice thing is that we can totally control loading of resources VS instantiation and playing. A SceneResource can be put into a loaded state, which will load everything from disk, including assets, without needing to actually create a SceneRuntime. This achieves what Unity does with it's stupid “activateScene” flag, that stops loading at 90% (why 90%? Screw you, that's why) and then requries you to set the flag back to “true” to finalize scene-load.

Here, if we want to pre-load a scene, we simply load it's Resource. If we want to instantiate a SceneRuntime, we do so, and if the scene is already loaded, it will not need to do so at all; it not, it will load the Resource all the same. I use this in the game to mark certain scenes as necessary - menu-scenes, for example - and those will not be unloaded when their Runtime is destroyed; as they are being accessed often, this reduces load-times between those scenes.

Coming up

So this is basically the engines core layout, that is both used internally as well as exposed to plugins, as well as general users. We use the same concepts, applied at different scopes, to present an interface that IMHO is way more expandable and convenient than the one-fit-all solutions of MonoBehaviours and the likes.

One thing you might be wondering is - but, what if we actually have things that do need to be placed in a scene? I'll explain that part the next time.

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