Action denied

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;

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.