How to control acceleration

To move a spaceship in unity, I wrote a script that applies a force in Z-direction when hitting the up-key and an opposite force when pressing the down-key. The velocity is limited with a clamp-function.

Now I’m facing several problems with this setup:

-When rotating the ship (using a 2nd mouselook-script), accelleration in the new direction at certain degrees is not possible

-Depending on rotation the ship can fly backwards (even though the clamp-function should avoid that)

-Console-output for speed doesn’t seem to show the real speed / it depends on ships orientation.

-Regulation of a constant speed is not possible

I think the first bunch of problems have something to do with the local/world-axis. However, the forces are applied to the rigidbody and therefore should be local. Am I wrong?

I found several threats where people had trouble with setting/getting the real local velocity. Also I’m not sure if my way of just limiting the speed is messing things up with the applied forces.

What I propably need is some kind of proportional-integral-regulation to automatically and dynamically let the acceleration apply to a wished speed. I know mathematically speed is the derivation of acceleration. Going backwards makes integral-calculation neccesary, based on user-input rather than a mathematical function. This is higher mathematics and usually found only in special devices.

Does unity provide such a regulator-function? I’m talking about something similar to the devices used in modern cars. The driver sets a target speed and the regulator automatically adjusts the acceleration (a lot at the beginning / very few when close to target-speed). My script right now is very simple and posted below.

// This script regulates object-movement with the keyboard
//
// -Forward/backward-movements are done with forces
//
// -Sidewards-movements are done with manually changing position
//
// -Booster also is done with position-change
//
// -The forces are limited by min- and max-speed (unity has to cut the accelleration because of the maxspeed
//    											  and has to stop backwards-drift due to min-speed.)	


function Update () {
	
	var stepx : float; // Boster
	var stepz : float; // Left-Right-Sliding
	
	var accell : float; //Start-Accelleration
	var breaks : float; //Max accelleration
	
	var minspeed : float; //Lowest possible speed
	var maxsped : int;	 // Fastest speed
	 
	minspeed = 10;  	  // 
	maxspeed = 30;   // Max-Speed
	
	accell = 80; //Acelleration
	breaks = -82; //Breaks-strength  (breaks a bit stronger than accell - Problem: backwards-flying occurs)
	
	stepx = 8.5; //  Booster Forward-steps
	stepz = 1.5; //Sideward steps Slider
		
	// Local z- and x-axis are switched
	//    -> "Vector3.right" must be used instead of "Vector3.forward"
			
		
		
				// UP		
	if(Input.GetKey("w") || Input.GetKey("up")) {
		  constantForce.relativeForce.x = accell;    	 	  
	}
			
				// DOWN
	if(Input.GetKey("s") || Input.GetKey("down")) {
		 
		if(rigidbody.velocity.x > 0) {
		 	constantForce.relativeForce = (Vector3.right * breaks);	 		 	 
	    }
	}	
	
				//TABULATOR (Booster)
	if(Input.GetKey("tab")) {
	  transform.Translate(Vector3.right * stepx);  //Booster just moves Obj. forward
		
	}
		
	
				// LEFT-Slide
	if(Input.GetKey("q")) {
		transform.Translate(Vector3.forward * stepz);
		
	}
		
		    	// RIGHT-Slide
	if(Input.GetKey("e")) {
		transform.Translate(Vector3.forward * -stepz);
	}
	
			
	    	// Clamp speed to the min and max   	       
 	rigidbody.velocity.x = Mathf.Clamp (rigidbody.velocity.x, minspeed, maxspeed);
 	
 	 	
 	 	 	
  	Debug.Log("Current Speed:" +rigidbody.velocity.x);
} // EndFunc Update

You’re doing a big mess here! constantForce.relativeForce is local, but rigidbody.velocity is world referenced, and you’re clamping only the x axis! This is causing the weird direction and speed readings.

You should limit the speed with Vector3.ClampMagnitude: this limits the velocity to a max magnitude while keeping its direction. Another solution could be to set rigidbody.velocity directly - but this would require a complete change in your code.

You can solve both problems changing the two last lines:

    // Clamp speed to max            
    rigidbody.velocity = Vector3.ClampMagnitude(rigidbody.velocity, maxspeed);
    Debug.Log("Current Speed:" +rigidbody.velocity.magnitude);

