Problem X: I’m trying to use LocalScripts in the workspace to allow for client prediction, used for things like click detectors and other game world user input requiring an immediate response. I would prefer to put this logic directly in the workspace because of multiple reasons:
It makes more sense to have logic relating to the game world inside the workspace
When the parent object is deleted, the script’s events should also be deleted
A million more reasons that would require a lot of background explaining which I will omit from my post for simplicity (however, I can explain if you really want more reasons as to why I would like to have LocalScripts running in the workspace)
I can’t really understand why Roblox disallows this because my workaround (detailed in Problem Y) works quite well but if there’s an oversight on my part on why LocalScripts shouldn’t run on the workspace, please tell me.
Problem Y: My solution to the problem was to use tagged module scripts that when detected, would get required and the returned module:run() function would be called. However, there are a couple of problems with this solution:
All events being created by the script must be manually unbound upon script deletion, this isn’t needed with Scripts and LocalScripts.
They don’t immediately stop their execution upon script deletion.
Please help me solve Problem X, Problem Y or at least explain to me why LocalScripts shouldn’t run on the workspace.
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 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:
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):
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.
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?
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
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 LocalScripts mirror the ModuleScripts contained in the `workspace.
The process can be summarized as so:
The helper detects when a module script tagged with “LocalScript” is added to the workspace.
A mirror script is created containing an ObjectValue reference to the script it will be handling
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
When the previously mentioned event is fired, the coroutine is yielded and the mirror LocalScript is deleted.