Skip to content

Instantly share code, notes, and snippets.

@lazalong
Last active July 20, 2020 03:46
Show Gist options
  • Save lazalong/0710e58e890f9636c42d28816008c156 to your computer and use it in GitHub Desktop.
Save lazalong/0710e58e890f9636c42d28816008c156 to your computer and use it in GitHub Desktop.
C# version of NetDynamics (https://github.com/nxrighthere/NetDynamics)
/****************************** Module Header ******************************\
* Module Name: NetDynamics_CS.cs
* Project: NetDynamics_CS
* Copyright (c) 2020 Steven 'lazalong' Gay
*
* C# version of https://github.com/nxrighthere/NetDynamics
*
* Data-oriented networking playground for the reliable UDP transports
*
* This source is subject to the Microsoft Public License.
* See https://opensource.org/licenses/MS-PL
* All other rights reserved.
*
* THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
\***************************************************************************/
using System;
using Raylib_cs;
using ENet;
using System.Numerics;
using NetStack.Serialization;
struct Settings
{
public uint resolutionWidth;
public uint resolutionHeight;
public uint framerateLimit;
public ushort vsync;
public ushort transport;
public string ip;
public ushort port;
public float sendRate;
public Int32 redundantBytes;
public int maxClients;
public int maxChannels;
public uint incomingBandwith;
public uint outgoingBandwith;
public int bufferSize;
public int maxEntities;
public int nbEntitiesSpawn;
public float maxMovementSpeed;
public void SetDefault()
{
resolutionWidth = 640;
resolutionHeight = 480;
framerateLimit = 60;
vsync = 0;
transport = 1;
ip = "::1";
port = 5000;
sendRate = 20;
redundantBytes = 0;
// ENet
maxClients = 32;
maxChannels = 0;
incomingBandwith = 0;
outgoingBandwith = 0;
bufferSize = 1024 * 1024;
maxEntities = 100000;
nbEntitiesSpawn = 10;
maxMovementSpeed = 80f;
}
};
internal unsafe struct Buffer
{
public const int bufferSize = 1024 * 1024;
public fixed byte redundantBuffer[bufferSize];
}
enum OpCode
{
NONE = 0,
SPAWN,
MOVE,
DESTROY
}
static class Program
{
static Settings settings;
static int nbEntities;
static float[] position_x;
static float[] position_y;
static float[] speed_x;
static float[] speed_y;
static float[] destination_x;
static float[] destination_y;
static Color[] color; // hexadecimal's RGB color value convverted to long i.e. 9C4A4A = 10242634
// white = 16777215 = xFFFFFF black = 0
// https://answers.unity.com/questions/812240/convert-hex-int-to-colorcolor32.html
static byte[] data = new byte[128];
static Random rand = new Random();
#if SERVER
static float sendTime = 0.0f;
#endif
public static void Main()
{
Buffer buffer = default;
Peer peer = default;
Texture2D texture;
Font font = default;
string status = "Not initialised";
int fps = 0;
int counter = 0;
int refreshRate = 20;
ENet.Event netEvent = default;
settings.SetDefault();
// -------- Set Window ----------
string title = GetTitle();
Console.WriteLine("---- " + title + " ----");
SetRaylib(title, ref font);
// ------ Serialisation -------------
// LATER: add own allocator, binn type
if (settings.redundantBytes > 0)
{
if (settings.redundantBytes > Buffer.bufferSize) // ? sizeof(buffer.redundantBuffer))
{
Console.WriteLine("RedundantBytes exceeds buffer: scaling back to " + Buffer.bufferSize);
settings.redundantBytes = Buffer.bufferSize;
}
for (int i = 0; i < settings.redundantBytes; i++)
{
unsafe
{
buffer.redundantBuffer[i] = 0; // ??? i % sizeof(uint8_t); isn't this alwyas 0?
//Console.WriteLine(i % 8);
}
}
}
#region --------- Set Network -----------
if (!Library.Initialize()) // TODO alloc callbacks
{
Console.WriteLine("ENet library initialisation failed");
return;
}
Address address = new Address();
address.Port = settings.port;
Host host = new Host();
#if SERVER
host.Create(address, settings.maxClients, settings.maxChannels, settings.incomingBandwith,
settings.outgoingBandwith, settings.bufferSize);
status = "Server listening";
#else
host.Create(null, 1, settings.maxChannels, settings.incomingBandwith,
settings.outgoingBandwith, settings.bufferSize);
if (!address.SetIP(settings.ip))
{
Console.WriteLine("Couldn't set ip");
return;
}
peer = host.Connect(address, settings.maxChannels, 0);
status = peer.State.ToString();
#endif
#endregion
#region ----------- Set Data -------------------
nbEntities = 0;
position_x = new float[settings.maxEntities];
position_y = new float[settings.maxEntities];
speed_x = new float[settings.maxEntities];
speed_y = new float[settings.maxEntities];
destination_x = new float[settings.maxEntities];
destination_y = new float[settings.maxEntities];
color = new Color[settings.maxEntities];
texture = Raylib.LoadTexture("neon_circle.png");
#if CLIENT
destination_x = new float[settings.maxEntities];
destination_y = new float[settings.maxEntities];
uint rtt = 0;
#else
float sendInterval = 1.0f / settings.sendRate;
#endif
#endregion
// ---------- MAIN LOOP ----------------------
while (!Raylib.WindowShouldClose())
{
float deltaTime = Raylib.GetFrameTime();
#region ---------- Systems ----------
bool polled = false;
while (!polled)
{
if (host.CheckEvents(out netEvent) <= 0)
{
if (host.Service(0, out netEvent) <= 0)
break;
polled = true;
}
switch (netEvent.Type)
{
case EventType.None:
break;
case EventType.Connect:
#if SERVER
netEvent.Peer.PingInterval(250);
for (int i = 0; i < nbEntities; i++)
{
// Test send opcode
//Packet packet = default;
//// Sends an int32, to read use: int opcode = Marshal.ReadInt32(netEvent.Packet.Data);
//data[0] = (byte)(i & 0x000000FF);
//data[1] = (byte)((i & 0x0000FF00) >> 8);
//data[2] = (byte)((i & 0x00FF0000) >> 16);
//data[3] = (byte)((i & 0xFF000000) >> 24);
//packet.Create(data, 4);
//netEvent.Peer.Send(0, ref packet);
Peer peer2 = netEvent.Peer;
SendMsg(ref peer2, OpCode.SPAWN, i);
}
#else
status = "Connected";
#endif
break;
case EventType.Disconnect:
#if SERVER
Console.WriteLine("Disconnection of peer " + netEvent.Peer.ID + " ip= " + netEvent.Peer.IP);
#else
status = "Disconnected";
EntityFlush(); // !
#endif
break;
case EventType.Receive:
// Test receive opcode
//int opcode = Marshal.ReadInt32(netEvent.Packet.Data);
//Console.WriteLine("Opcode: " + opcode);
int id = ReceiveMsg(ref netEvent);
#if SERVER
if (id == (int) OpCode.SPAWN)
{
// Send the new nbEntitiesSpawn's entities )
for (int i = nbEntities - settings.nbEntitiesSpawn; i < nbEntities; i++)
{
SendToAll(ref host, OpCode.SPAWN, i);
}
}
else if (id == (int) OpCode.DESTROY)
{
int nb = nbEntities - settings.nbEntitiesSpawn;
if (nb < 0)
nb = 0;
nbEntities = nb;
SendToAll(ref host, OpCode.DESTROY, 0);
}
#endif
netEvent.Packet.Dispose();
break;
case EventType.Timeout:
#if SERVER
Console.WriteLine("Timeout from " + netEvent.Peer.ID + " ip= " + netEvent.Peer.IP);
#endif
break;
}
}
#if CLIENT
rtt = peer.RoundTripTime;
#endif
#endregion
#region ---------- Timer ----------
#if SERVER
sendTime += deltaTime;
#endif
#endregion
#region ---------- Input Mgr = Spawn ----------
if (Raylib.IsMouseButtonDown(MouseButton.MOUSE_LEFT_BUTTON))
{
#if SERVER
Vector2 pos = Raylib.GetMousePosition();
Entity_Spawn(pos.X, pos.Y, settings.nbEntitiesSpawn);
if (host.PeersCount > 0)
{
host.Flush(); // <---------- why???
for (int i = nbEntities - settings.nbEntitiesSpawn; i < nbEntities; i++)
{
SendToAll(ref host, OpCode.SPAWN, i);
}
}
#else
if (peer.State == PeerState.Connected)
{
host.Flush(); // <---------- why??
SendMsg(ref peer, OpCode.SPAWN, 0);
}
#endif
}
#endregion
#region ---------- Move ----------
if (nbEntities>0)
{
#if SERVER
MoveEntity(deltaTime);
if (sendTime >= sendInterval)
{
sendTime -= sendInterval;
if (host.PeersCount > 0)
{
host.Flush(); // <---------- why???
for (int i = 0; i < nbEntities; i++)
{
SendToAll(ref host, OpCode.MOVE, i);
}
}
}
#else
for (int i = 0; i < nbEntities; i++)
{
// needed? if (destination[i].x == 0.0f && destination[i].y == 0.0f)
// continue;
MoveEntity(i, deltaTime);
}
#endif
}
#endregion
#region ---------- Destroy ----------
if (nbEntities > 0)
{
if (Raylib.IsMouseButtonDown(MouseButton.MOUSE_RIGHT_BUTTON))
{
#if SERVER
int nb = nbEntities - settings.nbEntitiesSpawn;
if (nb < 0)
nb = 0;
nbEntities = nb;
SendToAll(ref host, OpCode.DESTROY, 0);
#else
host.Flush(); // <---------- why??
SendMsg(ref peer, OpCode.DESTROY, 0);
#endif
}
}
#endregion
#region ---------- Render ----------
Raylib.BeginDrawing();
Raylib.ClearBackground(Color.LIGHTGRAY);
// if (error != null)
// Raylib.DrawTextEx(font, "Error " + error, new System.Numerics.Vector2(12, 30), 20, 0, Color.WHITE);
// else
// ------- Render Entities ----------------
if (nbEntities > 0)
{
for (int i = 0; i < nbEntities; i++)
{
Raylib.DrawTexture(texture, (int)position_x[i], (int)position_y[i], color[i]);
}
}
// ------ Render Stats ------------
if (counter < refreshRate)
{
counter++;
}
else
{
fps = Raylib.GetFPS();
refreshRate = fps;
counter = 0;
}
Raylib.DrawText("NetDynamics C# - " + title, 10, 10, 20, Color.WHITE);
Raylib.DrawText("FPS " + fps, 10, 35, 20, Color.BLACK);
Raylib.DrawText("Entities " + nbEntities, 10, 70, 20, Color.BLACK);
Raylib.DrawText("Status " + status, 10, 100, 20, Color.BLACK);
#if SERVER
Raylib.DrawText("Nb Peers " + host.PeersCount, 10, 125, 20, Color.BLACK);
Raylib.DrawText("Send Rate " + settings.sendRate, 10, 150, 20, Color.BLACK);
float mps = host.PeersCount * nbEntities * settings.sendRate;
Raylib.DrawText("MSG Per Sec: Target " + mps, 10, 175, 20, Color.BLACK);
//Raylib.DrawText("MSG Per Sec: Measures " + fps, 10, 200, 20, Color.BLACK);
#else
Raylib.DrawText("RTT " + peer.RoundTripTime, 10, 125, 20, Color.BLACK);
#endif
#if CLIENT
Raylib.DrawText("Byte Sent " + peer.BytesSent.ToString(), 10, 150, 20, Color.BLACK);
Raylib.DrawText("Packet Sent " + peer.PacketsSent.ToString(), 10, 175, 20, Color.BLACK);
Raylib.DrawText("Byte Received " + peer.BytesReceived.ToString(), 10, 200, 20, Color.BLACK);
Raylib.DrawText("Packet Lost " + peer.PacketsLost.ToString(), 10, 225, 20, Color.BLACK);
#else
Raylib.DrawText("Byte Sent " + host.BytesSent.ToString(), 10, 225, 20, Color.BLACK);
Raylib.DrawText("Packet Sent " + host.PacketsSent.ToString(), 10, 250, 20, Color.BLACK);
Raylib.DrawText("Byte Received " + host.BytesReceived.ToString(), 10, 275, 20, Color.BLACK);
#endif
Raylib.EndDrawing();
#endregion // Render
} // main loop
#region -------- Closure ----------------
if (host != null && host.IsSet)
{
#if CLIENT
peer.DisconnectNow(0);
#else
for (int i = 0; i < host.PeersCount; i++)
{
// TODO peers[i].disconnectNow(0);
}
#endif
host.Flush();
host.Dispose();
}
Library.Deinitialize();
Raylib.UnloadFont(font);
Raylib.UnloadTexture(texture);
Raylib.CloseWindow();
#endregion
// Console.WriteLine("\nType key to quit");
// Console.ReadKey();
}
static void SetRaylib(string title, ref Font font)
{
if (settings.vsync > 0)
Raylib.SetConfigFlags(ConfigFlag.FLAG_VSYNC_HINT);
Raylib.InitWindow((int)settings.resolutionWidth, (int)settings.resolutionHeight, title);
Raylib.SetTargetFPS((int)settings.framerateLimit);
font = Raylib.LoadFontEx("share_tech.ttf", 25, null, 0); // ?? why are both last param empty... see later.
Raylib.SetTextureFilter(font.texture, TextureFilterMode.FILTER_POINT); // TODO I don't see it used...
}
static string GetTitle()
{
string str;
#if CLIENT
#if DEBUG
str = "Debug Client";
#else
str = "Release Client";
#endif
#elif SERVER
#if DEBUG
str = "Debug Server";
#else
str = "Release Server";
#endif
#else
Console.WriteLine("No SERVER or CLIENT preprocessor. Aborting");
throw new Exception("No SERVER or CLIENT preprocessor. Aborting");
#endif
return str;
}
static void EntityFlush()
{
position_x[0] = 0;
position_y[0] = 0;
speed_x[0] = 0;
speed_y[0] = 0;
destination_x[0] = 0;
destination_y[0] = 0;
}
static class BufferPool
{
[ThreadStatic]
private static BitBuffer bitBuffer;
public static BitBuffer GetBitBuffer()
{
if (bitBuffer == null)
bitBuffer = new BitBuffer(1024);
return bitBuffer;
}
}
static void SendMsg(ref Peer peer, OpCode opcode, int id)
{
bool reliable = false;
Packet packet = default(Packet);
BitBuffer buffer = BufferPool.GetBitBuffer();
buffer.Clear();
Span<byte> span = new Span<byte>(data);
if (opcode == OpCode.SPAWN)
{
reliable = true;
buffer.AddInt((int)opcode);
#if SERVER
buffer.AddInt(id)
.AddLong(((long)(position_x[id] * 100000f)))
.AddLong(((long)(position_y[id] * 100000f)))
.AddLong(((long)(speed_x[id] * 100000f)))
.AddLong(((long)(speed_y[id] * 100000f)))
.AddByte(color[id].r)
.AddByte(color[id].g)
.AddByte(color[id].b)
.AddByte(color[id].a)
.ToSpan(ref span);
//Console.WriteLine(">>>>>>>>> x " + position_x[id] + " " + (long)(position_x[id] * 100000f));
#else
Vector2 mousePosition = Raylib.GetMousePosition();
//Console.WriteLine(" Mouse " + mousePosition.X + " " + mousePosition.Y);
//Console.WriteLine(" " + mousePosition.X * 100000f + " " + mousePosition.Y * 100000f);
//Console.WriteLine(" " + (long)(mousePosition.X * 100000f) + " " + (long)(mousePosition.Y * 100000f));
buffer.AddLong((long)(mousePosition.X * 100000f))
.AddLong((long)(mousePosition.Y * 100000f));
#endif
}
else if (opcode == OpCode.DESTROY)
{
reliable = true;
buffer.AddInt((int)opcode);
}
else
{
return;
}
if (settings.redundantBytes > 0)
{
for (int i = 0; i < settings.redundantBytes; i++)
{
buffer.AddByte(0);
}
}
buffer.ToSpan(ref span);
data = span.ToArray();
packet.Create(data, buffer.Length, reliable ? PacketFlags.Reliable : PacketFlags.None);
peer.Send(0, ref packet);
}
static int ReceiveMsg(ref ENet.Event netEvent)
{
ReadOnlySpan<byte> span;
unsafe
{
span = new ReadOnlySpan<byte>((byte*)netEvent.Packet.Data, netEvent.Packet.Length);
}
BitBuffer bitBuffer = BufferPool.GetBitBuffer();
bitBuffer.Clear();
bitBuffer.FromSpan(ref span, netEvent.Packet.Length); // LATER opcode as short hence 2
int opcode = bitBuffer.ReadInt();
if (opcode == (int)OpCode.NONE)
{
Console.WriteLine("Received empty msg...");
return 0;
}
else if (opcode == (int)OpCode.SPAWN)
{
#if SERVER
float px = ((float)bitBuffer.ReadLong()) / 100000f;
float py = ((float)bitBuffer.ReadLong()) / 100000f;
//Console.WriteLine("px " + px + " py " + py);
Entity_Spawn(px, py, settings.nbEntitiesSpawn);
// entity_spawn((Vector2) { binn_list_float(data, 2), binn_list_float(data, 3) }, NET_MAX_ENTITY_SPAWN);
#else
int id = bitBuffer.ReadInt();
float px = ((float)bitBuffer.ReadLong()) / 100000f;
float py = ((float)bitBuffer.ReadLong()) / 100000f;
float sx = ((float)bitBuffer.ReadLong()) / 100000f;
float sy = ((float)bitBuffer.ReadLong()) / 100000f;
Color col = new Color(
bitBuffer.ReadByte(),
bitBuffer.ReadByte(),
bitBuffer.ReadByte(),
bitBuffer.ReadByte());
Entity_Spawn(id, px, py, sx, sy, col);
// entity_spawn((Entity)binn_list_uint32(data, 2), (Vector2) { binn_list_float(data, 3), binn_list_float(data, 4) }, (Vector2) { binn_list_float(data, 5), binn_list_float(data, 6) }, (Color) { binn_list_uint8(data, 7), binn_list_uint8(data, 8), binn_list_uint8(data, 9), 255 });
#endif
}
else if (opcode == (int)OpCode.MOVE)
{
#if CLIENT
int id = bitBuffer.ReadInt();
float px = ((float)bitBuffer.ReadLong()) / 100000f;
float py = ((float)bitBuffer.ReadLong()) / 100000f;
float sx = ((float)bitBuffer.ReadLong()) / 100000f;
float sy = ((float)bitBuffer.ReadLong()) / 100000f;
MoveEntity(id, px, py, sx, sy);
#endif
}
else if (opcode == (int)OpCode.DESTROY)
{
#if CLIENT
int nb = nbEntities - settings.nbEntitiesSpawn;
if (nb < 0)
nb = 0;
nbEntities = nb;
#endif
}
return opcode;
}
static void Entity_Spawn(float x, float y, int nbEntitiesToSpawn)
{
if ((nbEntities + nbEntitiesToSpawn) > settings.maxEntities)
{
Console.WriteLine("Max nb entities reached");
return;
}
int firstEntity = nbEntities;
nbEntities += nbEntitiesToSpawn;
for (int i = firstEntity; i < nbEntities; i++)
{
position_x[i] = x;
position_y[i] = y;
speed_x[i] = (float)rand.NextDouble() - 0.5f;
speed_y[i] = (float) rand.NextDouble() - 0.5f;
destination_x[i] = x;
destination_y[i] = y;
// Unity use: int col = (int) (rand.NextDouble() * 16777215.0d);
color[i] = new Color(
(byte)rand.Next(255),
(byte)rand.Next(255),
(byte)rand.Next(255),
(byte)rand.Next(255));
//Console.WriteLine("id2: " + i + " <" + x + "," + y + "> <" + speed_x[i] + "," + speed_y[i] + ">");
}
}
static void Entity_Spawn(int entity, float px, float py, float sx, float sy, Color col)
{
//Console.WriteLine("id: " + entity + " <" + px + "," + py + "> <" + sx + "," + sy + ">");
position_x[entity] = px;
position_y[entity] = py;
speed_x[entity] = sx;
speed_y[entity] = sy;
destination_x[entity] = px;
destination_y[entity] = py;
color[entity] = col; // Unity use: (int) (rand.NextDouble() * 16777215.0d);
nbEntities++;
}
static void SendToAll(ref Host host, OpCode opcode, int id)
{
bool reliable = false;
Packet packet = default(Packet);
BitBuffer buffer = BufferPool.GetBitBuffer();
buffer.Clear();
Span<byte> span = new Span<byte>(data);
buffer.AddInt((int)opcode);
if (opcode == OpCode.SPAWN)
{
reliable = true;
buffer.AddInt(id)
.AddLong((long)(position_x[id] * 100000f))
.AddLong((long)(position_y[id] * 100000f))
.AddLong((long)(speed_x[id] * 100000f))
.AddLong((long)(speed_y[id] * 100000f))
.AddByte(color[id].r)
.AddByte(color[id].g)
.AddByte(color[id].b)
.AddByte(color[id].a);
//Console.WriteLine("server id: " + id + " <" + position_x[id] + "," + position_y[id] + "> <" + speed_x[id] + "," + speed_y[id] + ">");
//Console.WriteLine(" id: " + id + " <" + (long)(position_x[id] * 100000f) + "," + (long)(position_y[id] * 100000f) + "> <" + (long)(speed_x[id] * 100000f) + "," + (long)(speed_y[id] * 100000f) + ">");
}
else if (opcode == OpCode.MOVE)
{
buffer.AddInt(id)
.AddLong((long)(position_x[id] * 100000f))
.AddLong((long)(position_y[id] * 100000f))
.AddLong((long)(speed_x[id] * 100000f))
.AddLong((long)(speed_y[id] * 100000f));
}
else if (opcode == OpCode.DESTROY)
{
reliable = true;
buffer.AddInt(id);
}
if (settings.redundantBytes > 0)
{
for (int i = 0; i < settings.redundantBytes; i++)
{
buffer.AddByte(0);
}
}
buffer.ToSpan(ref span);
data = span.ToArray();
packet.Create(data, buffer.Length, reliable ? PacketFlags.Reliable : PacketFlags.None);
host.Broadcast(0, ref packet);
}
static void MoveEntity(int id, float deltaTime)
{
float to_x = destination_x[id] - position_x[id];
float to_y = destination_y[id] - position_y[id];
float sqrtDist = to_x * to_x + to_y * to_y;
float maxDistanceDelta = (float) Math.Sqrt(speed_x[id] * speed_x[id] + speed_y[id] * speed_y[id]);
float step = maxDistanceDelta * settings.maxMovementSpeed * deltaTime;
if (sqrtDist == 0f || (step >= 0f && sqrtDist <= step*step))
{
position_x[id] = destination_x[id];
position_y[id] = destination_y[id];
return;
}
float distance = (float) Math.Sqrt(sqrtDist);
position_x[id] = position_x[id] + to_x / distance * step;
position_y[id] = position_y[id] + to_y / distance * step;
}
static void MoveEntity(float deltaTime)
{
float speed = settings.maxMovementSpeed;
for (int i = 0; i < nbEntities; i++)
{
position_x[i] += speed_x[i] * speed * deltaTime;
position_y[i] += speed_y[i] * speed * deltaTime;
if (position_x[i] > settings.resolutionWidth || position_x[i] < 0)
speed_x[i] *= -1f;
if (position_y[i] > settings.resolutionHeight|| position_y[i] < 0)
speed_y[i] *= -1f;
}
}
static void MoveEntity(int id, float px, float py, float sx, float sy)
{
destination_x[id] = px;
destination_y[id] = py;
speed_x[id] = sx;
speed_y[id] = sy;
}
}
@lazalong
Copy link
Author

In debug 500'000 messages per seconds
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment