Skip to content

Instantly share code, notes, and snippets.

@ikt32
Last active June 15, 2022 15:27
Show Gist options
  • Save ikt32/207027cc29f1869a43f6ccef054e3845 to your computer and use it in GitHub Desktop.
Save ikt32/207027cc29f1869a43f6ccef054e3845 to your computer and use it in GitHub Desktop.
BeamNG steering assist (Last updated with 0.23.5)
-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1.
-- If a copy of the bCDDL was not distributed with this
-- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt
local M = {}
M.keys = {} -- Backwards compatibility
local MT = {} -- metatable
local keysDeprecatedWarned
MT.__index = function(tbl, key)
if not keysDeprecatedWarned then
log("E", "", "Vehicle "..dumps(vehiclePath).." is using input.keys["..dumps(key).."]. This may be removed in the next update; the creator of that vehicle should instead use \"vehicle-specific bindings\".")
keysDeprecatedWarned = true
end
return rawget(M.keys, key)
end
setmetatable(M.keys, MT)
M.state = {}
M.filterSettings = {}
M.lastFilterType = -1
local filterTypes = {[FILTER_KBD] = "Keyboard", [FILTER_PAD] = "Gamepad", [FILTER_DIRECT] = "Direct", [FILTER_KBD2] = "KeyboardDrift"}
--set kbd initial rates (derive these from the menu options eventually)
local kbdInRate = 2.2
local kbdOutRate = 1.6
--set kbd understeer limiting effect (A value of 1 will achieve min steering speed of 0*kbdOutRate)
local kbdUndersteerMult = 0.7
--set kbd oversteer help effect (A value of 1 will achieve max steering speed of 2*kbdOutRate)
local kbdOversteerMult = 0.7
local rateMult = nil
local kbdOutRateMult = 0
local kbdInRateMult = 0
local padSmoother = nil
local kbdSmoother = nil
local vehicleSteeringWheelLock = 450
local handbrakeSoundEngaging = nil
local handbrakeSoundDisengaging = nil
local handbrakeSoundDisengaged = nil
local inputNameCache = {}
local gxSmoothMax = 0
local gx_Smoother = newTemporalSmoothing(4) -- it acts like a timer
local min, max, abs = math.min, math.max, math.abs
local function init()
--inRate (towards the center), outRate (away from the center), autoCenterRate, startingValue
M.state = {
steering = { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(),
smootherPAD = newTemporalSmoothing(),
minLimit = -1, maxLimit = 1 },
throttle = { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(3, 3, 1000, 0),
smootherPAD = newTemporalSmoothing(100, 100, nil, 0),
minLimit = 0, maxLimit = 1 },
brake = { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(3, 3, 1000, 0),
smootherPAD = newTemporalSmoothing(100, 100, nil, 0),
minLimit = 0, maxLimit = 1 },
parkingbrake = { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(10, 10, nil, 0),
smootherPAD = newTemporalSmoothing(10, 10, nil, 0),
minLimit = 0, maxLimit = 1 },
clutch = { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(10, 20, 20, 0),
smootherPAD = newTemporalSmoothing(10, 10, nil, 0),
minLimit = 0, maxLimit = 1 },
}
end
local function initSecondStage()
--scale rates based on steering wheel degrees
local foundSteeringHydro = false
if hydros then
for _, h in pairs(hydros.hydros) do
--check if it's a steering hydro
if h.inputSource == "steering_input" then
foundSteeringHydro = true
--if the value is present, scale the values
if h.steeringWheelLock then
vehicleSteeringWheelLock = abs(h.steeringWheelLock)
break
end
end
end
end
if v.data.input and v.data.input.steeringWheelLock ~= nil then
vehicleSteeringWheelLock = v.data.input.steeringWheelLock
elseif foundSteeringHydro then
if v.data.input == nil then v.data.input = {} end
v.data.input.steeringWheelLock = vehicleSteeringWheelLock
end
for wi,wd in pairs(wheels.wheels) do
if wd.parkingTorque and wd.parkingTorque > 0 then
handbrakeSoundEngaging = handbrakeSoundEngaging or sounds.createSoundscapeSound('handbrakeEngaging')
handbrakeSoundDisengaging = handbrakeSoundDisengaging or sounds.createSoundscapeSound('handbrakeDisengaging')
handbrakeSoundDisengaged = handbrakeSoundDisengaged or sounds.createSoundscapeSound('handbrakeDisengaged')
break
end
end
rateMult = 5 / 8
if vehicleSteeringWheelLock ~= 1 then
rateMult = 450 / vehicleSteeringWheelLock
end
kbdOutRateMult = min(kbdOutRate * rateMult, 2.68)
kbdInRateMult = min(kbdInRate * rateMult, 3.68)
padSmoother = newTemporalSmoothing()
kbdSmoother = newTemporalSmoothing()
M.reset()
end
local function dynamicInputRateKbd(v, dt, curx)
local signv = sign(v)
local signx = sign(curx)
local gx = sensors.gx
local signgx = sign(gx)
local absgx = abs(gx)
local gs = kbdSmoother:getWithRateUncapped(0, dt, 3)
if absgx > gs then
gs = absgx
kbdSmoother:set(gs)
end
-- centering by lifting key:
if v == 0 then
return kbdInRateMult
end
local g = abs(obj:getGravity())
--reduce steering speed only when steered into turn and pressing key into direction of turn (help limit the understeer)
if signx == -signgx and signv == -signgx then
kbdSmoother:set(0)
local gLateral = min(absgx, g) / (g + 1e-30)
return kbdOutRateMult - (kbdOutRateMult * kbdUndersteerMult * gLateral)
end
--increase steering speed when pressing key out of direction of turn (help save the car from oversteer)
if signv == signgx then
local gLateralSmooth = min(gs, g) / (g + 1e-30)
return kbdOutRateMult + (kbdOutRateMult * kbdOversteerMult * gLateralSmooth)
end
return kbdOutRateMult
end
local function dynamicInputRateKbd2(v, curx)
local signv = sign(v)
local signx = sign(curx)
local gx = sensors.gx
local signgx = sign(gx)
local mov = v-curx
local signmov = sign(mov)
local speed = electrics.values['wheelspeed']
-- centering by lifting key:
if v == 0 then return kbdInRateMult end
-- centering by pressing opposite key:
if signmov ~= signx then return kbdInRateMult * 1.5 end
-- recovering from oversteer:
if signv == signgx or signmov == signgx or signx == signgx then return kbdInRateMult * 1.8 end
-- not enough data, fallback case
if speed == nil then return kbdInRateMult end
-- regular steering:
speed = abs(speed)
local g = abs(obj:getGravity())
return kbdOutRateMult * (1.4 - min(speed / 12, 1) * min(gxSmoothMax, g) / (g + 1e-30)) / 1.4
end
local function dynamicInputRatePad(v, dt, curx)
local ps = padSmoother:getWithRateUncapped(0, dt, 0.2)
local diff = v - curx
local absdiff = abs(diff) * 0.9
if absdiff > ps then
ps = absdiff
padSmoother:set(ps)
end
local baserate = (min(absdiff * 1.7, 3) + ps + 0.35)
if diff * sign(curx) < 0 then
return min(baserate * 2, 5) * rateMult
else
return baserate * rateMult
end
end
local lockTypeWarned
local function updateGFX(dt)
gxSmoothMax = gx_Smoother:getUncapped(0, dt)
local absgx = abs(sensors.gx)
if absgx > gxSmoothMax then
gx_Smoother:set(absgx)
gxSmoothMax = absgx
end
-- map the values
for k, e in pairs(M.state) do
local ival = 0
if e.filter == FILTER_DIRECT then
e.angle = e.angle or 0
e.lockType = e.angle <= 0 and 0 or e.lockType or 0
if e.lockType == 0 then
-- 1:N relation in the whole range
ival = e.val
else
local vehicleAngle = vehicleSteeringWheelLock * 2 -- convert from jbeam scale (half range) to input scale (full range)
local relation = e.angle / vehicleAngle
if e.lockType == 2 then
-- 1:1 relation in the first half
ival = e.val * relation + fsign(e.val) * square(2*max(0.5,abs(e.val))-1) * max(0, 1 - relation) -- ival = linear + nonlinear
elseif e.lockType == 1 then
-- 1:1 relation in the whole range
ival = clamp(e.val * relation, -1, 1)
else
if not lockTypeWarned then
lockTypeWarned = true
log("E", "", "Unsupported steering lock type: "..dumps(e.lockType))
end
end
end
else
ival = min(max(e.val, -1), 1)
if e.filter == FILTER_PAD then -- joystick / game controller - smoothing without autocentering
if k == 'steering' then
ival = e.smootherPAD:getWithRate(ival, dt, dynamicInputRatePad(ival, dt, e.smootherPAD:value()))
else
ival = e.smootherPAD:get(ival, dt)
end
elseif e.filter == FILTER_KBD then
if k == 'steering' then
ival = e.smootherKBD:getWithRate(ival, dt, dynamicInputRateKbd(ival, dt, e.smootherKBD:value()))
else
ival = e.smootherKBD:get(ival, dt)
end
elseif e.filter == FILTER_KBD2 then
if k == 'steering' then
ival = e.smootherKBD:getWithRate(ival, dt, dynamicInputRateKbd2(ival, e.smootherKBD:value()))
else
ival = e.smootherKBD:get(ival, dt)
end
end
end
if k == "steering" then
local f = M.filterSettings[e.filter] -- speed-sensitive steering limit
if playerInfo.anyPlayerSeated and not ai.isDriving() then
if e.filter ~= M.lastFilterType then
obj:queueGameEngineLua(string.format('extensions.hook("startTracking", {Name = "ControlsUsed", Method = "%s"})', filterTypes[e.filter]))
M.lastFilterType = e.filter
end
end
ival = ival * min(1, max(f.limitMultiplier, f.limitM * electrics.values.airspeed + f.limitB ))
end
ival = min(max(ival, e.minLimit), e.maxLimit)
-- Custom Steering
if k == "steering" and e.filter == FILTER_PAD then
local upVec = obj:getDirectionVectorUp()
local dirVec = obj:getDirectionVector()
local worldVel = obj:getVelocity()
local angle1 = math.acos(dirVec.x);
local mult = 1
if dirVec.y < 0 then
mult = -1
end
local yaw = (angle1 * mult) + math.pi -- 0 to 2pi
--print(math.deg(yaw))
-- cross(dir, up)
local rightVecX = dirVec.y * upVec.z - dirVec.z * upVec.y;
local rightVecY = dirVec.z * upVec.x - dirVec.x * upVec.z;
local rightVecZ = dirVec.x * upVec.y - dirVec.y * upVec.x;
-- forward (pos: forward)
local px = worldVel.x * dirVec.x + worldVel.y * dirVec.y + worldVel.z * dirVec.z;
-- up (pos: downward)
local py = worldVel.x * upVec.x + worldVel.y * upVec.y + worldVel.z * upVec.z
-- right (pos: right)
local pz = worldVel.x * rightVecX + worldVel.y * rightVecY + worldVel.z * rightVecZ
-- print(string.format("%.2f", px) .. " " .. string.format("%.2f", pz)) -- " " .. string.format("%.2f", pz))
-- jesus balls this took too long to figure out.
if px > 3.0 then
local len = math.sqrt(px * px + py * py + pz * pz)
local nx = px / len
local ny = py / len
local nz = pz / len
if len == 0.0 then
nx = 0
ny = 0
nz = 0
end
-- print(string.format("%.2f", len))
local travelDir = math.atan2(nx, nz) - math.pi/2.0
--print(string.format("%.2f", travelDir))
if travelDir > math.pi/2.0 then
travelDir = travelDir - math.pi
end
if travelDir < -math.pi/2.0 then
travelDir = travelDir + math.pi
end
-- here should be some user pref stuff
-- should be adjustable
local minrad = math.rad(-30.0)
local maxrad = math.rad(30.0)
if travelDir > maxrad then
travelDir = maxrad
end
if travelDir < minrad then
travelDir = minrad
end
-- print(string.format("%.2f", math.deg(travelDir)))
-- here should be some reduction code
-- beamng specific: map radian to steering input proportional to max steering angle
--local vehicleAngle = vehicleSteeringWheelLock * 2
-- 1.0 corresponds to about 40 deg
ival = ival - travelDir * 1.25
--print(string.format("%.2f", ival))
end
-- ival = ival + math.atan()
end
if k == "parkingbrake" then
local prev = M[k] or e.minLimit
if handbrakeSoundEngaging and prev == e.minLimit and ival > prev then sounds.playSoundSkipAI(handbrakeSoundEngaging ) end
if handbrakeSoundDisengaging and prev == e.maxLimit and ival < prev then sounds.playSoundSkipAI(handbrakeSoundDisengaging) end
if handbrakeSoundDisengaged and ival == e.minLimit and ival < prev then sounds.playSoundSkipAI(handbrakeSoundDisengaged ) end
end
M[k] = ival
inputNameCache[k] = inputNameCache[k] or k..'_input'
electrics.values[inputNameCache[k]] = ival
end
end
local function reset()
gxSmoothMax = 0
gx_Smoother:reset()
for k, e in pairs(M.state) do
e.smootherKBD:reset()
e.smootherPAD:reset()
end
M:settingsChanged()
end
local function getDefaultState(itype)
return { val = 0, filter = 0,
smootherKBD = newTemporalSmoothing(10, 10, nil, 0),
smootherPAD = newTemporalSmoothing(10, 10, nil, 0),
minLimit = -1, maxLimit = 1 }
end
local function event(itype, ivalue, filter, angle, lockType)
if M.state[itype] == nil then -- probably a vehicle-specific input
log("W", "", "Creating vehicle-specific input event type '"..dumps(itype).."' using default values")
M.state[itype] = getDefaultState(itype)
end
M.state[itype].val = ivalue
M.state[itype].filter = filter
M.state[itype].angle = angle
M.state[itype].lockType = lockType
end
local function toggleEvent(itype)
if M.state[itype] == nil then return end
if M.state[itype].val > 0.5 then
M.state[itype].val = 0
else
M.state[itype].val = 1
end
M.state[itype].filter = 0
end
-- keyboard (multi-key) compatibility
local kbdSteerLeft = 0
local kbdSteerRight = 0
local function kbdSteer(isRight, val, filter)
if isRight then kbdSteerRight = val
else kbdSteerLeft = val end
event('steering', kbdSteerRight-kbdSteerLeft, filter)
end
-- gamepad( (mono-axis) compatibility
local function padAccelerateBrake(val, filter)
if val > 0 then
event('throttle', val, filter)
event('brake', 0, filter)
else
event('throttle', 0, filter)
event('brake', -val, filter)
end
end
local function settingsChanged()
M.filterSettings = {}
for i,v in ipairs({ FILTER_KBD, FILTER_PAD, FILTER_DIRECT, FILTER_KBD2 }) do
local f = {}
local limitEnabled = settings.getValue("inputFilter"..tostring(v).."_limitEnabled" , false)
if limitEnabled then
local startSpeed = clamp(settings.getValue("inputFilter"..tostring(v).."_limitStartSpeed"), 0, 100) -- 0..100 m/s
local endSpeed = clamp(settings.getValue("inputFilter"..tostring(v).."_limitEndSpeed" ), 0, 100) -- 0..100 m/s
f.limitMultiplier= clamp(settings.getValue("inputFilter"..tostring(v).."_limitMultiplier"), 0, 1) -- 0..1 multi
if startSpeed > endSpeed then
log("W", "", "Invalid speeds for speed sensitive filter #"..dumps(v)..", sanitizing by swapping: ["..dumps(startSpeed)..".."..dumps(endSpeed).."]")
startSpeed, endSpeed = endSpeed, startSpeed
end
f.limitM = (f.limitMultiplier - 1) / (endSpeed - startSpeed)
f.limitB = 1 - f.limitM * startSpeed
else
f.limitMultiplier = 1
f.limitM = 0
f.limitB = 1
end
M.filterSettings[v] = f
end
end
-- public interface
M.updateGFX = updateGFX
M.init = init
M.initSecondStage = initSecondStage
M.reset = reset
M.event = event
M.toggleEvent = toggleEvent
M.kbdSteer = kbdSteer
M.padAccelerateBrake = padAccelerateBrake
M.settingsChanged = settingsChanged
return M
@mrwallace888
Copy link

Lol, this affects AI as well so they can drift a bit properly (they said when an AI drives it mimics using a gamepad).
Also, how do I turn it into a keyboard input instead?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment