There must be a better way to establish variables/reference objects, right...?

Hello :wave:

I’ve started working on some scripts recently, that interact with multiple objects. I like to keep everything organised, so when I am writing a variable, it takes a long time to do it! It’s even worse when I want to group objects and then I’d need to go and rewrite every single variable that interacts with an object.

This is how my variables look like:

local prompt = script.Parent.CrossLedA:WaitForChild("CrossButtonPrompt")
local promptB = script.Parent.Parent.PedestrianSignalB.CrossLedB:WaitForChild("CrossButtonPromptB")
local counterA = script.Parent.CounterPed.SurfaceGui:WaitForChild("TextLabel")
local redA = script.Parent.RedPed.SurfaceGui:WaitForChild("ImageLabel")
local greenA = script.Parent.GreenPed.SurfaceGui:WaitForChild("ImageLabel")
local ledA = script.Parent:WaitForChild("CrossLedA")
local soundA = script.Parent.SoundEmitterA:WaitForChild("SoundA")
local nameX = script.Parent.Parent.Name

If I were to group counterA, redA and green A (which are the bulbs in a pelican crossing signal), I’d have to make edits and confuse myself even more.

Is there any way to reference objects better, and ensure that any explorer changes don’t affect them?

2 Likes

You could organize your variables by using a dictionary, like so:

local GroupA = {
	Counter = script.Parent.CounterPed.SurfaceGui:WaitForChild("TextLabel"),
	Red = script.Parent.RedPed.SurfaceGui:WaitForChild("ImageLabel"),
	Green = script.Parent.GreenPed.SurfaceGui:WaitForChild("ImageLabel"),
	LED = script.Parent:WaitForChild("CrossLedA"),
	Sound = script.Parent.SoundEmitterA:WaitForChild("SoundA")
}

Doing so will ensure that if you need to do something to pelican crossing signal A, you’ll need to use the values stored in the GroupA table

1 Like

In all your variables, you are using script.Parent. Learn the concept of DRY (Don’t Repeat Yourself): For cleaner code, save frequently-used paths/variables/whatever by referencing them: It also improves performance!

local ObjectContainer = script.Parent

local groupA = ObjectContainer.GroupA -- perhaps a folder? try organizing your objects in the explorer
local groupB = ObjectContainer.GroupB

Adding comments as documentation also improves maintainability.

2 Likes

The posts above me offer excellent methods to write smarter code! I’d also like to put in my two cents as well;

For systems with fairly simple logic but which manipulate a lot of instances like this crosswalk system (or more commonly, UI) I tend to go with CollectionService tags to keep track of instances without worrying about the explorer hierarchy. Since we’re already manipulating instances, using tags alongside attributes allows us to add our simple logic on-top of the comparatively complex instance without having to recreate a whole class around it (which is/was fairly common practice but in my opinion is unnecessary for simple logic.)

For your use case, that could look something like this.

local crossWalk = script.Parent
local crossWalkDescendants = crossWalk:GetDescendants()

local function findFirstChildWithTag(tag: string): (Instance | false)
	for _, child: Instance in crossWalkDescendants do
		if child:HasTag(tag) then
			return child
		end
	end
	return false
end

local crossWalkComponents = {
	promptA = findFirstChildWithTag("CrossButtonPromptA"),
	promptB = findFirstChildWithTag("CrossButtonPromptB"),
	counterA = findFirstChildWithTag("CrosswalkCounterLabel"),
	redA = findFirstChildWithTag("RedPedestrianGui"),
	greenA = findFirstChildWithTag("GreenPedestrianGui"),
	ledA = findFirstChildWithTag("CrosswalkLedA"),
	soundA = findFirstChildWithTag("CrosswalkSoundA"),
	-- For "nameX", you may want to cache the instance name elsewhere since all
	-- of your other variables are fetching instances.
}

