Vehicles/VehicleScripts/VehicleMotor.lua

VehicleMotor implements a diesel engine together with a gearbox that powers the vehicle. This is the best option for all vehicles that are powered by any powertrain comparable to this setup, except if a very continuous speed level shall be achieved (in that case, head over to TrackedVehicle).

  25  VehicleMotor                        = VehicleMotor or {};

VehicleMotor:load(dataTable)

Creates all functions required for VehicleMotor. As many of them require return values, only some can be created using VehicleManager:newFunction().

Afterwards, the function loads all relevant configuration parameters from the vehicle's data table.

  31  function VehicleMotor:load(dataTable)
  32      self.getMotorTorqueAtRpm        = VehicleMotor.getMotorTorqueAtRpm;
  33      self.getDrivingWheelsRpm        = VehicleMotor.getDrivingWheelsRpm;
  34      self.getCurrentMotorTorque      = VehicleMotor.getCurrentMotorTorque;
  35      self.getGearTransmissionRatio   = VehicleMotor.getGearTransmissionRatio;
  36      self.getMotorInertia            = VehicleMotor.getMotorInertia;
  37      self.getDrivingWheelsCount      = VehicleMotor.getDrivingWheelsCount;
  38      self.updateWheels               = VehicleMotor.updateWheels;
  39      self.setAutomaticDriveActive    = VehicleMotor.setAutomaticDriveActive;
  40      self.setIsMotorOn               = VehicleManager:newFunction("setIsMotorOn");
  41      
  42      self.motorMinRpm                = dataTable.motorMinRpm or 800;
  43      self.motorMaxRpm                = dataTable.motorMaxRpm or 5000;
  44      self.motorInertiaMoment         = dataTable.motorInertiaMoment or 0.5;
  45      
  46      local speedPointerIndex         = dataTable.speedPointerIndex or "";
  47      self.speedPointerWheelRadius    = dataTable.wheelRadius or 0.5;
  48      self.speedPointerMaxSpeed       = dataTable.maxSpeed or 100;
  49      if speedPointerIndex ~= "" then
  50          self.speedPointerId         = getChild(self.id, speedPointerIndex);
  51          self.speedPointerMaxAngle   = dataTable.speedPointerMaxAngle or 90;
  52          self.speedPointerLastAngle  = 0;
  53      end;
  54      
  55      local rpmPointerIndex           = dataTable.rpmPointerIndex or "";
  56      self.rpmPointerMaxRpm           = dataTable.maxRpm or 5000;
  57      if rpmPointerIndex ~= "" then
  58          self.rpmPointerId           = getChild(self.id, rpmPointerIndex);
  59          self.rpmPointerMaxAngle     = dataTable.rpmPointerMaxAngle or 90;
  60          self.rpmPointerLastAngle    = 0;
  61      end;
  62      
  63      self.currentGear                = 2;
  64      
  65      self.gears                      = dataTable.gears       or {};
  66      self.gearLabels                 = dataTable.gearLabels  or {};
  67  
  68      if type(self.gears) ~= "table" then
  69          print("Error while loading vehicle " .. tostring(self.templateName) .. "'s gears. Make sure you've set the key 'gears' within you data table to a table value containing each gear's transmission ratio.");
  70          return;
  71      end;
  72      if type(self.gearLabels) ~= "table" then
  73          print("Error while loading vehicle " .. tostring(self.templateName) .. "'s gear labels. Make sure you've set the key 'gearLabels' within you data table to a table value containing each gear's label as string value.");
  74      end;
  75      
  76      self.gearCount                  = #self.gears;
  77      self.motorFinalDriveRatio       = dataTable.motorFinalDriveRatio or 1;
  78      for i, v in pairs(self.gears) do
  79          if type(self.gearLabels[i]) ~= "string" then
  80              self.gearLabels[i]      = (i==1 and "R" or tostring(i-1));
  81          end;
  82      end;
  83      
  84      self.automaticDrive             = getNoNil(dataTable.automaticDrive,    true);
  85      self.automaticDriveActive       = self.automaticDrive;
  86      self.upperLimitRpm              = dataTable.automaticUpperLimitRpm or 2700;
  87      self.lowerLimitRpm              = dataTable.automaticLowerLimitRpm or 1600;
  88      self.gearSwitchPace             = dataTable.automaticGearSwitchPace or 0.3;
  89      self.gearSwitchTimer            = 100;
  90      
  91      self.torqueCurve                = dataTable.motorTorqueCurve;
  92      -- !!TODO!! check if torque curve is valid
  93      
  94      self.motorRpm                   = 0;
  95      self.motorSmoothRpm             = 0; -- smooth rpm used for digital rpm-meter
  96      self.motorLastRpm               = 0;
  97      self.wheelRpm                   = 0;
  98      self.currentSpeed               = 0;
  99      self.isMotorOn                  = false;
 100      
 101      VehicleMotor.loadMotorLoadExhaust(self, dataTable);
 102  
 103      self.customWheelScriptActive    = true;
 104      self.isReversing                = false;
 105  
 106      VehicleMotor.loadMotorSounds(self, dataTable);
 107  
 108      -- finally apply the brake to our axles
 109      for n, axle in pairs(self.axles) do
 110          Wheel.setTorques(axle.leftWheel, 0, self.maxBrakeTorque);
 111          Wheel.setTorques(axle.rightWheel, 0, self.maxBrakeTorque);
 112      end;
 113  end;

