LocalScripts running in the workspace

I understand that, my questions are:

  1. Is there any reason why it shouldn’t run on the workspace?
  2. Is there any way to make them run on the workspace?
  3. How can I fix Problem Y if the previous question is impossible
1 Like

This is taking a turn towards the xy problem.

But they should not run in workspace because local scripts run on a client.

A local script’s ancestry determines on which client it will be running on. The workspace service is not a client.

Local scripts will only execute as a descendant of the workspace if in a player’s character. Other than that they will not run as descendant of workspace.

1 Like

I am aware that this is an XY Problem, which is why I made it a two part question.
I am also aware of how local scripts work.

But, a service can’t be a server or a client. Of course there are of course services that only exist on the client or the server and it would make sense that a local script in the ServerScriptService shouldn’t and can’t run on the client but the workspace is replicated among the client and the server and sometimes, it does make sense to have some things handled by the client in a service where the server is authoritative.

I’m asking if there is a reason behind the Roblox Engineers’ choice to not allow LocalScripts run in the workspace.

1 Like

I just explained why.

Is there a reason you cannot put it in a place where they can execute? Like PlayerScripts?


Sounds like circular logic.

1 Like

Let me explain my whole predicament:

I have a game where players can build ships. In order to avoid code repetition I created a system that would populate Instances with specific tags with specific parts. These parts could be scripts that should run on the client or server or just parts needed for visuals / etc.

In my game, each team has a ship which is represented by a model with the Ship tag. The ship gets populated with some code that must run on the server and some code which must run on the client.

It would be annoying for me to have some logic in the playerScripts that checks for new ships and runs code on them and then unbind all events when the ship is deleted and sort of defeats the purpose of an entity being populated with all its logic and required components.

Here’s how I imagine the problem solving process should work in this thread:

1 Like

Let me explain my whole predicament: I have a game where players can build ships. In order to avoid code repetition I created a system that would populate Instances with specific tags with specific parts. These parts could be scripts that should run on the client or server or just parts needed for visuals / etc. In my game, each team has a ship which is represented by a model with the Ship tag. The ship gets populated with some code that must run on the server and some code which must run on the client. It would be annoying for me to have some logic in the playerScripts that checks for new ships and runs code on them and then unbind all events when the ship is deleted and sort of defeats the purpose of an entity being populated with all its logic and required components. Here’s how I imagine the problem solving process should work in this thread:

Here’s an instance of when client scripting in the workspace could be useful: You have a turret enemy in your game. To make it feel more responsive, you make it so that it turns and faces your character on the client and perform damage on the server, that way, there’s no delay in between movement and the turret changing its aim direction. Here’s what a script parented to the turret model would look something like this (of course with checks for player character existence, etc):

game:GetService("RunService").RenderStepped:Connect(function()
    script.Parent:SetPrimaryPartCFrame(CFrame.new(script.Parent.PrimaryPart.Position, game.Players.LocalPlayer.Character.Head.Position))
end)

And here’s what a script inside the StarterPlayerScripts would look like

local CollectionService = game:GetService("CollectionService")

local function handle_new_turret(object)
    local render_stepped_event = nil
    render_stepped_event = game:GetService("RunService").RenderStepped:Connect(function()
          object:SetPrimaryPartCFrame( --[[ ... ]] )
     end)
     
     object.AncestryChanged:Connect(function()
        if not game:IsAncestorOf(object) then
             render_stepped_event:Disconnect()
        end
     end)
end

CollectionService:GetInstanceAddedSignal("turret"):Connect(handle_new_turret)

for _, object in pairs(CollectionService:GetTagged("turret")) do
     handle_new_turret(object)
end

The first script seems way more intuitive and simpler.

I don’t want client server communication, I want code to be ran directly on the object without the required boilerplate of having scripts in PlayerScripts. See my turret example on my super long post for an example of what I want and well as a clarification to my initial question.

