Action denied

Vehicles/VehicleScripts/SnowGroomer.lua

SnowGroomer is a vehicle script that is in use. It is responsible for the control of the groomer.

The lua script is based on the C# class SnowGroomerIK, which was specifically designed to solve 4-dimensional inverse kinematics problems. This leads to the best possible visual results at almost no performance impact. However, the script's configuration requires some parameters that have to be provided in the dataTable.

  28  SnowGroomer                         = SnowGroomer or {};
  29  SnowGroomer.directionSmoothRadius   = 0.8;

SnowGroomer:load(dataTable)

First, we set up the groomer-specific functions. Afterwards, we read in the configuration data from the dataTable.

Some remarks:

  • axis1 to axis4 are the indexes of the objects that are rotated for each axis. We need 4 degrees of freedom, because the groomer shall be allowed to rotate freely within some limits (i.e. three degrees of freedom in rotation), but it should also stay parallel to the ground (i.e. it needs another rotation axis to allow for a translation movement).
    • axis 1 rotates around Y (left/right, required for trail)
    • axis 2 rotates around X (up/down)
    • axis 3 again rotates around X (required for the 4th degree of freedom)
    • axis 4 rotates around Z (tilt)
  • v.lifted.axis1 to v.lifted.axis4 specify the rotation targets if the groomer is lifted.
  • If the groomer is lowered, the individual axis limits (axis1Limit, axis2MinLimit, axis2MaxLimit etc.) are activated and the groomer can move freely between the limits.
  • The script supports flaps and also a rotating milling shaft
  • As soon as the groomer is lowered, the milling shaft starts rotating, and the snowGroomerPrepAreas are activated. Only then, the slope can actually be groomed.
  45  function SnowGroomer:load(dataTable)
  46      self.updateSnowGroomerLimits    = VehicleManager:newFunction("updateSnowGroomerLimits");
  47      self.setIsSnowGroomerIsLowered  = VehicleManager:newFunction("setIsSnowGroomerIsLowered");
  48      self.setIsSnowGroomerIsLocked   = VehicleManager:newFunction("setIsSnowGroomerIsLocked");
  49      self.setSnowGroomerFlaps        = VehicleManager:newFunction("setSnowGroomerFlaps");
  50      
  51      if dataTable.snowGroomer ~= nil then
  52          local v                     = dataTable.snowGroomer;
  53          local axis1                 = getChild(self.id, v.axis1 or "");
  54          local axis2                 = getChild(self.id, v.axis2 or "");
  55          local axis3                 = getChild(self.id, v.axis3 or "");
  56          local axis4                 = getChild(self.id, v.axis4 or "");
  57          local raycastOrigin         = getChild(self.id, v.raycastOrigin or v.groomerCenter or "");
  58  
  59          self.snowGroomerIK          = SnowGroomerIK.new(self.id, axis1, axis2, axis3, axis4, raycastOrigin);
  60          SnowGroomerIK.setupGeometry(self.snowGroomerIK,
  61              v.axis23Length      or 1,
  62              v.groundToAxis4Y    or 0.5,
  63              v.groundToAxis4Z    or 0.5,
  64              v.axis4To3Y         or 0,
  65              v.axis4To3Z         or 0
  66          );
  67  
  68          self.snowGroomerLifted      = {
  69              axis1                   = v.lifted.axis1    or   0,
  70              axis2                   = v.lifted.axis2    or  45,
  71              axis3                   = v.lifted.axis3    or -45,
  72              axis4                   = v.lifted.axis4    or   0,
  73          };
  74          self.snowGroomerLowered     = {
  75              axis1Limit              = v.lowered.axis1Limit      or 20,
  76              axis2MinLimit           = v.lowered.axis2MinLimit   or -45,
  77              axis2MaxLimit           = v.lowered.axis2MaxLimit   or  45,
  78              axis3MinLimit           = v.lowered.axis3MinLimit   or -45,
  79              axis3MaxLimit           = v.lowered.axis3MaxLimit   or  45,
  80              axis4MinLimit           = v.lowered.axis4MinLimit   or -10,
  81              axis4MaxLimit           = v.lowered.axis4MaxLimit   or  10,
  82          };
  83          self.snowGroomerTimeToLower = math.max(v.timeToLower or 2, 0.1);
  84          self.snowGroomerTimeToLift  = math.max(v.timeToLift  or 2, 0.1);
  85          self.snowGroomerTimeToLock  = math.max(v.timeToLock  or 2, 0.1);
  86  
  87          if v.leftFlap ~= nil then
  88              self.snowGroomerLeftFlap    = getChild(self.id, v.leftFlap or "");
  89              Animation.stop(self.snowGroomerLeftFlap);
  90              Animation.sampleTime(self.snowGroomerLeftFlap, 0);
  91              self.leftFlapPosition       = 0;
  92              self.leftFlapTarget         = 1;
  93              self.leftFlapDuration       = Animation.getLength(self.snowGroomerLeftFlap);
  94          end;
  95          if v.rightFlap ~= nil then
  96              self.snowGroomerRightFlap   = getChild(self.id, v.rightFlap or "");
  97              Animation.stop(self.snowGroomerRightFlap);
  98              Animation.sampleTime(self.snowGroomerRightFlap, 0);
  99              self.rightFlapPosition      = 0;
 100              self.rightFlapTarget        = 1;
 101              self.rightFlapDuration      = Animation.getLength(self.snowGroomerRightFlap);
 102          end;
 103  
 104          if v.rotatingParts ~= nil then
 105              self.groomerRotatingParts   = {};
 106  
 107              for k, v in pairs(v.rotatingParts) do
 108                  table.insert(self.groomerRotatingParts, {
 109                      id                  = getChild(self.id, v.index),
 110                      axis                = v.axis or 1,
 111                      speed               = (v.speed or 300)/60 * 360, -- rpm (here converted to degrees per second)
 112                  });
 113              end;
 114          end;
 115  
 116          -- preparation areas
 117          self.snowGroomerPrepAreas       = {};
 118          if v.preparationAreas ~= nil then
 119              for k, n in pairs(v.preparationAreas) do
 120                  table.insert(self.snowGroomerPrepAreas, {
 121                      getChild(self.id, n[1] or ""),
 122                      getChild(self.id, n[2] or ""),
 123                      getChild(self.id, n[3] or ""),
 124                      n[4] or SnowGroomer.directionSmoothRadius,
 125                  });
 126              end;
 127          end;
 128  
 129          -- don't lower by default
 130          self.snowGroomerIsLowered   = false;
 131          self.snowGroomerIsLocked    = false;
 132          self.snowGroomerPos         = 0; -- position to interpolate between limits and lifted
 133          self.snowGroomerLockedPos   = 0;
 134  
 135          self:updateSnowGroomerLimits();
 136      end;
 137  end;