VehicleMotor.loadMotorLoadExhaust(self, dataTable)

 115  function VehicleMotor.loadMotorLoadExhaust(self, dataTable)
 116      -- exhaust load scale
 117      self.exhaustLoadScale           = dataTable.exhaustLoadScale or 2; -- three times as much exhaust in maximum load sound weight
 118  
 119      if self.exhaustLoadScale > 0 then
 120          for k, v in pairs(self.exhaustParticleSystems) do
 121              v.emissionRate          = ParticleSystem.getEmissionRateOverTime(v.id);
 122          end;
 123      end;
 124  end;

VehicleMotor.updateMotorLoadExhaust(self, motorLoad)

Updates the exhaust particle spawn rate depending on motor load. Use the factor emissionRate to have some black smoke particles if you press the accelerator.

 128  function VehicleMotor.updateMotorLoadExhaust(self, motorLoad)
 129      if self.exhaustLoadScale <= 0 then
 130          return;
 131      end;
 132  
 133      motorLoad                       = motorLoad + 1;
 134  
 135      for k, v in pairs(self.exhaustParticleSystems) do
 136          ParticleSystem.setEmissionRateOverTime(v.id, motorLoad * v.emissionRate);
 137      end;
 138  end;

VehicleMotor.loadMotorSounds(self, dataTable)

Loads all motor sounds used for VehicleMotor. This makes use of the Vehicle:loadSingleSound() function (see Vehicle).

 142  function VehicleMotor.loadMotorSounds(self, dataTable)
 143      
 144      if dataTable.sounds ~= nil then
 145          self.motorSoundStart        = self:loadSingleSound(dataTable.sounds.motorStart);
 146          self.motorSoundStop         = self:loadSingleSound(dataTable.sounds.motorStop);
 147  
 148          if self.motorSoundStart ~= nil then
 149              self.motorSoundStartLength      = AudioSource.getLength(self.motorSoundStart);
 150          end;
 151  
 152          self.motorSoundIdle         = VehicleMotor.loadMotorSound(self, dataTable.sounds.motorIdle);
 153          self.motorSoundLoad         = VehicleMotor.loadMotorSound(self, dataTable.sounds.motorLoad);
 154          self.motorSoundRun          = VehicleMotor.loadMotorSound(self, dataTable.sounds.motorRun);
 155  
 156          if dataTable.sounds.motorConfiguration ~= nil then
 157              local data                  = dataTable.sounds.motorConfiguration;
 158              self.motorSoundMinRunRpm    = data.minRunFadeRpm;
 159              self.motorSoundMaxRunRpm    = data.maxRunFadeRpm;
 160              self.motorSoundMinRunSpeed  = data.minRunFadeSpeed;
 161              self.motorSoundMaxRunSpeed  = data.maxRunFadeSpeed;
 162          end;
 163      end;
 164  end;

VehicleMotor.loadMotorSound(self, data)

Loads a motor sound. Note that one motor sound can consist of different samples.

 167  function VehicleMotor.loadMotorSound(self, data)
 168      if data == nil then
 169          return nil;
 170      end;
 171  
 172      local sound             = {};
 173  
 174      if type(data) == "table" then
 175          if data[1] ~= nil then
 176              for k, v in pairs(data) do
 177                  table.insert(sound, VehicleMotor.rawLoadMotorSound(self, v));
 178              end;
 179          else
 180              table.insert(sound, VehicleMotor.rawLoadMotorSound(self, data));
 181          end;
 182      end;
 183  
 184      return sound;
 185  end;

VehicleMotor.rawLoadMotorSound(self, data)

