My game has been made on a framework that uses the shared global to store data and utility functions across scripts.
How it works is, at the start of the game, a loader module loops through all other modules, and requires them into a table in the loader module. Then, we use a function shared.get(“name”) which then returns the module with the name out of the local table. It essentially allows us to replace require(modulePath) with shared.get(moduleName), which saves a lot of time, and is very convenient.
We also use it for storing frequently accessed folders in the DOM.
The “cache” table is fed with all the required modules, and then shared.get is set to a function which indexes the required module out of the table and returns it.
Recently, I decided to make the switch to parallel, and it turns out that shared doesnt work the same way. I tried to use sharedTable, but it turns out you cannot store functions or any non-serializable data in them.
Actor:SendMessage would not work, because we need each script to access a common place. I thought of sending the local table from the module to each actor, but scripts also manipulate values inside the shared object and thus it would require me to replace each instance of shared with firing bindables and other things to make the change replicate to other scripts.
Ran into this problem when I tried using actors. Honestly don’t think its worth it to use Actor because of the reason you listed. If you really want to use Actors I’ve heard you can call a BindableEvent and run the functions there, though if I were you, I’d just not use Actors.
I completely agree however our game has an AI npc system and we could be running 100+ AI at once which is also key to the gameplay so I would REALLY benefit from it
Worst comes worst, you use shared in serial and the rest of your expensive processing in parallel.
I’m not sure what (if any) implications this would have, but from my understanding it makes the most sense.
Maybe keep using SharedTables whilst in parallel, then when you have a value you need to share that is a function/Instance/etc. you can do that part in serial (i.e., task.synchronize, maybe using task.defer too so it doesn’t yield your current thread, unless you need it to).
yes, that was the plan. but it turns out that shared becomes nil as soon as you put a script under an actor. it has something to do with the lua VM thing, actors make an isolated environment for each script and shared becomes nil for the script. So its literally impossible, even if i run the full script in serial using task.sync
Then you might be forced to use BindableFunctions and BindableEvents unfortunately.
The only other way that could maybe work is a shared ModuleScript outside of an actor for non-parallel storage only.
Side note - why are you against using require here? I think it’s better than having a special cache, since require’d modules are already cached when on the same side of the network boundary.
Edit 2 - also instead of that you can have one central module that loads the rest of them, effectively replacing your need for shared.
True, but is it worth the hassle of figuring out this whole mess with shared?
The performance difference AFAIK is beyond negligible. (Accessing global shared & indexing it whilst waiting for values to exist vs. require cache lookup with execution if it didn’t run already)
Since you’re already waiting for a module which hasn’t been loaded to be in the cache, using one main module is functionally the same - using require on a ModuleScript that synchronously loads each dependency module guarantees they’re ready all at once
Your entire game’s backbone is built on a textbook race condition if you do this. There’s no guarantee that the script that registers shared.get runs before the scripts that index and call it. Even if you jerry-rigged a system that lazily executes a bunch of modules at startup to avoid this, what have you accomplished over using a single-script architecture?
You also lose all type and autocomplete info this way. Vanilla require has first class support for these by contrast.
No need to jump to conclusions so quick.
Shared.get is set before any other modules are required, and all scripts are disabled only to be enabled after that. The system is robust and extremely predictable.
I agree, im fully aware of the features require provides. I guess we’ll use some sort of bindables and event system to fix it.
I just had an issue with parallelism & modules. Beware: actors create completely new instances of modules when they’re required.
Simple test you can do to see this is have a shared module that returns a table, have one script require it from inside an Actor and one script require from outside, and print their string representations (i.e., print(tostring(requiredMod)). You’ll see two different memory addresses.
Sorry to bump, but I felt that this was necessary.
Thank you very much for the bump! I appreciate you giving the information. Yes, I was already aware of that. Fortunately, all code in our modules is wrapped inside .init functions so the actor simply duplicating the instance and requiring the module doesn’t actually run any functions, just caches them in the actor’s VM.
Your message will still be useful to future readers who stumble upon this.
I forgot to update this topic, but I have managed to find a workaround for that.
I realized that only the code for the alien AI had to be run parallelly in different actors; rest of the code can run outside of actors.
For the code inside the actor, we make a special actor initialization method in our loader. When the actor is setup, a script inside it runs the loader and caches the required modules for the actor’s VM; then deletes itself.
This allows us to continue our workflow of using shared while also getting benefits of parallel lua.
I don’t want to mark my own reply as solution, since it is usually frowned upon; Hence I will leave this here.
Thank you to everyone who contributed in the thread above.