JCar port to JS

if anyone is intrested…

original code

enum JWheelDrive {

    Front = 0,

    Back = 1,

    All = 2

}



    // if connected the controls will block if object not  active

    // (for example steer only if car camera is active).

var checkForActive : GameObject;

    

var wheelFR : Transform; // connect to Front Right Wheel transform

var wheelFL : Transform; // connect to Front Left Wheel transform

var wheelBR : Transform; // connect to Back Right Wheel transform

var wheelBL : Transform; // connect to Back Left Wheel transform

        

var suspensionDistance : float = 0.2; // amount of movement in suspension

var springs : float= 1000.0; // suspension springs

var dampers : float = 2; // how much damping the suspension has

var wheelRadius : float = 0.25; // the radius of the wheels

var torque : float = 100; // the base power of the engine (per wheel, and before gears)

var brakeTorque : float = 2000; // the power of the braks (per wheel)

var wheelWeight : float = 3; // the weight of a wheel

var shiftCentre : Vector3 = new Vector3(0.0, -0.25, 0.0); // offset of centre of mass

var steer : float;

var accel : float;



var maxSteerAngle : float = 30.0; // max angle of steering wheels

var  wheelDrive : JWheelDrive = JWheelDrive.Front; // which wheels are powered

    

var shiftDownRPM : float  = 1500.0; // rpm script will shift gear down

var shiftUpRPM : float = 2500.0; // rpm script will shift gear up

var idleRPM : float = 500.0; // idle rpm

    

var fwdStiffness : float = 0.1; // for wheels, determines slip

var swyStiffness : float = 0.1; // for wheels, determines slip

    

    // gear ratios (index 0 is reverse)

var gears : float[] = [-10, 9, 6, 4.5, 3, 2.5 ];

    

    // automatic, if true car shifts automatically up/down

var automatic : boolean = true;

    

var killEngineSoundTimeout : float = 3.0; // time until engine sound is cut off (in s.)

    

    // table of efficiency at certain RPM, in tableStep RPM increases, 1.0f is 100% efficient

    // at the given RPM, current table has 100% at around 2000RPM

var efficiencyTable : float[] = [ 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 1.0, 1.0, 0.95, 0.80, 0.70, 0.60, 0.5, 0.45, 0.40, 0.36, 0.33, 0.30, 0.20, 0.10, 0.05 ];

    

    // the scale of the indices in table, so with 250f, 750RPM translates to efficiencyTable[3].

var efficiencyTableStep : float = 250.0;

    

var currentGear :int = 1; // duh.

    

// shortcut to the component audiosource (engine sound).

var audioSource : AudioSource;



// every wheel has a wheeldata struct, contains useful wheel specific info

class WheelData {

    public var transform :  Transform;

    public var go :  GameObject;

    public var col :  WheelCollider;

    public var startPos : Vector3;

    public var rotation : float = 0.0;

    public var maxSteer : float;

    public var motor : boolean;

};



private var wheels : WheelData[]; // array with the wheel data







// setup wheelcollider for given wheel data

// wheel is the transform of the wheel

// maxSteer is the angle in degrees the wheel can steer (0f for no steering)

// motor if wheel is driven by engine or not

function SetWheelParams(wheel : Transform, maxSteer : float, motor : boolean) : WheelData {

    if (wheel == null) {

        throw new System.Exception("wheel not connected to script!");

    }

    var result : WheelData = new WheelData(); // the container of wheel specific data



    // we create a new gameobject for the collider and move, transform it to match

    // the position of the wheel it represents. This allows us to do transforms

    // on the wheel itself without disturbing the collider.

    var go : GameObject = new GameObject("WheelCollider");

    go.transform.parent = transform; // the car, not the wheel is parent

    go.transform.position = wheel.position; // match wheel pos

    

    // create the actual wheel collider in the collider game object

    var col : WheelCollider = go.AddComponent(typeof(WheelCollider));

    col.motorTorque = 0.0;

    

    // store some useful references in the wheeldata object

    result.transform = wheel; // access to wheel transform 

    result.go = go; // store the collider game object

    result.col = col; // store the collider self

    result.startPos = go.transform.localPosition; // store the current local pos of wheel

    result.maxSteer = maxSteer; // store the max steering angle allowed for wheel

    result.motor = motor; // store if wheel is connected to engine

    

    return result; // return the WheelData

}