There’s technically no difference between the Workspace on the client and the Workspace on the server to the engine. The network replication is different obviously but for the most part it’s a simple issue of LocalScripts not being able to identify what client they belong to unless ran within that environment. In the current client-server model, it could probably be altered to run everywhere, but LocalScripts were implemented far before network filtering was added and changing them now would break pre-existing behavior. Hopefully that answers your X problem.

As for your Y, my official suggestion would be to do what you’re doing because it’s the official supported method. Events by modules should be garbage collected if the Instance they’re connected to is destroyed so if you really wanted you could use BindableEvents that have no parent and then destroy them when the module was removed. That’s a bit more effort, but if you set up a factory for it it wouldn’t be too bad on a large scale. As for them not stopping their running instantly, there’s nothing to be done about that.

Another solution that’s less official would be to use normal server Scripts that are ran only on the client (probably by setting their Disabled property to false or being parented to Workspace from the client). That doesn’t work if you need access to client specific Instances or methods, but that’s the best hacky method I can think of. EDIT: This doesn’t work because Script Instance sources don’t replicate. That’s my bad.

4 Likes

@Dekkonot how are Scripts supposed to run on the client? Last time I checked, they were only supposed to be ran on the server and LocalScripts were only supposed to run on the client.

I don’t know if that actually works, but I assume he means having a server script that is disabled on the server, then re-enable it on the client (which won’t replicate to the server). But at the same time, normal scripts shouldn’t send their bytecode to the client…

@fireboltofdeath is correct. I forgot that normal scripts don’t replicate their source. As a result, that won’t work.

In that case, you’ll probably want to go with ModuleScripts. With the BindableEvent setup I mentioned before, the only remaining problem is with the script continuing to run after being destroyed, and I believe you can solve that using coroutines. I’m not sure if the bug with yielding modules applies in this situation however, so you’ll have to test that approach.

1 Like

I wasn’t here before FE wasn’t common so I don’t think I really understand how scripts work.

From what I understand, Scripts run on the server when the following conditions are met (according to developer docs):

  • Disabled property is false
  • The Script object is a descendant of the Workspace or
    ServerScriptService

… and LocalScripts run on the client when the following conditions are met (again, according to the developer docs):

The script is a descendant of:

  • A Player’s Backpack, such as a child of a Tool
  • A Player’s Player/Character|character model
  • A Player’s PlayerGui
  • A Player’s PlayerScripts.
  • The ReplicatedFirst service

Please correct me where I am wrong.

LocalScript sounds about right, unless I forgot any.

However, scripts can run in a lot more places than workspace and ServerScriptService. They can even run in a Tool in a player’s backpack.

That actually was my initial solution to the problem. It worked when I tried adding some connections to a BindableEvent and repetitively calling it but for some reason, it didn’t seem to work with RunService.RenderStepped. Thoughts?

I don’t see any reason why it shouldn’t be working though, unless you’re doing something unexpected. I presume there’s no output or anything. What exactly is going wrong?

Here’s my project

StartPlayer/StarterPlayerScripts/LocalScript

local script_obj = workspace.ModuleScript

local c = coroutine.create(function()
	require(script_obj):run()
end)

coroutine.resume(c)
print("Started!")

script_obj.AncestryChanged:Connect(function()
	if not game:IsAncestorOf(script_obj) then
		print("Deleted!")
		coroutine.yield(c)
	end
end)

-- Wait 1s and delete the script
wait(1)
script_obj:Destroy()

Workspace/ModuleScript

local module = {}

function module:run()
	game:GetService("RunService").RenderStepped:Connect(function()
		print(script:GetFullName())
	end)
end

return module

Client Output:
32%20PM

That seems unexpected. Given it’s doing the same thing even when I ensure the coroutine surrounding it is dead, I assume this is an issue with it being a signal that’s connected to a live Instance. I was able to fix the problem by manually disconnecting the event when the ancestry of the ModuleScript changed, which is cumbersome. You might consider using BindToRenderStep and UnbindFromRenderStep in this case (though attempting to unbind multiple functions by the same name throws a warning, it does work just fine) since they don’t require references to the connection object.

