Implementing better steering

This commit is contained in:
Ivan Grachyov 2021-12-11 17:36:34 +05:00
parent 7cdb222580
commit 0b71940310
14 changed files with 294 additions and 231 deletions

View File

@ -0,0 +1,42 @@
-- @name Better steering sample config
-- @author Koptilnya1337
-- @server
-- @include /koptilnya/better_steering/steering.txt
require('/koptilnya/better_steering/steering.txt')
Steering:new({
Sensitivity = 0.04,
Slaves = {{
Lock = 30,
Camber = -2,
Caster = 5,
Ackermann = 15.43,
Toe = 0,
Offset = 180
}, {
Lock = 30,
Camber = -2,
Caster = 5,
Ackermann = 15.43,
Toe = 0,
Offset = 180,
IsRight = true
}, {
Lock = 1,
Camber = -2,
Caster = 0,
Ackermann = 0,
Toe = 0,
Offset = 180,
Direction = -1
}, {
Lock = 1,
Camber = -2,
Caster = 0,
Ackermann = 0,
Toe = 0,
Offset = 180,
IsRight = true,
Direction = -1
}}
})

View File

@ -0,0 +1,114 @@
-- @include /koptilnya/libs/wire_component.txt
-- @include /koptilnya/libs/constants.txt
require('/koptilnya/libs/wire_component.txt')
require('/koptilnya/libs/constants.txt')
Slave = class('Slave', WireComponent)
function Slave:initialize(options)
options = options or {}
self.lock = options.Lock or 0
self.camber = options.Camber or 0
self.caster = options.Caster or 0
self.ackermann = options.Ackermann or 0
self.toe = options.Toe or 0
self.offset = options.Offset or 0
self.isRight = options.IsRight or false
self.direction = options.Direction or 1
self.order = options.Order or 0
self.entity = nil
self.wheel = nil
self.base = options.Base or nil
self.steer = 0
self.steeringAngle = 0
self.lateralVel = 0
self.lateralForce = 0
self.selfAligningTorque = 0
self.correctingAngle = 0
end
function Slave:getInputs()
local inputs = {}
inputs['SL_' .. self.order] = 'entity'
inputs['W_' .. self.order] = 'entity'
return inputs
end
function Slave:getOutputs()
local outputs = {}
outputs['SL_' .. self.order .. '_LateralForce'] = 'number'
outputs['SL_' .. self.order .. '_SelfAligningTq'] = 'number'
outputs['SL_' .. self.order .. '_CorrectingAngle'] = 'number'
outputs['W_' .. self.order .. '_LateralVel'] = 'number'
return outputs
end
function Slave:updateOutputs()
wire.ports['SL_' .. self.order .. '_LateralForce'] = self.lateralForce
wire.ports['SL_' .. self.order .. '_SelfAligningTq'] = self.selfAligningTorque
wire.ports['SL_' .. self.order .. '_CorrectingAngle'] = self.correctingAngle
wire.ports['W_' .. self.order .. '_LateralVel'] = self.lateralVel
end
function Slave:update()
self.entity = wire.ports['SL_' .. self.order]
self.wheel = wire.ports['W_' .. self.order]
if isValid(self.entity) and isValid(self.base) and isValid(self.wheel) then
local wheelMass = self.wheel:getMass()
local wheelInertia = self.wheel:getInertia():getLength()
local wheelRadius = self.wheel:getModelRadius() / UNITS_PER_METER
local wheelFriction = self.wheel:getFriction()
local lateralVel = self.wheel:worldToLocal(self.wheel:getVelocity() + self.wheel:getPos())[2]
self.lateralVel = lateralVel
if math.abs(self.steer) > 0 then
self.correctingAngle = 0
else
self.lateralForce = wheelFriction * wheelMass * lateralVel / TICK_INTERVAL /
UNITS_PER_METER
self.selfAligningTorque = self.lateralForce * wheelRadius * 2
self.correctingAngle = self.selfAligningTorque / wheelInertia * TICK_INTERVAL
end
if self.isRight then
self.correctingAngle = self.correctingAngle * -1
end
self.correctingAngle = 0
if not self.entity:isFrozen() and not self.entity:isPlayerHolding() then
self.entity:setFrozen(1)
end
local inc = self.steer * self.lock * self.direction
self.steeringAngle = math.clamp(self.steeringAngle + inc + self.correctingAngle, -self.lock, self.lock)
local camber = self.isRight and -self.camber or self.camber
local steerRatio = self.lock ~= 0 and self.steeringAngle / self.lock or 0
local caster = steerRatio * self.caster
local toe = self.isRight and self.toe or -self.toe
local ackermann = 0
if steerRatio > 0 then
ackermann = self.isRight and -steerRatio * self.ackermann or steerRatio * self.ackermann
elseif steerRatio < 0 then
ackermann = self.isRight and steerRatio * self.ackermann or -steerRatio * self.ackermann
end
-- Dividing ackermann by 2 because ackermann angle is difference between wheels angle
-- So if we don't divide it by 2, the result angle will be 2 times more than expected
local angle = Angle(0, self.offset + self.steeringAngle + toe + ackermann / 2, camber - caster)
local localizedAngle = self.base:localToWorldAngles(angle)
self.entity:setAngles(localizedAngle)
end
end