NOTE: forget about what I said about Q, E and Tab: you’re using Translate, which
by default operates in local space - thus the teleport will be ok.

EDITED:

That’s a rigidbody.velocity based version of your script: it’s more predictably, and you stop without that back flight problem. I didn’t test this, but the basic idea is correct - let me know if you have any problems with this script:

function Update () {
    var stepx : float = 8.5; // Booster Forward-steps
    var stepz : float = 1.5; // Sideward steps Slider
    var accell : float = 30.0; // Max speed forward

    private var locVel = Vector3.zero; // local velocity you set
    private var curVel = Vector3.zero; // current velocity - follow locVel
    var force: float = 2.0; // how fast curVel reaches locVel

    // Local z- and x-axis are switched
    //    -> "Vector3.right" must be used instead of "Vector3.forward"

    // UP     
    if(Input.GetKey("w") || Input.GetKey("up")) {
        locVel = Vector3(accell, 0, 0); // desired velocity = accell forward          
    }
    // DOWN
    if(Input.GetKey("s") || Input.GetKey("down")) {
        locVel = Vector3.zero; // desired velociy = 0
    }
    // fake a progressive acceleration with MoveTowards:
    curVel = Vector3.MoveTowards(curVel, locVel, force * Time.deltaTime);
    // convert to world space and apply to the rigidbody velocity:
    rigidbody.velocity = transform.TransformDirection(curVel);
    //TABULATOR (Booster)
    if(Input.GetKey("tab")) { // it's correct: Translate defaults to local space
      transform.Translate(Vector3.right * stepx);  //Booster just moves Obj. forward
    }
    // LEFT-Slide
    if(Input.GetKey("q")) {
       transform.Translate(Vector3.forward * stepz);
    }
    // RIGHT-Slide
    if(Input.GetKey("e")) {
       transform.Translate(Vector3.forward * -stepz);
    }
    Debug.Log("Current Speed:" + curVel.x);
} 

EDITED 2:

PI is a Proportional Integral control (you can read about the more complex PID -Proportional Integral Derivative - control in this article: PID controller - Wikipedia)

A simple P (Proportional) control subtracts the measured speed from the target speed, multiply the difference (called error signal) by PGain and use the result (the Proportional signal) to control the force applied. When the desired speed is reached, the error signal becomes zero, what also reduces to zero the force applied. But if friction or some external force reduce the speed, the system can never stay at zero error: there’s always a small error signal to be multiplied by PGain and supply the force necessary to null these external forces.

To ensure zero error, the I letter enters the scene: the error signal is continuously accumulated (Integrated) and the result is multiplied by IGain and added to the Proportional signal. In a PI control, the Integral (accumulated) error plays the role of the small residual error in a simple P control: it provides the necessary force to keep the speed, allowing for zero error signal. Unless you need to control supersonic missile flight, in practice there’s no need to do complex numerical integrations: just accumulate the error signal in a variable, multiply it by the IGain (usually << 1) and add to the Proportional signal.

Fortunately, in the game environment all this sophistication is rarely needed (unless the game goal is to fine-tune PI Control parameters, hardly a block-buster game!) In most cases, we just calculate the ideal behavior and make the objects follow our orders! In my script, for instance, the speed varies linearly up to the desired value, and obediently gets stuck there: it’s the dream-come-true of any control engineer! In real life, one must tweak the gains and available power to barely follow an ideal case - and the wrong parameters very often cause oscillation, overshoot, big residual errors, slow response - it’s hell on earth!

Anyway, if you really really really need to implement a servo control, you can avoid the Integral part (there’s no friction or other forces, so it’s not needed): just calculate the difference between current and target speeds, multiply by a gain factor, clamp to some limit and use the result to apply the force. But if you want to check the PI controller, go ahead and use the code below:

var targetSpeed = Vector3.zero; // the desired speed
var maxForce: float = 100; // the max force available
var pGain: float = 20; // the proportional gain
var iGain: float = 0.5; // the integral gain
private var integrator = Vector3.zero; // error accumulator
var curSpeed = Vector3.zero; // actual speed
var force = Vector3.zero; // 