// Use this for initialization

function Start () {

    // 4 wheels, if needed different size just modify and modify

    // the wheels[...] block below.

    wheels = new WheelData[4];

    

    // setup wheels

    var frontDrive : boolean = (wheelDrive == JWheelDrive.Front) || (wheelDrive == JWheelDrive.All);

    var backDrive : boolean = (wheelDrive == JWheelDrive.Back) || (wheelDrive == JWheelDrive.All);

    

    // we use 4 wheels, but you can change that easily if neccesary.

    // this is the only place that refers directly to wheelFL, ...

    // so when adding wheels, you need to add the public transforms,

    // adjust the array size, and add the wheels initialisation here.

    wheels[0] = SetWheelParams(wheelFR, maxSteerAngle, frontDrive);

    wheels[1] = SetWheelParams(wheelFL, maxSteerAngle, frontDrive);

    wheels[2] = SetWheelParams(wheelBR, 0.0, backDrive);

    wheels[3] = SetWheelParams(wheelBL, 0.0, backDrive);

    

    // found out the hard way: some parameters must be set AFTER all wheel colliders

    // are created, like wheel mass, otherwise your car will act funny and will

    // flip over all the time.

    for (var w : WheelData in wheels) {

        var col : WheelCollider = w.col;

        col.suspensionDistance = suspensionDistance;

        var js : JointSpring = col.suspensionSpring;

        js.spring = springs;

        js.damper = dampers;            

        col.suspensionSpring = js;

        col.radius = wheelRadius;

        col.mass = wheelWeight;

                    

        // see docs, haven't really managed to get this work

        // like i would but just try out a fiddle with it.

        var fc : WheelFrictionCurve = col.forwardFriction;

        fc.asymptoteValue = 5000.0f;

        fc.extremumSlip = 2.0f;

        fc.asymptoteSlip = 20.0f;

        fc.stiffness = fwdStiffness;

        col.forwardFriction = fc;

        fc = col.sidewaysFriction;

        fc.asymptoteValue = 7500.0f;

        fc.asymptoteSlip = 2.0f;

        fc.stiffness = swyStiffness;

        col.sidewaysFriction = fc;

    }

    

    // we move the centre of mass (somewhere below the centre works best.)

    rigidbody.centerOfMass += shiftCentre;

    

    // shortcut to audioSource should be engine sound, if null then no engine sound.

    audioSource = GetComponent(AudioSource);

    if (audioSource == null) {

        Debug.Log("No audio source, add one to the car with looping engine noise (but can be turned off");

    }

    

}





function Update() {

    if (Input.GetKeyDown("page up")) {

        ShiftUp();

    }

    if (Input.GetKeyDown("page down")) {

        ShiftDown();

    }

}



var shiftDelay : float = 0.0;





// handle shifting a gear up

function ShiftUp() {

    var now : float = Time.timeSinceLevelLoad;

    

    // check if we have waited long enough to shift

    if (now < shiftDelay) return;

    

    // check if we can shift up

    if (currentGear < gears.Length - 1) {

        currentGear ++;

        

        // we delay the next shift with 1s. (sorry, hardcoded)

        shiftDelay = now + 1.0;

    }

}



// handle shifting a gear down

function ShiftDown() {

    var now : float = Time.timeSinceLevelLoad;



    // check if we have waited long enough to shift

    if (now < shiftDelay) return;

    

    // check if we can shift down (note gear 0 is reverse)

    if (currentGear > 0) {

        currentGear --;



        // we delay the next shift with 1/10s. (sorry, hardcoded)

        shiftDelay = now + 0.1f;

    }

}



var wantedRPM : float = 0.0; // rpm the engine tries to reach



var motorRPM : float  = 0.0;



var killEngine : float = 0.0;



// handle the physics of the engine