Loads a single motor sound sample. Each sample can have different volume and pitch settings, as well as many others. If you are in doubt about how the parameters exactly work, please have a look at the code implementation below.

  • Use baseRpm along with baseRpmScale to control the pitch based on rpm OR
  • Use pitchOffset and pitchScale to control the pitch based on custom pitch/rpm settings. In any case, trial-and-error provide best results.
  • volumeScale is multiplied with rpm to determine the volume.
  • fadeInSpeed allows you to avoid awkward jumps in volume.
  • minFadeInRpm and maxFadeInRpm specify whether and how fast the sound shall be faded in depending on rpm. At any rpm below minFadeInRpm, the sound will be inactive.
  • minFadeOutRpm and maxFadeOutRpm specify whether and how fast the sound shall be faded in depending on rpm. At any rpm above maxFadeOutRpm, the sound will be inactive.
  • interieurVolumeScale allows to influence the sound volume in interior perspective.
  • interiorLowpass and exterieurLowpass specify low-pass amounts depending on perspective. Cutoff frequency will be 2500 Hz by default.
 197  function VehicleMotor.rawLoadMotorSound(self, data)
 198      local sound             = {};
 199  
 200      sound.sampleId          = self:loadSingleSound(data, true);
 201  
 202      sound.minPitch          = data.minPitch or 0.5;
 203      sound.maxPitch          = data.maxPitch or 2;
 204      sound.minVolume         = data.minVolume or 0.2;
 205      sound.maxVolume         = data.maxVolume or 1.5;
 206  
 207      -- load additional data for our motor sound
 208      if data.baseRpm ~= nil then
 209          sound.baseRpm       = data.baseRpm;
 210          sound.baseRpmScale  = data.baseRpmScale or 1;
 211      else
 212          sound.pitchOffset   = data.pitchOffset or 1;
 213          sound.pitchScale    = data.pitchScale or (sound.maxPitch - sound.minPitch) / (self.motorMaxRpm - self.motorMinRpm); -- 10% more pitch for 100 more rpm
 214      end;
 215  
 216      if data.volumeScale ~= nil then
 217          sound.volumeScale   = data.volumeScale;
 218      end;
 219  
 220      if data.fadeInSpeed ~= nil then
 221          sound.fadeInSpeed   = data.fadeInSpeed;
 222          sound.lastVolume    = 0;
 223      end;
 224      if data.fadeOutSpeed ~= nil then
 225          sound.fadeOutSpeed  = data.fadeOutSpeed;
 226          sound.lastVolume    = 0;
 227      end;
 228      if data.minFadeInRpm ~= nil then
 229          sound.minFadeInRpm  = data.minFadeInRpm;
 230          sound.maxFadeInRpm  = data.maxFadeInRpm or (sound.minFadeInRpm + 300);
 231      end;
 232      if data.minFadeOutRpm ~= nil then
 233          sound.minFadeOutRpm = data.minFadeOutRpm;
 234          sound.maxFadeOutRpm = data.maxFadeOutRpm or (sound.minFadeOutRpm + 300);
 235      end;
 236  
 237      if data.interieurVolumeScale ~= nil then
 238          sound.interieurVolumeScale  = data.interieurVolumeScale;
 239      end;
 240      if data.interieurLowpass ~= nil then
 241          sound.interieurLowpass      = data.interieurLowpass;
 242      end;
 243      if data.exterieurLowpass ~= nil then
 244          sound.exterieurLowpass      = data.exterieurLowpass;
 245      end;
 246  
 247      return sound;
 248  end;

VehicleMotor.foreachMotorSound(self, sound, call, ...)

Calls the function call (function) on each sound sample in sound (table) while passing over all arguments in .

 251  function VehicleMotor.foreachMotorSound(self, sound, call, ...)
 252      if sound == nil or call == nil then return end;
 253      
 254      for k, v in pairs(sound) do
 255          call(v.sampleId, ...);
 256      end;
 257  end;

VehicleMotor:customLoadAxle(axle, v)

This hooks into Vehicle:loadAxles() (see Vehicle for further details) to add further data to the axle configuration, which will be required later on in this script.

 261  function VehicleMotor:customLoadAxle(axle, v)
 262      -- read additional data from configuration
 263      axle.radius                     = v.radius  or 0.5 * (Wheel.getRadius(axle.leftWheel)   + Wheel.getRadius(axle.rightWheel));
 264      axle.wheelMass                  = v.mass    or 0.5 * (Wheel.getMass(axle.leftWheel)     + Wheel.getMass(axle.rightWheel));
 265      axle.tractionControl            = getNoNil(v.tractionControl, true);
 266      axle.useABS                     = getNoNil(v.useABS, true);
 267  end;

VehicleMotor:getMotorTorqueAtRpm(rpm)

