Duck Game: Netcode guide for modders

Understanding that, how the netcode works will make your weapons / items compatible with multiplayer without having to do a bunch of tests, and if you don't have friends or two computers, it can save hours of unnecessary debugging and hopeless situations.
P.s this guide is a translation. Original: https://steamcommunity.com/sharedfiles/filedetails/?id=1394086869

 

Connections and others

First, I want to briefly explain, how netcode works (network code), and in newer versions it is rather complicated and uses "ghosts" (ghosts) for data synchronization, not requiring intensive use of the processor. But it doesn't really matter to us, since it is part of the backend ("Internal netcode"), which doesn't affect your code.

More relevant parts of the netcode.

Netcode consists of connections with other ducks, and you can send NetMessage (network messaging) or specific connections, or all connections (usually just passing null as the connection).

Every thing on the level belongs to some connection, and the owner of this connection will send properties about the specific thing to everyone else; so the host / server is not in control of everything, which I think is a fairly common misconception. At the beginning of the level, the host controls everything, apart from other ducks.

I will explain this in more detail in those parts, where do you actually use them.

Statebinding

StateBinding (something like a state link) inform backend, what variables to sync with other connections.

public int myNumber = 25;
public StateBinding _number = new StaeBinding("myNumber",-1,false,false);
//это гарантирует автоматическую синхронизацию int myNumber.

There are several overloads for StateBinding, most of which will never be used.

Main, which you, most likely, use for almost everything (and which was used in the first example):

public StateBinding(string field, int bits = -1, bool rot = false, bool vel = false)

 

Bits value – this is the size of the field, that is, Int32 consists of 32 page. If the value is -1, the size will be received automatically. And saving a couple of nanoseconds each match is not worth the possible risk of making a mistake in the value. So leave it on -1. You can see more clearly, how it works in the BitBuffer class, using some decompiler (eg, DnsPy).

The boolean values ​​rot and vel are used only if, if you are dealing with rotations and speeds, and, as I understand, they are only used when calculating physics. Don't change their values ​​to true, if you don't know, what you are doing.

Here are other overloads, which you can use (and what do they do):

//изменяет значение постепенно вместо мгновенных скачков, работает только с числами с плавающей запятой.
public StateBinding(bool doLerp, string field, int bits = -1, bool rot = false, bool vel = false);
//наиболее специфичный конструктор statebind, работает как обычный + вы можете сделать его "плавным" и установить его важность (низкий, средний или высокий уровень). Использование данного конструктора может замедлить работу других вещей, поэтому вам не следует его использовать.
public StateBinding(GhostPriority p, string field, int bits = -1, bool rot = false, bool vel = false, bool doLerp = false)

Types, to which statebind can be applied:]
  • string
  • float
  • double
  • byte
  • sbyte
  • bool
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • char
  • Vec2
  • BitBuffer
  • NetIndex16
  • NetIndex2
  • NetIndex4dex4
  • NetIndex8
  • Everything, what inherits or is Thing

StateBind will also work with properties. One example is that, which is used in 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 regular sounds, and they can also trigger functions when playing sound. Also, their height may depend on a different value.. But the most important thing, when used with NetSoundBinding they will sync online.

NetSoundEffects and NetSoundBinding are used, eg, when the duck quacks or when you beat the drum.

You can use NetSoundEffect in two ways:

public StateBinding _netBounceSoundBinding = (StateBinding) new NetSoundBinding("_bounceSound "); //Statebinding для _bounceSound
public StateBinding _netYellSoundBinding = (StateBinding) new NetSoundBinding("_netYellSound "); //StateBinding для _netYellSound

public NetSoundEffect _bounceSound = new NetSoundEffect(); //Пустой SoundEffect, который будет привязан к функции.

public NetSoundEffect _netYellSound = new NetSoundEffect(new string[3]
    {
      "quackYell01",
      "quackYell02",
      "quackYell03"
    }); //Создание нового NetSoundEffect, который, будучи единожды воспроизведён, запустит три новых звука, и его синхронизация, чтобы остальные игроки услышали новый звук.
public override void Initialize()
{
    _bounceSound.function = new NetSoundEffect.Function(Bounce);
}

void Bounce()
{
    SFX.Play(GetPath("myBounceSound.wav"));
    shake = 5f;
}

Make sure, what's the type of the variable – StateBinding, а не NetSoundBinding