function FixedUpdate () {

    var delta : float = Time.fixedDeltaTime;

    

    steer = 0; // steering -1.0 .. 1.0

    accel = 0; // accelerating -1.0 .. 1.0

    var brake : boolean = false; // braking (true is brake)

    

    if ((checkForActive == null) || checkForActive.active) {

        // we only look at input when the object we monitor is

        // active (or we aren't monitoring an object).

        steer = Input.GetAxis("Horizontal");//InputManager.x;

        accel = Input.GetAxis("Vertical");//InputManager.y;

        brake = Input.GetButton("Jump");

    }

    

    // handle automatic shifting

    if (automatic  (currentGear == 1)  (accel < 0.0f)) {

        ShiftDown(); // reverse

    }

    else if (automatic  (currentGear == 0)  (accel > 0.0f)) {

        ShiftUp(); // go from reverse to first gear

    }

    else if (automatic  (motorRPM > shiftUpRPM)  (accel > 0.0f)) {

        ShiftUp(); // shift up

    }

    else if (automatic  (motorRPM < shiftDownRPM)  (currentGear > 1)) {

        ShiftDown(); // shift down

    }

    if (automatic  (currentGear == 0)) {

        accel = - accel; // in automatic mode we need to hold arrow down for reverse

    }

    if (accel < 0.0) {

        // if we try to decelerate we brake.

        brake = true;

        accel = 0.0;

        wantedRPM = 0.0;

    }



    // the RPM we try to achieve.

    wantedRPM = (5500.0 * accel) * 0.1 + wantedRPM * 0.9;

    

    var rpm : float = 0.0;

    var motorizedWheels : int = 0;

    var floorContact : boolean = false;

    

    // calc rpm from current wheel speed and do some updating

    for (var w : WheelData in wheels) {

        var hit : WheelHit;

        var col :WheelCollider = w.col;

        

        // only calculate rpm on wheels that are connected to engine

        if (w.motor) {

            rpm += col.rpm;

            motorizedWheels++;

        }

        

        // calculate the local rotation of the wheels from the delta time and rpm

        // then set the local rotation accordingly (also adjust for steering)

        w.rotation = Mathf.Repeat(w.rotation + delta * col.rpm * 360.0f / 60.0f, 360.0f);

        w.transform.localRotation = Quaternion.Euler(w.rotation, col.steerAngle, 0.0f);

        

        // let the wheels contact the ground, if no groundhit extend max suspension distance

        var lp : Vector3 = w.transform.localPosition;

        

        if (col.GetGroundHit(hit)) {

            lp.y -= Vector3.Dot(w.transform.position - hit.point, transform.up) - col.radius;

            floorContact = floorContact || (w.motor);

        }

        else {

            lp.y = w.startPos.y - suspensionDistance;

        }

        w.transform.localPosition = lp;

    }

    // calculate the actual motor rpm from the wheels connected to the engine

    // note we haven't corrected for gear yet.

    if (motorizedWheels > 1) {

        rpm = rpm / motorizedWheels;

    }

    

    // we do some delay of the change (should take delta instead of just 95% of

    // previous rpm, and also adjust or gears.

    motorRPM = 0.95 * motorRPM + 0.05 * Mathf.Abs(rpm * gears[currentGear]);

    if (motorRPM > 5500.0) motorRPM = 5500.0;

    

    // calculate the 'efficiency' (low or high rpm have lower efficiency then the

    // ideal efficiency, say 2000RPM, see table

    var index : int = Mathf.Round(motorRPM / efficiencyTableStep);

    if (index >= efficiencyTable.Length) index = efficiencyTable.Length - 1;

    if (index < 0) index = 0;



    // calculate torque using gears and efficiency table

    var newTorque : float = torque * gears[currentGear] * efficiencyTable[index];



    // go set torque to the wheels

    for(var w : WheelData in wheels) {

        col = w.col;

        

        // of course, only the wheels connected to the engine can get engine torque

        if (w.motor) {

            // only set torque if wheel goes slower than the expected speed

            if (Mathf.Abs(col.rpm) > Mathf.Abs(wantedRPM)) {

                // wheel goes too fast, set torque to 0

                col.motorTorque = 0;

            }

            else {

                // 

                var curTorque : float = col.motorTorque;

                col.motorTorque = curTorque * 0.9 + newTorque * 0.1;

            }

        }

        // check if we have to brake

        col.brakeTorque = (brake)?brakeTorque:0.0;

        

        // set steering angle

        col.steerAngle = steer * w.maxSteer;

    }

    

    // if we have an audiosource (motorsound) adjust pitch using rpm        

    if (audioSource != null) {

        // calculate pitch (keep it within reasonable bounds)

        var pitch : float = Mathf.Clamp(1.0 + ((motorRPM - idleRPM) / (shiftUpRPM - idleRPM) * 2.5), 1.0, 10.0);

        audioSource.pitch = pitch;

        

        if (motorRPM > 100) {

            // turn on sound if it's not playing yet and RPM is > 100.

            if (!audioSource.isPlaying) {

                audioSource.Play();

            }

            // how long we should wait with engine RPM <= 100 before killing engine sound

            killEngine = Time.time + killEngineSoundTimeout;

        }

        else if ((audioSource.isPlaying)  (Time.time > killEngine)) {

            // standing still, kill engine sound.

            audioSource.Stop();

        }

    }

}



