using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Mirror; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.Events; using UnityEngine.Pool; namespace PrefsGUI.Sync { /// /// Spawn時のデータが巨大になる場合に分割して送信するNetworkBehaviour /// public abstract class NetworkBehaviourDivideSpawnData : NetworkBehaviour { #region Server parameters public bool enableDivideSpawnData; public int spawnDataBytesPerChunk = 100000; // すでに分割Spawnを開始したコネクションのリスト private readonly HashSet _spawnedConnections = new(); #endregion #region Client paramters private int _lastReceivedSyncObjectIndex = -1; #endregion public bool debugLog; public UnityEvent onSpawnFinished; private bool _isSpawnFinished; // Spawnデータを分割するSyncObjectのリスト // サーバーとクライアントで同じものを指定している必要がある // SerializeAll/DeserializeAllでは無視しTargetRpcで別途データを分割送信する protected abstract IEnumerable DivideTargetSyncObjects { get; } public virtual bool IsSpawnFinished { get => _isSpawnFinished; protected set { if ( _isSpawnFinished == value ) return; _isSpawnFinished = value; if ( _isSpawnFinished ) { onSpawnFinished?.Invoke(); } } } public override void OnStartServer() { base.OnStartServer(); IsSpawnFinished = true; } public override void OnStartClient() { base.OnStartClient(); if (!enableDivideSpawnData) { IsSpawnFinished = true; } } private void CheckNewConnection(ulong divideTargetMask) { if (divideTargetMask == 0) return; Assert.IsTrue(syncDirection == SyncDirection.ServerToClient); using var _ = ListPool.Get(out var newConnections); newConnections.AddRange(netIdentity.observers.Values .Where(connection => connection is not LocalConnectionToClient) .Except(_spawnedConnections)); if (newConnections.Count == 0) return; foreach (var connection in newConnections) { StartSendSpawnData(connection, divideTargetMask); _spawnedConnections.Add(connection); } } private void StartSendSpawnData(NetworkConnectionToClient connection, ulong divideTargetMask) { StartCoroutine(SendSpawnDataCoroutine(connection, divideTargetMask)); } private IEnumerator SendSpawnDataCoroutine(NetworkConnectionToClient connection, ulong divideTargetMask) { for (var syncObjectIndex = 0; syncObjectIndex < syncObjects.Count; ++syncObjectIndex) { if ((divideTargetMask & (1ul << syncObjectIndex)) == 0) continue; var targetSyncObject = syncObjects[syncObjectIndex]; // すべてをNetworkWriterに書き出してから分割して送信 using var writer = NetworkWriterPool.Get(); targetSyncObject.OnSerializeAll(writer); var bytes = writer.ToArraySegment(); for(var sendPosition = 0; sendPosition < bytes.Count; sendPosition += spawnDataBytesPerChunk) { // 即送信はダメ // NetworkBehaviour.OnSerialize()内で呼ばれるがそこでは送信できないっぽい yield return null; var lastSend = sendPosition + spawnDataBytesPerChunk >= bytes.Count; var sendSize = lastSend ? bytes.Count - sendPosition : spawnDataBytesPerChunk; TargetSendDividedSpawnData(connection, syncObjectIndex, bytes.Slice(sendPosition, sendSize), lastSend ); if (debugLog) { DebugLog($"SendSpawnData syncObjectIndex:{syncObjectIndex} {sendPosition}/{bytes.Count} lastSend:{lastSend}"); } } } } [TargetRpc] private void TargetSendDividedSpawnData(NetworkConnectionToClient target, int syncObjectIndex, ArraySegment bytes, bool isLast) { if (debugLog) { DebugLog($"ReceiveSpawnData syncObjectIndex:{syncObjectIndex} size:{bytes.Count} isLast:{isLast}"); } if ( _lastReceivedSyncObjectIndex >= 0 && _lastReceivedSyncObjectIndex != syncObjectIndex) { Debug.LogWarning($"Received SyncObjectIndex is not match. Expected: {_lastReceivedSyncObjectIndex}, Received: {syncObjectIndex}"); return; } _lastReceivedSyncObjectIndex = syncObjectIndex; if (syncObjects[syncObjectIndex] is not DividedSpawnDataReceiverSyncObject receiverSyncObject) { Debug.LogWarning($"SyncObject is not DummySyncObjectDivideSpawnData. Index: {syncObjectIndex}"); return; } receiverSyncObject.AddDividedSpawnData(bytes); if (isLast) { receiverSyncObject.ApplyStockData(); syncObjects[syncObjectIndex] = receiverSyncObject.baseSyncObject; // すべてのSyncObjectが終わったか確認 if (syncObjects.All(syncObject => syncObject is not DividedSpawnDataReceiverSyncObject)) { IsSpawnFinished = true; } } } #region override OnSerialize/OnDeserialize public override void OnSerialize(NetworkWriter writer, bool initialState) { if (debugLog) { DebugLog($"{nameof(OnSerialize)} initialState:{initialState}"); } if (!enableDivideSpawnData) { base.OnSerialize(writer, initialState); return; } if (!initialState) { base.OnSerialize(writer, false); } else { var divideTargetMask = DivideTargetSyncObjects .Aggregate(0ul, (current, divideTargetSyncObject) => current | 1ul << syncObjects.IndexOf(divideTargetSyncObject)); SerializeObjectsAllWithMask(writer, divideTargetMask); SerializeSyncVars(writer, true); CheckNewConnection(divideTargetMask); } } private void SerializeObjectsAllWithMask(NetworkWriter writer, ulong divideTargetMask) { Compression.CompressVarUInt(writer, divideTargetMask); for (var i = 0; i < syncObjects.Count; i++) { if ((divideTargetMask & (1ul << i)) == 0) { syncObjects[i].OnSerializeAll(writer); } } } public override void OnDeserialize(NetworkReader reader, bool initialState) { if (debugLog) { DebugLog($"{nameof(OnDeserialize)} initialState:{initialState}"); } if (!enableDivideSpawnData) { base.OnDeserialize(reader, initialState); return; } if (!initialState) { base.OnDeserialize(reader, false); } else { DeserializeObjectsAllWithMask(reader); DeserializeSyncVars(reader, true); } } private void DeserializeObjectsAllWithMask(NetworkReader reader) { var divideTargetMask = Compression.DecompressVarUInt(reader); for (var i = 0; i < syncObjects.Count; i++) { if ((divideTargetMask & (1ul << i)) == 0) { syncObjects[i].OnDeserializeAll(reader); } // DivideTargetはダミーに入れ替える else { // syncObjects[i]がDividedSpawnDataReceiverSyncObjectでない場合は元に戻す // 分割Spawnデータ受信中にあらたにSpawnされた場合 var baseSyncObject = syncObjects[i]; if (baseSyncObject is DividedSpawnDataReceiverSyncObject receiverSyncObject) { baseSyncObject = receiverSyncObject.baseSyncObject; receiverSyncObject.Dispose(); } syncObjects[i] = new DividedSpawnDataReceiverSyncObject(baseSyncObject); } } IsSpawnFinished = divideTargetMask == 0; } #endregion protected virtual void DebugLog(string message) { Debug.Log($"FrameCount[{Time.frameCount}]: {message}"); } } }