[Discussion/Research] Manual transmission resource


#1

Manual Transmission resource

Abstract

The past few days I’ve been digging into making a manual transmission resource for FiveM (which is streamable from the server). Into doing so I’ve done a lot of research (as in how GTA 5 handling work as well how real life cars work) to come up with a very basic implementation for a basic manual transmission, hoping the community would pick up the work and make better variants. :slight_smile:

If you have any improvements to my code or discrepencies into my research or presumptions on how a realistic car works (I’m not really a car guy), please let me know so we can work on a better version.

Hopefully you guys find some usage out of my researches and tests. Nothing is set in stone, I’m just trying to make a manual transmission that balances reality and feels good to play with.

Background on simulating

What we essentially are trying to simulate are 3 components, the engine, clutch and gearbox, for the sake of this piece of documentation we are keeping these subjects short and sweet.

In a diagram it will look like this:

Engine

car nerd stuff

The engine is the powerhouse of the car, the engine has serveral pistons connecting to the crankshaft, causing a rotational force, this rotational force will be transfered to the input shaft of the gearbox (see diagram) if the clutch is engaged.

The crankshaft will rotate a certain RPM (rotations per minute).
This is clearly what we are looking to simulate, how fast the RPM ramps up depends on the strenght of the engine. The model GTA 5 uses makes this pretty simple, RPM represents the engines output.

Clutch

car nerd stuff

The can be in 2 distinctive states: engaged or disengaged. It being engaged means the crankshaft will make the input shaft rotate, transfering the rotational force.
It can also be disengaged, where the crankshaft and input shaft are disconnecting through the clutch mechanism.

It’s true that clutch can be anywhere between those states. 50% engaged means 50% of the rotational force of the crankshaft will be transfered.
When the clutch padle is released, the two clutch plates will make contact, bringing the input shaft back up to speed. There are a couple of things we would or can simulate:

  • Pressing the clutch pedal, cause the rpm of the input shaft to drop.
  • Re-engaging the clutch should transfer engine RPM based on the procentage the clutch pedal is engaged.
  • Stalling, when the engine doesn’t have enough torque to keep the engine going. (putting it in a gear that it can’t handle)
  • Smoking up the clutch plates, when you drive with a 50% engaged clutch, it will cause a lot of friction between the clutch plates, thus producing a lot of heat, if not melt.
  • Downshifting, should cause a drop in speed, because prop shaft to slow down due to lower gear ratio(s)

Gearbox

car nerd stuff

The gearbox is quite literally a box with gear. The input is a shaft (the input shaft) which carries the torque of engine. The input shaft will make the counter shaft (the one in red). The counter shaft has several gears attached to them. Each of these gear has a different size. As you might know, a smaller gear takes less effort to turn around than a big gear. If the gearbox wouldn’t exist, then the car won’t be able to start due to the lack of torque to keep turning.

That’s why the gearbox has different gear sizes, often reffered as gear ratio. Each car has their own set of gear ratios. Changing the gear ratio can cause the gear to be capable of reaching higher speed (thus a longer gear) and the opposite is true as well.

Now the magic is that you are able to select the correct gear with the shifter. The shifter is connected to a so called shifter fork. The shifter fork pushes syncronizer (the pink ring) into the teeth of gear you selected, making the propshaft rotate at the speed of that specific gear.

Simulation wise, we would only want to simulate the gear ratios.

Implementation

Now actually implementing this is a whole other beast. Since the simulation of GTA 5 is far from reality. I’ve roughly based it around the values you would get from a handling meta.

There are several areas we need to simulate:

  • “The trick”
  • Simulate the RPM and gearbox.
  • Speed limiting (…)
  • Clutch

The trick

programmer stuff

Normally, GTA 5 has it’s own tranmission system… which is fully automatic. Because of this, a manual transmission fully simulated by the GTA 5 code itself is impossible, due it shifting to the next gear automatically.

