Speed Trap Trailer Mod

This chapter will explore making a speed trap trailer which will detect and fine a vehicle exceeding the speed limit (see Figure 7-1). This will require you to learn methods for detecting vehicles, displaying certain effects with shaders, and deducting currency from a player’s balance. Let’s get started!

Figure 7-1
A screenshot of speed trap trailer mod.

Be aware, speeders will be fined!

Technical Requirements

Like the previous chapter, you will be working entirely in the GIANTS Editor and Studio and must meet the requirements mentioned in the “Technical Requirements” section of Chapter 2, “Getting Started with the GIANTS Editor.” Make sure you are always using the most recent version of the GIANTS Editor. This will ensure that you are able to take advantage of any new features. You can find all the code and assets used in this chapter in the book’s code repository on the GDN at the following link:

A Q R scan code.

https://gdn.giants-software.com/lp/scriptingBook.php

Creating Mod Scripts

This section will explore all of the scripts necessary for this mod. We will start by looking at the .xml files and then cover the .Lua files. Please see the “Preparing the Mod Folder Structure” section of Chapter 5, “Making a Diner with a Rotating Sign,” on how to set up a mod project and use the sample files provided on GDN.

Creating XML Files

We start by creating the modDesc.xml for the mod. As with the previous mods, we define a title, description, and icon along with other basic properties. Let us now look at its contents:

<?xml version="1.0" encoding="utf-8" standalone="no" ?> <modDesc descVersion="72">       <Author>GIANTS Software</author>       <version>1.0.0.0</version>       <multiplayer supported="true" />       <title>             <en>Sample Mod - Speed Trap Trailer</en>       </title>       <description>             <en>A sample mod</en>       </description>       <iconFilename>icon_speedTrapTrailer.png</iconFilename>       <extraSourceFiles>             <sourceFile filename="scripts/events/SpeedTrapEvent.Lua"/>       </extraSourceFiles>       <specializations>             <specialization name="speedTrap" className="SpeedTrap"                         filename="scripts/SpeedTrap.Lua" />       </specializations>       <vehicleTypes>             <type name="speedTrapTrailer" parent="baseAttachable"                         filename="$dataS/scripts/vehicles/Vehicle.Lua">                   <specialization name="speedTrap" />             </type>       </vehicleTypes>       <storeItems>             <storeItem xmlFilename="vehicle/speedTrapTrailer.xml"/>       </storeItems>       <l10n filenamePrefix="l10n/l10n" /> </modDesc>

In the extraSourceFiles field, we include SpeedTrapEvent.Lua, and in the specialization field, we include the speedTrap specialization which will use the SpeedTrap.Lua file we will cover in the next section. Through the vehicleTypes field, we add a new speedTrapTrailer vehicle type which uses our specialization and the functionality from the base Vehicle class.

Next, the storeItems field defines one or more placeables of the mod with the path of the .xml file for the item. Lastly, we use the l10n field to reference the folder containing the translation files for the mod.

Now we will define the configuration file for the trailer item itself, speedTrapTrailer.xml. Let us take a look at the file:

<?xml version="1.0" encoding="utf-8" standalone="no" ?> <vehicle type="speedTrapTrailer" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://validation.gdn.giants-software.com/xml/fs22/vehicle.xsd">       <annotation>             Copyright (C) GIANTS Software GmbH, All Rights Reserved.       </annotation>       <storeData>             <name>Speed Trap Trailer</name>             <functions>                   <function>$l10n_function_speedTrapTrailer</function>             </functions>             <image>vehicle/store_speedTrapTrailer.png</image>             <price>3500</price>             <lifetime>600</lifetime>             <rotation>0</rotation>             <brand>LIZARD</brand>             <category>misc</category>             <shopTranslationOffset>0 0.05 0</shopTranslationOffset>             <shopRotationOffset>0 -1.102 0</shopRotationOffset>             <vertexBufferMemoryUsage>0</vertexBufferMemoryUsage>             <indexBufferMemoryUsage>0</indexBufferMemoryUsage>             <textureMemoryUsage>0</textureMemoryUsage>             <instanceVertexBufferMemoryUsage>0</instanceVertexBufferMemoryUsage>             <instanceIndexBufferMemoryUsage>0</instanceIndexBufferMemoryUsage> </storeData>

