The class manages the rendering and animation of a synthetic crowd. It calculates the position and vertices for fan geometry based on a game atmosphere state.
using Sandbox;
using Socer;
using System;
using System.Collections.Generic;
public sealed partial class FootballWorldPanel
{
static readonly Color32[] CrowdShirts={new(49,141,182,255),new(204,105,72,255),new(218,216,200,255),new(53,69,89,255),new(215,180,86,255),new(71,121,99,255)};
static readonly Color32[] CrowdSkin={new(185,126,87,255),new(229,180,142,255),new(135,88,57,255)};
// World axes retain each editable section's scale. Default axes preserve
// the original procedural stadium and its editor bake path.
readonly record struct AnimatedFan(Point Position,float Level,int Stand,int Hash,Vector3 Right=default,Vector3 Up=default);
readonly List<AnimatedFan> animatedFans=new();
SceneDynamicObject animatedCrowd;
Vertex[] animatedVertices;
float nextCrowdFrame;
int crowdFrames;
public int AnimatedFanCount=>animatedFans.Count;
public int CrowdFrames=>crowdFrames;
void InitializeAnimatedCrowd()
{
animatedVertices=new Vertex[animatedFans.Count*6*4];
animatedCrowd=new SceneDynamicObject(RenderScene.SceneWorld)
{Material=DynamicGeometryMaterial};
animatedCrowd.Attributes.SetCombo("D_NO_CULLING",1);
animatedCrowd.Flags.CastShadows=false;
UpdateAnimatedCrowd(true);
}
void UpdateAnimatedCrowd(bool force=false)
{
if(animatedCrowd==null || Game?.Atmosphere==null || (!force && Time.Now<nextCrowdFrame))return;
nextCrowdFrame=Time.Now+.125f;
var mood=Game.Atmosphere;float t=mood.Pulse;
int index=0;
void Quad(Vector3 p,float low,float high,float width,Color32 colour,Vector3 right,Vector3 up)
{
var half=right*(width*.5f*Units);
var a=p-half+up*(low*Units);
var b=p+half+up*(low*Units);
var c=p+half+up*(high*Units);
var d=p-half+up*(high*Units);
animatedVertices[index++]=new Vertex(a,colour);animatedVertices[index++]=new Vertex(b,colour);
animatedVertices[index++]=new Vertex(c,colour);animatedVertices[index++]=new Vertex(a,colour);
animatedVertices[index++]=new Vertex(c,colour);animatedVertices[index++]=new Vertex(d,colour);
}
foreach(var fan in animatedFans)
{
float cheer=fan.Stand is 0 or 2?mood.HomeReaction:mood.AwayReaction;
float boo=mood.BooReaction;
float beat=t*(cheer>.25f?9:2.4f)+fan.Hash*.64f;
float bounce=MathF.Max(0,MathF.Sin(beat))*(.018f+cheer*.13f);
float sway=MathF.Sin(beat*.52f)*(boo*.085f+.018f);
var right=fan.Right.LengthSquared>.000001f?fan.Right:fan.Stand<2?Vector3.Forward:new Vector3(0,-1,0);
var up=fan.Up.LengthSquared>.000001f?fan.Up:Vector3.Up;
var center=WorldPoint(fan.Position,fan.Level)+right*(sway*Units)+up*(bounce*Units);
var shirt=CrowdShirts[fan.Hash%CrowdShirts.Length];var face=CrowdSkin[fan.Hash%CrowdSkin.Length];
Quad(center,.34f,.87f,.38f,shirt,right,up);
Quad(center,.85f,1.10f,.25f,face,right,up);
for(int side=-1;side<=1;side+=2)
{
var arm=center+right*(side*.27f*Units);
float raised=Math.Clamp(cheer*.58f+MathF.Max(0,MathF.Sin(beat+side))*.14f-boo*.3f,0,.7f);
Quad(arm,.52f+raised*.35f,.78f+raised,.12f,shirt,right,up);
}
}
animatedCrowd.Init(Graphics.PrimitiveType.Triangles);
animatedCrowd.AddVertex(animatedVertices.AsSpan(0,index));
crowdFrames++;
}
}