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.
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:
http://dsportmag.com/word/wp-content/uploads/2017/05/180-AutoBLip-QuickTech-005-GearingDiagram.jpg
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
Know what you are getting into when making a MT
A video:
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
- No burn outs
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
-- 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
Feel free to hit me up, if you wanna talk manual transmission