function FixedUpdate(){
  curSpeed = rigidbody.velocity; // measure actual speed
  var error = targetSpeed - curSpeed; // generate the error signal
  integrator += error;
  // calculate the force and limit it to the max force available:
  force = error * pGain + integrator * iGain;
  force = Vector3.ClampMagnitude(force, maxForce);
  // apply the force to accelerate the rigidbody:
  rigidbody.AddForce(force);
}

This code uses AddForce instead of constantForce - remember to zero any constantForce currently in your code. NOTE: targetSpeed is in world space: if you want do use local speed, define it, convert to world space with transform.TransformDirection and store in targetSpeed.

EDITED 3:

The integration above doesn’t take into account the time elapsed since the last frame. It’s an usual practice in the microcontroller world, because the integration occurs at a fixed rate, and this saves an expensive multiplication (iGain compensates for the different integration result). FixedUpdate also occurs at a fixed rate - or at least tries to do it: in very slow machines, FixedUpdate may not be able to keep a constant pace. To avoid problems in these cases, the integration can include Time.deltaTime, like this:

var targetSpeed = Vector3.zero; // the desired speed
var maxForce: float = 100; // the max force available
var pGain: float = 20; // the proportional gain
var iGain: float = 0.5; // the integral gain
private var integrator = Vector3.zero; // error accumulator
var curSpeed = Vector3.zero; // actual speed
var force = Vector3.zero; // 

function FixedUpdate(){
  curSpeed = rigidbody.velocity; // measure actual speed
  var error = targetSpeed - curSpeed; // generate the error signal
  integrator += error * Time.deltaTime; // integrate the error signal
  // calculate the force:
  force = error * pGain + integrator * iGain;
  // clamp to the max force available:
  force = Vector3.ClampMagnitude(force, maxForce);
  // apply the force to accelerate the rigidbody:
  rigidbody.AddForce(force);
}

The iGain in this case must be about 50 times greater, since the regular deltaTime is 1/50 second.

Solution for 2nd part - How to do an autopilot:

“A simple P (Proportional) control subtracts the measured speed from the target speed, multiply the difference (called error signal) by PGain and use the result (the Proportional signal) to control the force applied. When the desired speed is reached, the error signal becomes zero, what also reduces to zero the force applied”, as aldonaletto wrote.

Furthermore the integral part of a P-I-regulator is doing this:
“To ensure zero error […] the error signal is continuously accumulated (Integrated) and the result is multiplied by IGain and added to the Proportional signal. In a PI control, the Integral (accumulated) error plays the role of the small residual error in a simple P control: it provides the necessary force to keep the speed, allowing for zero error signal.”

…Meaning a P - or even better - a P-I-regulator is the motor of the idea. Is there one in unity’s physics-engine?

If not: Did someone write a script already to compensate the lack?

I searched the web for P-I- and P-I-D-controllers and indeed found some code. No Javascript but at least C and C#-versions as well as pseudo-code and a Basic-example.

Some good pictures what P- and PI-controllers do can be found in this well-done explanation.

P-only accelerates longer and higher around the target-value. P-I-controllers make the system getting stable faster and better. In slow responding systems, P-only-controllers sometimes never stabilize on the target-value and always oscillate around the target.

However, my problem now is: I want to transfer the found code into a javascript-version but I can’t find the lines where they calculate the integral. The code in general is not too difficult and basically always the same:

Pseudo-code from Wikipedia:

`
    previous_error = 0
    integral = 0 
    start:
      error = setpoint - PV [actual_position]
      integral = integral + error*dt
      derivative = (error - previous_error)/dt
      output = Kp*error + Ki*integral + Kd*derivative
      previous_error = error
      wait(dt)
    goto start
`

I don’t get the thing with the integral. According to Wikipedia, there are several methods to choose from to integrate (e.g. Romberg). But the PID-code-examples just use

integral = integral + error*dt

for calculating the integral. What am I missing?

C-version of a PID-controller (from Embedded’s Heaven):

    #ifndef PID_H_

#define PID_H_

#include

//Define parameter

#define epsilon 0.01