Luckly there a native function out there that can trick the transmission system. This one specific native is called SetVehicleHighGear. Setting the high gear of the vehicle will make the vehicle itself stop shifting and treat it as one big gear.

This way we can simply simulate our own values and set some pseudo gear, clutch and rpm accordingly.

Simulate the RPM and gearbox.

programmer stuff

For the engine we simply need to calculate the RPM that is outputted to the crankshaft. Also taking in account the gear ratios. In my approach I’ve opted to use the vehicles speed as basis for the RPM. In GTA 5, the RPM’s are normalized between 0.0 and 1.0

To get such values, we need to know 2 things:

  • The top-speed of the vehicle, the handling meta does provide use with a theoretical top speed called fInitialDriveMaxFlatVel
  • The number of gears.

Then we can subdivide all the gears into speed ranges, we need to know the minimum speed for a gear and maximum. For example a car goes 180 km/h max and has 4 gears:

1th gear - 0 km/h -> 45 km/h
2nd gear - 45 km/h -> 90 km/h
3th gear - 90 km/h -> 135 km/h
etc.

Now these values do not include the gear overlapping of the gears (I my code I use 10%
for stalling and over revving), also we need to take gear ratios into consideration. I’m currently using a hardcode table for each gear.

local gConstGearRatios = { 
    3.166,
    1.882,
    1.296,
    0.972,
    0.738
}

These get divided by 10 as follows:

local ratio = gearRatios[currentGear] / 10
local maxSpeed = speedPerGear * (1.0 + ratio)

Note that min speed should also take the ratio of the previous gear into consideration.

To get the RPM it’s fairly simple to do so using a function:

function ValueToProcentageInRange(min, max, input)
    return (input - min) / (max - min)
end

Saying you are in 3th gear:

local currentSpeed = 100
ValueToProcentageInRange(90, 135, 100)

Will output 0.2222 and can be directly used to map the RPM using the SetVehicleCurrentRpm native.
In my example I get my value from some exponential curve, in an attempt to make the RPM more realistic.

Limiting speed

programmer stuff

Now one other issue we are facing is that, because the game considers it as one big gear, it will accelerate indefinitly. Not ideal to have first gear and being capable of drivign 150 km/h+.

So we need to limit this somehow. Since we already know the top speed of every gear, we can simply call the native SetEntityMaxSpeed, like so:

    --Converting km/h to meters per second.
    local gearMaxSpeedInMs = gGearSpeedMax * 0.277778
    SetEntityMaxSpeed(vehicle, gearMaxSpeedInMs)

Now this does have it’s drawbacks. Because the native SetEntityMaxSpeed doesn’t take anything into consideration and “just” locks the speed to the given value. So this would mean downshifting would cause to instantly pop it to the gears make speed you downshifted to.

Now this can be easily circumvented by using linear interpolant to slowly lower to target speed, simulating the slow down you would get when using a clutch.

Clutch

programmer stuff

As of right now I’m not simulating the clutch, but definitely a thing I want to do. Currently looking to make my code more solid. But will definitely keep this thread updated if such change we’re to be added.

The pain points

programmer stuff

From what I can see and have experienced these are the pain points of developement

  • Getting the RPM feel right, previous attempts it felt the RPM was on the high side, or even too low that the car would not longer accelerate.
  • Some functionality are a bitch, adding plenty of if’s and random variables to make neutral rev the engine and reverse actually reversing. it becomes very tedious way to develop, you put them somewhere inside your simulation calculations and hope it doesn’t mess up everything :frowning:

Know what you are getting into when making a MT

A video:

https://streamable.com/obgl8

Using a xbox360 controller:

A to shift up,

B to shift down.

You can remap they keys in the code itself.

Complete code

