Init
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public class MasterManager : MonoBehaviour
|
||||
{
|
||||
public static MasterManager Instance;
|
||||
private readonly Dictionary<string, Player> _players = new();
|
||||
public string LocalPlayerId { get; set; }
|
||||
|
||||
[SerializeField] private GameObject _playerPrefab;
|
||||
[SerializeField] private Transform _playerParent;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
Application.targetFrameRate = 60;
|
||||
}
|
||||
|
||||
public void InitPlayersState(LoginResponse response)
|
||||
{
|
||||
for (int i = 0; i < response.Positions.Count; i++)
|
||||
{
|
||||
string id = response.PlayerId[i];
|
||||
if (_players.ContainsKey(id)) continue;
|
||||
Vector3 pos = response.Positions[i].ToVector3();
|
||||
if (string.IsNullOrEmpty(LocalPlayerId) || LocalPlayerId != id)
|
||||
{
|
||||
RegisterRemotePlayer(id, pos);
|
||||
}
|
||||
else
|
||||
{
|
||||
RegisterLocalPlayer(response.Speed, response.ServerTick);
|
||||
var ui = GameObject.Find("RegisterCanvas");
|
||||
ui.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterLocalPlayer(int speed, long serverTick)
|
||||
{
|
||||
Player player = GameObject.Instantiate(_playerPrefab, _playerParent).GetComponent<Player>();
|
||||
player.LocalInit(LocalPlayerId, speed, serverTick);
|
||||
_players.Add(LocalPlayerId, player);
|
||||
}
|
||||
|
||||
public void RegisterRemotePlayer(string playerId, Vector3 pos)
|
||||
{
|
||||
Player player = GameObject.Instantiate(_playerPrefab, _playerParent).GetComponent<Player>();
|
||||
player.RemoteInit(playerId, pos);
|
||||
_players.Add(playerId, player);
|
||||
}
|
||||
|
||||
public bool UnregisterPlayer(string playerId)
|
||||
{
|
||||
if (_players.TryGetValue(playerId, out var player))
|
||||
{
|
||||
GameObject.Destroy(player.gameObject);
|
||||
_players.Remove(playerId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void MovePlayer(string playerId, PlayerState movement)
|
||||
{
|
||||
if (_players.TryGetValue(playerId, out Player player))
|
||||
{
|
||||
player.SyncPosition(movement);
|
||||
}
|
||||
else Debug.LogWarning("Player not found");
|
||||
}
|
||||
|
||||
public Player GetCurrentPlayer()
|
||||
{
|
||||
return _players[LocalPlayerId];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 709370e59bad4f948b2dbb7225261c70
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,149 @@
|
||||
using System.Collections.Generic;
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public class MovementComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float _sendInterval = 0.05f;
|
||||
private Player _master;
|
||||
private int _speed = 2;
|
||||
[SerializeField] private Rigidbody _rigid;
|
||||
private float _lastSendTime = 0;
|
||||
private bool _isControlled = false;
|
||||
|
||||
private Vector3 _serverPosition;
|
||||
private bool _hasServerState = false;
|
||||
private PlayerState _lastServerState;
|
||||
|
||||
public long Tick { get; private set; } = 0;
|
||||
private long _startTickOffset = 0;
|
||||
private long _currentTickOffset = 0;
|
||||
private readonly List<PlayerInput> _inputBuffer = new List<PlayerInput>();
|
||||
|
||||
private Vector3 _serverPos;
|
||||
private Vector3 _currentPos;
|
||||
private float _lerpTime;
|
||||
[SerializeField] private float _lerpRate = 0.1f;
|
||||
private PlayerInput _cachedInput;
|
||||
|
||||
public void Init(bool isControlled, Player master, int speed = 0, long serverTick = 0)
|
||||
{
|
||||
this._master = master;
|
||||
this._isControlled = isControlled;
|
||||
this._speed = speed;
|
||||
this._startTickOffset = serverTick;
|
||||
_rigid.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
_rigid.isKinematic = !isControlled;
|
||||
_rigid.velocity = Vector3.zero;
|
||||
if (serverTick != 0 && _isControlled) MainUI.Instance.OnStartTickOffsetChanged(serverTick);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
_cachedInput = CaptureInput();
|
||||
|
||||
if (Time.time - _lastSendTime > _sendInterval)
|
||||
{
|
||||
if (_cachedInput != null)
|
||||
{
|
||||
NetworkManager.Instance.SendPlayerInput(_cachedInput);
|
||||
}
|
||||
|
||||
_lastSendTime = Time.time;
|
||||
Tick++;
|
||||
|
||||
MainUI.Instance.OnClientTickChanged(Tick);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
if (_hasServerState)
|
||||
{
|
||||
MainUI.Instance.OnServerPosChanged(_serverPosition);
|
||||
Reconcile(_lastServerState);
|
||||
_hasServerState = false;
|
||||
}
|
||||
|
||||
Simulate(_cachedInput);
|
||||
if (_cachedInput != null && (_inputBuffer.Count == 0 || _inputBuffer[_inputBuffer.Count - 1].Tick != _cachedInput.Tick))
|
||||
{
|
||||
_inputBuffer.Add(_cachedInput);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lerpTime += Time.fixedDeltaTime / 0.05f;
|
||||
_rigid.MovePosition(Vector3.Lerp(_currentPos, _serverPos, _lerpTime));
|
||||
}
|
||||
}
|
||||
|
||||
private void Reconcile(PlayerState state)
|
||||
{
|
||||
_serverPosition = state.Position.ToVector3();
|
||||
_rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate);
|
||||
_rigid.velocity = Vector3.zero;
|
||||
_inputBuffer.RemoveAll(i => i.Tick <= state.Tick);
|
||||
}
|
||||
|
||||
private PlayerInput CaptureInput()
|
||||
{
|
||||
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
|
||||
if (input == Vector3.zero) return null;
|
||||
return new PlayerInput()
|
||||
{
|
||||
PlayerId = _master.PlayerId,
|
||||
Input = ProtoExtensions.ToProtoVector3(input),
|
||||
Tick = Tick
|
||||
};
|
||||
}
|
||||
|
||||
private void Simulate(PlayerInput input)
|
||||
{
|
||||
Vector3 dir = input == null ? Vector3.zero : input.Input.ToVector3();
|
||||
_rigid.velocity = _speed * dir;
|
||||
if (_isControlled)
|
||||
{
|
||||
MainUI.Instance.OnClientPosChanged(_rigid.position);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnServerState(PlayerState state)
|
||||
{
|
||||
if (_isControlled)
|
||||
{
|
||||
_lastServerState = state;
|
||||
_hasServerState = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_serverPos = state.Position.ToVector3();
|
||||
_currentPos = _rigid.position;
|
||||
_lerpTime = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetServerTick(long serverTick)
|
||||
{
|
||||
_currentTickOffset = serverTick - Tick - _startTickOffset;
|
||||
if (_isControlled)
|
||||
{
|
||||
MainUI.Instance.OnServerTickChanged(serverTick);
|
||||
}
|
||||
|
||||
if (_currentTickOffset < 0)
|
||||
{
|
||||
_sendInterval = 0.052f;
|
||||
}
|
||||
if (_currentTickOffset > 0)
|
||||
{
|
||||
_sendInterval = 0.048f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db8117151f564304bae153aa55c0a960
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e8e93705a6701345bb1095d1ba4aadc
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 895d4be03eb18e941b9a0b25d937da97
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13502c02ef6908e46b00ba64f6df5b77
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Network.Defines
|
||||
{
|
||||
public enum MessageType : byte
|
||||
{
|
||||
Unknow = 0,
|
||||
|
||||
// 游戏相关
|
||||
PlayerInput = 1,
|
||||
PlayerState = 2,
|
||||
PlayerAction = 3,
|
||||
GameState = 4,
|
||||
PlayerJoin = 5,
|
||||
PlayerLeave = 6,
|
||||
|
||||
// 聊天相关
|
||||
ChatMessage = 10,
|
||||
PrivateMessage = 11,
|
||||
SystemMessage = 12,
|
||||
|
||||
// 系统相关
|
||||
HeartBeat = 20,
|
||||
|
||||
LoginRequest = 21,
|
||||
LoginResponse = 22,
|
||||
|
||||
LogoutRequest = 23,
|
||||
|
||||
// 房间管理
|
||||
CreateRoom = 30,
|
||||
JoinRoom = 31,
|
||||
LeaveRoom = 32,
|
||||
RoomList = 33,
|
||||
|
||||
Heartbeat = 40,
|
||||
HeartbeatResponse = 41,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1dd2a49fbda33064891c337fadc590ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Network.Defines
|
||||
{
|
||||
public static class ProtoExtensions
|
||||
{
|
||||
public static UnityEngine.Vector3 ToVector3(this Vector3 vec)
|
||||
{
|
||||
return new UnityEngine.Vector3()
|
||||
{
|
||||
x = vec.X,
|
||||
y = vec.Y,
|
||||
z = vec.Z
|
||||
};
|
||||
}
|
||||
|
||||
public static Vector3 ToProtoVector3(UnityEngine.Vector3 vec)
|
||||
{
|
||||
return new Vector3()
|
||||
{
|
||||
X = vec.x,
|
||||
Y = vec.y,
|
||||
Z = vec.z
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fefe99db6b80a43418075799ba64c176
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Network.Defines
|
||||
{
|
||||
public class SystemMessage
|
||||
{
|
||||
public string Content { get; set; }
|
||||
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||
public string Level { get; set; } = "info"; // "info", "warning", "error"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a9d4f06e72ce954598ae990364ac8ac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2abfb5f176e7f5e4c8bf371eb28ae588
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
|
||||
namespace Network.NetworkApplication
|
||||
{
|
||||
public class DelegateMessageHandler : IMessageHandler
|
||||
{
|
||||
private readonly Func<byte[], IPEndPoint, Task> handler;
|
||||
|
||||
public DelegateMessageHandler(Func<byte[], IPEndPoint, Task> handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public DelegateMessageHandler(Action<byte[], IPEndPoint> handler)
|
||||
{
|
||||
this.handler = (msg, sender) =>
|
||||
{
|
||||
handler(msg, sender);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
public Task HandleAsync(byte[] message, IPEndPoint sender)
|
||||
{
|
||||
return handler(message, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c940a6a8da48ccf429215f05e2d32154
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Network.NetworkApplication
|
||||
{
|
||||
public interface IMessageHandler
|
||||
{
|
||||
Task HandleAsync(byte[] message, IPEndPoint sender);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c46d2128446560d409c48949e2636bf1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
using Network.NetworkTransport;
|
||||
|
||||
namespace Network.NetworkApplication
|
||||
{
|
||||
public class MessageManager
|
||||
{
|
||||
private readonly ITransport transport;
|
||||
|
||||
private readonly Dictionary<MessageType, Func<byte[], IPEndPoint, Task>> handlers =
|
||||
new Dictionary<MessageType, Func<byte[], IPEndPoint, Task>>();
|
||||
|
||||
public MessageManager(ITransport transport)
|
||||
{
|
||||
this.transport = transport;
|
||||
this.transport.OnReceive += OnTransportReceiveAsync;
|
||||
}
|
||||
|
||||
public void RegisterHandler(MessageType type, IMessageHandler handler)
|
||||
{
|
||||
handlers[type] = async (payload, sender) => { await handler.HandleAsync(payload, sender); };
|
||||
|
||||
Console.WriteLine($"[MessageManager] 注册处理器:{type}");
|
||||
}
|
||||
|
||||
public void RegisterHandler(MessageType type, Func<byte[], IPEndPoint, Task> handler)
|
||||
{
|
||||
var han = new DelegateMessageHandler(handler);
|
||||
RegisterHandler(type, new DelegateMessageHandler(handler));
|
||||
}
|
||||
|
||||
public void RegisterHandler(MessageType type, Action<byte[], IPEndPoint> handler)
|
||||
{
|
||||
var han = new DelegateMessageHandler((msg, sender) => { handler(msg, sender); });
|
||||
RegisterHandler(type, new DelegateMessageHandler((msg, sender) =>
|
||||
{
|
||||
handler(msg, sender);
|
||||
return Task.CompletedTask;
|
||||
}));
|
||||
}
|
||||
|
||||
public void SendMessage<T>(T message, MessageType type, IPEndPoint target = null) where T : IMessage
|
||||
{
|
||||
var envelope = new Envelope()
|
||||
{
|
||||
Type = (int)type,
|
||||
Payload = message.ToByteString()
|
||||
};
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
transport.SendTo(envelope.ToByteArray(), target);
|
||||
}
|
||||
else
|
||||
{
|
||||
transport.Send(envelope.ToByteArray());
|
||||
}
|
||||
|
||||
Console.WriteLine($"[MessageManager] 发送消息:{type} -> {target?.ToString() ?? "default"}");
|
||||
}
|
||||
|
||||
public void BroadcastMessage<T>(T message, MessageType type) where T : IMessage
|
||||
{
|
||||
Console.WriteLine($"[MessageManager] 广播消息:{type}");
|
||||
var envelope = new Envelope()
|
||||
{
|
||||
Type = (int)type,
|
||||
Payload = message.ToByteString()
|
||||
};
|
||||
transport.SendToAll(envelope.ToByteArray());
|
||||
}
|
||||
|
||||
private async void OnTransportReceiveAsync(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
try
|
||||
{
|
||||
var envelope = Envelope.Parser.ParseFrom(data);
|
||||
var type = (MessageType)envelope.Type;
|
||||
Console.WriteLine($"[MessageManager] 收到消息:{type} 来自 {sender}");
|
||||
|
||||
if (handlers.TryGetValue(type, out var handler))
|
||||
{
|
||||
await handler(envelope.Payload.ToByteArray(), sender);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[MessageManager] 警告:未注册的消息类型 {type}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[MessageManager] 消息处理错误:{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75ac30aeadd168e44a8860b22bd467c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 32f1de5d4a6031049a2033d69eb21595
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Network.NetworkTransport
|
||||
{
|
||||
public class ClientSession
|
||||
{
|
||||
private ITransport _transport;
|
||||
|
||||
private IPEndPoint _remote;
|
||||
|
||||
public long LastActivityTs { get; private set; }
|
||||
|
||||
public uint SendSequenceNumber { get; private set; } = 0;
|
||||
|
||||
private int _currentTicks = 0;
|
||||
|
||||
private int _nextSendTicks = 0;
|
||||
|
||||
private int _sendInterval = 10;
|
||||
|
||||
// 重传时间 5s
|
||||
private long _retransmitTicks = 5000;
|
||||
|
||||
// 上层交付
|
||||
private readonly LinkedList<Packet> _sendQueue = new LinkedList<Packet>();
|
||||
|
||||
// 已发送但未确认
|
||||
private readonly LinkedList<Packet> _sendBuffer = new LinkedList<Packet>();
|
||||
|
||||
// 已收到但乱序
|
||||
private readonly LinkedList<Packet> _receiveBuffer = new LinkedList<Packet>();
|
||||
|
||||
// 已收到可交付
|
||||
private readonly LinkedList<Packet> _receiveQueue = new LinkedList<Packet>();
|
||||
|
||||
private bool _hasReceived = false;
|
||||
|
||||
private uint _expectedAck = 0;
|
||||
|
||||
private readonly object _lockObj = new object();
|
||||
|
||||
public ClientSession(ITransport transport, IPEndPoint remote)
|
||||
{
|
||||
_transport = transport;
|
||||
_remote = remote;
|
||||
LastActivityTs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
}
|
||||
|
||||
public uint GetExpectedAck() => _expectedAck;
|
||||
|
||||
public void SetSendInterval(int interval) => _sendInterval = interval;
|
||||
|
||||
private uint GetNextSendSequence()
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
return SendSequenceNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
public void Tick(int currentTicks)
|
||||
{
|
||||
_currentTicks = currentTicks;
|
||||
if (_currentTicks >= _nextSendTicks)
|
||||
{
|
||||
_nextSendTicks = currentTicks + _sendInterval;
|
||||
SendPacketInternal();
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPacket(byte[] data)
|
||||
{
|
||||
_sendQueue.AddLast(Packet.CreateDataPacket(GetNextSendSequence(), data));
|
||||
}
|
||||
|
||||
public List<Packet> ReceivePackets()
|
||||
{
|
||||
var list = new List<Packet>();
|
||||
lock (_lockObj)
|
||||
{
|
||||
while (_receiveQueue.Count > 0)
|
||||
{
|
||||
var packet = _receiveQueue.First.Value;
|
||||
|
||||
list.Add(packet);
|
||||
_receiveQueue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void SendPacketInternal()
|
||||
{
|
||||
if (_hasReceived)
|
||||
{
|
||||
var packet = Packet.CreateAckPacket(_expectedAck);
|
||||
_sendBuffer.AddLast(packet);
|
||||
var bytes = packet.ToBytes();
|
||||
_transport.SendTo(bytes, _remote);
|
||||
_hasReceived = false;
|
||||
}
|
||||
|
||||
foreach (var packet in _receiveBuffer)
|
||||
{
|
||||
if (_currentTicks - packet.Timestamp > _retransmitTicks)
|
||||
{
|
||||
var bytes = packet.ToBytes();
|
||||
_transport.SendTo(bytes, _remote);
|
||||
}
|
||||
else break;
|
||||
}
|
||||
|
||||
while (_sendQueue.Count > 0)
|
||||
{
|
||||
var packet = _sendQueue.First.Value;
|
||||
_sendBuffer.AddLast(packet);
|
||||
var bytes = packet.ToBytes();
|
||||
_transport.SendTo(bytes, _remote);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReceivePacketsInternal(Packet packet)
|
||||
{
|
||||
uint seq = packet.SequenceNumber;
|
||||
|
||||
// 是否是按序到达的包
|
||||
if (seq == _expectedAck)
|
||||
{
|
||||
_receiveQueue.AddLast(packet);
|
||||
while (_receiveBuffer.Count > 0)
|
||||
{
|
||||
var pendingPacket = _receiveBuffer.First.Value;
|
||||
if (seq != pendingPacket.SequenceNumber) break;
|
||||
seq++;
|
||||
_receiveQueue.AddLast(pendingPacket);
|
||||
_receiveBuffer.RemoveFirst();
|
||||
}
|
||||
|
||||
_expectedAck = seq + 1;
|
||||
_hasReceived = true;
|
||||
}
|
||||
// 将包按顺序追加在 receivingPackets 后面
|
||||
else
|
||||
{
|
||||
var firstNode = _receiveBuffer.First;
|
||||
while (firstNode.Next != null)
|
||||
{
|
||||
if (firstNode.Value.SequenceNumber > seq)
|
||||
{
|
||||
var node = new LinkedListNode<Packet>(packet);
|
||||
_receiveBuffer.AddBefore(firstNode, node);
|
||||
break;
|
||||
}
|
||||
|
||||
firstNode = firstNode.Next;
|
||||
}
|
||||
|
||||
if (firstNode == null) _receiveBuffer.AddLast(packet);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool TryProcessReceiveSequence(uint sequenceNumber, out bool shouldDeliver)
|
||||
{
|
||||
lock (_lockObj)
|
||||
{
|
||||
LastActivityTs = DateTime.Now;
|
||||
|
||||
if (sequenceNumber == _expectedAck)
|
||||
{
|
||||
_expectedAck++;
|
||||
_receivedSequences.Add(sequenceNumber);
|
||||
shouldDeliver = true;
|
||||
return true;
|
||||
}
|
||||
else if (sequenceNumber < _expectedAck)
|
||||
{
|
||||
shouldDeliver = false;
|
||||
return _receivedSequences.Contains(sequenceNumber);
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldDeliver = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ec6c25bc42967db499742dfa355380b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Network.NetworkTransport
|
||||
{
|
||||
public interface ITransport
|
||||
{
|
||||
void SendTo(byte[] data, IPEndPoint target);
|
||||
void SendToAll(byte[] data);
|
||||
event Action<byte[], IPEndPoint> OnReceive;
|
||||
Task StartAsync();
|
||||
void Stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc400a702b75abc40bb454eb27a34249
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Network.NetworkTransport
|
||||
{
|
||||
public enum PacketType : byte
|
||||
{
|
||||
Data = 1,
|
||||
Ack = 2,
|
||||
}
|
||||
|
||||
public struct Packet
|
||||
{
|
||||
public PacketType Type;
|
||||
public uint SequenceNumber;
|
||||
public byte[] Data;
|
||||
public long Timestamp;
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var result = new byte[1 + 4 + 8 + Data.Length];
|
||||
result[0] = (byte)Type;
|
||||
BitConverter.GetBytes(SequenceNumber).CopyTo(result, 1);
|
||||
BitConverter.GetBytes(Timestamp).CopyTo(result, 5);
|
||||
Data.CopyTo(result, 13);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Packet FromBytes(byte[] data)
|
||||
{
|
||||
return new Packet
|
||||
{
|
||||
Type = (PacketType)data[0],
|
||||
SequenceNumber = BitConverter.ToUInt32(data, 1),
|
||||
Timestamp = BitConverter.ToInt64(data, 5),
|
||||
Data = new ArraySegment<byte>(data, 5, data.Length - 5).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
public static Packet CreateDataPacket(uint seqNum, byte[] data)
|
||||
{
|
||||
return new Packet
|
||||
{
|
||||
Type = PacketType.Data,
|
||||
SequenceNumber = seqNum,
|
||||
Data = data,
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
}
|
||||
|
||||
public static Packet CreateAckPacket(uint seqNum)
|
||||
{
|
||||
return new Packet
|
||||
{
|
||||
Type = PacketType.Ack,
|
||||
SequenceNumber = seqNum,
|
||||
Data = Array.Empty<byte>(),
|
||||
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b84a2cb7ffe3cd14180358559e526dbe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,285 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Network.NetworkTransport
|
||||
{
|
||||
public class ReliableUdpTransport : ITransport
|
||||
{
|
||||
private readonly UdpClient _client;
|
||||
private readonly IPEndPoint _defaultRemoteEndPoint;
|
||||
private readonly bool _isServer;
|
||||
|
||||
private readonly List<ClientSession> _sessions = new();
|
||||
|
||||
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<byte[], IPEndPoint> OnReceive;
|
||||
|
||||
private Task _receiveTask;
|
||||
|
||||
// 构造函数——服务端模式
|
||||
public ReliableUdpTransport(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 ReliableUdpTransport(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] 传输层启动");
|
||||
|
||||
// 开始接收数据
|
||||
_receiveTask = ReceiveLoop();
|
||||
await Task.Delay(100); // 给接收循环一点启动时间
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
{
|
||||
foreach (var session in _sessions)
|
||||
{
|
||||
session.Tick(DateTime.UtcNow.Millisecond);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_isRunning = false;
|
||||
_retransmitTimer.Dispose();
|
||||
_cleanupTimer.Dispose();
|
||||
_client.Close();
|
||||
_sessions.Clear();
|
||||
Console.WriteLine("[Transport] 传输层停止");
|
||||
}
|
||||
|
||||
public async void SendTo(Packet packet, IPEndPoint target)
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = packet.ToBytes();
|
||||
await _client.SendAsync(bytes, bytes.Length, target);
|
||||
|
||||
Console.WriteLine($"[Transport] 发送数据包到 {target}");
|
||||
}
|
||||
|
||||
public void SendToAll(byte[] data)
|
||||
{
|
||||
foreach (var session in _sessions)
|
||||
{
|
||||
session.SendPacket(data);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CleanupSessions(object state)
|
||||
{
|
||||
if (!_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var toRemove = new List<string>();
|
||||
|
||||
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)
|
||||
{
|
||||
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.GetExpectedAck()},待确认: {session.PendingAcks.Count}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d26d19f5e4031fd4089d620dc62d5159
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using Network.NetworkTransport;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
public class NetworkManager : MonoBehaviour
|
||||
{
|
||||
public static NetworkManager Instance;
|
||||
private ReliableUdpTransport _transport;
|
||||
private MessageManager _messageManager;
|
||||
private IPEndPoint _serverPoint;
|
||||
private uint _sequence = 0;
|
||||
[SerializeField] private GameObject _wrongWindow;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
StartCoroutine(InitNetwork());
|
||||
}
|
||||
|
||||
private IEnumerator InitNetwork()
|
||||
{
|
||||
_transport = new ReliableUdpTransport("127.0.0.1", 8080);
|
||||
yield return _transport.StartAsync();
|
||||
_messageManager = new MessageManager(_transport);
|
||||
RegisterHandler();
|
||||
StartCoroutine(Heartbeat());
|
||||
}
|
||||
|
||||
private IEnumerator Heartbeat()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
if (_serverPoint != null)
|
||||
{
|
||||
var heartbeat = new Heartbeat();
|
||||
_messageManager.SendMessage(heartbeat, MessageType.Heartbeat, _serverPoint);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterHandler()
|
||||
{
|
||||
_messageManager.RegisterHandler(MessageType.LoginResponse, HandleLoginResponse);
|
||||
_messageManager.RegisterHandler(MessageType.PlayerState, HandlePlayerState);
|
||||
_messageManager.RegisterHandler(MessageType.HeartbeatResponse, HandleHeartbeatResponse);
|
||||
_messageManager.RegisterHandler(MessageType.LogoutRequest, HandleLogoutRequest);
|
||||
_messageManager.RegisterHandler(MessageType.PlayerJoin, HandlePlayerJoin);
|
||||
}
|
||||
|
||||
private void HandleLoginResponse(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
var response = LoginResponse.Parser.ParseFrom(data);
|
||||
_serverPoint = sender;
|
||||
if (response.Result)
|
||||
{
|
||||
MasterManager.Instance.InitPlayersState(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
_wrongWindow.SetActive(true);
|
||||
Debug.LogError("UserId 已经存在");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePlayerState(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
var message = PlayerState.Parser.ParseFrom(data);
|
||||
MasterManager.Instance.MovePlayer(message.PlayerId, message);
|
||||
Debug.Log($"收到PlayerState::PlayerID={message.PlayerId},Position=" + message.Position.ToVector3().ToString());
|
||||
}
|
||||
|
||||
private void HandleHeartbeatResponse(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
var response = HeartbeatResponse.Parser.ParseFrom(data);
|
||||
var player = MasterManager.Instance.GetCurrentPlayer();
|
||||
if (player != null)
|
||||
{
|
||||
player.SyncTick(response.ServerTick);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLogoutRequest(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
var request = LogoutRequest.Parser.ParseFrom(data);
|
||||
MasterManager.Instance.UnregisterPlayer(request.PlayerId);
|
||||
}
|
||||
|
||||
private void HandlePlayerJoin(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
var playerJoin = PlayerJoin.Parser.ParseFrom(data);
|
||||
if (MasterManager.Instance.LocalPlayerId == playerJoin.PlayerId) return;
|
||||
MasterManager.Instance.RegisterRemotePlayer(playerJoin.PlayerId, playerJoin.Position.ToVector3());
|
||||
}
|
||||
|
||||
public void SendPlayerInput(string playerId, Vector3 input)
|
||||
{
|
||||
var message = new PlayerInput()
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Input = ProtoExtensions.ToProtoVector3(input)
|
||||
};
|
||||
_messageManager.SendMessage(message, MessageType.PlayerInput);
|
||||
Debug.Log($"PlayerMoveSeq: {_sequence++}");
|
||||
}
|
||||
|
||||
public void SendPlayerInput(PlayerInput message)
|
||||
{
|
||||
_messageManager.SendMessage(message, MessageType.PlayerInput);
|
||||
Debug.Log($"PlayerMoveSeq: {_sequence++}");
|
||||
}
|
||||
|
||||
public void SendLoginRequest(string playerId, int speed)
|
||||
{
|
||||
var request = new LoginRequest()
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Speed = speed
|
||||
};
|
||||
_messageManager.SendMessage(request, MessageType.LoginRequest);
|
||||
Debug.Log($"Sent login request to player {playerId}");
|
||||
}
|
||||
|
||||
public void SendLogoutRequest(string playerId)
|
||||
{
|
||||
var request = new LogoutRequest()
|
||||
{
|
||||
PlayerId = playerId
|
||||
};
|
||||
_messageManager.SendMessage(request, MessageType.LogoutRequest);
|
||||
Debug.Log($"Sent logout request to player {playerId}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 816171fec883e2940ad0a89df5294066
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class OfflineMovementComponent : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int _speed;
|
||||
[SerializeField] private Rigidbody rigid;
|
||||
private Vector3 _cachedInput;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
_cachedInput = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
rigid.velocity = _cachedInput * _speed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf8f141cd490efa4aa23369000c805db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,60 @@
|
||||
using Network.Defines;
|
||||
using UnityEngine;
|
||||
|
||||
public class Player : MonoBehaviour
|
||||
{
|
||||
//[SerializeField] private float _moveSpeed = 10f;
|
||||
public string PlayerId { get; private set; } = "1001";
|
||||
[SerializeField] private MeshRenderer _meshRenderer;
|
||||
[SerializeField] private Material[] _materials;
|
||||
[SerializeField] private Camera _camera;
|
||||
[SerializeField] private MovementComponent _movement;
|
||||
[SerializeField] private PlayerUI _playerUI;
|
||||
[SerializeField] private bool _isControlled;
|
||||
|
||||
public void LocalInit(string playerId, int speed, long serverTick)
|
||||
{
|
||||
this.PlayerId = playerId;
|
||||
this._isControlled = true;
|
||||
|
||||
int idx = Random.Range(0, _materials.Length);
|
||||
_meshRenderer.material = _materials[idx];
|
||||
|
||||
_playerUI.Init(this);
|
||||
_movement.Init(true, this, speed, serverTick);
|
||||
}
|
||||
|
||||
public void RemoteInit(string playerId, UnityEngine.Vector3 pos)
|
||||
{
|
||||
this.PlayerId = playerId;
|
||||
this._isControlled = false;
|
||||
|
||||
int idx = Random.Range(0, _materials.Length);
|
||||
_meshRenderer.material = _materials[idx];
|
||||
|
||||
Destroy(_camera.gameObject);
|
||||
this.transform.position = pos;
|
||||
|
||||
_playerUI.Init(this);
|
||||
_movement.Init(false, this);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.PlayerId) && _isControlled)
|
||||
{
|
||||
NetworkManager.Instance.SendLogoutRequest(this.PlayerId);
|
||||
}
|
||||
}
|
||||
|
||||
public void SyncPosition(PlayerState movement)
|
||||
{
|
||||
if (this._movement == null) return;
|
||||
_movement.OnServerState(movement);
|
||||
}
|
||||
|
||||
public void SyncTick(long serverTick)
|
||||
{
|
||||
_movement.SetServerTick(serverTick);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef97bb6040fda094b8aeab1bff8f13f8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6fd6ab7e60cf1f248837c7096ee1e6f3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class MainUI : MonoBehaviour
|
||||
{
|
||||
public static MainUI Instance;
|
||||
|
||||
[SerializeField] private Text _serverPositionText;
|
||||
[SerializeField] private Text _clientPositionText;
|
||||
[SerializeField] private Text _serverTickText;
|
||||
[SerializeField] private Text _startTickOffsetText;
|
||||
[SerializeField] private Text _clientTickText;
|
||||
|
||||
public UnityAction<Vector3> OnServerPosChanged;
|
||||
public UnityAction<Vector3> OnClientPosChanged;
|
||||
public UnityAction<long> OnServerTickChanged;
|
||||
public UnityAction<long> OnStartTickOffsetChanged;
|
||||
public UnityAction<long> OnClientTickChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
OnServerPosChanged += UpdateServerPositionText;
|
||||
OnClientPosChanged += UpdateClientPositionText;
|
||||
OnServerTickChanged += UpdateServerTickText;
|
||||
OnClientTickChanged += UpdateClientTickText;
|
||||
OnStartTickOffsetChanged += UpdateStartTickOffsetText;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OnServerPosChanged -= UpdateServerPositionText;
|
||||
OnClientPosChanged -= UpdateClientPositionText;
|
||||
OnServerTickChanged -= UpdateServerTickText;
|
||||
OnClientTickChanged -= UpdateClientTickText;
|
||||
OnStartTickOffsetChanged -= UpdateStartTickOffsetText;
|
||||
}
|
||||
|
||||
private void UpdateServerPositionText(Vector3 pos)
|
||||
{
|
||||
_serverPositionText.text = "服务端位置:" + pos.ToString();
|
||||
}
|
||||
|
||||
private void UpdateClientPositionText(Vector3 pos)
|
||||
{
|
||||
_clientPositionText.text = "客户端位置:" + pos.ToString();
|
||||
}
|
||||
|
||||
private void UpdateServerTickText(long tick)
|
||||
{
|
||||
_serverTickText.text = "服务器Tick:" + tick;
|
||||
}
|
||||
|
||||
private void UpdateStartTickOffsetText(long tick)
|
||||
{
|
||||
_startTickOffsetText.text = "初始Tick差:" + tick;
|
||||
}
|
||||
|
||||
private void UpdateClientTickText(long tick)
|
||||
{
|
||||
_clientTickText.text = "客户端Tick:" + tick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 747bc5518e5dcfd429160d8425ae5200
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,44 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class PlayerUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Canvas _canvas;
|
||||
[SerializeField] private Text _text;
|
||||
private Player _master;
|
||||
private Camera _mainCamera;
|
||||
private bool _isVisible = true;
|
||||
|
||||
public void Init(Player master)
|
||||
{
|
||||
_canvas = this.transform.GetComponent<Canvas>();
|
||||
_mainCamera = Camera.main;
|
||||
this._master = master;
|
||||
this._text.text = _master.PlayerId;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (_isVisible)
|
||||
{
|
||||
if (_mainCamera == null)
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
if (_mainCamera != null)
|
||||
{
|
||||
_canvas.transform.LookAt(_mainCamera.transform);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBecameVisible()
|
||||
{
|
||||
_isVisible = true;
|
||||
}
|
||||
|
||||
private void OnBecameInvisible()
|
||||
{
|
||||
_isVisible = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03b1c6f9985e8ee43a27a9dfbafa1de8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class StartUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private InputField _playerIdInputField;
|
||||
[SerializeField] private InputField _speedInputField;
|
||||
[SerializeField] private Button _button;
|
||||
|
||||
|
||||
public void Login()
|
||||
{
|
||||
if (_playerIdInputField == null || _speedInputField == null)
|
||||
{
|
||||
Debug.LogError("_inputField is null");
|
||||
return;
|
||||
}
|
||||
|
||||
string id = _playerIdInputField.text;
|
||||
int speed = Convert.ToInt32(_speedInputField.text);
|
||||
MasterManager.Instance.LocalPlayerId = id;
|
||||
NetworkManager.Instance.SendLoginRequest(id, speed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8070d0d289bda324c9d3fd88c0582a53
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
enum CardState
|
||||
{
|
||||
Cooling,
|
||||
Ready,
|
||||
WaitingSun
|
||||
}
|
||||
|
||||
public class Card : MonoBehaviour
|
||||
{
|
||||
private CardState cardState = CardState.Cooling;
|
||||
|
||||
public GameObject sunflower;
|
||||
public GameObject sunflowerGray;
|
||||
public Image sunflowerMask;
|
||||
|
||||
|
||||
private void Update()
|
||||
{
|
||||
switch (cardState)
|
||||
{
|
||||
case CardState.Cooling:
|
||||
CoolingUpdate();
|
||||
break;
|
||||
case CardState.Ready:
|
||||
ReadyUpdate();
|
||||
break;
|
||||
case CardState.WaitingSun:
|
||||
WaitingSunUpdate();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void CoolingUpdate()
|
||||
{
|
||||
|
||||
}
|
||||
void ReadyUpdate()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void WaitingSunUpdate()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eff1752caaabe784fb85775608605426
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user