FootballCrowd.cs

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.

ReflectionNative Interop
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++;
    }
}