There are still couple of limitations that I’m facing:

  • Accelerating from a standstill only works in first gear. (realistically you would be able to do this in 2nd gear as well).
  • Downshifting instantly reduces speed (probably needs some linear interpolation)
  • Clutch is not simulated
  • Engine can’t blowup or stall
  • Cars with weak engines
  • Some kind of weird steering twitch :confused:
  • No burn outs :frowning:

Keep in mind that this code is highly experimental and cluttered as much as it can be, also stupid comments and variables that aren’t being used :thinking:

-- Deprecated variables
local vehicle = nil
local acclerationInputValue = -1
local engineRpm = 0
local simulatedEngineRpm = 0

---------------------------------------
-- Real time variables (rt -> realtime)
local rtEngineRpm = 0
local rtVehicleSpeed = 0

---------------------------------------
-- Vehicle stats variables (vst -> vehicle stat)
local vstTheoreticalMaxSpeed = 0;
local vstAcceleration = 0
local vstNumberOfGears = 0;

---------------------------------------
-- Gear variables (g -> gear)
local gMaxGears = 0;
local gGearCurrent = 0
local gGearDiff = 0
local gGearSpeedMin = 0
local gGearSpeedMax = 0
local gGearSpeedPrime = 0
local gConstStallingFactor = 0.2   -- -10% of gGearSpeedMin is the stalling range.
local gConstOverevvingFactor = 0.2  -- +10% of gGearSpeedMax is the over revving range.

-- Not const btw, car configuration at some point...
local gConstGearRatios = { 
    3.166,
    1.882,
    1.296,
    0.972,
    0.738
}

local gGearCurrentRatio = 0

---------------------------------------
-- Engine variables (e -> engine)
local eEngineRpm = 0
local eEngineTargetSpeed
local eConstEngineIdleRpm = 0.2

---------------------------------------
-- Throttle variables (t -> throttle)
local tThrottleRaw = 0
local tThrottleFull = 0


--TODO: Only run when the engine is started
--TODO: Don't simulate on basis of km/h, use m/s instead.
CreateThread(function()
    local player = PlayerPedId()
    while true do

        -- Check if the player is in any vehicle, apply gearbox calculations
        -- TODO: Some neater way of detecting vehicle changes/exits.
        if IsPedInAnyVehicle(player, false) then
            local currentVehicle = GetVehiclePedIsIn(player, false)

            -- Get information about the new vehicle
            if vehicle == nil or currentVehicle ~= vehicle then
                print("Got new vehicle info")
                ResetLastVehicle()
                vehicle = currentVehicle
                vstTheoreticalMaxSpeed = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fInitialDriveMaxFlatVel") * 1.32 
                vstAcceleration = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fInitialDriveForce")
                vstNumberOfGears = 5 --TODO: We are always assuming 5 gears, we want to get max gear for the current car from somewhere.
                SetVehicleHighGear(vehicle, 1)
                SimulateGears()
            end

            rtEngineRpm = GetVehicleCurrentRpm(vehicle)
            rtVehicleSpeed = GetEntitySpeed(vehicle) * 3.6

            DisableControlAction(0, 80, true)
            DisableControlAction(0, 21, true)
            --DisableControlAction(2, 72, true)

            
            -- Shift up and down
            if IsDisabledControlJustPressed(0, 21) then
                gGearCurrent = gGearCurrent + 1
                SimulateGears()
            elseif IsDisabledControlJustPressed(0, 80) then
                gGearCurrent = gGearCurrent - 1
                SimulateGears()
            end

            SimulateClutch()
            SimulateEngine()
            SimulateSpeed()
        else
            -- Reset the vehicle we left
            if vehicle ~= nil and DoesEntityExist(vehicle) then
                ResetLastVehicle()
            end
        end

        Wait(1)
    end
end)

