My game has an options menu which includes hiding a user’s trophies (attachable models to the character) and I am currently programming that option to work. Unfortunately, I’ve been toiling with it for several days despite it sounding really simple to accomplish, so I require help.
The main problem is that irrespective of the system I use, it seems the main problem comes down to how Roblox replicates tags and attributes. To give a rundown: both the server and the client can give any instance these. Client tags are overwritten by the server’s list before replication and the server’s value for an attribute overrides the client’s for an attribute before replication.
Now I thought that tag and attribute replication would be completely separate but they aren’t. The server is overriding attributes as well even though it does not ever configure attributes in this system, only the attachment of a new tag to register an active trophy. This leaves me at a standstill.
I won’t explain all the pipelines I’ve tried because they basically rehash each other with the same root problem at hand, which could tread on XY grounds. I’ll tell you what I am doing now though: waiting a frame before setting transparency. This works 75% of the time, the other 25% unworking times being that some parts are left unhidden when the user changes trophies.
Here is the code I am working with. Would appreciate any steps I could take to not wait a frame and instead have my module immediately and reliably be able to hide models when they’re tagged/enter the DataModel with specific conditions.
Specific problematic code:
--- Determine if new trophies should be hidden based on user settings
-- @param object the trophy object
local function onTrophyAdded(object)
-- Wait a frame because of inbound server replication on attributes/tags
-- Overrides the client immediately trying to set the tag, thus not hiding the trophy
Stepped:Wait()
-- In-line connection is required for object access
object:GetAttributeChangedSignal(VISIBILITY_ATTRIBUTE):Connect(function ()
setTrophyVisibility(object, object:GetAttribute(VISIBILITY_ATTRIBUTE))
end)
if (DataValues.Options.ShowTrophiesSelf == false and object:IsDescendantOf(Character)) or (DataValues.Options.ShowTrophiesOthers == false and not object:IsDescendantOf(Character)) then
object:SetAttribute(VISIBILITY_ATTRIBUTE, false)
end
end
Full TrophyVisibilityHandler module:
--- Independent system controlling the visibility of trophies.
-- Handled on a per-trophy basis by the client based on their attributes.
-- @module TrophyVisibilityHandler
-- @author colbert2677
local Players = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")
local LocalPlayer = Players.LocalPlayer
local Character = LocalPlayer.Character or LocalPlayer.CharacterAdded:Wait()
local Stepped = game:GetService("RunService").Stepped
-- This is a table. If you need to create a repro in Studio,
-- create a module and change the require path. The module
-- should contain the code as linked below this code block.
local DataValues = require(script.Parent.Parent.DataValues)
local TrophyVisibilityHandler = {}
local STRING_SUB = string.sub
local STRING_LEN = string.len
local TROPHY_TAG = "Trophy"
local VISIBILITY_ATTRIBUTE = "TrophyVisible"
local TROPHY_OPTION_PREFIX = "ShowTrophies"
local REFRESH_INTERVAL = 3
local steppedTime = 0
--- Change visibility of trophy instances by class
-- @param trophy instance to act on
-- @param visible a boolean representing the visibility value
local function setTrophyVisibility(trophy, visible)
for _, object in ipairs(trophy:GetDescendants()) do
-- Crude class check if-chain to take advantage of IsA
-- Class dictionary would require direct class checking which is not advantageous
if object:IsA("BasePart") or object:IsA("Decal") then
object.LocalTransparencyModifier = (visible == true and 0 or 1)
elseif object:IsA("GuiBase3d") or object:IsA("Constraint") then
object.Visible = visible
elseif object:IsA("LayerCollector") or object:IsA("Beam") or object:IsA("ParticleEmitter") or object:IsA("Trail") or object:IsA("Light") then
object.Enabled = visible
elseif object:IsA("Fire") or object:IsA("Smoke") or object:IsA("Sparkles") then
object.Enabled = visible
end
end
end
--- Update the trophy visibility routinely to catch visibility artifacts
-- Catches trophies that don't become fully invisible the first time
-- @see RunService.Heartbeat
local function updateStep(deltaTime)
steppedTime += deltaTime
if steppedTime >= REFRESH_INTERVAL then
steppedTime = 0
for _, trophy in ipairs(CollectionService:GetTagged(TROPHY_TAG)) do
setTrophyVisibility(trophy, trophy:GetAttribute(VISIBILITY_ATTRIBUTE))
end
end
end
--- Refresh character variable
-- @param newCharacter incoming character
local function onCharacterAdded(newCharacter)
Character = newCharacter
end
--- Call a method in TrophyVisibilityHandler based on parameters of OptionChanged
-- @see DataValues.OptionChanged
local function onOptionChanged(option, value)
if STRING_SUB(option, 1, STRING_LEN(TROPHY_OPTION_PREFIX)) == TROPHY_OPTION_PREFIX then
local mode = STRING_SUB(option, (STRING_LEN(TROPHY_OPTION_PREFIX) + 1))
TrophyVisibilityHandler[value == true and "ShowTrophy" or "HideTrophy"](mode)
end
end
--- Determine if new trophies should be hidden based on user settings
-- @param object the trophy object
local function onTrophyAdded(object)
-- Wait a frame because of inbound server replication on attributes/tags
-- Overrides the client immediately trying to set the tag, thus not hiding the trophy
Stepped:Wait()
-- In-line connection is required for object access
object:GetAttributeChangedSignal(VISIBILITY_ATTRIBUTE):Connect(function ()
setTrophyVisibility(object, object:GetAttribute(VISIBILITY_ATTRIBUTE))
end)
if (DataValues.Options.ShowTrophiesSelf == false and object:IsDescendantOf(Character)) or (DataValues.Options.ShowTrophiesOthers == false and not object:IsDescendantOf(Character)) then
object:SetAttribute(VISIBILITY_ATTRIBUTE, false)
end
end
--- Exposed method for showing trophies
-- @param mode act on the player's trophy or those of others
function TrophyVisibilityHandler.ShowTrophy(mode)
if mode == "Self" then
local equippedTrophy = Character:FindFirstChild("Trophy")
if equippedTrophy then
equippedTrophy:SetAttribute(VISIBILITY_ATTRIBUTE, true)
end
elseif mode == "Others" then
for _, trophy in ipairs(CollectionService:GetTagged(TROPHY_TAG)) do
if trophy:IsDescendantOf(Character) then continue end
trophy:SetAttribute(VISIBILITY_ATTRIBUTE, true)
end
end
end
--- Exposed method for hiding trophies
-- @see TrophyVisibilityHandler.ShowTrophy
function TrophyVisibilityHandler.HideTrophy(mode)
if mode == "Self" then
local equippedTrophy = Character:FindFirstChild("Trophy")
if equippedTrophy then
equippedTrophy:SetAttribute(VISIBILITY_ATTRIBUTE, false)
end
elseif mode == "Others" then
for _, trophy in ipairs(CollectionService:GetTagged(TROPHY_TAG)) do
if trophy:IsDescendantOf(Character) then continue end
trophy:SetAttribute(VISIBILITY_ATTRIBUTE, false)
end
end
end
LocalPlayer.CharacterAdded:Connect(onCharacterAdded)
DataValues.OptionChanged:Connect(onOptionChanged)
CollectionService:GetInstanceAddedSignal(TROPHY_TAG):Connect(onTrophyAdded)
Stepped:Connect(updateStep)
-- Only need to act initially if the user option is false
if DataValues.Options.ShowTrophiesSelf == false then
TrophyVisibilityHandler.HideTrophy("Self")
end
if DataValues.Options.ShowTrophiesOthers == false then
TrophyVisibilityHandler.HideTrophy("Others")
end
return TrophyVisibilityHandler
DataValues module code:
local optionChangedEvent = Instance.new("BindableEvent")
local DataValues = {
UserOptions = {
ShowTrophiesSelf = false,
ShowTrophiesOthers = false,
}
}
local optionsAccessProxy = setmetatable({}, {
__index = function(_, key)
if key == "All" then
return data.UserOptions
else
return data.UserOptions[key]
end
end,
__newindex = function(_, key, value)
data.UserOptions[key] = value
optionChangedEvent:Fire(key, value)
end
})
DataValues.Options = optionsAccessProxy
DataValues.OptionChanged = optionChangedEvent.Event
return DataValues
This is all client-side code. The server only has three responsibilities in this system: cloning, welding and tagging a model under any player character.
More explanation will be given on request. Please don’t reply if you aren’t sure what you’re talking about, I need substantiated answers. Thank you!