using Sandbox; using System; using System.Collections.Generic; [Library( "ent_car", Title = "Car", Spawnable = true )] public partial class CarEntity : Prop, IUse { [ConVar.Replicated( "debug_car" )] public static bool debug_car { get; set; } = false; [ConVar.Replicated( "car_accelspeed" )] public static float car_accelspeed { get; set; } = 500.0f; private CarWheel frontLeft; private CarWheel frontRight; private CarWheel backLeft; private CarWheel backRight; private float frontLeftDistance; private float frontRightDistance; private float backLeftDistance; private float backRightDistance; private bool frontWheelsOnGround; private bool backWheelsOnGround; private float accelerateDirection; private float airRoll; private float airTilt; private float grip; private TimeSince timeSinceDriverLeft; [Net] private float WheelSpeed { get; set; } [Net] private float TurnDirection { get; set; } [Net] private float AccelerationTilt { get; set; } [Net] private float TurnLean { get; set; } [Net] public float MovementSpeed { get; private set; } [Net] public bool Grounded { get; private set; } private struct InputState { public float throttle; public float turning; public float breaking; public float tilt; public float roll; public void Reset() { throttle = 0; turning = 0; breaking = 0; tilt = 0; roll = 0; } } private InputState currentInput; public CarEntity() { frontLeft = new CarWheel( this ); frontRight = new CarWheel( this ); backLeft = new CarWheel( this ); backRight = new CarWheel( this ); } [Net] public Player driver { get; private set; } private ModelEntity chassis_axle_rear; private ModelEntity chassis_axle_front; private ModelEntity wheel0; private ModelEntity wheel1; private ModelEntity wheel2; private ModelEntity wheel3; public override void Spawn() { base.Spawn(); var modelName = "models/car/car.vmdl"; SetModel( modelName ); SetupPhysicsFromModel( PhysicsMotionType.Dynamic, false ); SetInteractsExclude( CollisionLayer.Player ); EnableSelfCollisions = false; var trigger = new ModelEntity { Parent = this, Position = Position, Rotation = Rotation, EnableTouch = true, CollisionGroup = CollisionGroup.Trigger, Transmit = TransmitType.Never, EnableSelfCollisions = false, }; trigger.SetModel( modelName ); trigger.SetupPhysicsFromModel( PhysicsMotionType.Keyframed, false ); } public override void ClientSpawn() { base.ClientSpawn(); { var vehicle_fuel_tank = new ModelEntity(); vehicle_fuel_tank.SetModel( "entities/modular_vehicle/vehicle_fuel_tank.vmdl" ); vehicle_fuel_tank.Transform = Transform; vehicle_fuel_tank.Parent = this; vehicle_fuel_tank.LocalPosition = new Vector3( 0.75f, 0, 0 ) * 40.0f; } { chassis_axle_front = new ModelEntity(); chassis_axle_front.SetModel( "entities/modular_vehicle/chassis_axle_front.vmdl" ); chassis_axle_front.Transform = Transform; chassis_axle_front.Parent = this; chassis_axle_front.LocalPosition = new Vector3( 1.05f, 0, 0.35f ) * 40.0f; { wheel0 = new ModelEntity(); wheel0.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); wheel0.SetParent( chassis_axle_front, "Wheel_Steer_R", new Transform( Vector3.Zero, Rotation.From( 0, 180, 0 ) ) ); } { wheel1 = new ModelEntity(); wheel1.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); wheel1.SetParent( chassis_axle_front, "Wheel_Steer_L", new Transform( Vector3.Zero, Rotation.From( 0, 0, 0 ) ) ); } { var chassis_steering = new ModelEntity(); chassis_steering.SetModel( "entities/modular_vehicle/chassis_steering.vmdl" ); chassis_steering.SetParent( chassis_axle_front, "Axle_front_Center", new Transform( Vector3.Zero, Rotation.From( -90, 180, 0 ) ) ); } } { chassis_axle_rear = new ModelEntity(); chassis_axle_rear.SetModel( "entities/modular_vehicle/chassis_axle_rear.vmdl" ); chassis_axle_rear.Transform = Transform; chassis_axle_rear.Parent = this; chassis_axle_rear.LocalPosition = new Vector3( -1.05f, 0, 0.35f ) * 40.0f; { var chassis_transmission = new ModelEntity(); chassis_transmission.SetModel( "entities/modular_vehicle/chassis_transmission.vmdl" ); chassis_transmission.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Zero, Rotation.From( -90, 180, 0 ) ) ); } { wheel2 = new ModelEntity(); wheel2.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); wheel2.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Left * (0.7f * 40), Rotation.From( 0, 90, 0 ) ) ); } { wheel3 = new ModelEntity(); wheel3.SetModel( "entities/modular_vehicle/wheel_a.vmdl" ); wheel3.SetParent( chassis_axle_rear, "Axle_Rear_Center", new Transform( Vector3.Right * (0.7f * 40), Rotation.From( 0, -90, 0 ) ) ); } } } protected override void OnDestroy() { base.OnDestroy(); if ( driver is SandboxPlayer player ) { RemoveDriver( player ); } } public void ResetInput() { currentInput.Reset(); } [Event.Tick.Server] protected void Tick() { if ( driver is SandboxPlayer player ) { if ( player.LifeState != LifeState.Alive || player.Vehicle != this ) { RemoveDriver( player ); } } } public override void Simulate( Client owner ) { if ( owner == null ) return; if ( !IsServer ) return; using ( Prediction.Off() ) { currentInput.Reset(); if ( Input.Pressed( InputButton.Use ) ) { if ( owner.Pawn is SandboxPlayer player && !player.IsUseDisabled() ) { RemoveDriver( player ); return; } } currentInput.throttle = (Input.Down( InputButton.Forward ) ? 1 : 0) + (Input.Down( InputButton.Back ) ? -1 : 0); currentInput.turning = (Input.Down( InputButton.Left ) ? 1 : 0) + (Input.Down( InputButton.Right ) ? -1 : 0); currentInput.breaking = (Input.Down( InputButton.Jump ) ? 1 : 0); currentInput.tilt = (Input.Down( InputButton.Run ) ? 1 : 0) + (Input.Down( InputButton.Duck ) ? -1 : 0); currentInput.roll = (Input.Down( InputButton.Left ) ? 1 : 0) + (Input.Down( InputButton.Right ) ? -1 : 0); } } [Event.Physics.PreStep] public void OnPrePhysicsStep() { if ( !IsServer ) return; var selfBody = PhysicsBody; if ( !selfBody.IsValid() ) return; var body = selfBody.SelfOrParent; if ( !body.IsValid() ) return; var dt = Time.Delta; body.DragEnabled = false; var rotation = selfBody.Rotation; accelerateDirection = currentInput.throttle.Clamp( -1, 1 ) * (1.0f - currentInput.breaking); TurnDirection = TurnDirection.LerpTo( currentInput.turning.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.001f, dt ) ); airRoll = airRoll.LerpTo( currentInput.roll.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.0001f, dt ) ); airTilt = airTilt.LerpTo( currentInput.tilt.Clamp( -1, 1 ), 1.0f - MathF.Pow( 0.0001f, dt ) ); float targetTilt = 0; float targetLean = 0; var localVelocity = rotation.Inverse * body.Velocity; if ( backWheelsOnGround || frontWheelsOnGround ) { var forwardSpeed = MathF.Abs( localVelocity.x ); var speedFraction = MathF.Min( forwardSpeed / 500.0f, 1 ); targetTilt = accelerateDirection.Clamp( -1.0f, 1.0f ); targetLean = speedFraction * TurnDirection; } AccelerationTilt = AccelerationTilt.LerpTo( targetTilt, 1.0f - MathF.Pow( 0.01f, dt ) ); TurnLean = TurnLean.LerpTo( targetLean, 1.0f - MathF.Pow( 0.01f, dt ) ); if ( backWheelsOnGround ) { var forwardSpeed = MathF.Abs( localVelocity.x ); var speedFactor = 1.0f - (forwardSpeed / 5000.0f).Clamp( 0.0f, 1.0f ); var acceleration = speedFactor * (accelerateDirection < 0.0f ? car_accelspeed * 0.5f : car_accelspeed) * accelerateDirection * dt; var impulse = rotation * new Vector3( acceleration, 0, 0 ); body.Velocity += impulse; } RaycastWheels( rotation, true, out frontWheelsOnGround, out backWheelsOnGround, dt ); var onGround = frontWheelsOnGround || backWheelsOnGround; var fullyGrounded = (frontWheelsOnGround && backWheelsOnGround); Grounded = onGround; if ( fullyGrounded ) { body.Velocity += PhysicsWorld.Gravity * dt; } body.GravityScale = fullyGrounded ? 0 : 1; bool canAirControl = false; var v = rotation * localVelocity.WithZ( 0 ); var vDelta = MathF.Pow((v.Length / 1000.0f).Clamp( 0, 1 ), 5.0f).Clamp(0, 1); if ( vDelta < 0.01f ) vDelta = 0; if ( debug_car ) { DebugOverlay.Line( body.MassCenter, body.MassCenter + rotation.Forward.Normal * 100, Color.White, 0, false ); DebugOverlay.Line( body.MassCenter, body.MassCenter + v.Normal * 100, Color.Green, 0, false ); } var angle = ( rotation.Forward.Normal * MathF.Sign( localVelocity.x )).Normal.Dot( v.Normal ).Clamp( 0.0f, 1.0f ); angle = angle.LerpTo( 1.0f, 1.0f - vDelta ); grip = grip.LerpTo( angle, 1.0f - MathF.Pow( 0.001f, dt ) ); if ( debug_car ) { DebugOverlay.ScreenText( new Vector2( 200, 200 ), $"{grip}" ); } var angularDamping = 0.0f; angularDamping = angularDamping.LerpTo( 5.0f, grip ); body.LinearDamping = 0.0f; body.AngularDamping = fullyGrounded ? angularDamping : 0.5f; if ( onGround ) { localVelocity = rotation.Inverse * body.Velocity; WheelSpeed = localVelocity.x; var turnAmount = frontWheelsOnGround ? (MathF.Sign( localVelocity.x ) * 25.0f * CalculateTurnFactor( TurnDirection, MathF.Abs( localVelocity.x ) ) * dt) : 0.0f; body.AngularVelocity += rotation * new Vector3( 0, 0, turnAmount ); airRoll = 0; airTilt = 0; var forwardGrip = 0.1f; forwardGrip = forwardGrip.LerpTo( 0.9f, currentInput.breaking ); body.Velocity = VelocityDamping( Velocity, rotation, new Vector3( forwardGrip, grip, 0 ), dt ); } else { var s = selfBody.Position + (rotation * selfBody.LocalMassCenter); var tr = Trace.Ray( s, s + rotation.Down * 50 ) .Ignore( this ) .Run(); if ( debug_car ) DebugOverlay.Line( tr.StartPos, tr.EndPos, tr.Hit ? Color.Red : Color.Green ); canAirControl = !tr.Hit; } if ( canAirControl && (airRoll != 0 || airTilt != 0) ) { var offset = 50 * Scale; var s = selfBody.Position + (rotation * selfBody.LocalMassCenter) + (rotation.Right * airRoll * offset) + (rotation.Down * (10 * Scale)); var tr = Trace.Ray( s, s + rotation.Up * (25 * Scale) ) .Ignore( this ) .Run(); if ( debug_car ) DebugOverlay.Line( tr.StartPos, tr.EndPos ); bool dampen = false; if ( currentInput.roll.Clamp( -1, 1 ) != 0 ) { var force = tr.Hit ? 400.0f : 100.0f; var roll = tr.Hit ? currentInput.roll.Clamp( -1, 1 ) : airRoll; body.ApplyForceAt( selfBody.MassCenter + rotation.Left * (offset * roll), (rotation.Down * roll) * (roll * (body.Mass * force)) ); if ( debug_car ) DebugOverlay.Sphere( selfBody.MassCenter + rotation.Left * (offset * roll), 8, Color.Red ); dampen = true; } if ( !tr.Hit && currentInput.tilt.Clamp( -1, 1 ) != 0 ) { var force = 200.0f; body.ApplyForceAt( selfBody.MassCenter + rotation.Forward * (offset * airTilt), (rotation.Down * airTilt) * (airTilt * (body.Mass * force)) ); if ( debug_car ) DebugOverlay.Sphere( selfBody.MassCenter + rotation.Forward * (offset * airTilt), 8, Color.Green ); dampen = true; } if ( dampen ) body.AngularVelocity = VelocityDamping( body.AngularVelocity, rotation, 0.95f, dt ); } localVelocity = rotation.Inverse * body.Velocity; MovementSpeed = localVelocity.x; } private static float CalculateTurnFactor( float direction, float speed ) { var turnFactor = MathF.Min( speed / 500.0f, 1 ); var yawSpeedFactor = 1.0f - (speed / 1000.0f).Clamp( 0, 0.6f ); return direction * turnFactor * yawSpeedFactor; } private static Vector3 VelocityDamping( Vector3 velocity, Rotation rotation, Vector3 damping, float dt ) { var localVelocity = rotation.Inverse * velocity; var dampingPow = new Vector3( MathF.Pow( 1.0f - damping.x, dt ), MathF.Pow( 1.0f - damping.y, dt ), MathF.Pow( 1.0f - damping.z, dt ) ); return rotation * (localVelocity * dampingPow); } private void RaycastWheels( Rotation rotation, bool doPhysics, out bool frontWheels, out bool backWheels, float dt ) { float forward = 42; float right = 32; var frontLeftPos = rotation.Forward * forward + rotation.Right * right + rotation.Up * 20; var frontRightPos = rotation.Forward * forward - rotation.Right * right + rotation.Up * 20; var backLeftPos = -rotation.Forward * forward + rotation.Right * right + rotation.Up * 20; var backRightPos = -rotation.Forward * forward - rotation.Right * right + rotation.Up * 20; var tiltAmount = AccelerationTilt * 2.5f; var leanAmount = TurnLean * 2.5f; float length = 20.0f; frontWheels = frontLeft.Raycast( length + tiltAmount - leanAmount, doPhysics, frontLeftPos * Scale, ref frontLeftDistance, dt ) | frontRight.Raycast( length + tiltAmount + leanAmount, doPhysics, frontRightPos * Scale, ref frontRightDistance, dt ); backWheels = backLeft.Raycast( length - tiltAmount - leanAmount, doPhysics, backLeftPos * Scale, ref backLeftDistance, dt ) | backRight.Raycast( length - tiltAmount + leanAmount, doPhysics, backRightPos * Scale, ref backRightDistance, dt ); } float wheelAngle = 0.0f; float wheelRevolute = 0.0f; [Event.Frame] public void OnFrame() { wheelAngle = wheelAngle.LerpTo( TurnDirection * 25, 1.0f - MathF.Pow( 0.001f, Time.Delta ) ); wheelRevolute += (WheelSpeed / (14.0f * Scale)).RadianToDegree() * Time.Delta; var wheelRotRight = Rotation.From( -wheelAngle, 180, -wheelRevolute ); var wheelRotLeft = Rotation.From( wheelAngle, 0, wheelRevolute ); var wheelRotBackRight = Rotation.From( 0, 90, -wheelRevolute ); var wheelRotBackLeft = Rotation.From( 0, -90, wheelRevolute ); RaycastWheels( Rotation, false, out _, out _, Time.Delta ); float frontOffset = 20.0f - Math.Min( frontLeftDistance, frontRightDistance ); float backOffset = 20.0f - Math.Min( backLeftDistance, backRightDistance ); chassis_axle_front.SetBoneTransform( "Axle_front_Center", new Transform( Vector3.Up * frontOffset ), false ); chassis_axle_rear.SetBoneTransform( "Axle_Rear_Center", new Transform( Vector3.Up * backOffset ), false ); wheel0.LocalRotation = wheelRotRight; wheel1.LocalRotation = wheelRotLeft; wheel2.LocalRotation = wheelRotBackRight; wheel3.LocalRotation = wheelRotBackLeft; } private void RemoveDriver( SandboxPlayer player ) { driver = null; player.Vehicle = null; player.VehicleController = null; player.VehicleAnimator = null; player.VehicleCamera = null; player.Parent = null; player.PhysicsBody.Enabled = true; player.PhysicsBody.Position = player.Position; timeSinceDriverLeft = 0; ResetInput(); } public bool OnUse( Entity user ) { if ( user is SandboxPlayer player && player.Vehicle == null && timeSinceDriverLeft > 1.0f ) { player.Vehicle = this; player.VehicleController = new CarController(); player.VehicleAnimator = new CarAnimator(); player.VehicleCamera = new CarCamera(); player.Parent = this; player.LocalPosition = Vector3.Up * 10; player.LocalRotation = Rotation.Identity; player.LocalScale = 1; player.PhysicsBody.Enabled = false; driver = player; } return true; } public bool IsUsable( Entity user ) { return driver == null; } public override void StartTouch( Entity other ) { base.StartTouch( other ); if ( !IsServer ) return; var body = PhysicsBody; if ( !body.IsValid() ) return; body = body.SelfOrParent; if ( !body.IsValid() ) return; if ( other is SandboxPlayer player && player.Vehicle == null ) { var speed = body.Velocity.Length; var forceOrigin = Position + Rotation.Down * Rand.Float( 20, 30 ); var velocity = (player.Position - forceOrigin).Normal * speed; var angularVelocity = body.AngularVelocity; OnPhysicsCollision( new CollisionEventData { Entity = player, Pos = player.Position + Vector3.Up * 50, Velocity = velocity, PreVelocity = velocity, PostVelocity = velocity, PreAngularVelocity = angularVelocity, Speed = speed, } ); } } protected override void OnPhysicsCollision( CollisionEventData eventData ) { if ( !IsServer ) return; if ( eventData.Entity is SandboxPlayer player && player.Vehicle != null ) { return; } var propData = GetModelPropData(); var minImpactSpeed = propData.MinImpactDamageSpeed; if ( minImpactSpeed <= 0.0f ) minImpactSpeed = 500; var impactDmg = propData.ImpactDamage; if ( impactDmg <= 0.0f ) impactDmg = 10; var speed = eventData.Speed; if ( speed > minImpactSpeed ) { if ( eventData.Entity.IsValid() && eventData.Entity != this ) { var damage = speed / minImpactSpeed * impactDmg * 1.2f; eventData.Entity.TakeDamage( DamageInfo.Generic( damage ) .WithFlag( DamageFlags.PhysicsImpact ) .WithFlag( DamageFlags.Vehicle ) .WithAttacker( driver != null ? driver : this, driver != null ? this : null ) .WithPosition( eventData.Pos ) .WithForce( eventData.PreVelocity ) ); } } } }