online fix

This commit is contained in:
2025-10-27 12:37:18 +01:00
parent 96d50bfad5
commit e6759d6610
281 changed files with 7337 additions and 136 deletions

View File

@@ -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

View File

@@ -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

View 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;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fe91b8e8bcd3c2b8cb3f037428048c0b

View File

@@ -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);
}
}
}

View File

@@ -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();

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5137a566078d27c81957d1c4040e32eb

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a4ee45b08060ff11b9c4de6b6ab9d671

View File

@@ -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);
}
}
}

View File

@@ -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]

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 743d0432c7ac8c691a555633664b7f42
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 08fec3cef01a59840bc3352757c5902a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 946d6a345d25ee5a4ac39b2e4f4fc7d5
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 461055c25fc0ac12aad6aca9b078522b
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c18dd2a2ad9b70929b5c16d0e41d6be1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 471c514198bed37fd9284116d5179806
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7a9b28d2e66142e338fccaac4b38ab54
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 404c3ba9b2e2f4d9e92f1203076a27ac
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5f03c271dacccd7838fe3ef9912b3a1d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b493c55f3d9b7dd109844d107fbc736d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: eb80a25771da36073b8e99e258928dc4
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 35f19552c16c41a9784dafae0030c705
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5a72258c1ebeb18ef944c22eefb22959
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 10fd9e4d197a4c420bc87ae7c1bb1795
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 612e7d3b2a9b0d024a61498804de9108
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f5659dffb22f2d58be6edc348f2120c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9738e0f3db2d4c8249bff68e37aa9955
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e6e29844605285ef3afba4029f9c3dd0
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b24c5acf7c2737f608c2315ac1c43ae3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f015df6cfb8e7c141bb69128bbf07f36
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3b988bb70367bd0ca77081c5565a0b8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d197b958d50776784b025cc8bb26db95
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e9785a9b0ac0eeaf3801225ac8fbc4e1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c77915bfbd12c8eae96286f1123f9836
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 27ba183aaa1be5c24b9a6b1cabbf5a72
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 16920c5001a27193383d3f39d66020aa
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 266a22bfb73742039a571640d31eafd7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ff003878a86d7084ea4daee894d6b77a
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 163a06188dba81d07983ebcfa5367ca8
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 578683690548686bd8a36fa39db9a30c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 20d83a7bb2ed395fb83d4ec7a9c2e661
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 348b05ab0e3576d3294228718d0d8937
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e6f36aae0168306f69987b7951ebd3b1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 85741d2af2b1065baae0bdcf2e7455e9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4b781103bbb35b594900f5959dab063f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a17616b0065626cd2974694e8d515f0c
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3630f7764fc2dd4b698db73c9f5b4488
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d0f797227056f216ea586fd533747640
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 67c9adac75213a3cabad86e62c73df0d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 83c933666518ed7649fcae5e8f847bb6
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9fee1d5bc83e13cfcbd6a225dbfe7e23
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 23800000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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