using System.Collections; using System.Collections.Generic; using UnityEngine; // Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved. namespace SciFiShipController { /// /// Class for implementing proportional-integral-derivative (PID) controllers. /// [System.Serializable] public class PIDController { #region Public Variables /// /// Proportional (P) gain. /// public float pGain; /// /// Integral (I) gain. /// public float iGain; /// /// Derivative (D) gain. /// public float dGain; /// /// Whether input over time is used for the derivative (D) term instead of proportional (P) over time. /// Enabling this gets rid of the effect known as "derivative kick" where changes in the target value /// cause huge spikes in the calculated input. /// public bool derivativeOnMeasurement; /// /// Whether limits are used for the proportional (P), integral (I) and derivative (D) components individually. /// public bool useIndividualInputLimits; #endregion #region Private Variables /// /// Proportional (P) term. /// private float proportional; /// /// Integral (I) term. /// private float integral; /// /// Derivative (D) term. /// private float derivative; /// /// Proportional (P) term multiplied by p-gain value. /// private float proportionalTimesPGain; /// /// Integral (I) term multiplied by i-gain value. /// private float integralTimesIGain; /// /// Derivative (D) term multiplied by d-gain value. /// private float derivativeTimesDGain; /// /// The previous value of the proportional (P) term. /// private float previousProportional; /// /// The previous system value. /// private float previousValue; /// /// Whether this is the first update since this the system was last initialised or reset. /// private bool firstUpdate; /// /// The minimum allowed input value. /// private float minInput; /// /// The maximum allowed input value. /// private float maxInput; /// /// The minimum allowed proportional times p-gain value. /// private float minProportionalInput; /// /// The maximum allowed proportional times p-gain value. /// private float maxProportionalInput; /// /// The minimum allowed integral times i-gain value. /// private float minIntegralInput; /// /// The maximum allowed integral times i-gain value. /// private float maxIntegralInput; /// /// The minimum allowed derivative times d-gain value. /// private float minDerivativeInput; /// /// The maximum allowed derivative times d-gain value. /// private float maxDerivativeInput; private float requiredInput; #endregion #region Constructors // Class Constructor public PIDController (float kp, float ki, float kd) { this.pGain = kp; this.iGain = ki; this.dGain = kd; this.derivativeOnMeasurement = false; this.useIndividualInputLimits = false; this.firstUpdate = true; this.minInput = Mathf.NegativeInfinity; this.maxInput = Mathf.Infinity; this.minProportionalInput = Mathf.NegativeInfinity; this.maxProportionalInput = Mathf.Infinity; this.minIntegralInput = Mathf.NegativeInfinity; this.maxIntegralInput = Mathf.Infinity; this.minDerivativeInput = Mathf.NegativeInfinity; this.maxDerivativeInput = Mathf.Infinity; this.ResetController(); } #endregion #region Public Non-Static Methods ///// ///// Calculates the required input to move from the system from currentValue to targetValue. ///// lastFrameTime should be set to the time since this function was last called. ///// ///// ///// ///// ///// //public float RequiredInput (float targetValue, float currentValue, float lastFrameTime) //{ // // Implements the basic PID algorithm // // First check that the target value passed in is not NaN (as this can cause long-lasting issues) // if (float.IsNaN(targetValue)) { targetValue = 0f; } // // Calculate the delta to the target value // proportional = targetValue - currentValue; // // Multiply by iGain here instead of at the return line, to allow tuning parameters on the fly // // Otherwise when we change iGain the entire I term would change dramatically // integral += proportional * lastFrameTime * iGain; // // Clamp the integral term into the allowed input range // if (integral < minInput) { integral = minInput; } // else if (integral > maxInput) { integral = maxInput; } // if (derivativeOnMeasurement) // { // // Calculate how quickly the value is changing, then take the negative of it // // This works due to the following: // // P = target - current // // dP/dt = d(target)/dt - d(current)/dt // // Hence if we assume target is unchanging then... // // dP/dt = -d(current)/dt // // This eliminates spikes occuring when the target value changes // derivative = (previousValue - currentValue) / lastFrameTime; // } // else // { // // Calculate how quickly the proportional value is changing // derivative = (proportional - previousProportional) / lastFrameTime; // } // // Calculate the required input // // We don't multiply by iGain here as we have already done it // requiredInput = (proportional * pGain) + integral + (derivative * dGain); // // Clamp the required input into the allowed input range // if (requiredInput < minInput) { requiredInput = minInput; } // else if (requiredInput > maxInput) { requiredInput = maxInput; } // // Store this frame's values of the proportional and current value for use the next time this function is called // previousProportional = proportional; // previousValue = currentValue; // // Return the calculated value // return requiredInput; //} /// /// Calculates the required input to move from the system from currentValue to targetValue. /// lastFrameTime should be set to the time since this function was last called. /// /// /// /// /// public float RequiredInput(float targetValue, float currentValue, float lastFrameTime) { // Implements the basic PID algorithm // First check that the target value passed in is not NaN (as this can cause long-lasting issues) if (float.IsNaN(targetValue)) { targetValue = 0f; } // Calculate the delta to the target value proportional = targetValue - currentValue; // Multiply by iGain here instead of at the return line, to allow tuning parameters on the fly // Otherwise when we change iGain the entire I term would change dramatically integralTimesIGain += proportional * lastFrameTime * iGain; // Clamp the integral term into the allowed input range if (integralTimesIGain < minInput) { integralTimesIGain = minInput; } else if (integralTimesIGain > maxInput) { integralTimesIGain = maxInput; } // Don't measure derivative on first system update if (!firstUpdate) { if (derivativeOnMeasurement) { // Calculate how quickly the value is changing, then take the negative of it // This works due to the following: // P = target - current // dP/dt = d(target)/dt - d(current)/dt // Hence if we assume target is unchanging then... // dP/dt = -d(current)/dt // This eliminates spikes occuring when the target value changes derivative = (previousValue - currentValue) / lastFrameTime; } else { // Calculate how quickly the proportional value is changing derivative = (proportional - previousProportional) / lastFrameTime; } } else { derivative = 0f; firstUpdate = false; } // Multiply the terms by their respective gains // We don't multiply by iGain here as we have already done it proportionalTimesPGain = proportional * pGain; derivativeTimesDGain = derivative * dGain; // If necessary, clamp the individual terms if (useIndividualInputLimits) { // Clamp the proportional term into the required range if (proportionalTimesPGain < minProportionalInput) { proportionalTimesPGain = minProportionalInput; } else if (proportionalTimesPGain > maxProportionalInput) { proportionalTimesPGain = maxProportionalInput; } // Clamp the integral term into the required range if (integralTimesIGain < minIntegralInput) { integralTimesIGain = minIntegralInput; } else if (integralTimesIGain > maxIntegralInput) { integralTimesIGain = maxIntegralInput; } // Clamp the derivative term into the required range if (derivativeTimesDGain < minDerivativeInput) { derivativeTimesDGain = minDerivativeInput; } else if (derivativeTimesDGain > maxDerivativeInput) { derivativeTimesDGain = maxDerivativeInput; } } // Calculate the required input requiredInput = proportionalTimesPGain + integralTimesIGain + derivativeTimesDGain; // Clamp the required input into the allowed input range if (requiredInput < minInput) { requiredInput = minInput; } else if (requiredInput > maxInput) { requiredInput = maxInput; } // Store this frame's values of the proportional and current value for use the next time this function is called previousProportional = proportional; previousValue = currentValue; // Return the calculated value return requiredInput; } /// /// Sets the input limits of the controller. /// /// /// public void SetInputLimits(float minInputLimit, float maxInputLimit) { minInput = minInputLimit; maxInput = maxInputLimit; } /// /// Set the individual input limits of the controller. /// /// /// /// /// /// /// public void SetIndividualInputLimits(float minProportionalInputLimit, float maxProportionalInputLimit, float minIntegralInputLimit, float maxIntegralInputLimit, float minDerivativeInputLimit, float maxDerivativeInputLimit) { minProportionalInput = minProportionalInputLimit; maxProportionalInput = maxProportionalInputLimit; minIntegralInput = minIntegralInputLimit; maxIntegralInput = maxIntegralInputLimit; minDerivativeInput = minDerivativeInputLimit; maxDerivativeInput = maxDerivativeInputLimit; } /// /// Resets state values of the controller. /// public void ResetController () { // Reset state values. Integral accumulates over time and previousProportional/previousValue store previous values, // so they need to be reset. integralTimesIGain = 0f; previousProportional = 0f; previousValue = 0f; // Remember that we have reset the controller for the next update firstUpdate = true; } #endregion } }