View File

@ -0,0 +1,79 @@
-- @include /koptilnya/libs/wire_component.txt
-- @include ./slave.txt
-- @include /koptilnya/libs/table.txt
require('/koptilnya/libs/wire_component.txt')
require('./slave.txt')
require('/koptilnya/libs/table.txt')
Steering = class('Steering', WireComponent)
function Steering:initialize(options)
options = options or {}
self.slaves = {}
self.steer = 0
self.sensitivity = options.Sensitivity or 1
for key, slave in ipairs(options.Slaves) do
slave.Order = key
slave.Base = wire.ports.Base
table.insert(self.slaves, Slave:new(slave))
end
self:_adjustPorts()
hook.add('tick', 'update', function()
self:update()
end)
end
function Steering:_adjustPorts()
local inputs = {}
local outputs = {}
inputs = table.merge(inputs, self:getInputs())
outputs = table.merge(outputs, self:getOutputs())
for _, slave in ipairs(self.slaves) do
inputs = table.merge(inputs, slave:getInputs())
outputs = table.merge(outputs, slave:getOutputs())
end
wire.adjustPorts(inputs, outputs)
end
function Steering:getInputs()
return {
TargetSteer = 'number',
Base = 'entity'
}
end
function Steering:getOutputs()
return {
Steer = 'number'
}
end
function Steering:updateOutputs()
local totalSteeringAngle = table.reduce(self.slaves, function(steer, slave)
return steer + slave.direction * slave.steeringAngle / (slave.lock ~= 0 and slave.lock or 1)
end, 0)
local steerableSlaves = table.filter(self.slaves, function(slave)
return slave.lock ~= 0
end)
wire.ports.Steer = totalSteeringAngle / #steerableSlaves
end
function Steering:update()
self.steer = wire.ports.TargetSteer * self.sensitivity
self:updateOutputs()
for _, slave in ipairs(self.slaves) do
slave.steer = self.steer
slave:update()
slave:updateOutputs()
end
end

View File

@ -0,0 +1,45 @@
-- @shared
-- @name BMW M4 G82 Competition
-- @author Koptilnya1337
-- @include /koptilnya/mesh_loader/builder.txt
require("/koptilnya/mesh_loader/builder.txt")
DEBUG_MODE = true
local LINK = "https://drive.google.com/u/0/uc?id=1yxjPtVhoMROwSB56tfL_dbBPrW1Yjr9h&export=download"
local SCALE = Vector(1)
local Materials =
{
Body = "phoenix_storms/fender_white",
Carbon = "sprops/textures/sprops_cfiber2"
}
local Colors =
{
Body = Color(115,218,201),
Carbon = Color(255,255,255)
}
local builder = {}
if SERVER then
builder = MeshBuilder:new(LINK)
local this = chip()
-- Body group
builder:build("Body", Vector(0), Angle(0), SCALE, Colors.Body, Materials.Body, this, this)
builder:build("LeftDoor", Vector(0), Angle(0), SCALE, Colors.Body, Materials.Body, this, this)
builder:build("RightDoor", Vector(0), Angle(0), SCALE, Colors.Body, Materials.Body, this, this)
builder:build("Bonnet", Vector(0), Angle(0), SCALE, Colors.Body, Materials.Body, this, this)
builder:build("Trunk", Vector(0), Angle(0), SCALE, Colors.Body, Materials.Body, this, this)
builder:build("Roof", Vector(0), Angle(0), SCALE, Colors.Carbon, Materials.Carbon, this, this)
local result = builder:getResult()
else
PERMA.onPermissionsGained = function()
builder = MeshBuilder:new(LINK)
end
PERMA.build()
end

View File

