meta data for this page
Vehicles/Vehicle.lua
The Vehicle class is at the heart of all vehicle-related features, together with VehicleManager. It handles all aspects that affect all vehicles (such as buying, selling or initializing vehicle scripts).
26 27 Vehicle = Vehicle or {}; 28 local VehicleClass = Class(Vehicle, NetworkEntity); 29
Vehicle:addVehicleScript(vehicleScript)
Adds the Vehicle script class vehicleScript
(a table with functions, usually) to this vehicle's scripts.
38 function Vehicle:addVehicleScript(vehicleScript) 39 table.insert(self.vehicleScripts, vehicleScript); 40 end; 41
Vehicle:callVehicleScripts(functionName, ...)
Calls the function named functionName
(string) for all vehicle scripts that are registered to this vehicle.
44 function Vehicle:callVehicleScripts(functionName, ...) 45 -- calls all vehicle script functions that are named the same, using the proper arguments 46 for _, script in ipairs(self.vehicleScripts) do 47 -- ensure its not nil 48 if script[functionName] ~= nil then 49 script[functionName](self, ...); 50 end; 51 end; 52 end; 53
Vehicle:callSafe(functionName, ...)
Safely calls the function named functionName
(string) of this vehicle. If this function does not exist, no error will be thrown.
It makes sense to call this function e.g. if it is implemented by another vehicle script that could also be left out.
59 function Vehicle:callSafe(functionName, ...) 60 local func = self[functionName]; 61 62 if func ~= nil then 63 func(self, ...); 64 end; 65 end; 66
Vehicle:load(bundleId, vehicleType, dataTable, isDummy, addedColors)
Loads the newly created Vehicle instance using some parameters:
bundleId
(int) id of the mod's asset bundle, 0 for vanilla contentvehicleType
(string) name of the vehicle type, this will either bedefault.xyz
ormods.yourmodname.xyz
.dataTable
(table) the data table that was passed over toModLoader.addContent(name, dataTable)
.isDummy
(optional bool) is set true if the spawned vehicle should be a dummy(not registered). Example placing mode.
74 function Vehicle:load(bundleId, vehicleType, dataTable, isDummy, addedColors) 75 -- call NetworkEntity's load function 76 Vehicle:parentClass().load(self); 77 78 -- first load our vehicle prefab 79 local prefabName = dataTable.prefab or ""; 80 local id = Utils.loadBundleGameObject(bundleId, prefabName); 81 setPosition(id, 0,0,0); 82 setRotation(id, 0,0,0); 83 84 -- first store the main id 85 self.id = id; 86 self.bundleId = bundleId; 87 self.loadComplete = false; 88 89 -- store these two for the savegame 90 self.vehicleType = vehicleType; 91 self.dataTable = dataTable; 92 93 -- seat id system is a part of Vehicle.lua, because it is better if supported by all vehicles 94 self.lastSeatId = 0; 95 self.seatsById = {}; 96 97 self.balanceSheetId = 0; 98 self.isDummy = getNoNil(isDummy, false); 99 self.customColors = dataTable.customColors; 100 if addedColors ~= nil then 101 for k, color in pairs(addedColors) do 102 if color ~= nil then 103 self.customColors[k].value = color.value; 104 end; 105 end; 106 end; 107 108 -- filmmaking feature 109 self.colorCountIndex = 0; 110 111 self.vehicleScripts = {}; 112 113 self:loadComponents(dataTable); 114 self:loadVehicleScripts(dataTable); 115 116 if g_isMaster then 117 -- out of bounds callback 118 setOutOfBoundsCallback(self.id, function() VehicleManager:vehicleOutOfBounds(self); end); 119 end; 120 121 -- some parameter reading 122 self.maxWheelTorque = dataTable.maxWheelTorque or 1200; 123 124 self.maxBrakeTorque = dataTable.maxBrakeTorque or 1200; 125 self.parkingBrakeTorque = dataTable.parkingBrakeTorque or 550; 126 127 self.maxSteerAngle = dataTable.steeringAngle or 30; 128 self.steeringSteerSpeed = dataTable.steeringSteerSpeed or 2; 129 self.steeringReturnSpeed = dataTable.steeringReturnSpeed or 4; 130 self.steeringSlowdownCoeff = dataTable.steeringSlowdownCoeff or 0.01; 131 self.steeringMaxSlowdown = dataTable.steeringMaxSlowdown or 0.5; 132 133 self.width = dataTable.width; 134 self.length = dataTable.length; 135 136 self.hasCrashSoundsImplemented = false; 137 self.hasCrashBoxImplemented = false; 138 self:loadCrashBox(dataTable); 139 140 if dataTable.sounds ~= nil then 141 if dataTable.sounds.crashSounds ~= nil then 142 self.hasCrashSoundsImplemented = true; 143 end; 144 end; 145 146 -- runtime variables 147 self.parkingBrake = 1; -- 1 means applied, 0 released 148 self.brakeValue = 0; 149 self.throttleValue = 0; -- handled by this script, but applied by VehicleMotor 150 self.steerValue = 0; -- steer value for wheels 151 self.rawSteerValue = 0; -- raw input from user 152 self.rawSteerValueFixed = false; -- whether raw input from user is from joystick (true) or keyboard (false) 153 154 self.anyPlayerEntered = false; 155 156 local steeringWheelIndex = dataTable.steeringWheelIndex or ""; 157 if steeringWheelIndex ~= "" then 158 self.steeringWheelId = getChild(self.id, steeringWheelIndex); 159 self.steeringWheelAngle = getNoNil(dataTable.steeringWheelAngle, 450); 160 self.steeringWheelAngleExterior = getNoNil(dataTable.steeringWheelAngleExterior, self.steeringWheelAngle); 161 self.steeringWheelAxle = getNoNil(dataTable.steeringWheelAxis, 3); 162 end; 163 164 local steeringAnimationIndex = dataTable.steeringAnimationIndex; 165 if steeringAnimationIndex ~= nil then 166 self.steeringAnimationId = getChild(self.id, steeringAnimationIndex); 167 self.steeringAnimationLength = Animation.getLength(self.steeringAnimationId); 168 Animation.stop(self.steeringAnimationId); 169 Animation.sampleTime(self.steeringAnimationId, 0.5 * self.steeringAnimationLength); 170 end; 171 172 -- read drag values 173 self.dragControlMinSpeed = dataTable.dragControlMinSpeed; 174 self.dragControlMaxSpeed = dataTable.dragControlMaxSpeed; 175 self.dragControlMaxDrag = dataTable.dragControlMaxDrag; 176 self.dragControlMinDrag = dataTable.dragControlMinDrag; 177 178 self:loadSounds(dataTable.sounds); 179 180 self:loadAxles(dataTable); 181 182 self.driveDirectionChangeTimePrev = 0; 183 184 -- cruise control values 185 self.cruiseControlActive = false; 186 187 -- load exhausts 188 self.exhaustParticleSystems = {}; 189 if dataTable.exhaustParticleSystems ~= nil then 190 for k, v in pairs(dataTable.exhaustParticleSystems) do 191 local id = getChild(self.id, v.index); 192 193 if id ~= 0 then 194 local particleSystemId = Utils.loadBundleGameObject(self.bundleId, v.particleSystem or "$vehicles/particles/ExhaustParticles"); 195 196 setParent(particleSystemId, id); 197 setPosition(particleSystemId, 0,0,0); 198 setRotation(particleSystemId, 0,0,0); 199 setActive(particleSystemId, false); 200 201 table.insert(self.exhaustParticleSystems, { id = particleSystemId }); 202 end; 203 end; 204 end; 205 206 -- load snow particles 207 self.snowParticleSystems = {}; 208 if dataTable.snowParticleSystems ~= nil then 209 for k, v in pairs(dataTable.snowParticleSystems) do 210 local id = getChild(self.id, v.index); 211 if id ~= 0 then 212 local particleSystemId = Utils.loadBundleGameObject(self.bundleId, v.particleSystem); -- or "$vehicles/particles/SnowCrawler"); 213 214 setParent(particleSystemId, id); 215 setPosition(particleSystemId, 0,0,0); 216 setRotation(particleSystemId, 0,0,0); 217 setActive(particleSystemId, false); 218 219 table.insert(self.snowParticleSystems, { id = particleSystemId, amount = getNoNil(v.particleAmount, 100), reverse = getNoNil(v.reverse, false) }); 220 end; 221 end; 222 end; 223 224 -- load ik hand targets 225 if dataTable.steeringWheelHands ~= nil then 226 if dataTable.steeringWheelHands.handLeft ~= nil and dataTable.steeringWheelHands.handLeft ~= "" then 227 self.anchorSteeringLeftIK = getChild(self.id, dataTable.steeringWheelHands.handLeft); 228 end; 229 230 if dataTable.steeringWheelHands.handRight ~= nil and dataTable.steeringWheelHands.handRight ~= "" then 231 self.anchorSteeringRightIK = getChild(self.id, dataTable.steeringWheelHands.handRight); 232 end; 233 end; 234 235 -- set up a default HUD class 236 self.vehicleHUDClass = VehicleHUD; 237 238 self:callVehicleScripts("load", dataTable); 239 240 -- apply brake torque 241 if not self.customWheelScriptActive then 242 for n, axle in pairs(self.axles) do 243 Wheel.setTorques(axle.leftWheel, 0, self.maxBrakeTorque); 244 Wheel.setTorques(axle.rightWheel, 0, self.maxBrakeTorque); 245 end; 246 end; 247 248 self.loadComplete = true; 249 250 -- finally register the vehicle (only if this is not a dummy) 251 if not self.isDummy then 252 VehicleManager:registerVehicle(self); 253 254 -- this is called by vehicle manager (not by Vehicle itself any more) 255 --self:register(); 256 end; 257 end; 258
Vehicle:loadVehicleScripts(dataTable)
Loads all vehicle scripts from dataTable
and registers them to this vehicle.
262 function Vehicle:loadVehicleScripts(dataTable) 263 264 -- make sure that the vehicle scripts table exists 265 if type(dataTable.vehicleScripts) ~= "table" then 266 print("Error while loading vehicle type " .. tostring(self.vehicleType) .. ": vehicleScripts is a " .. type(dataTable.vehicleScripts) .. " value (table expected)."); 267 return; 268 end; 269 270 for i, class in pairs(dataTable.vehicleScripts) do 271 272 if type(class) == "table" then 273 self:addVehicleScript(class); 274 else 275 print("Error while loading vehicle type " .. tostring(self.vehicleType) .. "'s VehicleScripts: failed to find script number " .. tostring(i) .. ". Check if it's existent within the scope of your data table or if there's a spelling mistake somewhere (case sensitive)."); 276 end; 277 end; 278 end; 279
Vehicle:loadSounds(dataTable)
Loads some basic sounds (parking brake, switch sound).
282 function Vehicle:loadSounds(dataTable) 283 if dataTable == nil then return end; 284 285 self.parkingBrakeSound0 = self:loadSingleSound(dataTable.releaseParkingBrake); 286 self.parkingBrakeSound1 = self:loadSingleSound(dataTable.setParkingBrake); 287 288 self.switchSound0 = self:loadSingleSound(dataTable.switchTurnOff); 289 self.switchSound1 = self:loadSingleSound(dataTable.switchTurnOn); 290 end; 291
Vehicle:loadCrashBox(dataTable)
Loads the crashbox if there is one specified. Crashbox is needed for crashSounds etc.
294 function Vehicle:loadCrashBox(dataTable) 295 if dataTable.crashbox ~= nil then 296 self.crashbox = {}; 297 for k,v in pairs(dataTable.crashbox) do 298 local entry = { 299 origin = getChild(self.id, v.origin or ""), 300 dimensions = v.dimensions, 301 componentnumber = v.componentnumber, 302 }; 303 table.insert(self.crashbox, entry); 304 end; 305 self.hasCrashBoxImplemented = true; 306 end; 307 end; 308
Vehicle:loadSingleSound(data, soundOnly)
Loads a sound from the configuration table given in data
(table). If soundOnly
is equal to true, the volume specified in data
will not be applied.
This function is used several times by other classes, such as FuelTank, VehicleLighting, VehicleMotor and WarningSound. In general, it is recommended to use this function for all one-shot sounds.
313 function Vehicle:loadSingleSound(data, soundOnly) 314 if data == nil then 315 return nil; 316 end; 317 318 local index = nil; 319 320 if type(data) == "string" then 321 index = data; 322 elseif type(data) == "table" then 323 index = data.prefab; 324 end; 325 326 if index == nil or index == "" then 327 return nil; 328 end; 329 330 local id = Utils.loadBundleGameObject(self.bundleId, index); 331 332 if id == 0 then 333 return nil; 334 end; 335 336 -- make it a child to us 337 338 setParent(id, self.mainId); 339 setPosition(id, 0,0,0); 340 341 if not soundOnly and type(data) == "table" then 342 if data.volume ~= nil then 343 AudioSource.setVolume(id, data.volume); 344 end; 345 end; 346 347 -- and return it back to the caller 348 return id; 349 end; 350
Vehicle:playSound(sound)
Plays the specified sound with id sound
(int), if existing.
353 function Vehicle:playSound(sound) 354 if sound ~= nil then 355 AudioSource.play(sound); 356 end; 357 end; 358
Vehicle:stopSound(sound)
Stops playing the specified sound with id sound
(int), if existing.
361 function Vehicle:stopSound(sound) 362 if sound ~= nil then 363 AudioSource.stop(sound); 364 end; 365 end; 366
Vehicle:playOnOffSound(condition, onSound, offSound)
Starts or stops playing the specified sound with id sound
(int), depending on condition
(bool), unless that sound does not exist.
369 function Vehicle:playOnOffSound(condition, onSound, offSound) 370 if condition then 371 if onSound ~= nil then 372 AudioSource.play(onSound); 373 end; 374 else 375 if offSound ~= nil then 376 AudioSource.play(offSound); 377 end; 378 end; 379 end;
Vehicle:registerNewSeat(seat)
Assigns a new (entity, seatId) combination to the VehicleSeat seat
.
382 function Vehicle:registerNewSeat(seat) 383 self.lastSeatId = self.lastSeatId + 1; 384 self.seatsById[self.lastSeatId] = seat; 385 seat:assignSeatId(self, self.lastSeatId); 386 end;
Vehicle:getSeatById(seatId)
Returns the seat that has beem assigned the given seatId
to. (Used for MP synchronisation)
390 function Vehicle:getSeatById(seatId) 391 return self.seatsById[seatId]; 392 end;
Vehicle:setBalanceSheetId(balanceSheetId)
Assigns the balance sheet id balanceSheetId
(id) to this vehicle. This is called after the vehicle has been bought.
Please do not modify balance sheet ids. This can distort the ecosystem and especially financial figures due to the double book-keeping system in WRS.
397 function Vehicle:setBalanceSheetId(balanceSheetId) 398 self.balanceSheetId = balanceSheetId; 399 end;
Vehicle:saveToTable()
Joins all relevant variables that shall be saved from any vehicle script into a single table and returns it.
402 function Vehicle:saveToTable() 403 -- write our position etc in here 404 local px, py, pz = getPosition(self.id); 405 local rx, ry, rz = getRotation(self.id); 406 407 --- if there are more components than 1, apply the use the position and rotation from the first component 408 if #self.components > 1 then 409 px, py, pz = getPosition(self.mainId); 410 rx, ry, rz = getRotation(self.mainId); 411 end; 412 413 local tbl = { 414 vehicleType = self.vehicleType, 415 balanceSheetId = self.balanceSheetId, 416 customizationName = self.dataTable.customizationName, 417 customColors = self.customColors; 418 px = px, 419 py = py, 420 pz = pz, 421 rx = rx, 422 ry = ry, 423 rz = rz, 424 }; 425 self:callVehicleScripts("saveToTable", tbl); 426 return tbl; 427 end;
Vehicle:loadFromTable(tbl)
Restores the vehicle's variables and calls all vehicle scripts to load them as well.
Please note that position and rotation are already handled by VehicleManager when the vehicle is spawned.
432 function Vehicle:loadFromTable(tbl) 433 if tbl == nil then return end; 434 435 self:setBalanceSheetId(tbl.balanceSheetId or self.balanceSheetId); 436 self:callVehicleScripts("loadFromTable", tbl); 437 end;
Vehicle:getAge()
Returns the age of the vehicle in days (if possible, otherwise nil
).
440 function Vehicle:getAge() 441 local asset = g_scenario.accounting:getBalanceSheetEntry(self.balanceSheetId); 442 443 if asset == nil then 444 return nil; 445 end; 446 return asset.age; 447 end;
Vehicle:getMaintenanceCost()
Returns the daily maintenance cost.
After its acquisition, the vehicle will cost dataTable.minMaintenance
per day, while it will cost dataTable.maxMaintenance
per day once the depreciationPeriod
is exceeded.
451 function Vehicle:getMaintenanceCost() 452 local asset = g_scenario.accounting:getBalanceSheetEntry(self.balanceSheetId); 453 454 -- if there's no balance sheet item, then it is no longer in useful life 455 local remainingUsefulLife = 0; 456 457 if asset ~= nil and asset.age ~= nil then 458 remainingUsefulLife = map(0, self.dataTable.depreciationPeriod, 1, 0, asset.age); 459 end; 460 461 return map(1, 0, self.dataTable.minMaintenance, self.dataTable.maxMaintenance, clamp01(remainingUsefulLife)); 462 end;
Vehicle:getSellValue()
Returns the vehicle's current sell value in the game's currency.
465 function Vehicle:getSellValue() 466 local asset = g_scenario.accounting:getBalanceSheetEntry(self.balanceSheetId); 467 468 -- if there's no balance sheet item, then it is no longer in useful life 469 local remainingUsefulLife = 0; 470 471 if asset ~= nil and asset.age ~= nil then 472 -- a placeable vehicle that has been bought and sold today shall not deduct any money 473 if self.dataTable.isPlaceable and asset.age == 0 then 474 return self.dataTable.price; 475 end; 476 477 remainingUsefulLife = map(0, self.dataTable.depreciationPeriod, 1, 0, asset.age); 478 end; 479 480 -- use sell price of data table if customizable 481 local acquisitionPrice = self.dataTable.price; 482 483 return map(1, 0, 0.8*acquisitionPrice, 0.2*acquisitionPrice, clamp01(remainingUsefulLife)); 484 end;
Vehicle:sell()
Sells the vehicle, pays the money over to the player and destroys the vehicle.
487 function Vehicle:sell() 488 assert(g_isMaster, "Selling is only allowed on servers"); 489 490 -- determine the residual value 491 local sellValue = self:getSellValue(); 492 493 -- get some money 494 g_scenario.accounting:sellAsset(self.balanceSheetId, sellValue); 495 496 self:callVehicleScripts("onSell"); 497 498 -- next step: trigger destroying (this will also call destroy) 499 self:onDestroy(); 500 return sellValue; 501 end;
Vehicle:spawnAt(x,y,z, rx,ry,rz)
Teleports the vehicle to the position x,y,z
(all float) with rotation rx,ry,rz
(all float).
504 function Vehicle:spawnAt(x,y,z, rx,ry,rz) 505 -- just apply this position to our main component 506 -- any other component has to be adjusted automatically by joints 507 -- set the main component to kinematic 508 if Rigidbody.isRigidbody(self.mainId) then 509 self:setVehicleKinematic(true); 510 end; 511 512 setWorldPosition(self.mainId, x,y,z); 513 setWorldRotation(self.mainId, rx,ry,rz); 514 515 if g_isMaster and not self.dataTable.isKinematic then 516 -- block vehicle for 0.5 seconds of physics simulation 517 self.resetKinematic = 0.5; 518 end; 519 520 -- set the colors of the vehicle 521 self:applyVehicleColors(self.customColors); 522 end;
Vehicle:reset()
Resets the vehicle to a reset position returned by VehicleManager:findPosition
. View VehicleManager for more details.
525 function Vehicle:reset() 526 -- ask for a place to reset 527 local x,y,z,ry = VehicleManager:findPosition(self.dataTable.width, self.dataTable.length); 528 529 if x ~= nil then 530 -- call scripts 531 self:callVehicleScripts("onReset"); 532 533 self:spawnAt(x,y,z, 0,ry,0); 534 return true; 535 end; 536 537 return false; 538 end;
Vehicle:loadComponents(dataTable)
Loads all components if the vehicle consists of multiple components. Otherwise, it sets up the main component. This is useful for some special vehicles such as the Marmotta Vattenmask.
541 function Vehicle:loadComponents(dataTable) 542 if g_isClient then 543 self.canInterpolate = false; 544 self.interpolationWeight = 2; 545 self.interpolationDuration = 1; 546 end; 547 548 self.components = {}; 549 550 local dataTable_components = dataTable.components; 551 if dataTable_components == nil then 552 -- add some fake data if this section is empty in the vehicle's data table 553 dataTable_components = { 554 { 555 index = "", 556 massCenter = dataTable.massCenter, 557 }, 558 }; 559 end; 560 561 -- register all components 562 for k, v in ipairs(dataTable_components) do 563 local id = getChild(self.id, v.index or ""); 564 if not self.isDummy then 565 VehicleManager:registerVehicleRigidBody(self, id); 566 end; 567 568 local component = { 569 id = id, 570 }; 571 572 if g_isClient then 573 component.lastPosition = Vector3.zero:clone(); 574 component.currentPosition = Vector3.zero:clone(); 575 component.targetPosition = Vector3.zero:clone(); 576 577 -- use quaternions to avoid Gimbal Lock 578 component.lastRotation = Quaternion.zero:clone(); 579 component.currentRotation = Quaternion.zero:clone(); 580 component.targetRotation = Quaternion.zero:clone(); 581 582 -- remove rigidbody component! 583 if Rigidbody.isRigidbody(component.id) then 584 Rigidbody.setIsKinematic(component.id, true); 585 --Rigidbody.removeRigidbody(component.id); 586 end; 587 588 elseif g_isServer then 589 component.lastSentPosition = Vector3.zero:clone(); 590 component.lastSentRotation = Vector3.zero:clone(); 591 end; 592 593 table.insert(self.components, component); 594 595 if self.mainId == nil then 596 -- first component is always main component 597 self.mainId = id; 598 end; 599 600 if v.massCenter ~= nil and Rigidbody.isRigidbody(id) then 601 Rigidbody.setMassCenter(id, v.massCenter.x, v.massCenter.y, v.massCenter.z); 602 end; 603 end; 604 605 -- add collision ignore pairs 606 if dataTable.ignoreCollisions ~= nil then 607 for k, v in pairs(dataTable.ignoreCollisions) do 608 local componentA = getChild(self.id, v[1]); 609 local componentB = getChild(self.id, v[2]); 610 611 if componentA ~= nil and componentB ~= nil then 612 if Rigidbody.hasCollider(componentA) and Rigidbody.hasCollider(componentB) then 613 Physics.ignoreCollision(componentA, componentB, true); 614 615 elseif Rigidbody.hasCollider(componentA) then 616 for k1, child in getChildren(componentB) do 617 if Rigidbody.hasCollider(child) then 618 Physics.ignoreCollision(componentA, child, true); 619 end 620 end; 621 622 elseif Rigidbody.hasCollider(componentB) then 623 for k1, child in getChildren(componentA) do 624 if Rigidbody.hasCollider(child) then 625 Physics.ignoreCollision(componentB, child, true); 626 end 627 end; 628 629 else 630 for k1, child1 in getChildren(componentA) do 631 if Rigidbody.hasCollider(child1) then 632 for k2, child2 in getChildren(componentB) do 633 if Rigidbody.hasCollider(child2) then 634 Physics.ignoreCollision(child1, child2, true); 635 end 636 end; 637 end 638 end; 639 end; 640 end; 641 end; 642 end; 643 644 -- let's go on to joints 645 if dataTable.componentJoints ~= nil then 646 self.componentJoints = {}; 647 648 for k, v in pairs(dataTable.componentJoints) do 649 local component = self.components[v.component or 1]; 650 local attachTo = self.components[v.attachTo or 1]; 651 652 local attachTarget = 0; 653 if v.attachPosition ~= nil then 654 attachTarget = getChild(self.id, v.attachPosition); 655 end; 656 local attachAnchor = 0; 657 if v.attachAnchor ~= nil then 658 attachAnchor = getChild(self.id, v.attachAnchor); 659 end; 660 661 if component == nil then 662 print("Error while loading vehicle type '" .. tostring(self.vehicleType) .. "': Invalid joint component name '" .. tostring(v.component) .. "' (component). Make sure this vehicle component actually exists."); 663 elseif attachTo == nil then 664 print("Error while loading vehicle type '" .. tostring(self.vehicleType) .. "': Invalid joint component name '" .. tostring(v.attachTo) .. "' (attachTo). Make sure this vehicle component actually exists."); 665 elseif v.attachPosition ~= nil and attachTarget == 0 then 666 print("Error while loading vehicle type '" .. tostring(self.vehicleType) .. "': Failed to index joint position index '" .. tostring(v.attachPosition) .. "'. Make sure this object actually exists."); 667 elseif v.attachAnchor ~= nil and attachAnchor == 0 then 668 print("Error while loading vehicle type '" .. tostring(self.vehicleType) .. "': Failed to index joint anchor index '" .. tostring(v.attachAnchor) .. "'. Make sure this object actually exists."); 669 elseif component == attachTo then 670 print("Error while loading vehicle type '" .. tostring(self.vehicleType) .. "': Cannot connect a vehicle component to itself using a joint. Make sure there is no joint connecting to itself (components '" .. tostring(v.component) .. "' and '" .. tostring(v.attachTo) .. "')."); 671 else 672 -- alright, we may create this joint 673 local joint = {}; 674 joint.id = Joint.addJoint(component.id, attachTo.id, v.axis, attachTarget or 0, attachAnchor or 0); 675 end; 676 end; 677 end; 678 end;
Vehicle:loadAxles(dataTable)
Loads the axles from the configuration table, and calls the function customLoadAxle()
for all vehicle scripts. An example for a use is VehicleMotor:customLoadAxle()
(see VehicleMotor)
681 function Vehicle:loadAxles(dataTable) 682 self.axles = {}; 683 684 if dataTable.axles == nil then 685 return; 686 end; 687 688 local axleCount = #dataTable.axles; 689 690 for i, v in pairs(dataTable.axles) do 691 local leftWheelIndex = v.leftWheelIndex or ""; 692 local rightWheelIndex = v.rightWheelIndex or ""; 693 694 -- !!TODO!! check whether these are existing 695 local leftWheelId = getChild(self.id, leftWheelIndex); 696 local rightWheelId = getChild(self.id, rightWheelIndex); 697 698 local axle = self:newAxle(leftWheelId, rightWheelId); 699 self.axles[i] = axle; 700 701 -- now fill it up with some data 702 axle.motorGear = v.motor or axle.motorGear; -- unused as long as VehicleMotor script is not invoked 703 axle.brake = v.brake or false; 704 axle.steeringScale = v.steeringScale or axle.steeringScale; 705 706 if v.leftWheelCaliper ~= nil then 707 axle.leftWheelCaliper = getChild(self.id, v.leftWheelCaliper); 708 end; 709 if v.rightWheelCaliper ~= nil then 710 axle.rightWheelCaliper = getChild(self.id, v.rightWheelCaliper); 711 end; 712 713 if g_isClient then 714 axle.lastClientRpmLeft = 0; 715 axle.lastClientRpmRight = 0; 716 axle.targetClientRpmLeft = 0; 717 axle.targetClientRpmRight = 0; 718 axle.clientRpmLeft = 0; 719 axle.clientRpmRight = 0; 720 721 axle.lastRotationLeft = 0; 722 axle.lastRotationRight = 0; 723 end; 724 725 self:callVehicleScripts("customLoadAxle", axle, v); 726 end; 727 end;
Vehicle:newAxle(leftId, rightId)
Creates a new axle from the specified transform ids leftId
and rightId
(both int) and returns it.
730 function Vehicle:newAxle(leftId, rightId) 731 local axle = {}; 732 axle.leftId = leftId; 733 axle.rightId = rightId; 734 axle.leftWheel = Wheel.getWheelId(leftId); 735 axle.rightWheel = Wheel.getWheelId(rightId); 736 737 if getNumOfChildren(leftId) > 0 then axle.leftVisual = getChildAt(leftId, 0); end; 738 if getNumOfChildren(rightId) > 0 then axle.rightVisual = getChildAt(rightId, 0); end; 739 740 axle.motorGear = 0; -- 0: no power attached to this wheel, 1.0: all motor power to there 741 axle.brake = true; -- shall the brake be applied to this wheel? 742 axle.steeringScale = 0; -- coefficient defining steer angle min/max 743 return axle; 744 end; 745
Vehicle:getIsActive()
Returns whether the vehicle is active. Use this for all code blocks that shall be active when the vehicle shall be updated (e.g. lowering/lifting a snow groomer).
748 function Vehicle:getIsActive() 749 -- isActive gets true as soon as anybody has entered the vehicle 750 return self.currentUser ~= nil; 751 end; 752 753 function Vehicle:getIsLocalPlayerEntered(includePassengers) 754 if self.currentUser == g_scenario.player then 755 return true; 756 end; 757 758 if includePassengers then 759 return self:getIsEnteredAsPassenger(g_scenario.player); 760 end; 761 end;
Vehicle:getIsInputActive()
Returns whether scripts on this vehicle may handle input. ALWAYS make sure to call if self:getIsInputActive() then
before any input queries. Besides some rare exceptions, input keys should never be active if Vehicle:getIsInputActive()
could return false.
764 function Vehicle:getIsInputActive() 765 return g_scenario ~= nil and self.currentUser == g_scenario.player and not EscapeMenu.isActive and not g_GUI:getIsKeyInputForGUI(); 766 end;
Vehicle:getIsGUIActive()
Returns whether GUI elements regarding this vehicle are active and shall be updated. Currently, this is always true when the vehicle is entered, but this could change in the future (e.g. if we were to introduce new concepts for the overview menu).
769 function Vehicle:getIsGUIActive() 770 return g_scenario ~= nil and self.currentUser == g_scenario.player and self.vehicleHUD ~= nil; 771 end;
Vehicle:getIsColliding()
Returns whether there is a collision taking place between the crashbox and some external objects
774 function Vehicle:getIsColliding() 775 -- consider appropriate layers only 776 local mask = -(2^30+2^19+2^23+2^2+1); 777 if self.hasCrashBoxImplemented then 778 for k,v in pairs(self.crashbox) do 779 local alpha, beta, gamma = getRotation(self.components[v.componentnumber].id); 780 if Utils.checkBox(VectorUtils.getWorldPosition(v.origin), Vector3:new(v.dimensions.x, v.dimensions.y, v.dimensions.z), beta, mask) then 781 return true; 782 end; 783 end; 784 return false; 785 else 786 --print("getIsColliding() returns nil because Crashbox is not fully implemented, dimensions or crashbox origin is missing"); 787 return nil; 788 end; 789 end; 790
Vehicle:update(dt)
This handles a whole lot of things that need to be checked every frame:
- Checks whether the vehicle is active
- Handles input (both for separate throttle/brake and for combined controls) for the commonly used statements
self.throttleValue
,self.brakeValue
(both floats, range 0-1),self.steerValue
(float, range -1 to 1), andself.parkingBrake
(0 or 1) - Visual update of steering wheel and wheels.
796 function Vehicle:update(dt) 797 798 if self.resetKinematic ~= nil then 799 self.resetKinematic = self.resetKinematic - dt; 800 801 if self.resetKinematic <= 0 then 802 self.resetKinematic = nil; 803 if g_isMaster and not self.dataTable.isKinematic and Rigidbody.isRigidbody(self.mainId) then 804 self:setVehicleKinematic(false); 805 end; 806 end; 807 end; 808 809 self.isActive = self:getIsActive(); 810 811 if self:getIsGUIActive() then 812 -- update UI 813 self.vehicleHUD:setParkingBrake(self.parkingBrake > 0.5); 814 815 -- turn indicators are controlled in VehicleLighting.lua 816 end; 817 818 if self:getIsInputActive() then 819 820 local throttleValueInverter = 1; 821 if self:getShallInvertThrottle() then 822 throttleValueInverter = throttleValueInverter * -1; 823 end; 824 825 if GameplaySettings.throttleBrakeSeparate then 826 827 self.throttleValue = InputMapper:getAxis(InputMapper.Axis_Vehicle_Throttle, nil, true); 828 self.brakeValue = InputMapper:getAxis(InputMapper.Axis_Vehicle_Brake, nil, true); 829 830 if self:getShallInvertThrottle() and GameplaySettings.automaticDirectionChange then 831 self.throttleValue, self.brakeValue = self.brakeValue, self.throttleValue; 832 end; 833 else 834 local inputValue = InputMapper:getAxis(InputMapper.Axis_Vehicle_Throttle_Brake_Combined, nil, true) * throttleValueInverter; 835 836 if inputValue > 0 then 837 self.throttleValue = inputValue; 838 self.brakeValue = 0; 839 if math.abs(self.throttleValue) < 0.05 then 840 self.throttleValue = 0; 841 end 842 else 843 self.throttleValue = 0; 844 self.brakeValue = -inputValue; 845 if math.abs(self.brakeValue) < 0.05 then 846 self.brakeValue = 0; 847 end 848 end; 849 end; 850 851 -- gear switching. Converted from vehicle motor and tradcked vehicle into one function 852 if not GameplaySettings.automaticDirectionChange then 853 854 g_GUI:addKeyHint(InputMapper.Vehicle_InverseDirection, l10n.get("Input_Vehicle_InverseDirection")); 855 856 -- inverse direction 857 if InputMapper:getKeyDown(InputMapper.Vehicle_InverseDirection) then 858 self:setDrivingDirectionInverted(); 859 self:setCruiseControlEnabled(false); 860 end; 861 862 else 863 if not self:getShallInvertThrottle() and self.currentSpeed > -1 and self.currentSpeed < 1 then 864 if self:getIsBrakeActivated() then 865 if self.driveDirectionChangeTimePrev == nil then 866 self.driveDirectionChangeTimePrev = getTime(); 867 else 868 local driveDirectionChangeTime = getTime(); 869 if driveDirectionChangeTime > self.driveDirectionChangeTimePrev + GameplaySettings.driveDirectionChangeTime then 870 self:setDrivingDirectionInverted(); 871 self.driveDirectionChangeTimePrev = nil; 872 end; 873 end; 874 end; 875 end; 876 if self:getShallInvertThrottle() and self.currentSpeed > -1 and self.currentSpeed < 1 then 877 if self:getIsThrottleActivated() then 878 if self.driveDirectionChangeTimePrev == nil then 879 self.driveDirectionChangeTimePrev = getTime(); 880 else 881 local driveDirectionChangeTime = getTime(); 882 if driveDirectionChangeTime > self.driveDirectionChangeTimePrev + GameplaySettings.driveDirectionChangeTime then 883 self:setDrivingDirectionInverted(); 884 self.driveDirectionChangeTimePrev = nil; 885 end; 886 end; 887 end; 888 end; 889 end; 890 891 if InputMapper:getKeyDown(InputMapper.Vehicle_EnableCruiseControl) and self:getIsLocalPlayerEntered() then 892 self:setCruiseControlEnabled(not self.cruiseControlActive); 893 end; 894 895 if self.cruiseControlActive and (self.throttleValue > 0.05 or self.brakeValue > 0.05 or self.parkingBrake > 0.1) then 896 self:setCruiseControlEnabled(false); 897 end; 898 899 900 local steerValue, fixed = InputMapper:getAxis(InputMapper.Axis_Vehicle_Steer, nil, true); 901 self.rawSteerValue = steerValue; 902 self.rawSteerValueFixed = fixed; 903 904 905 if self.parkingBrake > 0.5 then 906 g_GUI:addKeyHint(InputMapper.Vehicle_ParkingBrake, l10n.get("Input_Vehicle_ParkingBrake_release")); 907 end; 908 909 if InputMapper:getKeyDown(InputMapper.Vehicle_ParkingBrake) then 910 self:setParkingBrake(self.parkingBrake ~= 1); 911 912 -- play a sound as feedback 913 self:playOnOffSound(self.parkingBrake == 1, self.parkingBrakeSound1, self.parkingBrakeSound0); 914 end; 915 end; 916 917 if self.isActive then 918 919 -- just for debug, turnon if you want to know if vehicle is colliding 920 --local coll = self:getIsColliding(); 921 --print(tostring(coll)); 922 923 -- update steering 924 -- directly write the analog value, but smoothen out digital values 925 if self.rawSteerValueFixed then 926 self.steerValue = self.rawSteerValue; 927 928 elseif self.rawSteerValue ~= self.steerValue then 929 930 local returnSpeed = self.steeringSteerSpeed; 931 932 if self.rawSteerValue == 0 or (self.steerValue >= 0) ~= (self.rawSteerValue >= 0) and self.steeringReturnSpeed > self.steeringSteerSpeed then 933 returnSpeed = self.steeringReturnSpeed; 934 end; 935 936 local speedCoeff = 1; 937 if self.currentSpeed ~= nil then 938 speedCoeff = math.abs(1 - clamp(self.currentSpeed * self.steeringSlowdownCoeff, 0, self.steeringMaxSlowdown)); 939 end; 940 941 if self.steerValue < self.rawSteerValue then 942 self.steerValue = math.min(self.steerValue + returnSpeed * speedCoeff * dt, self.rawSteerValue); 943 944 else 945 self.steerValue = math.max(self.steerValue - returnSpeed * speedCoeff *dt, self.rawSteerValue); 946 end; 947 end; 948 949 -- calculate the simulated drag according the current speed and the defined speed ranges in the datatable 950 if g_isMaster and self.dragControlMinSpeed ~= nil and self.dragControlMaxSpeed ~= nil and self.dragControlMinDrag ~= nil and self.dragControlMaxDrag ~= nil then 951 local drag = 0; 952 if self.currentSpeed < self.dragControlMinSpeed then 953 drag = self.dragControlMinDrag; 954 elseif self.currentSpeed > self.dragControlMaxSpeed then 955 drag = self.dragControlMaxDrag; 956 else 957 drag = map(0, self.dragControlMaxSpeed - self.dragControlMinSpeed, self.dragControlMaxDrag, self.dragControlMinDrag, self.dragControlMaxSpeed - self.currentSpeed); 958 end; 959 -- for vehicles with more than one component 960 for k, v in pairs(self.components) do 961 if Rigidbody.isRigidbody(v.id) then 962 Rigidbody.setDrag(v.id, drag); 963 end; 964 end; 965 end; 966 end; 967 968 if self.isActive --[[or math.abs(self.currentSpeed) > 1.5 969 then 970 971 -- update wheel visuals 972 for n, axle in pairs(self.axles) do 973 self:wheelPositionToVisual(axle.leftWheel, axle.leftVisual, axle.leftWheelCaliper); 974 self:wheelPositionToVisual(axle.rightWheel, axle.rightVisual, axle.rightWheelCaliper); 975 976 -- manually add some wheel rotation on clients 977 if g_isClient then 978 axle.clientRpmLeft = 0.8 * (axle.clientRpmLeft or 0) + 0.2 * (axle.targetClientRpmLeft or 0); 979 axle.clientRpmRight = 0.8 * (axle.clientRpmRight or 0) + 0.2 * (axle.targetClientRpmRight or 0); 980 981 -- times 6 because we want the result in degrees - rpm to sec^-1 is / 60, but sec^-1 to degree^-1 is * 360 => *6 in total 982 axle.lastRotationLeft = (axle.lastRotationLeft or 0) + axle.clientRpmLeft * 6 * dt; 983 axle.lastRotationRight = (axle.lastRotationRight or 0) + axle.clientRpmRight * 6 * dt; 984 985 if axle.leftVisual ~= nil then 986 rotate(axle.leftVisual, axle.lastRotationLeft, 0,0, false); 987 end; 988 if axle.rightVisual ~= nil then 989 rotate(axle.rightVisual, axle.lastRotationRight, 0,0, false); 990 end; 991 end; 992 end; 993 994 -- update steering wheel 995 if self.steeringWheelId ~= nil then 996 local steeringWheelAngle = self:getMaxVisualSteerAngle(); 997 998 if self.steeringWheelAxle == 1 then 999 setRotationX(self.steeringWheelId, steeringWheelAngle * self.steerValue); 1000 1001 elseif self.steeringWheelAxle == 2 then 1002 setRotationY(self.steeringWheelId, steeringWheelAngle * self.steerValue); 1003 1004 else 1005 setRotationZ(self.steeringWheelId, steeringWheelAngle * self.steerValue); 1006 end; 1007 end; 1008 if self.steeringAnimationId ~= nil then 1009 Animation.sampleTime(self.steeringAnimationId, self.steeringAnimationLength * 0.5 * (1 + self.steerValue)); 1010 end; 1011 end; 1012 1013 self:callVehicleScripts("update", dt); 1014 end; 1015
Vehicle:fixedUpdate(dt)
This is called every time the physics system is updated. This can happen more than once per frame. Please make sure that your vehicle scripts therefore do not waste any performance in here.
1017 function Vehicle:fixedUpdate(dt) 1018 -- apply wheel torques and steering angles 1019 if g_isMaster then 1020 if not self.customWheelScriptActive and #self.axles > 0 then 1021 self:wheelsUpdate(self:getCurrentMotorTorque(), self:getCurrentBrakeTorque(), self:getCurrentSteerAngle()); 1022 end; 1023 end; 1024 1025 -- do the client-side interpolation 1026 if g_isClient then 1027 if self.canInterpolate then 1028 local maxExtrapolation = 1.3; 1029 self.interpolationWeight = self.interpolationWeight + dt / self.interpolationDuration; 1030 1031 if self.interpolationWeight > maxExtrapolation then 1032 self.interpolationWeight = maxExtrapolation; 1033 self.canInterpolate = false; 1034 end; 1035 1036 for k, component in pairs(self.components) do 1037 -- interpolate position and rotation 1038 component.currentPosition = Vector3.lerp(component.lastPosition, component.targetPosition, self.interpolationWeight); 1039 component.currentRotation = Quaternion.lerpShortestPath(component.lastRotation, component.targetRotation, self.interpolationWeight); 1040 1041 -- apply them 1042 VectorUtils.setPosition(component.id, component.currentPosition); 1043 VectorUtils.setRotationQuaternion(component.id, component.currentRotation); 1044 end; 1045 1046 else 1047 -- cannot interpolate any more => keep position and rotation fixed 1048 for k, component in pairs(self.components) do 1049 VectorUtils.setPosition(component.id, component.currentPosition); 1050 VectorUtils.setRotationQuaternion(component.id, component.currentRotation); 1051 end; 1052 end; 1053 end; 1054 1055 self:callVehicleScripts("fixedUpdate", dt); 1056 1057 -- set dirty whenever the vehicle is active 1058 self:setDirty(self:getIsActive()); 1059 1060 if g_isServer and not self.isDirty then 1061 -- not yet dirty => check whether we need to sync the position 1062 for n, component in pairs(self.components) do 1063 local px,py,pz = getWorldPosition(component.id); 1064 local qx,qy,qz,qw = getWorldRotationQuaternion(component.id); 1065 1066 local lastPos, lastRot = component.lastSentPosition, component.lastSentRotation; 1067 if 1068 math.abs(px - lastPos.x) > 0.1 or 1069 math.abs(py - lastPos.y) > 0.08 or 1070 math.abs(pz - lastPos.z) > 0.1 or 1071 1072 math.abs(qx - lastRot.x) > 0.02 or 1073 math.abs(qy - lastRot.y) > 0.02 or 1074 math.abs(qz - lastRot.z) > 0.02 or 1075 math.abs(qw - lastRot.w) > 0.02 1076 then 1077 -- mark vehicle as dirty 1078 self:setDirty(true); 1079 break; 1080 end; 1081 end; 1082 end; 1083 end;
Vehicle:wheelsUpdate(motorTorque, brakeTorque, steerAngle)
Physics update for the vehicle's wheels.
1086 function Vehicle:wheelsUpdate(motorTorque, brakeTorque, steerAngle) 1087 for n, axle in pairs(self.axles) do 1088 -- apply wheel torques 1089 Wheel.setTorques(axle.leftWheel, motorTorque * axle.motorGear, axle.brake and brakeTorque or 0); 1090 Wheel.setTorques(axle.rightWheel, motorTorque * axle.motorGear, axle.brake and brakeTorque or 0); 1091 1092 -- and now steering 1093 if axle.steeringScale ~= 0 then 1094 Wheel.setSteerAngle(axle.leftWheel, steerAngle * axle.steeringScale); 1095 Wheel.setSteerAngle(axle.rightWheel, steerAngle * axle.steeringScale); 1096 end; 1097 end; 1098 end;
Vehicle:wheelPositionToVisual(wheelId, visualWheel, caliperId)
Applies the wheel's position as determined by physics to the wheel's visual. If existing, the caliper will also be adjusted accordingly.
1101 function Vehicle:wheelPositionToVisual(wheelId, visualWheel, caliperId) 1102 if visualWheel == 0 or visualWheel == nil then return; end; 1103 1104 Wheel.applyPose(wheelId, visualWheel); 1105 1106 if caliperId ~= nil and caliperId ~= 0 then 1107 Wheel.applyCaliperPose(wheelId, caliperId); 1108 end; 1109 end;
Vehicle:getMayEnter(player)
Returns whether the player player
may enter the vehicle on the driver seat.
1112 function Vehicle:getMayEnter(player) 1113 -- vehicle must have a driver seat and noone else can sit in there 1114 return self.driverSeat ~= nil and (self.currentUser == nil or self.currentUser == player); 1115 end;
Vehicle:getMayEnterOnAnySeat(player)
Returns whether the player player
may enter the vehicle on any seat (not just on the driver seat).
1118 function Vehicle:getMayEnterOnAnySeat(player) 1119 for k, seat in ipairs(self.seatsById) do 1120 if seat:getMayEnter(player) then 1121 return true; 1122 end; 1123 end; 1124 1125 return false; 1126 end;
Vehicle:getIsEntered(player)
Returns whether the player player
is entered in this vehicle.
1129 function Vehicle:getIsEntered(player) 1130 return self.driverSeat ~= nil and self.currentUser == player; 1131 end;
Vehicle:getIsEnteredAsPassenger(player)
Returns whether the player player
is entered as passenger.
1134 function Vehicle:getIsEnteredAsPassenger(player) 1135 if self.seatsById == nil then 1136 return false; 1137 end; 1138 1139 for k, seat in ipairs(self.seatsById) do 1140 if seat ~= self.driverSeat and seat.currentUser == player then 1141 return true; 1142 end; 1143 end; 1144 return false; 1145 end;
Vehicle:getShallInvertThrottle()
Returns whether the throttle input shall be inverted, i.e. whether the W and S key shall be inverted.
1148 function Vehicle:getShallInvertThrottle() 1149 return false; 1150 end;
Vehicle:getIsThrottleActivated()
Returns whether motor throttle is currently activated (after considering inverted throttle).
1153 function Vehicle:getIsThrottleActivated() 1154 return self.throttleValue > 0.05 or (self.brakeValue > 0.05 and self:getShallInvertThrottle()); 1155 end;
Vehicle:getIsBrakeActivated()
Returns whether the brake is currently activated (after considering inverted throttle).
1158 function Vehicle:getIsBrakeActivated() 1159 return self.brakeValue > 0.05 or (self.throttleValue > 0.05 and self:getShallInvertThrottle()); 1160 end; 1161
Vehicle:setDrivingDirectionInverted()
Inverts the current driving direction.
1165 function Vehicle:setDrivingDirectionInverted() 1166 -- call vehicleMotor or trackedVehicle function 1167 end; 1168
Vehicle:handleEnterRequest(player)
Master (= server or singleplayer) function if a client wants to enter a vehicle.
1171 function Vehicle:handleEnterRequest(player) 1172 assert(player ~= nil, "Vehicle:handleEnterRequest may not be called with nil player"); 1173 assert(g_isMaster, "Function may not be called on clients!"); 1174 1175 -- handle event from client 1176 1177 if not self:getMayEnter(player) then 1178 return false; 1179 end; 1180 1181 -- nobody is currently sitting in this vehicle => client may join 1182 self:setCurrentUser(player); 1183 1184 -- BUT: the player controller itself is no longer active 1185 --player:setCurrentUser(nil); 1186 1187 return true; 1188 end;
Vehicle:tryEnter(player)
Master (= server or singleplayer) function used for vehicle switching. The function tries to let player
enter this vehicle and returns true
if successful, otherwise false
.
1191 function Vehicle:tryEnter(player) 1192 assert(g_isMaster, "Function may not be called on clients!"); 1193 1194 if self:getMayEnter(player) then 1195 self:handleEnterRequest(player); 1196 return true; 1197 end; 1198 1199 for k, seat in ipairs(self.seatsById) do 1200 if seat ~= self.driverSeat and seat:getMayEnter(player) then 1201 -- we may enter! 1202 seat:enterRequest(player); 1203 return true; 1204 end; 1205 end; 1206 return false; 1207 end; 1208
Vehicle:handleLeave(player, isEnteringOtherVehicle)
Called whenever the player wants to leave this vehicle. If called on a client, this will call the exact same function on the server with the same arguments.
1211 function Vehicle:handleLeave(player, isEnteringOtherVehicle) 1212 --assert(player == self.currentUser, "Cannot handle leaving of a player that is not sitting in here"); 1213 if g_isClient then 1214 -- on client: inform server that we've left 1215 EventLeaveVehicle:send(self, isEnteringOtherVehicle); 1216 1217 else 1218 -- on server: just apply the change 1219 --self:setCurrentUser(nil); 1220 assert(player, "Must be called with a valid player on it"); 1221 -- TODO check if that player is actually entered in here 1222 1223 -- reenable player if he/she is not entering another vehicle 1224 if not isEnteringOtherVehicle then 1225 player:setCurrentUser(player); 1226 end; 1227 end; 1228 end;
Vehicle:setCurrentUser(newPlayer, noEvent)
This function is a core part of the network entity system as it handles entering/leaving vehicles as special case of setting an entity's user.
1231 function Vehicle:setCurrentUser(newPlayer, noEvent) 1232 local oldPlayer = self.currentUser; 1233 1234 if oldPlayer ~= nil and newPlayer ~= nil then 1235 if oldPlayer == newPlayer then 1236 -- same player => don't do anything 1237 return; 1238 end; 1239 1240 -- first kick out the old player if this is on a client 1241 -- on a server its not allowed though 1242 if g_isClient then 1243 self:setCurrentUser(nil, true); 1244 1245 else 1246 self:setCurrentUser(nil); 1247 error("currentUser must be nil before assigning a new user on a server"); 1248 end; 1249 end; 1250 1251 if g_isClient and self.currentUser == newPlayer then 1252 -- we already have the correct state (i.e. when leaving the vehicle) 1253 return; 1254 end; 1255 1256 local player = newPlayer or oldPlayer; 1257 local isEntering = newPlayer ~= nil; 1258 1259 Vehicle:parentClass().setCurrentUser(self, newPlayer, noEvent); 1260 1261 if oldPlayer ~= nil then 1262 self:onLeave(oldPlayer, oldPlayer:getIsLocalPlayer()); 1263 end; 1264 1265 if newPlayer ~= nil then 1266 self:onEnter(newPlayer, newPlayer:getIsLocalPlayer()); 1267 end; 1268 end; 1269
Vehicle:onEnter(player, isLocalPlayer)
This calls onEnter
for all vehicle scripts and handles parking brake and HUD.
1272 function Vehicle:onEnter(player, isLocalPlayer) 1273 self.anyPlayerEntered = true; 1274 1275 if isLocalPlayer then 1276 -- first enter the vehicle (and leave the currently entered one) 1277 VehicleManager:onEnterVehicle(self); 1278 end; 1279 1280 -- then call onEnter (do this always) 1281 self:callVehicleScripts("onEnter", player, isLocalPlayer); 1282 1283 if isLocalPlayer then 1284 if GameplaySettings.autoReleaseParkingBrake then 1285 self:setParkingBrake(false); 1286 end; 1287 1288 self.vehicleHUD = g_GUI:showHUD(self.vehicleHUDClass); 1289 self:callVehicleScripts("onHUDActivate"); 1290 end; 1291 end;
Vehicle:onLeave(player, isLocalPlayer)
This calls onLeave
for all vehicle scripts and resets several variables to their default values.
1294 function Vehicle:onLeave(player, isLocalPlayer) 1295 self.anyPlayerEntered = false; 1296 self:callVehicleScripts("onLeave", player, isLocalPlayer); 1297 1298 if isLocalPlayer then 1299 VehicleManager:onLeaveVehicle(self); 1300 1301 self:callVehicleScripts("onHUDDeactivate"); 1302 1303 if self.vehicleHUD ~= nil then 1304 -- destroy reference to hud before calling close (important in case close throws an error) 1305 local hud = self.vehicleHUD; 1306 self.vehicleHUD = nil; 1307 hud:close(); 1308 end; 1309 end; 1310 if g_isMaster then 1311 -- reset throttle and set the parking brake 1312 self.throttleValue = 0; 1313 self.brakeValue = 0; 1314 self:setParkingBrake(true); 1315 self:setCruiseControlEnabled(false); 1316 end; 1317 end;
Vehicle:getMinimapPositionAndRotation()
This function is called by MinimapHUD. It returns where on the map the vehicle is currently located at and in which direction it is oriented.
1320 function Vehicle:getMinimapPositionAndRotation() 1321 local x,y,z = getWorldPosition(self.mainId); 1322 local _a,ry,_b = getWorldRotation(self.mainId); 1323 return x,y,z, ry; 1324 end; 1325
Vehicle:getCurrentMotorTorque()
This is a dummy function that is usually replaced by VehicleMotor.
1328 function Vehicle:getCurrentMotorTorque() 1329 -- to be replaced by a motorising vehicle script 1330 return 0; 1331 end;
Vehicle:getCurrentBrakeTorque()
Returns the current brake torque in Nm per wheel.
1334 function Vehicle:getCurrentBrakeTorque() 1335 return self.maxBrakeTorque * self.brakeValue + self.parkingBrakeTorque * self.parkingBrake; 1336 end;
Vehicle:getCurrentSteerAngle()
Returns the steering angle in degrees.
1339 function Vehicle:getCurrentSteerAngle() 1340 return self.maxSteerAngle * self.steerValue; 1341 end; 1342
Vehicle:getMaxVisualSteerAngle()
Returns the maximum steering angle to be displayed on the driver's steering wheel in degrees.
1345 function Vehicle:getMaxVisualSteerAngle() 1346 if self:getIsLocalPlayerEntered() and self.driverSeat.isInterieurCamera then 1347 return self.steeringWheelAngle; 1348 end; 1349 return self.steeringWheelAngleExterior; 1350 end; 1351
Vehicle:setGear(value)
Shifts the gear box to gear number value
(ranging from 1 to the number of gears).
1354 function Vehicle:setGear(value) 1355 self.currentGear = clamp(value, 1, #self.gears); 1356 1357 -- avoid automatic drive interruption 1358 self.gearSwitchTimer = -self.gearSwitchPace; 1359 end;
Vehicle:setParkingBrake(value)
Sets the vehicle's parking brake, depending on value
(bool).
1362 function Vehicle:setParkingBrake(value) 1363 self.parkingBrake = value and 1 or 0; 1364 end;
Vehicle:onDestroy()
Starts destroying the vehicle. onDestroy is the function to call when you want to destroy a vehicle, destroy
will only be called as a result of it.
1367 function Vehicle:onDestroy() 1368 -- leave if entered 1369 if self:getIsActive() then 1370 self:setCurrentUser(nil); 1371 end; 1372 1373 -- kick out all passengers 1374 if self.seatsById ~= nil then 1375 for k, seat in ipairs(self.seatsById) do 1376 if seat ~= self.driverSeat and seat.currentUser ~= nil then 1377 seat:leaveRequest(seat.currentUser, false); 1378 end; 1379 end; 1380 end; 1381 1382 Vehicle:parentClass().onDestroy(self); 1383 end;
Vehicle:destroy()
Destroys the vehicle, e.g. if it is sold or if the game is closed.
1386 function Vehicle:destroy() 1387 Vehicle:parentClass().destroy(self); 1388 1389 -- if vehicle is dummy, skip unregister 1390 if not self.isDummy then 1391 VehicleManager:unregisterVehicle(self); 1392 1393 for k, v in pairs(self.components) do 1394 VehicleManager:unregisterVehicleRigidBody(v); 1395 end; 1396 end; 1397 self:callVehicleScripts("destroy"); 1398 1399 delete(self.id); 1400 end;
Vehicle:setVehicleKinematic(isKinematic)
Sets the vehicle's kinematic value to false or true.
1403 function Vehicle:setVehicleKinematic(isKinematic) 1404 -- if vehicle is dummy, skip unregister 1405 Rigidbody.setIsKinematic(self.mainId, isKinematic); 1406 1407 self:callVehicleScripts("setVehicleKinematic", isKinematic); 1408 end;
Vehicle:applyVehicleColors(customColors)
Sets the color of the different vehicle parts.
1411 function Vehicle:applyVehicleColors(customColors) 1412 -- set custom colors 1413 if customColors ~= nil then 1414 -- go through the different color channels 1415 for k, color in pairs(customColors) do 1416 local channel = color.colorChannel; 1417 -- convert color from hex to rgb 1418 local r, g, b = EscapeMenu:unpackHexColor(color.value); 1419 if color.vehicleParts ~= nil then 1420 for k1, vehiclePart in pairs(color.vehicleParts) do 1421 -- iterate through all objects the color should be applied to! 1422 local index = vehiclePart; 1423 local materialSlot = color.materialSlot; 1424 1425 -- the entry into "vehicleParts" could well be a table (and this way specify its own material slot) 1426 if type(vehiclePart) == "table" then 1427 index = vehiclePart.index or vehiclePart[1]; 1428 materialSlot = vehiclePart.colorSlot or vehiclePart[2]; 1429 end; 1430 1431 -- index object & create a new property block for it 1432 -- note that materialSlot may well also be nil! 1433 local objectToRecolor = getChild(self.id, index); 1434 local propertyBlock = Material.newPropertyBlock(objectToRecolor, materialSlot); 1435 1436 -- set color of channel, and apply it to the property! 1437 if channel == "" or channel == nil then 1438 channel = "_Color"; 1439 end; 1440 1441 -- finally apply colors 1442 Material.setPropertyBlockColor(propertyBlock, channel, r, g, b, 255); 1443 Material.applyPropertyBlock(objectToRecolor, propertyBlock, materialSlot); 1444 end; 1445 end; 1446 end; 1447 end; 1448 end;
Vehicle:setCruiseControlEnabled(isEnabled, noEvent)
Turns cruise control on or off, depending on isEnabled
. If turned on, setCruiseControlValues
will be called to e.g. store the current motor rpm value.
1451 function Vehicle:setCruiseControlEnabled(isEnabled, noEvent) 1452 self.cruiseControlActive = isEnabled; 1453 if self.vehicleHUD ~= nil then 1454 self.vehicleHUD:setIsCruiseControlOn(self.cruiseControlActive); 1455 end; 1456 1457 if g_isClient and not noEvent then 1458 EventVehicleCruiseControlEnable:send(self, isEnabled); 1459 return; 1460 end; 1461 1462 if self.cruiseControlActive then 1463 self:setCruiseControlValues(); 1464 end; 1465 end;
Vehicle:setCruiseControlValues()
Called whenever the cruise control is engaged. Can be overridden by VehicleScripts such as VehicleMotor.
1468 function Vehicle:setCruiseControlValues() 1469 -- implemented in vehicleMotor 1470 end; 1471
Vehicle:writeResync()
Resynchronizes all variables when a new player joins the game. The data sent by writeResync
will be received by readResync
.
1475 function Vehicle:writeResync() 1476 Vehicle:parentClass().writeResync(self); 1477 1478 local x,y,z = getWorldPosition(self.mainId); 1479 local rx,ry,rz = getWorldRotation(self.mainId); 1480 1481 streamWriteFloat(x); 1482 streamWriteFloat(y); 1483 streamWriteFloat(z); 1484 streamWriteFloat(rx); 1485 streamWriteFloat(ry); 1486 streamWriteFloat(rz); 1487 1488 streamWriteString(self.vehicleType); 1489 streamWriteString(self.dataTable.customizationName or ""); 1490 1491 -- write the colors 1492 if self.customColors ~= nil then 1493 local colorLength = #self.customColors; 1494 streamWriteUInt8(colorLength); 1495 for k,v in ipairs(self.customColors) do 1496 streamWriteString(v.value); 1497 end; 1498 else 1499 -- send length 1500 streamWriteUInt8(0); 1501 end; 1502 1503 self:writeComponentPositions(); 1504 1505 self:callVehicleScripts("writeResync"); 1506 --[[print("Syncing " .. tostring(self.vehicleType)); 1507 local functionName = "writeResync"; 1508 for _, script in pairs(self.vehicleScripts) do 1509 -- ensure its not nil 1510 if script[functionName] ~= nil then 1511 script[functionName](self); 1512 print("Script " .. getGlobalName(script) .. " offset " .. streamGetWriteOffset()); 1513 end; 1514 end; 1515 1516 Vehicle:parentClass().finishWriteResync(self); 1517 1518 -- set as dirty for the next frame 1519 self:setDirty(true); 1520 end;
Vehicle:readResync()
Resynchronizes all variables when a new player joins the game. The data sent by writeResync
will be received by readResync
.
1524 function Vehicle:readResync() 1525 Vehicle:parentClass().readResync(self); 1526 1527 local x,y,z = streamReadFloat(), streamReadFloat(), streamReadFloat(); 1528 local rx,ry,rz = streamReadFloat(), streamReadFloat(), streamReadFloat(); 1529 1530 -- next read the template name 1531 local vehicleType = streamReadString(); 1532 local customizationName = streamReadString(); 1533 1534 local colorLenght = streamReadUInt8(); 1535 local addedColors = nil; 1536 if colorLenght > 0 then 1537 addedColors = {}; 1538 for i = 1, colorLenght, 1 do 1539 addedColors[i] = {value = streamReadString()}; 1540 end; 1541 end; 1542 if customizationName == "" then 1543 customizationName = nil; 1544 end; 1545 local isDummy = false; 1546 1547 local successful, params = VehicleManager:getVehicleParams(vehicleType, customizationName, isDummy, addedColors); 1548 1549 -- if the vehicle was not found, we need to return a nil value 1550 if not successful then 1551 print("Error while resynchronizing vehicles: Vehicle type '" .. tostring(vehicleType) .. "' is apparently used by the host, but not registered on this client."); 1552 return nil; 1553 end; 1554 1555 self:load(unpack(params)); 1556 self:spawnAt(x,y,z, rx,ry,rz); 1557 1558 -- this is only ever on clients 1559 self.interpolationDuration = 1; 1560 self.interpolationWeight = 0; 1561 self.canInterpolate = false; 1562 1563 -- move all components to the correct position 1564 -- and apply target, last AND current position 1565 for k, component in ipairs(self.components) do 1566 component.targetPosition = Vector3:new(streamReadFloat(), streamReadFloat(), streamReadFloat()); 1567 component.targetRotation = Quaternion:new(streamReadFloat(), streamReadFloat(), streamReadFloat(), streamReadFloat()); 1568 1569 component.lastPosition = component.targetPosition; 1570 component.lastRotation = component.targetRotation; 1571 component.currentPosition = component.targetPosition; 1572 component.currentRotation = component.targetRotation; 1573 end; 1574 1575 -- sync all transforms to apply the new position 1576 Physics.syncTransforms(); 1577 1578 self:callVehicleScripts("readResync"); 1579 --[[print("Read Syncing " .. tostring(self.vehicleType)); 1580 local functionName = "readResync"; 1581 for _, script in pairs(self.vehicleScripts) do 1582 -- ensure its not nil 1583 if script[functionName] ~= nil then 1584 script[functionName](self); 1585 print("Script " .. getGlobalName(script) .. " offset " .. streamGetReadOffset()); 1586 end; 1587 end; 1588 1589 Vehicle:parentClass().finishReadResync(self); 1590 end; 1591 1592 function Vehicle:postWriteResync() 1593 assert(g_isServer, "Only allowed on server"); 1594 1595 self:callVehicleScripts("postWriteResync"); 1596 end;
Vehicle:writeUpdate()
Sends a network packet each frame when the vehicle is updated. The data sent by writeUpdate
will be received by readUpdate
.
1600 function Vehicle:writeUpdate() 1601 local isLocalPlayerEntered = self:getIsLocalPlayerEntered(); 1602 1603 if g_isServer then 1604 self:writeComponentPositions(); 1605 self:writeWheelRpms(); 1606 -- write extra information from server to client 1607 streamWriteFloat(self.rawSteerValue); 1608 else 1609 -- write from client -> server 1610 if streamWriteBool(isLocalPlayerEntered) then 1611 -- write input to server 1612 streamWriteFloat(self.throttleValue); 1613 streamWriteFloat(self.brakeValue); 1614 streamWriteFloat(self.rawSteerValue); 1615 streamWriteBool(self.rawSteerValueFixed); 1616 streamWriteBool(self.parkingBrake > 0.5); 1617 end; 1618 end; 1619 1620 self:callVehicleScripts("writeUpdate", isLocalPlayerEntered); 1621 end;
Vehicle:writeComponentPositions()
Writes this vehicle's component positions to the stream.
1625 function Vehicle:writeComponentPositions() 1626 assert(g_isServer, "Only allowed on server"); 1627 1628 for k, component in ipairs(self.components) do 1629 local px,py,pz = getWorldPosition(component.id); 1630 local qx,qy,qz,qw = getWorldRotationQuaternion(component.id); 1631 1632 -- write all of them to stream 1633 streamWriteFloat(px); streamWriteFloat(py); streamWriteFloat(pz); 1634 streamWriteFloat(qx); streamWriteFloat(qy); streamWriteFloat(qz); streamWriteFloat(qw); 1635 1636 component.lastSentPosition.x = px; 1637 component.lastSentPosition.y = py; 1638 component.lastSentPosition.z = pz; 1639 1640 component.lastSentRotation.x = qx; 1641 component.lastSentRotation.y = qy; 1642 component.lastSentRotation.z = qz; 1643 component.lastSentRotation.w = qw; 1644 end; 1645 end;
Vehicle:writeWheelRpms()
Writes this vehicle's wheel rpms to the stream.
1648 function Vehicle:writeWheelRpms() 1649 assert(g_isServer, "Only allowed on server"); 1650 1651 for n, axle in ipairs(self.axles) do 1652 streamWriteFloat(Wheel.getRpm(axle.leftWheel)); 1653 streamWriteFloat(Wheel.getRpm(axle.rightWheel)); 1654 end; 1655 end;
Vehicle:readWheelRpms()
Reads this vehicle's wheel rpms from the stream.
1658 function Vehicle:readWheelRpms() 1659 assert(g_isClient, "Only allowed on client"); 1660 1661 for n, axle in ipairs(self.axles) do 1662 axle.targetClientRpmLeft = streamReadFloat(); 1663 axle.targetClientRpmRight = streamReadFloat(); 1664 end; 1665 end;
Vehicle:readUpdate(connection, networkTime)
Receives a network packet each frame when the vehicle is updated. The data sent by writeUpdate
will be received by readUpdate
.
1669 function Vehicle:readUpdate(connection, networkTime) 1670 local isClientEntered = false; 1671 if g_isClient then 1672 1673 local minDelay = 0.05; 1674 local maxDelay = 0.6; 1675 local expectedDelay = 0.1; 1676 1677 local deltaTime; 1678 1679 if self.lastPacketTime == nil then 1680 deltaTime = NetworkGame.TICK_RATE; 1681 elseif networkTime < self.lastPacketTime then 1682 return; 1683 else 1684 deltaTime = networkTime - self.lastPacketTime; 1685 end; 1686 1687 self.lastPacketTime = networkTime; 1688 1689 1690 local timeLeft = self.interpolationDuration * (1 - self.interpolationWeight); 1691 local targetDelay = expectedDelay; 1692 if timeLeft > 0 then 1693 targetDelay = lerp(timeLeft, expectedDelay, 0.1); 1694 end; 1695 1696 self.interpolationDuration = clamp(targetDelay + deltaTime, minDelay, maxDelay); 1697 self.interpolationWeight = 0; 1698 1699 for k, component in ipairs(self.components) do 1700 component.lastPosition = component.currentPosition; 1701 component.lastRotation = component.currentRotation; 1702 1703 component.targetPosition = Vector3:new(streamReadFloat(), streamReadFloat(), streamReadFloat()); 1704 component.targetRotation = Quaternion:new(streamReadFloat(), streamReadFloat(), streamReadFloat(), streamReadFloat()); 1705 end; 1706 1707 self:readWheelRpms(); 1708 1709 self.rawSteerValue = streamReadFloat(); 1710 -- we can interpolate again 1711 self.canInterpolate = true; 1712 else 1713 1714 -- read from client => server 1715 -- only read from the client if he is still the user 1716 isClientEntered = streamReadBool(); 1717 1718 if isClientEntered then 1719 1720 if self.currentUser == nil or self.currentUser.connection ~= connection then 1721 -- client has sent an update for an entity that he is not entitled to use => ignore it 1722 if g_debugNetworking then 1723 --print("Warning: Ignoring input from client who is not the user of this entity"); 1724 end; 1725 1726 -- abort this update stream 1727 return; 1728 else 1729 1730 -- handle input 1731 self.throttleValue = streamReadFloat(); 1732 self.brakeValue = streamReadFloat(); 1733 self.rawSteerValue = streamReadFloat(); 1734 self.rawSteerValueFixed = streamReadBool(); 1735 self.parkingBrake = streamReadBool() and 1 or 0; 1736 end; 1737 end; 1738 end; 1739 1740 self:callVehicleScripts("readUpdate", connection, networkTime, isClientEntered); 1741 end;
Vehicle:onGUI()
Can be used for any IMGUI calls.
1744 function Vehicle:onGUI() 1745 self:callVehicleScripts("onGUI"); 1746 1747 if g_isDevelopmentVersion and g_isUnityEditor then 1748 -- render debug info 1749 IMGUI.renderLabel(20, 70, 400, 40, string.format("[EDITOR ONLY] motor torque %.2f\nbrake torque %.2f\nsteer angle %.2f", self:getCurrentMotorTorque(), self:getCurrentBrakeTorque(), self:getCurrentSteerAngle())) 1750 end; 1751 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.