Host-side spectator management for the MansionGame. Tracks per-connection spectator GameObjects, spawns a connection-owned spectator prefab when a player is dead or late-joining, and removes it when the player is alive again.
using System;
using System.Collections.Generic;
using Sandbox;
namespace BrickJam;
public sealed partial class MansionGame
{
// Host-side spectator pawn per connection (dead players + late joiners). Not networked itself - the
// spectator GameObjects are network-spawned and owned by their connection.
private readonly Dictionary<Guid, GameObject> spectators = new();
[Property] private GameObject SpectatorPrefab { get; set; }
/// <summary>
/// True when a client joining right now is too late for this round (timer running and past the grace
/// window) and should spectate until the next level. Legacy <c>ClientJoined</c> branch.
/// </summary>
private bool ShouldSpectateOnJoin => TimerActive && (TimePerLevel - (float)hostTimeOut) > TimeToJoin;
/// <summary>Spawn a connection-owned spectator fly-cam at <paramref name="at"/>. Host-only.</summary>
private void SpawnSpectator( Connection channel, Transform at )
{
RemoveSpectator( channel );
var go = SpectatorPrefab?.Clone( new CloneConfig () { Transform = at, Name = $"Spectator - {channel.DisplayName}", StartEnabled = true } );
go?.NetworkSpawn( channel );
spectators[channel.Id] = go;
}
private void RemoveSpectator( Connection channel )
{
if ( spectators.Remove( channel.Id, out var go ) && go.IsValid() )
go.Destroy();
}
/// <summary>
/// Host reconciliation each tick: a dead player gets a spectator pawn (and yields its camera via the
/// <see cref="Player.Spectating"/> flag); once alive again the spectator is reclaimed.
/// </summary>
private void UpdateSpectators()
{
foreach ( var go in spawnedPlayers.Values )
{
if ( !go.IsValid() || !go.Components.TryGet<Player>( out var player ) )
continue;
var channel = player.Network.Owner;
if ( channel is null )
continue;
var hasSpectator = spectators.ContainsKey( channel.Id );
if ( !player.IsAlive && !hasSpectator )
{
SpawnSpectator( channel, new Transform( player.EyePosition, player.WorldRotation ) );
player.Spectating = true;
}
else if ( player.IsAlive && hasSpectator )
{
RemoveSpectator( channel );
player.Spectating = false;
}
}
}
}