Duck Game

Duck Game

Not enough ratings
Netcode guide for modders
By Killer-Fackur
Actually understanding how the netcode works will make your weapons/items multiplayer compatible without having to do a bunch of testing, and if you dont have friends or 2 computers could save hours of unneccesary debugging and dead ends.
   
Award
Favorite
Favorited
Unfavorite
connections and other stuff
I just briefly wanna explain how the netcode works, and the new one is rather complex and uses "ghosts" to syncronize stuff without being that cpu intensive. But that part doesnt really matter as it's part of the "backend netcode" that doesnt affect your code.

Actual relevant parts of netcode
The netcode consists of connections to the other ducks, and you can send NetMessages to either specific connections or all connections (usually just by passing null as the connection).

Each thing in a level is owned by some connection, and the owner of that connection will send properties about the specific thing to all others; so the host/server doesnt contol everything which i find is a pretty common misconception. At the start of a level the host is owner of everything but the other ducks

I will explain these more in the parts where you actually use them.
Statebindings
Statebindings tell the "backend netcode" which variables to sync across other connections.
public int myNumber = 25; public StateBinding _number = new StateBinding("myNumber",-1,false,false); //This makes sure that the Integer myNumber is automatically synced.

There are a couple of overloads for statebinds, most of which are never going to be used.
The main one you're going to use for basically everything (and used in first example);
public StateBinding(string field, int bits = -1, bool rot = false, bool vel = false)
The bits value is the size of the field, ie Int32 is made of 32 bits. If this is set to -1 It will get the size of the value automatically. And saving a couple of nanoseconds every match is not worth the possible risk of getting the number wrong. so keep it at -1, You can see how it works more specifically in the BitBuffer class using some decompiler.

the booleans rot and vel, is only used if you're dealing with rotations and velocities and the only instanes i've found these used is when doing physics. keep these at false aswell unless you know what you're doing.

Heres the other ones you might use and what they do:
//lerps the value so that it will gradually change value instead of instant jumps, only works on floats public StateBinding(bool doLerp, string field, int bits = -1, bool rot = false, bool vel = false); //the most specific statebind constructor, works like the normal one + you can choose to make it lerp and set the importance of the statebind. (low medium or high) using this can slow down other stuff, hence you shouldn't use it. public StateBinding(GhostPriority p, string field, int bits = -1, bool rot = false, bool vel = false, bool doLerp = false)

Types that statebinds can be applied to:
  • string
  • float
  • double
  • byte
  • sbyte
  • bool
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • char
  • Vec2
  • BitBuffer
  • NetIndex16
  • NetIndex2
  • NetIndex4dex4
  • NetIndex8
  • Anything thats assignable as a Thing.

StateBinds will also work on properties, one example is the one used in the MegaLaser:
public StateBinding _frameBinding = new StateBinding("spriteFrame", -1, false, false); public byte spriteFrame { get { if (this._chargeAnim == null) return (byte) 0; return (byte) this._chargeAnim._frame; } set { if (this._chargeAnim == null) return; this._chargeAnim._frame = (int) value; } }

NetSoundBinding
NetSoundEffects are like normal sounds, and they can also run functions when the sound is played and have their pitch bound to some other value. But most importantly, when used with a NetSoundBinding they will be syncronised online.

NetSoundEffects and NetSoundBindings are used for things like when the duck quacks and when you hit a drum.

These are the two ways you'll be using a NetSoundEffect:
public StateBinding _netBounceSoundBinding = (StateBinding) new NetSoundBinding("_bounceSound "); //Statebinding for _bounceSound public StateBinding _netYellSoundBinding = (StateBinding) new NetSoundBinding("_netYellSound "); //StateBinding for _netYellSound public NetSoundEffect _bounceSound = new NetSoundEffect(); //Empty Sound effect that will will assign a function to. public NetSoundEffect _netYellSound = new NetSoundEffect(new string[3] { "quackYell01", "quackYell02", "quackYell03" }); //This creates a new NetSoundEffect that once played it will start one of the three sounds, and syncronise it so that others hear the same sound. public override void Initialize(){ _bounceSound.function = new NetSoundEffect.Function(Bounce); } void Bounce(){ SFX.Play(GetPath("myBounceSound.wav")); shake = 5f; }
make sure that the variable type is a StateBinding and not NetSoundBinding.


