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 self.getShallInvertThrottle = TrackedVehicle.getShallInvertThrottle; 42 self.setDrivingDirectionInverted= VehicleManager:newFunction("setDrivingDirectionInverted"); 43 44 self.trackedWheels = {}; 45 self.trackedWheelCount = 0; 46 47 if dataTable.trackedWheels ~= nil then 48 local default = dataTable.trackedWheelDefaults; 49 for k, v in pairs(dataTable.trackedWheels) do 50 local wheelId = getChild(self.id, v.index or ""); 51 52 local wheel = {}; 53 wheel.id = Wheel.getWheelId(wheelId); 54 55 wheel.steeringValue = v.steeringValue or default.steeringValue or 0; 56 wheel.brakeScale = v.brakeValue or default.brakeValue or 0; 57 wheel.motorScale = v.motorValue or default.motorValue or 0; 58 59 wheel.steeringValue = v.steeringValue or default.steeringValue or 0; 60 wheel.brakeScale = v.brakeValue or default.brakeValue or 0; 61 wheel.motorScale = v.motorValue or default.motorValue or 0; 62 63 if v.visualIndex ~= nil then 64 wheel.visual = getChild(self.id, v.visualIndex or ""); 65 end; 66 67 table.insert(self.trackedWheels, wheel); 68 end; 69 end; 70 self.trackedWheelCount = #self.trackedWheels; 71 72 self.hydraulicActiveDirection = true; 73 self.hydraulicSpeed = 0; 74 self.hydraulicLiftGroomer = true; 75 self.hydraulicAcceleration = dataTable.hydraulicAcceleration or 2; 76 self.hydraulicDeceleration = dataTable.hydraulicDeceleration or 3; 77 self.hydraulicMaxDeceleration = dataTable.hydraulicMaxDeceleration or 4; 78 self.hydraulicIdleDeceleration = dataTable.hydraulicIdleDeceleration or 1; 79 self.hydraulicIdleDecActive = true; 80 self.hydraulicMaxSpeed = (dataTable.hydraulicMaxSpeed or 23.4) / 3.6; 81 82 self.isMotorOn = false; 83 self.currentDrivePower = 0; 84 self.currentSpeed = 0; 85 86 self.cumulatedDelta = 0; 87 self.maxCorrectingForce = dataTable.maxCorrectingForce or 4200; 88 -- control integration time is the time until control reacts with 1 kN of force if it is 1 m/s too slow. 89 -- note that 1 kN of total force is relevant, NOT per wheel (therefore the conversion to trackedWheelCount here) 90 self.controlIntegrationFactor = 1000 / (dataTable.controlIntegrationTime or 0.005) / self.trackedWheelCount; 91 92 -- to counteract wheel slipping (which somewhat can't be totally avoided), we're running slower when going downhill 93 self.downhillBrakeCoeff = dataTable.downhillBrakeCoeff or 0.55; 94 95 self.motorRpm = 0; 96 self.motorSmoothRpm = 0; 97 98 self.motorMinRpm = dataTable.motorMinRpm or 800; 99 self.motorMaxRpm = dataTable.motorMaxRpm or 2500; 100 101 if dataTable.speedToRpmCoeff ~= nil then 102 self.speedToRpmCoeff = dataTable.speedToRpmCoeff; 103 else 104 self.speedToRpmCoeff = (self.motorMaxRpm - self.motorMinRpm) / self.hydraulicMaxSpeed; 105 end; 106 107 -- determine how much motor rpm is reduced for every kW provided by the motor 108 self.powerToRpmCoeff = dataTable.powerToRpmCoeff or -1.5; 109 110 -- motor load power: determines how many kW power the motor has to provide to make it blend fully into load sound 111 self.motorMaxLoadPower = dataTable.motorMaxLoadPower or 150; 112 self.motorSmoothLoad = 0; 113 114 VehicleMotor.loadMotorLoadExhaust(self, dataTable); 115 116 -- disable default wheel scripts 117 self.customWheelScriptActive = true; 118 119 -- interface for warning sound 120 self.isReversing = false; 121 122 -- time the player has to press the direction button to invert the vehilcles driving direction 123 self.gearChangeTimePrev = nil; 124 125 VehicleMotor.loadMotorSounds(self, dataTable); 126 127 -- automatically apply the brake 128 for k, wheel in pairs(self.trackedWheels) do 129 Wheel.setTorques(wheel.id, 0, self.maxBrakeTorque); 130 end; 131 end;
TrackedVehicle:update(dt)
This code is performed each frame. This includes the audio simulation, but also the determination of the active target speed level.
135 function TrackedVehicle:update(dt) 136 if not self.isActive then 137 if not self.isMotorOn and self.hydraulicSpeed == 0 then 138 return; 139 end; 140 end; 141 142 -- update motor rpm 143 self.motorSmoothRpm = 0.6 * self.motorSmoothRpm + 0.4 * self.motorRpm; 144 145 if self:getIsLocalPlayerEntered() then 146 -- changes the vehicle's cameras pov, according to it's speed 147 if not self.driverSeat.isInterieurCamera then 148 VehicleCamera.activeCamera.accelerationFOVFactor = clamp01(math.abs(self.hydraulicSpeed / self.hydraulicMaxSpeed)); 149 end; 150 end; 151 152 if self.isMotorOn then 153 if g_isMaster then 154 -- add fuel usage 155 self:callSafe("addFuelUsage", dt, self.hydraulicSpeed * dt); 156 157 if self:getHasRunOutOfFuel() then 158 self:setIsMotorOn(false); 159 end; 160 end; 161 162 -- now update sounds 163 local motorLerpValue = (self.motorSmoothRpm - self.motorMinRpm) / (self.motorMaxRpm - self.motorMinRpm); 164 local motorLoad = clamp01(math.abs(self.currentDrivePower / self.motorMaxLoadPower)); -- [0;1] 165 self.motorSmoothLoad = 0.6 * self.motorSmoothLoad + 0.4 * motorLoad; 166 motorLoad = self.motorSmoothLoad; 167 local runFade = 1; 168 169 if self.motorSoundMinRunRpm ~= nil and self.motorSoundMaxRunRpm ~= nil then 170 runFade = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunRpm, self.motorSoundMaxRunRpm, self.motorSmoothRpm))); 171 end; 172 if self.motorSoundMinRunSpeed ~= nil and self.motorSoundMaxRunSpeed ~= nil then 173 runFade = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunSpeed, self.motorSoundMaxRunSpeed, math.abs(self.currentSpeed)))); 174 end; 175 176 VehicleMotor.updateMotorLoadExhaust(self, motorLoad); 177 178 local isInterieurCamera = false; 179 if g_isMultiplayer then 180 isInterieurCamera = g_networkGame.localPlayer.isInterieurCamera; 181 else 182 isInterieurCamera = g_scenario.player.isInterieurCamera; 183 end; 184 185 VehicleMotor.updateMotorSound(self, self.motorSoundIdle, dt, isInterieurCamera, self.motorSmoothRpm, motorLerpValue); 186 VehicleMotor.updateMotorSound(self, self.motorSoundLoad, dt, isInterieurCamera, self.motorSmoothRpm, motorLerpValue, motorLoad, runFade); 187 VehicleMotor.updateMotorSound(self, self.motorSoundRun, dt, isInterieurCamera, self.motorSmoothRpm, motorLerpValue, (1-motorLoad), runFade); 188 end; 189 190 local isForward = self.hydraulicActiveDirection; 191 192 -- input for snow groomer 193 self.hydraulicLiftGroomer = not isForward or self.hydraulicSpeed < 0; 194 195 if not self.isActive or not self.isMotorOn or self.parkingBrake > 0 or (not isForward and self.hydraulicSpeed > 0) or (isForward and self.hydraulicSpeed < 0) then 196 -- (almost) immediately stop 197 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.hydraulicMaxDeceleration * dt); 198 199 elseif self.brakeValue > 0 then 200 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.brakeValue * self.hydraulicDeceleration * dt); 201 self.hydraulicIdleDecActive = false; 202 203 elseif self.throttleValue > 0 then 204 self.hydraulicSpeed = self.hydraulicSpeed + (isForward and 1 or -1) * self.throttleValue * self.hydraulicAcceleration * dt; 205 self.hydraulicIdleDecActive = false; 206 207 elseif self.hydraulicIdleDecActive or not self.cruiseControlActive then 208 -- throttle down gently 209 self.hydraulicSpeed = Utils.moveTowards(self.hydraulicSpeed, 0, self.hydraulicIdleDeceleration * dt); 210 end; 211 212 self.isReversing = not isForward; 213 214 if self:getIsInputActive() then 215 if not self:getHasRunOutOfFuel() then 216 if not self.isMotorOn then 217 -- start motor again 218 g_GUI:addKeyHint(InputMapper.Vehicle_StartMotor, l10n.get("Input_Vehicle_StartMotor")); 219 end; 220 221 if InputMapper:getKeyDown(InputMapper.Vehicle_StartMotor) then 222 self:setIsMotorOn(not self.isMotorOn); 223 end; 224 end; 225 end; 226 227 if self:getIsGUIActive() then 228 -- !!TODO!! replace 30 by some configurable value 229 self.vehicleHUD:showSpeed(self.currentSpeed, self.currentSpeed / 30, math.max(self.motorSmoothRpm, 900) / self.motorMaxRpm); 230 self.vehicleHUD:setIsMotorOn(self.isMotorOn); 231 self.vehicleHUD:setGear(self.hydraulicActiveDirection and "V" or "R"); 232 end; 233 234 -- clamp the speed 235 self.hydraulicSpeed = clamp(self.hydraulicSpeed, -self.hydraulicMaxSpeed, self.hydraulicMaxSpeed); 236 237 -- update wheel visuals 238 for k, wheel in pairs(self.trackedWheels) do 239 if wheel.visual ~= nil then 240 Wheel.applyPose(wheel.id, wheel.visual); 241 end; 242 end; 243 end;
TrackedVehicle:setHydraulicDriveDirection(dir)
Sets the direction of the hydraulic drive as specified by dir
(bool, true = forward).
247 function TrackedVehicle:setHydraulicDriveDirection(dir) 248 self.hydraulicActiveDirection = dir; 249 end; 250
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 using a PI-controller.
255 function TrackedVehicle:fixedUpdate(dt) 256 if g_isClient then 257 return; 258 end; 259 260 -- calculate current speed 261 local currentSpeed = TrackedVehicle.getCurrentSpeed(self); 262 self.currentSpeed = 0.95 * self.currentSpeed + 0.05 * (currentSpeed * 3.6); 263 264 -- determine vehicle inclination y (< 0: downhill, > 0: uphill) 265 local a, y, b = Utils.transformDirection(self.mainId, 0, 0, 1); 266 267 local hydraulicSpeed = self.hydraulicSpeed; 268 269 if y < 0 and self.downhillBrakeCoeff > 0 then 270 hydraulicSpeed = hydraulicSpeed * (1 - self.downhillBrakeCoeff*math.abs(y)); 271 end; 272 273 -- update wheels (replacement of default function) 274 -- this is a closed control loop approach (P/P+I-control) 275 -- 1) determine control deviation 276 local controlDeviation = hydraulicSpeed - currentSpeed; 277 278 -- 2) determine cumulated control deviation (I-control) 279 self.cumulatedDelta = clamp(self.cumulatedDelta + controlDeviation * dt * self.controlIntegrationFactor, -10000, 10000); 280 281 -- 3) determine correcting force 282 local correctingForce = clamp(self.cumulatedDelta, -self.maxCorrectingForce, self.maxCorrectingForce); 283 284 -- 4) choose whether correcting force is a brake force or a drive force 285 local motorForce, brakeForce = 0, 0; 286 if math.abs(hydraulicSpeed) < 1e-2 or (correctingForce < 0 and currentSpeed > 0) or (correctingForce > 0 and currentSpeed < 0) then 287 -- correcting force is a brake force 288 brakeForce = math.abs(correctingForce); 289 else 290 motorForce = correctingForce; 291 end; 292 293 self.currentDrivePower = 0.95 * self.currentDrivePower + 0.05 * motorForce * self.trackedWheelCount * currentSpeed * 0.001; 294 295 -- 5) apply to wheels (also including steering angle) 296 local steerAngle = self:getCurrentSteerAngle(); 297 298 -- let's apply these values 299 for k, wheel in pairs(self.trackedWheels) do 300 -- note: motor force is divided by speed - for an I control, this does not matter 301 -- nice effect: this avoids skidding wheels even though all wheels are controlled at once! 302 303 Wheel.setSteerAngle(wheel.id, steerAngle * wheel.steeringValue); 304 Wheel.setForces(wheel.id, motorForce / math.max(math.abs(Wheel.getSpeed(wheel.id)), 1), brakeForce); 305 end; 306 307 -- 6) calculate new motor rpm 308 self.motorRpm = self.motorMinRpm + self.speedToRpmCoeff * math.abs(self.hydraulicSpeed) + self.powerToRpmCoeff * math.abs(self.currentDrivePower); 309 end;
TrackedVehicle:getCurrentSpeed()
Determines and returns the current speed in m/s.
313 function TrackedVehicle:getCurrentSpeed() 314 local speedSum, count = 0, 0; 315 316 for k, wheel in pairs(self.trackedWheels) do 317 speedSum = speedSum + Wheel.getSpeed(wheel.id); 318 count = count + 1; 319 end; 320 321 -- [m/s] 322 return count == 0 and 0 or (speedSum/count); 323 end;
TrackedVehicle:applyWheelStiffness(fw, sw)
Applies the stiffness values fw
(forward stiffness) and sw
(sidewards stiffness) to the vehicle's wheels (both float).
327 function TrackedVehicle:applyWheelStiffness(fw, sw) 328 fw = fw or 1; 329 sw = sw or fw; 330 331 for k, wheel in pairs(self.trackedWheels) do 332 Wheel.setStiffness(wheel.id, fw, sw); 333 end; 334 end;
TrackedVehicle:onEnter(player, isLocalPlayer)
Starts the motor unless disabled in GameplaySettings.
338 function TrackedVehicle:onEnter(player, isLocalPlayer) 339 if isLocalPlayer and GameplaySettings.autoStartMotor and not self:getHasRunOutOfFuel() then 340 self:setIsMotorOn(true); 341 end; 342 end;
TrackedVehicle:onLeave(player, isLocalPlayer)
Stops the motor upon leaving.
346 function TrackedVehicle:onLeave(player, isLocalPlayer) 347 if g_isMaster then 348 self.hydraulicIdleDecActive = true; 349 end; 350 351 if GameplaySettings.autoStopMotor then 352 self:setIsMotorOn(false); 353 end; 354 end;
TrackedVehicle:setIsMotorOn(isOn, noEvent)
Sets whether the motor is turned on or not, depending on isOn
(bool). This is forwarded to VehicleMotor to reuse code.
358 function TrackedVehicle:setIsMotorOn(isOn, noEvent) 359 VehicleMotor.setIsMotorOn(self, isOn, noEvent); 360 end;
TrackedVehicle:getShallInvertThrottle()
Returns 'true' if the vehicle is going backwards.
364 function TrackedVehicle:getShallInvertThrottle() 365 return not self.hydraulicActiveDirection; 366 end;
TrackedVehicle:setDrivingDirectionInverted()
Inverts the current driving direction.
370 function TrackedVehicle:setDrivingDirectionInverted() 371 self:setHydraulicDriveDirection(not self.hydraulicActiveDirection); 372 end;
TrackedVehicle:writeUpdate(isLocalPlayerEntered)
Sends a network packet each frame when the vehicle is updated. The data sent by writeUpdate
will be received by readUpdate
.
376 function TrackedVehicle:writeUpdate(isLocalPlayerEntered) 377 if streamWriteBool(self:getIsActive()) then 378 if g_isServer then 379 streamWriteFloat(self.motorRpm); 380 streamWriteFloat(self.motorSmoothRpm); 381 streamWriteFloat(self.currentSpeed); 382 streamWriteFloat(self.hydraulicSpeed); 383 streamWriteFloat(self.currentDrivePower); 384 streamWriteBool(self.hydraulicLiftGroomer); 385 386 elseif isLocalPlayerEntered then 387 streamWriteBool(self.hydraulicActiveDirection); 388 streamWriteBool(self.hydraulicLiftGroomer); 389 end; 390 end; 391 end;
TrackedVehicle:readUpdate(connection, networkTime, isClientEntered)
Receives a network packet each frame when the vehicle is updated. The data sent by writeUpdate
will be received by readUpdate
.
395 function TrackedVehicle:readUpdate(connection, networkTime, isClientEntered) 396 if streamReadBool() then 397 if g_isClient then 398 self.motorRpm = streamReadFloat(); 399 self.motorSmoothRpm = streamReadFloat(); 400 self.currentSpeed = streamReadFloat(); 401 self.hydraulicSpeed = streamReadFloat(); 402 self.currentDrivePower = streamReadFloat(); 403 self.hydraulicLiftGroomer = streamReadBool(); 404 405 elseif isClientEntered then 406 self.hydraulicActiveDirection = streamReadBool(); 407 self.hydraulicLiftGroomer = streamReadBool(); 408 end; 409 end; 410 end;
Copyright
All contents of this page may be used for modding use for Winter Resort Simulator - Season 2 only. Any use exceeding this regulation is not permitted.
Copyright (C) HR Innoways, 2021. All Rights Reserved.