@ -1,8 +1,8 @@
-- @include ./wire_component.txt -- @include /koptilnya/libs/wire_component.txt
-- @include ./enums/gearbox.txt -- @include ./enums/gearbox.txt
-- @include /koptilnya/libs/constants.txt -- @include /koptilnya/libs/constants.txt
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('./wire_component.txt') require('/koptilnya/libs/wire_component.txt')
local gearboxTypes = require('./enums/gearbox.txt') local gearboxTypes = require('./enums/gearbox.txt')
Clutch = class('Clutch', WireComponent) Clutch = class('Clutch', WireComponent)

View File

@ -1,8 +1,8 @@
-- @server -- @server
-- @include /koptilnya/libs/constants.txt -- @include /koptilnya/libs/constants.txt
-- @include ./wire_component.txt -- @include /koptilnya/libs/wire_component.txt
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('./wire_component.txt') require('/koptilnya/libs/wire_component.txt')
Differential = class('Differential', WireComponent) Differential = class('Differential', WireComponent)

View File

@ -1,6 +1,6 @@
-- @include ./wire_component.txt -- @include /koptilnya/libs/wire_component.txt
-- @include /koptilnya/libs/constants.txt -- @include /koptilnya/libs/constants.txt
require('./wire_component.txt') require('/koptilnya/libs/wire_component.txt')
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
Engine = class('Engine', WireComponent) Engine = class('Engine', WireComponent)

View File

@ -1,5 +1,5 @@
-- @include ../wire_component.txt -- @include /koptilnya/libs/wire_component.txt
require('../wire_component.txt') require('/koptilnya/libs/wire_component.txt')
Gearbox = class('Gearbox', WireComponent) Gearbox = class('Gearbox', WireComponent)

View File

@ -1,8 +1,8 @@
-- @server -- @server
-- @include /koptilnya/libs/constants.txt -- @include /koptilnya/libs/constants.txt
-- @include ./wire_component.txt -- @include /koptilnya/libs/wire_component.txt
require('/koptilnya/libs/constants.txt') require('/koptilnya/libs/constants.txt')
require('./wire_component.txt') require('/koptilnya/libs/wire_component.txt')
Differential = class('Differential', WireComponent) Differential = class('Differential', WireComponent)

View File