We start the file by defining a storeData field which holds the price, brand, and description of the item which will be displayed in the shop. This part of the file should be familiar, as we defined this same field and its elements in the “Creating XML Files” sections of Chapter 5, “Making a Diner with Rotating Element,” and Chapter 6, “Rotating Mower Mod.” Let us continue through the file:

<base>       <typeDesc>$l10n_typeDesc_speedTrapTrailer</typeDesc>       <filename>vehicle/speedTrapTrailer.i3d</filename>       <size width="2.2" height="2.5" length="4" lengthOffset="0.1" />       <components maxMass="1850">             <component centerOfMass="0 0.45 0"                         solverIterationCount="10" mass="306" />       </components>       <schemaOverlay attacherJointPosition="0 0" name="IMPLEMENT" />       <mapHotspot type="TRAILER" /> </base>

The base field defines the base settings for the speed trap tool. In this field, we reference the path to the .i3d file containing the speed trap trailer, set a size which is used for determining the required store area when buying the tool, and make sure it shows on the in-game map as a trailer via the mapHotspot field. We additionally get the description of the trailer from the appropriate translation file. We will now continue through the file:

<wheels>       <wheelConfigurations>             <wheelConfiguration name="$l10n_configuration_valueDefault" price="0"                         brand="MITAS" saveId="MITAS_DEFAULT">                   <wheels>                         <wheel filename="$data/shared/wheels/tires/mitas/FL02/6_R9.xml"                                     isLeft="true" hasTireTracks="true" hasParticles="true">                               <physics tipOcclusionAreaGroupId="1" restLoad="0.13"                                     repr="wheelLeft"  forcePointRatio="0.15" initialCompression="10"                                     suspTravel="0.05" spring="21" damper="10" yOffset="0.015"/>                               <innerRim filename="$data/shared/wheels/rims/rimsCar.i3d"                                     node="3|0" scale="0.28 0.32 0.32" offset="0.01"/>                         </wheel>                         <wheel filename="$data/shared/wheels/tires/mitas/FL02/6_R9.xml"                                     isLeft="false" hasTireTracks="true" hasParticles="true">                               <physics tipOcclusionAreaGroupId="1" restLoad="0.13"                                     repr="wheelRight" forcePointRatio="0.15" initialCompression="10"                                     suspTravel="0.05" spring="21" damper="10" yOffset="0.015"/>                               <innerRim filename="$data/shared/wheels/rims/rimsCar.i3d"                                     node="3|1" scale="0.28 0.32 0.32" offset="0.01"/>                         </wheel>                   </wheels>             </wheelConfiguration>       </wheelConfigurations>       <rimColor material="18">SHARED_SILVER</rimColor> </wheels>

The wheels field contains information about the wheels of the trailer. It references the wheel components of the physical model and sets the relevant physics information. Let us now look at the next portion of the file:

<attachable>       <inputAttacherJoints>             <inputAttacherJoint node="attacherJoint" jointType="trailer" attacherHeight="0.48" />       </inputAttacherJoints>       <!-- support animation if trailer is detached -->       <support animationName="moveSupport" />       <brakeForce force="0.03" maxForce="0.15" maxForceMass="1850"/> </attachable>

Like in previous attachable tools, we define the attachment point for the tool via an attachable field. We also include the moveSupport animation if the trailer is detached via the support field.

<animations>       <animation name="moveSupport">             <part node="supportFeet" startTime="0.35" endTime="0.70"                   startTrans="0.108 0.250 1" endTrans="0.108 0.110 1" />       </animation> </animations>

This animation is then defined in the animations field where we associate the supportFeet node of the model with the animation.

<ai>       <allowTurnBackward value="false"/>       <turningRadiusLimitation radius="8"/>       <agentAttachment jointNode="attacherJoint" rotCenterWheelIndices="1 2"             width="1.6" height="1.1" length="3.2" lengthOffset="0.55"/> </ai>

Following this, the ai field configures behaviors for when the tool is being operated autonomously.

<foliageBending>       <bendingNode minX="-0.8" maxX="0.8" minZ="-1.75"             maxZ="0.85" yOffset="0.3" />       <bendingNode minX="-0.15" maxX="0.15" minZ="0.85"             maxZ="1.7" yOffset="0.3" /> </foliageBending>

The foliageBending field creates behavior for how foliage should bend when run over by the attachment.