function SimulateGears()
    gGearCurrent = Clamp(gGearCurrent, -1, vstNumberOfGears)

    --TODO: What do we need to do to make reverse happen?
    --TODO: What do we need to do when in neutral?

    -- We are in neutral or reverse, we don't have anything to simulate...
    --      Clear out the gear variables just in case.
    if gGearCurrent == 0 then 
        gGearSpeedMin = 0
        gGearSpeedMax = 0
        gGearSpeedPrime = 0
        gGearCurrentRatio = 1.0
        return 
    elseif gGearCurrent == 1.0 then 
        gGearSpeedMin = 0
        gGearSpeedMax = 60
    end

    local gearCurrentClamped = Clamp(gGearCurrent, 1, vstNumberOfGears)
    gGearCurrentRatio = 1.0 + gConstGearRatios[gearCurrentClamped] / 10
    local speedPerGear = (vstTheoreticalMaxSpeed / vstNumberOfGears) * gGearCurrentRatio
    
    -- Speed min is the top speed of the previous gear excluding over-revving %.
    --      Don't include reverse and neutral
    -- TODO: Previous gear needs to take in account the gear ratio of the previous gear.
    local previousGear = Clamp(gGearCurrent - 1, 0, vstNumberOfGears)
    local speedPreviousGear = previousGear * speedPerGear
    gGearSpeedMin = speedPreviousGear * (1.0 - gConstStallingFactor)                                -- Minimum speed for this gear
    gGearSpeedMax = (gGearCurrent * speedPerGear) * (1.0 + gConstOverevvingFactor)                  -- Max speed for this gear
    gGearSpeedPrime = ProcentageToValueInRange(speedPreviousGear, gGearSpeedMax, 0.7)               -- The best speed to shift (i.e. b)
    gGearDiff = Clamp((gGearCurrent / vstNumberOfGears), 0.25, 10.0) * (1.0 - vstAcceleration)      -- The difference in gears, "1.0 - vstAcceleration" is for cars with low engine power
end

function SimulateClutch()

    -- Tell the game we are in neutral, we don't want to spin the tires in neutral.
    if gGearCurrent == 0 then
        local hash = GetHashKey('SET_VEHICLE_CURRENT_GEAR') & 0xFFFFFFFF
        local hash2 = GetHashKey('SET_VEHICLE_NEXT_GEAR') & 0xFFFFFFFF
        
        Citizen.InvokeNative(hash, vehicle, 0)
        Citizen.InvokeNative(hash2, vehicle, 0)
    end
end

function SimulateEngine()
    -- TODO: Stall if < 0.2 rpm
    -- TODO: Overrevving 
    
    tThrottleRaw = GetControlNormal(0, 71) 
    tThrottleFull = (vstAcceleration * tThrottleRaw * GetFrameTime() * 100) * gGearDiff

    -- Do neutral things.
    if gGearCurrent == 0 then
        if tThrottleRaw > 0 then 
            SetVehicleCurrentRpm(vehicle, 1.0)
        else
            SetVehicleCurrentRpm(vehicle, 0.0)
        end
    elseif gGearCurrent == -1 then
        if tThrottleRaw > 0 then 
            SetVehicleCurrentRpm(vehicle, -1.0)
        else
            SetVehicleCurrentRpm(vehicle, 0.0)
        end
    else
        -- Returns the RPM, unclamped.
        eEngineRpm = ValueToProcentageInRange(gGearSpeedMin, gGearSpeedMax, rtVehicleSpeed) + eConstEngineIdleRpm + tThrottleFull
        engineRpm = ExponentialCurve(engineRpm, gGearDiff / 100)
        local rpm = Clamp(eEngineRpm, 0.0, 1.0)
        SetVehicleCurrentRpm(vehicle, rpm)
    end
end

function SimulateSpeed()
    --Note: don't lock speed in max gear.
    local gearMaxSpeedInMs = gGearSpeedMax * 0.277778
    SetEntityMaxSpeed(vehicle, gearMaxSpeedInMs)
end