Returns the motor torque at the rpm level rpm (float) in Nm.

 271  function VehicleMotor:getMotorTorqueAtRpm(rpm)
 272      if #self.torqueCurve < 2 then
 273          return 0;
 274      end;
 275      
 276      if rpm <= self.torqueCurve[1].rpm then
 277          return self.torqueCurve[1].torque;
 278          
 279      elseif rpm >= self.torqueCurve[#self.torqueCurve].rpm then
 280          return self.torqueCurve[#self.torqueCurve].torque;
 281      end;
 282      
 283      for i=1, #self.torqueCurve-1, 1 do
 284      
 285          local rpm1, rpm2            = self.torqueCurve[i].rpm, self.torqueCurve[i+1].rpm;
 286          
 287          if rpm >= rpm1 and rpm <= rpm2 then
 288              -- rpm is in this range
 289              local torque1, torque2  = self.torqueCurve[i].torque, self.torqueCurve[i+1].torque;
 290              
 291              return torque1 + (torque2 - torque1) * (rpm - rpm1) / (rpm2 - rpm1);
 292          end;
 293      end;
 294      
 295      -- actually we should never arrive down here
 296      return 0;
 297  end;

VehicleMotor:update(dt)

Updates the current rpm, the gearbox and some controls. This will also refresh GUI values. Finally, it also updates motor sound.

 300  function VehicleMotor:update(dt)
 301      if not self.isActive then return end;
 302      
 303      self.currentSpeed               = 0.9 * self.currentSpeed + 0.1 * (self.wheelRpm / 30 * self.speedPointerWheelRadius * math.pi * 3.6);
 304      self.motorSmoothRpm             = 0.6 * self.motorSmoothRpm + 0.4 * self.motorRpm;
 305  
 306      -- add fuel usage
 307      if self.isMotorOn then
 308          self:callSafe("addFuelUsage", dt, self.currentSpeed * dt);
 309  
 310          if self:getHasRunOutOfFuel() then
 311              self:setIsMotorOn(false);
 312          end;
 313      end;
 314      
 315      
 316      -- update gear
 317      local automaticActive           = self.automaticDrive and self.automaticDriveActive;
 318      if automaticActive then
 319          self.gearSwitchTimer        = self.gearSwitchTimer + dt;
 320          
 321          if self.gearSwitchTimer > 0 and self.currentGear ~= 1 then
 322              if self.motorSmoothRpm > self.upperLimitRpm then
 323                  -- switch one gear upwards
 324                  self:setGear(self.currentGear + 1);
 325                  
 326              elseif self.currentGear > 2 and self.motorSmoothRpm < self.lowerLimitRpm then
 327                  -- switch one gear upwards
 328                  self:setGear(self.currentGear - 1);
 329              end;
 330          end;
 331      end;
 332      
 333      -- make it possible to override input
 334      if self:getIsInputActive() then
 335          -- input help
 336  
 337          if not self:getHasRunOutOfFuel() then
 338              if not self.isMotorOn then
 339                  -- start motor again
 340                  g_GUI:addKeyHint(InputMapper.Vehicle_StartMotor,    l10n.get("Input_Vehicle_StartMotor"));
 341              end;
 342  
 343              if InputMapper:getKeyDown(InputMapper.Vehicle_StartMotor) then
 344                  self:setIsMotorOn(not self.isMotorOn);
 345              end;
 346          end;
 347  
 348          if self.currentGear == 1 then
 349              if automaticActive then
 350                  -- show the message only for automatic gear
 351                  g_GUI:addKeyHint(InputMapper.VehicleSnowcat_InverseDirection,   l10n.get("Input_Vehicle_GearUp_Forward"));
 352              end;
 353              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_InverseDirection) then
 354                  self:setGear(self.currentGear + 1);
 355              end;
 356  
 357          elseif self.currentGear == 2 then
 358              if automaticActive then
 359                  g_GUI:addKeyHint(InputMapper.VehicleSnowcat_InverseDirection,   l10n.get("Input_Vehicle_GearDown_Reverse"));
 360              end;
 361              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_InverseDirection) then
 362                  self:setGear(self.currentGear - 1);
 363              end;
 364          end;
 365  
 366          if not automaticActive then
 367              if self.currentGear > 1 then
 368                  g_GUI:addKeyHint(InputMapper.Vehicle_GearDown,          l10n.get("Input_Vehicle_GearDown"));
 369              end;
 370              if self.currentGear < self.gearCount then
 371                  g_GUI:addKeyHint(InputMapper.Vehicle_GearUp,            l10n.get("Input_Vehicle_GearUp"));
 372              end;
 373          end;
 374  
 375          if self.automaticDrive then
 376              if automaticActive then
 377                  g_GUI:addKeyHint(InputMapper.Vehicle_AutomaticDrive,    l10n.get("Input_Vehicle_AutomaticDrive1"));
 378  
 379              else
 380                  g_GUI:addKeyHint(InputMapper.Vehicle_AutomaticDrive,    l10n.get("Input_Vehicle_AutomaticDrive0"));
 381              end;
 382              
 383              if InputMapper:getKeyDown(InputMapper.Vehicle_AutomaticDrive) then
 384                  self:setAutomaticDriveActive(not self.automaticDriveActive)
 385              end;
 386          end;
 387  
 388          if InputMapper:getKeyDown(InputMapper.Vehicle_GearDown) then
 389              self:setGear(self.currentGear - 1);
 390              
 391          elseif InputMapper:getKeyDown(InputMapper.Vehicle_GearUp) then
 392              self:setGear(self.currentGear + 1);
 393          end;
 394      end;
 395      
 396      if self:getIsActive() then
 397          -- rpm pointer
 398          if self.rpmPointerId ~= nil then
 399              local newAngle              = self.rpmPointerMaxAngle * clamp01(self.motorRpm / self.rpmPointerMaxRpm);
 400              self.rpmPointerLastAngle    = 0.9 * self.rpmPointerLastAngle + 0.1 * newAngle;
 401              setRotationZ(self.rpmPointerId, self.rpmPointerLastAngle);
 402          end;
 403          if self.speedPointerId ~= nil then
 404              local newAngle              = self.speedPointerMaxAngle * clamp01(math.abs(self.currentSpeed / self.speedPointerMaxSpeed));
 405              self.speedPointerLastAngle  = 0.95 * self.speedPointerLastAngle + 0.05 * newAngle;
 406              setRotationZ(self.speedPointerId, self.speedPointerLastAngle);
 407          end;
 408  
 409          -- update reverse beep
 410          self.isReversing            = self:getGearTransmissionRatio() < 0;
 411      end;
 412  
 413      if self:getIsGUIActive() then
 414          local absSpeed              = math.abs(self.currentSpeed);
 415          UI.setImageFillAmount(getChild(GUI.vehicleHUD, "SpeedBar/Filled"),      0.4 * clamp01(absSpeed                              / self.speedPointerMaxSpeed));
 416          UI.setImageFillAmount(getChild(GUI.vehicleHUD, "RpmBar/Filled"),        0.4 * clamp01(math.max(self.motorSmoothRpm, 900)    / self.rpmPointerMaxRpm));
 417          
 418          
 419          if math.abs(self.currentSpeed) < 10 then
 420              UI.setLabelText(getChild(GUI.vehicleHUD, "Speed"),                  string.format("%.1f", math.abs(self.currentSpeed)));
 421          else
 422              UI.setLabelText(getChild(GUI.vehicleHUD, "Speed"),                  string.format("%.0f", math.abs(self.currentSpeed)));
 423          end;
 424          
 425          UI.setLabelText(getChild(GUI.vehicleHUD, "Gear"),                       tostring(self.gearLabels[self.currentGear]));
 426          setActive(getChild(GUI.vehicleHUD, "Icons2/Battery/dark"),              self.isMotorOn);
 427          setActive(getChild(GUI.vehicleHUD, "Icons2/Battery/bright"),            not self.isMotorOn);
 428      end;
 429      
 430      if self.isMotorOn then
 431          -- update sounds
 432          local motorLerpValue        = (self.motorSmoothRpm - self.motorMinRpm) / (self.motorMaxRpm - self.motorMinRpm);
 433          local motorLoad             = self.throttleValue; -- [0;1]
 434          local runFade               = 1;
 435  
 436          if self.motorSoundMinRunRpm ~= nil and self.motorSoundMaxRunRpm ~= nil then
 437              runFade                 = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunRpm, self.motorSoundMaxRunRpm, self.motorSmoothRpm)));
 438          end;
 439          if self.motorSoundMinRunSpeed ~= nil and self.motorSoundMaxRunSpeed ~= nil then
 440              runFade                 = math.min(runFade, clamp01(inverseLerp(self.motorSoundMinRunSpeed, self.motorSoundMaxRunSpeed, math.abs(self.currentSpeed))));
 441          end;
 442  
 443          VehicleMotor.updateMotorLoadExhaust(self, motorLoad);
 444          
 445          VehicleMotor.updateMotorSound(self, self.motorSoundIdle,    dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue);
 446          VehicleMotor.updateMotorSound(self, self.motorSoundLoad,    dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue,        motorLoad,      runFade);
 447          VehicleMotor.updateMotorSound(self, self.motorSoundRun,     dt, self.isInterieurCamera, self.motorSmoothRpm, motorLerpValue,        (1-motorLoad),  runFade);
 448      end;
 449  end;

