online fix
This commit is contained in:
@@ -13,7 +13,7 @@ MonoBehaviour:
|
||||
m_Name: BossSchedule
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Game.Scripts.Runtime.Data.BossSchedule
|
||||
events:
|
||||
- TimeSinceStart: 30
|
||||
- TimeSinceStart: 1000
|
||||
Boss: {fileID: 11400000, guid: 1bc4888fa172eb99f94756653be6c1ed, type: 2}
|
||||
Count: 1
|
||||
useSpawnRadiusOverride: 0
|
||||
|
||||
@@ -17,10 +17,10 @@ MonoBehaviour:
|
||||
projectilePrefab: {fileID: -6920969466594260193, guid: 6703b124cb13a577c8aae6a4851d0274, type: 3}
|
||||
projectileSpeed: 18
|
||||
projectileLifetime: 5
|
||||
shotsPerSecond: 1
|
||||
baseDamage: 10
|
||||
shotsPerSecond: 2
|
||||
baseDamage: 50
|
||||
range: 25
|
||||
projectilesPerShot: 1
|
||||
projectilesPerShot: 2
|
||||
spreadAngle: 0
|
||||
hitMask:
|
||||
serializedVersion: 2
|
||||
|
||||
79
Game/Scripts/Networking/EnemyDefinitionRegistry.cs
Normal file
79
Game/Scripts/Networking/EnemyDefinitionRegistry.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Game.Scripts.Runtime.Data;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
internal static class EnemyDefinitionRegistry
|
||||
{
|
||||
private static readonly Dictionary<string, EnemyDefinition> DefinitionsById = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<string, EnemyDefinition> DefinitionsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal static void Register(EnemyDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var id = NormalizeId(definition);
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
DefinitionsById[id] = definition;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(definition.name))
|
||||
{
|
||||
DefinitionsByName[definition.name] = definition;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGet(string idOrName, out EnemyDefinition definition)
|
||||
{
|
||||
definition = null;
|
||||
if (string.IsNullOrWhiteSpace(idOrName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (DefinitionsById.TryGetValue(idOrName, out definition))
|
||||
{
|
||||
return definition != null;
|
||||
}
|
||||
|
||||
if (DefinitionsByName.TryGetValue(idOrName, out definition))
|
||||
{
|
||||
return definition != null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string ResolveId(EnemyDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var id = NormalizeId(definition);
|
||||
Register(definition);
|
||||
return id;
|
||||
}
|
||||
|
||||
private static string NormalizeId(EnemyDefinition definition)
|
||||
{
|
||||
if (definition == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(definition.Id))
|
||||
{
|
||||
return definition.Id;
|
||||
}
|
||||
|
||||
return definition.name ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Game/Scripts/Networking/EnemyDefinitionRegistry.cs.meta
Normal file
2
Game/Scripts/Networking/EnemyDefinitionRegistry.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe91b8e8bcd3c2b8cb3f037428048c0b
|
||||
@@ -12,7 +12,10 @@ namespace MegaKoop.Game.Networking
|
||||
WeaponFire = 4,
|
||||
HealthSync = 5,
|
||||
ProjectileSpawn = 6,
|
||||
ProjectileImpact = 7
|
||||
ProjectileImpact = 7,
|
||||
GameState = 8,
|
||||
EnemySpawn = 9,
|
||||
EnemyDespawn = 10
|
||||
}
|
||||
|
||||
public enum ProjectileImpactKind : byte
|
||||
@@ -252,4 +255,139 @@ namespace MegaKoop.Game.Networking
|
||||
return new ProjectileImpactMessage(kind, position, normal);
|
||||
}
|
||||
}
|
||||
|
||||
public enum GameStateEvent : byte
|
||||
{
|
||||
Started = 0,
|
||||
Paused = 1,
|
||||
Resumed = 2,
|
||||
Stopped = 3,
|
||||
WaveAdvanced = 4,
|
||||
BossSpawned = 5,
|
||||
Heartbeat = 6
|
||||
}
|
||||
|
||||
public readonly struct GameStateMessage
|
||||
{
|
||||
public readonly GameStateEvent Event;
|
||||
public readonly float Elapsed;
|
||||
public readonly int CurrentWave;
|
||||
public readonly int PreviousWave;
|
||||
public readonly bool IsPaused;
|
||||
public readonly bool IsRunning;
|
||||
public readonly string DefinitionId;
|
||||
public readonly int Count;
|
||||
|
||||
public GameStateMessage(GameStateEvent evt, float elapsed, int currentWave, int previousWave, bool isPaused, bool isRunning, string definitionId, int count)
|
||||
{
|
||||
Event = evt;
|
||||
Elapsed = elapsed;
|
||||
CurrentWave = currentWave;
|
||||
PreviousWave = previousWave;
|
||||
IsPaused = isPaused;
|
||||
IsRunning = isRunning;
|
||||
DefinitionId = definitionId ?? string.Empty;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(GameStateMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write((byte)message.Event);
|
||||
writer.Write(message.Elapsed);
|
||||
writer.Write(message.CurrentWave);
|
||||
writer.Write(message.PreviousWave);
|
||||
writer.Write(message.IsPaused);
|
||||
writer.Write(message.IsRunning);
|
||||
writer.Write(message.DefinitionId ?? string.Empty);
|
||||
writer.Write(message.Count);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static GameStateMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
GameStateEvent evt = (GameStateEvent)reader.ReadByte();
|
||||
float elapsed = reader.ReadFloat();
|
||||
int currentWave = reader.ReadInt();
|
||||
int previousWave = reader.ReadInt();
|
||||
bool isPaused = reader.ReadBool();
|
||||
bool isRunning = reader.ReadBool();
|
||||
string definitionId = reader.ReadString();
|
||||
int count = reader.ReadInt();
|
||||
return new GameStateMessage(evt, elapsed, currentWave, previousWave, isPaused, isRunning, definitionId, count);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EnemySpawnMessage
|
||||
{
|
||||
public readonly string DefinitionId;
|
||||
public readonly Vector3 Position;
|
||||
public readonly Quaternion Rotation;
|
||||
public readonly int NetworkId;
|
||||
public readonly bool IsBoss;
|
||||
public readonly int WaveIndex;
|
||||
public readonly float Timestamp;
|
||||
|
||||
public EnemySpawnMessage(string definitionId, Vector3 position, Quaternion rotation, int networkId, bool isBoss, int waveIndex, float timestamp)
|
||||
{
|
||||
DefinitionId = definitionId ?? string.Empty;
|
||||
Position = position;
|
||||
Rotation = rotation;
|
||||
NetworkId = networkId;
|
||||
IsBoss = isBoss;
|
||||
WaveIndex = waveIndex;
|
||||
Timestamp = timestamp;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(EnemySpawnMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.DefinitionId ?? string.Empty);
|
||||
writer.Write(message.Position);
|
||||
writer.Write(message.Rotation);
|
||||
writer.Write(message.NetworkId);
|
||||
writer.Write(message.IsBoss);
|
||||
writer.Write(message.WaveIndex);
|
||||
writer.Write(message.Timestamp);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static EnemySpawnMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
string definitionId = reader.ReadString();
|
||||
Vector3 position = reader.ReadVector3();
|
||||
Quaternion rotation = reader.ReadQuaternion();
|
||||
int networkId = reader.ReadInt();
|
||||
bool isBoss = reader.ReadBool();
|
||||
int waveIndex = reader.ReadInt();
|
||||
float timestamp = reader.ReadFloat();
|
||||
return new EnemySpawnMessage(definitionId, position, rotation, networkId, isBoss, waveIndex, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EnemyDespawnMessage
|
||||
{
|
||||
public readonly int NetworkId;
|
||||
|
||||
public EnemyDespawnMessage(int networkId)
|
||||
{
|
||||
NetworkId = networkId;
|
||||
}
|
||||
|
||||
public static byte[] Serialize(EnemyDespawnMessage message)
|
||||
{
|
||||
using var writer = new NetworkWriter();
|
||||
writer.Write(message.NetworkId);
|
||||
return writer.ToArray();
|
||||
}
|
||||
|
||||
public static EnemyDespawnMessage Deserialize(byte[] buffer)
|
||||
{
|
||||
using var reader = new NetworkReader(buffer);
|
||||
int id = reader.ReadInt();
|
||||
return new EnemyDespawnMessage(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace MegaKoop.Game.Networking
|
||||
}
|
||||
|
||||
private static SteamCharacterStateCache instance;
|
||||
private static bool isApplicationQuitting;
|
||||
|
||||
private readonly Dictionary<int, CharacterState> states = new();
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
@@ -40,9 +41,10 @@ namespace MegaKoop.Game.Networking
|
||||
{
|
||||
get
|
||||
{
|
||||
if (instance == null)
|
||||
if (instance == null && !isApplicationQuitting)
|
||||
{
|
||||
var go = new GameObject("SteamCharacterStateCache");
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
instance = go.AddComponent<SteamCharacterStateCache>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
@@ -68,6 +70,21 @@ namespace MegaKoop.Game.Networking
|
||||
EnsureSubscription();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (instance == this)
|
||||
{
|
||||
Unsubscribe();
|
||||
states.Clear();
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
isApplicationQuitting = true;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
EnsureSubscription();
|
||||
|
||||
355
Game/Scripts/Networking/SteamEnemySpawnerNetworkBridge.cs
Normal file
355
Game/Scripts/Networking/SteamEnemySpawnerNetworkBridge.cs
Normal file
@@ -0,0 +1,355 @@
|
||||
using System.Collections.Generic;
|
||||
using Game.Scripts.Runtime.Data;
|
||||
using Game.Scripts.Runtime.Game;
|
||||
using Game.Scripts.Runtime.Pooling;
|
||||
using Game.Scripts.Runtime.Spawning;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DefaultExecutionOrder(-120)]
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamEnemySpawnerNetworkBridge : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private EnemySpawner spawner;
|
||||
[SerializeField] private GameController gameController;
|
||||
[SerializeField] private ObjectPooler pooler;
|
||||
[SerializeField] private List<EnemyDefinition> additionalDefinitions = new();
|
||||
|
||||
[Header("Behaviour")]
|
||||
[SerializeField] private bool autoFindReferences = true;
|
||||
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
private bool handlersRegistered;
|
||||
private bool spawnerSubscribed;
|
||||
private bool poolerSubscribed;
|
||||
private bool cachedAuthority;
|
||||
private bool hasPendingRemoteSpawn;
|
||||
private EnemySpawnMessage pendingRemoteSpawn;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (autoFindReferences)
|
||||
{
|
||||
spawner ??= GetComponent<EnemySpawner>();
|
||||
gameController ??= GetComponent<GameController>();
|
||||
pooler ??= spawner != null ? spawner.Pool : ObjectPooler.SharedInstance;
|
||||
}
|
||||
|
||||
cachedAuthority = DetermineAuthority();
|
||||
ApplyAuthorityOverride(cachedAuthority);
|
||||
RegisterDefinitionsFromSpawner();
|
||||
RegisterAdditionalDefinitions();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
RegisterHandlers();
|
||||
SubscribeEvents();
|
||||
cachedAuthority = DetermineAuthority();
|
||||
ApplyAuthorityOverride(cachedAuthority);
|
||||
RegisterDefinitionsFromSpawner();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnsubscribeEvents();
|
||||
UnregisterHandlers();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
|
||||
bool authority = DetermineAuthority();
|
||||
if (authority != cachedAuthority)
|
||||
{
|
||||
cachedAuthority = authority;
|
||||
ApplyAuthorityOverride(authority);
|
||||
}
|
||||
|
||||
if (!handlersRegistered)
|
||||
{
|
||||
RegisterHandlers();
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeEvents()
|
||||
{
|
||||
if (spawner != null && !spawnerSubscribed)
|
||||
{
|
||||
spawner.OnEnemySpawned += HandleEnemySpawned;
|
||||
spawnerSubscribed = true;
|
||||
}
|
||||
|
||||
if (pooler != null && !poolerSubscribed)
|
||||
{
|
||||
pooler.InstanceDespawned += HandleInstanceDespawned;
|
||||
poolerSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeEvents()
|
||||
{
|
||||
if (spawner != null && spawnerSubscribed)
|
||||
{
|
||||
spawner.OnEnemySpawned -= HandleEnemySpawned;
|
||||
spawnerSubscribed = false;
|
||||
}
|
||||
|
||||
if (pooler != null && poolerSubscribed)
|
||||
{
|
||||
pooler.InstanceDespawned -= HandleInstanceDespawned;
|
||||
poolerSubscribed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterHandlers()
|
||||
{
|
||||
if (handlersRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshNetworkManager();
|
||||
if (networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
networkManager.RegisterHandler(NetworkMessageType.EnemySpawn, HandleEnemySpawnMessage);
|
||||
networkManager.RegisterHandler(NetworkMessageType.EnemyDespawn, HandleEnemyDespawnMessage);
|
||||
handlersRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterHandlers()
|
||||
{
|
||||
if (!handlersRegistered || networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
networkManager.UnregisterHandler(NetworkMessageType.EnemySpawn, HandleEnemySpawnMessage);
|
||||
networkManager.UnregisterHandler(NetworkMessageType.EnemyDespawn, HandleEnemyDespawnMessage);
|
||||
handlersRegistered = false;
|
||||
}
|
||||
|
||||
private void RefreshNetworkManager()
|
||||
{
|
||||
if (networkManager == null)
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
private bool DetermineAuthority()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
if (networkManager == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!networkManager.IsConnected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return networkManager.IsHost;
|
||||
}
|
||||
|
||||
private void ApplyAuthorityOverride(bool authority)
|
||||
{
|
||||
spawner?.SetAuthorityOverride(authority);
|
||||
}
|
||||
|
||||
private void RegisterDefinitionsFromSpawner()
|
||||
{
|
||||
if (spawner == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var table = spawner.ActiveTable;
|
||||
if (table?.Entries == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var entry in table.Entries)
|
||||
{
|
||||
if (entry?.Def == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyDefinitionRegistry.Register(entry.Def);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterAdditionalDefinitions()
|
||||
{
|
||||
if (additionalDefinitions == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var definition in additionalDefinitions)
|
||||
{
|
||||
EnemyDefinitionRegistry.Register(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleEnemySpawned(GameObject instance, EnemyDefinition definition)
|
||||
{
|
||||
if (definition != null)
|
||||
{
|
||||
EnemyDefinitionRegistry.Register(definition);
|
||||
}
|
||||
|
||||
if (!DetermineAuthority() || networkManager == null)
|
||||
{
|
||||
if (hasPendingRemoteSpawn)
|
||||
{
|
||||
ApplyPendingRemoteSpawn(instance, definition);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance == null || definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string definitionId = EnemyDefinitionRegistry.ResolveId(definition);
|
||||
var identity = instance.GetComponent<NetworkIdentity>();
|
||||
int networkId = identity != null ? identity.NetworkId : 0;
|
||||
Vector3 spawnRootPosition = instance.transform.position - definition.PrefabPivotOffset;
|
||||
Quaternion rotation = instance.transform.rotation;
|
||||
bool isBoss = definition.IsBoss;
|
||||
int waveIndex = spawner != null ? spawner.CurrentWaveIndex : -1;
|
||||
float timestamp = gameController != null ? gameController.Elapsed : Time.time;
|
||||
|
||||
var message = new EnemySpawnMessage(definitionId, spawnRootPosition, rotation, networkId, isBoss, waveIndex, timestamp);
|
||||
byte[] payload = EnemySpawnMessage.Serialize(message);
|
||||
networkManager.SendToAll(NetworkMessageType.EnemySpawn, payload, EP2PSend.k_EP2PSendReliable);
|
||||
}
|
||||
|
||||
private void HandleInstanceDespawned(GameObject instance, EnemyDefinition definition)
|
||||
{
|
||||
if (!DetermineAuthority() || networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var identity = instance.GetComponent<NetworkIdentity>();
|
||||
if (identity == null || identity.NetworkId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = new EnemyDespawnMessage(identity.NetworkId);
|
||||
byte[] payload = EnemyDespawnMessage.Serialize(message);
|
||||
SteamCharacterStateCache.RemoveState(identity.NetworkId);
|
||||
networkManager.SendToAll(NetworkMessageType.EnemyDespawn, payload, EP2PSend.k_EP2PSendReliable);
|
||||
}
|
||||
|
||||
private void HandleEnemySpawnMessage(NetworkMessage message)
|
||||
{
|
||||
if (DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pendingRemoteSpawn = EnemySpawnMessage.Deserialize(message.Payload);
|
||||
hasPendingRemoteSpawn = true;
|
||||
|
||||
if (!EnemyDefinitionRegistry.TryGet(pendingRemoteSpawn.DefinitionId, out var definition))
|
||||
{
|
||||
Debug.LogWarning($"[SteamEnemySpawnerNetworkBridge] Missing EnemyDefinition for id '{pendingRemoteSpawn.DefinitionId}'.");
|
||||
hasPendingRemoteSpawn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = spawner != null && spawner.TrySpawn(definition, pendingRemoteSpawn.Position);
|
||||
if (success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject instance = pooler != null
|
||||
? pooler.Spawn(definition, pendingRemoteSpawn.Position, pendingRemoteSpawn.Rotation)
|
||||
: null;
|
||||
|
||||
if (instance == null)
|
||||
{
|
||||
Debug.LogWarning($"[SteamEnemySpawnerNetworkBridge] Fallback spawn failed for '{pendingRemoteSpawn.DefinitionId}'.");
|
||||
hasPendingRemoteSpawn = false;
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyPendingRemoteSpawn(instance, definition);
|
||||
}
|
||||
|
||||
private void HandleEnemyDespawnMessage(NetworkMessage message)
|
||||
{
|
||||
if (DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyDespawnMessage despawnMessage = EnemyDespawnMessage.Deserialize(message.Payload);
|
||||
if (despawnMessage.NetworkId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetworkIdentity.TryGet(despawnMessage.NetworkId, out var identity) || identity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SteamCharacterStateCache.RemoveState(identity.NetworkId);
|
||||
|
||||
var pooled = identity.GetComponent<PooledInstance>();
|
||||
if (pooled != null)
|
||||
{
|
||||
pooled.ReturnToPool();
|
||||
}
|
||||
else
|
||||
{
|
||||
identity.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPendingRemoteSpawn(GameObject instance, EnemyDefinition definition)
|
||||
{
|
||||
if (!hasPendingRemoteSpawn || instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
hasPendingRemoteSpawn = false;
|
||||
instance.transform.rotation = pendingRemoteSpawn.Rotation;
|
||||
if (definition != null)
|
||||
{
|
||||
EnemyDefinitionRegistry.Register(definition);
|
||||
}
|
||||
|
||||
var identity = instance.GetComponent<NetworkIdentity>();
|
||||
if (identity != null && pendingRemoteSpawn.NetworkId != 0 && identity.NetworkId != pendingRemoteSpawn.NetworkId)
|
||||
{
|
||||
identity.SetNetworkId(pendingRemoteSpawn.NetworkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5137a566078d27c81957d1c4040e32eb
|
||||
365
Game/Scripts/Networking/SteamGameControllerNetworkBridge.cs
Normal file
365
Game/Scripts/Networking/SteamGameControllerNetworkBridge.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using Game.Scripts.Runtime.Data;
|
||||
using Game.Scripts.Runtime.Game;
|
||||
using Game.Scripts.Runtime.Spawning;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MegaKoop.Game.Networking
|
||||
{
|
||||
[DefaultExecutionOrder(-140)]
|
||||
[DisallowMultipleComponent]
|
||||
public class SteamGameControllerNetworkBridge : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
[SerializeField] private GameController gameController;
|
||||
[SerializeField] private EnemySpawner enemySpawner;
|
||||
|
||||
[Header("Heartbeat")]
|
||||
[SerializeField, Min(0.1f)] private float heartbeatInterval = 0.5f;
|
||||
|
||||
private SteamCoopNetworkManager networkManager;
|
||||
private bool handlersRegistered;
|
||||
private bool controllerSubscribed;
|
||||
private bool cachedAuthority;
|
||||
private float heartbeatTimer;
|
||||
private int lastWaveBroadcast = -1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
gameController ??= GetComponent<GameController>();
|
||||
enemySpawner ??= GetComponent<EnemySpawner>();
|
||||
|
||||
cachedAuthority = DetermineAuthority();
|
||||
ApplyAuthorityOverride(cachedAuthority);
|
||||
RegisterBossDefinitions();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
RegisterHandlers();
|
||||
SubscribeControllerEvents();
|
||||
|
||||
cachedAuthority = DetermineAuthority();
|
||||
ApplyAuthorityOverride(cachedAuthority);
|
||||
heartbeatTimer = heartbeatInterval;
|
||||
RegisterBossDefinitions();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
UnsubscribeControllerEvents();
|
||||
UnregisterHandlers();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
|
||||
bool authority = DetermineAuthority();
|
||||
if (authority != cachedAuthority)
|
||||
{
|
||||
cachedAuthority = authority;
|
||||
ApplyAuthorityOverride(authority);
|
||||
}
|
||||
|
||||
if (authority)
|
||||
{
|
||||
RunHeartbeat(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunHeartbeat(float deltaTime)
|
||||
{
|
||||
heartbeatTimer -= deltaTime;
|
||||
if (heartbeatTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
heartbeatTimer = Mathf.Max(0.1f, heartbeatInterval);
|
||||
SendState(GameStateEvent.Heartbeat, lastWaveBroadcast);
|
||||
}
|
||||
|
||||
private void SubscribeControllerEvents()
|
||||
{
|
||||
if (gameController == null || controllerSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
gameController.OnGameStarted += HandleGameStarted;
|
||||
gameController.OnGamePaused += HandleGamePaused;
|
||||
gameController.OnGameResumed += HandleGameResumed;
|
||||
gameController.OnGameStopped += HandleGameStopped;
|
||||
gameController.OnWaveStarted += HandleWaveStarted;
|
||||
gameController.OnBossSpawned += HandleBossSpawned;
|
||||
controllerSubscribed = true;
|
||||
}
|
||||
|
||||
private void UnsubscribeControllerEvents()
|
||||
{
|
||||
if (gameController == null || !controllerSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
gameController.OnGameStarted -= HandleGameStarted;
|
||||
gameController.OnGamePaused -= HandleGamePaused;
|
||||
gameController.OnGameResumed -= HandleGameResumed;
|
||||
gameController.OnGameStopped -= HandleGameStopped;
|
||||
gameController.OnWaveStarted -= HandleWaveStarted;
|
||||
gameController.OnBossSpawned -= HandleBossSpawned;
|
||||
controllerSubscribed = false;
|
||||
}
|
||||
|
||||
private void HandleGameStarted()
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendState(GameStateEvent.Started, -1);
|
||||
lastWaveBroadcast = gameController != null ? gameController.CurrentWaveIndex : -1;
|
||||
}
|
||||
|
||||
private void HandleGamePaused()
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendState(GameStateEvent.Paused, gameController != null ? gameController.CurrentWaveIndex : -1);
|
||||
}
|
||||
|
||||
private void HandleGameResumed()
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendState(GameStateEvent.Resumed, gameController != null ? gameController.CurrentWaveIndex : -1);
|
||||
}
|
||||
|
||||
private void HandleGameStopped()
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendState(GameStateEvent.Stopped, gameController != null ? gameController.CurrentWaveIndex : -1);
|
||||
lastWaveBroadcast = -1;
|
||||
}
|
||||
|
||||
private void HandleWaveStarted(int waveIndex)
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int previousWave = lastWaveBroadcast >= 0 ? lastWaveBroadcast : waveIndex - 1;
|
||||
lastWaveBroadcast = waveIndex;
|
||||
SendState(GameStateEvent.WaveAdvanced, previousWave);
|
||||
}
|
||||
|
||||
private void HandleBossSpawned(EnemyDefinition boss, int count)
|
||||
{
|
||||
if (!DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnemyDefinitionRegistry.Register(boss);
|
||||
string definitionId = EnemyDefinitionRegistry.ResolveId(boss);
|
||||
SendState(GameStateEvent.BossSpawned, lastWaveBroadcast, definitionId, count);
|
||||
}
|
||||
|
||||
private void SendState(GameStateEvent evt, int previousWave, string definitionId = "", int count = 0)
|
||||
{
|
||||
if (networkManager == null || gameController == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float elapsed = gameController.Elapsed;
|
||||
int currentWave = gameController.CurrentWaveIndex;
|
||||
bool isPaused = gameController.IsPaused;
|
||||
bool isRunning = gameController.IsGameActive;
|
||||
|
||||
var message = new GameStateMessage(evt, elapsed, currentWave, previousWave, isPaused, isRunning, definitionId, count);
|
||||
byte[] payload = GameStateMessage.Serialize(message);
|
||||
var sendType = evt == GameStateEvent.Heartbeat ? EP2PSend.k_EP2PSendUnreliableNoDelay : EP2PSend.k_EP2PSendReliable;
|
||||
networkManager.SendToAll(NetworkMessageType.GameState, payload, sendType);
|
||||
}
|
||||
|
||||
private void RegisterHandlers()
|
||||
{
|
||||
if (handlersRegistered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshNetworkManager();
|
||||
if (networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
networkManager.RegisterHandler(NetworkMessageType.GameState, HandleGameStateMessage);
|
||||
handlersRegistered = true;
|
||||
}
|
||||
|
||||
private void UnregisterHandlers()
|
||||
{
|
||||
if (!handlersRegistered || networkManager == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
networkManager.UnregisterHandler(NetworkMessageType.GameState, HandleGameStateMessage);
|
||||
handlersRegistered = false;
|
||||
}
|
||||
|
||||
private void HandleGameStateMessage(NetworkMessage message)
|
||||
{
|
||||
if (DetermineAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameStateMessage state = GameStateMessage.Deserialize(message.Payload);
|
||||
ApplyRemoteState(state);
|
||||
}
|
||||
|
||||
private void ApplyRemoteState(GameStateMessage state)
|
||||
{
|
||||
if (gameController == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (state.Event)
|
||||
{
|
||||
case GameStateEvent.Started:
|
||||
if (!gameController.IsGameActive)
|
||||
{
|
||||
gameController.StartGame();
|
||||
}
|
||||
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.SetRunningFlagsFromRemote(state.IsRunning, state.IsPaused);
|
||||
gameController.ApplyRemoteWaveStarted(state.CurrentWave);
|
||||
lastWaveBroadcast = state.CurrentWave;
|
||||
break;
|
||||
|
||||
case GameStateEvent.Paused:
|
||||
if (!gameController.IsPaused && gameController.IsGameActive)
|
||||
{
|
||||
gameController.PauseGame();
|
||||
}
|
||||
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.SetRunningFlagsFromRemote(state.IsRunning, state.IsPaused);
|
||||
break;
|
||||
|
||||
case GameStateEvent.Resumed:
|
||||
if (gameController.IsPaused)
|
||||
{
|
||||
gameController.ResumeGame();
|
||||
}
|
||||
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.SetRunningFlagsFromRemote(state.IsRunning, state.IsPaused);
|
||||
break;
|
||||
|
||||
case GameStateEvent.Stopped:
|
||||
if (gameController.IsGameActive)
|
||||
{
|
||||
gameController.StopGame();
|
||||
}
|
||||
|
||||
lastWaveBroadcast = -1;
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.SetRunningFlagsFromRemote(state.IsRunning, state.IsPaused);
|
||||
break;
|
||||
|
||||
case GameStateEvent.WaveAdvanced:
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.ApplyRemoteWaveAdvance(state.PreviousWave, state.CurrentWave);
|
||||
lastWaveBroadcast = state.CurrentWave;
|
||||
break;
|
||||
|
||||
case GameStateEvent.BossSpawned:
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
if (EnemyDefinitionRegistry.TryGet(state.DefinitionId, out var bossDefinition))
|
||||
{
|
||||
gameController.ApplyRemoteBossSpawned(bossDefinition, state.Count);
|
||||
}
|
||||
break;
|
||||
|
||||
case GameStateEvent.Heartbeat:
|
||||
gameController.SetElapsedFromRemote(state.Elapsed);
|
||||
gameController.SetRunningFlagsFromRemote(state.IsRunning, state.IsPaused);
|
||||
if (state.CurrentWave >= 0 && state.CurrentWave != gameController.CurrentWaveIndex)
|
||||
{
|
||||
gameController.ApplyRemoteWaveAdvance(gameController.CurrentWaveIndex, state.CurrentWave);
|
||||
lastWaveBroadcast = state.CurrentWave;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshNetworkManager()
|
||||
{
|
||||
if (networkManager == null)
|
||||
{
|
||||
networkManager = SteamCoopNetworkManager.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
private bool DetermineAuthority()
|
||||
{
|
||||
RefreshNetworkManager();
|
||||
if (networkManager == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!networkManager.IsConnected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return networkManager.IsHost;
|
||||
}
|
||||
|
||||
private void ApplyAuthorityOverride(bool authority)
|
||||
{
|
||||
gameController?.SetAuthorityOverride(authority);
|
||||
enemySpawner?.SetAuthorityOverride(authority);
|
||||
}
|
||||
|
||||
private void RegisterBossDefinitions()
|
||||
{
|
||||
if (gameController?.BossSchedule?.Events == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var bossEvent in gameController.BossSchedule.Events)
|
||||
{
|
||||
if (bossEvent?.Boss == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
EnemyDefinitionRegistry.Register(bossEvent.Boss);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4ee45b08060ff11b9c4de6b6ab9d671
|
||||
@@ -19,7 +19,10 @@ namespace Game.Scripts.Runtime.Game
|
||||
|
||||
public float Elapsed => _elapsed;
|
||||
public bool IsRunning => _isRunning && !_isPaused;
|
||||
public bool IsGameActive => _isRunning;
|
||||
public bool IsPaused => _isPaused;
|
||||
public int CurrentWaveIndex => _currentWaveIndex;
|
||||
public BossSchedule BossSchedule => bossSchedule;
|
||||
|
||||
public event Action OnGameStarted;
|
||||
public event Action OnGamePaused;
|
||||
@@ -35,6 +38,8 @@ namespace Game.Scripts.Runtime.Game
|
||||
private bool _isPaused;
|
||||
private int _currentWaveIndex = -1;
|
||||
private int _bossCursor;
|
||||
private bool _hasAuthorityOverride;
|
||||
private bool _authorityOverride;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
@@ -66,9 +71,16 @@ namespace Game.Scripts.Runtime.Game
|
||||
spawner?.SetClock(this);
|
||||
spawner?.Begin();
|
||||
|
||||
UpdateWaveState();
|
||||
if (HasAuthority())
|
||||
{
|
||||
UpdateWaveState();
|
||||
_loop = StartCoroutine(GameLoop());
|
||||
}
|
||||
else
|
||||
{
|
||||
_loop = null;
|
||||
}
|
||||
|
||||
_loop = StartCoroutine(GameLoop());
|
||||
OnGameStarted?.Invoke();
|
||||
Log($"Game started – waveDuration={waveDurationSeconds}s");
|
||||
}
|
||||
@@ -268,5 +280,84 @@ namespace Game.Scripts.Runtime.Game
|
||||
Debug.Log($"[{name}] {message}", this);
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetAuthorityOverride(bool hasAuthority)
|
||||
{
|
||||
_hasAuthorityOverride = true;
|
||||
if (_authorityOverride == hasAuthority)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_authorityOverride = hasAuthority;
|
||||
|
||||
if (!HasAuthority() && _loop != null)
|
||||
{
|
||||
StopCoroutine(_loop);
|
||||
_loop = null;
|
||||
}
|
||||
else if (HasAuthority() && _isRunning && _loop == null)
|
||||
{
|
||||
_loop = StartCoroutine(GameLoop());
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasAuthority() => !_hasAuthorityOverride || _authorityOverride;
|
||||
|
||||
internal void SetElapsedFromRemote(float elapsed)
|
||||
{
|
||||
_elapsed = Mathf.Max(0f, elapsed);
|
||||
}
|
||||
|
||||
internal void SetRunningFlagsFromRemote(bool isRunning, bool isPaused)
|
||||
{
|
||||
_isRunning = isRunning;
|
||||
_isPaused = isPaused;
|
||||
}
|
||||
|
||||
internal void ApplyRemoteWaveAdvance(int previousWave, int newWave)
|
||||
{
|
||||
int oldWave = _currentWaveIndex;
|
||||
|
||||
if (previousWave >= 0 && oldWave == previousWave)
|
||||
{
|
||||
OnWaveCompleted?.Invoke(previousWave);
|
||||
}
|
||||
else if (oldWave >= 0 && oldWave != newWave)
|
||||
{
|
||||
OnWaveCompleted?.Invoke(oldWave);
|
||||
}
|
||||
|
||||
_currentWaveIndex = newWave;
|
||||
if (_currentWaveIndex >= 0)
|
||||
{
|
||||
spawner?.SetWaveIndex(_currentWaveIndex);
|
||||
OnWaveStarted?.Invoke(_currentWaveIndex);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ApplyRemoteWaveStarted(int waveIndex)
|
||||
{
|
||||
if (_currentWaveIndex != waveIndex)
|
||||
{
|
||||
_currentWaveIndex = waveIndex;
|
||||
}
|
||||
|
||||
if (_currentWaveIndex >= 0)
|
||||
{
|
||||
spawner?.SetWaveIndex(_currentWaveIndex);
|
||||
OnWaveStarted?.Invoke(_currentWaveIndex);
|
||||
}
|
||||
}
|
||||
|
||||
internal void ApplyRemoteWaveCompleted(int waveIndex)
|
||||
{
|
||||
OnWaveCompleted?.Invoke(waveIndex);
|
||||
}
|
||||
|
||||
internal void ApplyRemoteBossSpawned(EnemyDefinition boss, int count)
|
||||
{
|
||||
OnBossSpawned?.Invoke(boss, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using Game.Scripts.Runtime.Abstractions;
|
||||
using Game.Scripts.Runtime.Data;
|
||||
using UnityEngine;
|
||||
using Unity.Netcode;
|
||||
|
||||
namespace Game.Scripts.Runtime.Pooling
|
||||
{
|
||||
@@ -112,7 +113,7 @@ namespace Game.Scripts.Runtime.Pooling
|
||||
}
|
||||
|
||||
bucket.Active.Add(go);
|
||||
go.transform.SetParent(null, true);
|
||||
SetParentSafely(go, null, true);
|
||||
go.transform.SetPositionAndRotation(position + definition.PrefabPivotOffset, rotation);
|
||||
go.SetActive(true);
|
||||
|
||||
@@ -137,7 +138,12 @@ namespace Game.Scripts.Runtime.Pooling
|
||||
bucket.Active.Remove(instance);
|
||||
|
||||
instance.SetActive(false);
|
||||
instance.transform.SetParent(poolRoot, false);
|
||||
var netObj = instance.GetComponent<NetworkObject>();
|
||||
SetParentSafely(instance, poolRoot, false, netObj, false);
|
||||
if (netObj != null)
|
||||
{
|
||||
Destroy(netObj);
|
||||
}
|
||||
bucket.Available.Enqueue(instance);
|
||||
item.Handle?.NotifyDespawned();
|
||||
InstanceDespawned?.Invoke(instance, item.Definition);
|
||||
@@ -172,7 +178,8 @@ namespace Game.Scripts.Runtime.Pooling
|
||||
}
|
||||
|
||||
EnsureRoot();
|
||||
var go = Instantiate(definition.Prefab, poolRoot);
|
||||
var go = Instantiate(definition.Prefab);
|
||||
SetParentSafely(go, poolRoot, false);
|
||||
go.name = $"{definition.Prefab.name}_Pooled";
|
||||
go.SetActive(false);
|
||||
|
||||
@@ -230,6 +237,38 @@ namespace Game.Scripts.Runtime.Pooling
|
||||
root.transform.SetParent(transform, false);
|
||||
poolRoot = root.transform;
|
||||
}
|
||||
|
||||
private void SetParentSafely(GameObject instance, Transform parent, bool worldPositionStays, NetworkObject cachedNetworkObject = null, bool restoreAutoSync = true)
|
||||
{
|
||||
if (instance == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var netObj = cachedNetworkObject != null ? cachedNetworkObject : instance.GetComponent<NetworkObject>();
|
||||
var shouldRestore = false;
|
||||
var previousAutoSync = false;
|
||||
|
||||
if (netObj != null && !HasListeningNetworkManager(netObj))
|
||||
{
|
||||
previousAutoSync = netObj.AutoObjectParentSync;
|
||||
netObj.AutoObjectParentSync = false;
|
||||
shouldRestore = restoreAutoSync;
|
||||
}
|
||||
|
||||
instance.transform.SetParent(parent, worldPositionStays);
|
||||
|
||||
if (shouldRestore && netObj != null)
|
||||
{
|
||||
netObj.AutoObjectParentSync = previousAutoSync;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasListeningNetworkManager(NetworkObject netObj)
|
||||
{
|
||||
var manager = netObj != null ? (netObj.NetworkManager != null ? netObj.NetworkManager : NetworkManager.Singleton) : NetworkManager.Singleton;
|
||||
return manager != null && manager.IsListening;
|
||||
}
|
||||
}
|
||||
|
||||
[DisallowMultipleComponent]
|
||||
|
||||
@@ -6,6 +6,7 @@ using Game.Scripts.Runtime.Data;
|
||||
using Game.Scripts.Runtime.Pooling;
|
||||
using Game.Scripts.Runtime.Navigation;
|
||||
using MegaKoop.Game;
|
||||
using MegaKoop.Game.Networking;
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
@@ -68,6 +69,8 @@ namespace Game.Scripts.Runtime.Spawning
|
||||
private IEnemyFactory _factory;
|
||||
private bool _warnedMissingPlayer;
|
||||
private bool _warnedMissingNavMesh;
|
||||
private bool _authorityOverrideSet;
|
||||
private bool _authorityOverrideValue = true;
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
@@ -170,8 +173,16 @@ namespace Game.Scripts.Runtime.Spawning
|
||||
_spawnedThisWave.Clear();
|
||||
_localElapsed = 0f;
|
||||
_nextSpawnTimestamp = 0f;
|
||||
_spawnLoop = StartCoroutine(SpawnLoop());
|
||||
LogLifecycle($"Begin – wave {CurrentWaveIndex}, pool prewarm complete, maxConcurrent={_runtimeConfig.MaxConcurrent}");
|
||||
if (HasAuthority())
|
||||
{
|
||||
_spawnLoop = StartCoroutine(SpawnLoop());
|
||||
LogLifecycle($"Begin – wave {CurrentWaveIndex}, pool prewarm complete, maxConcurrent={_runtimeConfig.MaxConcurrent}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_spawnLoop = null;
|
||||
LogLifecycle($"Begin – follower mode (no spawn loop), wave {CurrentWaveIndex}, maxConcurrent={_runtimeConfig?.MaxConcurrent}");
|
||||
}
|
||||
}
|
||||
|
||||
public void End()
|
||||
@@ -226,6 +237,12 @@ namespace Game.Scripts.Runtime.Spawning
|
||||
_localElapsed = _clock.Elapsed;
|
||||
}
|
||||
|
||||
if (!HasAuthority())
|
||||
{
|
||||
yield return null;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_localElapsed >= _nextSpawnTimestamp)
|
||||
{
|
||||
ExecuteSpawnTick();
|
||||
@@ -640,6 +657,22 @@ namespace Game.Scripts.Runtime.Spawning
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetAuthorityOverride(bool hasAuthority)
|
||||
{
|
||||
_authorityOverrideSet = true;
|
||||
_authorityOverrideValue = hasAuthority;
|
||||
|
||||
if (!HasAuthority() && _spawnLoop != null)
|
||||
{
|
||||
StopCoroutine(_spawnLoop);
|
||||
_spawnLoop = null;
|
||||
}
|
||||
else if (HasAuthority() && _spawnLoop == null && _runtimeConfig != null && _runtimeTable != null)
|
||||
{
|
||||
_spawnLoop = StartCoroutine(SpawnLoop());
|
||||
}
|
||||
}
|
||||
|
||||
private bool RequirePlayerReference()
|
||||
{
|
||||
if (EnsurePlayerReference())
|
||||
@@ -706,5 +739,26 @@ namespace Game.Scripts.Runtime.Spawning
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasAuthority()
|
||||
{
|
||||
if (_authorityOverrideSet)
|
||||
{
|
||||
return _authorityOverrideValue;
|
||||
}
|
||||
|
||||
var manager = SteamCoopNetworkManager.Instance;
|
||||
if (manager == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!manager.IsConnected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return manager.IsHost;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Game.Scripts.Runtime.Data.SpawnTable
|
||||
entries:
|
||||
- Def: {fileID: 11400000, guid: 1bc4888fa172eb99f94756653be6c1ed, type: 2}
|
||||
Weight: 10
|
||||
MinWave: 10
|
||||
MaxWave: 40
|
||||
Cap: 10
|
||||
Weight: 5
|
||||
MinWave: 0
|
||||
MaxWave: 10
|
||||
Cap: 50
|
||||
|
||||
@@ -14,7 +14,7 @@ MonoBehaviour:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::Game.Scripts.Runtime.Data.SpawnerConfig
|
||||
MinSpawnRadius: 8
|
||||
MaxSpawnRadius: 24
|
||||
SpawnInterval: 2
|
||||
SpawnInterval: 10
|
||||
SpawnRateOverTime:
|
||||
serializedVersion: 2
|
||||
m_Curve:
|
||||
@@ -39,7 +39,7 @@ MonoBehaviour:
|
||||
m_PreInfinity: 2
|
||||
m_PostInfinity: 2
|
||||
m_RotationOrder: 4
|
||||
MaxConcurrent: 30
|
||||
MaxConcurrent: 20
|
||||
MaxPlacementAttempts: 6
|
||||
PrewarmPerType: 4
|
||||
PoolHardCapPerType: 60
|
||||
@@ -55,5 +55,5 @@ MonoBehaviour:
|
||||
NavMeshAreaMask: -1
|
||||
useSeed: 1
|
||||
seedValue: 0
|
||||
DrawGizmos: 1
|
||||
DrawGizmos: 0
|
||||
GizmoSampleCount: 16
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 743d0432c7ac8c691a555633664b7f42
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08fec3cef01a59840bc3352757c5902a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 946d6a345d25ee5a4ac39b2e4f4fc7d5
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 461055c25fc0ac12aad6aca9b078522b
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c18dd2a2ad9b70929b5c16d0e41d6be1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 471c514198bed37fd9284116d5179806
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a9b28d2e66142e338fccaac4b38ab54
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 404c3ba9b2e2f4d9e92f1203076a27ac
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f03c271dacccd7838fe3ef9912b3a1d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b493c55f3d9b7dd109844d107fbc736d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb80a25771da36073b8e99e258928dc4
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35f19552c16c41a9784dafae0030c705
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a72258c1ebeb18ef944c22eefb22959
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10fd9e4d197a4c420bc87ae7c1bb1795
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 612e7d3b2a9b0d024a61498804de9108
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f5659dffb22f2d58be6edc348f2120c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9738e0f3db2d4c8249bff68e37aa9955
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6e29844605285ef3afba4029f9c3dd0
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b24c5acf7c2737f608c2315ac1c43ae3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f015df6cfb8e7c141bb69128bbf07f36
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c3b988bb70367bd0ca77081c5565a0b8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d197b958d50776784b025cc8bb26db95
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9785a9b0ac0eeaf3801225ac8fbc4e1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c77915bfbd12c8eae96286f1123f9836
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27ba183aaa1be5c24b9a6b1cabbf5a72
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16920c5001a27193383d3f39d66020aa
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 266a22bfb73742039a571640d31eafd7
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff003878a86d7084ea4daee894d6b77a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 163a06188dba81d07983ebcfa5367ca8
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 578683690548686bd8a36fa39db9a30c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20d83a7bb2ed395fb83d4ec7a9c2e661
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 348b05ab0e3576d3294228718d0d8937
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f36aae0168306f69987b7951ebd3b1
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 85741d2af2b1065baae0bdcf2e7455e9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b781103bbb35b594900f5959dab063f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a17616b0065626cd2974694e8d515f0c
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3630f7764fc2dd4b698db73c9f5b4488
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d0f797227056f216ea586fd533747640
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 67c9adac75213a3cabad86e62c73df0d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 83c933666518ed7649fcae5e8f847bb6
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fee1d5bc83e13cfcbd6a225dbfe7e23
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d8a8cf9eea4b75059c402cfd48b646a
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 23800000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user