Launching a Netcode session in the Write Better Netcode project can be done in not one, not two, but three ways!
It supports launching via Multiplayer Playmode (MPPM) tags, via Command Line arguments (CmdArgs), and via interactive GUI (next article).
I implemented all three in about 6 hours while also fixing some bugs and adding the Components Registry.
I mention this because it came as a surprise to myself – I attribute this in part to the fact that I had code available to scrap – although I rewrote 90% of it. But also the interface to NetcodeState
is so simple and effective.
Table of Contents
Launch Netcode Via MPPM
Multiplayer Playmode (MPPM) allows you to connect up to three Virtual Players (VP) to the Main Editor Player for a total of four participants.
MPPM makes the often misspelled ParrelSync (not: “ParallelSync”) obsolete – except for those not yet using Unity 6.
MPPM Setup
In Project Settings you define Player Tags for use with MPPM:
In the Multiplayer Play Mode window (spelled inconsistently) you can assign both tags and the so-called Multiplayer Role:
This is what it looks like in Play Mode, if you were to squeeze things together to fit it into a not-so-wide screenshot:
Note that each window is actually a separate editor instance with the same project open. The VPs largely share the Library and Assets with the main editor, thus disk space usage of MPPM is less than that of ParrelSync.
Why Not Use Multiplayer Role?
Ah, yeah. So we’re adding Tags to these VPs in order to decide in code which multiplayer role the VPs assume but then there’s also a Multiplayer Role setting. Huh?
The Multiplayer Role (MPR) feature is from the Dedicated Server package. Its main purpose is to allow for code and asset stripping based on whether the build contains Client-only or Server-only code and assets, or both (no stripping).
Technically it would be possible to use MPR to make the role decision. However if MPR is set to “Client and Server” that doesn’t mean it’s going to be the host – the VP may also want to play as Client, or be the Server.
So it’s best to use Tags for greatest flexibility and to avoid confusion. It also lets you test the unsupported cases, such as trying to launch as Client in a Server-only build or vice versa – you may want to handle such cases with a user-facing error message.
This may seem trivial but consider that certain role-specific components may have been stripped from the build and thus calling into that code may throw an exception.
How Does MPPM Launch Work?
I added an MPPM Launcher game object to the scene with the MppmLauncher
script on it, as well as a utility script DestroyInBuilds
which does exactly what it says since MPPM is an editor-only feature.
The MppmLauncher
script looks like this:
First, we try to get the network role from the VP player tags:
If we find a valid role, we start networking with that role:
The role is assigned to a NetcodeConfig
instance which has the connection limit set to the number of VPs. Although strictly speaking you could have more connections since you can also connect builds to the editor or VPs.
For the TransportConfig it just uses whatever the NetworkManager Inspector settings are:
Note: I follow the standard C# pascal naming convention which dictates that there must not be consecutive uppercase letters: MPPM is written as Mppm, GUI is Gui, SQL is Sql, and so on.
RequestStartNetwork
The NetcodeState
class now has this method:
This just logs the DTOs and then assigns them to the corresponding Statemachine variables. Where’s the magic happening?
In the Statemachine, of course!
If you recall, IsNetcodeRole
respectively its negated counterpart IsNotNetcodeRole
checks the NetcodeConfig
variable:
And off the Statemachine goes to start networking, with or without Relay. This now just happens automagically. It’s a solved problem!
MPPM, Relay, Latency And The Simulator
In the future I could allow enabling Relay for MPPM. I already had this working for MultiPal, primarily to test with additional latency.
This however requires providing clients automated access to the host’s Relay join code via the file system, or other means, which complicates matters for very little benefit.
If you want to test with latency, you can use the Network Simulator and set it to the Home DSL connection preset which is a good, but not great connection.
I always have the Network Simulator active during development with a not-so-great connection preset to develop and test all features from the perspective of an average player!
The ideal scenario where there’s practically no latency for locally connected clients does NOT exist in the real world! Plenty of devs are running afoul of not considering this fact for far too long.
I learned that lesson in the 90s when we were developing GameBoy games on a PC emulator, playing with our keyboards. Writing an EEPROM cartridge took an hour, and we only had two. The pinpoint accuracy of a keyboard’s arrow keys is the exact opposite of the GameBoy’s wobbly directional pad. This only became obvious when we worked on our first twitch-action sports game.
The MppmLauncher could be expanded to support additional tags that adjust the latency for individual VPs. You could then force VP4 to play through a 2G connection to observe its teleportativeness.
What If I Want To Skip MPPM Launch?
Not every time you enter playmode would you want to auto-connect your VP players. Perhaps you want to test the GUI, then what?
Well, all it takes is to remove the Tags from the VPs and they’ll stop auto-launching the network session.
MPPM 1.3 introduces the Scenarios window where you can define and quickly switch between various layouts (roles, tags, and so on).
Launch Netcode Via CmdArgs
And now for something completely different: Command Line Args.
I also added a CMD Launcher with a CmdLauncher
script:
Behold the extremely complex CmdLauncher
:
Nothing special here nor is StartNetworkWithRole
:
It does push out reading in the command line arguments to the DTO objects. Let’s check TransportConfig
as an example:
For Transport, it’s important to get the defaults first and then only override them with whatever was specified by the command line arguments since we may get only some of them specified. The existing values are passed as the default value (2nd parameter).
The RelayConfig
contains no surprise either, minus the default values being, well, default
:
Without arguments the default is to not use the Relay Service. Omitting MaxConnections will cause it to be 0 which is interpreted as 100 by TransportSetup
which is Relay’s connection limit.
Did you notice the use of the nameof()
expression?
This is not just for convenience (eg avoid typos). It also ensures that each command line parameter always matches the DTO’s field name. I can refactor-rename a field and the command line parameter will also be updated accordingly.
This is fine during development. Though after release, if users are actually supposed to use those parameters, you will want to be more careful in renaming them.
Parsing Command Line Arguments
The gist of parsing is in the static CmdArgs
class which holds a dictionary of the arguments (keys) and their values (if any):
The Args are lazy-initialized. First time the Args
property is accessed, the command line arguments are parsed once and stored in the s_Args
field.
Above code is a bit technical so I’ll focus on the takeaways:
- all command line arguments are case insensitive
- all command line arguments start with a dash
- each argument’s key is stored without the dash
- each argument may have an optional value at
args[i+1]
- duplicate arguments are ignored and a warning is logged
There’s an extra method Log()
not shown here which just dumps the command line arguments to the Player.log for .. debugging.
Getting an argument’s value is similar to PlayerPrefs/EditorPrefs:
Basic dictionary TryGetValue with a default value.
For the bool type I also call TryParse to see if its convertible and still use the default if it’s not. This requires you to type boolean values as true
or false
on the command line, which I find acceptable.
Mind The Culture-Gap!
Note that TryParse
is not without gotchas.
Without specifying NumberStyles
and CultureInfo
any number may be treated differently based on the machine’s locale!
That’s why using the InvariantCulture
is so important. This forces all users to use the US notation of -1,234,567.89
for floating point numbers. The same holds true for integer values.
Imagine the argument -someFloat 1,234.0
put into a batch file and shared with others around the globe. Some users may find that this batch isn’t working for them.
That’s why we always have to consider locale when reading input data from users. One of the most prominent examples of a file format that does awkwardly, unnecessarily read/write differently based on the user’s locale is … can you guess it?
Yes, CSV! It defaults to being either comma or semicolon delimited depending on the machine’s locale. You know it when you import a CSV and all data is crammed into the leftmost column.
Next …
Read on with 7. Launch Netcode GUI
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!
Leave a Reply