I understand that, my questions are:
- Is there any reason why it shouldn’t run on the
workspace
? - Is there any way to make them run on the
workspace
? - How can I fix Problem Y if the previous question is impossible
I understand that, my questions are:
workspace
?workspace
?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.
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
.
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.
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 Instance
s 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:
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 Instance
s 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.
@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.
I wasn’t here before FE wasn’t common so I don’t think I really understand how scripts work.
From what I understand, Script
s run on the server when the following conditions are met (according to developer docs):
… and LocalScript
s run on the client when the following conditions are met (again, according to the developer docs):
The script is a descendant of:
Backpack
, such as a child of a Tool
Player/Character|character
modelPlayerGui
PlayerScripts
.ReplicatedFirst
servicePlease 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:
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.
I found an even better way where LocalScript
s mirror the ModuleScripts
contained in the `workspace.
The process can be summarized as so:
ObjectValue
reference to the script it will be handlingLocalScript
starts a new coroutine which requires and runs the actual ModuleScript
and an even is registered to listen for the destruction of the module scriptLocalScript
is deleted.Code will be posted soon.
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