StateBinding (continuation)

public StateFlagBinding(params string[] fields)

StateFlagBinding is very useful, when you are dealing with multiple booleans, eg, if something is open, closed or blocked.

It's just a faster way to do multiple logical state bindings, which is actually pretty much outdated.

Parameters must be bool to use, and then their values ​​will be compressed into one ushort. Using ushort means, that you can't have more 16 logical 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. Actually, fields _charging and_fired are not used in it, and it depends on the sprite frame and animation index, which, in turn, synchronized. And this is the best way to call, otherwise there would be a delay between all clients. Moreover, in Writing Proper Code this works with StateBindigs (parts).

Types of StateBindigs, which you don't need to know, since they are only used for optimization:

DataBinding(string field) – Works more efficiently with BitBuffers, and only with them.
CompressedVec2Binding – Reduces Vec2 size. All constructors come with maximum values.
CompressedFloatBinding – Decreases float size. All constructors come with maximum values.
InterpolatedVec2Binding[/b] – Highest priority CompressedFloatBinding extension.

[h]How to write correct code, which works with StateBindinds.[/h]
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.

This is especially important, if you are dealing with timers or values, which change rapidly. Essentially you need to write the code like this, as if it can accidentally skip a few numbers and still work

Here's an example: You have a float timer, which decreases its value every frame, and as soon as it reaches zero, something is happening.
Here, how to do not worth it:

timer–;
if(timer == 0) doStuff();

