Skip to content

(Draft) Implement Networking using GameNetworkingSockets and Quake 3-like snapshot system #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions Samples/mocha-minimal/code/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public class Game : BaseGame
{
[HotloadSkip] private UIManager Hud { get; set; }

public string NetworkedString { get; set; }
[Sync] public string NetworkedString { get; set; }

public override void OnStartup()
{
Expand All @@ -32,9 +32,15 @@ public override void OnStartup()
}
}

[Event.Tick]
public void Tick()
[Event.Tick, ServerOnly]
public void ServerTick()
{
DebugOverlay.ScreenText( $"Tick... ({GetType().Assembly.GetHashCode()})" );
DebugOverlay.ScreenText( $"Server Tick... ({GetType().Assembly.GetHashCode()})" );
}

[Event.Tick, ClientOnly]
public void ClientTick()
{
DebugOverlay.ScreenText( $"Client Tick... ({GetType().Assembly.GetHashCode()})" );
}
}
18 changes: 18 additions & 0 deletions Source/Mocha.Common/Attributes/Networking/ClientOnlyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Mocha.Common;

[AttributeUsage( AttributeTargets.Class |
AttributeTargets.Interface |
AttributeTargets.Struct |
AttributeTargets.Enum |
AttributeTargets.Field |
AttributeTargets.Property |
AttributeTargets.Constructor |
AttributeTargets.Method |
AttributeTargets.Delegate |
AttributeTargets.Event, Inherited = true, AllowMultiple = false )]
public sealed class ClientOnlyAttribute : Attribute
{
public ClientOnlyAttribute()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Mocha;

[System.AttributeUsage( AttributeTargets.Class, Inherited = false, AllowMultiple = false )]
internal sealed class HandlesNetworkedTypeAttribute<T> : Attribute
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[System.AttributeUsage( AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false )]
public sealed class SyncAttribute : Attribute { }
18 changes: 18 additions & 0 deletions Source/Mocha.Common/Attributes/Networking/ServerOnlyAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
namespace Mocha.Common;

[AttributeUsage( AttributeTargets.Class |
AttributeTargets.Interface |
AttributeTargets.Struct |
AttributeTargets.Enum |
AttributeTargets.Field |
AttributeTargets.Property |
AttributeTargets.Constructor |
AttributeTargets.Method |
AttributeTargets.Delegate |
AttributeTargets.Event, Inherited = true, AllowMultiple = false )]
public sealed class ServerOnlyAttribute : Attribute
{
public ServerOnlyAttribute()
{
}
}
1 change: 1 addition & 0 deletions Source/Mocha.Common/Entities/IEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public interface IEntity
{
string Name { get; set; }
uint NativeHandle { get; }
NetworkId NetworkId { get; set; }

Vector3 Position { get; set; }
Rotation Rotation { get; set; }
Expand Down
11 changes: 11 additions & 0 deletions Source/Mocha.Common/Networking/IClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Mocha.Common;

/// <summary>
/// Represents a client connected to a server.
/// </summary>
public interface IClient
{
public abstract string Name { get; set; }
public abstract int Ping { get; set; }
public abstract IEntity Pawn { get; set; }
}
97 changes: 97 additions & 0 deletions Source/Mocha.Common/Types/NetworkId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
namespace Mocha.Common;

/// <summary>
/// A NetworkId is a wrapper around a 64-bit unsigned integer value used to identify an entity.<br />
/// The first bit of the value is used in order to determine whether the value is networked or local.<br />
/// The binary representation of the value is used to distinguish between local and networked entities.<br />
/// Note that the same ID (e.g., 12345678) can be used twice - once locally, and once networked - to<br />
/// refer to two distinct entities. The IDs used within this class should not reflect the native engine<br />
/// handle for the entity - NetworkIds are unique to a managed context.
/// </summary>
/// <remarks>
/// For example, take an entity with the ID 12345678:<br />
/// <br />
/// <b>Local</b><br />
/// <code>00000000000000000000000000000000000000000000000000000000010111101</code><br />
/// <br />
/// <b>Networked</b><br />
/// <code>10000000000000000000000000000000000000000000000000000000010111101</code><br />
/// <br />
/// Note that the first bit is set to 1 in the binary representation of the networked entity.
/// </remarks>
public class NetworkId : IEquatable<NetworkId>
{
internal ulong Value { get; private set; }

internal NetworkId( ulong value )
{
Value = value;
}

public NetworkId() { }

public bool IsNetworked()
{
// If first bit of the value is set, it's a networked entity
return (Value & 0x8000000000000000) != 0;
}
public bool IsLocal()
{
// If first bit of the value is not set, it's a local entity
return (Value & 0x8000000000000000) == 0;
}

public ulong GetValue()
{
// Returns the value without the first bit
return Value & 0x7FFFFFFFFFFFFFFF;
}

public static NetworkId CreateLocal()
{
// Create a local entity by setting the first bit to 0
// Use EntityRegistry.Instance to get the next available local id
return new( (uint)EntityRegistry.Instance.Count() << 1 );
}

public static NetworkId CreateNetworked()
{
// Create a networked entity by setting the first bit to 1
// Use EntityRegistry.Instance to get the next available local id
return new( (uint)EntityRegistry.Instance.Count() | 0x8000000000000000 );
}

public static implicit operator ulong( NetworkId id ) => id.GetValue();
public static implicit operator NetworkId( ulong value ) => new( value );

public override string ToString()
{
return $"{(IsNetworked() ? "Networked" : "Local")}: {GetValue()} ({Value})";
}

public bool Equals( NetworkId? other )
{
if ( other is null )
return false;

return Value == other.Value;
}

public override bool Equals( object? obj )
{
if ( obj is NetworkId id )
return Equals( id );

return false;
}

public static bool operator ==( NetworkId? a, NetworkId? b )
{
return a?.Equals( b ) ?? false;
}

public static bool operator !=( NetworkId? a, NetworkId? b )
{
return !(a?.Equals( b ) ?? false);
}
}
40 changes: 39 additions & 1 deletion Source/Mocha.Engine/BaseGame.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
namespace Mocha;
using Mocha.Networking;

namespace Mocha;

public class BaseGame : IGame
{
public static BaseGame Current { get; set; }

private static Server? s_server;
private static Client? s_client;

public BaseGame()
{
Current = this;
Expand Down Expand Up @@ -55,6 +60,10 @@ public void FrameUpdate()

public void Update()
{
// TODO: This is garbage and should not be here!!!
s_server?.Update();
s_client?.Update();

if ( Core.IsClient )
{
// HACK: Clear DebugOverlay here because doing it
Expand All @@ -75,6 +84,19 @@ public void Shutdown()

public void Startup()
{
if ( Core.IsClient )
{
s_client = new BaseGameClient( "127.0.0.1" );
}
else
{
s_server = new BaseGameServer()
{
OnClientConnectedEvent = ( connection ) => OnClientConnected( connection.GetClient() ),
OnClientDisconnectedEvent = ( connection ) => OnClientDisconnected( connection.GetClient() ),
};
}

OnStartup();
}

Expand All @@ -93,6 +115,22 @@ public virtual void OnStartup()
public virtual void OnShutdown()
{

}

/// <summary>
/// Called on the server whenever a client joins
/// </summary>
public virtual void OnClientConnected( IClient client )
{

}

/// <summary>
/// Called on the server whenever a client leaves
/// </summary>
public virtual void OnClientDisconnected( IClient client )
{

}
#endregion

Expand Down
124 changes: 124 additions & 0 deletions Source/Mocha.Engine/BaseGameClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using Mocha.Networking;
using System.Reflection;

namespace Mocha;
public class BaseGameClient : Client
{
private ServerConnection _connection;

public BaseGameClient( string ipAddress, ushort port = 10570 ) : base( ipAddress, port )
{
_connection = new ServerConnection();
RegisterHandler<KickedMessage>( OnKickedMessage );
RegisterHandler<SnapshotUpdateMessage>( OnSnapshotUpdateMessage );
RegisterHandler<HandshakeMessage>( OnHandshakeMessage );
}

public override void OnMessageReceived( byte[] data )
{
InvokeHandler( _connection, data );
}

public void OnKickedMessage( IConnection connection, KickedMessage kickedMessage )
{
Log.Info( $"BaseGameClient: We were kicked: '{kickedMessage.Reason}'" );
}

private Type? LocateType( string typeName )
{
var type = Type.GetType( typeName )!;

if ( type != null )
return type;

type = Assembly.GetExecutingAssembly().GetType( typeName );

if ( type != null )
return type;

type = Assembly.GetCallingAssembly().GetType( typeName );

if ( type != null )
return type;

foreach ( var assembly in AppDomain.CurrentDomain.GetAssemblies() )
{
type = assembly.GetType( typeName );
if ( type != null )
return type;
}

return null;
}

public void OnSnapshotUpdateMessage( IConnection connection, SnapshotUpdateMessage snapshotUpdateMessage )
{
foreach ( var entityChange in snapshotUpdateMessage.EntityChanges )
{
// Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} changed" );

// Does this entity already exist?
var entity = EntityRegistry.Instance.FirstOrDefault( x => x.NetworkId == entityChange.NetworkId );

if ( entity == null )
{
// Entity doesn't exist locally - let's create it
var type = LocateType( entityChange.TypeName );

if ( type == null )
{
// Log.Error( $"BaseGameClient: Unable to locate type '{entityChange.TypeName}'" );
continue;
}

entity = (Activator.CreateInstance( type ) as IEntity)!;

// Set the network ID
entity.NetworkId = entityChange.NetworkId;

// Log.Info( $"BaseGameClient: Created entity {entity.NetworkId}" );
}

foreach ( var memberChange in entityChange.MemberChanges )
{
if ( memberChange.Data == null )
continue;

var member = entity.GetType().GetMember( memberChange.FieldName ).First()!;
var value = NetworkSerializer.Deserialize( memberChange.Data, member.GetMemberType() );

if ( value == null )
continue;

if ( member.MemberType == MemberTypes.Field )
{
var field = (FieldInfo)member;
field.SetValue( entity, value );

// Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} field {memberChange.FieldName} changed to {value}" );
}
else if ( member.MemberType == MemberTypes.Property )
{
var property = (PropertyInfo)member;
property.SetValue( entity, value );

// Log.Info( $"BaseGameClient: Entity {entityChange.NetworkId} property {memberChange.FieldName} changed to {value}" );
}

//if ( memberChange.FieldName == "PhysicsSetup" )
//{
// // Physics setup changed - let's update the physics setup
// var physicsSetup = (ModelEntity.Physics)value;

// if ( physicsSetup.PhysicsModelPath != null )
// ((ModelEntity)entity).SetMeshPhysics( physicsSetup.PhysicsModelPath );
//}
}
}
}

public void OnHandshakeMessage( IConnection connection, HandshakeMessage handshakeMessage )
{
Log.Info( $"BaseGameClient: Handshake received. Tick rate: {handshakeMessage.TickRate}, nickname: {handshakeMessage.Nickname}" );
}
}
Loading
Loading