local function validateComponents()
	for componentLabel, component in crossWalkComponents do
		assert(component, `TrafficPole is missing required instance '{componentLabel}'; Path [{crossWalk:GetFullName()}]`)
	end
end
validateComponents()

The downside of this approach is that your script will error when you try to use the variable rather than when you assign it, which in most cases leads to things breaking in worse and harder to find ways. That’s why we call the validateComponents function on the last line!

You can use the same logic with Instance:FindFirstChild which has a second parameter that will search for all descendants of that name rather than just direct children. This is of course an option but be aware that this relies on instance names which means your logic is still directly tied to the hierarchy and you cannot have duplicate names, unlike with tags. This also comes with the drawbacks of both your original method and the tags but can allow you to write quicker, dirtier code if that’s what you need.

That could look something like this:

local crossWalk = script.Parent

local crossWalkComponents = {
	-- The 2nd parameter is key! It lets us search all descendants.
	promptA = crossWalk:FindFirstChild("CrossButtonPrompt", true),
	promptB = crossWalk:FindFirstChild("CrossButtonPromptB", true),
	-- In your original example these have their default names but you will need
	-- to give them unique identifiers.
	counterA = crossWalk:FindFirstChild("CounterTextLabel", true),
	redA = crossWalk:FindFirstChild("RedPedImageLabel", true),
	greenA = crossWalk:FindFirstChild("GreenPedImageLabel", true),
	ledA = crossWalk:FindFirstChild("CrossLedA", true),
	soundA = crossWalk:FindFirstChild("SoundA", true),
}

local function validateComponents()
	for componentLabel, component in crossWalkComponents do
		assert(component, `TrafficPole is missing required instance '{componentLabel}'; Path [{crossWalk:GetFullName()}]`)
	end
end
validateComponents()

I hope this helped and I’d be glad to elaborate on anything you might be confused on. Cheers!

1 Like

Yk what would be faster? Setting a tag that’s equal to the object name then using GetTagged to retrieve it. This is clean and easy in my opinion and it works under any parent except nil ofc :blush:

local CS = game:GetService("CollectionService") 

local promptA = CS:GetTagged("CrossButtonPromptA")[1]

That would work if used in a server script, but can result in the value of promptA being nil if used on a LocalScript and CrossButtonPromptA hasn’t finished loading in-time before the LocalScript runs

Then you’d add this to the beginning

if not game:IsLoaded() then game.Loaded:Wait() end

This looks wonderful … only thing I would have done different is minor.

local root = script.Parent
local promptB = root.Parent:WaitForChild("PedestrianSignalB")
	:WaitForChild("CrossLedB"):WaitForChild("CrossButtonPromptB")
local counterA = root.CounterPed.SurfaceGui:WaitForChild("TextLabel")
local greenA = root.GreenPed.SurfaceGui:WaitForChild("ImageLabel")
local prompt = root.CrossLedA:WaitForChild("CrossButtonPrompt")
local redA = root.RedPed.SurfaceGui:WaitForChild("ImageLabel")
local soundA = root.SoundEmitterA:WaitForChild("SoundA")
local ledA = root:WaitForChild("CrossLedA")
local nameX = root.Parent.Name

And this would probably work just as well. (time may need to be a tick or two higher)

task.wait(3) 
local root = script.Parent
local promptB = root.Parent:WaitForChild("PedestrianSignalB")
	:WaitForChild("CrossLedB"):WaitForChild("CrossButtonPromptB")
local counterA = root.CounterPed.SurfaceGui.TextLabel
local greenA = root.GreenPed.SurfaceGui.ImageLabel
local prompt = root.CrossLedA.CrossButtonPrompt
local redA = root.RedPed.SurfaceGui.ImageLabel
local soundA = root.SoundEmitterA.SoundA
local nameX = root.Parent.Name
local ledA = root.CrossLedA
1 Like

That’s not a perfect solution either, unfortunately. If your game uses streaming, there’s a chance that the CrossButtonPromptA is outside of the player’s streaming radius. game:IsLoaded() would return true even if Instances outside of the streaming radius haven’t finished loading