#define dt
0.01            
//100ms loop time

#define MAX 
4                  
//For Current Saturation

#define MIN -4

#define Kp  0.1

#define Kd  0.01

#define Ki  0.005

float PIDcal(float setpoint,float
actual_position)

{

static float
pre_error = 0;

static float
integral = 0;

float error;

float derivative;

float output;

//Caculate P,I,D

error = setpoint -
actual_position;

//In case of error
too small then stop intergration

if(abs(error) >
epsilon)

{

integral = integral + error*dt;

}

derivative = (error

  • pre_error)/dt;

output = Kperror +
Ki
integral + Kd*derivative;

//Saturation Filter

if(output > MAX)

{

output = MAX;

}

else if(output <
MIN)

{

output = MIN;

}

//Update error

pre_error = error;

return output;

}

#endif /PID_H_/

An extensive C#-version of a PID-controller (from Codeproject.com):

*  PID Loop Control example.  By Lowell Cady, LowTech LLC (c) 2009

*/

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Drawing.Drawing2D;

using System.Linq;

using System.Text;

using System.Windows.Forms;

namespace PID_Example

{

public partial class Form1 : Form

{

private double pSetpoint
= 0;

private double pPV =
0;  // actual possition (Process Value)

private double pError =
0;   // how much SP and PV are diff (SP - PV)

private double pIntegral
= 0; // curIntegral + (error * Delta Time)

private double
pDerivative = 0;  //(error - prev error) / Delta time

private double pPreError
= 0; // error from last time (previous Error)

private double pKp = 0.2,
pKi = 0.01, pKd = 1; // PID constant multipliers

private double pDt =
100.0; // delta time

private double pOutput =
0; // the drive amount that effects the PV.

private double
pNoisePercent = 0.02; // amount of the full range to randomly alter
the PV

private StripChart
stripChart;  //builds and contains the strip chart bmp

private double pNoise =
0;  // random noise added to PV

public double
setpoint  //SP, the value desired from the process (i.e.
desired temperature of a heater.)

{

get { return pSetpoint; }

set {

pSetpoint = value;

// change the label for setpoint value

lblSP.Text = pSetpoint.ToString();

// change the slider possition if it does not match

if ( (int)Math.Round(pSetpoint) != trackBarSP.Value) // don’t use
doubles in == or != expressions due to bit resolution

trackBarSP.Value = (int)Math.Round(pSetpoint);

}

}

public double
PV    //Process Value, the measured value of the
process (i.e. actual temperature of a heater)

{

get { return pPV; }

set {

pPV = value;

// place limits on the measured value

if (pPV < 0)

pPV = 0;

else if (pPV > 1000)

pPV = 1000;

// update the text

lblPV.Text = pPV.ToString();

// update progress bar

if (pPV > progBarPV.Maximum)

progBarPV.Value = progBarPV.Maximum;

else if (pPV < progBarPV.Minimum)

progBarPV.Value = progBarPV.Minimum;

else

progBarPV.Value = (int)pPV;

}

}

public double error
//difference between Setpoint and Process value (SP - PV)

{

get { return pError; }

set

{

pError = value;

// uodate the lable

lblError.Text = pError.ToString();

}

}

public double integral
//sum of recent errors

{

get { return pIntegral; }

set

{

pIntegral = value;

// update the lable

lblIntegral.Text = integral.ToString();

}

}

public double
derivative    //How much the error is changing (the
slope of the change)

{

get { return pDerivative; }

set

{

pDerivative = value;

lblDeriv.Text = derivative.ToString();

}

}

public double
preError  //Previous error, the error last time the process was
checked.

{

get { return pPreError; }

set { pPreError = value; }

}

public double
Kp    //proportional gain, a “constant” the error is
multiplied by. Partly contributes to the output as (Kp * error)

{

get { return pKp; }

set

{

pKp = value;

// update the textBox

if (pKp.CompareTo(validateDouble(tbKp.Text, pKp)) != 0)

tbKp.Text = pKp.ToString();

}

}

public double
Ki    // integral gain, a “constant” the sum of
errors will be multiplied by.