@ -1,188 +0,0 @@
-- @name Grip Steering
-- @author DarkSupah
-- @server
-- @include ./slave.txt
Steering = class('Steering')
require('./slave.txt')
function inrange(val, min, max)
return val >= min and val <= max
end
function isNaN(val)
return val ~= val
end
function vectorIsNaN(vec)
return isNaN(vec[1]) or isNaN(vec[2]) or isNaN(vec[3])
end
function getLocalVelocity(ent)
return ent:worldToLocal((ent:getVelocity() + ent:getPos()))
end
local options = {
Camber = -2,
Caster = 5,
Ackermann = 15,
Lock = 45,
ZeroThreshold = 2,
StartSpeedThreshold = 40,
EndSpeedThreshold = 150,
SpeedCoeff = 0.2,
StartCorrectionThreshold = 5,
EndCorrectionThreshold = 40,
CorrectionOnCoeff = 0.6,
CorrectionOffCoeff = 1,
CorrectionOnLerp = 0.3,
CorrectionOffLerp = 0.6
}
function Steering:_adjustPorts()
local inputs = {
Axles = 'table',
Base = 'entity',
LeftSlave = 'entity',
RightSlave = 'entity'
}
local outputs = {
SteerAngle = 'number',
TargetAngle = 'number',
CorrectionAngle = 'number'
}
wire.adjustPorts(inputs, outputs)
end
function Steering:_createSlaves()
local entities = {wire.ports.LeftSlave, wire.ports.RightSlave}
local slaves = {}
for k, v in pairs(entities) do
local slave = Slave:new({
Entity = v,
IsLeft = k % 2 == 1,
Offset = 180,
Base = wire.ports.Base,
Camber = self.slavesConfig.Camber,
Caster = self.slavesConfig.Caster,
Ackermann = self.slavesConfig.Ackermann
})
table.insert(self.slaves, slave)
end
end
function Steering:_updateOutputs()
wire.ports.TargetAngle = self.targetAngle
wire.ports.SteerAngle = self.angle
wire.ports.CorrectionAngle = self.correction
end
function Steering:_getCorrectionGradient(speed)
return math.clamp(math.remap(speed, self.startCorrectionThreshold, self.endCorrectionThreshold, 0, 1), 0, 1)
end
function Steering:getBaseSpeed()
local MPH = self.base:getVelocity():getLength() * 3600 / 63360
local KPH = MPH * 1.609
return KPH
end
function Steering:_getSpeedCorrectionGradient(speed)
return math.clamp(math.remap(speed, self.startSpeedThreshold, self.endSpeedThreshold, 0, 1), 0, 1)
end
function Steering:_getCorrectionAngle()
local localVel = getLocalVelocity(self.base)
local forwardVel = (localVel * Vector(1, 0, 0)):getNormalized()
local planarVec = (localVel * Vector(1, 1, 0)):getNormalized()
forwardVel = vectorIsNaN(forwardVel) and Vector(0, 0, 0) or forwardVel
planarVec = vectorIsNaN(planarVec) and Vector(0, 0, 0) or planarVec
local cross = forwardVel:cross(planarVec)
return math.deg(math.acos(forwardVel:dot(planarVec))) * math.sign(localVel[2])
end
function Steering:initialize(options)
self:_adjustPorts()
options = options or {}
self.base = wire.ports.Base
self.slavesConfig = {
Camber = options.Camber,
Caster = options.Caster,
Ackermann = options.Ackermann
}
self.lock = options.Lock
self.targetAngle = 0
self.angle = 0
self.zeroThreshold = options.ZeroThreshold
self.speedCoeff = options.SpeedCoeff
self.startSpeedThreshold = options.StartSpeedThreshold
self.endSpeedThreshold = options.EndSpeedThreshold
self.startCorrectionThreshold = options.StartCorrectionThreshold
self.endCorrectionThreshold = options.EndCorrectionThreshold
self.correctionOnCoeff = options.CorrectionOnCoeff
self.correctionOffCoeff = options.CorrectionOffCoeff
self.correctionOnLerp = options.CorrectionOnLerp
self.correctionOffLerp = options.CorrectionOffLerp
self.correction = 0
self.slaves = {}
self:_createSlaves()
end
function Steering:update()
local horizontal = wire.ports.Axles.Horizontal.Value
local horizontalTarget = wire.ports.Axles.Horizontal.Target
local baseSpeedKMH = self:getBaseSpeed()
local angleCorrection = self:_getCorrectionGradient(baseSpeedKMH)
local speedCorrection = self.angle * self:_getSpeedCorrectionGradient(baseSpeedKMH) * self.speedCoeff
self.targetAngle = horizontal * self.lock
local correctionCoeff = horizontalTarget ~= 0 and self.correctionOnCoeff or self.correctionOffCoeff
local correctionLerpCoeff = horizontalTarget ~= 0 and self.correctionOnLerp or self.correctionOffLerp
local correction = self:_getCorrectionAngle() * angleCorrection * correctionCoeff
self.correction = math.lerp(correctionLerpCoeff, self.correction, isNaN(correction) and 0 or correction)
local possibleAngle = math.clamp(horizontal * self.lock + speedCorrection + self.correction, -self.lock, self.lock)
if inrange(possibleAngle, -self.zeroThreshold, self.zeroThreshold) then
self.angle = 0
else
self.angle = possibleAngle
end
for k, v in pairs(self.slaves) do
v:rotate(self.angle)
end
self:_updateOutputs()
end
local steering = Steering:new(options)
hook.add('think', 'SteeringUpdate', function()
steering:update()
end)

View File

@ -1,29 +0,0 @@
Slave = class('Slave')
function Slave:initialize(options)
options = options or {}
self.entity = options.Entity
self.base = options.Base
self.camber = options.Camber
self.caster = options.Caster
self.ackermann = options.Ackermann
self.isLeft = options.IsLeft
self.offset = options.Offset
end
function Slave:rotate(ang)
if self.entity:isFrozen() == false then
self.entity:setFrozen(1)
end
local ackermann = math.sin(math.rad(ang)) ^ 2 * self.ackermann * (self.isLeft and -1 or 1)
local yaw = self.offset + ang + ackermann
local roll = (self.isLeft and 1 or -1) * self.camber - ang / 270 * self.caster
local angle = Angle(0, yaw, roll)
local transformedAngle = self.base:localToWorldAngles(angle)
self.entity:setAngles(transformedAngle)
end

View File

@ -2,8 +2,8 @@ Axles = {
Horizontal = { Horizontal = {
Negative = KEY.D, Negative = KEY.D,
Positive = KEY.A, Positive = KEY.A,
Gravity = 0.1, Gravity = 1,
Sensitivity = 0.2 Sensitivity = 1
}, },
Throttle = { Throttle = {
Positive = KEY.W, Positive = KEY.W,
@ -20,5 +20,5 @@ Axles = {
Negative = MOUSE.RIGHT, Negative = MOUSE.RIGHT,
Gravity = 1, Gravity = 1,
Sensitivity = 1 Sensitivity = 1
}, }
} }