-- TODO: Reduce RPM to a nominal value when shifting up.
-- TODO: Multiply RPM by some curve (sigmoid?)
-- TODO: Enable up/down shifting with proper RPM
-- TODO: Neutral and reverse... how would this work?
-- TODO: Overlapping RPM/speed ranges, allowing for stalling and overrevving
-- TODO: Allow revving in neutral
-- TODO: Disallow reversing when not in neutral... or instanly explode the car :thinking:

-- TODO: A more solid way of resetting a car.
function ResetLastVehicle()
    --TODO: This needs cleaning up.
    SetVehicleHighGear(vehicle, 5) --TODO: REMOVE
    engineRpm = 0
    vehicle = nil
    gGearCurrent = 0
    gGearSpeedMin = 0
    gGearSpeedMax = 0
    gGearSpeedPrime = 0 
    gMaxGears = 0
    gGearCurrentRatio = 0
    vstNumberOfGears = 0
    vstAcceleration = 0
    vstTheoreticalMaxSpeed = 0
    rtEngineRpm = 0
    rtVehicleSpeed = 0
    eEngineRpm = 0
    tThrottleRaw = 0
    tThrottleFull = 0
    print("Vehicle info has been reset")
end

-- TODO: Seggregate this code to an ui thread or so
CreateThread(function()
    local player = PlayerPedId()
    while true do

        if vehicle ~= nil then
            DrawScreenText2D(0.01, 0.01, "-= Realtime", true)   
            DrawScreenText2D(0.01, 0.03, string.format("rtVehicleSpeed: %s", tostring(rtVehicleSpeed)), true)
            DrawScreenText2D(0.01, 0.05, string.format("rtEngineRpm: %s", tostring(rtEngineRpm)), true)

            DrawScreenText2D(0.01, 0.08, "-== Vehicle stats", true)   
            DrawScreenText2D(0.01, 0.10, string.format("vstTheoreticalMaxSpeed: %s", tostring(vstTheoreticalMaxSpeed)), true)
            DrawScreenText2D(0.01, 0.12, string.format("vstAcceleration: %s", tostring(vstAcceleration)), true)            
            DrawScreenText2D(0.01, 0.14, string.format("vstNumberOfGears: %s", tostring(vstNumberOfGears)), true)

            DrawScreenText2D(0.01, 0.17, "-== Gearbox", true) 
            DrawScreenText2D(0.01, 0.19, string.format("gGearCurrent: %s", tostring(gGearCurrent)), true)
            DrawScreenText2D(0.01, 0.21, string.format("gGearDiff: %s", tostring(gGearDiff)), true)
            DrawScreenText2D(0.01, 0.23, string.format("gGearCurrentRatio: %s", tostring(gGearCurrentRatio)), true)
            DrawScreenText2D(0.01, 0.25, string.format("gGearSpeedMin: %s", tostring(gGearSpeedMin)), true)
            DrawScreenText2D(0.01, 0.27, string.format("gGearSpeedMax: %s", tostring(gGearSpeedMax)), true)
            DrawScreenText2D(0.01, 0.29, string.format("gGearSpeedPrime: %s", tostring(gGearSpeedPrime)), true)

            DrawScreenText2D(0.01, 0.32, "-== Engine", true) 
            DrawScreenText2D(0.01, 0.34, string.format("eEngineRpm: %s", tostring(eEngineRpm)), true)
            DrawScreenText2D(0.01, 0.36, string.format("eEngineSpeed: %s", tostring(eEngineSpeed)), true)
            DrawScreenText2D(0.01, 0.38, string.format("eConstEngineIdleRpm: %s", tostring(eConstEngineIdleRpm)), true)

            DrawScreenText2D(0.01, 0.41, "-== Throttle", true) 
            DrawScreenText2D(0.01, 0.43, string.format("tThrottleRaw: %s", tostring(tThrottleRaw)), true)
            DrawScreenText2D(0.01, 0.45, string.format("tThrottleFull: %s", tostring(tThrottleFull)), true)
            
        end
        Wait(1)
    end
end)

