In this chapter, you will create a mod that displays a mileage counter user interface (UI) element to players when they are seated in a vehicle (see Figure 8-1). This will teach you to not only create UI elements and the networking that is required to update them but also how to add additional functionality to existing specializations and systems. Let us begin!

Figure 8-1
A 3-D illustration of a tractor on the road with a factory nearby. It depicts tracking of mileage with a U I element next to the speedometer.

Track your mileage with a UI element next to the speedometer

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 code presents a pattern of pixelated patterns in a square. A small concentric square is on 3 corners.

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 defining the modDesc.xml file for the mod. We do not introduce any new fields for the file in this mod, so you should be familiar with each of them. Let us now look at the contents of the file:

<?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 - Mileage Counter</en>       </title>       <description>             <en>A sample mod</en>       </description>       <iconFilename>icon_mileageCounter.png</iconFilename>       <extraSourceFiles>             <sourceFile filename="scripts/InjectSpecialization.lua"/>             <sourceFile filename="scripts/MileageDisplay.lua"/>             <sourceFile filename="scripts/MileageHUDExtension.lua"/>       </extraSourceFiles>       <specializations>             <specialization name="mileageCounter" className="MileageCounter"                   filename="scripts/MileageCounter.lua" />       </specializations> </modDesc>

In the extraSourceFiles field, we include InjectSpecialization.lua, MileageDisplay.lua, and MileageHUDExtension.lua. These three files are in the scripts subdirectory of the mod directory. In the specialization field, we include the mileageCounter specialization which will use the MileageCounter.lua file we will create in the next section.

Creating Lua Files

The first Lua file we will create is InjectSpecialization.lua. This script is used to add the mileage counter to vehicles that use the driveable specialization. Let us explore the file’s contents:

local modName = g_currentModName TypeManager.finalizeTypes = Utils.prependedFunction(       TypeManager.finalizeTypes,       function(self, ...)             if self.typeName == "vehicle" then                   for typeName, typeEntry in pairs(self:getTypes()) do                         for name, _ in pairs(typeEntry.specializationsByName) do                               if name == "motorized" then                                     self:addSpecialization(typeName, modName..".mileageCounter")                                     break                               end                         end                   end             end       end )

We start by prepending a new function to the finalizeTypes() function of the TypeManager class. To prepend a function to another function, is to have the function we are prepending be called first whenever the function we are prepending to is called. The function we are prepending takes self as an argument, which refers to an instance of some class. There are two typeManagers in the game: one is responsible for placeable types and the other for vehicle types. We use self.typeName to identify the manager and ensure we are working with one of the vehicle type – that is, the object we are interacting with is a vehicle. If so, we will loop over all registered vehicle types by using the getTypes() method of self. For each vehicle type, we will check if it uses the motorized specialization. If the vehicle is motorized, then we will add the mileageCounter specialization to the vehicle via the addSpecialization() method.

Next, we will create MileageCounter.lua. This file is the core Lua script of the mod. Let us now cover its contents:

local modName = g_currentModName MileageCounter = {} MileageCounter.SPEC_TABLE_NAME = "spec_"..modName..".mileageCounter" function MileageCounter.prerequisitesPresent(specializations)       return true end function MileageCounter.registerEventListeners(vehicleType)       SpecializationUtil.registerEventListener(vehicleType, "onLoad", MileageCounter)       SpecializationUtil.registerEventListener(vehicleType,             "onReadStream", MileageCounter)       SpecializationUtil.registerEventListener(vehicleType,             "onWriteStream", MileageCounter)       SpecializationUtil.registerEventListener(vehicleType,             "onReadUpdateStream", MileageCounter)       SpecializationUtil.registerEventListener(vehicleType,             "onWriteUpdateStream", MileageCounter)       SpecializationUtil.registerEventListener(vehicleType,             "onUpdate", MileageCounter) end function MileageCounter.registerFunctions(vehicleType)       SpecializationUtil.registerFunction(vehicleType,             "getDrivenDistance", MileageCounter.getDrivenDistance) end function MileageCounter.initSpecialization()       local schemaSavegame = Vehicle.xmlSchemaSavegame       schemaSavegame:register(XMLValueType.FLOAT,             "vehicles.vehicle(?)."..modName..".mileageCounter#drivenDistance",             "Driven distance in meters") end

