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;

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.