Due to this, you can’t really guarantee that an Instance will exist when dealing with client-sided scripts (With the exception of Instances stored inside of ReplicatedStorage, although I still prefer to retrieve them with WaitForChild)

Personally I organize my variables like this:

--//PLAYER ELEMENTS//--
local player = game.Players.LocalPlayer
local playerGui = player.PlayerGui

--//PLAYER LEADERSTATS//--
local leaderstats = player:WaitForChild("leaderstats")
local leaderstatsTixes = leaderstats:WaitForChild("Tixes")

--//FOLDERS//--
local valuesFolder = playerGui:WaitForChild("ValuesFolder")

--//BUILT-IN INSTANCES//--
local replicatedStorage = game:GetService("ReplicatedStorage")

--//REMOTE EVENTS//--
local updatingValuesEvent = replicatedStorage:WaitForChild("RemoteEventsFolder").UpdatingValuesEvent -- This is used to update the Values on the Server, to prevent exploiting.

--//UI//--
local progressBarGui = player.PlayerGui:WaitForChild("ProgressBarGui")
local progressBarBackground = progressBarGui.ProgressBarBackground
local collectedTix = progressBarBackground.CollectedTix
local progressBar = progressBarBackground.ProgressBar

--//SERVICES//--
local CollectionService =  game:GetService("CollectionService")
local SoundService = game:GetService("SoundService")

--//TAGS//--
local tixes = CollectionService:GetTagged("Tixes")

--//SOUNDS//--
local tixFoundSoundEffect = SoundService:WaitForChild("TixFoundSoundEffect")

--//VALUES//--
local collectedTixValue = valuesFolder:WaitForChild("CollectedTixValue")
local progressBarValue = valuesFolder:WaitForChild("ProgressBarValue")

It’s not the best but it’s good, considering that my project isn’t complicated.

2 Likes

You’re repeating script.Parent a few times, that could probably be a variable. Also, you don’t need to use :WaitForChild unless the Instance it is waiting for is created by a Script. All static instances (instances that exist in Studio) will exist before a Script runs, and :Clone blocks the engine until everything is copied.

This is actually the best way to do things… I do hate doing this myself but, In the long run when you come back to this script 3 years from now, you’ll be happy you over remed it a bit.

Some of these are selfexplatory and don’t need rems but, this is a professional way of scripting.

–//SERVICES//-- someone knows a few other languages :rofl:
In Roblox .lua that is --SERVICES or …

–[[
for more than one line
of comments
]]

2 Likes

Ideally code is self-documenting. The comments don’t explain anything, they are just labelling what is already labelled by good variable names, so I personally wouldn’t really recommend this style to anyone. In my opinion, it is redundant. Comments are for code where it is not immediately obvious what the code is doing.

2 Likes

It’s not redundant 3 years from now. I agree with most of what you said. But to be real, comments to your future self are pure gold when that day comes.

This is a technique I use all the time and it works great! In this circumstance, I assumed that OP needs all of these components to be related to each other (such as disabling the green light when the red light turns on) so finding tagged instances using a common ancestor was the method I chose. If you get all instances tagged, you either need to figure out which instance is related to what or use unique tags for each case as you suggested, which carries the same issues as using the name to identify your instances.

StreamingEnabled is a whole other can of worms I was trying not to focus on but I figure I’d like to give some input:

@JohhnyLegoKing is correct here. This is also a bit overkill; waiting for every single instance to load could have other unexpected side effects over waiting for the specific instances you need.

This however, isn’t entirely accurate. Model.ModelStreamingMode is a property that forces the client to keep a model loaded regardless of StreamingEnabled, or to ensure that an entire model loads in as one piece rather than having missing components. In the code sample I provided, you’d set the ModelStreamingMode on the crossWalk parent instance to Enum.ModelStreamingMode.Atomic or Enum.ModelStreamingMode.Persistent to ensure you have all the pieces loaded.

