Compare commits

...

7 Commits

Author SHA1 Message Date
edmand46 5f3b2d7ed8 - Added support entities on scene
- Improved performance
2022-07-13 07:56:44 +04:00
edmand46 773adeefb2 feat: added scheduler, minor fixes 2022-07-02 11:02:09 +04:00
edmand46 e674600308 fixed: removed outdated code 2022-06-25 23:00:00 +04:00
edmand46 4f587fa59c chore: update readme 2022-06-25 13:32:05 +04:00
edmand46 76caa840bd update version 2022-06-25 13:26:39 +04:00
edmand46 1bff47e56b chore: removed prerelease flag 2022-06-25 11:09:36 +04:00
edmand46 1e41b9f2eb refactor 2022-06-25 11:08:50 +04:00
36 changed files with 402 additions and 692 deletions
-275
View File
@@ -1,275 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace DisruptorUnity3d
{
/// <summary>
/// Implementation of the Disruptor pattern
/// </summary>
/// <typeparam name="T">the type of item to be stored</typeparam>
public class RingBuffer<T>
{
private readonly T[] _entries;
private readonly int _modMask;
private Volatile.PaddedLong _consumerCursor = new Volatile.PaddedLong();
private Volatile.PaddedLong _producerCursor = new Volatile.PaddedLong();
/// <summary>
/// Creates a new RingBuffer with the given capacity
/// </summary>
/// <param name="capacity">The capacity of the buffer</param>
/// <remarks>Only a single thread may attempt to consume at any one time</remarks>
public RingBuffer(int capacity)
{
capacity = NextPowerOfTwo(capacity);
_modMask = capacity - 1;
_entries = new T[capacity];
}
/// <summary>
/// The maximum number of items that can be stored
/// </summary>
public int Capacity
{
get { return _entries.Length; }
}
public T this[long index]
{
get { unchecked { return _entries[index & _modMask]; } }
set { unchecked { _entries[index & _modMask] = value; } }
}
/// <summary>
/// Removes an item from the buffer.
/// </summary>
/// <returns>The next available item</returns>
public T Dequeue()
{
var next = _consumerCursor.ReadAcquireFence() + 1;
while (_producerCursor.ReadAcquireFence() < next) // makes sure we read the data from _entries after we have read the producer cursor
{
Thread.SpinWait(1);
}
var result = this[next];
_consumerCursor.WriteReleaseFence(next); // makes sure we read the data from _entries before we update the consumer cursor
return result;
}
/// <summary>
/// Attempts to remove an items from the queue
/// </summary>
/// <param name="obj">the items</param>
/// <returns>True if successful</returns>
public bool TryDequeue(out T obj)
{
var next = _consumerCursor.ReadAcquireFence() + 1;
if (_producerCursor.ReadAcquireFence() < next)
{
obj = default(T);
return false;
}
obj = Dequeue();
return true;
}
/// <summary>
/// Add an item to the buffer
/// </summary>
/// <param name="item"></param>
public void Enqueue(T item)
{
var next = _producerCursor.ReadAcquireFence() + 1;
long wrapPoint = next - _entries.Length;
long min = _consumerCursor.ReadAcquireFence();
while (wrapPoint > min)
{
min = _consumerCursor.ReadAcquireFence();
Thread.SpinWait(1);
}
this[next] = item;
_producerCursor.WriteReleaseFence(next); // makes sure we write the data in _entries before we update the producer cursor
}
/// <summary>
/// The number of items in the buffer
/// </summary>
/// <remarks>for indicative purposes only, may contain stale data</remarks>
public int Count { get { return (int)(_producerCursor.ReadFullFence() - _consumerCursor.ReadFullFence()); } }
private static int NextPowerOfTwo(int x)
{
var result = 2;
while (result < x)
{
result <<= 1;
}
return result;
}
}
public static class Volatile
{
private const int CacheLineSize = 64;
[StructLayout(LayoutKind.Explicit, Size = CacheLineSize * 2)]
public struct PaddedLong
{
[FieldOffset(CacheLineSize)]
private long _value;
/// <summary>
/// Create a new <see cref="PaddedLong"/> with the given initial value.
/// </summary>
/// <param name="value">Initial value</param>
public PaddedLong(long value)
{
_value = value;
}
/// <summary>
/// Read the value without applying any fence
/// </summary>
/// <returns>The current value</returns>
public long ReadUnfenced()
{
return _value;
}
/// <summary>
/// Read the value applying acquire fence semantic
/// </summary>
/// <returns>The current value</returns>
public long ReadAcquireFence()
{
var value = _value;
Thread.MemoryBarrier();
return value;
}
/// <summary>
/// Read the value applying full fence semantic
/// </summary>
/// <returns>The current value</returns>
public long ReadFullFence()
{
Thread.MemoryBarrier();
return _value;
}
/// <summary>
/// Read the value applying a compiler only fence, no CPU fence is applied
/// </summary>
/// <returns>The current value</returns>
[MethodImpl(MethodImplOptions.NoOptimization)]
public long ReadCompilerOnlyFence()
{
return _value;
}
/// <summary>
/// Write the value applying release fence semantic
/// </summary>
/// <param name="newValue">The new value</param>
public void WriteReleaseFence(long newValue)
{
Thread.MemoryBarrier();
_value = newValue;
}
/// <summary>
/// Write the value applying full fence semantic
/// </summary>
/// <param name="newValue">The new value</param>
public void WriteFullFence(long newValue)
{
Thread.MemoryBarrier();
_value = newValue;
}
/// <summary>
/// Write the value applying a compiler fence only, no CPU fence is applied
/// </summary>
/// <param name="newValue">The new value</param>
[MethodImpl(MethodImplOptions.NoOptimization)]
public void WriteCompilerOnlyFence(long newValue)
{
_value = newValue;
}
/// <summary>
/// Write without applying any fence
/// </summary>
/// <param name="newValue">The new value</param>
public void WriteUnfenced(long newValue)
{
_value = newValue;
}
/// <summary>
/// Atomically set the value to the given updated value if the current value equals the comparand
/// </summary>
/// <param name="newValue">The new value</param>
/// <param name="comparand">The comparand (expected value)</param>
/// <returns></returns>
public bool AtomicCompareExchange(long newValue, long comparand)
{
return Interlocked.CompareExchange(ref _value, newValue, comparand) == comparand;
}
/// <summary>
/// Atomically set the value to the given updated value
/// </summary>
/// <param name="newValue">The new value</param>
/// <returns>The original value</returns>
public long AtomicExchange(long newValue)
{
return Interlocked.Exchange(ref _value, newValue);
}
/// <summary>
/// Atomically add the given value to the current value and return the sum
/// </summary>
/// <param name="delta">The value to be added</param>
/// <returns>The sum of the current value and the given value</returns>
public long AtomicAddAndGet(long delta)
{
return Interlocked.Add(ref _value, delta);
}
/// <summary>
/// Atomically increment the current value and return the new value
/// </summary>
/// <returns>The incremented value.</returns>
public long AtomicIncrementAndGet()
{
return Interlocked.Increment(ref _value);
}
/// <summary>
/// Atomically increment the current value and return the new value
/// </summary>
/// <returns>The decremented value.</returns>
public long AtomicDecrementAndGet()
{
return Interlocked.Decrement(ref _value);
}
/// <summary>
/// Returns the string representation of the current value.
/// </summary>
/// <returns>the string representation of the current value.</returns>
public override string ToString()
{
var value = ReadFullFence();
return value.ToString();
}
}
}
}
+1
View File
@@ -20,6 +20,7 @@ namespace Ragon.Common
PLAYER_LEAVED,
CREATE_ENTITY,
CREATE_STATIC_ENTITY,
DESTROY_ENTITY,
SNAPSHOT,
@@ -4,19 +4,19 @@ namespace Game.Source
{
public class SimplePlugin: PluginBase
{
public override void OnStart()
{
_logger.Info("Plugin started");
// _logger.Info("Plugin started");
}
public override void OnStop()
{
_logger.Info("Plugin stopped");
// _logger.Info("Plugin stopped");
}
public override void OnPlayerJoined(Player player)
{
// _logger.Info($"Player({player.PlayerName}) joined to Room({GameRoom.Id})");
}
+7 -4
View File
@@ -1,8 +1,11 @@
{
"key": "defaultkey",
"tickRate": 30,
"statisticsInterval": 5,
"sendRate": 30,
"port": 4444,
"skipTimeout": 60,
"server": {
"port": 4444
}
"reconnectTimeout": 300,
"maxConnections": 4095,
"maxPlayersPerRoom": 20,
"maxRooms": 200
}
+7 -59
View File
@@ -86,9 +86,9 @@ namespace Stress
{
ragonSerializer.Clear();
ragonSerializer.WriteOperation(RagonOperation.JOIN_OR_CREATE_ROOM);
ragonSerializer.WriteInt(2);
ragonSerializer.WriteInt(20);
ragonSerializer.WriteString("map");
ragonSerializer.WriteInt(1);
ragonSerializer.WriteInt(5);
var sendData = ragonSerializer.ToArray();
var packet = new Packet();
@@ -139,7 +139,7 @@ namespace Stress
break;
}
}
Console.WriteLine(op);
// Console.WriteLine(op);
// Console.WriteLine("Packet received from server - Channel ID: " + netEvent.ChannelID + ", Data length: " + netEvent.Packet.Length);
netEvent.Packet.Dispose();
break;
@@ -165,7 +165,7 @@ namespace Stress
}
}
Thread.Sleep(16);
Thread.Sleep(33);
}
}
@@ -192,65 +192,13 @@ namespace Stress
{
Library.Initialize();
for (var i = 0; i < 80; i ++)
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
thread.Start("49.12.70.233", 4444, 50);
Thread.Sleep(300);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
{
var thread = new SimulationThread();
thread.Start("127.0.0.1", 4444, 250);
}
Thread.Sleep(3000);
Console.ReadKey();
Library.Deinitialize();
}
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+1 -1
View File
@@ -11,7 +11,7 @@ namespace Ragon.Core
{
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly GameThread _gameThread;
private readonly ENetServer _netServer;
public Application(PluginFactory factory, Configuration configuration)
{
_gameThread = new GameThread(factory, configuration);
@@ -14,7 +14,7 @@ public class AuthorizationManager : IAuthorizationManager
private RagonSerializer _serializer;
private readonly Dictionary<uint, Player> _playersByPeers;
private readonly Dictionary<string, Player> _playersByIds;
public AuthorizationManager(IAuthorizationProvider provider, IGameThread gameThread, Lobby lobby, RagonSerializer serializer)
{
_serializer = serializer;
@@ -27,12 +27,10 @@ public class AuthorizationManager : IAuthorizationManager
public void OnAuthorization(uint peerId, string key, string name, byte protocol)
{
var dispatcher = _gameThread.Dispatcher;
var dispatcher = _gameThread.ThreadDispatcher;
_provider.OnAuthorizationRequest(key, name, protocol, Array.Empty<byte>(),
(playerId, playerName) =>
{
dispatcher.Dispatch(() => Accepted(peerId, playerId, playerName));
},
(playerId, playerName) => { dispatcher.Dispatch(() => Accepted(peerId, playerId, playerName)); },
(errorCode) => { dispatcher.Dispatch(() => Rejected(peerId, errorCode)); });
}
@@ -42,7 +40,7 @@ public class AuthorizationManager : IAuthorizationManager
_serializer.WriteOperation(RagonOperation.AUTHORIZED_SUCCESS);
_serializer.WriteString(playerId);
_serializer.WriteString(playerName);
var player = new Player()
{
Id = playerId,
@@ -52,10 +50,10 @@ public class AuthorizationManager : IAuthorizationManager
Entities = new List<Entity>(),
EntitiesIds = new List<int>(),
};
_playersByIds.Add(playerId, player);
_playersByPeers.Add(peerId, player);
var sendData = _serializer.ToArray();
_gameThread.Server.Send(peerId, sendData, DeliveryType.Reliable);
}
@@ -77,11 +75,14 @@ public class AuthorizationManager : IAuthorizationManager
_playersByIds.Remove(player.Id);
}
public Player GetPlayer(uint peerId)
public Player? GetPlayer(uint peerId)
{
return _playersByPeers[peerId];
if (_playersByPeers.TryGetValue(peerId, out var player))
return player;
return null;
}
public Player GetPlayer(string playerId)
{
return _playersByIds[playerId];
+8 -8
View File
@@ -2,17 +2,17 @@
namespace Ragon.Core
{
[Serializable]
public struct Server
{
public ushort Port;
}
[Serializable]
public struct Configuration
{
public string Key;
public ushort TickRate;
public Server Server;
public int StatisticsInterval;
public ushort SendRate;
public ushort Port;
public int SkipTimeout;
public int ReconnectTimeout;
public int MaxConnections;
public int MaxPlayersPerRoom;
public int MaxRooms;
}
}
@@ -9,7 +9,7 @@ namespace Ragon.Core
public static class ConfigurationLoader
{
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private static readonly string _serverVersion = "1.0.5-rc";
private static readonly string _serverVersion = "1.0.9-rc";
private static void CopyrightInfo()
{
@@ -1,6 +0,0 @@
namespace Ragon.Core;
public interface Receiver<T>
{
public bool Receive(out T data);
}
@@ -1,6 +0,0 @@
namespace Ragon.Core;
public interface ISender<T>
{
public void Send(T data);
}
-15
View File
@@ -1,15 +0,0 @@
using System;
using System.Diagnostics;
namespace Ragon.Core;
public class DispatcherTask
{
public Action Action;
public Action Callback;
public void Execute()
{
Action?.Invoke();
}
}
@@ -1,130 +0,0 @@
using System.Collections.Generic;
using System.Threading;
namespace Ragon.Core.Core.Utils;
public class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public int Count
{ get { return innerCache.Count; } }
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReadLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
~SynchronizedCache()
{
if (cacheLock != null) cacheLock.Dispose();
}
}
+3 -1
View File
@@ -6,15 +6,17 @@ public class Entity
{
private static int _idGenerator = 0;
public int EntityId { get; private set; }
public int StaticId { get; private set; }
public uint OwnerId { get; private set; }
public ushort EntityType { get; private set; }
public RagonAuthority Authority { get; private set; }
public EntityState State { get; private set; }
public EntityState Payload { get; private set; }
public Entity(uint ownerId, ushort entityType, RagonAuthority stateAuthority, RagonAuthority eventAuthority)
public Entity(uint ownerId, ushort entityType, int staticId, RagonAuthority stateAuthority, RagonAuthority eventAuthority)
{
OwnerId = ownerId;
StaticId = staticId;
EntityType = entityType;
EntityId = _idGenerator++;
State = new EntityState(stateAuthority);
-12
View File
@@ -1,12 +0,0 @@
using System;
namespace Ragon.Core
{
public struct SocketEvent
{
public EventType Type;
public DeliveryType Delivery;
public byte[] Data;
public uint PeerId;
}
}
-10
View File
@@ -1,10 +0,0 @@
namespace Ragon.Core
{
public enum EventType
{
CONNECTED,
DISCONNECTED,
TIMEOUT,
DATA,
}
}
+108 -40
View File
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
using Ragon.Common;
@@ -19,28 +18,28 @@ namespace Ragon.Core
private Dictionary<uint, Player> _players = new();
private Dictionary<int, Entity> _entities = new();
private uint _owner;
private uint _ticks;
private readonly PluginBase _plugin;
private readonly IScheduler _scheduler;
private readonly IGameThread _gameThread;
private readonly PluginBase _plugin;
private readonly RagonSerializer _serializer = new(512);
// Cache
private uint[] _readyPlayers = Array.Empty<uint>();
private uint[] _allPlayers = Array.Empty<uint>();
private Entity[] _entitiesAll = Array.Empty<Entity>();
public GameRoom(IGameThread gameThread, PluginBase pluginBase, string map, int min, int max)
{
_gameThread = gameThread;
_plugin = pluginBase;
_scheduler = new Scheduler();
Map = map;
PlayersMin = min;
PlayersMax = max;
Id = Guid.NewGuid().ToString();
_logger.Info($"Room created with plugin: {_plugin.GetType().Name}");
_plugin.Attach(this);
}
@@ -50,7 +49,7 @@ namespace Ragon.Core
{
_owner = player.PeerId;
}
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.PLAYER_JOINED);
@@ -61,10 +60,10 @@ namespace Ragon.Core
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
}
_players.Add(player.PeerId, player);
_allPlayers = _players.Select(p => p.Key).ToArray();
{
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.JOIN_SUCCESS);
@@ -94,7 +93,7 @@ namespace Ragon.Core
{
_allPlayers = _players.Select(p => p.Key).ToArray();
_readyPlayers = _players.Where(p => p.Value.IsLoaded).Select(p => p.Key).ToArray();
var isOwnershipChange = player.PeerId == _owner;
{
@@ -120,6 +119,8 @@ namespace Ragon.Core
var newRoomOwnerId = _allPlayers[0];
var newRoomOwner = _players[newRoomOwnerId];
_owner = newRoomOwnerId;
{
_plugin.OnOwnershipChanged(newRoomOwner);
@@ -140,10 +141,10 @@ namespace Ragon.Core
{
var operation = (RagonOperation) rawData[0];
var payloadRawData = rawData.Slice(1, rawData.Length - 1);
_serializer.Clear();
_serializer.FromSpan(ref payloadRawData);
switch (operation)
{
case RagonOperation.REPLICATE_ENTITY_STATE:
@@ -157,11 +158,13 @@ namespace Ragon.Core
var entityStateData = _serializer.ReadData(_serializer.Size);
ent.State.Write(ref entityStateData);
}
break;
}
case RagonOperation.REPLICATE_ENTITY_EVENT:
{
var evntId = _serializer.ReadUShort();
var evntMode = _serializer.ReadByte();
var entityId = _serializer.ReadInt();
if (!_entities.TryGetValue(entityId, out var ent))
@@ -173,7 +176,7 @@ namespace Ragon.Core
Span<byte> payloadRaw = stackalloc byte[_serializer.Size];
var payloadData = _serializer.ReadData(_serializer.Size);
payloadData.CopyTo(payloadRaw);
ReadOnlySpan<byte> payload = payloadRaw;
if (_plugin.InternalHandle(peerId, entityId, evntId, ref payload))
return;
@@ -181,30 +184,76 @@ namespace Ragon.Core
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_ENTITY_EVENT);
_serializer.WriteUShort(evntId);
_serializer.WriteUShort((ushort) peerId);
_serializer.WriteByte(evntMode);
_serializer.WriteInt(entityId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.REPLICATE_EVENT:
{
var evntId = _serializer.ReadUShort();
var evntMode = _serializer.ReadByte();
Span<byte> payloadRaw = stackalloc byte[_serializer.Size];
var payloadData = _serializer.ReadData(_serializer.Size);
payloadData.CopyTo(payloadRaw);
ReadOnlySpan<byte> payload = payloadRaw;
if (_plugin.InternalHandle(peerId, evntId, ref payload))
return;
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.REPLICATE_EVENT);
_serializer.WriteUShort((ushort) peerId);
_serializer.WriteByte(evntMode);
_serializer.WriteUShort(evntId);
_serializer.WriteData(ref payload);
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
}
case RagonOperation.CREATE_STATIC_ENTITY:
{
var entityType = _serializer.ReadUShort();
var staticId = _serializer.ReadUShort();
var stateAuthority = (RagonAuthority) _serializer.ReadByte();
var eventAuthority = (RagonAuthority) _serializer.ReadByte();
var entity = new Entity(peerId, entityType, staticId, stateAuthority, eventAuthority);
{
var entityPayload = _serializer.ReadData(_serializer.Size);
entity.Payload.Write(ref entityPayload);
}
var player = _players[peerId];
player.Entities.Add(entity);
player.EntitiesIds.Add(entity.EntityId);
var ownerId = (ushort) peerId;
_entities.Add(entity.EntityId, entity);
_entitiesAll = _entities.Values.ToArray();
_plugin.OnEntityCreated(player, entity);
_serializer.Clear();
_serializer.WriteOperation(RagonOperation.CREATE_STATIC_ENTITY);
_serializer.WriteUShort(entityType);
_serializer.WriteUShort(staticId);
_serializer.WriteByte((byte) stateAuthority);
_serializer.WriteByte((byte) eventAuthority);
_serializer.WriteInt(entity.EntityId);
_serializer.WriteUShort(ownerId);
{
var entityPayload = entity.Payload.Read();
_serializer.WriteData(ref entityPayload);
}
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
@@ -214,8 +263,8 @@ namespace Ragon.Core
var entityType = _serializer.ReadUShort();
var stateAuthority = (RagonAuthority) _serializer.ReadByte();
var eventAuthority = (RagonAuthority) _serializer.ReadByte();
var entity = new Entity(peerId, entityType, stateAuthority, eventAuthority);
var entity = new Entity(peerId, entityType, -1, stateAuthority, eventAuthority);
{
var entityPayload = _serializer.ReadData(_serializer.Size);
entity.Payload.Write(ref entityPayload);
@@ -239,12 +288,12 @@ namespace Ragon.Core
_serializer.WriteByte((byte) eventAuthority);
_serializer.WriteInt(entity.EntityId);
_serializer.WriteUShort(ownerId);
{
var entityPayload = entity.Payload.Read();
_serializer.WriteData(ref entityPayload);
}
var sendData = _serializer.ToArray();
Broadcast(_readyPlayers, sendData, DeliveryType.Reliable);
break;
@@ -291,14 +340,36 @@ namespace Ragon.Core
_serializer.WriteUShort((ushort) playerPeerId);
_serializer.WriteString(_players[playerPeerId].PlayerName);
}
_serializer.WriteInt(_entitiesAll.Length);
foreach (var entity in _entitiesAll)
var dynamicCount = _entitiesAll.Where(e => e.StaticId == -1).ToArray();
_serializer.WriteInt(dynamicCount.Length);
foreach (var entity in dynamicCount)
{
if (entity.StaticId != -1) continue;
var payload = entity.Payload.Read();
var state = entity.State.Read();
_serializer.WriteInt(entity.EntityId);
_serializer.WriteByte((byte) entity.State.Authority);
_serializer.WriteByte((byte) entity.Authority);
_serializer.WriteUShort(entity.EntityType);
_serializer.WriteUShort((ushort) entity.OwnerId);
_serializer.WriteUShort((ushort) payload.Length);
_serializer.WriteData(ref payload);
_serializer.WriteUShort((ushort) state.Length);
_serializer.WriteData(ref state);
}
var staticCount = _entitiesAll.Where(e => e.StaticId != -1).ToArray();
_serializer.WriteInt(staticCount.Length);
foreach (var entity in staticCount)
{
var payload = entity.Payload.Read();
var state = entity.State.Read();
_serializer.WriteInt(entity.EntityId);
_serializer.WriteUShort((ushort) entity.StaticId);
_serializer.WriteByte((byte) entity.State.Authority);
_serializer.WriteByte((byte) entity.Authority);
_serializer.WriteUShort(entity.EntityType);
@@ -320,11 +391,10 @@ namespace Ragon.Core
}
}
}
public void Tick(float deltaTime)
{
_ticks++;
_plugin.OnTick(_ticks, deltaTime);
_scheduler.Tick(deltaTime);
foreach (var entity in _entitiesAll)
{
@@ -347,24 +417,28 @@ namespace Ragon.Core
public void Start()
{
_logger.Info("Room started");
_plugin.OnStart();
}
public void Stop()
{
_logger.Info("Room stopped");
foreach (var peerId in _allPlayers)
_gameThread.Server.Disconnect(peerId, 0);
_plugin.OnStop();
_plugin.Detach();
}
public Player GetPlayerById(uint peerId) => _players[peerId];
public Entity GetEntityById(int entityId) => _entities[entityId];
public Player GetOwner() => _players[_owner];
public IDispatcher GetThreadDispatcher() => _gameThread.ThreadDispatcher;
public IScheduler GetScheduler() => _scheduler;
public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
_gameThread.Server.Send(peerId, rawData, deliveryType);
@@ -372,18 +446,12 @@ namespace Ragon.Core
public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
foreach (var peer in peersIds)
{
_gameThread.Server.Send(peer, rawData, deliveryType);
}
_gameThread.Server.Broadcast(peersIds, rawData, deliveryType);
}
public void Broadcast(byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable)
{
foreach (var peer in _allPlayers)
{
_gameThread.Server.Send(peer, rawData, deliveryType);
}
_gameThread.Server.Broadcast(_allPlayers, rawData, deliveryType);
}
}
}
+32 -42
View File
@@ -9,37 +9,37 @@ namespace Ragon.Core
{
public class GameThread : IGameThread, IHandler
{
private readonly Dictionary<uint, GameRoom> _socketByRooms;
private readonly RoomManager _roomManager;
private readonly ISocketServer _server;
private readonly Thread _thread;
private readonly Server _serverConfiguration;
private readonly Stopwatch _gameLoopTimer;
private readonly Lobby _lobby;
private readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private readonly float _deltaTime = 0.0f;
private readonly Stopwatch _packetsTimer;
private int _packets = 0;
public IDispatcher Dispatcher { get; private set; }
private readonly Stopwatch _statisticsTimer;
private readonly Configuration _configuration;
private readonly IDispatcherInternal _dispatcherInternal;
public IDispatcher ThreadDispatcher { get; private set; }
public ISocketServer Server { get; private set; }
public GameThread(PluginFactory factory, Configuration configuration)
{
_configuration = configuration;
var authorizationProvider = factory.CreateAuthorizationProvider(configuration);
var dispatcher = new Dispatcher();
_dispatcherInternal = dispatcher;
Dispatcher = new Dispatcher();
ThreadDispatcher = dispatcher;
Server = new ENetServer(this);
_serverConfiguration = configuration.Server;
_deltaTime = 1000.0f / configuration.TickRate;
_deltaTime = 1000.0f / configuration.SendRate;
_roomManager = new RoomManager(factory, this);
_lobby = new Lobby(authorizationProvider, _roomManager, this);
_gameLoopTimer = new Stopwatch();
_packetsTimer = new Stopwatch();
_socketByRooms = new Dictionary<uint, GameRoom>();
_statisticsTimer = new Stopwatch();
_thread = new Thread(Execute);
_thread.Name = "Game Thread";
@@ -48,19 +48,19 @@ namespace Ragon.Core
public void Start()
{
Server.Start(_serverConfiguration.Port);
Server.Start(_configuration.Port, _configuration.MaxConnections);
_gameLoopTimer.Start();
_packetsTimer.Start();
_statisticsTimer.Start();
_thread.Start();
}
public void Stop()
{
Server.Stop();
_gameLoopTimer.Stop();
_packetsTimer.Stop();
_statisticsTimer.Stop();
_thread.Interrupt();
}
@@ -69,8 +69,9 @@ namespace Ragon.Core
while (true)
{
Server.Process();
Dispatcher.Process();
_dispatcherInternal.Process();
var elapsedMilliseconds = _gameLoopTimer.ElapsedMilliseconds;
if (elapsedMilliseconds > _deltaTime)
{
@@ -79,47 +80,36 @@ namespace Ragon.Core
continue;
}
if (_packetsTimer.Elapsed.Seconds > 1)
if (_statisticsTimer.Elapsed.Seconds > _configuration.StatisticsInterval && _roomManager.RoomsBySocket.Count > 0)
{
_logger.Trace($"Clients: {_socketByRooms.Keys.Count} Packets: {_packets} per sec");
_packetsTimer.Restart();
_packets = 0;
_logger.Trace($"Rooms: {_roomManager.Rooms.Count} Clients: {_roomManager.RoomsBySocket.Count}");
_statisticsTimer.Restart();
}
Thread.Sleep(15);
}
}
public void Attach(uint peerId, GameRoom room)
{
_socketByRooms.Add(peerId, room);
}
public void Detach(uint peerId)
{
_socketByRooms.Remove(peerId);
}
public void OnEvent(Event evnt)
{
if (evnt.Type == ENet.EventType.Timeout || evnt.Type == ENet.EventType.Disconnect)
if (evnt.Type == EventType.Timeout || evnt.Type == EventType.Disconnect)
{
if (_socketByRooms.Remove(evnt.Peer.ID, out var room))
room.Leave(evnt.Peer.ID);
var player = _lobby.AuthorizationManager.GetPlayer(evnt.Peer.ID);
if (player != null)
_roomManager.Left(player, Array.Empty<byte>());
_lobby.OnDisconnected(evnt.Peer.ID);
}
if (evnt.Type == ENet.EventType.Receive)
if (evnt.Type == EventType.Receive)
{
_packets += 1;
try
{
var peerId = evnt.Peer.ID;
var dataRaw = new byte[evnt.Packet.Length];
evnt.Packet.CopyTo(dataRaw);
var data = new ReadOnlySpan<byte>(dataRaw);
if (_socketByRooms.TryGetValue(peerId, out var room))
if (_roomManager.RoomsBySocket.TryGetValue(peerId, out var room))
{
room.ProcessEvent(peerId, data);
}
+2
View File
@@ -11,6 +11,8 @@ public interface IGameRoom
public Player GetPlayerById(uint peerId);
public Entity GetEntityById(int entityId);
public Player GetOwner();
public IDispatcher GetThreadDispatcher();
public IScheduler GetScheduler();
public void Send(uint peerId, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable);
public void Broadcast(uint[] peersIds, byte[] rawData, DeliveryType deliveryType = DeliveryType.Unreliable);
+1 -3
View File
@@ -4,8 +4,6 @@ namespace Ragon.Core;
public interface IGameThread
{
public void Attach(uint peerId, GameRoom room);
public void Detach(uint peerId);
public IDispatcher Dispatcher { get; }
public IDispatcher ThreadDispatcher { get; }
public ISocketServer Server { get; }
}
+6
View File
@@ -0,0 +1,6 @@
namespace Ragon.Core;
public interface ILobby
{
}
+13 -7
View File
@@ -1,13 +1,16 @@
using System;
using System.Collections.Generic;
using Ragon.Common;
namespace Ragon.Core;
public class Lobby
public class Lobby : ILobby
{
private readonly RagonSerializer _serializer;
private readonly AuthorizationManager _authorizationManager;
private readonly RoomManager _roomManager;
private readonly AuthorizationManager _authorizationManager;
public AuthorizationManager AuthorizationManager => _authorizationManager;
public Lobby(IAuthorizationProvider provider, RoomManager manager, IGameThread gameThread)
{
@@ -38,22 +41,25 @@ public class Lobby
{
var roomId = _serializer.ReadString();
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.Join(player, roomId, Array.Empty<byte>());
if (player != null)
_roomManager.Join(player, roomId, Array.Empty<byte>());
break;
}
case RagonOperation.JOIN_OR_CREATE_ROOM:
{
var min = _serializer.ReadUShort();
var max = _serializer.ReadUShort();
var map = _serializer.ReadString();
var min = _serializer.ReadInt();
var max = _serializer.ReadInt();
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.JoinOrCreate(player, map, min, max, Array.Empty<byte>());
if (player != null)
_roomManager.JoinOrCreate(player, map, min, max, Array.Empty<byte>());
break;
}
case RagonOperation.LEAVE_ROOM:
{
var player = _authorizationManager.GetPlayer(peerId);
_roomManager.Left(player, Array.Empty<byte>());
if (player != null)
_roomManager.Left(player, Array.Empty<byte>());
break;
}
}
+11 -15
View File
@@ -18,12 +18,12 @@ namespace Ragon.Core
private readonly BitBuffer _buffer = new();
private readonly RagonSerializer _serializer = new();
protected IGameRoom GameRoom { get; private set; }
protected ILogger _logger;
protected IGameRoom GameRoom { get; private set; } = null!;
protected ILogger Logger = null!;
public void Attach(GameRoom gameRoom)
{
_logger = LogManager.GetLogger($"Plugin<{GetType().Name}>");
Logger = LogManager.GetLogger($"Plugin<{GetType().Name}>");
GameRoom = gameRoom;
@@ -41,7 +41,7 @@ namespace Ragon.Core
{
if (_globalEvents.ContainsKey(evntCode))
{
_logger.Warn($"Event subscriber already added {evntCode}");
Logger.Warn($"Event subscriber already added {evntCode}");
return;
}
@@ -50,7 +50,7 @@ namespace Ragon.Core
{
if (raw.Length == 0)
{
_logger.Warn($"Payload is empty for event {evntCode}");
Logger.Warn($"Payload is empty for event {evntCode}");
return;
}
@@ -65,7 +65,7 @@ namespace Ragon.Core
{
if (_globalEvents.ContainsKey(evntCode))
{
_logger.Warn($"Event subscriber already added {evntCode}");
Logger.Warn($"Event subscriber already added {evntCode}");
return;
}
@@ -78,7 +78,7 @@ namespace Ragon.Core
{
if (_entityEvents[entity.EntityId].ContainsKey(evntCode))
{
_logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
Logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
return;
}
@@ -87,7 +87,7 @@ namespace Ragon.Core
{
if (raw.Length == 0)
{
_logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
Logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
return;
}
@@ -107,7 +107,7 @@ namespace Ragon.Core
{
if (raw.Length == 0)
{
_logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
Logger.Warn($"Payload is empty for entity {ent.EntityId} event {evntCode}");
return;
}
@@ -125,7 +125,7 @@ namespace Ragon.Core
{
if (_entityEvents[entity.EntityId].ContainsKey(evntCode))
{
_logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
Logger.Warn($"Event subscriber already added {evntCode} for {entity.EntityId}");
return;
}
@@ -264,11 +264,7 @@ namespace Ragon.Core
public virtual void OnStop()
{
}
public virtual void OnTick(ulong ticks, float deltaTime)
{
}
#endregion
}
}
+21 -10
View File
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Text;
@@ -12,14 +11,19 @@ public class RoomManager
private readonly IGameThread _gameThread;
private readonly PluginFactory _factory;
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
private List<GameRoom> _rooms = new List<GameRoom>();
private readonly List<GameRoom> _rooms = new List<GameRoom>();
private readonly Dictionary<uint, GameRoom> _roomsBySocket;
public IReadOnlyDictionary<uint, GameRoom> RoomsBySocket => _roomsBySocket;
public IReadOnlyList<GameRoom> Rooms => _rooms;
public RoomManager(PluginFactory factory, IGameThread gameThread)
{
_gameThread = gameThread;
_factory = factory;
}
_roomsBySocket = new Dictionary<uint, GameRoom>();
}
public void Join(Player player, string roomId, byte[] payload)
{
if (_rooms.Count > 0)
@@ -29,7 +33,7 @@ public class RoomManager
if (existRoom.Id == roomId && existRoom.PlayersCount < existRoom.PlayersMax)
{
existRoom.Joined(player, payload);
_gameThread.Attach(player.PeerId, existRoom);
_roomsBySocket.Add(player.PeerId, existRoom);
break;
}
}
@@ -45,8 +49,7 @@ public class RoomManager
if (existRoom.Map == map && existRoom.PlayersCount < existRoom.PlayersMax)
{
existRoom.Joined(player, payload);
_gameThread.Attach(player.PeerId, existRoom);
_roomsBySocket.Add(player.PeerId, existRoom);
return;
}
}
@@ -56,17 +59,25 @@ public class RoomManager
if (plugin == null)
throw new NullReferenceException($"Plugin for map {map} is null");
var room = new GameRoom(_gameThread, plugin, map, min, max);
var room = new GameRoom(_gameThread, plugin, map, min, max);
room.Joined(player, payload);
room.Start();
_gameThread.Attach(player.PeerId, room);
_roomsBySocket.Add(player.PeerId, room);
_rooms.Add(room);
}
public void Left(Player player, byte[] payload)
{
if (_roomsBySocket.Remove(player.PeerId, out var room))
{
room.Leave(player.PeerId);
if (room.PlayersCount < room.PlayersMin)
{
room.Stop();
_rooms.Remove(room);
}
}
}
public void Tick(float deltaTime)
@@ -1,7 +1,4 @@
using System;
using System.Diagnostics;
using System.Threading;
using DisruptorUnity3d;
using ENet;
using NLog;
@@ -33,19 +30,41 @@ namespace Ragon.Core
_handler = handler;
}
public void Start(ushort port)
public void Start(ushort port, int connections)
{
_address = default;
_address.Port = port;
_peers = new Peer[2048];
_peers = new Peer[connections];
_host = new Host();
_host.Create(_address, 2048, 2, 0, 0, 1024 * 1024);
_host.Create(_address, connections, 2, 0, 0, 1024 * 1024);
Status = Status.Listening;
_logger.Info($"Network listening on {port}");
}
public void Broadcast(uint[] peersIds, byte[] data, DeliveryType type)
{
var newPacket = new Packet();
var packetFlags = PacketFlags.Instant;
byte channel = 1;
if (type == DeliveryType.Reliable)
{
packetFlags = PacketFlags.Reliable;
channel = 0;
}
else if (type == DeliveryType.Unreliable)
{
channel = 1;
packetFlags = PacketFlags.None;
}
newPacket.Create(data, data.Length, packetFlags);
foreach (var peerId in peersIds)
_peers[peerId].Send(channel, ref newPacket);
}
public void Send(uint peerId, byte[] data, DeliveryType type)
{
var newPacket = new Packet();
@@ -79,7 +98,7 @@ namespace Ragon.Core
{
if (_host.CheckEvents(out _netEvent) <= 0)
{
if (_host.Service(0, out _netEvent) <= 0)
if (_host.Service(15, out _netEvent) <= 0)
break;
polled = true;
@@ -87,27 +106,27 @@ namespace Ragon.Core
switch (_netEvent.Type)
{
case ENet.EventType.None:
case EventType.None:
Console.WriteLine("None event");
break;
case ENet.EventType.Connect:
case EventType.Connect:
{
_peers[_netEvent.Peer.ID] = _netEvent.Peer;
_handler.OnEvent(_netEvent);
break;
}
case ENet.EventType.Disconnect:
case EventType.Disconnect:
{
_handler.OnEvent(_netEvent);
break;
}
case ENet.EventType.Timeout:
case EventType.Timeout:
{
_handler.OnEvent(_netEvent);
break;
}
case ENet.EventType.Receive:
case EventType.Receive:
{
_handler.OnEvent(_netEvent);
_netEvent.Packet.Dispose();
+2 -1
View File
@@ -2,9 +2,10 @@ namespace Ragon.Core;
public interface ISocketServer
{
public void Start(ushort port);
public void Start(ushort port, int connections);
public void Process();
public void Stop();
public void Send(uint peerId, byte[] data, DeliveryType type);
public void Broadcast(uint[] peersIds, byte[] data, DeliveryType type);
public void Disconnect(uint peerId, uint errorCode);
}
@@ -3,19 +3,20 @@ using System.Collections.Generic;
namespace Ragon.Core;
public class Dispatcher: IDispatcher
public class Dispatcher: IDispatcher, IDispatcherInternal
{
public Queue<DispatcherTask> _actions = new Queue<DispatcherTask>();
public Queue<Action> _actions = new Queue<Action>();
public void Dispatch(Action action)
{
lock (_actions)
_actions.Enqueue(new DispatcherTask() { Action = action });
_actions.Enqueue(action);
}
public void Process()
{
lock(_actions)
while(_actions.TryDequeue(out var action))
action.Execute();
action?.Invoke();
}
}
@@ -5,5 +5,4 @@ namespace Ragon.Core;
public interface IDispatcher
{
public void Dispatch(Action action);
public void Process();
}
@@ -0,0 +1,6 @@
namespace Ragon.Core;
public interface IDispatcherInternal
{
public void Process();
}
+12
View File
@@ -0,0 +1,12 @@
using System;
namespace Ragon.Core;
public interface IScheduler
{
public SchedulerTask Schedule(Action<SchedulerTask> action, float interval, int count = 1);
public SchedulerTask ScheduleForever(Action<SchedulerTask> action, float interval);
public void StopSchedule(SchedulerTask schedulerTask);
public void Tick(float deltaTime);
}
+98
View File
@@ -0,0 +1,98 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
namespace Ragon.Core
{
public class Scheduler: IScheduler
{
List<SchedulerTask> _scheduledTasks;
public Scheduler(int defaultCapacity = 100)
{
_scheduledTasks = new List<SchedulerTask>(defaultCapacity);
}
public SchedulerTask Schedule(Action<SchedulerTask> action, float interval, int count = 1)
{
var newTask = new SchedulerTask(action, interval, count - 1);
_scheduledTasks.Add(newTask);
return newTask;
}
public SchedulerTask ScheduleForever(Action<SchedulerTask> action, float interval)
{
var newTask = new SchedulerTask(action, interval, -1);
_scheduledTasks.Add(newTask);
return newTask;
}
public void StopSchedule(SchedulerTask schedulerTask)
{
if (_scheduledTasks.Contains(schedulerTask))
_scheduledTasks.Remove(schedulerTask);
}
public void Tick(float deltaTime)
{
for (int i = _scheduledTasks.Count - 1; i >= 0; i--)
{
var scheduledTask = _scheduledTasks[i];
scheduledTask.Tick(deltaTime);
if (!scheduledTask.IsActive)
_scheduledTasks.Remove(scheduledTask);
}
}
public void Dispose()
{
_scheduledTasks.Clear();
}
}
public class SchedulerTask
{
private Action<SchedulerTask> _action;
private float _timer = 0;
private float _interval = 0;
private int _repeats = 0;
private bool _active;
public int Repeats => _repeats;
public bool IsActive => _active;
public SchedulerTask(Action<SchedulerTask> task, float interval, int repeatCount = 0)
{
_action = task;
_interval = interval;
_timer = 0;
_active = true;
_repeats = repeatCount;
}
public void Tick(float deltaTime)
{
_timer += deltaTime;
if (_timer >= _interval)
{
_action.Invoke(this);
if (_repeats == -1)
{
_timer = 0;
return;
}
if (_repeats > 0)
{
_timer = 0;
_repeats--;
return;
}
if (_repeats == 0)
_active = false;
}
}
}
}
+9 -13
View File
@@ -4,34 +4,31 @@
## Ragon Server
Ragon is fully free high perfomance room based game server with plugin based architecture.
Ragon is fully free, small and high perfomance room based game server with plugin based architecture.
<a href="">Documentation</a>
<a href="https://ragon-server.com/docs/category/basics">Documentation</a>
<br>
<a href="">Get started</a>
<a href="https://ragon-server.com/docs/get-started">Get started</a>
### Features:
- Effective
- Free
- Simple matchmaking
- Flexiable API
- Room based architecture
- Extendable room logic via plugin
- Custom authorization
- No CCU limitations*
- Multi-threaded
- No CCU limitations*
- Engine agnostic
- Support any client architecture (MonoBehaviors, ECS)
- UDP
- RUDP
### Roadmap:
- Allow customize matchmaking
- Refactoring some moments(a lot duplications of code, etc...)
- Use native memory
- Reduce allocations
- Dashboard for monitoring entities and players in realtime
- Statistics for monitoring state of server, cpu, memory
- Horizontal Scaling
- Docker support
- Add additional API to plugin system
@@ -40,9 +37,8 @@ Ragon is fully free high perfomance room based game server with plugin based arc
- .NET 6.0
### Dependencies
* ENet-Sharp v2.4.8
* NetStack latest
* RingBuffer-Unity3D latest
* ENet-Sharp [v2.4.8]
* NetStack [latest]
### License
SSPL-1.0