CharacterSeparation.cs
using Sandbox;
using System;
namespace PaintballBaddies;
/// <summary>Characters never physically collide with each other (see
/// Collision.config: that caused launches and jitter). Instead this gently
/// slides overlapping characters apart every frame, so the crew and the
/// sausages never stand inside each other and nobody gets flung.
/// Each object is only moved by whoever simulates it (the host for AI and
/// sausages, each player for themselves), never through walls.</summary>
public sealed class CharacterSeparation : Component
{
const float ActorRadius=15,SausageRadius=16,MaxSpeed=140,Response=9;
public static void Ensure(Scene scene)
{
if(scene is null || scene.GetAllComponents<CharacterSeparation>().Any())return;
new GameObject(true,"Character separation"){NetworkMode=NetworkMode.Never}.Components.Create<CharacterSeparation>();
}
private readonly List<(GameObject Go,float Radius,float Give,NavMeshAgent Agent)> bodies=new();
protected override void OnFixedUpdate()
{
bodies.Clear();
foreach(var c in Scene.GetAllComponents<PaintballCombatant>())
{
if(!c.IsValid() || !c.Active || c.Eliminated)continue;
// Humans give way less than AI, so the player never feels shoved.
bool human=c.Components.Get<PlayerController>().IsValid();
bool anchored=c.Components.Get<CoverController>() is {Attached:true} or {Sliding:true}
|| c.Components.Get<VaultController>()?.IsVaulting==true
|| c.Components.Get<CloseCombat>()?.Incapacitated==true
|| c.Components.Get<ArenaOpponent>()?.UsingCover==true;
bodies.Add((c.GameObject,ActorRadius,anchored ? 0 : human ? .25f : 1,c.Components.Get<NavMeshAgent>()));
}
foreach(var e in Scene.GetAllComponents<OverdriveEnemy>())
if(e.IsValid() && e.Active && !e.Dead)bodies.Add((e.GameObject,SausageRadius,e.FrozenRemaining>0 ? 0 : 1,e.Components.Get<NavMeshAgent>()));
if(bodies.Count<2)return;
var push=new Vector3[bodies.Count];
for(int i=0;i<bodies.Count;i++)
for(int j=i+1;j<bodies.Count;j++)
{
var a=bodies[i];var b=bodies[j];
var d=(a.Go.WorldPosition-b.Go.WorldPosition);
if(MathF.Abs(d.z)>60)continue;
d=d.WithZ(0);float min=a.Radius+b.Radius;float len=d.Length;
if(len>=min)continue;
// Exactly on top of each other: split along a stable direction.
var n=len>.01f ? d/len : Rotation.FromYaw((i*73+j*151)%360).Forward;
float overlap=min-len,total=a.Give+b.Give;
if(total<=0)continue;
push[i]+=n*overlap*(a.Give/total);push[j]-=n*overlap*(b.Give/total);
}
for(int i=0;i<bodies.Count;i++)
{
if(push[i].Length<.05f)continue;
var b=bodies[i];
if(b.Go.IsProxy)continue;
var step=push[i]*MathF.Min(1,Response*Time.Delta);
float max=MaxSpeed*Time.Delta;if(step.Length>max)step=step.Normal*max;
var from=b.Go.WorldPosition+Vector3.Up*24;
var hit=Scene.Trace.Sphere(8,from,from+step).WithoutTags("paintball_actor","overdrive_enemy","paintball_debris","trigger").Run();
if(hit.Hit || hit.StartedSolid)continue;
var to=b.Go.WorldPosition+step;
if(b.Agent.IsValid() && b.Agent.UpdatePosition)b.Agent.SetAgentPosition(to);
else b.Go.WorldPosition=to;
}
}
}