3. Scene Flow (Write Better Netcode)


The Scene Flow

Now we’re starting with a fundamental concept: Scene Flow.

Let’s talk about when to change scenes and why. And, also …

What’s A Scene, anyway?

The question we never considered to ask:

A scene is a container for content (game objects).

Load another scene in single-load mode and you’re replacing one set of content with another.

Simple and self-understood. Why talk about this?

You Create Too Many Scenes!

Because most create far, faaaar too many scenes …

These are all single-loaded scenes, duplicating much of the same content!

Then they implement awkward, slow, broken solutions to persist state when moving from one (single-loaded) scene to another.

Remember: single-loading a new scene destroys all current objects and components, then instantiates the objects in the new scene.

Some of these objects may be the exact same ones in all scenes!

Persisting State Between Scenes

Every game has these fundamental requirements:

  1. Global game state persists all the time, is accessible any time.
  2. Select objects persist for a set duration, but not indefinetely.

Both requirements have known solutions:

  1. DontDestroyOnLoad
  2. Additive Scenes

There’s actually two more: static fields and ScriptableObject instances. At runtime both can be used to represent global state. Neither of them are scene-related, thus out of scope for this article.

DontDestroyOnLoad (DDOL)

Game objects marked as DontDestroyOnLoad(gameObject); get moved into an aptly named, automatically created DontDestroyOnLoad scene. Such objects will no longer get destroyed when changing scenes.

Good. Except … there’s a pervasive problem: When you load a scene with DDOL objects again. And again. Without extra code this will create duplicates of the scene’s DDOL objects!

No. Avoid duplicates, you must!

You may be tempted to fix this by checking if the object already exists, but even finding the existing instance can already be tricky.

Regardless, any DDOL object will be instantiated again, only to get destroyed again. This is downright awful and error-prone!

CAUTION: Don’t Destroy NetworkManager!

If you don’t own the component’s code, as is the case for NetworkManager, you may even be tempted (or told) to:

Destroy(NetworkManager.Singleton); 😖🤢🤬

Community feedback has proven numerous times that destroying the NetworkManager leads to all sorts of issues!

Avoid Duplicates With A DDOL Scene

The real slim solution to avoid DDOL object duplication is to have all DDOL objects in a scene that’s loaded only once. Typically the first scene in the build list, the one with build index 0.

From the DDOL or launch or global objects scene you can then immediately transition to the next, actual first scene:

Client Scene Flow
The Launch Scene approach I generally suggest.

This is what I had implemented in MultiPal but it came with a grievance: you always have to enter playmode through the launch scene.

The solution I’m opting for now is radical. I can tell you that we’re going to go all in. I’ll explain soon but first the prerequisite …

Additive Scene Loading

Sometimes, we need the same game objects in many, but not all scenes. Common example would be the player character and the game’s HUD – both exist in all levels but not in menu screens.

To allow for this, we could assume a “game scene” that merely acts as a container for everything that happens during the game.

This scene contains the global objects that never change, and a system to load other additive scenes, let’s assume they’re “levels”.

Game Objects persist. The bold additive scene is currently loaded as well.

Insert “Loading …” Scene

Before we actually start to additively load a level scene, you can first additively (synchronously) load an animated “Loading…” scene.

When the new level has loaded you unload the loading scene again.

With additive scenes, you can start developing without a loading screen, and later introduce a loading screen just by adding these two steps at the start and end of the sequence.

Contrast this with single-scene level loading: You’ll have a headache if you decide to add a level loading screen at a later time!

Additive Level Transitions

How do we move from one level to the next with additive scenes?

We would first call UnloadSceneAsync to unload the current additively loaded level scene – if any.

The next level’s content is loaded additively with LoadSceneAsync.

By default, Unity won’t unload unused assets for additive scenes. By calling Resources.UnloadUnusedAssets we can free up any memory of the previous scene and it’s up to us when to do so.

We have two points in time where we can unload unused assets. We can even decide at runtime which option we prefer: Avoid the memory spike but slower load, or load faster but with a mem spike.

Load Slower Without Memory Spike

By calling UnloadUnusedAssets right after the current scene has unloaded we ensure our memory footprint goes down before we start loading the next level:

No memory spike but load takes longer as shared assets are re-loaded.

Load Faster With Memory Spike

You may want to unload unused resources only after the new level has loaded instead:

Shared assets still present in memory will load the next level faster.

This speeds up level loading since shared assets will still be in memory. But it will also cause a memory spike during the load because we also keep the soon-to-be-unused assets in memory.

Use this approach if one or several statements are true:

  • Levels share a lot of assets, or few but heavy ones.
  • You develop for a platform with plenty of memory.
  • Your game is not memory hungry.

In all other cases it’s best to try both options – with the above concept you can simply toggle a checkbox in the Inspector.

State Persists Naturally

Now what happened to the player and the HUD when changing levels the additive way?

Nothing!

Their state persisted automatically because they never get destroyed in the first place! This means they can all comfortable subscribe to sceneLoaded events in case they need to prepare for the next level.