public StateFlagBinding(params string[] fields)
The StateFlagBinding Is very useful when you're dealing with multiple booleans, such as if someting is open, closed or locked.

Its just a faster way of doing multiple boolean statebindings and is actually pretty obsolete.

The parameters should be the name of booleans to use, and it will then compress the values into one ushort. The usage of a ushort makes it so that you cant have more than 16 boolean fields.
public StateBinding _laserStateBinding = (StateBinding) new StateFlagBinding(new string[3] { "_charging", "_fired", "doBlast" }); public bool doBlast; public bool _fired; public bool _charging;
Example taken from HugeLaser, It doesnt actually use the _charging and _fired bools and is dependant on the sprite frame and animation index being synced instead. And that is the best call, as there would be a delay between all clients otherwise, more on that in the Writing Proper Code that works with StateBindings parts.


types of statebinds that you dont need to know about as they're only for optimization
DataBinding(string field) - works more efficiently with bitbuffers, and only bitbuffers.
CompressedVec2Binding - reduces size of Vec2s. all constructors comes with max value.
CompressedFloatBinding - reduces size of floats. all constructors comes with max value.
InterpolatedVec2Binding - extension of CompressedFloatBinding with highest priority.

Writing Proper Code that works with StateBindings
This is especially important if you're dealing with stuff like timers or values that change rapidly. Basically what you have to do is write your stuff as if they could randomly skip a bunch of numbers and still work in the end.