This works great in local play; but online, each frame can only send a limited amount of data, and most likely the timer value will decrease every three or more frames. In this case, the timer will decrease like this(assuming, what does it start with 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
How do you see, 0 was skipped and doStuff() never started.

Here, How to fix it:

timer–;
if(timer <= 0) doStuff();

Easily, is not it? And it will really work!
It was because of this that there was a bug with an endless loading of the Internet map. London (developer) used ==, and when the required number was skipped due to the curves of the maps, it went further into infinity. Fortunately, it was fixed in beta.

Although, this is only half of the puzzle, as it is equally important to choose the correct values ​​for synchronization. In addition to the _charging and_fired fields, Huge laser – this is a great example of, what timers should look like.

Briefly, then here, what is happening there: 2 StateBinding for animations, using animation index and sprite frame. This can be used to synchronize animations.

public StateFlagBinding(params string[] fields)

StateFlagBinding is very useful, when you are dealing with multiple booleans, eg, if something is open, closed or blocked.

It's just a faster way to do multiple logical state bindings, which is actually pretty much outdated.

Parameters must be bool to use, and then their values ​​will be compressed into one ushort. Using ushort means, that you can't have more 16 logical 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. Actually, fields _charging and_fired are not used in it, and it depends on the sprite frame and animation index, which, in turn, synchronized. And this is the best way to call, otherwise there would be a delay between all clients. Moreover, in Writing Proper Code this works with StateBindigs (parts).

Types of StateBindigs, which you don't need to know, since they are only used for optimization:

DataBinding(string field) – Works more efficiently with BitBuffers, and only with them.
CompressedVec2Binding – Reduces Vec2 size. All constructors come with maximum values.
CompressedFloatBinding – Decreases float size. All constructors come with maximum values.
InterpolatedVec2Binding[/b] – Highest priority CompressedFloatBinding extension.

[h]How to write correct code, which works with StateBindinds.[/h]
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.

This is especially important, if you are dealing with timers or values, which change rapidly. Essentially you need to write the code like this, as if it can accidentally skip a few numbers and still work

Here's an example: You have a float timer, which decreases its value every frame, and as soon as it reaches zero, something is happening.
Here, how to do not worth it:

timer–;
if(timer == 0) doStuff();

This works great in local play; but online, each frame can only send a limited amount of data, and most likely the timer value will decrease every three or more frames. In this case, the timer will decrease like this(assuming, what does it start with 10):
10 -> 10 -> 10 -> 7 -> 7 -> 5 -> 5 -> 5 -> 2 -> 2 -> 2 -> -1 -> -1 -> -3……
How do you see, 0 was skipped and doStuff() never started.

Here, How to fix it:

timer–;
if(timer <= 0) doStuff();

Easily, is not it? And it will really work!
It was because of this that there was a bug with an endless loading of the Internet map. London (developer) used ==, and when the required number was skipped due to the curves of the maps, it went further into infinity. Fortunately, this was fixed in beta, although, this is only half of the puzzle, as it is equally important to choose the correct values ​​for synchronization. In addition to the _charging and_fired fields, Huge laser – this is a great example of, what timers should look like., then here, what is happening there: 2 StateBinding for animations, using animation index and sprite frame. This can be used to sync animations and there is also a bool doBlast field., which becomes true, when the owner fired the weapon. AND, if doBlast is true (because it is a StateBinding) and you are not the owner, there is a shot.
The most important parts follow., which do not allow a double shot and instant doBlast shutdown, so that other players always get updated games

NetWork class properties and more

Network.isServer

– is the current computer a host.

Network.isClient

– is the current computer a host.

Network.isActive

– is the network active.

When creating AmmoType, suitable for multiplayer, it must have an empty constructor (no parameters).
It is he who is called, so, if you have another constructor, it won't work at all and will crash when unpacking the message.

this.Fire() synchronized online.
Actually, synchronized bullet spawn, but if your weapon shoots with ordinary objects, you do not need to take additional measures, To make sure, that everything is in sync.

OnHoldAction is syncing, and OnReleaseAction is not.
It means, that you can use OnHoldAction to charge things (how is it done with Phaser). But you'll need some way to make sure, that OnReleaseAction is synchronized (eg, using this.Fire, as in Phaser, or using StateBinding).

Connections and how to spawn things

As mentioned earlier: Every thing (Thing) there is a connection field, and all StateBidning values ​​will be synchronized with the values ​​of these connections. Not with host / server. Still not an opinion, at the beginning of each level, the host will control everything, apart from other ducks.
For things like this, as Holdables, there is optimization – as soon as they are picked up, they change connection with ducks. But if you, eg, create some new thing, which does something, you will need to take a couple of additional steps., what do you need to do, to spawn the box, if your mod is for local play:
Level.Add(new Crate(500,500));
If your mod is for multiplayer:
if(isServerForObject)
  Level.Add(new Crate(500,500));

Yes, you need an additional condition, and that's why:

isServerForObject

Since each client has their own weapon code running, заспавнится 2 boxes; one locally, and the second from the owner of the object.
isServerForObject checks, whether the current computer owns the object, so that the object spawns only once.

Actually, you can check, is the duck local, using isServerForObject.

Congratulations! You did it.

Actually, there is one more thing, which you should know about, and this is Fondleing. In fact, it allows you to take control of the thing and establish a 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 to keep things in sync, which you don't pick up, as, eg, RCCar, to control which you select only the controller. Remember, that Fondle exists. He will save your life, when you try to do something complicated and it doesn't work online.

If you want to see an example, look at the code of the RCController class and its Update. This is also used in aitemboxes..

Bitbuffer’ы

BitBuffers contain data, which you can send to others. If you are going to do more complex NetMessage, it's important to know, how do they work.

In fact, BitBuffers contain data and allow you to read / write them as different types. BitBuffer has a position, meaning that, where are you currently reading data from. And as the values ​​are read, the position will increase. In this way, you don't need to deal with variables, but only with the order of that, how they were placed.

Writing values ​​to 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();

Very important, so that the order of reading and writing is the same, otherwise it won't work.

Here is an example, how can I write an array to BitBuffer, on occasion, if it helps you understand it Little it is better.

int[] myArray = new int[] { 5,125,123123,67,2,324 };
BitBuffer myData = new BitBuffer();myData.Write(myArray.Length); //array size
foreach (var num in myArray)
myData.Write(on one); //write each numberBitBuffer sameData = new BitBuffer(myData.buffer);int arraySize = sameData.ReadInt(); //reading the first int (size)
int[] theArray = new int[arraySize];for (int i = 0; i < arraySize; i++)
theArray = sameData.ReadInt(); //reading each number and putting it into an array[/quote]

BitBuffer limit.

Each BitBuffer stores its size in ushost, which is a positive number from 0 to 2?? or 65535. And since each byte consists of 8 page, BitBuffer can contain up to 8kb of data.

 

Custom Netmessages

Synchronize things with StateBinding and be sure, that they have the right owners is not always enough to work right.

How to get your netmessages to work

DuckGame loads all NetMessages before loading mods, so your NetMessage types will not be added to the netmessage list.

To combat this, we must add our MessageType's.

public static void UpdateNetmessageTypes()
public static void UpdateNetmessageTypes() {
IEnumerable 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();
}

public override void OnPostInitialize() {
    UpdateNetmessageTypes();
}

Voila, now you can use NetMessage in your mod.

In fact, all NetMessage, which you will use, these are events (events).
You can also use regular NetMessage(NMDuckNetwork), but only if you send data to others, such as custom hats.

Your hypothetical NetMessage works something like this:
OnSerilize override fills the serializedData bitbuffer with data from every field in your class (as opposed to StateBidning, properties don't work), then they are forwarded by interveb(interwebs), where upon receipt they go through OnDeserilize override, where it puts all the values ​​from the bitbuffer into the correct fields.

For all Netmessages you need one empty constructor, because, when you get the message, a new object of this type will be created, using an empty constructor. If you don't have it, you will be crushed (not an anime player).

NetMessage creation.

Here, what a simple NetMessage 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);
        }

    }