<wearable wearDuration="480" workMultiplier="5" fieldMultiplier="2"/> <washable dirtDuration="90"  washDuration="1"       workMultiplier="3" fieldMultiplier="2"/>

The wearable and washable fields are used for calculating repair costs and visual effects on the tool. More specifically, wearDuration and dirtDuration define the time it takes until the vehicle is completely dirty or worn. The workMultiplier and fieldMultiplier fields are factors that speed up this process. The washDuration field is the time it takes to fully clean the vehicle. The repair costs are based on the initial price, age, and the current wear factor. Let us continue through the file:

<speedTrap maxSpeedKmh="30" fine="1000" cooldownDuration="20">       <raycast node="raycastNode" maxDistance="35" detectionRadius="2"             numDetectionSamples="10"/>       <flash node="flashNode" duration="0.15" />       <sounds>             <trap file="sounds/speedTrap.wav" innerRadius="5.0" outerRadius="65.0"                         fadeOut="0.1" linkNode="speedTrapTrailer_main_component1">                   <volume indoor="2" outdoor="4.1" />                   <pitch indoor="1.00" outdoor="1" />                   <lowpassGain indoor="0.50" outdoor="1.00" />             </trap>       </sounds> </speedTrap>p>

In this section of the file, we define a custom XML element called speedTrap. In this element, we declare custom attributes such as maxSpeedKmh, fine, and cooldownDuration.

The maxSpeedKmh field defines the maximum speed permitted along the road in kilometers per hour. The fine field is how much a violator will be fined. The fine will not be paid to any user, simply deducted from the violator’s farm. The cooldownDuration field determines how much time there is between a vehicle being able to be fined again. Inside the speedTrap element, we include a raycast element. A raycast is a construct used in many game engines in which a ray is a line in space with a start point but no fixed endpoint. With raycasts, we can get information about intersections with the ray, making them very useful for hit detection. We define a maximum length for our raycast by defining maxDistance, which is set to 35 meters. A ray does not have any width, so we define detectionRadius to do some tricks internally to widen our hit detection. We then assign a flash effect to the flashNode on the tool. We also include sounds for the tool, which are defined via a sounds field like in previous chapters.

      <i3dMappings>             <i3dMapping id="speedTrapTrailer_main_component1" node="0>" />             <i3dMapping id="speedTrapTrailer_vis" node="0>0" />             <i3dMapping id="attacherJoint" node="0>0|0|0" />             <i3dMapping id="supportFeet" node="0>0|0|1|0" />             <i3dMapping id="supportCol" node="0>0|0|1|0|0" />             <i3dMapping id="wheelLeft" node="0>0|1|0" />             <i3dMapping id="wheelRight" node="0>0|1|1" />             <i3dMapping id="raycastNode" node="0>0|2|0" />             <i3dMapping id="flashNode" node="0>0|2|1" />       </i3dMappings> </vehicle>

Finally, we define .i3d mappings via an i3dMappings element which references points on the tool model.

With speedTrapTrailer.xml now complete, we are ready to create the next file, flashShader.xml. This file contains code written in the High-Level Shader Language (HLSL) which interacts with the part of the engine responsible for graphics rendering. You are not expected to fully understand the code written here, but your knowledge of Lua should be helpful in being able to follow the general work being done by the code. Let us now explore the file’s contents:

