diff --git a/.gitignore b/.gitignore
index 63acec8..ec04993 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,3 +83,6 @@ crashlytics-build.properties
/.dotnet
/.dotnet-home
+
+EditMode-err.txt
+
diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index d9a0439..2b72319 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -1595,90 +1595,6 @@ MeshFilter:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 490537900}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
---- !u!1 &520045882
-GameObject:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- serializedVersion: 6
- m_Component:
- - component: {fileID: 520045884}
- - component: {fileID: 520045883}
- m_Layer: 0
- m_Name: GameObject
- m_TagString: Untagged
- m_Icon: {fileID: 0}
- m_NavMeshLayer: 0
- m_StaticEditorFlags: 0
- m_IsActive: 1
---- !u!212 &520045883
-SpriteRenderer:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 520045882}
- m_Enabled: 1
- m_CastShadows: 0
- m_ReceiveShadows: 0
- m_DynamicOccludee: 1
- m_StaticShadowCaster: 0
- m_MotionVectors: 1
- m_LightProbeUsage: 1
- m_ReflectionProbeUsage: 1
- m_RayTracingMode: 0
- m_RayTraceProcedural: 0
- m_RenderingLayerMask: 1
- m_RendererPriority: 0
- m_Materials:
- - {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
- m_StaticBatchInfo:
- firstSubMesh: 0
- subMeshCount: 0
- m_StaticBatchRoot: {fileID: 0}
- m_ProbeAnchor: {fileID: 0}
- m_LightProbeVolumeOverride: {fileID: 0}
- m_ScaleInLightmap: 1
- m_ReceiveGI: 1
- m_PreserveUVs: 0
- m_IgnoreNormalsForChartDetection: 0
- m_ImportantGI: 0
- m_StitchLightmapSeams: 1
- m_SelectedEditorRenderState: 0
- m_MinimumChartSize: 4
- m_AutoUVMaxDistance: 0.5
- m_AutoUVMaxAngle: 89
- m_LightmapParameters: {fileID: 0}
- m_SortingLayerID: 0
- m_SortingLayer: 0
- m_SortingOrder: 0
- m_Sprite: {fileID: 0}
- m_Color: {r: 1, g: 1, b: 1, a: 1}
- m_FlipX: 0
- m_FlipY: 0
- m_DrawMode: 0
- m_Size: {x: 1, y: 1}
- m_AdaptiveModeThreshold: 0.5
- m_SpriteTileMode: 0
- m_WasSpriteAssigned: 0
- m_MaskInteraction: 0
- m_SpriteSortPoint: 0
---- !u!4 &520045884
-Transform:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_GameObject: {fileID: 520045882}
- serializedVersion: 2
- m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: -23.49001, y: 0.5700337, z: 11.206667}
- m_LocalScale: {x: 1, y: 1, z: 1}
- m_ConstrainProportionsScale: 0
- m_Children: []
- m_Father: {fileID: 0}
- m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &632206104
GameObject:
m_ObjectHideFlags: 0
@@ -3733,7 +3649,7 @@ Transform:
m_GameObject: {fileID: 1186082399}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
- m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalPosition: {x: 0, y: 0, z: -30}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
@@ -6194,4 +6110,3 @@ SceneRoots:
- {fileID: 1298430417}
- {fileID: 789249236}
- {fileID: 679132025}
- - {fileID: 520045884}
diff --git a/Assets/Scripts/ClientAuthoritativePlayerState.cs b/Assets/Scripts/ClientAuthoritativePlayerState.cs
index aea0728..6e66994 100644
--- a/Assets/Scripts/ClientAuthoritativePlayerState.cs
+++ b/Assets/Scripts/ClientAuthoritativePlayerState.cs
@@ -118,7 +118,18 @@ public sealed class ClientAuthoritativePlayerStateSnapshot
public int Hp { get; }
- public Quaternion RotationQuaternion => Quaternion.Euler(0f, Rotation, 0f);
+ public Quaternion RotationQuaternion => Quaternion.Euler(0f, NormalizeDegrees(90f - Rotation), 0f);
+
+ private static float NormalizeDegrees(float degrees)
+ {
+ var normalized = degrees % 360f;
+ if (normalized < 0f)
+ {
+ normalized += 360f;
+ }
+
+ return normalized;
+ }
}
public sealed class ClientCombatPresentationSnapshot
diff --git a/Assets/Scripts/MovementComponent.cs b/Assets/Scripts/MovementComponent.cs
index cbec8a1..9d8bdf7 100644
--- a/Assets/Scripts/MovementComponent.cs
+++ b/Assets/Scripts/MovementComponent.cs
@@ -23,8 +23,8 @@ public static class ClientGameplayInputFlow
{
PlayerId = playerId,
Tick = tick,
- MoveX = input.x,
- MoveY = input.z
+ TurnInput = -input.x,
+ ThrottleInput = input.z
};
return true;
}
@@ -104,6 +104,8 @@ public class MovementComponent : MonoBehaviour
{
[SerializeField] private float _sendInterval = 0.05f;
private Player _master;
+ private const float TurnSpeedDegreesPerSecond = 180f;
+ private const float UnityYawOffsetDegrees = 90f;
private int _speed = 2;
[SerializeField] private Rigidbody _rigid;
private float _lastSendTime = 0;
@@ -131,9 +133,10 @@ public class MovementComponent : MonoBehaviour
_isControlled = isControlled;
_speed = speed;
_startTickOffset = serverTick;
- _rigid.interpolation = RigidbodyInterpolation.Interpolate;
+ _rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate;
_rigid.isKinematic = !isControlled;
_rigid.velocity = Vector3.zero;
+ _rigid.angularVelocity = Vector3.zero;
if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
}
@@ -145,7 +148,6 @@ public class MovementComponent : MonoBehaviour
var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput);
if (hasMovement)
{
- _lastAimDirection = _cachedMoveInput;
_stopMessagePending = false;
}
else if (_wasMovingLastFrame)
@@ -215,9 +217,10 @@ public class MovementComponent : MonoBehaviour
}
_serverPosition = snapshot.Position;
- _rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate);
- _rigid.rotation = Quaternion.Slerp(_rigid.rotation, snapshot.RotationQuaternion, _lerpRate);
+ _rigid.position = _serverPosition;
+ _rigid.rotation = snapshot.RotationQuaternion;
_rigid.velocity = snapshot.Velocity;
+ _rigid.angularVelocity = Vector3.zero;
ReplayPendingInputs(replayInputs);
}
@@ -240,19 +243,19 @@ public class MovementComponent : MonoBehaviour
private Vector3 ResolveAimDirection()
{
- if (ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection))
+ var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up);
+ if (ClientGameplayInputFlow.HasPlanarInput(planarForward))
{
- return _lastAimDirection;
+ _lastAimDirection = planarForward;
+ return planarForward;
}
- var forward = _master != null ? _master.transform.forward : transform.forward;
- var planarForward = new Vector3(forward.x, 0f, forward.z);
- return ClientGameplayInputFlow.HasPlanarInput(planarForward) ? planarForward : Vector3.forward;
+ return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection) ? _lastAimDirection : ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y));
}
private void Simulate(Vector3 input)
{
- _rigid.velocity = _speed * input;
+ ApplyTankMovement(-input.x, input.z, Time.fixedDeltaTime);
if (_isControlled)
{
if (MainUI.Instance != null)
@@ -301,7 +304,7 @@ public class MovementComponent : MonoBehaviour
{
foreach (var replayInput in replayInputs)
{
- _rigid.position += _speed * new Vector3(replayInput.MoveX, 0f, replayInput.MoveY) * _sendInterval;
+ ApplyTankMovement(replayInput.TurnInput, replayInput.ThrottleInput, _sendInterval);
}
if (_isControlled)
@@ -312,4 +315,50 @@ public class MovementComponent : MonoBehaviour
}
}
}
+
+ private void ApplyTankMovement(float turnInput, float throttleInput, float deltaTime)
+ {
+ if (deltaTime <= 0f)
+ {
+ _rigid.velocity = Vector3.zero;
+ return;
+ }
+
+ var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f);
+ var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f);
+ var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) + (clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime));
+ _rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f);
+
+ var forward = ResolveHeadingForward(heading);
+ var velocity = forward * (clampedThrottleInput * _speed);
+ _rigid.velocity = velocity;
+ _rigid.position += velocity * deltaTime;
+ }
+
+ private static Vector3 ResolveHeadingForward(float headingDegrees)
+ {
+ var rotationRadians = headingDegrees * Mathf.Deg2Rad;
+ return new Vector3(Mathf.Cos(rotationRadians), 0f, Mathf.Sin(rotationRadians));
+ }
+
+ private static float HeadingToUnityYaw(float headingDegrees)
+ {
+ return NormalizeDegrees(UnityYawOffsetDegrees - headingDegrees);
+ }
+
+ private static float UnityYawToHeading(float unityYawDegrees)
+ {
+ return NormalizeDegrees(UnityYawOffsetDegrees - unityYawDegrees);
+ }
+
+ private static float NormalizeDegrees(float degrees)
+ {
+ var normalized = degrees % 360f;
+ if (normalized < 0f)
+ {
+ normalized += 360f;
+ }
+
+ return normalized;
+ }
}
diff --git a/Assets/Scripts/Network/Defines/Message.cs b/Assets/Scripts/Network/Defines/Message.cs
index 4f6f8e0..517a6cf 100644
--- a/Assets/Scripts/Network/Defines/Message.cs
+++ b/Assets/Scripts/Network/Defines/Message.cs
@@ -60,7 +60,7 @@ namespace Network.Defines {
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LoginResponse), global::Network.Defines.LoginResponse.Parser, new[]{ "PlayerId", "Positions", "Speed", "ServerTick", "Result" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerJoin), global::Network.Defines.PlayerJoin.Parser, new[]{ "PlayerId", "Position" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LogoutRequest), global::Network.Defines.LogoutRequest.Parser, new[]{ "PlayerId" }, null, null, null, null),
- new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "MoveX", "MoveY" }, null, null, null, null),
+ new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "TurnInput", "ThrottleInput" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.ShootInput), global::Network.Defines.ShootInput.Parser, new[]{ "PlayerId", "Tick", "DirX", "DirY", "TargetId" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.CombatEvent), global::Network.Defines.CombatEvent.Parser, new[]{ "Tick", "EventType", "AttackerId", "TargetId", "Damage", "HitPosition" }, null, null, null, null),
new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp" }, null, null, null, null),
@@ -1628,8 +1628,8 @@ namespace Network.Defines {
public MoveInput(MoveInput other) : this() {
playerId_ = other.playerId_;
tick_ = other.tick_;
- moveX_ = other.moveX_;
- moveY_ = other.moveY_;
+ turnInput_ = other.turnInput_;
+ throttleInput_ = other.throttleInput_;
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}
@@ -1663,27 +1663,27 @@ namespace Network.Defines {
}
}
- /// Field number for the "move_x" field.
- public const int MoveXFieldNumber = 3;
- private float moveX_;
+ /// Field number for the "turn_input" field.
+ public const int TurnInputFieldNumber = 3;
+ private float turnInput_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public float MoveX {
- get { return moveX_; }
+ public float TurnInput {
+ get { return turnInput_; }
set {
- moveX_ = value;
+ turnInput_ = value;
}
}
- /// Field number for the "move_y" field.
- public const int MoveYFieldNumber = 4;
- private float moveY_;
+ /// Field number for the "throttle_input" field.
+ public const int ThrottleInputFieldNumber = 4;
+ private float throttleInput_;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
- public float MoveY {
- get { return moveY_; }
+ public float ThrottleInput {
+ get { return throttleInput_; }
set {
- moveY_ = value;
+ throttleInput_ = value;
}
}
@@ -1704,8 +1704,8 @@ namespace Network.Defines {
}
if (PlayerId != other.PlayerId) return false;
if (Tick != other.Tick) return false;
- if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveX, other.MoveX)) return false;
- if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveY, other.MoveY)) return false;
+ if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(TurnInput, other.TurnInput)) return false;
+ if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(ThrottleInput, other.ThrottleInput)) return false;
return Equals(_unknownFields, other._unknownFields);
}
@@ -1715,8 +1715,8 @@ namespace Network.Defines {
int hash = 1;
if (PlayerId.Length != 0) hash ^= PlayerId.GetHashCode();
if (Tick != 0L) hash ^= Tick.GetHashCode();
- if (MoveX != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveX);
- if (MoveY != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveY);
+ if (TurnInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(TurnInput);
+ if (ThrottleInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(ThrottleInput);
if (_unknownFields != null) {
hash ^= _unknownFields.GetHashCode();
}
@@ -1743,13 +1743,13 @@ namespace Network.Defines {
output.WriteRawTag(16);
output.WriteInt64(Tick);
}
- if (MoveX != 0F) {
+ if (TurnInput != 0F) {
output.WriteRawTag(29);
- output.WriteFloat(MoveX);
+ output.WriteFloat(TurnInput);
}
- if (MoveY != 0F) {
+ if (ThrottleInput != 0F) {
output.WriteRawTag(37);
- output.WriteFloat(MoveY);
+ output.WriteFloat(ThrottleInput);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(output);
@@ -1769,13 +1769,13 @@ namespace Network.Defines {
output.WriteRawTag(16);
output.WriteInt64(Tick);
}
- if (MoveX != 0F) {
+ if (TurnInput != 0F) {
output.WriteRawTag(29);
- output.WriteFloat(MoveX);
+ output.WriteFloat(TurnInput);
}
- if (MoveY != 0F) {
+ if (ThrottleInput != 0F) {
output.WriteRawTag(37);
- output.WriteFloat(MoveY);
+ output.WriteFloat(ThrottleInput);
}
if (_unknownFields != null) {
_unknownFields.WriteTo(ref output);
@@ -1793,10 +1793,10 @@ namespace Network.Defines {
if (Tick != 0L) {
size += 1 + pb::CodedOutputStream.ComputeInt64Size(Tick);
}
- if (MoveX != 0F) {
+ if (TurnInput != 0F) {
size += 1 + 4;
}
- if (MoveY != 0F) {
+ if (ThrottleInput != 0F) {
size += 1 + 4;
}
if (_unknownFields != null) {
@@ -1817,11 +1817,11 @@ namespace Network.Defines {
if (other.Tick != 0L) {
Tick = other.Tick;
}
- if (other.MoveX != 0F) {
- MoveX = other.MoveX;
+ if (other.TurnInput != 0F) {
+ TurnInput = other.TurnInput;
}
- if (other.MoveY != 0F) {
- MoveY = other.MoveY;
+ if (other.ThrottleInput != 0F) {
+ ThrottleInput = other.ThrottleInput;
}
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
}
@@ -1851,11 +1851,11 @@ namespace Network.Defines {
break;
}
case 29: {
- MoveX = input.ReadFloat();
+ TurnInput = input.ReadFloat();
break;
}
case 37: {
- MoveY = input.ReadFloat();
+ ThrottleInput = input.ReadFloat();
break;
}
}
@@ -1886,11 +1886,11 @@ namespace Network.Defines {
break;
}
case 29: {
- MoveX = input.ReadFloat();
+ TurnInput = input.ReadFloat();
break;
}
case 37: {
- MoveY = input.ReadFloat();
+ ThrottleInput = input.ReadFloat();
break;
}
}
diff --git a/Assets/Scripts/Network/Defines/MessageType.cs b/Assets/Scripts/Network/Defines/MessageType.cs
index e4bfd66..eac0836 100644
--- a/Assets/Scripts/Network/Defines/MessageType.cs
+++ b/Assets/Scripts/Network/Defines/MessageType.cs
@@ -4,34 +4,18 @@ namespace Network.Defines
{
Unknow = 0,
- // Gameplay
+ // Canonical dedicated-server MVP runtime vocabulary.
MoveInput = 1,
PlayerState = 2,
ShootInput = 3,
CombatEvent = 4,
PlayerJoin = 5,
- PlayerLeave = 6,
- PlayerAction = 7,
- GameState = 8,
- // Chat
- ChatMessage = 10,
- PrivateMessage = 11,
- SystemMessage = 12,
-
- // Session
- HeartBeat = 20,
LoginRequest = 21,
LoginResponse = 22,
LogoutRequest = 23,
- // Room management
- CreateRoom = 30,
- JoinRoom = 31,
- LeaveRoom = 32,
- RoomList = 33,
-
Heartbeat = 40,
HeartbeatResponse = 41,
}
-}
\ No newline at end of file
+}
diff --git a/Assets/Scripts/Network/Defines/message.proto b/Assets/Scripts/Network/Defines/message.proto
index 510a57e..fbf877f 100644
--- a/Assets/Scripts/Network/Defines/message.proto
+++ b/Assets/Scripts/Network/Defines/message.proto
@@ -39,8 +39,8 @@ message LogoutRequest {
message MoveInput {
string player_id = 1;
int64 tick = 2;
- float move_x = 3;
- float move_y = 4;
+ float turn_input = 3;
+ float throttle_input = 4;
}
message ShootInput {
diff --git a/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta b/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta
index edfdef0..407f6de 100644
--- a/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta
+++ b/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta
@@ -1,4 +1,4 @@
-fileFormatVersion: 2
+fileFormatVersion: 2
guid: 3dc7b1ecbad541ea86a9d700dd5148e0
MonoImporter:
externalObjects: {}
diff --git a/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs b/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs
index c45f526..227c207 100644
--- a/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs
+++ b/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
@@ -87,9 +87,10 @@ namespace Network.NetworkApplication
if (sessionManager.State == ConnectionState.Disconnected)
{
sessionManager.NotifyTransportConnected();
+ return;
}
- sessionManager.NotifyInboundActivity();
+ sessionManager.NotifyTransportActivity();
}
public void NotifyTransportConnected(IPEndPoint remoteEndPoint)
diff --git a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs
index aa23760..b2f0f6f 100644
--- a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs
+++ b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs
@@ -60,7 +60,8 @@ namespace Network.NetworkApplication
SyncSequenceTracker syncSequenceTracker = null,
Func transportFactory = null,
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
- ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
+ ServerAuthoritativeCombatConfiguration authoritativeCombat = null,
+ IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null)
{
ValidateDualPortConfiguration(reliablePort, syncPort);
@@ -86,7 +87,8 @@ namespace Network.NetworkApplication
deliveryPolicyResolver,
syncSequenceTracker,
authoritativeMovement,
- authoritativeCombat);
+ authoritativeCombat,
+ authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance);
}
public static Task StartServerRuntimeAsync(ServerRuntimeConfiguration configuration)
diff --git a/Assets/Scripts/Network/NetworkApplication/SessionManager.cs b/Assets/Scripts/Network/NetworkApplication/SessionManager.cs
index c3b8e77..7238329 100644
--- a/Assets/Scripts/Network/NetworkApplication/SessionManager.cs
+++ b/Assets/Scripts/Network/NetworkApplication/SessionManager.cs
@@ -1,11 +1,12 @@
-using System;
+using System;
namespace Network.NetworkApplication
{
public sealed class SessionManager
{
private readonly Func utcNowProvider;
- private DateTimeOffset? lastLivenessUtc;
+ private DateTimeOffset? lastAcceptedLivenessUtc;
+ private DateTimeOffset? lastTransportActivityUtc;
private DateTimeOffset? lastHeartbeatSentUtc;
private DateTimeOffset? nextReconnectAtUtc;
@@ -24,7 +25,9 @@ namespace Network.NetworkApplication
public SessionReconnectPolicy ReconnectPolicy { get; }
- public DateTimeOffset? LastLivenessUtc => lastLivenessUtc;
+ public DateTimeOffset? LastLivenessUtc => lastAcceptedLivenessUtc;
+
+ public DateTimeOffset? LastTransportActivityUtc => lastTransportActivityUtc;
public DateTimeOffset? LastHeartbeatSentUtc => lastHeartbeatSentUtc;
@@ -70,13 +73,18 @@ namespace Network.NetworkApplication
public void NotifyTransportConnected()
{
var now = utcNowProvider();
- lastLivenessUtc = now;
+ lastTransportActivityUtc = now;
lastHeartbeatSentUtc = null;
nextReconnectAtUtc = null;
LastFailureReason = null;
TransitionTo(ConnectionState.TransportConnected, SessionEventKind.TransportConnected, now);
}
+ public void NotifyTransportActivity()
+ {
+ lastTransportActivityUtc = utcNowProvider();
+ }
+
public void NotifyLoginStarted()
{
TransitionTo(ConnectionState.LoginPending, SessionEventKind.LoginStarted, utcNowProvider());
@@ -85,7 +93,8 @@ namespace Network.NetworkApplication
public void NotifyLoginSucceeded()
{
var now = utcNowProvider();
- lastLivenessUtc = now;
+ lastTransportActivityUtc = now;
+ lastAcceptedLivenessUtc = now;
LastFailureReason = null;
TransitionTo(ConnectionState.LoggedIn, SessionEventKind.LoginSucceeded, now);
}
@@ -105,7 +114,8 @@ namespace Network.NetworkApplication
public void NotifyHeartbeatReceived()
{
var now = utcNowProvider();
- lastLivenessUtc = now;
+ lastTransportActivityUtc = now;
+ lastAcceptedLivenessUtc = now;
if (lastHeartbeatSentUtc.HasValue)
{
LastRoundTripTime = now - lastHeartbeatSentUtc.Value;
@@ -116,7 +126,9 @@ namespace Network.NetworkApplication
public void NotifyInboundActivity()
{
- lastLivenessUtc = utcNowProvider();
+ var now = utcNowProvider();
+ lastTransportActivityUtc = now;
+ lastAcceptedLivenessUtc = now;
}
public void NotifyTransportDisconnected(string reason = null)
@@ -152,17 +164,18 @@ namespace Network.NetworkApplication
private bool ShouldTimeout(DateTimeOffset now)
{
- if (State != ConnectionState.TransportConnected && State != ConnectionState.LoginPending && State != ConnectionState.LoggedIn)
+ switch (State)
{
- return false;
+ case ConnectionState.TransportConnected:
+ case ConnectionState.LoginPending:
+ return lastTransportActivityUtc.HasValue &&
+ now - lastTransportActivityUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
+ case ConnectionState.LoggedIn:
+ return lastAcceptedLivenessUtc.HasValue &&
+ now - lastAcceptedLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
+ default:
+ return false;
}
-
- if (!lastLivenessUtc.HasValue)
- {
- return false;
- }
-
- return now - lastLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout;
}
private void TransitionTo(
diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs
new file mode 100644
index 0000000..c7eae16
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Net;
+
+namespace Network.NetworkHost
+{
+ public sealed class AuthoritativeMovementWorldValidationRequest
+ {
+ public AuthoritativeMovementWorldValidationRequest(
+ IPEndPoint remoteEndPoint,
+ string playerId,
+ float currentPositionX,
+ float currentPositionY,
+ float currentPositionZ,
+ float candidatePositionX,
+ float candidatePositionY,
+ float candidatePositionZ,
+ float velocityX,
+ float velocityY,
+ float velocityZ)
+ {
+ RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint));
+ PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId));
+ CurrentPositionX = currentPositionX;
+ CurrentPositionY = currentPositionY;
+ CurrentPositionZ = currentPositionZ;
+ CandidatePositionX = candidatePositionX;
+ CandidatePositionY = candidatePositionY;
+ CandidatePositionZ = candidatePositionZ;
+ VelocityX = velocityX;
+ VelocityY = velocityY;
+ VelocityZ = velocityZ;
+ }
+
+ public IPEndPoint RemoteEndPoint { get; }
+
+ public string PlayerId { get; }
+
+ public float CurrentPositionX { get; }
+
+ public float CurrentPositionY { get; }
+
+ public float CurrentPositionZ { get; }
+
+ public float CandidatePositionX { get; }
+
+ public float CandidatePositionY { get; }
+
+ public float CandidatePositionZ { get; }
+
+ public float VelocityX { get; }
+
+ public float VelocityY { get; }
+
+ public float VelocityZ { get; }
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta
new file mode 100644
index 0000000..5d77096
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 44fc9b6a7ea518c419516af9014d6a46
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs
new file mode 100644
index 0000000..93f8cc4
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs
@@ -0,0 +1,22 @@
+namespace Network.NetworkHost
+{
+ public readonly struct AuthoritativeMovementWorldValidationResult
+ {
+ private AuthoritativeMovementWorldValidationResult(bool isAllowed)
+ {
+ IsAllowed = isAllowed;
+ }
+
+ public bool IsAllowed { get; }
+
+ public static AuthoritativeMovementWorldValidationResult Allow()
+ {
+ return new AuthoritativeMovementWorldValidationResult(true);
+ }
+
+ public static AuthoritativeMovementWorldValidationResult Reject()
+ {
+ return new AuthoritativeMovementWorldValidationResult(false);
+ }
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta
new file mode 100644
index 0000000..1b4c5d2
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4838ecf8df980f8498d225a15ab84df3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs
new file mode 100644
index 0000000..e435a8c
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs
@@ -0,0 +1,7 @@
+namespace Network.NetworkHost
+{
+ public interface IAuthoritativeMovementWorldValidator
+ {
+ AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request);
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta
new file mode 100644
index 0000000..dd3785c
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b6ae924c07d515d44b00298c08c121b2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs
new file mode 100644
index 0000000..5988ba8
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Network.NetworkHost
+{
+ public sealed class PermissiveAuthoritativeMovementWorldValidator : IAuthoritativeMovementWorldValidator
+ {
+ private PermissiveAuthoritativeMovementWorldValidator()
+ {
+ }
+
+ public static PermissiveAuthoritativeMovementWorldValidator Instance { get; } = new PermissiveAuthoritativeMovementWorldValidator();
+
+ public AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request)
+ {
+ if (request == null)
+ {
+ throw new ArgumentNullException(nameof(request));
+ }
+
+ return AuthoritativeMovementWorldValidationResult.Allow();
+ }
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta
new file mode 100644
index 0000000..6a9635e
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 251269ca4373a134b955c8a2a5786f95
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs
index 0ffbd29..43b1921 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs
@@ -11,16 +11,19 @@ namespace Network.NetworkHost
internal sealed class ServerAuthoritativeCombatCoordinator
{
private readonly object gate = new();
+ private readonly ServerNetworkHost host;
private readonly MessageManager messageManager;
private readonly ServerAuthoritativeMovementCoordinator movementCoordinator;
private readonly Dictionary statesByPeer = new();
private readonly ServerAuthoritativeCombatConfiguration configuration;
public ServerAuthoritativeCombatCoordinator(
+ ServerNetworkHost host,
MessageManager messageManager,
ServerAuthoritativeMovementCoordinator movementCoordinator,
ServerAuthoritativeCombatConfiguration configuration)
{
+ this.host = host ?? throw new ArgumentNullException(nameof(host));
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
@@ -56,13 +59,13 @@ namespace Network.NetworkHost
return Task.CompletedTask;
}
- if (!TryValidateAcceptedShot(input, sender, out var attackerState, out var targetState))
+ if (!TryValidateAcceptedShot(input, sender, out var acceptedPeer, out var attackerState, out var targetState))
{
BroadcastRejectedShot(input);
return Task.CompletedTask;
}
- movementCoordinator.TryUpdateState(sender, state =>
+ movementCoordinator.TryUpdateState(acceptedPeer, state =>
{
state.LastAcceptedShootTick = input.Tick;
}, out attackerState);
@@ -85,6 +88,7 @@ namespace Network.NetworkHost
Z = targetState.PositionZ
};
+ // Keep the coordinator as the only gameplay-relevant authoritative combat-result emitter.
messageManager.BroadcastMessage(new CombatEvent
{
Tick = input.Tick,
@@ -157,16 +161,19 @@ namespace Network.NetworkHost
private bool TryValidateAcceptedShot(
ShootInput input,
IPEndPoint sender,
+ out IPEndPoint acceptedPeer,
out ServerAuthoritativeMovementState attackerState,
out ServerAuthoritativeMovementState targetState)
{
+ acceptedPeer = null;
attackerState = null;
targetState = null;
if (input == null ||
string.IsNullOrWhiteSpace(input.PlayerId) ||
!IsFinite(input.DirX) ||
- !IsFinite(input.DirY))
+ !IsFinite(input.DirY) ||
+ !host.TryResolveAcceptedPeer(sender, input.PlayerId, out acceptedPeer))
{
return false;
}
@@ -177,8 +184,8 @@ namespace Network.NetworkHost
return false;
}
- if (!movementCoordinator.TryGetState(sender, out attackerState) &&
- !movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState))
+ if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) &&
+ !movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState))
{
return false;
}
@@ -191,7 +198,12 @@ namespace Network.NetworkHost
return false;
}
- return TryResolveTargetState(input, attackerState, out targetState);
+ if (!TryResolveTargetState(input, attackerState, out targetState))
+ {
+ return false;
+ }
+
+ return host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId);
}
private bool TryResolveTargetState(
diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs
index 2c97ee9..95632da 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs
@@ -6,6 +6,8 @@ namespace Network.NetworkHost
{
public float MoveSpeed { get; set; } = 5f;
+ public float TurnSpeedDegreesPerSecond { get; set; } = 180f;
+
public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50);
public int DefaultHp { get; set; } = 100;
@@ -17,6 +19,11 @@ namespace Network.NetworkHost
throw new ArgumentOutOfRangeException(nameof(MoveSpeed), "Move speed must be finite and non-negative.");
}
+ if (float.IsNaN(TurnSpeedDegreesPerSecond) || float.IsInfinity(TurnSpeedDegreesPerSecond) || TurnSpeedDegreesPerSecond < 0f)
+ {
+ throw new ArgumentOutOfRangeException(nameof(TurnSpeedDegreesPerSecond), "Turn speed must be finite and non-negative.");
+ }
+
if (BroadcastInterval <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive.");
diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs
index 2432ba0..8dbcd3e 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs
@@ -14,6 +14,7 @@ namespace Network.NetworkHost
private readonly MessageManager messageManager;
private readonly ServerNetworkHost host;
private readonly ServerAuthoritativeMovementConfiguration configuration;
+ private readonly IAuthoritativeMovementWorldValidator worldValidator;
private readonly Dictionary statesByPeer = new();
private long nextBroadcastTick = 1;
private TimeSpan accumulatedBroadcastTime;
@@ -21,11 +22,13 @@ namespace Network.NetworkHost
public ServerAuthoritativeMovementCoordinator(
ServerNetworkHost host,
MessageManager messageManager,
- ServerAuthoritativeMovementConfiguration configuration)
+ ServerAuthoritativeMovementConfiguration configuration,
+ IAuthoritativeMovementWorldValidator worldValidator)
{
this.host = host ?? throw new ArgumentNullException(nameof(host));
this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
+ this.worldValidator = worldValidator ?? throw new ArgumentNullException(nameof(worldValidator));
}
public IReadOnlyList States
@@ -48,7 +51,7 @@ namespace Network.NetworkHost
throw new ArgumentNullException(nameof(remoteEndPoint));
}
- if (string.IsNullOrWhiteSpace(playerId))
+ if (string.IsNullOrWhiteSpace(playerId) || !host.IsAcceptedPlayer(remoteEndPoint, playerId))
{
state = null;
return false;
@@ -99,13 +102,14 @@ namespace Network.NetworkHost
}
if (string.IsNullOrWhiteSpace(input.PlayerId) ||
- !IsFinite(input.MoveX) ||
- !IsFinite(input.MoveY))
+ !IsFinite(input.TurnInput) ||
+ !IsFinite(input.ThrottleInput) ||
+ !host.TryResolveAcceptedPeer(sender, input.PlayerId, out var acceptedPeer))
{
return Task.CompletedTask;
}
- var normalizedSender = Normalize(sender);
+ var normalizedSender = Normalize(acceptedPeer);
var key = normalizedSender.ToString();
lock (gate)
@@ -113,7 +117,8 @@ namespace Network.NetworkHost
if (statesByPeer.TryGetValue(key, out var existingState))
{
if (!string.Equals(existingState.PlayerId, input.PlayerId, StringComparison.Ordinal) ||
- input.Tick <= existingState.LastAcceptedMoveTick)
+ input.Tick <= existingState.LastAcceptedMoveTick ||
+ !host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId))
{
return Task.CompletedTask;
}
@@ -122,6 +127,11 @@ namespace Network.NetworkHost
return Task.CompletedTask;
}
+ if (!host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId))
+ {
+ return Task.CompletedTask;
+ }
+
var state = new ServerAuthoritativeMovementState(
normalizedSender,
input.PlayerId,
@@ -172,6 +182,7 @@ namespace Network.NetworkHost
foreach (var pendingBroadcast in pendingBroadcasts)
{
+ // Keep the coordinator as the only gameplay-relevant PlayerState broadcast path.
messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState);
host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick);
}
@@ -330,32 +341,20 @@ namespace Network.NetworkHost
private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input)
{
state.LastAcceptedMoveTick = input.Tick;
- state.InputX = input.MoveX;
- state.InputY = input.MoveY;
+ state.InputX = ClampInput(input.TurnInput);
+ state.InputY = ClampInput(input.ThrottleInput);
- if (input.MoveX == 0f && input.MoveY == 0f)
+ if (state.InputY == 0f)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
state.VelocityZ = 0f;
- return;
}
-
- var length = MathF.Sqrt((input.MoveX * input.MoveX) + (input.MoveY * input.MoveY));
- if (length <= 0f)
- {
- state.VelocityX = 0f;
- state.VelocityY = 0f;
- state.VelocityZ = 0f;
- return;
- }
-
- state.Rotation = MathF.Atan2(input.MoveY, input.MoveX) * (180f / MathF.PI);
}
private void IntegrateState(ServerAuthoritativeMovementState state, TimeSpan elapsed)
{
- if (state.IsDead || (state.InputX == 0f && state.InputY == 0f))
+ if (state.IsDead)
{
state.VelocityX = 0f;
state.VelocityY = 0f;
@@ -363,25 +362,83 @@ namespace Network.NetworkHost
return;
}
- var length = MathF.Sqrt((state.InputX * state.InputX) + (state.InputY * state.InputY));
- if (length <= 0f)
- {
- state.VelocityX = 0f;
- state.VelocityY = 0f;
- state.VelocityZ = 0f;
- return;
- }
-
- var normalizedX = state.InputX / length;
- var normalizedY = state.InputY / length;
- state.VelocityX = normalizedX * configuration.MoveSpeed;
- state.VelocityY = 0f;
- state.VelocityZ = normalizedY * configuration.MoveSpeed;
-
var deltaSeconds = (float)elapsed.TotalSeconds;
- state.PositionX += state.VelocityX * deltaSeconds;
- state.PositionY += state.VelocityY * deltaSeconds;
- state.PositionZ += state.VelocityZ * deltaSeconds;
+ if (deltaSeconds <= 0f)
+ {
+ state.VelocityX = 0f;
+ state.VelocityY = 0f;
+ state.VelocityZ = 0f;
+ return;
+ }
+
+ var turnInput = ClampInput(state.InputX);
+ var throttleInput = ClampInput(state.InputY);
+ if (turnInput != 0f)
+ {
+ state.Rotation = NormalizeDegrees(state.Rotation + (turnInput * configuration.TurnSpeedDegreesPerSecond * deltaSeconds));
+ }
+
+ if (throttleInput == 0f)
+ {
+ state.VelocityX = 0f;
+ state.VelocityY = 0f;
+ state.VelocityZ = 0f;
+ return;
+ }
+
+ var rotationRadians = state.Rotation * (MathF.PI / 180f);
+ var forwardX = MathF.Cos(rotationRadians);
+ var forwardZ = MathF.Sin(rotationRadians);
+ state.VelocityX = forwardX * (throttleInput * configuration.MoveSpeed);
+ state.VelocityY = 0f;
+ state.VelocityZ = forwardZ * (throttleInput * configuration.MoveSpeed);
+
+ var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds);
+ var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds);
+ var candidatePositionZ = state.PositionZ + (state.VelocityZ * deltaSeconds);
+ var validationResult = worldValidator.Validate(new AuthoritativeMovementWorldValidationRequest(
+ state.RemoteEndPoint,
+ state.PlayerId,
+ state.PositionX,
+ state.PositionY,
+ state.PositionZ,
+ candidatePositionX,
+ candidatePositionY,
+ candidatePositionZ,
+ state.VelocityX,
+ state.VelocityY,
+ state.VelocityZ));
+ if (!validationResult.IsAllowed)
+ {
+ state.VelocityX = 0f;
+ state.VelocityY = 0f;
+ state.VelocityZ = 0f;
+ return;
+ }
+
+ state.PositionX = candidatePositionX;
+ state.PositionY = candidatePositionY;
+ state.PositionZ = candidatePositionZ;
+ }
+
+ private static float ClampInput(float value)
+ {
+ return MathF.Max(-1f, MathF.Min(1f, value));
+ }
+
+ private static float NormalizeDegrees(float degrees)
+ {
+ var normalized = degrees % 360f;
+ if (normalized <= -180f)
+ {
+ normalized += 360f;
+ }
+ else if (normalized > 180f)
+ {
+ normalized -= 360f;
+ }
+
+ return normalized;
}
private static PlayerState BuildPlayerState(ServerAuthoritativeMovementState state, long tick)
diff --git a/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs b/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs
index 2ff9c11..36f3dc9 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Threading.Tasks;
using Network.Defines;
@@ -17,6 +18,8 @@ namespace Network.NetworkHost
private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator;
private readonly object playerIdentityGate = new();
private readonly Dictionary playerIdsByPeer = new();
+ private readonly Dictionary canonicalPeersByPlayerId = new(StringComparer.Ordinal);
+ private readonly Dictionary> peerKeysByPlayerId = new(StringComparer.Ordinal);
public ServerNetworkHost(
ITransport transport,
@@ -27,7 +30,8 @@ namespace Network.NetworkHost
IMessageDeliveryPolicyResolver deliveryPolicyResolver = null,
SyncSequenceTracker syncSequenceTracker = null,
ServerAuthoritativeMovementConfiguration authoritativeMovement = null,
- ServerAuthoritativeCombatConfiguration authoritativeCombat = null)
+ ServerAuthoritativeCombatConfiguration authoritativeCombat = null,
+ IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null)
{
this.transport = transport ?? throw new ArgumentNullException(nameof(transport));
this.syncTransport = syncTransport;
@@ -44,11 +48,14 @@ namespace Network.NetworkHost
deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(),
this.syncTransport,
syncSequenceTracker ?? new SyncSequenceTracker());
+ var resolvedWorldValidator = authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance;
authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator(
this,
messageManager,
- authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration());
+ authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration(),
+ resolvedWorldValidator);
authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator(
+ this,
messageManager,
authoritativeMovementCoordinator,
authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration());
@@ -102,6 +109,8 @@ namespace Network.NetworkHost
lock (playerIdentityGate)
{
playerIdsByPeer.Clear();
+ canonicalPeersByPlayerId.Clear();
+ peerKeysByPlayerId.Clear();
}
PublishMetricsSessionSnapshots();
}
@@ -137,6 +146,117 @@ namespace Network.NetworkHost
return authoritativeCombatCoordinator.TryGetState(remoteEndPoint, out state);
}
+ public bool TryGetAcceptedPlayerId(IPEndPoint remoteEndPoint, out string playerId)
+ {
+ return TryGetKnownPlayerId(remoteEndPoint, out playerId);
+ }
+
+ public bool IsAcceptedPlayer(IPEndPoint remoteEndPoint, string playerId)
+ {
+ return !string.IsNullOrWhiteSpace(playerId) &&
+ TryGetKnownPlayerId(remoteEndPoint, out var acceptedPlayerId) &&
+ string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal);
+ }
+
+ public bool TryResolveAcceptedPeer(IPEndPoint remoteEndPoint, string playerId, out IPEndPoint acceptedPeer)
+ {
+ acceptedPeer = null;
+ if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
+ {
+ return false;
+ }
+
+ var normalizedRemoteEndPoint = Normalize(remoteEndPoint);
+ var remoteKey = normalizedRemoteEndPoint.ToString();
+
+ lock (playerIdentityGate)
+ {
+ if (playerIdsByPeer.TryGetValue(remoteKey, out var mappedPlayerId))
+ {
+ if (!string.Equals(mappedPlayerId, playerId, StringComparison.Ordinal))
+ {
+ return false;
+ }
+
+ if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer))
+ {
+ acceptedPeer = normalizedRemoteEndPoint;
+ }
+
+ return true;
+ }
+
+ if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer))
+ {
+ return false;
+ }
+
+ playerIdsByPeer[remoteKey] = playerId;
+ if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
+ {
+ peerKeys = new HashSet(StringComparer.Ordinal);
+ peerKeysByPlayerId[playerId] = peerKeys;
+ }
+
+ peerKeys.Add(remoteKey);
+ return true;
+ }
+ }
+
+ public bool IsPlayerIdInUse(string playerId)
+ {
+ if (string.IsNullOrWhiteSpace(playerId))
+ {
+ return false;
+ }
+
+ lock (playerIdentityGate)
+ {
+ foreach (var acceptedPlayerId in playerIdsByPeer.Values)
+ {
+ if (string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+ }
+
+ foreach (var state in authoritativeMovementCoordinator.States)
+ {
+ if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ foreach (var state in authoritativeCombatCoordinator.States)
+ {
+ if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool TryRefreshAcceptedGameplayActivity(IPEndPoint remoteEndPoint, string playerId)
+ {
+ if (!TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer))
+ {
+ return false;
+ }
+
+ if (!TryGetSession(acceptedPeer, out var session) ||
+ session.SessionManager.State != ConnectionState.LoggedIn)
+ {
+ return false;
+ }
+
+ NotifyInboundActivity(acceptedPeer);
+ return true;
+ }
+
public void NotifyLoginStarted(IPEndPoint remoteEndPoint)
{
SessionCoordinator.NotifyLoginStarted(remoteEndPoint);
@@ -159,7 +279,7 @@ namespace Network.NetworkHost
public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null)
{
SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason);
- ForgetPlayerId(remoteEndPoint);
+ ForgetPeerIdentity(remoteEndPoint);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
@@ -189,20 +309,34 @@ namespace Network.NetworkHost
public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null)
{
- if (!SessionCoordinator.TryGetSession(remoteEndPoint, out var session))
+ if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId) ||
+ !TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer) ||
+ !SessionCoordinator.TryGetSession(acceptedPeer, out var session))
{
return false;
}
- var removed = SessionCoordinator.RemoveSession(remoteEndPoint, reason);
+ var removed = SessionCoordinator.RemoveSession(acceptedPeer, reason);
if (!removed)
{
return false;
}
- authoritativeMovementCoordinator.RemoveState(remoteEndPoint);
- authoritativeCombatCoordinator.RemoveState(remoteEndPoint);
- ForgetPlayerId(remoteEndPoint);
+ authoritativeMovementCoordinator.RemoveState(acceptedPeer);
+ authoritativeCombatCoordinator.RemoveState(acceptedPeer);
+
+ var knownPeerEndpoints = GetKnownPeerEndpointsForPlayerId(playerId);
+ ForgetPlayerId(playerId);
+
+ foreach (var peerEndPoint in knownPeerEndpoints)
+ {
+ SessionCoordinator.RemoveSession(peerEndPoint, reason);
+ RemoveTransportPeerSession(transport, peerEndPoint);
+ if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
+ {
+ RemoveTransportPeerSession(syncTransport, peerEndPoint);
+ }
+ }
RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected);
if (syncTransport != null && !ReferenceEquals(syncTransport, transport))
@@ -216,10 +350,17 @@ namespace Network.NetworkHost
private void HandleTransportReceive(byte[] data, IPEndPoint sender)
{
SessionCoordinator.ObserveTransportActivity(sender);
- ObservePlayerIdentity(data, sender);
PublishMetricsSessionSnapshot(sender);
}
+ private static void RemoveTransportPeerSession(ITransport transport, IPEndPoint remoteEndPoint)
+ {
+ if (transport is IPeerSessionTransport peerSessionTransport)
+ {
+ peerSessionTransport.RemovePeerSession(remoteEndPoint);
+ }
+ }
+
private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint)
{
if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId))
@@ -230,41 +371,6 @@ namespace Network.NetworkHost
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _);
}
- private void ObservePlayerIdentity(byte[] data, IPEndPoint sender)
- {
- if (data == null || sender == null)
- {
- return;
- }
-
- Envelope envelope;
- try
- {
- envelope = Envelope.Parser.ParseFrom(data);
- }
- catch
- {
- return;
- }
-
- if ((MessageType)envelope.Type != MessageType.LoginRequest)
- {
- return;
- }
-
- LoginRequest request;
- try
- {
- request = LoginRequest.Parser.ParseFrom(envelope.Payload);
- }
- catch
- {
- return;
- }
-
- RememberPlayerId(sender, request.PlayerId);
- }
-
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
{
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
@@ -272,10 +378,19 @@ namespace Network.NetworkHost
return;
}
- var key = Normalize(remoteEndPoint).ToString();
+ var normalizedRemoteEndPoint = Normalize(remoteEndPoint);
+ var key = normalizedRemoteEndPoint.ToString();
lock (playerIdentityGate)
{
playerIdsByPeer[key] = playerId;
+ canonicalPeersByPlayerId[playerId] = normalizedRemoteEndPoint;
+ if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
+ {
+ peerKeys = new HashSet(StringComparer.Ordinal);
+ peerKeysByPlayerId[playerId] = peerKeys;
+ }
+
+ peerKeys.Add(key);
}
}
@@ -294,7 +409,51 @@ namespace Network.NetworkHost
}
}
- private void ForgetPlayerId(IPEndPoint remoteEndPoint)
+ private IReadOnlyList GetKnownPeerEndpointsForPlayerId(string playerId)
+ {
+ if (string.IsNullOrWhiteSpace(playerId))
+ {
+ return Array.Empty();
+ }
+
+ lock (playerIdentityGate)
+ {
+ if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
+ {
+ return Array.Empty();
+ }
+
+ return peerKeys
+ .Select(ParseEndPoint)
+ .Where(static endpoint => endpoint != null)
+ .ToArray();
+ }
+ }
+
+ private void ForgetPlayerId(string playerId)
+ {
+ if (string.IsNullOrWhiteSpace(playerId))
+ {
+ return;
+ }
+
+ lock (playerIdentityGate)
+ {
+ if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
+ {
+ foreach (var peerKey in peerKeys)
+ {
+ playerIdsByPeer.Remove(peerKey);
+ }
+
+ peerKeysByPlayerId.Remove(playerId);
+ }
+
+ canonicalPeersByPlayerId.Remove(playerId);
+ }
+ }
+
+ private void ForgetPeerIdentity(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
{
@@ -304,10 +463,49 @@ namespace Network.NetworkHost
var key = Normalize(remoteEndPoint).ToString();
lock (playerIdentityGate)
{
+ if (!playerIdsByPeer.TryGetValue(key, out var playerId))
+ {
+ return;
+ }
+
playerIdsByPeer.Remove(key);
+ if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys))
+ {
+ peerKeys.Remove(key);
+ if (peerKeys.Count == 0)
+ {
+ peerKeysByPlayerId.Remove(playerId);
+ canonicalPeersByPlayerId.Remove(playerId);
+ }
+ }
}
}
+ private static IPEndPoint ParseEndPoint(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return null;
+ }
+
+ var lastColonIndex = value.LastIndexOf(':');
+ if (lastColonIndex <= 0 || lastColonIndex >= value.Length - 1)
+ {
+ return null;
+ }
+
+ var addressText = value.Substring(0, lastColonIndex);
+ if (addressText.Length > 1 && addressText[0] == '[' && addressText[addressText.Length - 1] == ']')
+ {
+ addressText = addressText.Substring(1, addressText.Length - 2);
+ }
+
+ return IPAddress.TryParse(addressText, out var address) &&
+ int.TryParse(value.Substring(lastColonIndex + 1), out var port)
+ ? new IPEndPoint(address, port)
+ : null;
+ }
+
private static IPEndPoint Normalize(IPEndPoint remoteEndPoint)
{
if (remoteEndPoint == null)
diff --git a/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs b/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs
index 26af0f5..8557986 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs
@@ -14,6 +14,7 @@ namespace Network.NetworkHost
}
ReliablePort = reliablePort;
+ AuthoritativeMovementWorldValidator = PermissiveAuthoritativeMovementWorldValidator.Instance;
}
public int ReliablePort { get; }
@@ -36,6 +37,8 @@ namespace Network.NetworkHost
public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; }
+ public IAuthoritativeMovementWorldValidator AuthoritativeMovementWorldValidator { get; set; }
+
internal void Validate()
{
if (ReliablePort <= 0)
@@ -58,6 +61,10 @@ namespace Network.NetworkHost
AuthoritativeMovement?.Validate();
AuthoritativeCombat?.Validate();
+ if (AuthoritativeMovementWorldValidator == null)
+ {
+ throw new ArgumentNullException(nameof(AuthoritativeMovementWorldValidator));
+ }
}
}
}
diff --git a/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs b/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs
index ffaa31a..36432e4 100644
--- a/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs
+++ b/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs
@@ -25,7 +25,8 @@ namespace Network.NetworkHost
configuration.SyncSequenceTracker,
configuration.TransportFactory,
configuration.AuthoritativeMovement,
- configuration.AuthoritativeCombat);
+ configuration.AuthoritativeCombat,
+ configuration.AuthoritativeMovementWorldValidator);
try
{
diff --git a/Assets/Scripts/Network/NetworkTransport/ClientSession.cs b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs
new file mode 100644
index 0000000..8375d81
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+
+namespace Network.NetworkTransport
+{
+ public class ClientSession
+ {
+ public IPEndPoint EndPoint { get; }
+ public DateTime LastActivity { get; private set; }
+
+ public uint SendSequenceNumber { get; private set; } = 0;
+
+ //TODO: 数据结构——ConcurrentDictionary
+ public ConcurrentDictionary PendingAcks { get; } =
+ new ConcurrentDictionary();
+
+ public uint ExpectedReceiveSequence { get; private set; } = 0;
+ private HashSet _receivedSequences { get; } = new HashSet();
+
+ private readonly object _lockObj = new object();
+
+ public ClientSession(IPEndPoint endPoint)
+ {
+ EndPoint = endPoint;
+ LastActivity = DateTime.Now;
+ }
+
+ public uint GetNextSendSequence()
+ {
+ lock (_lockObj)
+ {
+ return SendSequenceNumber++;
+ }
+ }
+
+ public bool TryProcessReceiveSequence(uint sequenceNumber, out bool shouldDeliver)
+ {
+ lock (_lockObj)
+ {
+ LastActivity = DateTime.Now;
+
+ if (sequenceNumber == ExpectedReceiveSequence)
+ {
+ ExpectedReceiveSequence++;
+ _receivedSequences.Add(sequenceNumber);
+ shouldDeliver = true;
+ return true;
+ }
+ else if (sequenceNumber < ExpectedReceiveSequence)
+ {
+ shouldDeliver = false;
+ return _receivedSequences.Contains(sequenceNumber);
+ }
+ else
+ {
+ shouldDeliver = false;
+ return false;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta
new file mode 100644
index 0000000..38fb162
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c007e0986e972b14cb4b4ab2151459c3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs
new file mode 100644
index 0000000..1e94787
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs
@@ -0,0 +1,9 @@
+using System.Net;
+
+namespace Network.NetworkTransport
+{
+ public interface IPeerSessionTransport
+ {
+ bool RemovePeerSession(IPEndPoint remoteEndPoint);
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta
new file mode 100644
index 0000000..7a9ba10
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b2637543abd875349aaa27b3e31f9dbe
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta b/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta
index b03a95a..87dad67 100644
--- a/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta
+++ b/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta
@@ -1,3 +1,11 @@
-fileFormatVersion: 2
+fileFormatVersion: 2
guid: 63fb533d620e4ac0bf15ba0a9a71331e
-timeCreated: 1774593005
\ No newline at end of file
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs b/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs
index 510457a..5f6e977 100644
--- a/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs
+++ b/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Net;
@@ -10,7 +10,7 @@ using kcp;
namespace Network.NetworkTransport
{
- public partial class KcpTransport : ITransport, ITransportMetricsSink
+ public partial class KcpTransport : ITransport, ITransportMetricsSink, IPeerSessionTransport
{
private const uint DefaultConv = 1;
private const int DefaultNoDelay = 1;
@@ -233,6 +233,26 @@ namespace Network.NetworkTransport
}
}
+ public bool RemovePeerSession(IPEndPoint remoteEndPoint)
+ {
+ if (remoteEndPoint == null)
+ {
+ throw new ArgumentNullException(nameof(remoteEndPoint));
+ }
+
+ var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint);
+ var key = normalizedEndPoint.ToString();
+ if (!_sessions.TryRemove(key, out var session))
+ {
+ return false;
+ }
+
+ RecordSessionDiagnostics(session, "removed");
+ session.Dispose();
+ _metricsModule.RecordSessionClosed(session.RemoteEndPoint);
+ return true;
+ }
+
private KcpSession GetOrCreateSession(IPEndPoint remoteEndPoint, uint conv)
{
var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint);
diff --git a/Assets/Scripts/Network/NetworkTransport/Packet.cs b/Assets/Scripts/Network/NetworkTransport/Packet.cs
new file mode 100644
index 0000000..f7bc8f8
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/Packet.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Linq;
+
+namespace Network.NetworkTransport
+{
+ public enum PacketType : byte
+ {
+ Data = 1,
+ Ack = 2,
+ }
+
+ public struct Packet
+ {
+ public PacketType Type;
+ public uint SequenceNumber;
+ public byte[] Data;
+
+ public byte[] ToBytes()
+ {
+ var result = new byte[1 + 4 + Data.Length];
+ result[0] = (byte)Type;
+ BitConverter.GetBytes(SequenceNumber).CopyTo(result, 1);
+ Data.CopyTo(result, 5);
+ return result;
+ }
+
+ public static Packet FromBytes(byte[] data)
+ {
+ return new Packet
+ {
+ Type = (PacketType)data[0],
+ SequenceNumber = BitConverter.ToUInt32(data, 1),
+ //TODO: 结构体——ArraySegment
+ Data = new ArraySegment(data, 5, data.Length - 5).ToArray()
+ };
+ }
+
+ public static Packet CreateDataPacket(uint seqNum, byte[] data)
+ {
+ return new Packet
+ {
+ Type = PacketType.Data,
+ SequenceNumber = seqNum,
+ Data = data
+ };
+ }
+
+ public static Packet CreateAckPacket(uint seqNum)
+ {
+ return new Packet
+ {
+ Type = PacketType.Ack,
+ SequenceNumber = seqNum,
+ Data = Array.Empty()
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta b/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta
new file mode 100644
index 0000000..e25a990
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a16c0628d84e6274bbe164e0e57533e4
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs
new file mode 100644
index 0000000..bcd9a3a
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs
@@ -0,0 +1,312 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Network.NetworkTransport
+{
+ internal sealed class LegacyUdpTransportAdapter : ITransport, IPeerSessionTransport
+ {
+ private readonly UdpClient _client;
+ private readonly IPEndPoint? _defaultRemoteEndPoint;
+ private readonly bool _isServer;
+
+ private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary();
+ private readonly ConcurrentDictionary _resentPacketTimes = new ConcurrentDictionary();
+ private readonly Timer _retransmitTimer;
+ private readonly Timer _cleanupTimer;
+
+ //TODO: volatile 关键字
+ private volatile bool _isRunning;
+
+ // 配置参数
+ private const int RetransmitTimeoutMs = 1000;
+ private const int SessionTimeoutMs = 30000;
+ private const int MaxRetransmitAttempts = 5;
+
+ public event Action? OnReceive;
+
+ // 构造函数——服务端模式
+ public LegacyUdpTransportAdapter(int listenPort)
+ {
+ _client = new UdpClient(listenPort);
+ _isServer = true;
+ _retransmitTimer = new Timer(CheckRetransmit, null, 100, 100);
+ _cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000);
+ Console.WriteLine($"[Transport] 服务端模式,监听端口: {listenPort}");
+ }
+
+ // 构造函数——客户端模式
+ public LegacyUdpTransportAdapter(string serverIP, int serverPort)
+ {
+ _client = new UdpClient(0);
+ _defaultRemoteEndPoint = new IPEndPoint(IPAddress.Parse(serverIP), serverPort);
+
+ _isServer = false;
+ _retransmitTimer = new Timer(CheckRetransmit, null, 100, 100);
+ _cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000);
+ Console.WriteLine($"[Transport] 客户端模式,目标: {_defaultRemoteEndPoint}");
+ }
+
+ public async Task StartAsync()
+ {
+ _sessions.Clear();
+
+ _isRunning = true;
+ Console.WriteLine("[Transport] 传输层启动");
+
+ // 开始接收数据
+ _ = Task.Run(ReceiveLoop);
+ await Task.Delay(100); // 给接收循环一点启动时间
+ }
+
+ public void Stop()
+ {
+ _isRunning = false;
+ _retransmitTimer.Dispose();
+ _cleanupTimer.Dispose();
+ _client.Close();
+ _sessions.Clear();
+ Console.WriteLine("[Transport] 传输层停止");
+ }
+
+ public void Send(byte[] data)
+ {
+ if (!_isServer && _defaultRemoteEndPoint != null)
+ {
+ SendTo(data, _defaultRemoteEndPoint);
+ }
+ else
+ {
+ throw new InvalidOperationException("服务端模式必须使用 SendTo 指定目标");
+ }
+ }
+
+ public void SendTo(byte[] data, IPEndPoint target)
+ {
+ if (!_isRunning)
+ {
+ return;
+ }
+
+ var session = GetOrCreateSession(target);
+ uint seqNum = session.GetNextSendSequence();
+ var packet = Packet.CreateDataPacket(seqNum, data);
+
+ session.PendingAcks[seqNum] = (packet, DateTime.Now);
+
+ SendPacketTo(packet, target);
+ Console.WriteLine($"[Transport] 发送数据包到 {target} SeqNum={seqNum}, DataLen={data.Length}");
+ }
+
+ public void SendToAll(byte[] data)
+ {
+ foreach (var session in _sessions.Values)
+ {
+ SendTo(data, session.EndPoint);
+ }
+ }
+
+ public bool RemovePeerSession(IPEndPoint remoteEndPoint)
+ {
+ if (remoteEndPoint == null)
+ {
+ throw new ArgumentNullException(nameof(remoteEndPoint));
+ }
+
+ return _sessions.TryRemove(remoteEndPoint.ToString(), out _);
+ }
+
+ private async void ReceiveLoop()
+ {
+ while (_isRunning)
+ {
+ try
+ {
+ var result = await _client.ReceiveAsync();
+ var packet = Packet.FromBytes(result.Buffer);
+
+ if (packet.Type == PacketType.Data)
+ {
+ HandleDataPacket(packet, result.RemoteEndPoint);
+ }
+ else if (packet.Type == PacketType.Ack)
+ {
+ HandleAckPacket(packet, result.RemoteEndPoint);
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ return; // 正常关闭
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"[Transport] 接收错误:{e.Message}");
+ }
+ }
+ }
+
+ private void HandleDataPacket(Packet packet, IPEndPoint senderEndPoint)
+ {
+ var session = GetOrCreateSession(senderEndPoint);
+
+ Console.WriteLine(
+ $"[Transport] 收到数据包从{senderEndPoint} SeqNum={packet.SequenceNumber}, DataLen={packet.Data.Length}");
+
+ // 发送ACK
+ var ackPacket = Packet.CreateAckPacket(packet.SequenceNumber);
+ SendPacketTo(ackPacket, senderEndPoint);
+ Console.WriteLine($"[Transport] 发送ACK 到 {senderEndPoint} SeqNum={packet.SequenceNumber}");
+
+ // 检查是否应该交付
+ if (session.TryProcessReceiveSequence(packet.SequenceNumber, out bool shouldDeliver))
+ {
+ if (shouldDeliver)
+ {
+ OnReceive?.Invoke(packet.Data, senderEndPoint);
+ Console.WriteLine($"[Transport] 交付数据包从 {senderEndPoint} SeqNum={packet.SequenceNumber}");
+ }
+ else
+ {
+ Console.WriteLine($"[Transport] 重复包从 {senderEndPoint} SeqNum={packet.SequenceNumber},忽略");
+ }
+ }
+ else
+ {
+ // 乱序到达,暂存(简化处理:直接丢弃,依赖重传)
+ Console.WriteLine($"[Transport] 乱序包从 {senderEndPoint} SeqNum={packet.SequenceNumber},丢弃");
+ }
+ }
+
+ private void HandleAckPacket(Packet packet, IPEndPoint senderEndPoint)
+ {
+ var session = GetOrCreateSession(senderEndPoint);
+ Console.WriteLine($"[Transport] 收到ACK从 {senderEndPoint} SeqNum={packet.SequenceNumber}");
+
+ if (session.PendingAcks.TryRemove(packet.SequenceNumber, out _))
+ {
+ Console.WriteLine($"[Transport] 确认包到 {senderEndPoint} SeqNum={packet.SequenceNumber}");
+ }
+ }
+
+ private ClientSession GetOrCreateSession(IPEndPoint endPoint)
+ {
+ string key = endPoint.ToString();
+ return _sessions.GetOrAdd(key, _ =>
+ {
+ var session = new ClientSession(endPoint);
+ Console.WriteLine($"创建新会话:{endPoint}");
+ return session;
+ });
+ }
+
+ private void CheckRetransmit(object? state)
+ {
+ if (!_isRunning)
+ {
+ return;
+ }
+
+ var now = DateTime.Now;
+ var toRetransmit = new List<(IPEndPoint target, uint seqNum, Packet packet)>();
+
+ foreach (var sessionKvp in _sessions)
+ {
+ var session = sessionKvp.Value;
+ foreach (var ackKvp in session.PendingAcks)
+ {
+ var timeSinceLastSend = now - ackKvp.Value.sendTime;
+ if (timeSinceLastSend.TotalMilliseconds > RetransmitTimeoutMs)
+ {
+ toRetransmit.Add((session.EndPoint, ackKvp.Key, ackKvp.Value.packet));
+ _resentPacketTimes.TryAdd(ackKvp.Value.packet, 0);
+ }
+ }
+ }
+
+
+ foreach (var (target, seqNum, packet) in toRetransmit)
+ {
+ var session = GetOrCreateSession(target);
+ if (session.PendingAcks.ContainsKey(seqNum))
+ {
+ // 更新发送时间
+ session.PendingAcks[seqNum] = (packet, now);
+ SendPacketTo(packet, target);
+ Console.WriteLine($"[Transport] 重传包到 {target} SeqNum={seqNum}");
+
+ _resentPacketTimes[packet]++;
+ if (_resentPacketTimes[packet] >= MaxRetransmitAttempts)
+ {
+ // 达到最大重传次数,放弃该会话
+ Console.WriteLine($"[Transport] 达到最大重传次数,放弃会话 {target}");
+ _sessions.TryRemove(target.ToString(), out _);
+ }
+ }
+ }
+ }
+
+ private void CleanupSessions(object? state)
+ {
+ if (!_isRunning)
+ {
+ return;
+ }
+
+ var now = DateTime.Now;
+ var toRemove = new List();
+
+ foreach (var sessionKvp in _sessions)
+ {
+ var session = sessionKvp.Value;
+ var timeSinceLastActivity = now - session.LastActivity;
+
+ if (timeSinceLastActivity.TotalMilliseconds > SessionTimeoutMs)
+ {
+ toRemove.Add(sessionKvp.Key);
+ }
+ }
+
+ foreach (string key in toRemove)
+ {
+ //TODO: 清理会话的同时清理PlayerManager中的玩家数据
+ if (_sessions.TryRemove(key, out var session))
+ {
+ Console.WriteLine($"[Transport] 清理超时会话:{session.EndPoint}");
+ }
+ }
+
+ if (_isServer)
+ {
+ PrintSessionInfo();
+ }
+ }
+
+ private async void SendPacketTo(Packet packet, IPEndPoint? endPoint)
+ {
+ try
+ {
+ var data = packet.ToBytes();
+ await _client.SendAsync(data, data.Length, endPoint);
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine($"[Transport] 发送错误:{e.Message}");
+ }
+ }
+
+ private void PrintSessionInfo()
+ {
+ Console.WriteLine($"当前活跃会话数:{_sessions.Count}");
+ foreach (var sessionKvp in _sessions)
+ {
+ var session = sessionKvp.Value;
+ Console.WriteLine(
+ $" 会话:{session.EndPoint},发送SeqNum:{session.SendSequenceNumber},期望接收:{session.ExpectedReceiveSequence},待确认: {session.PendingAcks.Count}");
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta
new file mode 100644
index 0000000..23516e2
--- /dev/null
+++ b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e740e71ab20f62d40bf5f56c2142bf02
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta b/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta
index c5a663a..8c711a5 100644
--- a/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta
+++ b/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta
@@ -8,4 +8,4 @@ MonoImporter:
icon: {instanceID: 0}
userData:
assetBundleName:
- assetBundleVariant:
\ No newline at end of file
+ assetBundleVariant:
diff --git a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs
index af5d8cf..7048a5a 100644
--- a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs
+++ b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs
@@ -49,6 +49,11 @@ namespace Tests.EditMode.Network
clientRuntime.StartAsync().GetAwaiter().GetResult();
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
+ serverRuntime.Host.NotifyLoginStarted(ClientPeer);
+ serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a");
+ serverRuntime.Host.NotifyLoginStarted(RemotePeer);
+ serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b");
+
serverTransports[9001].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.MoveInput,
@@ -56,8 +61,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-b",
Tick = 1,
- MoveX = 0f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 0f
}),
RemotePeer);
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
@@ -69,8 +74,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-a",
Tick = 1,
- MoveX = 1f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 1f
},
MessageType.MoveInput);
ClientGameplayInputFlow.SendShootInput(
@@ -162,9 +167,9 @@ namespace Tests.EditMode.Network
RemotePeer);
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
- serverRuntime.Host.NotifyLoginSucceeded(ClientPeer);
+ serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a");
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
- serverRuntime.Host.NotifyLoginSucceeded(RemotePeer);
+ serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b");
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
@@ -186,8 +191,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-b",
Tick = 1,
- MoveX = 1f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 1f
}),
RemotePeer);
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
diff --git a/Assets/Tests/EditMode/Network/MessageManagerTests.cs b/Assets/Tests/EditMode/Network/MessageManagerTests.cs
index b273025..7c15814 100644
--- a/Assets/Tests/EditMode/Network/MessageManagerTests.cs
+++ b/Assets/Tests/EditMode/Network/MessageManagerTests.cs
@@ -68,8 +68,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-1",
Tick = 12,
- MoveX = 1,
- MoveY = -1
+ TurnInput = 1,
+ ThrottleInput = -1
};
manager.SendMessage(message, MessageType.MoveInput);
@@ -82,8 +82,8 @@ namespace Tests.EditMode.Network
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
Assert.That(parsed.Tick, Is.EqualTo(12));
- Assert.That(parsed.MoveX, Is.EqualTo(1));
- Assert.That(parsed.MoveY, Is.EqualTo(-1));
+ Assert.That(parsed.TurnInput, Is.EqualTo(1));
+ Assert.That(parsed.ThrottleInput, Is.EqualTo(-1));
}
[Test]
@@ -100,8 +100,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-1",
Tick = 13,
- MoveX = 0f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 0f
};
manager.SendMessage(message, MessageType.MoveInput);
@@ -114,8 +114,8 @@ namespace Tests.EditMode.Network
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
Assert.That(parsed.Tick, Is.EqualTo(13));
- Assert.That(parsed.MoveX, Is.EqualTo(0f));
- Assert.That(parsed.MoveY, Is.EqualTo(0f));
+ Assert.That(parsed.TurnInput, Is.EqualTo(0f));
+ Assert.That(parsed.ThrottleInput, Is.EqualTo(0f));
}
[Test]
@@ -350,12 +350,12 @@ namespace Tests.EditMode.Network
});
transport.EmitReceive(
- BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, MoveX = 1 }),
+ BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, ThrottleInput = 1 }),
Sender);
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
transport.EmitReceive(
- BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, MoveX = -1 }),
+ BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, ThrottleInput = -1 }),
Sender);
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
diff --git a/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs b/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs
index b8e4f4d..bfd1dc6 100644
--- a/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs
+++ b/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs
@@ -39,8 +39,8 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
- PrimePlayer(createdTransports[9000], PeerA, "player-a", 1);
- PrimePlayer(createdTransports[9000], PeerB, "player-b", 1);
+ PrimePlayer(runtime, PeerA, "player-a");
+ PrimePlayer(runtime, PeerB, "player-b");
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
@@ -111,8 +111,8 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
- PrimePlayer(createdTransports[9001], PeerA, "player-a", 1);
- PrimePlayer(createdTransports[9001], PeerB, "player-b", 1);
+ PrimePlayer(runtime, PeerA, "player-a");
+ PrimePlayer(runtime, PeerB, "player-b");
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
@@ -166,8 +166,8 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
- PrimePlayer(createdTransports[9000], PeerA, "player-a", 1);
- PrimePlayer(createdTransports[9000], PeerB, "player-b", 1);
+ PrimePlayer(runtime, PeerA, "player-a");
+ PrimePlayer(runtime, PeerB, "player-b");
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput
@@ -192,15 +192,10 @@ namespace Tests.EditMode.Network
Assert.That(runtime.AuthoritativeCombatStates.Count, Is.EqualTo(0));
}
- private static void PrimePlayer(FakeTransport transport, IPEndPoint peer, string playerId, long tick)
+ private static void PrimePlayer(ServerRuntimeHandle runtime, IPEndPoint peer, string playerId)
{
- transport.EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
- {
- PlayerId = playerId,
- Tick = tick,
- MoveX = 0f,
- MoveY = 0f
- }), peer);
+ runtime.Host.NotifyLoginStarted(peer);
+ runtime.Host.NotifyLoginSucceeded(peer, playerId);
}
private static FakeTransport CreateTransport(IDictionary createdTransports, int port)
diff --git a/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs b/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs
index b2ccfcf..e75d2b3 100644
--- a/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs
+++ b/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs
@@ -33,26 +33,31 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
+ runtime.Host.NotifyLoginStarted(PeerA);
+ runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
+ runtime.Host.NotifyLoginStarted(PeerB);
+ runtime.Host.NotifyLoginSucceeded(PeerB, "player-b");
+
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
PlayerId = "player-a",
Tick = 10,
- MoveX = 1f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 1f
}), PeerA);
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
PlayerId = "player-a",
Tick = 8,
- MoveX = 0f,
- MoveY = 1f
+ TurnInput = 1f,
+ ThrottleInput = 0f
}), PeerA);
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
PlayerId = "player-b",
Tick = 3,
- MoveX = 0f,
- MoveY = 1f
+ TurnInput = 0f,
+ ThrottleInput = -1f
}), PeerB);
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
@@ -66,8 +71,8 @@ namespace Tests.EditMode.Network
Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(stateB.PlayerId, Is.EqualTo("player-b"));
Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3));
- Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f));
- Assert.That(stateB.PositionZ, Is.EqualTo(0.2f).Within(0.0001f));
+ Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f));
+ Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2));
}
@@ -89,12 +94,15 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
+ runtime.Host.NotifyLoginStarted(PeerA);
+ runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
+
createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
PlayerId = "player-a",
Tick = 1,
- MoveX = 1f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 1f
}), PeerA);
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
@@ -118,8 +126,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "player-a",
Tick = 2,
- MoveX = 0f,
- MoveY = 0f
+ TurnInput = 0f,
+ ThrottleInput = 0f
}), PeerA);
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
@@ -164,7 +172,7 @@ namespace Tests.EditMode.Network
}), PeerA);
runtime.Host.NotifyLoginStarted(PeerA);
- runtime.Host.NotifyLoginSucceeded(PeerA);
+ runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
@@ -202,12 +210,15 @@ namespace Tests.EditMode.Network
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
+ runtime.Host.NotifyLoginStarted(PeerA);
+ runtime.Host.NotifyLoginSucceeded(PeerA, "player-a");
+
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput
{
PlayerId = "player-a",
Tick = 5,
- MoveX = 0f,
- MoveY = -1f
+ TurnInput = 0f,
+ ThrottleInput = -1f
}), PeerA);
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
@@ -217,9 +228,9 @@ namespace Tests.EditMode.Network
var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]);
Assert.That(broadcast.Tick, Is.EqualTo(1));
- Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
- Assert.That(broadcast.Position.Z, Is.EqualTo(-0.3f).Within(0.0001f));
- Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f));
+ Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f));
+ Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
+ Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f));
}
private static FakeTransport CreateTransport(IDictionary createdTransports, int port)
diff --git a/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs b/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs
index ca39a7f..6107868 100644
--- a/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs
+++ b/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs
@@ -196,6 +196,10 @@ namespace Tests.EditMode.Network
host.StartAsync().GetAwaiter().GetResult();
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerA);
transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerB);
+ host.NotifyLoginStarted(peerA);
+ host.NotifyLoginSucceeded(peerA, "player-a");
+ host.NotifyLoginStarted(peerB);
+ host.NotifyLoginSucceeded(peerB, "player-b");
var removed = host.RemoveSession(peerA, "peer closed");
@@ -203,7 +207,7 @@ namespace Tests.EditMode.Network
Assert.That(host.ManagedSessions.Count, Is.EqualTo(1));
Assert.That(host.TryGetSession(peerA, out _), Is.False);
Assert.That(host.TryGetSession(peerB, out var sessionB), Is.True);
- Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.TransportConnected));
+ Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.LoggedIn));
}
[Test]
diff --git a/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs b/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs
index 8cdf072..fbf50f4 100644
--- a/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs
+++ b/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs
@@ -110,8 +110,8 @@ namespace Tests.EditMode.Network
{
PlayerId = "shared-player",
Tick = 33,
- MoveX = 1f,
- MoveY = -1f
+ TurnInput = 1f,
+ ThrottleInput = -1f
};
runtime.MessageManager.SendMessage(message, MessageType.MoveInput);
@@ -161,7 +161,7 @@ namespace Tests.EditMode.Network
{
PlayerId = "shared-player",
Tick = 77,
- MoveX = 1f
+ ThrottleInput = 1f
};
runtime.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
@@ -191,7 +191,7 @@ namespace Tests.EditMode.Network
{
PlayerId = "server-player",
Tick = 88,
- MoveY = 1f
+ ThrottleInput = 1f
};
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
@@ -215,7 +215,7 @@ namespace Tests.EditMode.Network
{
PlayerId = "fallback-player",
Tick = 99,
- MoveX = -1f
+ TurnInput = -1f
};
host.MessageManager.SendMessage(moveInput, MessageType.MoveInput);
diff --git a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs
index b8083ba..b74442d 100644
--- a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs
+++ b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs
@@ -33,8 +33,8 @@ namespace Tests.EditMode.Network
Assert.That(stopInput, Is.Not.Null);
Assert.That(stopInput.PlayerId, Is.EqualTo("player-1"));
Assert.That(stopInput.Tick, Is.EqualTo(8));
- Assert.That(stopInput.MoveX, Is.EqualTo(0f));
- Assert.That(stopInput.MoveY, Is.EqualTo(0f));
+ Assert.That(stopInput.TurnInput, Is.EqualTo(0f));
+ Assert.That(stopInput.ThrottleInput, Is.EqualTo(0f));
Assert.That(continuedIdle, Is.False);
Assert.That(idleInput, Is.Null);
}
@@ -85,9 +85,9 @@ namespace Tests.EditMode.Network
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
{
var buffer = new ClientPredictionBuffer();
- buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f });
- buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, MoveX = 1f });
- buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, MoveX = 1f });
+ buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
+ buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = 1f });
+ buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f });
var accepted = buffer.TryApplyAuthoritativeState(
new PlayerState { PlayerId = "player-1", Tick = 11 },
@@ -104,7 +104,7 @@ namespace Tests.EditMode.Network
public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored()
{
var buffer = new ClientPredictionBuffer();
- buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f });
+ buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f });
buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _);
var accepted = buffer.TryApplyAuthoritativeState(
@@ -139,7 +139,7 @@ namespace Tests.EditMode.Network
Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f)));
Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f)));
Assert.That(snapshot.Rotation, Is.EqualTo(90f));
- Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(90f).Within(0.01f));
+ Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(0f).Within(0.01f));
Assert.That(snapshot.Hp, Is.EqualTo(73));
}
@@ -413,13 +413,13 @@ namespace Tests.EditMode.Network
});
transport.EmitReceive(
- BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, MoveX = 1f }),
+ BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, ThrottleInput = 1f }),
peerA);
transport.EmitReceive(
- BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, MoveX = -1f }),
+ BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, ThrottleInput = -1f }),
peerA);
transport.EmitReceive(
- BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, MoveY = 1f }),
+ BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, TurnInput = 1f }),
peerB);
Assert.That(handledTicksByPeer[peerA.ToString()], Is.EqualTo(new long[] { 5 }));