Last active
June 15, 2022 15:27
-
-
Save ikt32/207027cc29f1869a43f6ccef054e3845 to your computer and use it in GitHub Desktop.
BeamNG steering assist (Last updated with 0.23.5)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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?