function OnGUI() {

    if (checkForActive.active) {

        // calculate actual speed in Km/H (SI metrics rule, so no inch, yard, foot,

        // stone, or other stupid length measure!)

        var speed : float = rigidbody.velocity.magnitude * 3.6;

    

        // message to display

       	var msg : String = "Speed " + speed + "Km/H, " + motorRPM + "RPM, gear " + currentGear; //  + " torque " + newTorque.ToString("f2") + ", efficiency " + table[index].ToString("f2");



        GUILayout.BeginArea(new Rect(Screen.width - 282, 32, 250, 40), GUI.skin.window);

        GUILayout.Label(msg);

        GUILayout.EndArea();

    }

}

OH… your ported it… sorry, was a little confused.

Thanks. :smile:

it might have to do with the gameobject, but it keeps turning slightly to the right.

Hmmm… I put a test car on a terrain, dumped JCar onto it, assigned the wheels, rigid body, and collider. The basics.

I gave the car 1000 in mass, and proceeded to drive it around. The first thing I noticed is that the vehicle does exactly what you are specifying, The back end comes around a bit, next when I make a curve the whole car flips. Not good.

OK, in order to use this script, and script the script you must first understand the fine balance that is a car. First, lets discuss center of mass. You initially set teh vehicle at 0,-0.25, 0. This is all vehicle dependent, I ended up using 0,-0.75, 1. This means that I want more weight on the front wheels, and I want the balance of the vehicle to be towards the front. I will tell you, that as soon as I found my sweet spot, I found that the back end no longer came around or veered while driving.

The next thing you need to do is strike a balance on the Center of Mass using springs. I set the car about 20 feet in the air, and dropped it, The first thing I noticed was that there was no spring. It hit the ground like a rock. So, I got to tinkering with a spring for the car. I ended up with about 4000 spring. This caused the car to bounce slightly when dropped. It even lifted when accelerating, balanced and tipped when stopping. So that was a good deal. As I drove this around, I noticed that since my Center of Mass Z was forward, the mass of the vehicle sat more on the front of the vehicle. Thus causing it to sit leaning. This is not good. You want to strike a balance between the front and back even if the front has more weight.

So to counter this. I added some code:

// new global variable
var balance : float = 1.1; // front/rear spring balance

// added in the assignment of the wheel spring value
var spring=w.transform.localPosition.z>0.0 ? springs * balance : springs / balance;
js.spring = spring;

Once this was done, and I tweaked my car out to about 1.25 spring balance then it started sitting right, and acting a bit more normal.

I could tweak it more but that was ok.

What does need to be added here is individualized wheels. What happens when you have a vehicle with large rear wheels than front? Suddenly the Wheel Radius becomes a liability. What if you wanted a front wheel drive car. JCar is not setup for that.

As a port, it is good. As an overall JCar lacked some functionality that I needed in making a vehicle controller script.

So here is your task… Take what you have done, add in individual wheel assignment. You could also omit some of the variables like wheel radius, by using renderer.bounds.y to get the height of the wheel. There are many other tricks to get you down to “Drag a car, drag the script, set the wheels, set the weight and center of mass and balance” Once you get it that simple, and drivable, you will have something really special.