VehicleMotor.updateMotorSound(self, sound, ...)

Calls VehicleMotor.rawUpdateMotorSound on each sample in this group of motor sounds.

 453  function VehicleMotor.updateMotorSound(self, sound, ...)
 454      if sound == nil then return end;
 455  
 456      for k, v in pairs(sound) do
 457          VehicleMotor.rawUpdateMotorSound(self, v, ...);
 458      end;
 459  end;

VehicleMotor.rawUpdateMotorSound(self, sound, dt, isInterieur, motorRpm, motorLerpValue, blendValue, blendScale)

This actually updates a motor sound, while making use of the parameters explained in VehicleMotor.rawLoadMotorSound (above on this page).

 462  function VehicleMotor.rawUpdateMotorSound(self, sound, dt, isInterieur, motorRpm, motorLerpValue, blendValue, blendScale)
 463  
 464      local pitch, maxVolume      = 1, 1;
 465  
 466      if sound.baseRpm ~= nil then
 467          pitch                   = self.motorSmoothRpm / sound.baseRpm;
 468  
 469          if sound.baseRpmScale ~= nil then
 470              pitch               = 1 + (pitch - 1) * sound.baseRpmScale;
 471          end;
 472      else
 473          pitch                   = sound.pitchOffset + sound.pitchScale * clamp(self.motorSmoothRpm - self.motorMinRpm, 0, self.motorMaxRpm);
 474      end;
 475  
 476      if sound.volumeScale ~= nil then
 477          volume                  = sound.minVolume + sound.volumeScale * (self.motorSmoothRpm - self.motorMinRpm);
 478      else
 479          volume                  = lerp(sound.minVolume, sound.maxVolume, motorLerpValue);
 480      end;
 481  
 482      if blendValue ~= nil then
 483          -- blend volume down to this value
 484          local targetVolume      = volume * blendValue;
 485  
 486          if sound.lastVolume ~= nil then
 487              if sound.lastVolume < targetVolume then
 488                  if sound.fadeInSpeed ~= nil then
 489                      volume      = math.min(sound.lastVolume + sound.fadeInSpeed * dt, targetVolume);
 490                  else
 491                      volume      = targetVolume;
 492                  end;
 493  
 494              elseif sound.lastVolume > targetVolume then
 495                  if sound.fadeOutSpeed ~= nil then
 496                      volume      = math.max(sound.lastVolume - sound.fadeOutSpeed * dt, targetVolume);
 497                  else
 498                      volume      = targetVolume;
 499                  end;
 500              else
 501                  volume          = targetVolume;
 502              end;
 503  
 504              -- store current sound volume for next frame
 505              sound.lastVolume    = volume;
 506          else
 507              volume              = targetVolume;
 508          end;
 509  
 510      else
 511          -- don't clamp if blend value is applied
 512          volume                  = clamp(volume * (blendScale or 1), sound.minVolume,    sound.maxVolume);
 513      end;
 514  
 515      -- ramp volume up/down (depending on rpm)
 516      if sound.minFadeInRpm ~= nil and sound.maxFadeInRpm then
 517          if motorRpm < sound.minFadeInRpm then
 518              volume              = 0;
 519  
 520          elseif motorRpm < sound.maxFadeInRpm then
 521              volume              = volume * clamp01(inverseLerp(sound.minFadeInRpm, sound.maxFadeInRpm, motorRpm));
 522          end;
 523      end;
 524  
 525      if sound.minFadeOutRpm ~= nil and sound.maxFadeOutRpm then
 526          if motorRpm > sound.maxFadeOutRpm then
 527              volume              = 0;
 528  
 529          elseif motorRpm > sound.minFadeOutRpm then
 530              volume              = volume * clamp01(inverseLerp(sound.maxFadeOutRpm, sound.minFadeOutRpm, motorRpm));
 531          end;
 532      end;
 533  
 534      if isInterieur and sound.interieurVolumeScale ~= nil then
 535          volume                  = volume * sound.interieurVolumeScale;
 536      end;
 537      if sound.interieurLowpass ~= nil or sound.exterieurLowpass ~= nil then
 538          if isInterieur then
 539              AudioSource.setLowPass(sound.sampleId, sound.interieurLowpass or 0);
 540          else
 541              AudioSource.setLowPass(sound.sampleId, sound.exterieurLowpass or 0);
 542          end;
 543      end;
 544  
 545      -- apply them both (but only clamped)
 546      AudioSource.setPitch(sound.sampleId,    clamp(pitch,    sound.minPitch,     sound.maxPitch));
 547      AudioSource.setVolume(sound.sampleId,   volume);
 548  end;

