Version 1.2.
The main change is the addition of the clutch. You can now rev up the engine in neutral, and releasing the clutch when still would stall the engine (the code makes sure that doesn't happen though. still, you can disable the automatic clutch by setting this.automaticClutch to false).
The automatic clutch may still feel a little slow though, it's not trivial to do an efficient algorithm.
The engine provides now 0 torque at 0 RPM, so there is a minimum RPM and a minimum quantity of "fuel" the engine takes to make sure it doesn't go below that limit.
I'd like to implement manual clutch, but unfortunately i don't have a wheel with one, and using a key is just wrong. :/
/*
Vehicle script file
Copyright (C) 2012 - 2013 Giulio Camuffo <giuliocamuffo@gmail.com>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
//see http://xtrac.outerra.com/index.fcgi/wiki/vehicle for example and documentation
// VERSION 1.2
//invoked only the first time the model is loaded, or upon reload
function init_chassis(){
var wheelparam = {
radius: 0.30,
width: 0.20,
suspension_max: 0.1,
suspension_min: -0.05,
suspension_stiffness: 50.0,
damping_compression: 0.06,
damping_relaxation: 0.05,
slip: 5.0,
roll_influence: 0.1,
rotation: -1
};
this.add_wheel('wheel_FL', wheelparam);
this.add_wheel('wheel_FR', wheelparam);
this.add_wheel('wheel_RL', wheelparam);
this.add_wheel('wheel_RR', wheelparam);
this.load_sound("e90_startup.ogg"); //startup
this.load_sound("e90_onidle.ogg"); //idle
this.load_sound("e90_offlow.ogg"); //off
this.load_sound("e90_onlow.ogg"); //on
this.basepitch = [ 1, 1, 0.4, 0.4 ]; //base pitch for start, idle, off, on
this.add_sound_emitter("wheel_FL");
//engine properties -- modify these to change the engine behaviour
this.wheelRadius = 0.24;
this.torquePoints = [ { rpm: 200, torque: 0},
{ rpm: 1000, torque: 250},
{ rpm: 2000, torque: 335},
{ rpm: 3000, torque: 380},
{ rpm: 4000, torque: 400},
{ rpm: 5000, torque: 395},
{ rpm: 6000, torque: 400},
{ rpm: 7000, torque: 390},
{ rpm: 8000, torque: 365},
{ rpm: 9000, torque: 0 } ];
this.forwardGears = [ { ratio: 4.06, shiftUp: 8000, shiftDown: -1 },
{ ratio: 2.40, shiftUp: 8000, shiftDown: 4500 },
{ ratio: 1.58, shiftUp: 8000, shiftDown: 5000 },
{ ratio: 1.19, shiftUp: 8000, shiftDown: 5500 },
{ ratio: 1.00, shiftUp: 8000, shiftDown: 6000 },
{ ratio: 0.87, shiftUp: -1, shiftDown: 6000 } ];
this.reverseGears = [ { ratio: 4.68, shiftUp: -1, shiftDown: -1 } ];
this.differentialRatio = 3.85;
this.efficiency = 0.9;
this.speedLimiter = 5; // maximum speed in m/s. a value < 0 means there's no limiter
this.engineBrakeCoefficient = 0.4;
this.breakingForce = 5000.0;
this.pitchMultiplier = 1.5;
this.automaticTransmission = false;
this.automaticClutch = true;
this.engineInertia = 0.05;
this.minimumRPM = 1000;
this.minimum = 0.2;
//end of engine properties
this.maxPowerTP = this.torquePoints[0];
this.maxRPM = 0;
var maxPw = 0;
for (var i = 1; i < this.torquePoints.length; ++i) {
var tp = this.torquePoints[i];
if (tp.rpm * tp.torque > maxPw) {
this.maxPowerTP = tp;
}
if (tp.rpm > this.maxRPM) {
this.maxRPM = tp.rpm;
}
}
this.neutroGear = { ratio: 0.0, shiftUp: -1, shiftDown: -1, index: 0 };
var prev = null;
for (var i = 0; i < this.forwardGears.length; ++i) {
var gear = this.forwardGears[i];
if (prev)
prev.next = gear;
gear.prev = prev;
gear.index = i + 1;
prev = gear;
}
prev = null;
for (var i = 0; i < this.reverseGears.length; ++i) {
var gear = this.reverseGears[i];
if (prev)
prev.prev = gear;
gear.next = prev;
gear.index = -i - 1;
prev = gear;
}
this.torque = engineTorque;
this.shiftUp = shift_up;
this.shiftDown = shift_down;
this.selectGear = select_gear;
this.die = die;
this.updateAutomaticTransmission = automatic_transmission;
this.animate = animate;
return {mass:1570, steering:1.0, steering_ecf:10000, centering: 2.0, centering_ecf: 10};
}
//invoked for each new instance of the vehicle
function init_vehicle() {
this.set_fps_camera_pos({x:-0.33,y:-0.25,z:1.15});
this.snd = this.sound();
this.snd.set_ref_distance(0, 9.0);
this.started = false;
this.gear = this.neutroGear;
this.direction = 0;
this.engineRPM = 0;
this.appliedTorque = 0;
this.starting = false;
this.clutch = 0.0;
this.clutchState = 1;
}
//invoked when engine starts or stops
function engine(start) {
if (start) {
this.started = false;
this.soundId = 0;
this.starting = true;
} else {
this.started = false;
}
}
function engineTorque(rpm) {
var min = 0;
var max = this.torquePoints.length - 1;
if (rpm > this.torquePoints[max].rpm || rpm < this.torquePoints[min].rpm)
return 0;
while (max - min > 1) {
var mid = Math.floor(min + (max - min) / 2);
var tp = this.torquePoints[mid];
if (tp.rpm == rpm) {
return tp.torque;
} else if (tp.rpm > rpm) {
max = mid;
} else {
min = mid;
}
}
var minTp = this.torquePoints[min];
var maxTp = this.torquePoints[max];
var TM = maxTp.torque;
var Tm = minTp.torque;
var RM = maxTp.rpm;
var Rm = minTp.rpm;
var a = (TM - Tm) / (RM - Rm);
return rpm * a + Tm - Rm * a;
}
function sign(x) {
return (x > 0.0 ? 1 : (x < 0.0 ? -1 : 0));
}
function action(key, value, dt)
{
if (key == AAuxb1 && value == 0) {
this.automaticTransmission = !this.automaticTransmission;
this.log_inf("Automatic transmission " + (this.automaticTransmission ? "enabled" : "disabled"));
}
if (value == 1 && !this.automaticTransmission) {
switch(key) {
case AShiftUp:
this.shiftUp();
break;
case AShiftDown:
this.shiftDown();
break;
default:
break;
}
}
}
function select_gear(gear)
{
var i = this.gear.index;
this.gear = gear;
this.log_inf("gear " + i + " => " + this.gear.index);
}
function shift_up()
{
var i = this.gear.index;
if (this.gear.next) {
this.selectGear(this.gear.next);
} else if (i == 0) {
this.selectGear(this.forwardGears[0]);
} else if (i == -1) {
this.selectGear(this.neutroGear);
}
}
function shift_down()
{
var i = this.gear.index;
if (this.gear.prev) {
this.selectGear(this.gear.prev);
} else if (i == 0) {
this.selectGear(this.reverseGears[0]);
} else if (i == 1) {
this.selectGear(this.neutroGear);
}
}
function die()
{
this.started = false;
}
function automatic_transmission(engine)
{
if (this.engineRPM < 10 && this.gear.index != 0) {
this.gear = this.neutroGear;
this.direction = 0;
this.selectGear(this.neutroGear);
}
if (engine != 0.0) {
if (sign(engine) != this.direction) {
this.direction = sign(engine);
if (this.direction == 1.0) {
this.selectGear(this.forwardGears[0]);
} else if (this.direction == -1.0) {
this.selectGear(this.reverseGears[0]);
}
}
if (this.engineRPM > this.gear.shiftUp && this.gear.next) {
this.shiftUp();
} else if (this.engineRPM < this.gear.shiftDown && this.gear.prev) {
this.shiftDown();
}
}
}
function animate()
{
this.animate_wheels();
}
//invoked each frame to handle the inputs and animate the model
function update_frame(dt, engine, brake, steering) {
steering *= 0.6;
this.steer(-2, steering);
var wheelsRpm = this.max_rpm();
if (sign(this.speed()) != sign(this.gear.index) && this.gear.index != 0) {
wheelsRpm = 0;
}
if (this.automaticTransmission) {
this.updateAutomaticTransmission(engine);
}
if (engine < 0) {
engine = -engine;
}
var clutched = this.clutchState == 1 && this.gear.index != 0;
var clutchRPM = this.minimumRPM + 2000;
if (this.automaticClutch) {
if (clutched && this.engineRPM < this.minimumRPM) {
this.clutchState = 0;
} else if (!clutched && this.engineRPM > clutchRPM) {
this.clutchState = 1;
}
clutched = this.clutchState == 1 && this.gear.index != 0;
}
var w = wheelsRpm * this.gear.ratio * this.differentialRatio;
if (clutched) {
this.clutch = (w > this.minimumRPM ? this.minimumRPM : w) / this.minimumRPM;
// This makes an horizontal S-like shape. (mirrored, like Z)
var f = this.clutch;
if (f < 0.5) {
f = f * 2 - 1;
this.clutch = -(f * f * f * f - 1) / 5;
} else {
f = 2 * f - 1;
this.clutch = f * f * f * f + 0.2;
}
this.clutch += 0.2;
var r = this.engineRPM / clutchRPM;
this.clutch *= (r > 1 ? 1 : (r < 0 ? 0 : r));
if (this.clutch > 1) {
this.clutch = 1;
}
} else if (!clutched) {
this.clutch = 0;
}
this.appliedTorque = 0;
if (this.starting) {
if (this.engineRPM > this.minimumRPM) {
this.starting = false;
this.started = true;
} else {
this.appliedTorque = 100 * (1 - this.engineRPM / this.minimumRPM);
}
}
var oldRPM = this.engineRPM;
if (this.gear.index != 0) {
this.engineRPM = this.engineRPM * (1 - this.clutch) + wheelsRpm * this.clutch * this.gear.ratio * this.differentialRatio;
}
var engineStarted = this.started;
if (engineStarted || this.starting) {
if (this.engineRPM < this.minimumRPM && engine < this.minimum) {
engine = this.minimum;
}
if (Math.abs(this.speed()) < this.speedLimiter) {
this.appliedTorque += engine * this.torque(this.engineRPM);
}
}
var friction = 0.1;
this.appliedTorque -= sign(this.engineRPM) * this.maxPowerTP.torque * this.engineBrakeCoefficient *
(friction + (1.0 - friction) * this.engineRPM / this.maxRPM);
this.engineRPM = this.engineRPM + this.appliedTorque * dt / this.engineInertia;
if (oldRPM < 300) {
this.die();
}
var tq = this.appliedTorque + this.engineInertia * (oldRPM - this.engineRPM) / dt;
var force = tq * this.clutch * this.gear.ratio * this.differentialRatio * this.efficiency / this.wheelRadius;
if (this.gear.index < 0)
force = -force;
this.wheel_force(2, force / 2);
this.wheel_force(3, force / 2);
brake *= this.breakingForce;
this.wheel_brake(-1, brake);
this.animate();
if (this.gear.index == 0 && (!this.snd.is_playing(0) || this.soundId != 1)) {
this.snd.stop(0);
this.snd.play(0, 1, true, false);
this.soundId = 1;
} else if (this.gear.index != 0 && engine != 1 && (!this.snd.is_playing(0) || this.soundId != 2)) { //idle
this.snd.stop(0);
this.snd.play(0, 2, true, false);
this.soundId = 2;
} else if (this.gear.index != 0 && engine == 1 && (!this.snd.is_playing(0) || this.soundId != 3)) { //throttle
this.snd.stop(0);
this.snd.play(0, 3, true, false);
this.soundId = 3;
}
var pitch = this.pitchMultiplier * (this.engineRPM - this.minimumRPM) / this.maxRPM;
this.snd.set_pitch(0, pitch + this.basepitch[this.soundId]);
var g = this.engineRPM / this.minimumRPM;
this.snd.set_gain(0, Math.min(g * g * g, 1.0));
}