<?xml version="1.0" encoding="utf-8"?> <CustomShader version="5">       <Parameters>             <Parameter name = "flashFactor"  target = "flashFactor" type = "float"                   defaultValue = "1" minValue = "0.0" maxValue = "1"/>       </Parameters>       <UvUsages/>       <LodLevel startDistance="0">             <CodeInjections>                   <CodeInjection position="CONFIG_DEFINES">                         <![CDATA[                         #if defined( ALPHA_BLENDED )  // only for alpha blended materials                               #undef FOG_INSCATTERING   // only apply the fog extinction                               #undef SPECULAR           // also remove specular                         #endif                         ]]>                   </CodeInjection>                   <CodeInjection position = "OBJECT_PARAMETERS">                         <![CDATA[                               float flashFactor;                         ]]>                   </CodeInjection>

This first section starts by specifying the shader parameters that can be changed and set by the script. We include the parameter flashFactor which is a float value between 0 and 1 used to determine the light level of the flash. Following this, we use the CodeInjections field to inject two blocks of HLSL code into the shader. The first block in this case changes the base configurations of the shader, while the second block defines our custom variable. Let us continue:

<CodeInjection position="LIB_FUNCTION_VS">       <![CDATA[       float4x3 getBillboardMatrix( float3 centerPosition, VS_INPUT In,             ObjectParameters& object )       {             float3 pos = mul(object.modelMatrix, float4(centerPosition, 1)).xyz;             float3 negDirVector = normalize(pos);             float3 upVector = float3(invViewMatrix[0][1], invViewMatrix[1][1],                   invViewMatrix[2][1]);             float3 sideVector = normalize(cross(negDirVector, upVector));             upVector = cross(sideVector, negDirVector);             float4x3 billboardMatrix = float4x3(pos, sideVector, upVector, negDirVector);             return billboardMatrix;       }       float3 transformBillboardPoint(float3 centerPosition, VS_INPUT In, ObjectParameters& object)       {             float4x3 billboardMatrix = getBillboardMatrix(centerPosition,In,object);             float3 pos          = billboardMatrix[0];             float3 sideVector   = billboardMatrix[1];             float3 upVector     = billboardMatrix[2];             float3 negDirVector = billboardMatrix[3];             return (pos + sideVector*In.position.x + upVector*In.position.y); } float3 transformBillboardVector(float3 centerPosition, float3 inputVector, VS_INPUT In, ObjectParameters& object) {       float4x3 billboardMatrix = getBillboardMatrix(centerPosition,In,object);       float3 pos          = billboardMatrix[0];       float3 sideVector   = billboardMatrix[1];       float3 upVector     = billboardMatrix[2];       float3 negDirVector = billboardMatrix[3];       return (sideVector*inputVector.x + upVector*inputVector.y –             negDirVector*inputVector.z); } ]]> </CodeInjection>

This section of the file injects additional HLSL code. Here, we define custom functions for the vertex shader which is responsible for manipulating the vertices of the mesh the material is applied to.

So you can assume that all these lines are applied on each single vertex. We need these functions to create a billboard. A billboard in computer games is a simple two-dimensional plane that always faces the player’s camera. These helper functions are later used to recalculate the position, normal, and tangent of a vertex to face the player’s camera.

We could alternatively rotate the physical mesh in Lua with the setRotation() function, but it is much faster creating this effect with a shader.

We will now cover the next section of the file:

<CodeInjection position="GET_TANGENT_VS">       <![CDATA[       {             return transformBillboardVector(float3(0.0,0.0,0.0), In.tangent.xyz, In, object);       }       ]]> </CodeInjection> <CodeInjection position="GET_NORMAL_VS">       <![CDATA[       {             return transformBillboardVector(float3(0.0,0.0,0.0), In.normal.xyz, In, object);       }       ]]> </CodeInjection> <CodeInjection position="GET_POSITION_VS">       <![CDATA[             return transformBillboardPoint(float3(0.0,0.0,0.0), In, object);       ]]> </CodeInjection> <CodeInjection position="POST_GET_WORLD_POSE_VS">       <![CDATA[       {             worldPosition = position;             prevWorldPosition = worldPosition; // no motion blur             worldTangent   = normalize(getTangent(In, object));             worldBitangent = normalize(getBitangent(In, object));             worldNormal    = normalize(getNormal(In, object));       }       ]]> </CodeInjection>

This section of the file begins by injecting three lines which change the tangent, normal, and position in the vertex shader. It then injects code to customize some data after the vertex world data has been calculated in the vertex shader. Let us now look at the final section of the file:

<CodeInjection position="LIB_FUNCTION_FS">       <![CDATA[       float getDepthFade(FS_INPUT In, FS_GLOBALS globals, ObjectParameters& object, float fadeDistance)       {             float screenDepth = In.vs.screenPosZ / In.vs.screenPosW;             float screenDepthLinear = convertDepthToEyeZ(screenDepth);             float sceneDepthLinear = getLinearSceneDepth(In, globals,object);             return saturate((sceneDepthLinear - screenDepthLinear)/fadeDistance);       }       ]]> </CodeInjection> <CodeInjection position="ALPHA_FS">       <![CDATA[       #if defined(ALPHA_BLENDED) || defined(ALPHA_TESTED)             // increase emissive color, in order to enable             // bloom post process             float scaler = 5.0;             // for low pec profile bloom post process is             // disabled             #if GPU_PROFILE < GPU_PROFILE_MEDIUM                   scaler = 1.0;             #endif             alpha *= scaler*object.flashFactor;       #endif       #if defined( ALPHA_BLENDED )             // with high gpu profile add soft blending to             // the contact             // of the alpha blended mesh             #if GPU_PROFILE >= GPU_PROFILE_HIGH                   alpha *= getDepthFade(In, globals, object,0.1);             #endif             reflectingLightingScale = alpha;       #endif       ]]> </CodeInjection> <CodeInjection position="FINAL_POS_FS">       <![CDATA[       #if defined(ALPHA_BLENDED)             oColor.a = 0.0; // enable additive blending       #endif       ]]>       </CodeInjection> </CodeInjections> </LodLevel> </CustomShader>>

We conclude the file with additional injections. The first code injection defines custom functions in the fragment shader (see www.khronos.org/opengl/wiki/Fragment_Shader for more information). We then inject code to customize the alpha value in the fragment shader. Finally, we customize data at the end of the fragment shader with another code injection and close off the customShader element.

We have now covered the bulk of the .xml content for this mod. We must now include some simple translation files as we have for previous chapters so that players who speak different languages can interact with the mod more easily. Let us start with the contents of the English translation file, l10n_en.xml:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <l10n>       <elements>             <e k="function_speedTrapTrailer" v="Speed Trap Trailer"/>             <e k="typeDesc_speedTrapTrailer" v="Speed Trap Trailer"/>       </elements> </l10n>

In this file, we only include an element which holds text for the description and function of the tool in the shop. For both, we simply set the text to Speed Trap Trailer.

Let us now look at l10n_de.xml which will hold the same translations but for the German language:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <l10n>       <elements>             <e k="function_speedTrapTrailer" v="Mobile Radar-Kontrolle"/>             <e k="typeDesc_speedTrapTrailer" v="Radar-Kontrolle"/>       </elements> </l10n>

Like in the English file, we have two elements corresponding to the description and function of the tool for the shop. That concludes all of the .xml files needed for the mod. In the next section, we will explore the Lua files required for the mod.

Creating Lua Files

The first Lua file we will create is SpeedTrapEvent.Lua. Like in previous chapters, this script will implement some new behavior for the base Event class. Let us begin:

SpeedTrapEvent = {} local SpeedTrapEvent_mt = Class(SpeedTrapEvent, Event) InitEventClass(SpeedTrapEvent, "SpeedTrapEvent") function SpeedTrapEvent.emptyNew()       local self = Event.new(SpeedTrapEvent_mt)       return self end function SpeedTrapEvent.new(vehicle)       local self = SpeedTrapEvent.emptyNew()       self.vehicle = vehicle       return self end function SpeedTrapEvent:writeStream(streamId, connection)       NetworkUtil.writeNodeObject(streamId, self.vehicle) end function SpeedTrapEvent:readStream(streamId, connection)       self.vehicle = NetworkUtil.readNodeObject(streamId)       self:run(connection) end function SpeedTrapEvent:run(connection)       assert(connection:getIsServer(), "SpeedTrapEvent is server to client only")       if self.vehicle ~= nil and self.vehicle:getIsSynchronized() then             self.vehicle:activateSpeedTrapFlash()       end end

After creating a new event from the Event base class, we create two constructors with one that takes no arguments and one that takes a vehicle. If a vehicle is passed to the constructor, then that vehicle is assigned to the vehicle field of the class. We then define the writeStream() function and, like in previous chapters, it will write updates to the network stream. The readStream() function similarly reads updates from the network stream like in previous chapters. In our case, we only have to sync the speed trap trailer object that should activate its flash. Finally, the run() function executes the event. In our case, we require that the trap flash is activated locally only if the trailer is fully synchronized on the client side. If so, then the activateSpeedTrapFlash() function of the specialization we will define in SpeedTrap.Lua is called.

The SpeedTrap.Lua script defines our specialization and is the main Lua component of the mod. Let us start covering its contents:

local modName = g_currentModName SpeedTrap = {} SpeedTrap.SPEC_TABLE_NAME = "spec_"..modName..".speedTrap" function SpeedTrap.prerequisitesPresent(specializations)       return true end function SpeedTrap.registerEventListeners(vehicleType)       SpecializationUtil.registerEventListener(vehicleType, "onLoad", SpeedTrap)       SpecializationUtil.registerEventListener(vehicleType, "onDelete", SpeedTrap)       SpecializationUtil.registerEventListener(vehicleType, "onUpdate", SpeedTrap) end function SpeedTrap.registerFunctions(vehicleType)       SpecializationUtil.registerFunction(vehicleType, "activateSpeedTrapFlash",             SpeedTrap.activateSpeedTrapFlash)       SpecializationUtil.registerFunction(vehicleType, "onSpeedTrapRaycastCallback",             SpeedTrap.onSpeedTrapRaycastCallback) end

After defining the namespace for the mod, we include the default functions like in previous chapters. This mod has no prerequisites, so we simply return true in the prerequisitesPresent() function. Following this, we register the onLoad(), onDelete(), and onUpdate() base functions in the registerEventListeners() function. Lastly, we define two new functions in registerFunctions() called activateSpeedTrapFlash() and onSpeedTrapRaycastCallback(). We will define these functions later in this section.

Let us continue through the script:

function SpeedTrap.initSpecialization()       local schema = Vehicle.xmlSchema       schema:setXMLSpecializationType("SpeedTrap")       schema:register(XMLValueType.NODE_INDEX,             "vehicle.speedTrap.raycast#node", "Raycast start node")       schema:register(XMLValueType.FLOAT,             "vehicle.speedTrap.raycast#maxDistance", "Max. raycast distance")       schema:register(XMLValueType.INT,             "vehicle.speedTrap.raycast#detectionRadius",             "Detection sample radius at max raycast distance")       schema:register(XMLValueType.INT,             "vehicle.speedTrap.raycast#numDetectionSamples",             "Number of sample for detection")       schema:register(XMLValueType.FLOAT,             "vehicle.speedTrap#maxSpeedKmh", "Max. speed in km/h")       schema:register(XMLValueType.FLOAT,             "vehicle.speedTrap#fine", "The fine for speeding")       schema:register(XMLValueType.TIME,             "vehicle.speedTrap#cooldownDuration", "Cooldown time in seconds")       schema:register(XMLValueType.NODE_INDEX,             "vehicle.speedTrap.flash#node", "Flash node")       schema:register(XMLValueType.TIME,             "vehicle.speedTrap.flash#duration", "Flash duration in seconds")       SoundManager.registerSampleXMLPaths(schema,             "vehicle.speedTrap.sounds", "trap")       schema:setXMLSpecializationType() end

The initSpecialization() function works like those implemented in previous chapters, registering all of the XML elements and attributes associated with our mod in the script. Additionally, we register the path to the sound for the tool. We will now continue:

function SpeedTrap:onLoad(savegame)       local spec = self[SpeedTrap.SPEC_TABLE_NAME]       if self.isServer then             local rayKey = "vehicle.speedTrap.raycast"             local node = self.xmlFile:getValue(rayKey .. "#node", nil, self.components, self.i3dMappings)             if node ~= nil then                   spec.raycastNode = node                   spec.maxRaycastDistance =                         self.xmlFile:getValue(rayKey .. "#maxDistance", 25)                   spec.detectionRadius = self.xmlFile:getValue(rayKey .. "#detectionRadius", 2)                   spec.numDetectionSamples =                         self.xmlFile:getValue(rayKey .. "#numDetectionSamples", 10)                   spec.currentDetectionSample = 0                   spec.raycastCollisionMask = 2 ^ 13 -- bit 13                   -- identifies a vehicle                   spec.ignoredVehicles = {}                   spec.cooldownDuration =                         self.xmlFile:getValue("vehicle.speedTrap#cooldownDuration", 30)                   spec.maxSpeedKmh =                         self.xmlFile:getValue("vehicle.speedTrap#maxSpeedKmh", 20)                   spec.fine = self.xmlFile:getValue("vehicle.speedTrap#fine", 500)             else                   Logging.xmlWarning(self.xmlFile, "Trigger node missing for speed trap!")             end       end       spec.flashDuration =             self.xmlFile:getValue("vehicle.speedTrap.flash#duration", 0.5)       spec.flashTimeRemaining = 0       if self.isClient then             spec.flashNode = self.xmlFile:getValue("vehicle.speedTrap.flash#node", nil,                   self.components, self.i3dMappings)             spec.samples = {}             spec.samples.trap = g_soundManager:loadSampleFromXML(self.xmlFile,                   "vehicle.speedTrap.sounds", "trap", self.baseDirectory,                   self.components, 1, AudioGroup.VEHICLE, self.i3dMappings, self)       end end

The onLoad() function also works like in previous chapters. For each field we need to define for the specialization, we get these values from the relevant XML files. We define the raycastNode, maxRaycastDistance, detectionRadius, numDetectionSamples, currentDetectionSample, and raycastCollisionMask fields for use with raycasting. Most of these fields were previously explained in the “Creating XML Files” section. Two newly defined fields are raycastCollisionMask and currentDetectionSample. The currentDetectionSample field keeps track of the sample we are currently collecting.

Because a raycast has no width, we shoot numDetectionSamples + 1 raycasts in a cone. The target of each raycast is determined using the currentDetectionSample field. The raycastCollisionMask defines a collision mask for detecting objects with the raycast. A collision mask is a 32-bit unsigned integer. Two objects collide or interact if they have at least one matching bit. This concept is used for raycasts, collisions, and triggers. You can use the system to include or exclude objects from physical interaction. A similar system called Object masks is also used for rendering.

Next, we define ignoredVehicles, which holds a list of vehicles that have already been detected by the speed trap. Next, cooldownDuration is used to determine how much time must elapse before a vehicle can be removed from the ignoredVehicles list and be trapped again. As explained in the previous section, the maxSpeedKmh and fine fields determine the maximum allowed speed and the fine for speeding. The flashDuration field sets the duration of the flash on the tool. The flashTimeRemaining is used internally to track how much time there is remaining for the flash animation. If the specialization is being run on the client, then we include the flashNode field as well as a table for samples called samples. In the samples table, we also load the trap sound under the trap index. Let us now continue through the script:

function SpeedTrap:onDelete()       local spec = self[SpeedTrap.SPEC_TABLE_NAME]       if self.isClient then             g_soundManager:deleteSamples(spec.samples)       end end

In the onDelete() function, we simply delete the recorded samples if we are on the client:

function SpeedTrap:onUpdate(dt, isActiveForInput,             isActiveForInputIgnoreSelection, isSelected)       local spec = self[SpeedTrap.SPEC_TABLE_NAME]       if self.isServer and                   (self.getAttacherVehicle == nil or self:getAttacherVehicle() == nil) then             for vehicle, lastTrappedTime in pairs(spec.ignoredVehicles) do                   if g_time - lastTrappedTime > spec.cooldownDuration then                         -- remove vehicle from ignore list again                         spec.ignoredVehicles[vehicle] = nil                   end             end             local x, y, z = getWorldTranslation(spec.raycastNode)             local tx = 0             local ty = 0             local tz = spec.maxRaycastDistance             if spec.currentDetectionSample > 0 then                   local factor = spec.currentDetectionSample / spec.numDetectionSamples                   tx = spec.detectionRadius * math.cos(factor * 2 * math.pi)                   ty = spec.detectionRadius * math.sin(factor * 2 * math.pi)             end             tx, ty, tz = localToWorld(spec.raycastNode, tx, ty, tz)             local dirX, dirY, dirZ = MathUtil.vector3Normalize(tx - x, ty - y, tz - z)       raycastAll(x, y, z, dirX, dirY, dirZ, "onSpeedTrapRaycastCallback",             spec.maxRaycastDistance, self, spec.raycastCollisionMask, false, true)             spec.currentDetectionSample = spec.currentDetectionSample + 1             if spec.currentDetectionSample > spec.numDetectionSamples then                   spec.currentDetectionSample = 0             end       end       if self.isClient then             if spec.flashNode ~= nil and spec.flashTimeRemaining > 0 then                   spec.flashTimeRemaining = math.max(spec.flashTimeRemaining - dt, 0)                   local factor = spec.flashTimeRemaining / spec.flashDuration                   local alpha = math.sin(factor * math.pi)                   setShaderParameter(spec.flashNode, "flashFactor", alpha, 0, 0, 0, false)             end       end       self:raiseActive() end

The onUpdate() function is responsible for frequent updates of our tool as well as raycasting. Client physics are not super accurate as they are interpolated based on the data received from the server. Therefore, we do vehicle detection on the server only and then send an event to the clients to show the flash animation if we detect speeding.

If the specialization is being run on the server and the tool is not attached to a vehicle, then we start by iterating over the ignoredVehicles list. If any of the vehicles have been in the list for longer than cooldownDuration value, then they are removed from the list so that they may be trapped again.

Next, we get the position of the raycast node and calculate the direction our raycast should go. We want the raycast to come out of the forward-facing direction of the raycast node. The direction is a 3D vector that is a unit vector, meaning its magnitude is 1. This is important as having a vector with a magnitude not equal to one can cause odd behavior for our raycast, particularly in the distance we want to cast.

We use sine and cosine functions to calculate the target point of the vector. If the currentDetectionSample field is greater than 0, the target point should be on a circle around the raycastNode with the given detectionRadius. After the vector is normalized, we shoot the raycast at the specified position, in the specified direction, for the distance set by maxRaycastDistance.

We then increment currentDetectionSample by 1 and reset it to 0 if it exceeds the numDetectionSamples value. Continuing through the function, if the specialization is being run on the client, we need to update the visual of the flash. We use the flashTimeRemaining field to record how far along in the animation we are, then using a sine wave, we can create an effect of the flash smoothly transitioning between off and on. After calculating the flashFactor for the flash, we set it via the internal setShaderParameter() function which passes the value to our custom shader.

Lastly, we call raiseActive() to call onUpdate() for the next cycle. Let us now cover the final section of the script:

function SpeedTrap:activateSpeedTrapFlash()       local spec = self[SpeedTrap.SPEC_TABLE_NAME]       g_soundManager:playSample(spec.samples.trap)       spec.flashTimeRemaining = spec.flashDuration end

In this section, we define our custom functions activateSpeedTrapFlash() and onSpeedTrapRaycastCallback(). The activateSpeedTrapFlash() function is called when a violating vehicle is detected. It simply plays the trap sound and sets the flashTimeRemaining field to flashDuration to begin a new flash animation.

function SpeedTrap:onSpeedTrapRaycastCallback(hitActorId, x, y, z, distance, nx, ny, nz, subShapeIndex, shapeId, isLast)       local spec = self[SpeedTrap.SPEC_TABLE_NAME]       local vehicle = g_currentMission:getNodeObject(hitActorId)       if vehicle ~= nil and spec.ignoredVehicles[vehicle] == nil then             local isActiveDrivable = vehicle.getIsControlled ~= nil and vehicle:getIsControlled()             if isActiveDrivable then                   local speedKmh = vehicle:getLastSpeed()                   if speedKmh > spec.maxSpeedKmh then                         local farmId = vehicle:getOwnerFarmId()                         g_currentMission:addMoney(-spec.fine, farmId, MoneyType.OTHER, true, true)                         spec.ignoredVehicles[vehicle] = g_time                         g_server:broadcastEvent(SpeedTrapEvent.new(self), true)                   end             end       end       return true end

The onSpeedTrapRaycastCallback() function is passed the entity ID of a hit vehicle’s collision shape. If the vehicle exists and is not contained in the ignoredVehicles list, then we check that the vehicle is driveable and being actively driven. If so, then we get the speed of the vehicle via the getLastSpeed() function. If the last speed of the vehicle exceeds the value of maxSpeedKmh, then we get the farm ID of the driver and remove the fine amount from their balance. We then add their vehicle to the ignoredVehicles list and broadcast the event to the network.

This concludes all of the programming for the mod. Take a moment to review what you have accomplished and the new concepts you have learned.

Testing the Mod

With all of the XML and Lua files for the mod created, we are ready to begin testing. Start by running the game without debugging from the Debug application menu of the GIANTS Studio. After you begin a new game on the map of your choice with your mod selected, buy the trailer and buy a tractor. Attach the trailer to the tractor and drive to a road. Place the trailer so its camera is facing oncoming traffic and detach it from the tractor. Next, drive your tractor at full speed toward the trap, and you should be fined for exceeding the speed limit.

Summary

In this chapter, you learned how to use raycasting to create a tool that gauges the speed of passing vehicles and charges a fine accordingly. You also saw how the mods can change rendering behavior by injecting HLSL code into the engine.

In the next chapter, you will learn to create a mileage counter to record how far a vehicle has been driven and sync these values with elements of a player’s user interface.