Heres one example: You have a timer which is a float and decrease its value by one each frame, and once it reaches zero you do something.
Heres how not to write it:
timer--; if(timer == 0) doStuff();
So, this works really well when playing locally; but when we think of how netcode works, it can only send so much data each frame, and theres a high likelyhood of the timer value to decrease every three or more frames and so then what happens is that the timer would decrease something like this on the other clients (supposing that it starts at 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3......
As you can see it skipped 0 and thus doStuff() wouldn't be run.

So here how you could fix that:
timer--; if(timer <= 0) doStuff();
Easy fix. And it will work!
This is actually what causes the infamous infinite internet map downloading bug to happen, the developer used ==, and when it skipped a number because of faulty maps it would exceed the max value and go on forever. Luckely it's fixed in the beta.

This is just half the puzzle though, as it is just important to choose the correct values to bind.
Apart from the _charging and _fired fields, the HugeLaser is a great example of how this should/could be done when dealing with timings.

Heres a summary of what it does: 2 statebinds for the animation, using the animation index and sprite frame to make sure that the animation is synced, this could be used for syncing any animations.
And then theres the doBlast bool, which becomes true when the gun is fired by the owner. And if doBlast is true (because its statebound) and you're not the owner, then you fire.
Then comes the most important parts, making sure that you dont fire twice and that you dont disable doBlast immidiately so that others always recieve the update.

The grenade does the same thing in a way more preculiar manner using OnBulletsFired and a bunch of extra stuff which i dont even think is used in the current netcode. anyways, its way more complicated and has roughly the same accuracy.
Network class properties and extra netcode stuff
Network.isServer
- whether or not the current computer is the host.

Network.isClient
- whether or not the current computer is not the host.

Network.isActive
- whether or not its online.

When creating an ammotype suitable for multiplayer it has to have an empty constructor (without parameters that is).
Thats the one that gets called, so if you have another constructor for creating the ammotype it will not work at all and cause a message unpack failure.

this.Fire() is synced online
Actually, its the bullets spawning that is synced online, but if you have a weapon that shoots normal stuff you dont need to do extra measures of making sure that its synced.

OnHoldAction is synced online, but not OnReleaseAction
Basically what this means is that you can use the OnHoldAction for charging stuff (as done with the Phaser). But you'll need some way of making sure that OnReleaseAction is synced (such as using this.Fire like in the Phaser)
Connections and how to spawn stuff
As mentioned earlier: Each Thing has a connection field, and all statebinding values will by synced to that connections values. And not the Host/Server. However; in the start of each Level, the host will have control over everything but the other ducks.

There are some automation on stuff like Holdables where once picked up they will change connection to the Ducks. But if you for instance create some new thing that does stuff you will have to do a couple extra steps.

This is all you need to do to spawn a crate if your mod was intended for local only:
Level.Add(new Crate(500,500));

And to spawn a crate intended for online:
if(isServerForObject) Level.Add(new Crate(500,500));

A bunch of new stuff, but I'll go through them one by one and explain why you actually add these.

isServerForObject
Since each client runs its own code for the weapon, it will spawn 2 crates; one locally, and one from the owner of the object.
What isServerForObject does is that it checks whether or not the current computer owns the object, that way it will only be spawned once.

You can actually check if a duck is local using isServerForObject.

Congratulations! You did it
Theres actually one more thing you have to learn, and that is Fondleing. It basically lets you take ownership over another thing and set its connection.

public void Fondle(Thing t) { if (t == null || !Network.isActive || (!this.isServerForObject || !t.CanBeControlled()) || (this.connection != DuckNetwork.localConnection || t.connection == this.connection)) return; t.connection = this.connection; ++t.authority; }

This is used for syncronizing stuff you dont pick up like the RCCar, where you only pick up the controller. remeber that this exists, it will save your life when you try to do complicated stuff and it just doesnt work online

If you want to see an example check out the RCController class and its Update override. Its also used in Itemboxes.

Bitbuffers
Bitbuffers contains data you can send to others. If you're going to make more complex netmessages it's important to know how these work.

Basically what a bitbuffer does is that it holds a bunch of data and lets you read/write it as different types. The bitbuffer has a position, which is where its currently reading. And as you read a value it will increase to position. That way you dont have to deal with sizes of the different variables, just the order of how they were placed.

Writing values to a Bitbuffer
BitBuffer myData = new BitBuffer(); myData.Write((byte)25); myData.Write((int)50); myData.Write(true); myData.Write(new Vec2(25,25));

Reading values from the same Bitbuffer
BitBuffer sameData = new BitBuffer(myData.buffer); byte mybyte = sameData.ReadByte(); int myInt = sameData.ReadInt(); bool myBool = sameData.ReadBool(); Vec2 myVec = sameData.ReadVec2();

Its very important to keep the same order on read and writes, otherwise it will not work.

Heres an example of how you can write an array to a bitbuffer incase it will help you understand it a bit better:
int[] myArray = new int[] { 5,125,123123,67,2,324 }; BitBuffer myData = new BitBuffer(); myData.Write(myArray.Length); //size of array foreach (var num in myArray) myData.Write(num); //write each number BitBuffer sameData = new BitBuffer(myData.buffer); int arraySize = sameData.ReadInt(); //get the first int (size) int[] theArray = new int[arraySize]; for (int i = 0; i < arraySize; i++)
theArray[i] = sameData.ReadInt(); //read each number and put it into the array

Bitbuffer Limit
Every bitbuffer store their length in a ushort which is a positive number from 0 to 2¹⁶ or 65535. And since every byte contains 8 bits it will at most be able to store 8kb of data.
Custom Netmessages
Getting stuff syncronised using statebinds and making sure they have the correct owners isn't always enough for it to work. In those cases you'll have to use custom netmessages.

Getting your netmessages to work
DuckGame loads all netmessages before mods are loaded, and thus none of your netmessage types have been added to the list of netmessages.

To combat this we have to add our messagestypes.
public static void UpdateNetmessageTypes() { IEnumerable<System.Type> subclasses = Editor.GetSubclasses(typeof(NetMessage)); Network.typeToMessageID.Clear(); ushort key = 1; foreach (System.Type type in subclasses) { if (type.GetCustomAttributes(typeof(FixedNetworkID), false).Length != 0) { FixedNetworkID customAttribute = (FixedNetworkID)type.GetCustomAttributes(typeof(FixedNetworkID), false)[0]; if (customAttribute != null) Network.typeToMessageID.Add(type, customAttribute.FixedID); } } foreach (System.Type type in subclasses) { if (!Network.typeToMessageID.ContainsValue(type)) { while (Network.typeToMessageID.ContainsKey(key)) ++key; Network.typeToMessageID.Add(type, key); ++key; } } } public override void OnPostInitialize() { UpdateNetmessageTypes(); }

Badabimbadabom, you can now use netmessages in your mod.

Basically the only netmessage you'll be using are events. You can use normal netmessages (NMDuckNetwork) aswell, but only if you're only sending data to others such as custom hats.

your average netmessage basically works like this:
the OnSerilize override fills the bitbuffer serializedData with data from each field in your class (unlike statebinds, properties do not work), then it gets sent across the interwebs where once recieved it will go through the OnDeserilize override where it applies all the values from the bitbuffer into the correct fields.

For all Netmessages you'll need one empty constructor, because when you recieve a message it will create a new object from that type using an empty constructor. If you dont have one you'll crash.

Making a netmessage
This is how a simple netmessage event looks like:
class NMTimeLimit : NMDuckNetworkEvent { public string message; public NMTimeLimit() { } public NMTimeLimit(string msg) { message = msg; } public override void Activate() { base.Activate(); HUD.AddInputChangeDisplay(message); } }

This is a list of all types that can be serilized and deserillized automatically It's very important to have the fields public! otherwise they won't get serilized:
  • string
  • float
  • bool
  • byte
  • sbyte
  • double
  • int
  • ulong
  • uint
  • ushort
  • short
  • NetIndex4
  • Vec2
  • Anything assignable from Thing

when applying stuff to ducks: use a byte for the netIndex.
duck.profile.networkIndex
and then find the duck using
DuckNetwork.profiles[(int) index].duck;
That way all your duck related messages will be sent way faster and chances of getting desynced decrease alot.

Serilizing custom data
Now what if you wanna have an array of some kind? as it is not supported automatically we will have to change the OnSerilize and OnDeserilize overrides.


int[] myArray = new int[] { 25,30,29};
protected override void OnSerialize()
{
base.OnSerialize(); //this is what automatically serilizes the fields, so dont remove it

serializedData.Write(myArray.Length);
foreach (var num in myArray)
serializedData.Write(num); //this is the same as the bitbuffer example
}

public override void OnDeserialize(BitBuffer msg)
{
base.OnDeserialize(msg); //automatically deserilizes fields, dont remove this
//since base.OnSerilize was first when serilizing, base.OnDeserilize must be first here aswell

myArray = new int[msg.ReadInt()];
for (int i = 0; i < myArray.Length; i++)
myArray[i] = msg.ReadInt();
}

//basically what you wanna do is figure out a way to turn something into a bitbuffer.

Sending netmessages with a size >8kb
Since everything in duckgame is sent with bitbuffers the max size for the Netmessage will be 8kb minus the header size (The Header contains info about the netmessage so that it can be turned back into a netmessage just from a bunch of bytes).

The way DuckGame deals with this problem is using a NMLevelDataHeader, multiple NMLevelDataChunks which are fragments of the level which can be pieced back together and once recieved it will send a NMLevelDataReady message to confirm that the transfer completed.

While this works, I wouldn't recommend copying duckgames code as it only works when doing one transfer at the time with either one or all clients at the time. The way i did it was using one message containing a session and a finished bool, and then have a data manager that would figure out when a new session was opened and when it's finished using the last bool. Before duckgame sends its netmessages it will sort them based of size, and thus all but the last message has to be the same size and the last one has to be smaller if you decide to just stack the recieved data into an array when recieved.

Instead of sorting the array when i was sending it i opted for sorting it back when all data had been recieved. Now that i think of it im not sure which were the better move, but it doesn't matter that much. Anyhow, Heres some code to demonstrate how it could be done from my Reskins Mod which use the transfermanager approach: DataTransferManager[github.com], DataTransferSession[github.com] and NMDataSlice[github.com].

Since im a decent coder and wrote flexible code you could copy the files and use the DataTransferManager.SendLotsOfData function and hook something up to the DataTransferManager.onMessageCompleted Event and you should be good to go.
5 Comments
Polanas 26 Apr, 2021 @ 11:12pm 
Great guide, helped me a lot! :)
Killer-Fackur  [author] 23 Apr, 2021 @ 9:26am 
sure, go for it
korperka 23 Apr, 2021 @ 2:38am 
Hello! I found your guide very interesting and want to translate it to Russian so that more people in the community will read it. Can you give a consent to the translation?
uxvi 24 May, 2020 @ 2:12am 
thanks
be kind and have fun 23 Jul, 2018 @ 8:21am 
iq too high