Property setter priority/alternatives

Often times a property will be set from multiple locations in potentially overlapping time periods
For example one might want to lock the mouse when in first person or when the right button is down but have it unlocked otherwise (mirroring classic camera player script functionality)
But also it might be desired to lock the mouse when dragging in a viewport in the UI

What do you do to handle these types of scenarios where multiple scripts desire to change a property at the same time? Is there a particular generic solution? Do you have one script reconcile the conditions or do you take a more automatic approach such as by assigning a priority to each change, and only doing the change if it is the highest priority?

3 Likes

Have some sort of middle-man module that handles this stuff. The scripts should use that module which verifys which script gets to do the change. Ideally, if you are managing a group of scripts that do similar tasks, making it one script would be easier.

You can use condition variables (aka debounces), that only when in the correct combination, allow a function to run. For example:

  1. Only allow an animation to play when a brick is red and is in the position (0, 5 10).

  2. Only allow door to open when the bool open is false.

2 Likes

The thing I don’t like about this approach is that it breaks modular behavior, I write a lot of modules that I hope to never touch again (it makes me very happy when something always just works)

The conditions depend on other scripts (and thus your debounce solution makes scripts dependent on each other too), e.g the example I mention in the OP would mean the camera needs to be aware if a UI window is open which just seems quite random to me : /

1 Like

In that case then an option would be to set priorities.


You can make a DeciderScript with BindableFunctions as children (in folders for neatness).

You invoke a BindableFunction (a descendant of DeciderScript) called lets say ‘CanOpenDoor’. When that BindableFunction is invoked, the DeciderScript checks a list of values, and priorities by invoking other scripts (using more BindableFunctions) and retrieving their statuses. It then generates and returns a response (true or false) based on what current running process(es) is the most important.

For example, the DeciderScript may only allow the door to open when:

  • BrickColor of the door is green
  • The variable bool in another script is true
  • The debounce variable isOpen is equal to false.

The current script invokes the BindableFunction called CanOpenDoor. And only when all these conditions are met, will the DeciderScript return true, in which the door can now open.


Sorry if my example was bad, it was really hard thinking of one because I don’t get into these situations.

1 Like

Just so you know,
BindableFunctions are great for communicating between scripts

Your approach works but it means I have to have a script explicitly reconciling the state

with the priority approach I was thinking of earlier I can just do

do
	local prop=_G.property()
	function mt.setbehavior(priority,enum)
		prop:set(priority,enum)
		enum=prop:get()or Enum.MouseBehavior.Default
		_G.uis.MouseBehavior=enum
		mt.behavior=enum
	end
end

in the ui script:

view.MouseButton1Down:connect(function()
	last=Vector2.new(_G.mouse.x,_G.mouse.y)
	dragging=true
	_G.mouse.setbehavior(1,Enum.MouseBehavior.LockCurrentPosition)
end)
view.MouseButton1Up:connect(function()
	dragging=false
	_G.mouse.setbehavior(1,nil)
end)
view.MouseLeave:connect(function()
	dragging=false
	_G.mouse.setbehavior(1,nil)
end)

throughout the camera scripts

_G.mouse.setbehavior(0,md.firstperson and Enum.MouseBehavior.LockCenter or rightdown and Enum.MouseBehavior.LockCurrentPosition or nil)

and this as the property module:

--[[
	property setter/getter with priority
	
	assumes same priorities are same val
	
	lg N
	
--]]

local md={}
md.__index=md

function md:get()
	local priority=self.priorities:max()
	priority=priority and priority[4]
	return self.vals[priority],priority
end
function md:set(priority,val)
	if val==nil and self.vals[priority]~=nil then--delete
		self.priorities:delete(priority)
		self.vals[priority]=nil
	elseif val~=nil and self.vals[priority]~=nil then--update
		self.vals[priority]=val
	elseif val~=nil then--insert
		self.vals[priority]=val
		self.priorities:insert(priority)
	end
	return md
end

function md.new()
	return setmetatable({
		vals={},--priority=val
		priorities=_G.rbt(),
	},md)
end

return md

Though I’m also thinking about global variables now that define the game’s state such as if UI is open etc or whether I should just assume the UI window will always exist in every game with this framework

Another solution is to just rely on BindToRenderStep’s priorities and let those handle it for you although with this you have to set everything every frame and that is a little inefficient