1 Like

A post was merged into an existing topic: Off-topic and bump posts

I found an even better way where LocalScripts mirror the ModuleScripts contained in the `workspace.

The process can be summarized as so:

  1. The helper detects when a module script tagged with “LocalScript” is added to the workspace.
  2. A mirror script is created containing an ObjectValue reference to the script it will be handling
  3. The mirror LocalScript starts a new coroutine which requires and runs the actual ModuleScript and an even is registered to listen for the destruction of the module script
  4. When the previously mentioned event is fired, the coroutine is yielded and the mirror LocalScript is deleted.

Code will be posted soon.

1 Like

Ok, here’s the code implementing this solution. Note: it turns out that you don’t even need the coroutines I was talking about.

Usage:
Runs the module:run() method on all module scripts tagged as “LocalScript” who are descendants of game.Workspace or game.Players.


StarterPlayer/StarterPlayerScripts/WorkspaceClientScripts

--[[
	WorkspaceClientScripts
	Author: radbuglet
--]]

-- /// Services ///
local CollectionService = game:GetService("CollectionService")

-- /// Constants ///
local LOCAL_SCRIPT_TAG = "LocalScript"
local APPLICABLE_ANCESTORS = { workspace, game.Players }

local MIRROR_SCRIPTS_CONTAINER_FOLDER = script
local MIRROR_SCRIPT_TEMPLATE = script.MirrorScriptTemplate

-- /// Helpers ///
local function run_for_each_tagged_instance(tag, callback)
	local instances_detected = {} -- @WARN there's a possible memory leak here because deleted instances are never cleaned up
	
	local function wrapped_callback(object)
		-- Temporary patch for double firing error detailed [here](https://devforum.roblox.com/t/double-firing-of-collectionservice-getinstanceaddedsignal-when-applying-tag-in-same-frame-that-object-is-added-to-datamodel/244235?u=radbuglet)
		if instances_detected[object] == true then return end
		instances_detected[object] = true
		
		-- Check if is descendant of applicable ancestor
		for _, ancestor in pairs(APPLICABLE_ANCESTORS) do
			if ancestor:IsAncestorOf(object) then
				callback(object)
				break
			end
		end
	end
	
	CollectionService:GetInstanceAddedSignal(tag):Connect(wrapped_callback)
	for _, object in pairs(CollectionService:GetTagged(tag)) do
		wrapped_callback(object)
	end
end

-- /// Setup ///
run_for_each_tagged_instance(LOCAL_SCRIPT_TAG, function(object)
	if not object:IsA("ModuleScript") then
		error("Objects tagged with " .. LOCAL_SCRIPT_TAG .. " must be module scripts!")
		return
	end
	
	local mirror_script = MIRROR_SCRIPT_TEMPLATE:Clone()
	mirror_script.Name = object:GetFullName()
	mirror_script.Disabled = false
	
	local master_script_value = Instance.new("ObjectValue")
	master_script_value.Name = "MasterScript"
	master_script_value.Value = object
	master_script_value.Parent = mirror_script
	
	mirror_script.Parent = MIRROR_SCRIPTS_CONTAINER_FOLDER
end)

StarterPlayer/StarterPlayerScripts/WorkspaceClientScripts/MirrorScriptTemplate (Must be Disabled!)

--[[
	WorkspaceClientScripts/MirrorScriptTemplate
	Author: radbuglet
--]]

local master_script = script.MasterScript.Value

master_script.AncestryChanged:Connect(function()
	if not game:IsAncestorOf(master_script) then
		script:Destroy()
	end
end)

require(master_script):run()

Please tell me if there are any bugs I need to fix or an easier way to fix my problem.

Thanks,
rad

2 Likes