SnowGroomer:saveToTable(tbl)

As always, we need to save some variables in our savegame.

 141  function SnowGroomer:saveToTable(tbl)
 142      if tbl == nil then return end;
 143      if self.snowGroomerIK == nil then return end;
 144  
 145      tbl.snowGroomerIsLowered        = self.snowGroomerIsLowered;
 146      tbl.snowGroomerIsLocked         = self.snowGroomerIsLocked;
 147      tbl.snowGroomerPos              = self.snowGroomerPos;
 148      tbl.snowGroomerLockedPos        = self.snowGroomerLockedPos;
 149  
 150      tbl.leftFlapTarget              = self.leftFlapTarget;
 151      tbl.leftFlapPosition            = self.leftFlapPosition;
 152      tbl.rightFlapTarget             = self.rightFlapTarget;
 153      tbl.rightFlapPosition           = self.rightFlapPosition;
 154  end;

SnowGroomer:loadFromTable(tbl)

Restore the variables from the savegame.

 158  function SnowGroomer:loadFromTable(tbl)
 159      if tbl == nil then return end;
 160      if self.snowGroomerIK == nil then return end;
 161  
 162      self.snowGroomerIsLowered       = getNoNil(tbl.snowGroomerIsLowered,    self.snowGroomerIsLowered);
 163      self.snowGroomerIsLocked        = getNoNil(tbl.snowGroomerIsLocked,     self.snowGroomerIsLocked);
 164      self.snowGroomerPos             = getNoNil(tbl.snowGroomerPos,          self.snowGroomerPos);
 165      self.snowGroomerLockedPos       = getNoNil(tbl.snowGroomerLockedPos,    self.snowGroomerLockedPos);
 166  
 167      self.leftFlapTarget             = getNoNil(tbl.leftFlapTarget,          self.leftFlapTarget);
 168      self.leftFlapPosition           = getNoNil(tbl.leftFlapPosition,        self.leftFlapPosition);
 169      self.rightFlapTarget            = getNoNil(tbl.rightFlapTarget,         self.rightFlapTarget);
 170      self.rightFlapPosition          = getNoNil(tbl.rightFlapPosition,       self.rightFlapPosition);
 171  
 172      -- always update real flap position in update
 173      if self.leftFlapTarget ~= nil then
 174          self.leftFlapPosition       = 0.5;
 175      end;
 176      if self.rightFlapTarget ~= nil then
 177          self.rightFlapPosition      = 0.5;
 178      end;
 179  
 180      SnowGroomer.updateGroomerActive(self, 0, true);
 181  end;

