using System; using NWH.DWP2.WaterData; using UnityEngine; using UnityEngine.Serialization; namespace NWH.DWP2.ShipController { /// /// Engine object. Contains all the parameters related to ships's propulsion systems. /// [RequireComponent(typeof(AdvancedShipController))] [Serializable] public class Engine { public enum ThrottleBinding { Throttle, Throttle2, Throttle3, Throttle4 } public ThrottleBinding throttleBinding = ThrottleBinding.Throttle; public enum RotationDirection { Left, Right, } public enum State { On, Off, Starting, Stopping, } public string name = "Engine"; /// /// Should the engine start when the throttle is applied? /// [Tooltip("Should the engine start when the throttle is applied?")] public bool startOnThrottle = true; /// /// Is the engine currently on? /// [FormerlySerializedAs("_isOn")] public bool isOn; /// /// Min RPM of the engine. /// [FormerlySerializedAs("_minRPM")] [Tooltip("Min RPM of the engine.")] public float minRPM = 800; /// /// Max RPM of the engine. /// [FormerlySerializedAs("_maxRPM")] [Tooltip("Max RPM of the engine.")] public float maxRPM = 6000; /// /// Thrust at max RPM. /// [FormerlySerializedAs("_maxThrust")] [Tooltip("Thrust at max RPM.")] public float maxThrust = 5000; /// /// Time needed to spin up the engines up to max RPM /// [FormerlySerializedAs("_spinUpTime")] [Tooltip("Time needed to spin up the engines up to max RPM")] public float spinUpTime = 2f; /// /// Engine RPM when turning over. Used to determine starting sound pitch. /// [FormerlySerializedAs("startingRpm")] [Tooltip("Used to determine starting sound pitch. Engine RPM when turning over.")] public float startingRPM = 300f; /// /// How long the engine starting phase take? /// [Tooltip("How long will the engine starting take?")] public float startDuration = 1.3f; /// /// How long will the engine stopping phase take? /// [Tooltip("How long will the engine stopping take?")] public float stopDuration = 0.8f; /// /// Local position where the thust is applied, relative to ship. /// [Tooltip("Local position at which the force will be applied.")] public Vector3 thrustPosition; /// /// Local direction in which the force will be applied. Does not affect the rotation /// of the propeller, which always happens around the local Z-axis of the propeller transform. /// [Tooltip("Local direction in which the force will be applied. Does not affect the rotation " + "of the propeller, which always happens around the local Z-axis of the propeller transform.")] public Vector3 thrustDirection = Vector3.forward; /// /// Should thrust be applied when above water? /// [Tooltip("Should thrust be applied when above water?")] public bool applyThrustWhenAboveWater; /// /// Amount of thrust that will be applied if ship is reversing /// [Tooltip("Amount of thrust that will be applied if ship is reversing.")] public float reverseThrustCoefficient = 0.3f; /// /// Ship peed at which propeller will reach it's maximum rotational speed. /// [Tooltip("Ship speed at which propeller will reach it's maximum rotational speed.")] public float maxSpeed = 20f; /// /// Thrust curve of the propeller. X axis is speed in m/s and y axis is efficiency. /// [Tooltip("Thrust curve of the propeller. X axis is speed in m/s and y axis is efficiency.")] public AnimationCurve thrustCurve = new AnimationCurve(new Keyframe(0f, 1f), new Keyframe(0.5f, 0.95f), new Keyframe(1f, 0f)); /// /// Optional. Only use if you vessel has propeller mounted to the rudder (as in outboard engines). Propuslion force /// direction will be rotated with rudder if assigned. /// [Tooltip( "Optional. Only use if you vessel has propeller mounted to the rudder (as in outboard engines). Propuslion force direction will be rotated with rudder if assigned.")] public Transform rudderTransform; /// /// Optional. Propeller transform. Visual rotation only, does not affect physics. /// [Tooltip("Optional. Propeller transform. Visual rotation only, does not affect physics.")] public Transform propellerTransform; /// /// Engine RPM will be multiplied by this value to get rotation speed of the propeller. Animation only. /// [Tooltip("Engine RPM will be multiplied by this value to get rotation speed of the propeller. Animation only.")] public float propellerRpmRatio = 0.1f; /// /// Direction of propeller rotation. Affects animation only. /// [Tooltip("Direction of propeller rotation. Animation only.")] public RotationDirection rotationDirection = RotationDirection.Right; /// /// Engine running audio source. /// [Tooltip("Engine running audio source.")] public AudioSource runningSource; /// /// Engine starting source. /// [Tooltip("[Optional] Sound of engine starting. If left empty fade-in will be used.")] public AudioSource startingSource; /// /// Engine stopping source. /// [Tooltip("[Optional] Sound of engine stopping. If left empty cut-out will be used.")] public AudioSource stoppingSource; /// /// Base volume of the engine /// [Tooltip("Base (idle) volume of the engine.")] [Range(0, 2)] public float volume = 0.2f; /// /// Idle pitch of the engine /// [Tooltip("Base (idle) pitch of the engine.")] [Range(0, 2)] public float pitch = 0.5f; /// /// Volume range of the engine. /// [Tooltip("Volume range of the engine.")] [Range(0, 2)] public float volumeRange = 0.8f; /// /// Pitch range of the engine. /// [Tooltip("Pitch range of the engine.")] [Range(0, 2)] public float pitchRange = 1f; private float _rpm; private float _spinVelocity; private State _engineState; private float _startTime; private float _stopTime; private bool _wasOn; private AdvancedShipController _sc; public State EngineState { get => _engineState; set => _engineState = value; } /// /// Current RPM of the engine /// public float RPM { get { return Mathf.Clamp(_rpm, minRPM, maxRPM); } } /// /// Percentage of rpm range engine is currently on /// public float RpmPercent { get { return Mathf.Clamp01((RPM - minRPM) / maxRPM); } } /// /// Current thrust generated by the engine / propeller /// public float Thrust { get; private set; } /// /// True if engine's thrust postion is under water. /// public bool Submerged { get { return _sc.referenceWaterObject == null || _sc.referenceWaterObject.GetWaterHeightSingle(ThrustPosition) > ThrustPosition.y; } } /// /// Point at which thrust will be applied to the Rigidbody, in world coordinates. /// public Vector3 ThrustPosition { get { return _sc.transform.TransformPoint(thrustPosition); } } /// /// Direction of thrust force in world coordinates. /// public Vector3 ThrustDirection { get { if (rudderTransform == null) { return _sc.transform.TransformDirection(thrustDirection).normalized; } return rudderTransform.TransformDirection(thrustDirection).normalized; } } public void Initialize(AdvancedShipController sc) { _sc = sc; if (isOn) { _engineState = State.On; _wasOn = true; } else { _engineState = State.Off; _wasOn = false; } // Init sound SoundInit(); } public virtual void Update() { if (_sc.input.EngineStartStop) { if (isOn) { StopEngine(); } else { StartEngine(); } } // Get throttle float throttleInput = 0; switch (throttleBinding) { case ThrottleBinding.Throttle: throttleInput = _sc.input.Throttle; break; case ThrottleBinding.Throttle2: throttleInput = _sc.input.Throttle2; break; case ThrottleBinding.Throttle3: throttleInput = _sc.input.Throttle3; break; case ThrottleBinding.Throttle4: throttleInput = _sc.input.Throttle4; break; } // Start on throttle if (!isOn && startOnThrottle && Mathf.Abs(throttleInput) > 0.2f) { StartEngine(); } // Check engine state if (_engineState == State.Starting && !isOn) { _engineState = State.Off; } else if (isOn && !_wasOn) { _engineState = State.Starting; _startTime = Time.realtimeSinceStartup; } else if (!isOn && _wasOn) { _engineState = State.Stopping; _stopTime = Time.realtimeSinceStartup; } // Run timer starting or stopping if (_engineState == State.Starting) { if (Time.realtimeSinceStartup > _startTime + startDuration) { _engineState = State.On; } } else if (_engineState == State.Stopping) { if (Time.realtimeSinceStartup > _stopTime + startDuration) { _engineState = State.Off; } } // RPM float newRpm = 0f; switch (_engineState) { case State.On: newRpm = (0.7f + 0.3f * (_sc.Speed / maxSpeed)) * Mathf.Abs(throttleInput) * maxRPM; newRpm = Mathf.Clamp(newRpm, minRPM, maxRPM); if (!Submerged) { newRpm = maxRPM; } break; case State.Off: newRpm = 0; break; case State.Starting: newRpm = startingRPM; break; case State.Stopping: newRpm = 0f; break; } _rpm = Mathf.SmoothDamp(_rpm, newRpm, ref _spinVelocity, spinUpTime); if (_engineState == State.On) { // Check if propeller under water bool applyForce = Submerged || applyThrustWhenAboveWater; // Check if thrust can be applied Thrust = 0; if (applyForce && maxRPM != 0 && maxSpeed != 0 && RPM > minRPM + 1f && throttleInput != 0) { Thrust = Mathf.Sign(throttleInput) * (_rpm / maxRPM) * thrustCurve.Evaluate(Mathf.Abs(_sc.Speed) / maxSpeed) * maxThrust; Thrust = Mathf.Sign(throttleInput) == 1 ? Thrust : Thrust * reverseThrustCoefficient; if (!_sc.MultiplayerIsRemote) { _sc.vehicleRigidbody.AddForceAtPosition(Thrust * ThrustDirection, ThrustPosition); } } if (propellerTransform != null) { float zRotation = _rpm * propellerRpmRatio * 6.0012f * Time.fixedDeltaTime; if (rotationDirection == RotationDirection.Right) { zRotation = -zRotation; } propellerTransform.RotateAround(propellerTransform.position, propellerTransform.forward, zRotation); } } SoundUpdate(); _wasOn = isOn; } /// /// Starts the ship engine. /// public void StartEngine() { isOn = true; } /// /// Stops the ship engine. /// public void StopEngine() { isOn = false; StopAll(); } public virtual void SoundInit() { if (runningSource != null) { runningSource.loop = true; runningSource.playOnAwake = false; } if (startingSource != null) { startingSource.loop = false; startingSource.playOnAwake = false; } if (stoppingSource != null) { stoppingSource.loop = false; stoppingSource.playOnAwake = false; } } public virtual void SoundUpdate() { if (runningSource == null) { //Debug.LogWarning($"No AudioSource assigned to Running Source field of object {_sc.name}"); return; } // Pitch runningSource.pitch = pitch + RpmPercent * pitchRange; // Volume runningSource.volume = volume + RpmPercent * volumeRange; if (_engineState == State.On) { PlayRunning(); } else if (_engineState == State.Off) { StopAll(); } else if (_engineState == State.Starting) { PlayStarting(); } else if (_engineState == State.Stopping) { PlayStopping(); } } private void PlayStarting() { if (startingSource == null) { if (!runningSource.isPlaying) { runningSource.Play(); } runningSource.volume = Mathf.Lerp(0f, volume, (Time.realtimeSinceStartup - _startTime) / startDuration); } if (stoppingSource != null) { stoppingSource.Stop(); } if (startingSource != null && runningSource != null) { runningSource.Stop(); } if (startingSource != null) { startingSource.Play(); } } private void PlayRunning() { //if (startingSource != null) startingSource.Stop(); if (stoppingSource != null) { stoppingSource.Stop(); } if (runningSource != null) { if (!runningSource.isPlaying) { runningSource.Play(); } } } private void PlayStopping() { if (startingSource != null) { startingSource.Stop(); } if (runningSource != null) { runningSource.Stop(); } if (stoppingSource != null) { stoppingSource.Play(); } } private void StopAll() { if (startingSource != null) { startingSource.Stop(); } if (runningSource != null) { runningSource.Stop(); } if (stoppingSource != null) { stoppingSource.Stop(); } } public void SetDefaults() { name = "Engine"; throttleBinding = ThrottleBinding.Throttle; minRPM = 500; maxRPM = 3000; maxThrust = 8000; spinUpTime = 2; startingRPM = 300; startDuration = 1.3f; stopDuration = 0.8f; thrustPosition = Vector3.zero; thrustDirection = Vector3.forward; reverseThrustCoefficient = 0.3f; maxSpeed = 20f; thrustCurve = AnimationCurve.Constant(0, 1f, 1f); volume = 0.2f; volumeRange = 0.4f; pitch = 0.4f; pitchRange = 0.6f; } } }