StreamPlayer.cs
/// <summary>
/// One viewer's avatar in the world.
///
/// Note that this component does NOT listen to the stream itself - it doesn't implement
/// <see cref="Streamer.IEvents"/>. Instead the <see cref="StreamGameManager"/> owns all the stream wiring
/// and, once it knows a chat message belongs to this viewer, calls <see cref="OnChatCommand"/>. That keeps
/// the avatar simple: it just knows how to act on a command and how to animate.
/// </summary>
public sealed class StreamPlayer : Component
{
[Property]
public SoundEvent PoopSound { get; set; }
/// <summary>
/// The viewer this avatar represents. Set in <see cref="Setup"/> when the manager spawns us.
/// </summary>
public Streamer.Viewer Viewer { get; private set; }
NavMeshAgent _agent;
SkinnedModelRenderer _renderer;
Rotation _targetRotation = Rotation.Identity;
bool _ducked;
/// <summary>
/// Called by <see cref="StreamGameManager"/> right after the avatar is cloned (and before it's enabled).
/// Caches the components we'll use, points the name tag at the viewer, gives them a random facing, and
/// randomises their outfit.
/// </summary>
public void Setup( Streamer.Viewer viewer )
{
Viewer = viewer;
_agent = GetComponent<NavMeshAgent>( true );
_renderer = GetComponentInChildren<SkinnedModelRenderer>( true );
if ( GetComponentInChildren<PlayerTag>( true ) is PlayerTag tag )
tag.Viewer = viewer;
_targetRotation = new Angles( 0, Random.Shared.Float( -360, 360 ), 0 );
WorldRotation = _targetRotation;
if ( GetComponentInChildren<Dresser>( true ) is Dresser dresser )
dresser.Randomize();
}
/// <summary>
/// Act on a chat message aimed at this avatar. The manager has already matched the message to our
/// <see cref="Viewer"/>, so all we do here is map the text to an action. Unknown text is ignored.
/// </summary>
public void OnChatCommand( string command )
{
switch ( command )
{
case "go":
case "forward":
MoveBy( WorldRotation.Forward.WithZ( 0 ) * 100 );
break;
case "back":
MoveBy( WorldRotation.Forward.WithZ( 0 ) * -100 );
break;
case "move":
MoveBy( Vector3.Random * 100 );
break;
case "left":
_targetRotation *= new Angles( 0, 45, 0 );
break;
case "right":
_targetRotation *= new Angles( 0, -45, 0 );
break;
case "turn":
_targetRotation *= new Angles( 0, 180, 0 );
break;
case "duck":
_ducked = !_ducked;
break;
case "poop":
Poop();
break;
}
}
protected override void OnUpdate()
{
// Ease toward whatever rotation the latest turn command asked for...
WorldRotation = Rotation.Lerp( WorldRotation, _targetRotation, Time.Delta * 5f );
// ...and keep the animation graph in step with the NavMeshAgent's movement.
Animate();
}
/// <summary>
/// Walk to a point relative to where we are now.
/// </summary>
void MoveBy( Vector3 offset )
{
_agent.MoveTo( WorldPosition + offset );
}
/// <summary>
/// Drop a little pile of physics cubes behind the avatar and bump this viewer's score. The score is
/// stored on the viewer's <see cref="Streamer.Viewer.Data"/> bag rather than on this component, so any
/// UI (here, the ScoreBoard) can read it straight off the viewer without needing a reference to us.
/// </summary>
void Poop()
{
for ( int i = 0; i < 10; i++ )
{
var poop = new GameObject( true, "Poop" );
poop.WorldPosition = WorldPosition + WorldRotation.Up * 40 + WorldRotation.Backward * 10;
poop.WorldScale = new Vector3(
Random.Shared.Float( 0.05f, 0.1f ),
Random.Shared.Float( 0.05f, 0.2f ),
Random.Shared.Float( 0.05f, 0.07f ) );
var renderer = poop.AddComponent<ModelRenderer>();
renderer.Model = Model.Cube;
renderer.Tint = "#070402";
poop.AddComponent<BoxCollider>().Elasticity = 0.6f;
poop.AddComponent<Rigidbody>().Velocity = WorldRotation.Backward * Random.Shared.Float( 10, 500 );
}
GameObject.PlaySound( PoopSound );
Viewer.Data.Set( "poops", Viewer.Data.Get( "poops", 0 ) + 1 );
}
/// <summary>
/// Feed the NavMeshAgent's movement into the animation graph so the citizen model walks, turns and ducks.
/// </summary>
void Animate()
{
// The citizen animgraph takes two velocity sets: "move_*" for actual movement and "wish_*" for
// intended movement (used for things like leaning and arm-swing).
SetMovement( "move", _agent.Velocity );
SetMovement( "wish", _agent.WishVelocity );
_renderer.Set( "duck", _ducked ? 1 : 0 );
}
/// <summary>
/// Push a velocity into the animation graph under one of its parameter sets ("move" or "wish").
/// </summary>
void SetMovement( string set, Vector3 velocity )
{
var forward = _renderer.WorldRotation.Forward.Dot( velocity );
var sideward = _renderer.WorldRotation.Right.Dot( velocity );
var angle = MathF.Atan2( sideward, forward ).RadianToDegree().NormalizeDegrees();
_renderer.Set( $"{set}_direction", angle );
_renderer.Set( $"{set}_speed", velocity.Length );
_renderer.Set( $"{set}_groundspeed", velocity.WithZ( 0 ).Length );
_renderer.Set( $"{set}_x", forward );
_renderer.Set( $"{set}_y", sideward );
_renderer.Set( $"{set}_z", velocity.z );
}
}