Enjoy automatic and implicit conversion of all incoming data to the parameters that you request, no more need to create a Player
object from an int
yourself. This is done by converting the internal MsgPack byte data directly to the requested type, unlike mono v1 who constructs an intermediate object first, packs it in an object[]
array, who in turn are converted again.
This approach is at least 3 times faster than the one used in mono v1, that does mean that creating multiple listeners is not advised as the conversion cost will now be per listener, best keep it to 1 or at least 3 or less.
Example:
[EventHandler("myEvent")] public static async Coroutine GimmeAll(int a)
=> Debug.WriteLine($"GimmeAll1 {a}");
[EventHandler("myEvent")] public static async Coroutine GimmeAll(string a, int b)
=> Debug.WriteLine($"GimmeAll2 {a} {b}");
[EventHandler("myEvent")] public static async Coroutine GimmeAll(string a, int b, string c = "Hey")
=> Debug.WriteLine($"GimmeAll3 {a} {b} {c}");
[EventHandler("myEvent")] public static async Coroutine GimmeAll(int a, string b, string c = "Oh", int d = 678)
=> Debug.WriteLine($"GimmeAll4 {a} {b} {c} {d}");
[EventHandler("myEvent")] public static async Coroutine GimmeAll(int a, Player b, string c = "Oh", int d = 678)
=> Debug.WriteLine($"GimmeAll5 {a} {b} {c} {d}");
// Trigger it!
[Command("Gimme")] public async Coroutine Gimme(uint source, object[] objects, string raw)
=> Events.TriggerServerEvent("myEvent", 1234, "5678");
The above will result in the following output:
[ script:csharp_v2] GimmeAll1 1234
[ script:csharp_v2] GimmeAll2 1234 5678
[ script:csharp_v2] GimmeAll3 1234 5678 Hey
[ script:csharp_v2] GimmeAll4 1234 5678 Oh 678
[ script:csharp_v2] GimmeAll5 1234 Player(5678) Oh 678
Want to automate your custom type's (de)serialization, then include using CitizenFX.MsgPack;
and apply the MsgPackSerializable(Layout)
attribute to your type.
Before we jump into how this is done, here's a list of best practices to make your resource more performant and as a result lets your servers run more smoothly.
- Reduce packet size:
- Only send data that is necessary, e.g.: send update packets with player ids instead of sending the player's name and/or other data you don't need with each request.
- Use
Layout.Indexed
over any mapped/keyed based solution, removing unnecessary key strings in your packets. - Send integers (incl. enums) instead of strings.
- Don't send strings, unless it's required, mind you: each character takes in 1 byte and we need to store the length.
- Send strings only once, e.g.: sending a players name per each name change.
- Send
float
s instead ofdouble
s, this'll reduce byte usage by half: 4B vs 8B. - Check the MsgPack spec for type sizes. We already store integers in the smallest possible container that can hold the given value.
- While networking: stay below or aim for 1KB packets or more wisely: read into and consider maximum packet sizes of MTU and UDP, also consider headers and our network layer overhead, including event names.
- Locally: best to keep everything within 1 assembly and limit your use of events, exports, and callbacks (NUI, refFuncs), although size is less of an issue, the serialization + internal redirection logic + deserialization will slow down your resources unnecessarily.
- Send packets as little as possible:
- Better to send a packet that triggers an event to happen every second than sending an event every second, the more you send the more packet loss can occur.
- The less you send the more players you can be of service, both computational and network overhead.
You can choose between 3 types of layouts for your types: Indexed
, Keyed
, and Default
, mentioned in the order you should generally consider them. Read below for more information about them.
Note: classes and structs that aren't marked as MsgPackSerializable
will default to the Layout.Default
layout. This may result in errors for some types, generally these are considered to be a user-error, meaning you are responsible for fixing these in your code.
(De)serializes all fields and properties marked with the Index
attribute to/from an array. Great to reduce data and increase networking performance.
[MsgPackSerializable(Layout.Indexed)]
public class MyClass
{
[Index(0)] public uint m_id = 0;
public string m_name = "My very long name"; // Let's not send this to reduce packet size, client/server should already know who this is with the given id
[Index(1)] private float m_health = 300.0;
}
TThe above will result in the following serialized form (visualized): [ 0, 300.0f ]
// 6 bytes
* Concerns: there's a lack of context per field, meaning other scripts who don't know about the array's layout/order may not be able to use them dynamically.
Maps all fields and properties marked with the Key
attribute
[MsgPackSerializable(Layout.Keyed)]
public class MyClass
{
[Key("id")] public uint m_id = 0;
[Key("name")] public string m_name = "My very long name";
private double m_health = 300.0;
[Key("health")] public double Health
{
get => m_health;
set => m_health = value;
}
}
The above will result in the following serialized form (visualized): { "id": 0, "name": "My very long name", "health": 300.0 }
// 43 bytes
* Concerns: the strings and double values take most room in our packet, consider going for Layout.Indexed
, remove name
, and/or (de)serializing floats instead of doubles.
Maps all public fields and properties, except those marked with the Ignore
attribute
[MsgPackSerializable(Layout.Default)]
public class MyClass
{
public uint m_id = 0;
[Ignore] public string m_name = "My very long name"; // Let's not send this to reduce packet size, client/server should already know who this is with the given id
private double m_health = 300.0;
public double Health
{
get => m_health;
set => m_health = value;
}
}
The above will result in the following serialized form (visualized): { "m_id": 0, "Health": 300.0 }
// 22 bytes
* Concerns: the strings and double values take most room in our packet, consider going for Layout.Indexed
and/or (de)serializing floats instead of doubles.
Don't want to open up all fields/properties for write permission? Use a constructor instead. Make sure you match the fields/properties and constructor's parameter types, including their order.
Note: only incoming arrays and types marked Layout.Indexed
are considered for this fast form of construction.
[MsgPackSerializable(Layout.Indexed)]
public class MyClass
{
// Mind the types and order of these fields
[Index(0)] readonly public uint m_id = 0;
[Index(1)] public float m_health = 100.0;
private float m_armor = 100.0;
[Index(2)] public float Armor => m_armor;
// Follow above fiels/properties their types and order
public MyClass(uint id, float health, float armor)
{
m_id = id;
m_health = health;
m_armor = armor;
}
}