GameLoop/GameManager.cs
public sealed partial class GameManager : GameObjectSystem<GameManager>, Component.INetworkListener, ISceneStartup
{
public GameManager( Scene scene ) : base( scene )
{
Listen( Stage.FinishUpdate, 0, Update, "Update" );
}
void ISceneStartup.OnHostInitialize()
{
Scene.NavMesh.AgentRadius = 20;
Scene.NavMesh.AgentHeight = 72;
Scene.NavMesh.IsEnabled = true;
BotIdCounter = 0;
var lobbyPrivacy = GameSettings.IsBenchmark ? Sandbox.Network.LobbyPrivacy.Private : Sandbox.Network.LobbyPrivacy.Public;
// If we're not hosting a lobby, start hosting one
// so that people can join this game.
Networking.CreateLobby( new Sandbox.Network.LobbyConfig() { Privacy = lobbyPrivacy, MaxPlayers = 32, Name = "Sandbox Deathmatch", DestroyWhenHostLeaves = true } );
}
public void Update()
{
if ( Scene.IsEditor ) return;
if ( GameSettings.Debug > 0 )
{
var debugString = $"""
Time: {Time.Now:n0}
CurrentGameStage: {CurrentGameStage}
Game Time Remaining: {GameTimeRemaining:n0}
""";
DebugOverlaySystem.Current.ScreenText( 30, debugString, 13, flags: TextFlag.LeftTop );
}
StageUpdate();
}
void Component.INetworkListener.OnActive( Connection channel )
{
channel.CanSpawnObjects = false;
var playerData = CreatePlayerInfo( channel );
SpawnPlayer( playerData );
// First player? Restart the game loop
if ( Connection.All.Count < 2 )
{
RestartGame();
}
Notify( $"{channel.DisplayName} has joined the game" );
}
/// <summary>
/// Called when someone leaves the server. This will only be called for the host.
/// </summary>
void Component.INetworkListener.OnDisconnected( Connection channel )
{
var pd = PlayerData.For( channel );
if ( pd is not null )
{
pd.GameObject.Destroy();
}
// If we have no more players, restart the game loop
if ( !Connection.All.Any() )
{
RestartGame();
}
Notify( $"{channel.DisplayName} has left the game" );
}
private PlayerData CreatePlayerInfo( Connection channel )
{
var go = new GameObject( true, $"PlayerInfo - {channel.DisplayName}" );
var data = go.AddComponent<PlayerData>();
data.SteamId = (long)channel.SteamId;
data.PlayerId = channel.Id;
data.DisplayName = channel.DisplayName;
go.NetworkSpawn( null );
go.Network.SetOwnerTransfer( OwnerTransfer.Fixed );
return data;
}
public void SpawnPlayer( Connection connection ) => SpawnPlayer( PlayerData.For( connection ) );
public void SpawnPlayer( PlayerData playerData )
{
Assert.NotNull( playerData, "PlayerData is null" );
Assert.True( Networking.IsHost, $"Client tried to SpawnPlayer: {playerData.DisplayName}" );
if ( CurrentGameStage != GameManager.GameStage.Game )
{
// spawn spectator?
return;
}
// does this connection already have a player?
if ( Scene.GetAll<Player>().Where( x => x.Network.Owner?.Id == playerData.PlayerId ).Any() )
return;
// Find a spawn location for this player
var startLocation = FindSpawnLocation().WithScale( 1 );
// Spawn this object and make the client the owner
var playerGo = GameObject.Clone( "/player.prefab", new CloneConfig { Name = playerData.DisplayName, StartEnabled = false, Transform = startLocation } );
var player = playerGo.GetComponent<Player>( true );
player.PlayerData = playerData;
if ( playerData.IsBot )
{
var playerBot = playerGo.AddComponent<PlayerBotController>();
playerGo.NetworkSpawn( null );
playerGo.GetComponent<PlayerStats>().Enabled = false;
player.Controller.UseInputControls = false;
player.Controller.UseCameraControls = false;
var dresser = playerGo.GetComponent<Dresser>();
if ( dresser.IsValid() )
{
dresser.Source = Dresser.ClothingSource.Manual;
dresser.Randomize();
}
}
else
{
var owner = Connection.Find( playerData.PlayerId );
// Main player will get a bot controller too if we're benchmarking
if ( GameSettings.IsBenchmark )
{
var playerBot = playerGo.AddComponent<PlayerBotController>();
player.Controller.UseInputControls = false;
}
playerGo.NetworkSpawn( owner );
}
IPlayerEvent.PostToGameObject( player.GameObject, x => x.OnSpawned() );
player.EquipBestWeapon();
PlayerSpawnEffects( playerGo.WorldPosition + Vector3.Up * 40 );
}
public void SpawnPlayerDelayed( PlayerData playerData )
{
GameTask.RunInThreadAsync( async () =>
{
await Task.Delay( 4000 );
await GameTask.MainThread();
if ( Current is not null )
Current.SpawnPlayer( playerData );
} );
}
[Rpc.Broadcast]
public static void PlayerSpawnEffects( Vector3 center )
{
if ( Application.IsDedicatedServer ) return;
Sound.Play( "audio/sounds/player_spawn.sound", center );
}
/// <summary>
/// In the editor, spawn the player where they're looking
/// </summary>
public static Transform EditorSpawnLocation { get; set; }
/// <summary>
/// Find the most appropriate place to respawn
/// </summary>
Transform FindSpawnLocation()
{
//
// If we have any SpawnPoint components in the scene, then use those
//
var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToArray();
if ( spawnPoints.Length == 0 )
{
if ( Application.IsEditor && !EditorSpawnLocation.Position.IsNearlyZero() )
{
return EditorSpawnLocation;
}
return Transform.Zero;
}
var players = Scene.GetAll<Player>();
if ( !players.Any() )
{
return Random.Shared.FromArray( spawnPoints ).Transform.World;
}
//
// Find spawnpoint furthest away from any players
// TODO: in the future we may want a different logic, as spawning far away is not necessarily good.
// But good enough for now and also reduces chances of players from spawning on top of or inside each other.
//
SpawnPoint spawnPointFurthestAway = null;
float spawnPointFurthestAwayDistanceSqr = float.MinValue;
foreach ( var spawnPoint in spawnPoints )
{
float closestPlayerDistanceToSpawnpointSqr = float.MaxValue;
foreach ( var player in players )
{
float playerDistanceToSpawnPointSqr = (spawnPoint.Transform.World.Position - player.Transform.World.Position).LengthSquared;
if ( playerDistanceToSpawnPointSqr < closestPlayerDistanceToSpawnpointSqr )
{
closestPlayerDistanceToSpawnpointSqr = playerDistanceToSpawnPointSqr;
}
}
if ( closestPlayerDistanceToSpawnpointSqr > spawnPointFurthestAwayDistanceSqr )
{
spawnPointFurthestAwayDistanceSqr = closestPlayerDistanceToSpawnpointSqr;
spawnPointFurthestAway = spawnPoint;
}
}
return spawnPointFurthestAway.Transform.World;
}
[Rpc.Broadcast]
private static void SendMessage( string msg )
{
Log.Info( msg );
}
[Rpc.Broadcast( NetFlags.HostOnly )]
private void Notify( string text )
{
Sandbox.Platform.Chat.AddText( text );
}
/// <summary>
/// Called on the host when a player is killed
/// </summary>
public void OnDeath( Player player, DamageInfo dmg )
{
Assert.True( Networking.IsHost );
Assert.True( player.IsValid(), "Player invalid" );
Assert.True( player.PlayerData.IsValid(), $"{player.GameObject.Name}'s PlayerData invalid" );
var weapon = dmg.Weapon;
var attacker = dmg.Attacker?.GetComponent<Player>();
bool isSuicide = attacker == player;
if ( !isSuicide && attacker.IsValid() )
{
attacker.PlayerData.Kills++;
attacker.PlayerData.AddStat( $"kills" );
if ( weapon.IsValid() )
{
attacker.PlayerData.AddStat( $"kills.{weapon.Name}" );
}
Scene.RunEvent<KillTracker>( x => x.OnPlayerKilled( attacker.PlayerData, player.PlayerData, dmg ) );
}
player.PlayerData.Deaths++;
var w = weapon.IsValid() ? weapon.GetComponentInChildren<IKillIcon>() : null;
Scene.RunEvent<Feed>( x => x.NotifyDeath( player.PlayerData, attacker?.PlayerData, w?.DisplayIcon, dmg.Tags ) );
string attackerName = attacker.IsValid() ? attacker.DisplayName : dmg.Attacker?.Name ?? "unknown";
if ( string.IsNullOrEmpty( attackerName ) )
SendMessage( $"{player.DisplayName} died (tags: {dmg.Tags})" );
else if ( weapon.IsValid() )
SendMessage( $"{attackerName} killed {(isSuicide ? "self" : player.DisplayName)} with {weapon.Name} (tags: {dmg.Tags})" );
else
SendMessage( $"{attackerName} killed {(isSuicide ? "self" : player.DisplayName)} (tags: {dmg.Tags})" );
}
}