Deusald Sharp is a library that contains useful functionalities when creating game in C#.
You can add this library to Unity project in 2 ways:
- In package manager add library as git repository using format:
https://github.com/Deusald/DeusaldSharp.git?path=/UnityPackage.NetStandard2.1#vX.X.X - Add package using Scoped Register: https://openupm.com/packages/com.deusald.deusaldsharp/
- Game Server Clock in two versions: Standard and Precise
- Clear C# Coroutines system similar to one that is build in Unity Engine
- Useful Enum/String/Task/List Extensions and Helpers
- Useful Math utils
- Messages system for sending messages to classes that can subscribe for specific messages
- 2D Spline class
- Vector2, Vector3 and Typed Vector2/Vector 3 classes with all classic vector math in it
Library contains two implementations of game server clock: precision one (that is more precise but eats more resources) and standard one. Clocks can be used to execute game server logic in fixed time ticks.
PrecisionClock serverClock = new PrecisionClock(50);
serverClock.Tick += frameNumber =>
{
// Game Logic
};
serverClock.TooLongFrame += () => Console.WriteLine("Too long frame");
serverClock.Kill();GameServerClock serverClock = new GameServerClock(50, 100);
serverClock.Tick += (frameNumber, deltaTime) =>
{
// Game Logic
};
serverClock.Log += Console.WriteLine;
serverClock.Kill();DeusaldSharp contains an implementation of Unity style coroutines logic. It can be used to execute part of the game logic separated by server frames/seconds/conditions.
CoRoCtrl.Reset();
IEnumerator<ICoData> TestMethod() // Definition of coroutine method
{
// Game logic
yield return CoRoCtrl.WaitForOneTick(); // Pause executing method for the next game server tick
// Game logic
yield return CoRoCtrl.WaitUntilDone(CoRoCtrl.RunCoRoutine(TestMethodTwo())); // Pause until other coroutine is done
// Game logic
yield return CoRoCtrl.WaitForSeconds(0.5f); // Pause for 0.5 second
// Game logic
yield return CoRoCtrl.WaitUntilTrue(() => pass); // Pause until condition is true
// Game logic
}
CoRoCtrl.RunCoRoutine(TestMethod()); // Run coroutine
CoRoCtrl.Update(0.33f); // Update all coroutines with a given delta time
// You can start coRoutine with CoTag, CoTag is a tag to mark group of logically connected CoRoutines.
// The CoTag can be later used to pause or kill all CoRoutines marked with specific tag.
CoRoCtrl.RunCoRoutine(TestMethod2(), new CoTag(1));
// You can also save coHandle of coRoutine that let's you check and control specific coRoutine
ICoHandle coHandle = CoRoCtrl.RunCoRoutine(TestMethod2());
if (coHandle.IsAlive) coHandle.Kill();Group of useful extensions and helper methods.
[Flags]
private enum TestFlags
{
None = 0,
A = 1 << 0,
B = 1 << 1,
C = 1 << 2
}
private enum TestEnum
{
A = 0,
B = 1
}
// Testing if only one bit is set
TestFlags flags = TestFlags.A;
TestFlags flags2 = TestFlags.A | TestFlags.C;
Assert.AreEqual(true, flags.IsSingleFlagOn());
Assert.AreEqual(false, flags2.IsSingleFlagOn());
// Getting random bit from those that are set
flags.GetRandomFlag((min, max) => new Random().Next(min, max));
flags.HasAnyFlag(TestFlags.A | TestFlags.C);
// Taking out arguments out of args
string[] args = { "10", "B" };
int ten = args.TakeSimpleType(0, 0);
TestEnum b = args.TakeEnum(1, TestEnum.A);
// Shuffle list
List<int> list = new List<int> { 1, 2, 3 };
list.Shuffle();
// ... etc.DeusaldSharp contains many math methods that can be useful when coding Game Server.
// For example
4.Clamp(2, 3); // 3
MathUtils.Lerp(1, 10, 0.25f); // 3.25f
MathUtils.InverseLerp(1, 10, 7.75f); // 0.75f
7.451587f.RoundToDecimal(3); // 7.452f
0.1f.IsFloatZero(); // False
1.55f.AreFloatsEquals(1.55f); // True
7.MarkBit(2, false); // 5
// ... etc.Module that enables a possibility to listen for specific messages in a class.
private class ExampleMsg
{
public int Int { get; set; }
public float Float { get; set; }
public string String { get; set; }
public object Object { get; set; }
}
// You can register class to listen for specific message type by using [MessageSlot] attribute
private class AttributeTest
{
public int ReceivedMessages { get; private set; }
public AttributeTest()
{
MsgCtrl.Register(this);
}
public void Unregister()
{
MsgCtrl.Unregister(this);
}
[MessageSlot]
public void Receive(ExampleMsg message)
{
++ReceivedMessages;
}
}
// Allocate new instance of a message.
ExampleMsg msg = MsgCtrl.Allocate<ExampleMsg>(); // Messages instances are recycled from the pool.
msg.Int = 10;
msg.Float = 10.5f;
msg.String = "Test";
msg.Object = objectToReceive;
MsgCtrl.Send(msg); // Send messages to all classes that listen for this specific message
// Instead of using [MessageSlot] attribute you can also use Bind/Unbind methods
MsgCtrl.Bind<ExampleMsg>(Receive);
MsgCtrl.Unbind<ExampleMsg>(Receive);DeusaldSharp contains class that lets you define and use 2d spline.
// Arrange
Spline2D one = new Spline2D(new List<Vector2> {new Vector2(0, 0), new Vector2(0, 1), new Vector2(0, 2)});
Spline2D two = new Spline2D(new List<Vector2> {new Vector2(0, 0), new Vector2(1, 1), new Vector2(2, 2)});
// Act
Vector2 positionOne = one.InterpolateDistance(1.5f);
Vector2 positionTwo = two.InterpolateDistance(1.5f);
Vector2 positionThree = one.InterpolateDistance(0.5f);
Vector2 positionFour = two.InterpolateDistance(0.5f);
Vector2 positionFive = one.InterpolateDistance(1.5f);
Vector2 positionSix = two.InterpolateDistance(1.5f);
// Assert
Assert.Multiple(() =>
{
Assert.AreEqual(new Vector2(0, 1.5015914f), positionOne);
Assert.AreEqual(new Vector2(1.0576555f, 1.0576555f), positionTwo);
Assert.AreEqual(new Vector2(0, 0.49840856f), positionThree);
Assert.AreEqual(new Vector2(0.35238677f, 0.35238677f), positionFour);
Assert.AreEqual(new Vector2(0, 1.5015914f), positionFive);
Assert.AreEqual(new Vector2(1.0576555f, 1.0576555f), positionSix);
});DeusaldSharp contains classes that implements Vector 2 and Vector 3 with all basic math connected to vectors.
Vector2 one = new Vector2(1f, 1f);
Vector2 two = new Vector2(2f, -5f);
one.Normalized;
one.Negated;
one.Skew;
one.Set(2f, 3f);
Vector2.Add(one, two);
one.Cross(two);
one.Distance(two);
one.Dot(two);
one.Reflect(two);
Vector2.Clamp(one, oneMin, oneMax);
// etc.DeusaldSharp contains classes for typed Vector 2 and Vector 3.
TVector2<int> one = new TVector2<int> {x = 10, y = 15};
TVector3<int> one = new TVector3<int> {x = 10, y = 15, z = 20};UsernameVerificator is a helper class for validating and cleaning usernames based on:
- minimum and maximum length,
- optional whitespace rule (no leading/trailing spaces),
- allowed character set (provided as a regex character-class).
using DeusaldSharp;
UsernameVerificator verificator = new UsernameVerificator(
minCharacters: 3,
maxCharacters: 16,
whitespaceRequirement: true,
charactersRequirementRegex: @"[A-Za-z0-9 _]"
);
bool isValid = verificator.CheckUsernameRequirements("Adam_123");
string cleaned = verificator.CleanUsername(" Ad!am@@ 12_3 "); // "Adam 12_3"Glicko is a helper for updating player ratings using a Glicko-2-like system.
using DeusaldSharp;
GlickoData player = new GlickoData
{
Rating = Glicko.DEFAULT_RATING,
Deviation = Glicko.DEFAULT_DEVIATION,
Volatility = Glicko.DEFAULT_VOLATILITY
};
// 1v1 update
GlickoData a = player;
GlickoData b = player;
// a wins (score = 1.0). draw = 0.5. loss = 0.0.
Glicko.Update(a, b, out GlickoData newA, out GlickoData newB, playerAScore: 1.0);
// Update vs multiple opponents
List<(GlickoData, double)> opponents = new()
{
(b, 1.0), // win
(b, 0.5), // draw
};
GlickoData updated = Glicko.Update(a, opponents);
// Win probability
double p = Glicko.GetWinProbability(a, b);
Decay (inactivity)
GlickoData decayed = Glicko.DecayPlayer(a, lastPlayedUtc, out bool didDecay);DeusaldSharp includes a set of BinaryWriter / BinaryReader extension methods for compact, allocation-friendly binary serialization of common types and their List<T> variants:
- Primitive lists:
byte,sbyte,bool,short,ushort,int,uint,long,ulong,float,double,char,string Guid,DateTime,TimeSpan,Version(and their list variants)- Serializable enums (zero-boxing, explicit wire type) via
[SerializableEnum]
using System.Collections.Generic;
using System.IO;
using DeusaldSharp;
using MemoryStream ms = new MemoryStream();
using BinaryWriter bw = new BinaryWriter(ms);
bw.Write(new List<int> { 1, 2, 3 }); // writes count + elements
bw.Write(new List<string> { "a", null }); // null -> "" (empty string)
bw.Flush();
ms.Position = 0;
using BinaryReader br = new BinaryReader(ms);
List<int> ints = br.ReadIntList();
List<string> strings = br.ReadStringList();Annotate an enum with [SerializableEnum] to define the on-the-wire numeric type. This avoids boxing and keeps the binary format explicit and stable.
using System.IO;
using DeusaldSharp;
[SerializableEnum(SerializableEnumType.Byte)]
public enum WeaponType : byte
{
Sword = 1,
Bow = 2
}
using MemoryStream ms = new MemoryStream();
using BinaryWriter bw = new BinaryWriter(ms);
bw.WriteSerializableEnum(WeaponType.Sword);
bw.WriteSerializableEnumList(new System.Collections.Generic.List<WeaponType> { WeaponType.Sword, WeaponType.Bow });
bw.Flush();
ms.Position = 0;
using BinaryReader br = new BinaryReader(ms);
WeaponType one = br.ReadSerializableEnum<WeaponType>();
System.Collections.Generic.List<WeaponType> many = br.ReadSerializableEnumList<WeaponType>();Notes
- List serialization format is always: int count followed by count elements.
- Write(List) writes v ?? string.Empty.
- Serializable enums require the
[SerializableEnum]attribute; otherwise reading/writing throws.
Below is a single, detailed README section you can copy-paste as-is. It documents the design goals, wire format, guarantees, edge cases, and usage patterns of your Proto module, aligned with the final, fixed implementation and the expanded test suite.
The Proto module is a lightweight, explicit, binary message system designed for:
- deterministic serialization,
- schema evolution (forward/backward compatibility),
- zero reflection at runtime,
- full control over wire format,
- safe skipping of unknown fields.
It is intentionally not Protocol Buffers–compatible; instead, it is optimized for game networking, save files, and internal tooling where stability and control matter more than compact varints.
Base class for all messages.
Responsibilities:
- owns a static
ProtoModel<TSelf>schema - provides:
byte[] Serialize()static TSelf Deserialize(byte[])
Each message type must assign _model in its static constructor.
public sealed class MyMsg : ProtoMsg<MyMsg>
{
public int X;
static MyMsg()
{
_model = new ProtoModel<MyMsg>(
ProtoField.Int<MyMsg>(1, static (ref MyMsg o) => ref o.X)
);
}
}Defines the schema for a message.
- Holds an ordered list of
ProtoField<T> - Serializes each field independently as:
[ushort fieldId][int payloadLength][payloadBytes]
During deserialization:
- fields are read sequentially until end-of-stream
- unknown
fieldIds are skipped - duplicate
fieldIds are allowed; last value wins
This guarantees:
- backward compatibility (new readers can read old data)
- forward compatibility (old readers skip new fields)
Factory class for defining fields.
Each field provides:
- a writer
(BinaryWriter, ref T) - a reader
(BinaryReader, ref T)
Factories are strongly typed, explicit, and allocation-aware.
Every field is length-prefixed.
This means:
- unknown fields can be skipped safely
- corrupted or truncated payloads throw immediately
- nested objects and lists are safe
For the same object state:
Serialize()always produces identical byte output- field order is stable (schema order)
This is verified by tests and enables:
- hashing
- caching
- binary diffs
- deterministic replays
If the same field ID appears multiple times in the stream:
[id=1][payload=10]
[id=1][payload=99]
Result:
field == 99This allows:
- patch-style updates
- stream merging
- late overrides
Bool, Byte, SByte,
Short, UShort,
Int, UInt,
Long, ULong,
Float, Double,
Char, StringExample:
ProtoField.Int<MyMsg>(1, static (ref MyMsg o) => ref o.Value);Serialized as:
[bool hasValue][value?]
Factories:
NullableInt
NullableGuid
NullableTimeSpan
NullableSerializableEnumExample:
ProtoField.NullableInt<MyMsg>(2, static (ref MyMsg o) => ref o.OptionalValue);Serialized as:
[int count][item][item][item]...
Primitive list factories:
ByteList, SByteList, BoolList,
ShortList, UShortList,
IntList, UIntList,
LongList, ULongList,
FloatList, DoubleList,
CharList, StringListSpecialized lists:
GuidList
DateTimeList
TimeSpanList
VersionList
SerializableEnumListExample:
ProtoField.IntList<MyMsg>(3, static (ref MyMsg o) => ref o.Values);Serialized as:
[bool hasValue]
false -> null
true -> [int count][items...]
Factories:
NullableIntList
NullableStringList
NullableGuidList
NullableSerializableEnumList
...Example:
ProtoField.NullableStringList<MyMsg>(4, static (ref MyMsg o) => ref o.Tags);Enums are serialized using the SerializableEnum system.
Requirements:
- enum must be annotated with
[SerializableEnum] - wire type is explicit and stable
[SerializableEnum(SerializableEnumType.SByte)]
public enum State : sbyte
{
Idle = 0,
Active = 1,
Disabled = -1
}Field usage:
ProtoField.SerializableEnum<MyMsg, State>(5, static (ref MyMsg o) => ref o.State);
ProtoField.NullableSerializableEnum<MyMsg, State>(6, static (ref MyMsg o) => ref o.OptionalState);Negative values are fully supported.
Objects are serialized as length-delimited nested messages:
[int byteLength][objectBytes]
Factory:
ProtoField.Object<Parent, Child>(7, static (ref Parent o) => ref o.Child);Serialized as:
[int byteLength]
0 -> null
>0 -> nested message bytes
Factory:
ProtoField.NullableObject<Parent, Child>(8, static (ref Parent o) => ref o.OptionalChild);null roundtrips as null.
Serialized as:
[int count]
[int len][object bytes]
[int len][object bytes]
...
Factories:
ObjectList
NullableObjectListThis design guarantees:
- no overreads
- no stream corruption
- safe partial deserialization
New fields default to zero/null.
Unknown fields are skipped.
Safe — field IDs define meaning, not order.
Safe — reader simply never sees them.
- Corrupt payload lengths →
EndOfStreamException - Enum overflow / mismatch →
InvalidOperationException - Missing enum attribute →
InvalidOperationException
Errors fail fast and loudly.
This Proto module is intentionally:
- explicit over magical
- schema-driven
- allocation-aware
- debuggable
- stable over time
It is especially suitable for:
- multiplayer game protocols
- save file formats
- editor tooling
- deterministic simulations
If you need varints, reflection, or codegen — this is not that. If you need control, safety, and long-term stability — this is.