{

get { return pKi; }

set

{

pKi = value;

// update the textBox

if (pKi.CompareTo(validateDouble(tbKi.Text, pKi)) != 0)

tbKi.Text = pKi.ToString();

}

}

public double
Kd    // derivative gain, a “constant” the rate of
change will be multiplied by.

{

get { return pKd; }

set

{

pKd = value;

// update the textBox

if (pKd.CompareTo(validateDouble(tbKd.Text, pKd)) != 0)

tbKd.Text = pKd.ToString();

}

}

public double
Dt    // delta time, the interval between saples (in
milliseconds).

{

get { return pDt; }

set { pDt = value; }

}

public double
output    //the output of the process, the value
driving the system/equipment.  (i.e. the amount of electricity
supplied to a heater.)

{

get { return pOutput; }

set

{

pOutput = value;

// limit the output

if (pOutput < 0)

pOutput = 0;

else if (pOutput > 1000)

pOutput = 1000;

if (pOutput > progBarOut.Maximum)

progBarOut.Value = progBarOut.Maximum;

else if (pOutput < progBarOut.Minimum)

progBarOut.Value = progBarOut.Minimum;

else

progBarOut.Value = (int)pOutput;

lblOutput.Text = pOutput.ToString();

}

}

public double
noisePercent  //upper limit to the amount of artificial noise
(random distortion) to add to the PV (measured value).  0.0 to
1.0 (0 to 100%)

{

get { return pNoisePercent;}

set { pNoisePercent = value; }

}

public double
noise     //amount of random noise added to the
process value

{

get { return pNoise; }

set { pNoise = value; }

}

// form constructor

public Form1()

{

InitializeComponent();

stripChart = new StripChart();

}

// set up some of the
labels when the form loads.

private void
Form1_Load(object sender, EventArgs e)

{

lblInterval.Text = trackBarInterval.Value.ToString();

lblSP.Text = trackBarSP.Value.ToString();

}

// set the sample rate

private void
trackBarInterval_Scroll(object sender, EventArgs e)

{

lblInterval.Text = trackBarInterval.Value.ToString();

tmrPID_Ctrl.Enabled = false;

tmrPID_Ctrl.Interval = trackBarInterval.Value;

Dt = tmrPID_Ctrl.Interval;

tmrPID_Ctrl.Enabled = true;

}

/*This represents the
speed at which electronics could actualy

sample the process values… and do any work on them.

* [most industrial
batching processes are SLOW, on the order of minutes.

*  but were
going to deal in times 10ms to 1 second.

*  Most PLC’s
have relativly slow data busses, and would sample


temperatures on the order of 100’s of milliseconds. So our

*  starting
time interval is 100ms]

*/

private void
tmrPID_Ctrl_Tick(object sender, EventArgs e)

{   /*

  • Pseudocode from Wikipedia

*

previous_error = 0

integral = 0

start:

error = setpoint - PV(actual_position)

integral = integral + error*dt

derivative = (error - previous_error)/dt

output = Kperror + Kiintegral + Kd*derivative

previous_error = error

wait(dt)

goto start

*/

// calculate the difference between the desired value and the actual
value

error = setpoint - PV;

// track error over time, scaled to the timer interval

integral = integral + (error * Dt);

// determin the amount of change from the last time checked

derivative = (error - preError) / Dt;

// calculate how much drive the output in order to get to the

// desired setpoint.

output = (Kp * error) + (Ki * integral) + (Kd * derivative);

// remember the error for the next time around.

preError =
error;

}

//This timer updates the
process data. it needs to be the fastest

// interval in the
system.

private void
tmrPV_Tick(object sender, EventArgs e)

{

/* this my version of cruise control.

  • PV = PV + (output * .2) - (PV*.10);

  • The equation contains values for speed, efficiency,

*  and wind resistance.

Here ‘PV’ is the speed of the car.

‘output’ is the amount of gas supplied to the engine.

  • (It is only 20% efficient in this example)

And it looses energy at 10% of the speed. (The faster the

car moves the more PV will be reduced.)

  • Noise is added randomly if checked, otherwise noise is 0.0

  • (Noise doesn’t relate to the cruise control, but can be useful

*  when modeling other processes.)

*/

PV = PV + (output * 0.20) - (PV * 0.10) + noise;

// change the above equation to fit your aplication

}