VehicleMotor:fixedUpdate(dt)

This is called once per physics update, i.e. possibly multiple times per frame. We therefore only perform necessary calculations.

 552  function VehicleMotor:fixedUpdate(dt)
 553      self.motorLastRpm               = self.motorRpm;
 554      self.wheelRpm                   = self:getDrivingWheelsRpm();
 555      self.motorRpm                   = 0.9 * self.motorRpm + 0.1 * self.wheelRpm * self:getGearTransmissionRatio();
 556  
 557      self:updateWheels();
 558  end;

VehicleMotor:updateWheels()

Updates the wheel torques, as well as steering values. The actual updating is done by VehicleMotor.singleWheel.

 561  function VehicleMotor:updateWheels()
 562      local drivingWheelCount         = self:getDrivingWheelsCount();
 563      local steerAngle                = self:getCurrentSteerAngle();
 564      local brakeTorque               = self:getCurrentBrakeTorque();
 565      
 566      -- get motor torque and inertia
 567      local motorTorque, motorInertia = 0, 0;
 568      if drivingWheelCount > 0 then
 569          local a, b                  = self:getCurrentMotorTorque();
 570          motorTorque                 = a / drivingWheelCount; -- motor torque in [Nm],       relative to wheel
 571          motorInertia                = b / drivingWheelCount; -- motor inertia in [kgm^2],   relative to the wheel
 572      end;
 573  
 574      for n, axle in pairs(self.axles) do
 575          
 576          -- apply wheel torques only when on ground
 577          VehicleMotor.singleWheel(self, axle.leftWheel,  axle,   motorTorque * axle.motorGear,   axle.brake and brakeTorque or 0);
 578          VehicleMotor.singleWheel(self, axle.rightWheel, axle,   motorTorque * axle.motorGear,   axle.brake and brakeTorque or 0);
 579          
 580          -- and now steering
 581          if axle.steeringScale ~= 0 then
 582              Wheel.setSteerAngle(axle.leftWheel,     steerAngle * axle.steeringScale);
 583              Wheel.setSteerAngle(axle.rightWheel,    steerAngle * axle.steeringScale);
 584          end;
 585  
 586          -- add powertrain inertia
 587          if axle.motorGear ~= 0 then
 588              local extraMass         = motorInertia / axle.radius^2;
 589              Wheel.setMass(axle.leftWheel,   axle.wheelMass + extraMass);
 590              Wheel.setMass(axle.rightWheel,  axle.wheelMass + extraMass);
 591          end;
 592      end;
 593  end;