ContextActionService is definitely your friend in these scenarios! Whenever you want to work with making sure an input is treated correctly in a certain environment, you can bind actions to events that change the environment, even at specific priorities, and even across multiple local scripts!

e.g, let’s use your example of making the mouse lock in place when on-screen, but drag a viewport gui when you mouse over it.

Let’s call this the CameraScript:

local ContextActionService = game:GetService("ContextActionService")
local UserInputService = game:GetService("UserInputService")

-- set up camera stuff
local x = 0 --camera's lateral movement
local y = 0 --camera's vertical movement

ContextActionService:BindActionAtPriority("CameraControls",function(name,state,input)
	if input.UserInputType == Enum.UserInputType.MouseMovement then
		x = x + input.Delta.X
		y = y + input.Delta.Y
	elseif input.UserInputType == Enum.UserInputType.MouseButton2 then
		if state == Enum.UserInputState.Begin then
			UserInputService.MouseBehavior = Enum.MouseBehavior.LockCurrentPosition
		elseif state == Enum.UserInputState.End then
			UserInputService.MouseBehavior = Enum.MouseBehavior.Default
		end
	end
end, false, 0, 
	Enum.UserInputType.MouseMovement, 
	Enum.UserInputType.MouseButton2
)

And this would be in your gui script:

local ContextActionService = game:GetService("ContextActionService")

local viewport = --viewport frame

local function BindDrag(frame, mousePos)
	local offset = Vector2.new(mousePos.X, mousePos.Y) - frame.AbsolutePosition
	ContextActionService:BindActionAtPriority("StartDrag"..frame.Name, function(name, state, input)
		--type will always = movement and state wil always = change
		
		--might not be correct, just an example.
		frame.Position = UDim2.new(0, input.X + offset.X, 0, input.Y + offset.Y)

	end, false, 1, Enum.UserInputType.MouseMovement)
end

local function UnbindDrag(frame)
	ContextActionService:UnbindAction("StartDrag"..frame.Name)
end

viewport.MouseEnter:Connect(function()
	ContextActionService:BindActionAtPriority("DetectDrag"..viewport.Name, function(name,state,input)
		--since we know it will always be MouseButton2, we don't account for type

		if state == Enum.UserInputState.Begin then
			BindDrag(viewport,input.Position)
		elseif state == Enum.UserInputState.End then
			UnbindDrag(viewport)
		end
		
		--default response if nil is returned, but just for good measure!
		return Enum.ContextActionResult.Sink
	end, false, 1, Enum.UserInputType.MouseButton2)
end)

viewport.MouseLeave:Connect(function()
	ContextActionService:UnbindAction("DetectDrag"..viewport.Name)
end)

If you implement this into your game, MouseButton2 will be overridden by the DetectDrag action whenever the viewport frame is hovering over, but will be unbound whenever you leave the frame, and similarly for MouseMovement.


In more general terms, however, when dealing with priorities for changing a property, it’s better to:

  • Create an object for each script to control, instead of one object that every script controls

  • Force the getting and setting of properties to be handled from a single module script

  • Combine all reason for getting and setting that object’s properties into one module script/ normal script. This is the most common one.

2 Likes

I’ve never used ContextActionService before, but I’m wondering, is it always the most recent one on top you want to have priority?

Yes, the most recently bound action will always have top priority, but only if you use :BindAction, or bind two actions at the same priority, I believe.

1 Like

Oh I didn’t look at the wiki lol rip

I’m a little confused about how it works with the Roblox chat and other core guis
When I searched through the chat modules, they don’t use ContextActionService at all…so wouldn’t that mean your scripts take priority over the user input? Or not because you would be typing into a textbox?

And with ContextActionService is there any way to bind with a higher priority than Roblox Textboxes/Buttons/Frames with Active property or would I need to make a custom ContextActionService for that?

I tested this myself, you aren’t able to bind actions at a higher priority than Roblox gui’s.
e.g, this code won’t sink all input from the keyboard, like Escape or /, which would activate Roblox’s gui elements:

ContextActionService:BindActionAtPriority("StopInput",function()
	return Enum.ContextActionResult.Sink
end,false,math.huge,Enum.UserInputType.Keyboard)

As for binding things over roblox gui’s, you could probably use StarterGui:SetCore or similar to disable parts of the Roblox gui whenever you bind something new over it, or you could disable their Active property, like you said, but only when your gui element is affected by it.

1 Like