Unit entity class for the game, derived from Thing. It manages unit state like health/armor, flashing, flinching, jumping, repel/explosion velocities, hitstop, shaking, status handling, and spawns VFX/prefabs and RPCs to sync effects across network.
using Sandbox;
using Sandbox.Diagnostics;
using System;
public enum UnitFlashType { PlayerDmg, EnemyDmg, SelfDmg, Heal, BossDmg }
public partial class Unit : Thing
{
[Property] public SkinnedModelRenderer ModelRenderer { get; set; }
public virtual float HpPercent => 0f;
public virtual bool ShowHealthbar => false;
public virtual float HealthbarWidth => 650f; // todo: override for certain minibosses
public virtual float HealthbarOffset => 100f;
public virtual float HealthbarOpacity => 1f;
public virtual float HealthbarArmorOpacity => 1f;
[Sync] public bool IsDying { get; set; }
[Sync] public int Armor { get; set; }
[Sync] public RealTimeSince RealTimeSinceArmorChanged { get; protected set; }
public bool IsFlashing { get; protected set; }
protected TimeSince _timeSinceFlash;
protected RealTimeSince _realTimeSinceFlash;
protected float _flashTime;
public bool IsFlinching { get; set; }
public TimeSince _timeSinceLastFlinch { get; set; }
private float _flinchTime;
public TimeSince TimeSinceJump { get; set; }
public float JumpTotalTime { get; set; }
public float JumpMaxHeight { get; set; }
public Vector2 JumpStartPos { get; set; }
public Vector2 JumpTargetPos { get; set; }
public Vector2 RepelVelocity { get; private set; }
public float MaxRepelVelocity { get; set; }
public Vector2 ExplosionVelocity { get; set; }
public float RepelDeceleration { get; set; }
public float ExplosionDeceleration { get; set; }
public float Height { get; set; }
public float AnimSpeedModifier { get; set; }
public virtual bool IsInanimate => false;
public virtual bool CanBeTargeted => true;
public bool HitstopActive { get; set; }
protected TimeSince _timeSinceHitstop;
protected float _hitstopTime;
public virtual bool CanHitstop => true;
public float HitstopOldPlaybackSpeed { get; set; }
public bool IsShaking { get; set; }
private RealTimeSince _realTimeSinceShake;
protected float _shakeStrengthStart;
protected float _shakeStrengthEnd;
private float _shakeTime;
private EasingType _shakeEasingType;
public virtual bool IsBoss => false;
/// <summary>
/// Multiplied by Height to find the y-pos of status particles (ignore if 0)
/// </summary>
public virtual float ParticleYPosOverride => 0f;
public virtual float StunParticleYPosOverride => 0f;
protected override void OnStart()
{
base.OnStart();
RepelDeceleration = 12.9f;
ExplosionDeceleration = 2f;
MaxRepelVelocity = 500f;
if ( Collider is CapsuleCollider capsuleCollider )
Height = (capsuleCollider.End.z - capsuleCollider.Start.z) * SpawnScale.z;
else if ( Collider is SphereCollider sphereCollider )
Height = sphereCollider.Radius * 2f;
AnimSpeedModifier = 1f;
if(ShowHealthbar)
{
var healthbarGo = GameObject.Clone( "prefabs/unit_healthbar.prefab", new CloneConfig { StartEnabled = true, Parent = GameObject } );
var healthbar = healthbarGo.GetComponent<UnitHealthbar>();
//healthbar.Unit = this;
}
}
protected override void OnUpdate()
{
base.OnUpdate();
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"TimeScale: {TimeScale}", new global::Transform( WorldPosition ) );
if ( IsShaking )
HandleShaking();
if ( HitstopActive )
{
OnHitstopUpdate();
if ( _timeSinceHitstop > _hitstopTime )
HitstopEnd();
}
if ( IsProxy )
return;
if( !HitstopActive )
{
if ( RepelVelocity.LengthSquared > 0f )
HandleRepelFromThings();
if ( ExplosionVelocity.LengthSquared > 0f )
HandleExplosionVelocity();
if ( IsInTheAir )
HandleJumping();
}
if ( Manager.Instance.IsGameOver )
return;
HandleStatuses( Time.Delta );
}
void HandleRepelFromThings()
{
WorldPosition += (Vector3)RepelVelocity * Time.Delta;
RepelVelocity *= Math.Max( 1f - Time.Delta * RepelDeceleration * Manager.Instance.GlobalFrictionModifier, 0f );
}
public void AddRepelVelocity( Vector2 vel )
{
RepelVelocity += vel;
if(RepelVelocity.LengthSquared > MaxRepelVelocity * MaxRepelVelocity)
RepelVelocity = RepelVelocity.Normal * MaxRepelVelocity;
}
public void ResetRepelVelocity()
{
RepelVelocity = Vector2.Zero;
}
void HandleExplosionVelocity()
{
WorldPosition += (Vector3)ExplosionVelocity * (1f / Weight) * Time.Delta;
ExplosionVelocity *= Math.Max( 1f - Time.Delta * ExplosionDeceleration * Manager.Instance.GlobalFrictionModifier, 0f );
}
void HandleJumping()
{
if( TimeSinceJump > JumpTotalTime )
{
JumpFinishRpc();
}
else
{
var pos = Vector2.Lerp( JumpStartPos, JumpTargetPos, Utils.Map( TimeSinceJump, 0f, JumpTotalTime, 0f, 1f, EasingType.Linear ) );
var zPos = Utils.MapReturn( TimeSinceJump, 0f, JumpTotalTime, 0f, JumpMaxHeight, EasingType.QuadOut );
WorldPosition = new Vector3( pos.x, pos.y, zPos );
}
}
[Rpc.Broadcast]
public void FlashRpc( float time, UnitFlashType flashType )
{
Flash( time, flashType );
}
public virtual void Flash( float time, UnitFlashType flashType )
{
//if ( IsFlashing )
// return;
IsFlashing = true;
_timeSinceFlash = 0f;
_realTimeSinceFlash = 0f;
_flashTime = time;
}
protected virtual void HandleFlashing()
{
var timeSince = Manager.Instance.IsPaused ? _timeSinceFlash.Relative : _realTimeSinceFlash.Relative;
if ( IsFlashing && timeSince > _flashTime )
StopFlashing();
}
protected virtual void StopFlashing()
{
IsFlashing = false;
}
public virtual void Flinch( float time, Vector2 dir )
{
if( time > 0f )
{
IsFlinching = true;
_timeSinceLastFlinch = 0f;
_flinchTime = time;
}
}
protected virtual void HandleFlinching()
{
if ( _timeSinceLastFlinch > _flinchTime )
StopFlinching();
}
public virtual void StopFlinching()
{
IsFlinching = false;
}
[Rpc.Broadcast(NetFlags.Reliable)]
public void JumpRpc( Vector2 targetPos, float height, float lifetime )
{
Jump( targetPos, height, lifetime );
}
protected virtual void Jump( Vector2 targetPos, float height, float lifetime )
{
if ( IsInTheAir )
return;
IsInTheAir = true;
JumpStartPos = Position2D;
JumpTargetPos = Manager.Instance.ClampPosToBounds( targetPos );
if ( IsProxy )
return;
TimeSinceJump = 0f;
JumpTotalTime = lifetime;
JumpMaxHeight = height;
Velocity = Vector2.Zero;
}
[Rpc.Broadcast]
public void JumpFinishRpc()
{
JumpFinish();
}
public virtual void CancelJump()
{
if ( !IsInTheAir )
return;
IsInTheAir = false;
WorldPosition = WorldPosition.WithZ( 0f );
if ( IsProxy )
return;
Velocity = Vector2.Zero;
RepelVelocity = Vector2.Zero;
ExplosionVelocity = Vector2.Zero;
}
public virtual void JumpFinish()
{
IsInTheAir = false;
GameObject.Clone( "prefabs/effects/cloud.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( ((Vector3)JumpTargetPos).WithZ( 10f ) ) } );
// todo: sfx
if ( IsProxy )
return;
WorldPosition = new Vector3( JumpTargetPos.x, JumpTargetPos.y, 0f );
Velocity = Vector2.Zero;
RepelVelocity = Vector2.Zero;
ExplosionVelocity = Vector2.Zero;
}
protected override void OnOutOfBounds( Direction direction )
{
base.OnOutOfBounds( direction );
if ( direction == Direction.Left )
{
RepelVelocity = new Vector2( Math.Abs( RepelVelocity.x ), RepelVelocity.y );
ExplosionVelocity = new Vector2( Math.Abs( ExplosionVelocity.x ), ExplosionVelocity.y );
}
else if ( direction == Direction.Right )
{
RepelVelocity = new Vector2( -Math.Abs( RepelVelocity.x ), RepelVelocity.y );
ExplosionVelocity = new Vector2( -Math.Abs( ExplosionVelocity.x ), ExplosionVelocity.y );
}
else if ( direction == Direction.Down )
{
RepelVelocity = new Vector2( RepelVelocity.x, Math.Abs( RepelVelocity.y ) );
ExplosionVelocity = new Vector2( ExplosionVelocity.x, Math.Abs( ExplosionVelocity.y ) );
}
else if ( direction == Direction.Up )
{
RepelVelocity = new Vector2( RepelVelocity.x, -Math.Abs( RepelVelocity.y ) );
ExplosionVelocity = new Vector2( ExplosionVelocity.x, -Math.Abs( ExplosionVelocity.y ) );
}
}
[Rpc.Owner]
public void AddExplosionVelocity( Vector2 vel )
{
ExplosionVelocity += vel;
}
public override void Colliding( Thing other, float percent, float dt )
{
base.Colliding( other, percent, dt );
for ( int i = UnitStatuses.Count - 1; i >= 0; i-- )
UnitStatuses.Values.ElementAt( i ).Colliding( other, percent, dt );
}
public virtual void SetTimeScale( float timeScale )
{
AnimSpeedModifier = timeScale;
TimeScale = timeScale;
}
[Rpc.Broadcast]
public void HitstopRpc( float time )
{
}
public void Hitstop( float time )
{
if ( IsBoss )
time *= 0.66f;
if ( HitstopActive || _timeSinceHitstop < 0.1f || time < 0.001f )
return;
HitstopActive = true;
_timeSinceHitstop = 0f;
_hitstopTime = time;
//_hitstopOldPlaybackSpeed
OnHitstopStart();
}
protected virtual void OnHitstopStart()
{
}
protected virtual void OnHitstopUpdate()
{
}
public void HitstopEnd()
{
HitstopActive = false;
OnHitstopEnd();
}
protected virtual void OnHitstopEnd()
{
}
[Rpc.Broadcast]
public void ShakeRpc( float startStrength, float endStrength, float time, EasingType easingType = EasingType.Linear )
{
Shake( startStrength, endStrength, time, easingType );
}
public void Shake( float startStrength, float endStrength, float time, EasingType easingType = EasingType.Linear )
{
IsShaking = true;
_realTimeSinceShake = 0f;
_shakeStrengthStart = startStrength;
_shakeStrengthEnd = endStrength;
_shakeTime = time;
_shakeEasingType = easingType;
}
void HandleShaking()
{
if ( _realTimeSinceShake > _shakeTime )
StopShaking();
else
UpdateShaking( Utils.Map( _realTimeSinceShake, 0f, _shakeTime, _shakeStrengthStart, _shakeStrengthEnd, _shakeEasingType ) );
}
protected virtual void UpdateShaking( float strength )
{
}
protected virtual void StopShaking()
{
IsShaking = false;
}
[Rpc.Owner]
public void GainArmorRpc( int amount )
{
GainArmor( amount );
}
public void GainArmor( int amount )
{
Assert.True( !IsProxy );
AdjustArmor( amount, Vector2.Zero );
}
public void LoseArmor( int amount, Vector2 dir, bool playSfx = true )
{
AdjustArmor( -amount, dir, playSfx );
}
void AdjustArmor( int amount, Vector2 flinchDir, bool playSfx = true )
{
if ( IsProxy || amount == 0 )
return;
Armor = Math.Clamp( Armor + amount, 0, 999 );
RealTimeSinceArmorChanged = 0f;
ArmorVfx( amount, flinchDir, playSfx );
if( amount > 0 && this is Player player )
{
player.AddResultsStat( ResultStat.ArmorGained, 1 );
}
}
[Rpc.Broadcast]
public void ArmorVfx( int amount, Vector2 flinchDir, bool playSfx = false )
{
var isPlayer = this is Player;
if( isPlayer )
{
float size = Utils.Map( Math.Abs( amount ), 1, 15, 1.25f, 2.5f, EasingType.Linear );
var color = amount > 0 ? new Color( 0.7f, 0.7f, 0.7f ) : new Color( 0.65f, 0.5f, 0.5f );
var str = $"{(amount > 0 ? "+" : "-")}{MathF.Abs( amount )}⛊";
Manager.Instance.SpawnFloaterText( WorldPosition.WithZ( 65f ), str, color, size, amount > 0 ? FloaterType.ArmorGain : FloaterType.ArmorLose );
}
if ( amount < 0 )
{
ArmorFlinch( amount, flinchDir );
}
//FlashHeal( 0.12f );
if ( playSfx )
{
int armorSfxMax = isPlayer ? 50 : 300;
if ( amount > 0 )
Manager.Instance.PlaySfxNearby( "armor_gain", Position2D, pitch: Utils.Map( amount, 1, 10, 0.8f, 1f, EasingType.Linear ) * Utils.Map( Armor, 0, 50, 0.8f, 1.1f, EasingType.Linear ) * Game.Random.Float( 0.97f, 1.03f ), volume: Utils.Map( amount, 1, 10, 0.85f, 1f ), maxDist: 320f );
else
Manager.Instance.PlaySfxNearby( "armor_hit", Position2D, pitch: Utils.Map( Armor, armorSfxMax, 0, 0.8f, 1f, EasingType.Linear ) * Game.Random.Float( 0.97f, 1.03f ), volume: Utils.Map( amount, 1, 10, 1.2f, 1.6f ), maxDist: 350f );
}
}
protected virtual void ArmorFlinch( int amount, Vector2 flinchDir )
{
}
public void CreateTeleportParticle( Vector2 pos, float radius, Color color, float lifetime, bool useRealTime = false )
{
var zPos = 10f * Game.Random.Float( 0.95f, 1.05f );
var angle = 90f + Game.Random.Float( -10f, 10f );
var particleGo = GameObject.Clone( $"prefabs/effects/blink.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), new Angles( 90f, angle, 0f ) ) } );
var particleEffect = particleGo.GetComponent<ParticleEffect>();
particleEffect.Tint = color;
particleEffect.Lifetime = lifetime;
var particleSpriteRenderer = particleGo.GetComponent<ParticleSpriteRenderer>();
particleSpriteRenderer.Scale = radius;
var particleGo2 = GameObject.Clone( $"prefabs/effects/blink2.prefab", new CloneConfig { StartEnabled = true, Transform = new Transform( new Vector3( pos.x, pos.y, zPos ), new Angles( 90f, 90f, 0f ) ) } );
var particleEffect2 = particleGo2.GetComponent<ParticleEffect>();
if ( useRealTime )
{
particleEffect.Timing = ParticleEffect.TimingMode.RealTime;
particleEffect2.Timing = ParticleEffect.TimingMode.RealTime;
}
}
}