VehicleMotor:singleWheel(wheelId, axle, motorTorque, brakeTorque)

Updates a single wheel using ABS (anti-lock braking system) to avoid wheel lock. This also includes a traction control to avoid spinning wheels. Both ABS and traction control operate depending on forward slip (which is physically not correct, but the best option for this game for performance reasons).

 597  function VehicleMotor:singleWheel(wheelId, axle, motorTorque, brakeTorque)
 598      local hit, ff, sf, force        = Wheel.getGroundHit(wheelId);
 599  
 600      if hit then
 601          if axle.useABS and ff < -0.6 then
 602              -- brake traction control (ABS)
 603              local brakeEnabled      = (getTime()%1.2) < 0.6;
 604              Wheel.setTorques(wheelId, motorTorque, brakeEnabled and brakeTorque or 0);
 605          
 606          elseif axle.tractionControl and ff > 0.5 then
 607              -- traction control
 608              Wheel.setTorques(wheelId, motorTorque, self.maxBrakeTorque);
 609              
 610          else
 611              Wheel.setTorques(wheelId, motorTorque, brakeTorque);
 612          end;
 613  
 614      else
 615          Wheel.setTorques(wheelId, 0, brakeTorque);
 616      end;
 617  end;

VehicleMotor:setIsMotorOn(isOn)