//change the setpoint

private void
trackBarSP_Scroll(object sender, EventArgs e)

{

setpoint = trackBarSP.Value;

}

// change a double only
if the string can be parsed as a double

private double
validateDouble(string text,double startVal)

{

double d;

if (double.TryParse(text, out d))

return d;

else

return
startVal;

}

// Change the
Proportional gain text, so validate it.

private void
tbKp_TextChanged(object sender, EventArgs e)

{

Kp = validateDouble(tbKp.Text, Kp);

}

// Change the Integral
gain text, so validate it.

private void
tbKi_TextChanged(object sender, EventArgs e)

{

Ki = validateDouble(tbKi.Text, Ki);

}

// Change the Derivative
gain text, so validate it.

private void
tbKd_TextChanged(object sender, EventArgs e)

{

Kd = validateDouble(tbKd.Text, Kd);

}

// change the amount of
noise introduced every tmrPV data update

private void
nudNoisePercent_ValueChanged(object sender, EventArgs e)

{

noisePercent = (double)(nudNoisePercent.Value) / 100.0;

}

// allow the whole
process to be toggled on and off (in case you see something
interesting in the chart.)

private void
btnStartProcess_Click(object sender, EventArgs e)

{

tmrPV.Enabled = !tmrPV.Enabled;

tmrPID_Ctrl.Enabled = tmrPV.Enabled;

tmrChart.Enabled = tmrPV.Enabled;

if (tmrPV.Enabled)

btnStart.Text = “Stop Process”;

else

btnStart.Text = “Start Process”;

}

// The chart requires
more resources when updated.

// so the update chart
happens at a slower rate than the data update (tmrPV is the data
update)

// However, setting the
chart update too slow (more than 3x the data update)

// will cause resolution
loss in the graph (the graph will look blurry).

private void
tmrChart_Tick(object sender, EventArgs e)

{

// update the stripchart

stripChart.addSample(setpoint, PV, output);

// update will rebuild the chart bitmap.

// put the new chart bitmap in the pictureBox

pictureBox1.Image = stripChart.bmp;

}

private void
tmrNoise_Tick(object sender, EventArgs e)

{

// noise is added if the checkBox has been clicked

Random r = new Random();

if (cbNoise.Checked)

noise = (progBarPV.Maximum * noisePercent) * (r.NextDouble() - 0.5);

else

noise = 0;

// add a positive or negative noise

// first get the max allowable noise, then multiply by a random
value between -0.5 and 0.5

/*[The noise doesn’t really represent what happens to a car

  • in the real world, but it is usefull if you model other processes.

  • This part of the code into its own timer in order to have much
    more

  • control over the noise frequency.

*/

}

}

/* this class will build a strip chart bitmap
from consecutive

*  SP and PV values.  For
simplicity, the limits are fixed.

*/

public class StripChart