Here is a list of all data types, which will be automatically serialized and deserialized(very important, to make the fields public! otherwise they will not be serialized):

  • string
  • float
  • double
  • byte
  • sbyte
  • bool
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • char
  • Vec2
  • Everything, what inherits or is Thing

When applying this to ducks: use byte for netIndex.

duck.profile.networkIndex

and then find a duck, using

DuckNetwork.profiles[(int) index].duck;

In this way, all your messages, duck related, will ship much faster, and the chances of out of sync will be greatly reduced.

Serializing custom data.

And what, if you want some kind of array? Since it is not automatically supported, we will have to change the OnSerilize and OnDeserilize overrirides.

int[] myArray = new int[] { 25,30,29};
protected override void OnSerialize()
{
      base.OnSerialize(); //это то, что автоматически сериализирует поля, так что не убирайте это.

      serializedData.Write(myArray.Length);
      foreach (var num in myArray)
             serializedData.Write(num); //то же самое, что и в примере с BitBuffer'ом

public override void OnDeserialize(BitBuffer msg)
{
      base.OnDeserialize(msg); //автоматически десериализирует поля, не убирайте это.
      //так как при сериализации base.OnSerilize выполнялся первым, base.OnDeserilize тоже должен выполняться первым.

      myArray = new int[msg.ReadInt()];
      for (int i = 0; i < myArray.Length; i++)
          myArray[] = msg.ReadInt();
}
//по сути, вам нужно выяснить, как превратить что-либо в BitBuffer
Sending NetMessages in size > 8kb

Since everything in duckgame is sent with BitBuffers, the maximum size of a Netmessage message will be 8 KB minus header size (Header (heading) contains information about NetMessage, so that it can be turned into a byte heap NetMessage again).

Way, by which DuckGame solves this problem, is to use NMLevelDataHeader, multiple NMLevelDataChunks, which are fragments of the level, that can be put together, and after receiving them, it will send an NMLevelDataReady message, to confirm, that the transfer is complete.

Even though it works, i would not recommend copying the code, as it only works when doing one transfer at the same time with one client or all clients at the same time. I used one post, containing session and terminated bool (session and a finished bool), and then I had a data manager, who figured out, when a new session opened, and when it ended, using the last bool. Before, how DuckGame will send its NetMessages, it sorts them by size, and therefore all messages, except the last, must be the same size, and the latter should be less, if you decide to just add the received data into an array when receiving.

Instead of sorting the array, when i sent it, i decided to sort it earlier, when all data was received. Now, when i think about it, I'm not sure, which way was better, but it doesn't really matter. Anyway, here is the code, showing, How can I do that (from the author's mod, Reskins), using the transfermanager approach: DataTransferSession[github.com], DataTransferSession[github.com] and NMDataSlice[github.com]

Since I am a good programmer and I wrote flexible code, you can copy files and use DataTransferManager.SendLotsOfData function and hook something to DataTransferManager.onMessageCompleted event, and everything should be ok.

NetDebug

If you still have a need to protest something, but constantly ask your friends to download your mod and go to your lobby – not an option, especially for you there is a launch parameter -netdebug.

To activate it, right click on the game in the library:

In the panel that opens, enter the launch parameter in the appropriate field:

To activate it, right click on the game in the library:

From Polanas

Be the first to comment

Leave a Reply

Your email address will not be published.


*