Because you’d still have to reset some state, like position or cooldowns. But any other state like health, inventory, skill tree – that’s just going to stick around. Like it’s supposed to!

Furthermore, you do not have to “find” already established references to other global objects either. Only references to within the new level scene may need to be gathered.

Additive Scenes Allow Multi-User Editing

Additive scene loading is an elegant solution another challenge!

You can split a scene’s content so two people can work on the “same scene” simultaneously, when previously this would cause (unresolvable) merge conflicts with the .unity scene file.

A designer works on the platformer’s level layout (an additive scene) while an artist is simultaneously working on the parallax background layers, postprocessing FX and lighting (one or more additive scenes). They won’t get in the way of each other!

What If There’s Only One Scene?

Imagine the entire game, dozens of menu screens, hundreds of levels, thousands of dynamically instantiated prefabs – all hosted in a single scene!

Is that possible?

Yes, absolutely!

Is it a good idea?

Well, after …

… I say we’re ready for:

One Scene To Rule Them All!

I’m convinced a single scene is the best solution for our project!

The one Scene contains global objects and loads/unloads additive scenes.

It’s a hypothesis that I intend to prove. And backpedal if necessary. But all evidence points to this being ultimately the best way to work with scenes in Unity at the expense of a slight managing overhead.

It allows us to have truly global objects without having to resort to DDOL and we have more control over resource usage.

The Additive Scenes Todo List

To make this work, we likely need to create the following:

  • Properly separating the content into additive scenes:
    • Client-Only content
    • Server-Only content
    • Developer content (debugging, cheats, stats, etc)
    • Game content (menu, levels, interaction, etc)
  • Additive Scene Loader (Server and Client)
    • Keeps track of loaded scenes
    • Ensures scenes are not duplicated
    • Content change transitions (ie level load)
    • Implements a scene Unload strategy (caching?)
  • Scene bundle specification
    • A level or game mode may consist of multiple scenes
    • Likely a ScriptableObject
    • Perhaps a custom editor window
  • Developer launch flags and settings
    • To directly start a given game mode and level, for:
    • Builds (command line), enter playmode, and unit tests
    • Likely needs a configuration object (SO)
    • Perhaps a custom editor window
  • Automated Tests
    • Integration tests ensure all the above functions correctly.

There may be some issues down the road, but they all seem to have proper solutions – manually updating Light probes for instance.

I feel it’s worth doing for the benefits of not suffering through object lifecycle-limbo: Objects get destroyed and instantiated again, events get unregistered or not, and generally event execution order woes.

Implement The Game Flow First

I’ll delay implementing all the features outlined above but focus on getting the single-scene concept to a working state with connecting clients together.

And disconnecting them. And reconnecting in other roles.

It’s crucial to create the complete game flow of going into and back out of online sessions first, as within that cycle lie many pitfalls.

We don’t want to be the developer who seemingly makes good progress but months later faces the issue that moving out and back into network sessions fails catastrophically in many ways.

About Those Cute Diagrams …

Yeah, I know, right? 🙂

I did not actually spend hours clickedy-click and draggedy-noooo!

The diagrams are created with PlantUML as “code” – the .puml sources are in the repo. I used the Rider PlantUML plugin to create diagrams in realtime interactively.

Learn more about PlantUML here. It’s a revelation!

Next …

Continue reading with 4. Netcode Statemachine

Return to the Write Better Netcode Overview

Source Code on GitHub (GPL3 License)

Join my Patreon – it’s free! Get the latest updates by email.

Leave a comment below if you have any questions or feedback!

4 responses

  1. Loving the tutorial so far. I’ve been making small scale multiplayer games as a hobby, but I wish I knew all of this going into it! Awaiting part 4 patiently 🙂

    1. Thank you! That’s always great to hear and motivating. 🙂

  2. First of all, thanks for the tutorial! It’s very well explained.
    I wanted to ask you about loading a ‘Loading…’ scene. If you load a loading scene additively before unloading the rest, it will cause the objects to overlap and all be visible at the same time. How would you solve this issue?
    I understand that the final goal is to transition from one level to another without the player noticing the loading and unloading process, so the ‘Loading…’ scene should appear smoothly and, if possible, with a transition (for example, fade out/fade in).

    1. A smooth transition of a loading screen is actually rare (and when it happens it ought to happen in a fraction of a second – nobody likes having to wait extra just for a fade to complete, some GUIs are pretty obnoxious by fading everything in/out for merely 0.5 seconds). Nevertheless, the loading screen would simply require a delay before and after the actual load operation to “make time” for the loading screen to fade in and out.

      To make the fade in/out work, you need a fullscreen image. Often this would be a simple black image which after fade-in obstructs everything behind it. In many games the fade-out / disabling of the loading image is delayed too because sometimes it is necessary to allow the content to “initialize”. For instance in a streaming terrain system sometimes even the close-up area may be of low quality until the high-resolution meshes and textures have been loaded in. This is why you sometimes will see a loading screen during a “teleportation” (fast travel) event even though the player is merely repositioned by a few meters during fast travel. You can observe this in Bethesda games (Fallout) for instance.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

WordPress Cookie Notice by Real Cookie Banner