2 Likes

I’m not going to lie what you’re saying is true and I don’t disagree but to some extent, it will work flawlessly especially if the object is within the player’s streaming radius.

1 Like
  1. Use Tables: Instead of individual variables, you can store your object references in tables. This allows you to group related objects together and access them more efficiently. For example:
local signals = {
    counterA = script.Parent.CounterPed.SurfaceGui:WaitForChild("TextLabel"),
    redA = script.Parent.RedPed.SurfaceGui:WaitForChild("ImageLabel"),
    greenA = script.Parent.GreenPed.SurfaceGui:WaitForChild("ImageLabel"),
    ledA = script.Parent:WaitForChild("CrossLedA"),
    soundA = script.Parent.SoundEmitterA:WaitForChild("SoundA")
}

local buttons = {
    prompt = script.Parent.CrossLedA:WaitForChild("CrossButtonPrompt"),
    promptB = script.Parent.Parent.PedestrianSignalB.CrossLedB:WaitForChild("CrossButtonPromptB")
}

local name = script.Parent.Parent.Name

This way, you can access your objects using signals.counterA, buttons.prompt, and name instead of individual variables.

  1. Use a Configuration Module: You can create a separate module that stores all your object references and other configuration data. This keeps your main script clean and makes it easier to update references if the Explorer structure changes.
-- config.lua
return {
    signals = {
        counterA = script.Parent.CounterPed.SurfaceGui:WaitForChild("TextLabel"),
        redA = script.Parent.RedPed.SurfaceGui:WaitForChild("ImageLabel"),
        greenA = script.Parent.GreenPed.SurfaceGui:WaitForChild("ImageLabel"),
        ledA = script.Parent:WaitForChild("CrossLedA"),
        soundA = script.Parent.SoundEmitterA:WaitForChild("SoundA")
    },
    buttons = {
        prompt = script.Parent.CrossLedA:WaitForChild("CrossButtonPrompt"),
        promptB = script.Parent.Parent.PedestrianSignalB.CrossLedB:WaitForChild("CrossButtonPromptB")
    },
    name = script.Parent.Parent.Name
}
-- main.lua
local config = require(script.Parent.config)

local signals = config.signals
local buttons = config.buttons
local name = config.name
  1. Use a Custom Class: You can create a custom class that encapsulates the object references and provides methods to interact with them. This can help you organize your code and make it more modular.
-- PedestrianSignal.lua
local PedestrianSignal = {}
PedestrianSignal.__index = PedestrianSignal

function PedestrianSignal.new(parent)
    local self = setmetatable({}, PedestrianSignal)
    self.signals = {
        counterA = parent.CounterPed.SurfaceGui:WaitForChild("TextLabel"),
        redA = parent.RedPed.SurfaceGui:WaitForChild("ImageLabel"),
        greenA = parent.GreenPed.SurfaceGui:WaitForChild("ImageLabel"),
        ledA = parent:WaitForChild("CrossLedA"),
        soundA = parent.SoundEmitterA:WaitForChild("SoundA")
    }
    self.buttons = {
        prompt = parent.CrossLedA:WaitForChild("CrossButtonPrompt"),
        promptB = parent.Parent.PedestrianSignalB.CrossLedB:WaitForChild("CrossButtonPromptB")
    }
    self.name = parent.Parent.Name
    return self
end

function PedestrianSignal:UpdateCounter(value)
    self.signals.counterA.Text = tostring(value)
end

-- main.lua
local PedestrianSignal = require(script.Parent.PedestrianSignal)
local signal = PedestrianSignal.new(script.Parent)
signal:UpdateCounter(42)
1 Like

Thank you all for the detailed and constructive responses! I am still playing with all of them, and will try and utilize them all until I find what suits me best. To eventually close the thread, I’ll mark my response as a solution.

Thank you once again for your time and help! :heart_hands:

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.