Abstract
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. 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!
You have full access to this open access chapter, Download chapter PDF
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!
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.](http://media.springernature.com/lw685/springer-static/image/chp%3A10.1007%2F979-8-8688-0060-3_8/MediaObjects/610821_1_En_8_Figa_HTML.png)
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.
Author information
Authors and Affiliations
Rights and permissions
Open Access This chapter is licensed under the terms of the Creative Commons Attribution 4.0 International License (http://creativecommons.org/licenses/by/4.0/), which permits use, sharing, adaptation, distribution and reproduction in any medium or format, as long as you give appropriate credit to the original author(s) and the source, provide a link to the Creative Commons license and indicate if changes were made.
The images or other third party material in this chapter are included in the chapter's Creative Commons license, unless indicated otherwise in a credit line to the material. If material is not included in the chapter's Creative Commons license and your intended use is not permitted by statutory regulation or exceeds the permitted use, you will need to obtain permission directly from the copyright holder.
Copyright information
© 2024 The Author(s)
About this chapter
Cite this chapter
Brumbaugh, Z., Leithner, M. (2024). Mileage Counter HUD Mod. In: Scripting Farming Simulator with Lua. Apress, Berkeley, CA. https://doi.org/10.1007/979-8-8688-0060-3_8
Download citation
DOI: https://doi.org/10.1007/979-8-8688-0060-3_8
Published:
Publisher Name: Apress, Berkeley, CA
Print ISBN: 979-8-8688-0059-7
Online ISBN: 979-8-8688-0060-3
eBook Packages: Professional and Applied ComputingProfessional and Applied Computing (R0)Apress Access Books