- Home /
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
I found a function called Vector3.SmoothDamp but can't use it. Unity doesn't compile and says something like "wrong version"...and "Error BCE0025". Not sure it is what I was looking for anyway:
http://unity3d.com/support/documentation/ScriptReference/Vector3.SmoothDamp.html
Any other suggestions?...Someone?
To summarize the above text:
1) Why does my ship accelerate in only one (world-) direction?
2) How do I calculate an acceleration depending on current and target speed?
It's just an idea - would Quaternion.FromToRotation do the trick?
http://unity3d.com/support/documentation/ScriptReference/Quaternion.FromToRotation.html
Ins$$anonymous$$d of two different position-vectors I would give the function two different velocity-vectors (e.g. 0,0,10 and 0,0,100). $$anonymous$$aybe the difference in vector-length would be interpolated by the function, resulting in an acceleration in Z-direction....
But how do I calculate the coordinates for different speed-values then (for the velocity set by the user and the current speed)?
This is the example from script-reference:
transform.rotation = Quaternion.FromToRotation (Vector3.up, transform.forward);
You can interpolate between two vectors or positions using Vector3.$$anonymous$$oveTowards(p1, p2, distance): it returns a vector3 at distance units from p1 in the p2 direction, and never goes beyond p2.
Answer by spacepilot · Dec 13, 2011 at 09:46 AM
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):
#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 = Kp*error + 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) 2009Distributed under "The Code Project Open License (CPOL) 1.02"
http://www.codeproject.com/info/CPOL.zip
Modify and/or re-apply code at your own risk.
/
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 = Kp*error + Ki*integral + 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<double> qSP; //collection of Setpoints
private Queue<double> qPV; //collection of Precess Values
private Queue<double> 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<double>(1000);
qPV = new Queue<double>(1000);
qMV = new Queue<double>(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[x] = 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[x] = 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[x] = new Point(x, y);
}
/
foreach (double d in qSP)
{
y = 1000 - (int)Math.Round(d);
//gPath.AddRectangle(new Rectangle(x,y,4,4));
pointSP[x] = 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:
' 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.
"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*dtfor calculating the integral. What am I missing?"
I think I can answer myself: They all use the simple rectangle-method:
ErrorValue * DeltaTime
...is a square under the curve in one time-interval:

The time is on the X-, the error-value on the Y-Axis.
With doing a huge number of time-steps during operation, the overall error in the controller will get smaller and smaller. It's digitalization: At the end, with just enough steps, it doesn't matter that in fact many rectangles got integrated ins$$anonymous$$d of a curve.
![]()
I think integration could at least be done a bit better for a manual written javascript:
The trapezoid-rule at least uses a better shape during integration. It's code is simple too and I bet the selfmade PID-controller will give better results a lot earlier than usual PIDs.
In every step this area will be calculated:
Trapezoid-Integration
Ins$$anonymous$$d of only this:
Rectangle-Integration
$$anonymous$$ore and better integration methods are visualized here. But since the time-steps are small (e.g. 100ms) and their number high, such an improvement would hardly cause a noticeable effect. The integration-error will be $$anonymous$$imized too fast for the need of a better method which is a lot harder to script. Trapezium-integration is simple and enough.
C-Code for Trapezium-rule from here:
template <class ContainerA, class ContainerB>
double trapezoid_integrate(const ContainerA &x, const ContainerB &y) {
if (x.size() != y.size()) {
throw std::logic_error("x and y must be the same size");
}
double sum = 0.0;
for (int i = 1; i < x.size(); i++) {
sum += (x[i] - x[i-1]) * (y[i] + y[i-1]);
}
return sum * 0.5;
}
I'm sorry I'm just a program$$anonymous$$g-noob, but how do I convert the whole thing (PID-Controller with Trapezium-Integration) into a javascript-script for any use in unity? I don't even get why they output an error when X and Y are not equal (in above code).
Tried putting the long code-blocks in \-tags to shrink them into small scroll-boxes. What works in the preview just gets overrided by the forum's system. So...sorry for the long threat. It's the ad$$anonymous$$'s fault.
Finally the formula for integration and it's explanation for the trapezodial-rule:
The yellow area is equal to the light-blue area. To calculate trapezoids under the curve (ins$$anonymous$$d of rectangles), we multiply the yellow's area width with it's height:
Width: deltatime (x-difference in every step)
Height: The average of two points on the curve (two measured values in a timeframe, (Y1+Y2) / 2)
Width * Height = Area
(x2-x1) * (y1+y2) / 2 = converged Integral from x1 to x2
Since there still is a white uncovered area above the trapezoid, we just increase the number of calculations per timestep to get a close overall-value. Integrating 100 times per second, gives a good result for usage.
Provided a timestep is one second on the picture, the light-blue-trapezoid gets replaced with 100 very small trapezoids, filling even more of the area under the curve. As with digicam-pictures, the "resolution" at a certain level just is good enough for usage. With doing 100 integrations per second we just say the area under the curve is calculated, since the missing part is too small to be of interest.
To use trapezodial ins$$anonymous$$d of rectangle-integration, the calculation in the posted examples need to be exchanged as follows: Ins$$anonymous$$d of
Integral = Integral + Errorvalue * deltatime;
it should read:
Integral = Integral + (Errorvalue + PreviousError)/2 * deltatime;
to pimp the selfmade PI-regulator additionally. In the function, deltatime has to bet set to 1/100 second manually with some wait-function to always get two different Errorvalues. The magic JS-code should look somewhat like this:
time2=0;
while(counter <= integrals){
counter += 1;
time1=Time.time;
deltatime=time1-time2;
Errorvalue = target - current;
// Rectangle-Integration (unprecise):
// Integral = Integral + Errorvalue * deltatime;
//Trapezoid-Integration (better than Rectangle-$$anonymous$$ethod, using midpoints):
Integral = Integral + (Errorvalue + PreviousError)/2 * deltatime;
Derivative = (Errorvalue - PreviousError)/deltatime;
Output = $$anonymous$$p*Errorvalue + $$anonymous$$i*Integral + $$anonymous$$d*Derivative;
PreviousError = Errorvalue;
waitms (itime); // Test this !!
time2=Time.time;
} //Went
function waitms(timevalue:float){
timecapture1 = Time.time;
timecapture2 = Time.time;
while (timecapture2 < timecapture1 + timevalue){
timecapture2 = Time.time;
} //went
} //EndFunc waitms
The whole thing is still not tested. I don't know if the function waitms will do what it should and if unity's integrated wait wouldn't do it too ...
Answer by aldonaletto · Dec 11, 2011 at 02:33 PM
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: http://en.wikipedia.org/wiki/PID_controller)
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.
Thanks for the teleporting-tip. I didn't find a local alternative to rigidbody.velocity. The ship get's accellerated only on it's local X-Axis (X- and Z-Axis got switched during mesh-import). I'll try the new clamp-function as soon as possible.
Is there a local velocity nevertheless or is rigidbody.velocity.magnitude in fact the local velocity?
"transform.InverseTransformDirection transforms a direction from world space to local space", reads the script-reference.
Does this also apply to velocity? Is the following line correct then?
Debug.Log ("Current Speed: " +transform.InverseTransformDirection(rigidbody.velocity.x));
If yes, I could do the same with clamp-function and without using magnitude (which I don't understand completely).
Think of a vector as an arrow based on (0,0,0) and with its tip on the (x,y,z) vector coordinates, and you can figure out the direction and magnitude (the arrow length). Vector3.Clamp$$anonymous$$agnitude adjusts the (x,y,z) coordinates proportionally so that the vector length is lower or equal to the limit, but without altering its direction.
Anyway, you could use InverseTransformDirection this way:
var locVel = transform.InverseTrasformDirection(rigidbody.velocity);
The variable locVel will contain the "local velocity version", and locVel.x is its forward direction (in this case). But there's a problem: if the ship collide with anything, z and y may become non-zero, thus everything will become weird again.
@spacepilot, I posted a velocity-based version of your script in my answer - it's easier to use, and fakes a constant acceleration up to accel, or a constant deceleration to 0.
NOTE: I was wrong about the teleport - Translate defaults to local space, thus the boost/slide code was correct.
Dude, I don't know what you want: I answered your questions using all my experience as analog and digital designer, and as a game programmer, but you systematically doubted everything I said! What's the point in asking something if you are not going to accept the answer?
I edited my answer once again and included the I error correction. It was tested and works perfectly - but, as I said, the I factor mess things a lot. Attach it to a rigidbody, set any targetSpeed value you want and watch the variables curSpeed and force in the Inspector changing to reach targetSpeed. Play around with gravity on/off, drag, pGain, iGain, maxForce etc. Notice that setting iGain to zero makes the system much more stable, but cause a residual error. Zeroing drag and turning gravity off kills the residual error with or without iGain.
Answer by spacepilot · Dec 12, 2011 at 06:59 PM
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?
@aldonaletto: I can see that PGain in a P-regulator must be something like a constant pre-defined acceleration, the regulator is "regulating around" (sometimes more, sometimes less). So...for the time between start and target, the P-regulator continuously applies parts of that pre-defined acceleration to the vehicle/vessel or multiplies that predefined amount several times. $$anonymous$$y question is now: What is IGain for and in which limits does the regulator apply quantities of that pre-defined acceleration? I think the integral part avoids the signal to "run-away", once the target is reached.
Since the I part is accumulating the error signal, when the error becomes zero the I part stops growing/decreasing: it just "stores" the value needed to keep a zero error signal. The IGain defines how much I signal will be added to the P signal to drive the force. It's more critical than the PGain, because it can cause much more instabilities - that's why you should avoid it whenever possible.
Can you write that in a formula? I don't see any reason for an IGain at all when the integration-result already is the accumulated error-signal. You just need to add it to the P-part. Without I-part, problems can occur with reaching the target. With the always remaining error-signal from the P-regulator the ship won't ever stop accelerating because at the end, the regulator itself produces a difference between current and target-speed. The I-part is fundamental!
Your answer
