meta data for this page
Vehicles/VehicleScripts/TrackedVehicle.lua
TrackedVehicle is the main vehicle script for snowcats and any other vehicles that are driven by a diesel-hydraulic engine. In this regard, it is the opposite of VehicleMotor (which implements a diesel engine with a gearbox). Therefore, please make sure that you never activate both scripts on the same vehicle.
While VehicleMotor simulates the torque curve of a diesel motor and calculates the driving torque, we need a different approach for tracked vehicles. We chose to optimise tracked vehicles for a very continuous operation at rather stable speed levels, as this is the preferred use-case for snowcats. Hydraulic drives are able to apply very high forces to the PistenBully's crawler chain.
TrackedVehicle therefore uses a closed control loop to adjust each wheel's motor torque as close to the target speed as possible. The control behaviour can be influenced using the controlIntegrationFactor
.
29 30 TrackedVehicle = TrackedVehicle or {};
TrackedVehicle:load(dataTable)
Loads all data from the vehicle's configuration.
In contrast to road vehicles (which are using axles
and are powered by VehicleMotor
), this type of vehicle allows to configure individual wheels.
Each wheel will be controlled separately, and each wheel can have different brake/motor/steering scales.
37 function TrackedVehicle:load(dataTable) 38 self.setHydraulicDriveDirection = VehicleManager:newFunction("setHydraulicDriveDirection"); 39 self.applyWheelStiffness = VehicleManager:newFunction("applyWheelStiffness"); 40 self.setIsMotorOn = VehicleManager:newFunction("setIsMotorOn"); 41 42 self.trackedWheels = {}; 43 self.trackedWheelCount = 0; 44 45 if dataTable.trackedWheels ~= nil then 46 local default = dataTable.trackedWheelDefaults; 47 for k, v in pairs(dataTable.trackedWheels) do 48 local wheelId = getChild(self.id, v.index or ""); 49 50 local wheel = {}; 51 wheel.id = Wheel.getWheelId(wheelId); 52 53 wheel.steeringValue = v.steeringValue or default.steeringValue or 0; 54 wheel.brakeScale = v.brakeValue or default.brakeValue or 0; 55 wheel.motorScale = v.motorValue or default.motorValue or 0; 56 57 wheel.steeringValue = v.steeringValue or default.steeringValue or 0; 58 wheel.brakeScale = v.brakeValue or default.brakeValue or 0; 59 wheel.motorScale = v.motorValue or default.motorValue or 0; 60 61 if v.visualIndex ~= nil then 62 wheel.visual = getChild(self.id, v.visualIndex or ""); 63 end; 64 65 table.insert(self.trackedWheels, wheel); 66 end; 67 end; 68 self.trackedWheelCount = #self.trackedWheels; 69 70 self.hydraulicActiveDirection = true; 71 self.hydraulicSpeed = 0; 72 self.hydraulicLiftGroomer = true; 73 self.hydraulicAcceleration = dataTable.hydraulicAcceleration or 1; 74 self.hydraulicDeceleration = dataTable.hydraulicDeceleration or 2; 75 self.hydraulicMaxDeceleration = dataTable.hydraulicMaxDeceleration or 4; 76 self.hydraulicIdleDeceleration = dataTable.hydraulicIdleDeceleration or 1; 77 self.hydraulicIdleDecActive = true; 78 self.hydraulicMaxSpeed = (dataTable.hydraulicMaxSpeed or 23.4) / 3.6; 79 80 self.isMotorOn = false; 81 self.currentDrivePower = 0; 82 self.currentSpeed = 0; 83 84 self.cumulatedDelta = 0; 85 self.maxCorrectingForce = dataTable.maxCorrectingForce or 4200; 86 -- control integration time is the time until control reacts with 1 kN of force if it is 1 m/s too slow. 87 -- note that 1 kN of total force is relevant, NOT per wheel (therefore the conversion to trackedWheelCount here) 88 self.controlIntegrationFactor = 1000 / (dataTable.controlIntegrationTime or 0.005) / self.trackedWheelCount; 89 90 -- to counteract wheel slipping (which somewhat can't be totally avoided), we're running slower when going downhill 91 self.downhillBrakeCoeff = dataTable.downhillBrakeCoeff or 0.55; 92 93 self.motorRpm = 0; 94 self.motorSmoothRpm = 0; 95 96 self.motorMinRpm = dataTable.motorMinRpm or 800; 97 self.motorMaxRpm = dataTable.motorMaxRpm or 2500; 98 99 if dataTable.speedToRpmCoeff ~= nil then 100 self.speedToRpmCoeff = dataTable.speedToRpmCoeff; 101 else 102 self.speedToRpmCoeff = (self.motorMaxRpm - self.motorMinRpm) / self.hydraulicMaxSpeed; 103 end; 104 105 -- determine how much motor rpm is reduced for every kW provided by the motor 106 self.powerToRpmCoeff = dataTable.powerToRpmCoeff or -1.5; 107 108 -- motor load power: determines how many kW power the motor has to provide to make it blend fully into load sound 109 self.motorMaxLoadPower = dataTable.motorMaxLoadPower or 150; 110 self.motorSmoothLoad = 0; 111 112 VehicleMotor.loadMotorLoadExhaust(self, dataTable); 113 114 -- disable default wheel scripts 115 self.customWheelScriptActive = true; 116 117 -- interface for warning sound 118 self.isReversing = false; 119 120 VehicleMotor.loadMotorSounds(self, dataTable); 121 122 -- automatically apply the brake 123 for k, wheel in pairs(self.trackedWheels) do 124 Wheel.setTorques(wheel.id, 0, self.maxBrakeTorque); 125 end; 126 end;
TrackedVehicle:update(dt)
This code that is performed each frame. This includes the audio simulation, but also the determination of the active target speed level.
130 function TrackedVehicle:update(dt) 131 if not self.isActive then return end; 132 133 -- update motor rpm 134 self.motorSmoothRpm = 0.6 * self.motorSmoothRpm + 0.4 * self.motorRpm; 135 136 if self:getIsInputActive() then 137 if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_InverseDirection) then 138 self:setHydraulicDriveDirection(not self.hydraulicActiveDirection); 139 end; 140 end; 141 142 if self.isMotorOn then 143 -- add fuel usage 144 self:callSafe("addFuelUsage", dt, self.hydraulicSpeed * dt); 145 146 if self:getHasRunOutOfFuel() then 147 self:setIsMotorOn(false); 148 end; 149 150 -- now update sounds 151 local motorLerpValue = (self.motorSmoothRpm - self.motorMinRpm) / (self.motorMaxRpm - self.motorMinRpm); 152 local motorLoad = clamp01(math.abs(self.currentDrivePower / self.motorMaxLoadPower)); -- [0;1] 153 self.motorSmoothLoad = 0.6 * self.motorSmoothLoad + 0.4 * motorLoad; 154 motorLoad = self.motorSmoothLoad; 155 local runFade = 1; 156 157 if self.motorSoundMinRunRpm ~= nil and self.motorSoundMaxRunRpm ~= nil then 158 runFade = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunRpm, self.motorSoundMaxRunRpm, self.motorSmoothRpm))); 159 end; 160 if self.motorSoundMinRunSpeed ~= nil and self.motorSoundMaxRunSpeed ~= nil then 161 runFade = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunSpeed, self.motorSoundMaxRunSpeed, math.abs(self.currentSpeed)))); 162 end; 163 164 VehicleMotor.updateMotorLoadExhaust(self, motorLoad); 165 166 VehicleMotor.updateMotorSound(self, self.motorSoundIdle, dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue); 167 VehicleMotor.updateMotorSound(self, self.motorSoundLoad, dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue, motorLoad, runFade); 168 VehicleMotor.updateMotorSound(self, self.motorSoundRun, dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue, (1-motorLoad), runFade); 169 end; 170 171 local isForward = self.hydraulicActiveDirection; 172 local cruiseControlActive = false; 173 174 -- input for snow groomer 175 self.hydraulicLiftGroomer = not isForward or self.hydraulicSpeed < 0; 176 177 if not self.isMotorOn or self.parkingBrake > 0 or (not isForward and self.hydraulicSpeed > 0) or (isForward and self.hydraulicSpeed < 0) then 178 -- (almost) immediately stop 179 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.hydraulicMaxDeceleration * dt); 180 181 elseif self.brakeValue > 0 then 182 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.brakeValue * self.hydraulicDeceleration * dt); 183 self.hydraulicIdleDecActive = true; 184 185 elseif self.throttleValue > 0 then 186 self.hydraulicSpeed = self.hydraulicSpeed + (isForward and 1 or -1) * self.throttleValue * self.hydraulicAcceleration * dt; 187 self.hydraulicIdleDecActive = false; 188 cruiseControlActive = self.hydraulicSpeed >= self.hydraulicMaxSpeed and GameplaySettings.snowcatCruiseControl; 189 190 elseif self.hydraulicIdleDecActive or not GameplaySettings.snowcatCruiseControl then 191 -- throttle down gently 192 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.hydraulicIdleDeceleration * dt); 193 194 elseif math.abs(self.hydraulicSpeed) > 0.1 then 195 cruiseControlActive = true; 196 end; 197 198 self.isReversing = not isForward; 199 200 if self:getIsInputActive() then 201 if not self:getHasRunOutOfFuel() then 202 if not self.isMotorOn then 203 -- start motor again 204 g_GUI:addKeyHint(InputMapper.Vehicle_StartMotor, l10n.get("Input_Vehicle_StartMotor")); 205 end; 206 207 if InputMapper:getKeyDown(InputMapper.Vehicle_StartMotor) then 208 self:setIsMotorOn(not self.isMotorOn); 209 end; 210 end; 211 end; 212 213 if self:getIsGUIActive() then 214 local absSpeed = math.abs(self.currentSpeed); 215 UI.setImageFillAmount(getChild(GUI.vehicleHUD, "SpeedBar/Filled"), 0.4 * clamp01(absSpeed / 30)); 216 UI.setImageFillAmount(getChild(GUI.vehicleHUD, "RpmBar/Filled"), 0.4 * clamp01(self.motorSmoothRpm / self.motorMaxRpm)); 217 218 219 if math.abs(self.currentSpeed) < 10 then 220 UI.setLabelText(getChild(GUI.vehicleHUD, "Speed"), string.format("%.1f", math.abs(self.currentSpeed))); 221 else 222 UI.setLabelText(getChild(GUI.vehicleHUD, "Speed"), string.format("%.0f", math.abs(self.currentSpeed))); 223 end; 224 225 UI.setLabelText(getChild(GUI.vehicleHUD, "Gear"), tostring(self.hydraulicActiveDirection and "V" or "R")); 226 227 setActive(getChild(GUI.vehicleHUD, "Icons2/CruiseControl/dark"), not cruiseControlActive); 228 setActive(getChild(GUI.vehicleHUD, "Icons2/CruiseControl/bright"), cruiseControlActive); 229 setActive(getChild(GUI.vehicleHUD, "Icons2/Battery/dark"), self.isMotorOn); 230 setActive(getChild(GUI.vehicleHUD, "Icons2/Battery/bright"), not self.isMotorOn); 231 end; 232 233 -- clamp the speed 234 self.hydraulicSpeed = clamp(self.hydraulicSpeed, -self.hydraulicMaxSpeed, self.hydraulicMaxSpeed); 235 236 -- update wheel visuals 237 for k, wheel in pairs(self.trackedWheels) do 238 if wheel.visual ~= nil then 239 Wheel.applyPose(wheel.id, wheel.visual); 240 end; 241 end; 242 end;
TrackedVehicle:setHydraulicDriveDirection(dir)
Sets the direction of the hydraulic drive as specified by dir
(bool, true = forward).
246 function TrackedVehicle:setHydraulicDriveDirection(dir) 247 self.hydraulicActiveDirection = dir; 248 end; 249
TrackedVehicle:fixedUpdate(dt)
fixedUpdate is called once per physics step, i.e. can be called multiple times per frame. It performs the actual task of controlling wheel speeds.
254 function TrackedVehicle:fixedUpdate(dt) 255 -- calculate current speed 256 local currentSpeed = TrackedVehicle.getCurrentSpeed(self); 257 self.currentSpeed = 0.95 * self.currentSpeed + 0.05 * (currentSpeed * 3.6); 258 259 -- determine vehicle inclination y (< 0: downhill, > 0: uphill) 260 local a, y, b = Utils.transformDirection(self.mainId, 0, 0, 1); 261 262 local hydraulicSpeed = self.hydraulicSpeed; 263 264 if y < 0 and self.downhillBrakeCoeff > 0 then 265 hydraulicSpeed = hydraulicSpeed * (1 - self.downhillBrakeCoeff*math.abs(y)); 266 end; 267 268 -- update wheels (replacement of default function) 269 -- this is a closed control loop approach (P/P+I-control) 270 -- 1) determine control deviation 271 local controlDeviation = hydraulicSpeed - currentSpeed; 272 273 -- 2) determine cumulated control deviation (I-control) 274 self.cumulatedDelta = clamp(self.cumulatedDelta + controlDeviation * dt * self.controlIntegrationFactor, -10000, 10000); 275 276 -- 3) determine correcting force 277 local correctingForce = clamp(self.cumulatedDelta, -self.maxCorrectingForce, self.maxCorrectingForce); 278 279 -- 4) choose whether correcting force is a brake force or a drive force 280 local motorForce, brakeForce = 0, 0; 281 if math.abs(hydraulicSpeed) < 1e-2 or (correctingForce < 0 and currentSpeed > 0) or (correctingForce > 0 and currentSpeed < 0) then 282 -- correcting force is a brake force 283 brakeForce = math.abs(correctingForce); 284 else 285 motorForce = correctingForce; 286 end; 287 288 self.currentDrivePower = 0.95 * self.currentDrivePower + 0.05 * motorForce * self.trackedWheelCount * currentSpeed * 0.001; 289 290 -- 5) apply to wheels (also including steering angle) 291 local steerAngle = self:getCurrentSteerAngle(); 292 293 -- let's apply these values 294 for k, wheel in pairs(self.trackedWheels) do 295 -- note: motor force is divided by speed - for an I control, this does not matter 296 -- nice effect: this avoids skidding wheels even though all wheels are controlled at once! 297 298 Wheel.setSteerAngle(wheel.id, steerAngle * wheel.steeringValue); 299 Wheel.setForces(wheel.id, motorForce / math.max(math.abs(Wheel.getSpeed(wheel.id)), 1), brakeForce); 300 end; 301 302 -- 6) calculate new motor rpm 303 self.motorRpm = self.motorMinRpm + self.speedToRpmCoeff * math.abs(self.hydraulicSpeed) + self.powerToRpmCoeff * math.abs(self.currentDrivePower); 304 end;
TrackedVehicle:getCurrentSpeed()
Determines and returns the current speed in m/s.
308 function TrackedVehicle:getCurrentSpeed() 309 local speedSum, count = 0, 0; 310 311 for k, wheel in pairs(self.trackedWheels) do 312 speedSum = speedSum + Wheel.getSpeed(wheel.id); 313 count = count + 1; 314 end; 315 316 -- [m/s] 317 return count == 0 and 0 or (speedSum/count); 318 end;
TrackedVehicle:applyWheelStiffness(fw, sw)
Applies the stiffness values fw
(forward stiffness) and sw
(sidewards stiffness) to the vehicle's wheels (both float).
322 function TrackedVehicle:applyWheelStiffness(fw, sw) 323 fw = fw or 1; 324 sw = sw or fw; 325 326 for k, wheel in pairs(self.trackedWheels) do 327 Wheel.setStiffness(wheel.id, fw, sw); 328 end; 329 end;
TrackedVehicle:onEnter()
Start the motor unless disabled in GameplaySettings.
333 function TrackedVehicle:onEnter() 334 if GameplaySettings.autoStartMotor and not self:getHasRunOutOfFuel() then 335 self:setIsMotorOn(true); 336 end; 337 end;
TrackedVehicle:onLeave()
Stop the motor upon leaving.
341 function TrackedVehicle:onLeave() 342 self.hydraulicIdleDecActive = true; 343 344 if GameplaySettings.autoStopMotor then 345 self:setIsMotorOn(false); 346 end; 347 348 -- automatically apply the brake upon leaving (because wheels will no longer be updated) 349 for k, wheel in pairs(self.trackedWheels) do 350 Wheel.setTorques(wheel.id, 0, self.maxBrakeTorque); 351 end; 352 end;
TrackedVehicle:setIsMotorOn(isOn)
Sets whether the motor is turned on or not, depending on isOn
(bool).
356 function TrackedVehicle:setIsMotorOn(isOn) 357 VehicleMotor.setIsMotorOn(self, isOn); 358 end;
Copyright
All contents of this page may be used for modding use for Winter Resort Simulator only. Any use exceeding this regulation is not permitted.
Copyright (C) HR Innoways, 2020. All Rights Reserved.