Sets the motor to on or off, depending on isOn (bool).

 621  function VehicleMotor:setIsMotorOn(isOn)
 622      if isOn == self.isMotorOn then return end;
 623  
 624      self.isMotorOn                  = isOn;
 625  
 626      if self.isMotorOn then
 627          if self.motorSoundStop ~= nil then      AudioSource.stop(self.motorSoundStop);      end;
 628  
 629          if self.motorSoundStart ~= nil then
 630              AudioSource.play(self.motorSoundStart);
 631  
 632              if self.motorSoundIdle ~= nil then
 633                  VehicleMotor.foreachMotorSound(self, self.motorSoundIdle, AudioSource.playDelayed, self.motorSoundStartLength);
 634              end;
 635              if self.motorSoundLoad ~= nil then
 636                  VehicleMotor.foreachMotorSound(self, self.motorSoundLoad, AudioSource.playDelayed, self.motorSoundStartLength);
 637              end;
 638              if self.motorSoundRun ~= nil then
 639                  VehicleMotor.foreachMotorSound(self, self.motorSoundRun, AudioSource.playDelayed, self.motorSoundStartLength);
 640              end;
 641  
 642          else
 643              if self.motorSoundIdle ~= nil then  VehicleMotor.foreachMotorSound(self, self.motorSoundIdle,   AudioSource.play);      end;
 644              if self.motorSoundLoad ~= nil then  VehicleMotor.foreachMotorSound(self, self.motorSoundLoad,   AudioSource.play);      end;
 645              if self.motorSoundRun ~= nil then   VehicleMotor.foreachMotorSound(self, self.motorSoundRun,    AudioSource.play);      end;
 646          end;
 647  
 648      else
 649          if self.motorSoundIdle ~= nil then      VehicleMotor.foreachMotorSound(self, self.motorSoundIdle,   AudioSource.stop);      end;
 650          if self.motorSoundLoad ~= nil then      VehicleMotor.foreachMotorSound(self, self.motorSoundLoad,   AudioSource.stop);      end;
 651          if self.motorSoundRun ~= nil then       VehicleMotor.foreachMotorSound(self, self.motorSoundRun,    AudioSource.stop);      end;
 652  
 653          if self.motorSoundStart ~= nil then     AudioSource.stop(self.motorSoundStart);     end;
 654          if self.motorSoundStop ~= nil then      AudioSource.play(self.motorSoundStop);      end;
 655      end;
 656  
 657      -- update exhaust particles
 658      for k, v in pairs(self.exhaustParticleSystems) do
 659          setActive(v.id, self.isMotorOn or false);
 660      end;
 661  end;

VehicleMotor:onEnter()

Start the motor unless disabled in GameplaySettings.

 665  function VehicleMotor:onEnter()
 666      if GameplaySettings.autoStartMotor and not self:getHasRunOutOfFuel() then
 667          self:setIsMotorOn(true);
 668      end;
 669  end;

VehicleMotor:onLeave()

Stop the motor upon leaving. Also apply maximum braking torque to save performance on wheel physics.

 673  function VehicleMotor:onLeave()
 674      if GameplaySettings.autoStopMotor then
 675          self:setIsMotorOn(false);
 676      end;
 677  
 678      -- automatically apply the brake upon leaving (because wheels will no longer be updated)
 679      for n, axle in pairs(self.axles) do
 680          Wheel.setTorques(axle.leftWheel, 0, self.maxBrakeTorque);
 681          Wheel.setTorques(axle.rightWheel, 0, self.maxBrakeTorque);
 682      end;
 683  end;

VehicleMotor:getDrivingWheelsRpm()

Returns the current wheel rpm (average of all driving wheels).

 687  function VehicleMotor:getDrivingWheelsRpm()
 688      local numerator                 = 0;
 689      local denominator               = 0;
 690      
 691      for n, axle in pairs(self.axles) do
 692          if axle.motorGear > 0 then
 693              numerator               = numerator + 0.5 * (Wheel.getRpm(axle.leftWheel) + Wheel.getRpm(axle.rightWheel));
 694              denominator             = denominator + axle.motorGear;
 695          end;
 696      end;
 697      
 698      return (denominator > 0 and numerator / denominator or 0);
 699  end;

VehicleMotor:getDrivingWheelsCount()

Returns the number of driving wheels.

 703  function VehicleMotor:getDrivingWheelsCount()
 704      local sum                       = 0;
 705      for n, axle in pairs(self.axles) do
 706          sum                         = sum + axle.motorGear;
 707      end;
 708  
 709      return 2*sum;
 710  end;

VehicleMotor:getGearTransmissionRatio()

Returns the currently activated gear transmission ratio. This will be negative for reverse gear.

 714  function VehicleMotor:getGearTransmissionRatio()
 715      return self.gears[self.currentGear] * self.motorFinalDriveRatio;
 716  end;

VehicleMotor:getCurrentMotorTorque()

Returns the current motor torque in Nm, depending on current transmission ratio and current motor torque (which itself depends on motorRpm and throttle value).

 719  function VehicleMotor:getCurrentMotorTorque()
 720      local transmissionRatio         = self:getGearTransmissionRatio();
 721      local motorOutputTorque         = not self.isMotorOn and 0 or self:getMotorTorqueAtRpm(self.motorRpm) * self.throttleValue;
 722  
 723      -- motor inertia is directly applied to the wheel
 724      inertia                         = 2 * self.motorInertiaMoment * transmissionRatio * transmissionRatio;
 725  
 726      return motorOutputTorque * transmissionRatio, math.abs(inertia);
 727  end;

VehicleMotor:setAutomaticDriveActive(isActive)

Enables or disables automatic drive, depending on isActive (bool).

 730  function VehicleMotor:setAutomaticDriveActive(isActive)
 731      self.automaticDriveActive       = isActive;
 732  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.