StreamGameManager.cs
/// <summary>
/// The hub of the example, and the one place that talks to the streaming system.
///
/// It implements <see cref="Streamer.IEvents"/>, which any <c>Component</c> can do to receive stream
/// events. The engine dispatches these to the active scene on the main thread, so it's safe to touch
/// GameObjects directly from them.
///
/// The flow is:
/// - a viewer joins chat -> spawn them an avatar (a <see cref="StreamPlayer"/>)
/// - a viewer leaves chat -> remove their avatar
/// - a viewer sends a chat message -> turn it into a command and route it to their avatar
///
/// Keeping all of the stream handling here - rather than letting every avatar listen to chat itself -
/// gives us one clear entry point, and lets <see cref="StreamPlayer"/> stay a simple component that just
/// does what it's told.
/// </summary>
public sealed class StreamGameManager : Component, Streamer.IEvents
{
/// <summary>
/// The prefab cloned for each viewer. Assign your avatar prefab to this in the inspector.
/// </summary>
[Property]
public PrefabFile ViewerPrefab { get; set; }
/// <summary>
/// The avatar we've spawned for each viewer currently in chat. We keep this map so we can find a
/// viewer's avatar when they chat and tear it down when they leave. A <see cref="Streamer.Viewer"/> is
/// a stable object for as long as that viewer is in chat, so it's safe to use as a dictionary key.
/// </summary>
readonly Dictionary<Streamer.Viewer, StreamPlayer> _avatars = new();
/// <summary>
/// A viewer joined chat - give them an avatar.
/// </summary>
void Streamer.IEvents.OnStreamJoin( Streamer.Viewer viewer )
{
Log.Info( $"{viewer.DisplayName} joined" );
SpawnAvatar( viewer );
}
/// <summary>
/// A viewer left chat - remove their avatar.
/// </summary>
void Streamer.IEvents.OnStreamLeave( Streamer.Viewer viewer )
{
Log.Info( $"{viewer.DisplayName} left" );
RemoveAvatar( viewer );
}
/// <summary>
/// A viewer sent a chat message. "spawn" (re)spawns their avatar; every other message is forwarded to
/// that viewer's avatar as a command (see <see cref="StreamPlayer.OnChatCommand"/>).
/// </summary>
void Streamer.IEvents.OnStreamMessage( Streamer.ChatMessage message )
{
var viewer = message.Viewer;
if ( message.Message == "spawn" )
{
SpawnAvatar( viewer );
return;
}
// Route the message to this viewer's avatar, if they have one. This is the part that used to live
// on StreamPlayer; doing it here keeps every stream interaction in a single component.
if ( _avatars.TryGetValue( viewer, out var avatar ) && avatar.IsValid() )
{
avatar.OnChatCommand( message.Message );
}
}
/// <summary>
/// Clone the avatar prefab for a viewer and register it. If they already had an avatar it's removed
/// first, so sending "spawn" again acts as a respawn.
/// </summary>
void SpawnAvatar( Streamer.Viewer viewer )
{
RemoveAvatar( viewer );
// Clone disabled so we can configure the avatar in Setup() before its components start running,
// then enable it once it's ready.
var go = GameObject.Clone( ViewerPrefab, new CloneConfig { StartEnabled = false } );
go.Name = viewer.Username;
go.WorldPosition = (Vector3.Random * 300f).WithZ( 0 );
var avatar = go.GetComponent<StreamPlayer>( true );
avatar.Setup( viewer );
go.Enabled = true;
_avatars[viewer] = avatar;
}
/// <summary>
/// Remove and destroy a viewer's avatar, if they have one.
/// </summary>
void RemoveAvatar( Streamer.Viewer viewer )
{
if ( !_avatars.Remove( viewer, out var avatar ) )
return;
avatar?.GameObject?.Destroy();
}
protected override void OnUpdate()
{
OrbitCamera();
}
/// <summary>
/// Slowly orbit the scene camera so there's always something to look at.
/// </summary>
void OrbitCamera()
{
const float distance = 1100f;
var angle = (Rotation)new Angles( 45, Time.Now * 0.2f, 0 );
Scene.Camera.WorldPosition = angle.Backward * distance;
Scene.Camera.WorldRotation = angle;
}
}