Like in previous mods, there are some functions we must define for every specialization we create. After defining the namespace for the specialization, we include the prerequisitesPresent() function. This mod does not depend on any other specializations, so we only need to return true.

Next, we add the registerEventListeners() function which will create the event listeners associated with the vehicle specialization. In registerFunctions(), we register a custom getDrivenDistance() function, which we will define later in this file. Lastly, in the initSpecialization() function, we register the path to the saved value for the mileage counter.

For more details on required functions of specializations, refer to the “Creating Lua Files” section of Chapter 6, “Rotating Mower Mod,” where we cover RotateMower.lua. We will now continue through the file:

function MileageCounter:onLoad(savegame)       local spec = self[MileageCounter.SPEC_TABLE_NAME]       spec.drivenDistance = 0       if savegame ~= nil then             spec.drivenDistance = savegame.xmlFile:getValue(                   savegame.key .. "."..modName..".mileageCounter#drivenDistance", 0)       end       spec.drivenDistanceNetworkThreshold = 10       spec.drivenDistanceSent = spec.drivenDistance       spec.dirtyFlag = self:getNextDirtyFlag() end function MileageCounter:saveToXMLFile(xmlFile, key, usedModNames)       local spec = self[MileageCounter.SPEC_TABLE_NAME]       xmlFile:setValue(key .. "#drivenDistance", spec.drivenDistance) end

In the onLoad() function, we set the value for the drivenDistance field of the class. If the player is joining a game they have saved previously, then the drivenDistance field is set to the saved value; otherwise, it is set to 0. The drivenDistanceNetworkThreshold field specifies the distance the driven distance must change before a signal to update the distance display is sent to the client. In the HUD, we display the distance in kilometers with 100 meters of precision; thus, the threshold needs to be 10 meters because of rounding in the display. The drivenDistanceSent field records the value of drivenDistance when the counter was last updated and is used to determine whether an update should be sent to the client. Lastly, the dirtyFlag field is used by the network to determine if the specialization is in need of an update. Next, we create the saveToXML() function, which will save the value of drivenDistance to the saved .xml file.

function MileageCounter:onReadStream(streamId, connection)       local spec = self[MileageCounter.SPEC_TABLE_NAME]       spec.drivenDistance = streamReadInt32(streamId) end function MileageCounter:onWriteStream(streamId, connection)       streamWriteInt32(streamId, spec.drivenDistance) end

The onReadStream() function is used to synchronize the value of drivenDistance with new players that join the game. The onWriteStream() function is called on the server if a player joins the game to sync the drivenDistance value.

function MileageCounter:onReadUpdateStream(streamId, timestamp, connection)       if connection:getIsServer() then             if streamReadBool(streamId) then                   local spec = self[MileageCounter.SPEC_TABLE_NAME]                   spec.drivenDistance = streamReadInt32(streamId)             end       end end function MileageCounter:onWriteUpdateStream(streamId, connection, dirtyMask)       if not connection:getIsServer() then             local spec = self[MileageCounter.SPEC_TABLE_NAME]             if streamWriteBool(streamId, bitAND(dirtyMask, spec.dirtyFlag) ~= 0) then                   streamWriteInt32(streamId, spec.drivenDistance)             end       end end

The onReadUpdateStream() function is used by the client to read values written to the stream and update the driveDistance field accordingly. The onWriteUpdateStream() function is used by the server to tell the client the new value for the drivenDistance field only if the mileage counter has changed.

Let us now continue through the file:

function MileageCounter:onUpdate(dt, isActiveForInput,             isActiveForInputIgnoreSelection, isSelected)       local spec = self[MileageCounter.SPEC_TABLE_NAME]       if self:getIsMotorStarted() then             if self.isServer then                   if self.lastMovedDistance > 0.001 then                         spec.drivenDistance = spec.drivenDistance + self.lastMovedDistance                         if math.abs(spec.drivenDistance - spec.drivenDistanceSent) >                         spec.drivenDistanceNetworkThreshold then                               self:raiseDirtyFlags(spec.dirtyFlag)                               spec.drivenDistanceSent = spec.drivenDistance                         end                   end             end       end end

The onUpdate() function will update the state of the mileage counter. We first check that the vehicle is turned on, and then if the vehicle has moved more than 0.001 meters, we increase the drivenDistance field by that distance. If the difference between the drivenDistance and drivenDistanceSent fields is greater than the drivenDistanceNetworkThreshold value, then the dirtyFlag is raised to mark the vehicle for a network update in the next network package. We also set drivenDistanceSent to drivenDistance so we do not send multiple updates.