SnowGroomer:update(dt)

Update the controls each frame (i.e. check whether the player wants to lower/lift the groomer, lock the trail or move some flaps). The actual groomer update is done in SnowGroomer:updateGroomerActive.

 186  function SnowGroomer:update(dt)
 187      if not self.isActive then return end;
 188      if self.snowGroomerIK == nil then return end;
 189      
 190      if self:getIsInputActive() and not self.hydraulicLiftGroomer then
 191          if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LowerGroomer) then
 192              self:setIsSnowGroomerIsLowered(not self.snowGroomerIsLowered);
 193          end;
 194          g_GUI:addKeyHint(InputMapper.VehicleSnowcat_LowerGroomer,       l10n.get(self.snowGroomerIsLowered  and "Input_VehicleSnowcat_LowerGroomer1"    or "Input_VehicleSnowcat_LowerGroomer0"));
 195  
 196          if self.snowGroomerIsLowered then
 197              -- only allow to lock/unlock if snow groomer is lowered
 198              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LockTrail) then
 199                  self:setIsSnowGroomerIsLocked(not self.snowGroomerIsLocked);
 200              end;
 201              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_LeftFlap) then
 202                  self:setSnowGroomerFlaps(self.leftFlapTarget == 0, nil);
 203              end;
 204              if InputMapper:getKeyDown(InputMapper.VehicleSnowcat_RightFlap) then
 205                  self:setSnowGroomerFlaps(nil, self.rightFlapTarget == 0);
 206              end;
 207  
 208              g_GUI:addKeyHint(InputMapper.VehicleSnowcat_LockTrail,      l10n.get(self.snowGroomerIsLocked       and "Input_VehicleSnowcat_LockTrail1"       or "Input_VehicleSnowcat_LockTrail0"));
 209          end;
 210      end;
 211  
 212      SnowGroomer.updateGroomerActive(self, dt);
 213  end;

SnowGroomer:updateGroomerActive(dt, forceUpdate)

This is called in every frame if the groomer is active. It determines current animation positions and grooms the preparation areas.

 217  function SnowGroomer:updateGroomerActive(dt, forceUpdate)
 218      -- update groomer position
 219      local target                = self.snowGroomerIsLowered and not self.hydraulicLiftGroomer   and 1 or 0;
 220      local lockTarget            = self.snowGroomerIsLocked                                      and 1 or 0;
 221      local leftTarget            = (target < 1 or self.snowGroomerPos < 0.5) and 0 or self.leftFlapTarget;
 222      local rightTarget           = (target < 1 or self.snowGroomerPos < 0.5) and 0 or self.rightFlapTarget;
 223  
 224      local update                = false;
 225      if math.abs(self.snowGroomerPos - target) > 1e-3 or forceUpdate then
 226          local duration          = self.snowGroomerIsLowered and self.snowGroomerTimeToLower or self.snowGroomerTimeToLift;
 227          self.snowGroomerPos     = Utils.moveTowards(self.snowGroomerPos, target, dt/duration);
 228          update                  = true;
 229      end;
 230  
 231      if math.abs(self.snowGroomerLockedPos - lockTarget) > 1e-3 or forceUpdate then
 232          self.snowGroomerLockedPos = Utils.moveTowards(self.snowGroomerLockedPos, lockTarget, dt/self.snowGroomerTimeToLock);
 233          update                  = true;
 234      end;
 235  
 236      if self.snowGroomerLeftFlap ~= nil and (math.abs(self.leftFlapPosition - leftTarget) > 1e-3 or forceUpdate) then
 237          self.leftFlapPosition   = Utils.moveTowards(self.leftFlapPosition, leftTarget, dt/self.leftFlapDuration);
 238          Animation.sampleTime(self.snowGroomerLeftFlap, self.leftFlapPosition * self.leftFlapDuration);
 239      end;
 240      if self.snowGroomerRightFlap ~= nil and (math.abs(self.rightFlapPosition - rightTarget) > 1e-3 or forceUpdate) then
 241          self.rightFlapPosition  = Utils.moveTowards(self.rightFlapPosition, rightTarget, dt/self.rightFlapDuration);
 242          Animation.sampleTime(self.snowGroomerRightFlap, self.rightFlapPosition * self.rightFlapDuration);
 243      end;
 244  
 245      if update then
 246          self:updateSnowGroomerLimits();
 247      end;
 248  
 249      -- prepare
 250      if self.snowGroomerPos > 0.5 then
 251          for k, v in pairs(self.snowGroomerPrepAreas) do
 252              SnowSystem.groomTriangle(unpack(v));
 253          end;
 254      end;
 255  
 256      -- animate rotating parts
 257      if self.groomerRotatingParts ~= nil and self.snowGroomerPos > 0.9 and self.isMotorOn then
 258          for k, rp in pairs(self.groomerRotatingParts) do
 259              if rp.axis == 1 then
 260                  rotate(rp.id, rp.speed * dt, 0,0);
 261  
 262              elseif rp.axis == 2 then
 263                  rotate(rp.id, 0, rp.speed * dt, 0);
 264  
 265              else
 266                  rotate(rp.id, 0,0, rp.speed * dt);
 267              end;
 268          end;
 269      end;
 270  end;

