Table of Contents
… so why aren’t we using more Statemachines to write our Network code? Perhaps because the most common implementation is no better than any code, really.
I’ll post this as I got detoured from my Write Better Netcode project because I ran into an issue, and had a revelation.
The Issue
I noticed that Multiplayer Playmode has a bug that won’t let me use MonoBehaviour scripts that I have in my own packages.
For this project, to make downloading and using the project completely self-contained, I decided to pull the packages into the project as git subtrees under /Packages/..
which makes them “embedded packages”. Alas,
Multiplayer Playmode fails to load embedded package components!
So I decided to remove the packages, and subtrees, and place all files for the project right under Assets. I can sort them out into packages at a later time.
Combatting Frustration
Oh … mind you, yes of course I was quite frustrated!
I stopped the day early because of this. It was painful to remove part of my originally and carefully planned project setup. At one point, I was overcome by this feeling of being totally blocked – although I knew it wasn’t true, couldn’t be true, I just didn’t want to accept or even consider alternatives.
But such is life as a game developer. You have to find a workaround and move on. And in between, chill out playing a game for instance. Anything that puts your mind in a different state, at ease preferably.
Same as every time you run into a %#§$*&!!!
And then you have to deal with git giving you the finger. Ah well.
The Revelation
It’s in the title! Netcode is Statemachine code. Everything we do is or can be expressed as a Statemachine.
NetworkManager starts, client connects, disconnects, shutdown. All different states.
Authentication: not signed in, signing in, linking account, sign out. Also states.
Plus all the error handling – every state can have “issues” such as losing the network connection or the live service going offline or the player’s account got banned or deleted.
And what about gameplay? Full of Statemachines: switch weapon, firing, overheat, cooldown, firing, reloading, drop weapon.
The Enum Statemachine
Commonly we’ll find ourselves creating an enum and a switch statement with a case for every state. Done, right?
Well, that’s really the poor man’s Statemachine. The main problem being that you can change state anywhere by any means necessary. It’s just an enum field after all.
You’d have to be very disciplined to change states only from within the main switch. Glancing over this switch statement does not enable you to see the state flow either. It’s hidden between many lines of code.
Then try dumping that code to a diagram. Hopeless.
A Better Statemachine
How about this for a change?
The Statemachine Diagram
This is readable. It’s been generated automatically from the statemachine representation.
Why So Many States?
This statemachine seems to have an awful lot of states for Start/Stop, right?
Well, it properly fixes a common issue many devs have when trying to restart a network session: you cannot shutdown and instantly start networking again.
You have to wait for shutdown to complete, here represented by IsNetworkManagerOffline
. And you can’t Start right away either, you have to wait for NetworkManager to be ready (same condition).
And while we’re starting, this may or may not be instantaneous. But most importantly: it may fail. The corresponding failure condition is missing here though. I didn’t say it’s finished. 🤭
But maybe you can guess: even a simple flow can get quite elaborate if you consider all failure conditions. And that’s part of the point: if you don’t accomodate for failure, your multiplayer game is broken even if it works 99% of the time!
The Statemachine Code
Here’s what the above diagram looks like as C# code:
var states = FSM.S(Enum.GetNames(typeof(State)));
m_Statemachine.WithStates(states);
var initState = states[(Int32)State.Initializing];
var offState = states[(Int32)State.Offline];
var startState = states[(Int32)State.ServerStarting];
var onlineState = states[(Int32)State.ServerOnline];
var downState = states[(Int32)State.ServerShuttingDown];
initState.WithTransitions(FSM.T("Goto Offline", offState)
.WithConditions(new IsNetworkManagerOffline()));
offlineState.WithTransitions(FSM.T("Start Server", startState)
.WithConditions(FSM.Variable.IsTrue(m_StartServerVar))
.WithActions(new NetworkManagerStartServer()));
startState.WithTransitions(FSM.T("Server started", onlineState)
.WithConditions(new IsServerOnline()));
onlineState.WithTransitions(FSM.T("Server stopped", downState)
.WithConditions(FSM.NOT(new IsServerOnline()))
.WithActions(new NetworkManagerShutdown()));
downState.WithTransitions(FSM.T("Server shutdown", offState)
.WithConditions(new IsNetworkManagerOffline())
.WithActions(FSM.Variable.SetFalse(m_StartServerVar)));
Notice that I leaned into heavily abbreviating some things:
- FSM.S = create state
- FSM.T = create transition
- FSM.C = create lambda condition
- FSM.A = create lambda action
I don’t normally do that but since this code will get used a lot and there’s little reason to fear ambiguity I went for it. For now.
I consider the lambda conditions/actions for prototyping only since they hide their actual intention in the logs / diagrams.
To run the statemachine, you’d first call Start to initialize it and then repeatedly call Evaluate:
// Init
m_Statemachine.Start();
// Update
m_Statemachine.Evaluate();
Why Not Stateless?
I know there’s Stateless, a lightweight C# statemachine library.
Well, lightweight is relative:
var phoneCall = new StateMachine<State, Trigger>(State.OffHook);
phoneCall.Configure(State.OffHook)
.Permit(Trigger.CallDialled, State.Ringing);
phoneCall.Configure(State.Connected)
.OnEntry(t => StartCallTimer())
.OnExit(t => StopCallTimer())
.InternalTransition(Trigger.MuteMicrophone, t => OnMute())
.InternalTransition(Trigger.UnmuteMicrophone, t => OnUnmute())
.InternalTransition<int>(_setVolumeTrigger, (volume, t) => OnSetVolume(volume))
.Permit(Trigger.LeftMessage, State.OffHook)
.Permit(Trigger.PlacedOnHold, State.OnHold);
There’s plenty of things I won’t need, and I want conditions and actions to be self-contained, expressive classes precisely to get the output above.
I practically recalled my implementation from memory. Pretty much the same Statemachine system I created 20 years ago. It may have taken me a week to build this but I revel in the outlook of creating a strongly statemachine-driven Netcode project.
The dump to PlantUML was also a nice bonus. I like to be in control of both the API and the logs since that matters a lot to me.
The Source Code
Feel free to browse the Statemachine code here.
Do note that it’s licensed under the GPL 3. This has to do with my Patreon as I intend to provide the code with a permissive license to subscribers.
Outlook
The idea I have in mind is to encapsulate NetworkManager (Start/Shutdown), the Services (Authentication, Relay), the whole game flow (menu, lobby, game mode, ingame, summary, quit) and individual Netcode features (send RPC, verify, act, return RPC) through these statemachines.
The big benefit is that everything is self-contained conditions and actions. A single action will perform a service call, and another condition monitors a service exception, and then I can more easily add things like retry at a later time or pop up a failure message and exit.
All the while I’ll create reusable Conditions and Actions. I like working with small, piecemeal code. I work better that way, this provides the sort of focus I lose whenever code gets more complex than a couple dozen lines.
Also, I’ve started pondering about writing a C# wrapper for generating PlantUML state diagrams (and eventually others) but I don’t want to lean into that now.
PlantUML generation is for when I feel the need to work on a purely logical task again. Writing data transform code is how I relax.
Oh and I wanted to write this post in place of the “Start Networking” article (#4) to uphold regular activity. Don’t like to go silent for too long.
Leave a Reply