{

private Bitmap
pBmp;    // the chart bitmap

private
Queue qSP;  //collection of Setpoints

private
Queue qPV;  //collection of Precess Values

private
Queue qMV;  //collection of Manipulated values
(outputs)

private int x = 0, y = 0;
// used to define points added to line array

// define brushes

private SolidBrush
brSP;    // setpoint brush

private SolidBrush
brPV;    // process value brush

private SolidBrush
brMV;    // Manipulated Value brush

// define pens

private Pen
pSP;

private Pen pPV;

private Pen pMV;

public Bitmap
bmp   // allow the bitmap to be accessed, but not mutated
publicly.

{

get { return pBmp; }

}

public StripChart()

{

// define the bitmap.

pBmp = new Bitmap(1007, 1007,
System.Drawing.Imaging.PixelFormat.Format24bppRgb);

// instatiate the queues

qSP = new Queue(1000);

qPV = new Queue(1000);

qMV = new Queue(1000);

// make brushes that are lightly transparent

brSP = new SolidBrush(Color.FromArgb(255, Color.Green));

brPV = new SolidBrush(Color.FromArgb(128, Color.Blue));

brMV = new SolidBrush(Color.FromArgb(128, Color.Red));

// make pens that will be used to draw the lines

pSP = new Pen(brSP, 5);

pPV = new Pen(brPV, 5);

pMV = new Pen(brMV, 5);

}

//enter sampled values
into the associated queues

public void
addSample(double sp, double pv, double mv)

{

// limit the size to 1000, if more than 1000 remove the first item

if (qSP.Count > 999)

qSP.Dequeue();

if (qPV.Count > 999)

qPV.Dequeue();

if (qMV.Count > 999)

qMV.Dequeue();

// place values into the queues

qSP.Enqueue(sp);

qPV.Enqueue(pv);

qMV.Enqueue(mv);

// update the bitmap.

buildChart();

}

// empty out the queues

public void clearQueus()

{

qSP.Clear();

qPV.Clear();

qMV.Clear();

}

public void buildChart()

{

Graphics g = Graphics.FromImage(pBmp);

// clear the bmp

g.Clear(Color.LightGray);

// make point arrays for the lines that will be drawn

Point pointSP = new Point[qSP.Count];

Point pointPV = new Point[qPV.Count];

Point pointMV = new Point[qMV.Count];

for (x = 0; x < qSP.Count; x++)

{

y = 1000 - (int)Math.Round(qSP.ElementAt(x));

//if (y > 1000) y = 1000; // prevent the line from falling out of
image.

pointSP = new Point(x, y);

y = 1000 - (int)Math.Round(qPV.ElementAt(x));

//if (y > 993) y = 993; // prevent the line from falling out of
image.

pointPV = new Point(x, y);

y = 1000 - (int)Math.Round(qMV.ElementAt(x));

//if (y > 993) y = 993; // prevent the line from falling out of
image.

pointMV = new Point(x, y);

}

/*

foreach (double d in qSP)

{

y =  1000 - (int)Math.Round(d);

//gPath.AddRectangle(new Rectangle(x,y,4,4));

pointSP = new Point(x, y);

x++;

}

*/

if (pointSP.Length > 1)

{

//g.DrawPath(Pens.Red, gPath);

g.DrawLines(pSP, pointSP);

g.DrawLines(pPV, pointPV);

g.DrawLines(pMV, pointMV);

}

g.Dispose();

}

}

}

And finally an old Basic-PID-controller:

' Variables:

’   
Input         
Process input

’   
InputD        
Process input plus
derivative

’   
InputLast      Process input from
last pass, used in deriv. calc.

’   
Err           
Error, Difference between input and set point

’   
SetP          
Set
point

’    OutPutTemp    
Temporary value of output

’   
OutP          
Output of PID algorithm

’   
Feedback       Result of lag
in positive feedback loop.

’   
Mode          
value is ‘AUTO’ if loop is in automatic

’   
Action        
value is ‘DIRECT’ if loop is direct acting

’    The PID emulation code:

IF Mode = ‘AUTO’ THEN

InputD=Input+(Input-InputLast)*Derivative *60 
derivative.

InputLast = Input

Err=SetP-InputD            
'Error based on reverse action.

IF Action = ‘DIRECT’ THEN Err=0 – Err  'Change sign
if direct.

ENDIF

OutPutTemp = Err*Gain+Feedback 'Calculate the gain time
the error and add the feedback.

IF OutPutTemp > 100 THEN OutPutTemp =100  'Limit
output to between

IF OutPutTemp < 0 THEN OutPutTemp
=0       '0 and 100 percent.

OutP =
OutPutTemp                            
'The final output of the controller.

Feedback=Feedback+(OutP - Feedback)*ResetRate/60

ELSE

InputLast=Input   'While loop in manual, stay
ready for bumpless switch to Auto.

Feedback=OutP

ENDIF

'If external feedback is used, the variable “OutP” in line
11 is replaced with the variable containing the external
feedback.

The C#-version is not complete - it’s the main-file from the project’s source-code. It won’t work in unity without modification - and I’m not familiar with C#.

If there was a ready-to-go Unity-Javascript (or for professionals a C#-script), a PID-regulator could be used for any purpose in future: No matter if simulating automatic-gears in a car, some stove in a puzzle-game, cruise-controls in airplanes or automatic spotlight-adjustment in games with day-night-cycles. Possible applications are countless. So please help.