SnowGroomer:setIsSnowGroomerIsLowered(isLowered)

Lower/lift the groomer, depending on isLowered (bool).

 274  function SnowGroomer:setIsSnowGroomerIsLowered(isLowered)
 275      self.snowGroomerIsLowered           = isLowered or false;
 276  end;

SnowGroomer:setIsSnowGroomerIsLocked(isLocked)

Lock/release the groomer, depending on isLocked (bool).

 280  function SnowGroomer:setIsSnowGroomerIsLocked(isLocked)
 281      self.snowGroomerIsLocked            = isLocked or false;
 282  end;

SnowGroomer:setSnowGroomerFlaps(left, right)

Fold out the left and the right flap (both bool). true means the flap shall be moved out.

 285  function SnowGroomer:setSnowGroomerFlaps(left, right)
 286      if left ~= nil then     self.leftFlapTarget         = left and 1 or 0;      end;
 287      if right ~= nil then    self.rightFlapTarget        = right and 1 or 0;     end;
 288  end;

SnowGroomer:updateSnowGroomerLimits()

Passes on the axis target values and limits to the C# class SnowGroomerIK, which will then solve the 4D inverse kinematics.

 292  function SnowGroomer:updateSnowGroomerLimits()
 293      if self.snowGroomerIK == nil then return end;
 294  
 295      local weight                    = self.snowGroomerPos;
 296      local axis1Weight               = clamp01(weight - self.snowGroomerLockedPos);
 297  
 298      SnowGroomerIK.setAxis1Limit(self.snowGroomerIK, lerp(self.snowGroomerLifted.axis1, self.snowGroomerLowered.axis1Limit, axis1Weight));
 299      SnowGroomerIK.setAxis2Limit(self.snowGroomerIK,
 300          lerp(self.snowGroomerLifted.axis2, self.snowGroomerLowered.axis2MinLimit, weight),
 301          lerp(self.snowGroomerLifted.axis2, self.snowGroomerLowered.axis2MaxLimit, weight)
 302      );
 303      SnowGroomerIK.setAxis3Limit(self.snowGroomerIK,
 304          lerp(self.snowGroomerLifted.axis3, self.snowGroomerLowered.axis3MinLimit, weight),
 305          lerp(self.snowGroomerLifted.axis3, self.snowGroomerLowered.axis3MaxLimit, weight)
 306      );
 307      SnowGroomerIK.setAxis4Limit(self.snowGroomerIK,
 308          lerp(self.snowGroomerLifted.axis4, self.snowGroomerLowered.axis4MinLimit, weight),
 309          lerp(self.snowGroomerLifted.axis4, self.snowGroomerLowered.axis4MaxLimit, weight)
 310      );
 311  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.