Note that we have this system in place as sending an update signal whenever the vehicle moves in the slightest would be a waste of resources and risk overloading the network.

function MileageCounter:getDrivenDistance()       -- first get the specialization namespace       local spec = self[MileageCounter.SPEC_TABLE_NAME]       return spec.drivenDistance end

Finally, the getDrivenDistance() function simply returns the value of the drivenDistance field.

The next script we will create is MileageDisplay.lua. This script is responsible for creating and managing the actual HUD UI element which will display the driven distance to the player. Let us now look at the script:

local modDirectory = g_currentModDirectory MileageDisplay = {} local MileageDisplay_mt = Class(MileageDisplay, HUDDisplayElement) function MileageDisplay.new()       local backgroundOverlay = MileageDisplay.createBackground()       local self = MileageDisplay:superClass().new(backgroundOverlay,             nil, MileageDisplay_mt)       self.vehicle = nil       self:applyValues(1)       return self end

We begin by creating the constructor for the class, MileageDisplay.new(). The constructor will create a background element, create a HUDDisplayElement from the background, and apply a default UI scale value of 1. We create the background by calling the createBackground() function, which we will define later in the script.

function MileageDisplay:setVehicle(vehicle)       if vehicle ~= nil and vehicle.getDrivenDistance == nil then             vehicle = nil       end       self.vehicle = vehicle end

Next, the setVehicle() function will set the vehicle field of the class to the passed vehicle reference.

function MileageDisplay:draw()       if self.vehicle == nil then             return       end       MileageDisplay:superClass().draw(self)       local drivenDistance = self.vehicle:getDrivenDistance()       local distanceInKM = drivenDistance / 1000       distanceInKM = distanceInKM % 999999.9       local distance = g_i18n:getDistance(distanceInKM)       local unit = g_i18n:getMeasuringUnit()       local textBG = string.format("%08.1f %s", distance, unit)       local text = string.format("%.1f %s", distance, unit)       local textColor = MileageDisplay.COLOR.TEXT       local textColorBG = MileageDisplay.COLOR.TEXT_BACKGROUND       local textSize = self.textSize       local posX, posY = self:getPosition()       posX = posX + self.textOffsetX       posY = posY + self.textOffsetY       setTextBold(false)       setTextAlignment(RenderText.ALIGN_RIGHT)       setTextColor(textColorBG[1], textColorBG[2], textColorBG[3], textColorBG[4])       renderText(posX, posY, textSize, textBG)       setTextColor(textColor[1], textColor[2], textColor[3], textColor[4])       renderText(posX, posY, textSize, text)       setTextAlignment(RenderText.ALIGN_LEFT)       setTextColor(1, 1, 1, 1) end

The draw() method is used to “draw” the mileage display. That is, this function will render the UI element and set its contents such as text. After rendering the element via the draw() function of the overlay’s superclass, we retrieve the drivenDistance value via the getDrivenDistance() function we defined earlier. The drivenDistance value is in meters, and we want the mileage counter to display in kilometers, so we must simply divide by 1000. The largest value we want to display on the mileage counter is 999999.9, so using a modulo operation, it will flip over back to 0 just like a real-life odometer!

Because not everyone in the world uses the metric system, we will use the il8n utility to convert the value to the appropriate unit of kilometers or miles and get the corresponding abbreviations (km or mi).

Next, we format these values in a string before updating the element’s text. Now we have all of the information to render on the screen.

We first calculate the x and y positions on the screen. We then disable the bold text rendering mode with setTextBold(false). Our mileage counter display should be right aligned, so we set the global text rendering alignment to RenderText.ALIGN_RIGHT. Before rendering, we set the correct text color for the background text.

We can now render the background text. The text is used to always show eight digits. So, if our mileage counter is 100 miles, we render the text with five leading zeros. After rendering the background, we need to set the text color for the foreground and render the text.

Finally, we need to reset our changes to the global text rendering (alignment, color) to make sure that our changes do not affect other text rendering scripts. Let us continue through the program:

function MileageDisplay:setScale(uiScale)       MileageDisplay:superClass().setScale(self, uiScale, uiScale)       local posX, posY = MileageDisplay.getBackgroundPosition(uiScale)       self:setPosition(posX, posY)       self:applyValues(uiScale) end function MileageDisplay:applyValues(uiScale)       local textOffsetX, textOffsetY =             getNormalizedScreenValues(unpack(MileageDisplay.POSITION.TEXT_OFFSET))       local _, textSize = getNormalizedScreenValues(0, MileageDisplay.SIZE.TEXT)       self.textOffsetX = textOffsetX*uiScale       self.textOffsetY = textOffsetY*uiScale       self.textSize = textSize*uiScale end

The setScale() method will scale the mileage counter element as a whole and adjust its position to fit different screens and devices. This function then calls applyValues() which will adjust the scale and offset of the text element within the background.

function MileageDisplay.getBackgroundPosition(uiScale)       local width, _ = getNormalizedScreenValues(unpack(MileageDisplay.SIZE.SELF))       local offsetX, offsetY =             getNormalizedScreenValues(unpack(MileageDisplay.POSITION.OFFSET))       local posX = 1 - width*uiScale + offsetX*uiScale       local posY = offsetY*uiScale       return posX, posY end

Next, we include the getBackgroundPosition() function which will return the absolute position of the mileage counter element in pixels. Note that it accounts for the current uiScale value.

function MileageDisplay.createBackground()       local posX, posY = MileageDisplay.getBackgroundPosition(1)       local width, height =             getNormalizedScreenValues(unpack(MileageDisplay.SIZE.SELF))       local filename = Utils.getFilename("hud/mileageCounterBackground.png",             modDirectory)       local overlay = Overlay.new(filename, posX, posY, width, height)       return overlay end MileageDisplay.SIZE = {       SELF = {128, 32},       TEXT = 17 } MileageDisplay.POSITION = {       OFFSET = {-35, 280},       TEXT_OFFSET = {115, 10} } MileageDisplay.COLOR = {       TEXT = {1, 1, 1, 1},       TEXT_BACKGROUND = {0.15, 0.15, 0.15, 1} }

The last function we define is createBackground() which uses the background image in the hud subdirectory of our mod and creates a new overlay instance, which it then returns.

Finally, we set an initial size, position, and color for the text background and text element.

The last script is MileageHUDExtension.lua. This program will add our mileage display UI element to the game’s interface. Let us look at its contents:

HUD.createDisplayComponents =       Utils.appendedFunction(HUD.createDisplayComponents, function(self, uiScale)             self.mileageDisplay = MileageDisplay.new()             self.mileageDisplay:setScale(uiScale)             table.insert(self.displayComponents, self.mileageDisplay)       end) HUD.drawControlledEntityHUD =       Utils.appendedFunction(HUD.drawControlledEntityHUD, function(self)             if self.isVisible then                   self.mileageDisplay:draw()             end       end) HUD.setControlledVehicle = Utils.appendedFunction(HUD.setControlledVehicle,       function(self, vehicle)             self.mileageDisplay:setVehicle(vehicle)       end)

In this script, we append three new functions to existing internal HUD system functions. The first we define is appended to the createDisplayComponents() function of the HUD. In the new function, we call the constructor for the mileage display, set its scale to the current uiScale value, and insert the display into the UI components currently displayed.

Next, we append a function onto the drawControlledEntityHUD() function of the HUD. This function is responsible for drawing HUD elements, and the function we append will call the draw() function we defined earlier for the mileage counter.

Lastly, we append a new function to the setControlledVehicle() function of the HUD. As the name implies, this function is used to associate a vehicle with a HUD element. The function we append will call the setVehicle() function of the mileage counter we defined earlier, passing along the vehicle reference.

You have now finished creating all of the scripts for the mod. You should take some time to do a high-level review of everything we have implemented in this chapter and what you have learned from it.

Testing the Mod

We will follow the testing procedure from the previous chapter. From the GIANTS Studio, you can run the game without debugging from the Debug application menu. After you begin a new game on the map of your choice, you should open the vehicle shop. After spawning any motorized vehicle, sit in it, and you should see the mileage counter displayed to you. If everything is working, the mileage reading should increase as you drive and be displayed in the units of your preference.

Summary

In this chapter, you created a mileage counter that displays the distance that has been driven by a motorized vehicle to players. This mod taught you how to create UI elements and have them use existing systems as well as injecting additional functionality into existing specializations.

In the next chapter, you will work on another UI-oriented mod where players will be able to spawn bales of different shapes and types.