-- TODO: Some util for natives?
function SetVehicleGear(vehicle, gear)
    local hash = GetHashKey('SET_VEHICLE_CURRENT_GEAR') & 0xFFFFFFFF
    Citizen.InvokeNative(hash, vehicle, gear)
    SetVehicleHighGear(vehicle, gear)
end

-- TODO: Seggregate this code to an helper function.
function DrawScreenText2D(x, y, message, dropShadow, outline)
    SetTextFont(0)
    SetTextProportional(1)
    SetTextScale(0.0, 0.3)
    SetTextColour(180, 20, 20, 255)
    SetTextDropshadow(0, 0, 0, 0, 255)
    SetTextEdge(1, 0, 0, 0, 255)

    if dropShadow then
        SetTextDropShadow()
    end

    if outline then
        SetTextOutline()
    end

    SetTextEntry("STRING")
    AddTextComponentString(message)
    DrawText(x, y)
end

-- TODO: Seggregate to util.
function Clamp(value, min, max)
    if value < min then return min end
    if value > max then return max end
    return value
end 

function ValueToProcentageInRange(min, max, input)
    return (input - min) / (max - min)
end

function ProcentageToValueInRange(min, max, input)
    return input * (max - min) + min
end

function ExponentialCurve(n, k)
    return Pow(k, n) - 1 / n - 1
end

Hopefully you guys will pick it up and make it a kickass resource :mascot:
Feel free to hit me up, if you wanna talk manual transmission :thinking:


Access memory in LUA
#2

Hot stuff


#3

Hopefully this will make streaming the manual transmission from the server possible, no longer relying on people having to install a clientside mod (no offense to @ikt, your mod is awesome).

But right now it’s a bit too rough around the edges and I’m starting to work on areas that fall outside of “manual transmission”, namely car engine simulation. So I thought I might as well drop the knowledgde i’ve gathered and see if someone comes up with a proper MT resource :hugs:


#4

Can’t see the video :stuck_out_tongue:

I wouldn’t mind seeing a custom FiveM standalone/server-streamed version, it’d be nicer even than needing a client mod, I’d say. If you don’t already know - the source code for my mod is public and you may use it.

For gear ratios (and available gears), you could read the game memory, though I’m not sure how that native to force one gear changes that.

For the speed limiter you can just lift the throttle (disable throttle input).


#5

I would love to try this and test this, as I was about to try to create something like this at this point. But I don’t know why, but it just doesn’t work for me. I’ve done some custom resources before (even the ones directly controlling vehicles), but when I’ve put in this one, game just ignore it. GTA’s AT again.

I’ve stopped every resource like realistic vehicle damage etc. Didn’t help… Any ideas?

Code above is just client.lua + latest resource.lua, right?


#6

I had this happen too, usually restarting the resource from the console makes it work again.


#7

I though that

vstNumberOfGears = GetVehicleHandlingFloat(vehicle, “CHandlingData”, “nInitialDriveGears”)

might be very simple solution for getting accurate number of gears for every car. Unfortunately, this shows up after (…) :smile:

gears

and when I change to GetVehicleHandlingINT, this

gears2

:rofl:

Edit.: At least, in the last case, I am somehow able to shift up to “actual” highest gear of each car, even though that insanely high value above messed up the whole calculation for min and max speed for each gear, which means I can’t move, because the car is limited to 0.00023 km/h…


FiveM is not reading handling data correctly?
#8

Yes, looks like a bug in FiveM. It actually spits errors in console when requesting nInitialDriveGears. I’m aware as I’ve tried this myself. It was the original intent to get the number of gears from the handling file.

I should file a bug report on this :confused:


#9

Please, do. This sucks big time!

Edit.: Also, is ok, if I will try to post some progress/testing here?


#10

image

I think you can :stuck_out_tongue: