using System; using System.Collections.Generic; using System.Diagnostics; using AtomicEngine; public class Vehicle : CSComponent { private List _wheels = new List(); private Input _input = AtomicNET.GetSubsystem(); private RigidBody2D _rigidBody; private ParticleEmitter2D _exhaustParticles; private SoundSource3D _soundSource; private SoundSource3D _suspensionSoundSource; private Sound _accelSound; private Sound _brakeSound; private Sound[] _suspensionSounds; private int _horsePower; private int _maxSpdFwd; private int _maxSpdBwd; private int _rollForce; //private float lastSuspensionSound; //TODO there's no Time subsystem? private DateTime lastSuspensionSound = DateTime.Now; // We use this for the wheel logic private class Wheel { private readonly RigidBody2D _rigidBody; private readonly ConstraintWheel2D _constraint; private readonly ParticleEmitter2D _particleEmitter; private readonly float _particlesDistance; // This struct is used to pass wheel dynamics data public struct DynamicsData { public bool IsInContact; public float Resistance; public float AngularVelocity; } public Wheel(RigidBody2D rigidBody, ConstraintWheel2D constraint, ParticleEmitter2D particleEmitter, float particlesDistance) { _rigidBody = rigidBody; _constraint = constraint; _particleEmitter = particleEmitter; _particlesDistance = particlesDistance; } // Applies force and returns the work (normalized) necessary to reach the target speed public DynamicsData ApplyNonLinearTorque(int power, int targetSpeed, bool onlyInContact = false) { bool inContact = EmitSurfaceParticles(); float fraction = _rigidBody.AngularVelocity/targetSpeed; float mult = fraction > 0 ? 1-fraction : 1; if (!onlyInContact || inContact) _rigidBody.ApplyTorque(mult*power, true); return new DynamicsData { Resistance = fraction*mult, AngularVelocity = _rigidBody.AngularVelocity, IsInContact = inContact }; } // Emit particles if close to a surface point and returns true if positive public bool EmitSurfaceParticles() { Vector3 nearestSurfPoint = AtomicMain.GetSurfacePointClosestToPoint(_rigidBody.Node); float contactDistance = Vector3.Distance(_rigidBody.Node.Position, nearestSurfPoint); if (contactDistance > _particlesDistance) { _particleEmitter.Effect.StartColor = new Color(0, 0, 0, 0); return false; } _particleEmitter.Effect.StartColor = Color.White; _particleEmitter.Node.Position = nearestSurfPoint; return true; } // Returns current suspension compression for the wheel public float CurrentSuspensionCompression() { Vector2 anchorPosition = new Vector2(_constraint.OwnerBody.Node.WorldPosition + _constraint.OwnerBody.Node.WorldRotation*new Vector3(_constraint.Anchor)); return Vector2.Distance(_constraint.OtherBody.Node.Position2D, anchorPosition); } } public Vehicle CreateChassis(Vector2 colliderCenter, float colliderRadius, int massDensity, Vector3 exhaustPosition, ParticleEffect2D exhaustParticles, Sound engineSound, Sound tireSound, Sound[] suspensionSounds, int horsePower, int maxSpeedFwd, int maxSpeedBwd, int rollForce) { // We set out private fields _horsePower = horsePower; _maxSpdFwd = maxSpeedFwd; _maxSpdBwd = maxSpeedBwd; _rollForce = rollForce; _rigidBody = GetComponent(); // We add the collider (circle collider at the moment) var col = AtomicMain.AddCollider(Node, dens:massDensity, fric:0); col.SetRadius(colliderRadius); col.SetCenter(colliderCenter); // We create the exhaust particle system var exhaustParticlesNode = Node.CreateChild(); exhaustParticlesNode.SetPosition(exhaustPosition); _exhaustParticles = exhaustParticlesNode.CreateComponent(); _exhaustParticles.SetEffect(exhaustParticles); // We setup the engine sound and other sound effect engineSound.SetLooped(true); _soundSource = Node.CreateComponent(); _soundSource.SetNearDistance(10); _soundSource.SetFarDistance(50); _accelSound = engineSound; _brakeSound = tireSound; _suspensionSoundSource = Node.CreateComponent(); _suspensionSounds = suspensionSounds; // We return the Vehicle for convenience, since this function is intended to be the vehicle's init function return this; } public Node CreateWheel(Sprite2D sprite, Vector2 relativePosition, float radius, int suspensionFrequency, float suspensionDamping, ParticleEffect2D particles, float distanceToEmitParticles) { Node wheelNode = AtomicMain.CreateSpriteNode(sprite); wheelNode.SetPosition2D(relativePosition); // CreateSpriteNode adds a RigidBody for us, so we get it here RigidBody2D wheelRigidBody = wheelNode.GetComponent(); // We activate CCD wheelRigidBody.SetBullet(true); AtomicMain.AddCollider(wheelNode).SetRadius(radius); // The Box2D wheel joint provides spring for simulating suspension ConstraintWheel2D wheelJoint = Node.CreateComponent(); wheelJoint.SetOtherBody(wheelRigidBody); wheelJoint.SetAnchor(relativePosition); wheelJoint.SetAxis(Vector2.UnitY); wheelJoint.SetFrequencyHz(suspensionFrequency); wheelJoint.SetDampingRatio(suspensionDamping); // Each wheel has a particle emitter to emit particles when it's in contact with the surface Node particlesNode = Node.Scene.CreateChild(); particlesNode.SetPosition(new Vector3(relativePosition.X, relativePosition.Y, 14)); ParticleEmitter2D particleEmitter = particlesNode.CreateComponent(); particleEmitter.SetEffect(particles); // We create a new Wheel struct and add to the _wheels list _wheels.Add(new Wheel(wheelRigidBody, wheelJoint, particleEmitter, distanceToEmitParticles)); return wheelNode; } public Node CreateHead(Sprite2D sprite, Vector3 relativePosition, float colliderRadius, Vector2 neckAnchor) { Node head = AtomicMain.CreateSpriteNode(sprite); head.SetPosition(relativePosition); AtomicMain.AddCollider(head).SetRadius(colliderRadius); // This is the actual neck joint ConstraintRevolute2D joint = head.CreateComponent(); joint.SetOtherBody(_rigidBody); joint.SetAnchor(neckAnchor); // This is the spring, it's attached to the body with an offset ConstraintDistance2D spring = head.CreateComponent(); spring.SetOtherBody(_rigidBody); spring.SetOwnerBodyAnchor(-Vector2.UnitY*2); spring.SetOtherBodyAnchor(Node.WorldToLocal2D(head.WorldPosition2D-Vector2.UnitY*2)); spring.SetFrequencyHz(3); spring.SetDampingRatio(0.4f); return head; } // Update is called once per frame private void Update(float dt) { // Wheel controls bool isBraking = _input.GetKeyDown((int) SDL.SDL_Keycode.SDLK_DOWN); bool isAccelerating = _input.GetKeyDown((int) SDL.SDL_Keycode.SDLK_UP); foreach (Wheel wheel in _wheels) { // We give priority to braking if (isBraking) { // This function also emit particles Wheel.DynamicsData wheelDynamics = wheel.ApplyNonLinearTorque(_horsePower, _maxSpdBwd, true); if (wheelDynamics.IsInContact) { // If the wheel is in contact, we play the braking sound according to the current work in the wheel if (_soundSource.Sound != _brakeSound || !_soundSource.IsPlaying()) _soundSource.Play(_brakeSound); _soundSource.SetFrequency(48000); _soundSource.Gain = wheelDynamics.Resistance*-1; if (_soundSource.Gain > 1) _soundSource.Gain = 1; } else { _soundSource.Stop(); } } else if (isAccelerating) { Wheel.DynamicsData wheelDynamics = wheel.ApplyNonLinearTorque(-_horsePower, -_maxSpdFwd); // We set the sound frequency according to the speed and gain according to the work being done _soundSource.SetFrequency((wheelDynamics.AngularVelocity/-40+1)*48000); _soundSource.Gain += 0.03f; if (_soundSource.Gain > 1) _soundSource.Gain = 1; _soundSource.Gain += Math.Abs(1-wheelDynamics.Resistance*5); if (_soundSource.Sound != _accelSound || !_soundSource.IsPlaying()) _soundSource.Play(_accelSound); } else { // If it's not receiving any input, fades out engine sound or stop other sounds if (_soundSource.Sound == _accelSound) { _soundSource.SetFrequency(48000); _soundSource.Gain -= 0.01f; if (_soundSource.Gain < 0.3f) _soundSource.Gain = 0.3f; } else _soundSource.Stop(); // We emit surface particles anyway wheel.EmitSurfaceParticles(); } } // Roll controls if (_input.GetKeyDown((int) SDL.SDL_Keycode.SDLK_LEFT)) _rigidBody.ApplyTorque(_rollForce,true); if (_input.GetKeyDown((int) SDL.SDL_Keycode.SDLK_RIGHT)) _rigidBody.ApplyTorque(_rollForce*-1,true); // Debug control for when you rolled over if (_input.GetKeyDown((int) SDL.SDL_Keycode.SDLK_SPACE)) _rigidBody.SetAngularVelocity(2); // We apply bit of the vehicle's velocity to the exhaust particles so they look OK Vector2 currentVelocity = _rigidBody.GetLinearVelocity(); _exhaustParticles.Effect.Speed = Math.Abs(100 + currentVelocity.LengthFast * 20); _exhaustParticles.Effect.SpeedVariance = currentVelocity.LengthFast * 10; _exhaustParticles.Effect.Angle = -Node.Rotation2D + (float)(Math.Atan2(currentVelocity.X - 5, -currentVelocity.Y) * (360 / (Math.PI * 2))) - 90; // Suspension sounds foreach (Wheel wheel in _wheels) { if (wheel.CurrentSuspensionCompression() > 0.7f) { if (DateTime.Now - lastSuspensionSound > TimeSpan.FromMilliseconds(500)) { lastSuspensionSound=DateTime.Now; _suspensionSoundSource.Play(_suspensionSounds[new Random().Next(_suspensionSounds.Length)]); } } } } }