Firework, a general purpose framework

Firework :fireworks:

As I’ve worked on games throughout the years, I’ve developed a framework to simplify what I actually need to write to wire up a game. It’s lightweight and battle-tested over a variety of projects, prioritizing easy script communication and networking.

Get it here - Firework, a general purpose framework :fireworks: - Creator Marketplace (roblox.com)

Features

  • Automatic remotes
  • Automatic submodules
  • Runtime module registration
  • Finding a module by name
  • Loading order control
  • Start, which runs after all scripts have been initialized
  • Customizable initialize and start function names

Usage and Setup

To get Firework up and running,

  • Place it somewhere both the server and client can find, like ReplicatedStorage
  • Create a server script that calls the initialize function, for example:
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Framework = require(ReplicatedStorage.Framework)

Framework.Initialize(
	script.CollisionGroups,
	script.ChunkSystem,
	script.MineSystem,
	script.PlayerTracker
)

Like shown, send initialize a reference to each module you want to register. Do not use curly braces for a table. On the client side, it might be worth using WaitForChild.

The order that you send here will be the order that each module is loaded in, where their initialize and start functions will be called in the same order.

Note: Creating a client side is optional, but you would just run the same code in a client script.

This is an example of a setup I like to use. I find it helpful to name the scripts “Client” and “Server” respectively to help with identifying where errors occur.

Script Communication

By default, modules are registered by name. So, when “PlayerTracker” is registered in the above example, I can find it in any other registered module by using the Get feature:

function MineSystem.Initialize()
	MineSystem.PlayerTracker = MineSystem.Framework.Get("PlayerTracker")

Of course, you don’t have to store Get results the same way I did here. However, initialize is the intended place to do it. Make sure your Get calls run after they have been registered!

You can also register things during runtime using RegisterRuntime. This will immediately call initialize, then start.

function MineSystem.Initialize()
	MineSystem.Framework.RegisterRuntime(script.Mine, "MineClass")

For demonstration, I added the name “MineClass” to Mine. This is an optional argument for RegisterRuntime that allows you to provide custom names. Now somewhere else in code, I can retrieve this module with the same name:

... Framework.Get("MineClass")

Remotes

Remotes are set up using predefined table names. By default, these names are “Server” and “Client.”

It helps me to think to use the opposite context that the script is running in. If a script was running on the client, I would add a Server table for server-accessible functions.

In this example, MineSystem is on the server side. So, if we want to give the client access to a server function, we could do it like so:

local MineSystem = {}
MineSystem.Client = {}

function MineSystem.Client.Mine(player, position)
...

And for the client to run this code, we call to the framework:

...Framework.Fire("MineSystem", "Mine", position)

First provide the module name, then the function name, then the arguments. If you are calling this from the server side, the first argument provided will act as the target player.

The framework has a few options for using remotes. For the client you only have Fire and Invoke.
But on the server, you gain FireAllClients and FireAllExcept.

In the case of FireAllExcept, the first argument is the player not to fire.

function Framework.FireAllExcept(player, moduleName, remoteName, ...)

Notes

  • You can find some preferences in the Settings module
  • A “Framework” reference is added to any registered module
  • All modules are registered before any initialize calls are made
  • The server must initialize the framework if the client wants to use it
  • Any functions made available to the client will pass the player argument first
  • I may write documentation in the future, but the function names and arguments are obvious

Why release it when there are other frameworks?

I want to show off my work! Also, a lot of frameworks are clogged with useless features that just make it impossible to make sense of. It does exactly, only what I want, and I understand it at a glance. I want to know how every aspect of my game’s codebase works. I would recommend creating one of your own if you also feel this way.

Releases I’ve used this framework in:

Super Speed Run - Roblox
Power Fighting Tycoon - Roblox
Anime Fighters Tycoon - Roblox
send memes to your enemies to destroy them tycoon - Roblox
Defend your Christmas Tree! - Roblox
Game Recommender - Roblox
Hexagon Smash - Roblox

Updates

Submodules

Submodules Update

Added support for submodules! You can now register modules as part of other modules.

To add submodules, include a Submodules table:
(the keyword for this can be changed in settings)

local Player = {}

Player.Submodules = {
	script.DataStoreHandler,
	script.PurchaseHandler,
	script.Character
}

It uses the same register system as regular modules. The load order is done module first, then submodules. Finally, the submodules table is replaced with a dictionary.

If you want a more advanced way to find these submodules, you can provide a callback instead of a table.

local EffectSystem = {}

EffectSystem.Submodules = function()
	local submodules = {}

	for _, module in pairs(script:GetChildren()) do
		table.insert(submodules, module)
	end

	return submodules
end

To get submodules, the Framework.Get method has been upgraded! You can now provide a chain of submodules after the initial module name.

Player.Framework.Get("Player", "Character")

If you want to perform a deep search, you can keep adding arguments.
(I haven’t extensively tested this, let me know if this has issues)

CombatSystem.Framework.Get("CombatSystem", "Finders", "GetPlayers")

Register and RegisterRuntime have also been upgraded! You can now pass a table to register the given module into. This is optional only for RegisterRuntime, where no table will default to Framework.Modules as usual.

Framework.RegisterRuntime(module, name, modules)

For example:

Character.Framework.RegisterRuntime(script:WaitForChild("Local"), nil, Character.Types)

Some other quality of life changes were made, including more descriptive errors. Let me know if you run into any problems!

Remotes yielding during loading

Remote Queue Update

I've made a small change that queues up server-to-client remotes while the framework is loading. I ran into an issue where the server was telling the client to do things that weren't loaded yet, so I added this setting as a remedy.
QueueRemotesDuringLoading = true

In addition to this, you can now check if the framework is loaded or wait for it to finish loading.

-- to wait for loading:
Framework.WaitForLoaded()

-- to check if loading has completed:
if Framework.IsLoaded then
	-- some code here
end

Note that IsLoaded is not a function, but a boolean property. That’s all for now!

UnreliableRemoteEvent Support

UnreliableRemoteEvent Support

With the introduction of the UnreliableRemoteEvent, I have added new syntax for marking remotes as unreliable. They are handled separately from normal remotes as to prevent functions marked as unreliable from being called reliably, and vice-versa.

To mark a function as unreliable, the new default keyword for this is “Unreliable” and is modifiable in the Settings module. Here’s an example:

local Test = {}
Test.Unreliable = {} -- notice that we need to create this table manually

function Test.Initialize()
	-- dummy function
end

function Test.Unreliable.Print(player, text) -- can only be called unreliably
	print(text)
end

return Test

If we assume the above is server code, we can call it from the client using the new FireUnreliable

Test.Framework.FireUnreliable("Test", "Print", "hello")

This syntax works on both the client and the server. However, the server must still provide a player argument for firing remotes. The server also has access to a few new unreliable methods:

Framework.FireAllClientsUnreliable(moduleName, remoteName, ...)
Framework.FireAllExceptUnreliable(player, moduleName, remoteName, ...)

These are variations of FireAllClients and FireAllExcept and function the same way, just using unreliable remotes. I am excited to see the applications of this new feature!

FireAllClientsNearby

FireAllClientsNearby

Today I added a new function out of necessity. You can now call Framework.FireAllClientsNearby from the server, and you can also provide a callback to check distance if you need. This was added to try and reduce network usage for players far from an event that don’t necessarily need to be replicated to.

function Framework.FireAllClientsNearby(position, radius, callback, moduleName, remoteName, ...)
	callback = callback or function(player, maxDistance)
		if not player.Character or not player.Character.PrimaryPart then
			return
		end
		
		local distance = (player.Character.PrimaryPart.Position - position).Magnitude
		
		return distance < maxDistance
	end
	
	for _, player in pairs(Players:GetPlayers()) do
		if callback(player, radius) then
			Framework.Fire(moduleName, remoteName, player, ...)
		end
	end
end

In code, the default callback just tests character primary part distance from the given position. If you want to use the default, just provide nil to the function. The rest of the arguments are passed to the usual Framework.Fire method.

I will update this framework regularly as I continue to develop it for new projects. Feel free to suggest ideas or changes! :slight_smile:

34 Likes

Flamework in roblox-ts But for lua i see.

4 Likes

Nice framework,
Loving it so far but could you make a YouTube video with a couple good examples on why and how to use the framework?

1 Like

Honestly really cool. Seems similar to knit. Is it inspired by knit?

1 Like

Maybe, but all of the information you would need is in this post. It’s a very simple framework that gives most of the power to individual modules.

@kiloe2 Yeah, it’s a lot like Knit, but it’s a lot lighter. It was designed to solve problems I encountered while developing various projects.

2 Likes

Update: Added an assert to the Get method to catch some mistakes.

function Framework.Get(name)
	assert(Framework.Modules[name], name .. " is not registered")
	
	return Framework.Modules[name]
end
1 Like

Man I really love these frameworks that let you step and run code in order, ever since I found about these types of structures for writing games thanks to chickynoid, I dont think I could ever stop using it

So this framework goes to my list of cool frameworks (Canary engine, Knit, Firework)

2 Likes

Submodules Update

Added support for submodules! You can now register modules as part of other modules.

To add submodules, include a Submodules table:
(the keyword for this can be changed in settings)

local Player = {}

Player.Submodules = {
	script.DataStoreHandler,
	script.PurchaseHandler,
	script.Character
}

It uses the same register system as regular modules. The load order is done module first, then submodules. Finally, the submodules table is replaced with a dictionary.

If you want a more advanced way to find these submodules, you can provide a callback instead of a table.

local EffectSystem = {}

EffectSystem.Submodules = function()
	local submodules = {}

	for _, module in pairs(script:GetChildren()) do
		table.insert(submodules, module)
	end

	return submodules
end

To get submodules, the Framework.Get method has been upgraded! You can now provide a chain of submodules after the initial module name.

Player.Framework.Get("Player", "Character")

If you want to perform a deep search, you can keep adding arguments.
(I haven’t extensively tested this, let me know if this has issues)

CombatSystem.Framework.Get("CombatSystem", "Finders", "GetPlayers")

Register and RegisterRuntime have also been upgraded! You can now pass a table to register the given module into. This is optional only for RegisterRuntime, where no table will default to Framework.Modules as usual.

Framework.RegisterRuntime(module, name, modules)

For example:

Character.Framework.RegisterRuntime(script:WaitForChild("Local"), nil, Character.Types)

Some other quality of life changes were made, including more descriptive errors. Let me know if you run into any problems!

1 Like

May I know how Bindable events work with this framework?

There is no added functionality with bindables in this framework, just remotes. They can be used the same as usual.

1 Like

I am a little confused at how you would get other modules within one module and then use its functions throughout, are you able to provide a more detailed example to that than the one you gave above?

Also, I wanted to ask if there is a reliable way I could get the Player when I need to on the server side modules, or do I just have to use this?

 game.Players.PlayerAdded:Connect(function(player)
	player.ChracterAdded:Connect(function(char)
	end)
end)

In any module that has been registered, you gain access to the framework. For example, if you had a module named “PlayerHandler” like so, here are some ways to access other modules:

local PlayerHandler = {}

function PlayerHandler.Initialize()
    -- we know the module has been registered now, since initialize runs afterwards
    PlayerHandler.DataStoreHandler = PlayerHandler.Framework.Get("DataStoreHandler")
end

function PlayerHandler.SomeOtherFunction()
    -- As long as we call this after PlayerHandler has been initialized,
    -- we can use PlayerHandler.Framework to reference the framework and get
    -- other modules. Note that DataStoreHandler must also be registered.
    local DataStoreHandler = PlayerHandler.Framework.Get("DataStoreHandler")

    -- now we have two ways of accessing the DataStoreHandler
    -- one from Initialize:
    PlayerHandler.DataStoreHandler.Save()
    -- and one from this function:
    DataStoreHandler.Save()
end

return PlayerHandler

Now that we have a reference to DataStoreHandler, we can make calls to it as if we required it. For example: PlayerHandler.DataStoreHandler.Save()

For completeness, here’s an example of initializing the framework with these modules:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Framework = require(ReplicatedStorage.Framework)

Framework.Initialize(
	script.PlayerHandler,
	script.DataStoreHandler
)

The load order here doesn’t have any significant impact on when modules are registered. However, it does determine the order in which the initialize functions are called. Just keep that in mind.

I like to make these Get calls in the initialize function, because we can assume everything else in the framework has already been registered. This is not the case when using RegisterRuntime, which is why I did the submodules update.

For the player, yes you will have to set up your own player handler. This framework is light and does not provide inbuilt systems for things like that as of now. It’s just meant to augment and automate some backend work.

1 Like

Alright I get it now, thank you!

The problem with this framework is Intellisense same as knit getting module by name is not good at all as it defeats Intellisense overall good framework I love it and I hope it improves to the limitless!

The purpose of switching to names is to do away with parent chains, or just script paths in general. It’s a tradeoff, but one I’ll take over having to do something like this over and over:

local EffectSystem = require(script.Parent.Parent.Parent.EffectSystem)

Another issue with this method is that you have to change the path depending on what script is using it. If you wanted the example EffectSystem in another module, you might have to modify that path each time you copy and paste it. With names, you can copy and paste the Framework.Get method to any registered module.

But yeah, no intellisense. Maybe one day we’ll have built-in AI that can figure it out at a glance.

1 Like

Remote Queue Update

I’ve made a small change that queues up server-to-client remotes while the framework is loading. I ran into an issue where the server was telling the client to do things that weren’t loaded yet, so I added this setting as a remedy.

QueueRemotesDuringLoading = true

In addition to this, you can now check if the framework is loaded or wait for it to finish loading.

-- to wait for loading:
Framework.WaitForLoaded()

-- to check if loading has completed:
if Framework.IsLoaded then
	-- some code here
end

Note that IsLoaded is not a function, but a boolean property. That’s all for now!

It’s a nice framework and it’s lightweight.

  • Has no benefits to using anything else
  1. No Intellisense support, a key major role in 99% of workflows
  2. Slow remotes, uses default remotes which are terrible at packet management

Should you use it? Probably not, would only recommend in small projects.

Also may I add this is very redundant, why wait for it to load and check if it’s loaded? If you waited for it then you can guarantee its loaded.

Should also be a Future to remove the fact of auto yielding (yucky)

  • Has no benefits to using anything else

Maybe? There are a few features here like automatic submodules that aren’t included in any frameworks I’ve come across before. It’s relatively new and being expanded upon frequently. Feel free to spark some ideas for me.

  1. No Intellisense support, a key major role in 99% of workflows

I usually end up looking at the functions I want to use before using them anyway, but yes this is a slowdown. You don’t get to see any type info you might have set. I don’t have a solution for this yet.

  1. Slow remotes, uses default remotes which are terrible at packet management

What other option do you have? Remotes are guaranteed to go through in the desired order and are never dropped. I’m curious what you mean by this.

Should you use it? Probably not, would only recommend in small projects.

Up to you. This is just my workflow, and I actively use it on big projects over years of development. To me, it seems like this framework lacks some things you want to use in your workflow.

Also may I add this is very redundant, why wait for it to load and check if it’s loaded? If you waited for it then you can guarantee its loaded.

This is just example code to show how to use it. This was a bonus feature from making remote calls wait until the corresponding remote handlers were indexed and findable in the framework. Might be useful for something that is not part of the framework.

Should also be a Future to remove the fact of auto yielding (yucky)

If you mean the remotes waiting for the framework to load, yes this is a setting. In practice, you will find that you want this on. Any remotes fired to the client during loading will error.

Overall, I don’t mean to criticize you for reasonable points. You bring up some issues that I want to look into, and I appreciate your time to judge what I’ve got here.

1 Like

UnreliableRemoteEvent Support

With the introduction of the UnreliableRemoteEvent, I have added new syntax for marking remotes as unreliable. They are handled separately from normal remotes as to prevent functions marked as unreliable from being called reliably, and vice-versa.

To mark a function as unreliable, the new default keyword for this is “Unreliable” and is modifiable in the Settings module. Here’s an example:

local Test = {}
Test.Unreliable = {} -- notice that we need to create this table manually

function Test.Initialize()
	-- dummy function
end

function Test.Unreliable.Print(player, text) -- can only be called unreliably
	print(text)
end

return Test

If we assume the above is server code, we can call it from the client using the new FireUnreliable

Test.Framework.FireUnreliable("Test", "Print", "hello")

This syntax works on both the client and the server. However, the server must still provide a player argument for firing remotes. The server also has access to a few new unreliable methods:

Framework.FireAllClientsUnreliable(moduleName, remoteName, ...)
Framework.FireAllExceptUnreliable(player, moduleName, remoteName, ...)

These are variations of FireAllClients and FireAllExcept and function the same way, just using unreliable remotes. I am excited to see the applications of this new feature!

1 Like

FireAllClientsNearby

Today I added a new function out of necessity. You can now call Framework.FireAllClientsNearby from the server, and you can also provide a callback to check distance if you need. This was added to try and reduce network usage for players far from an event that don’t necessarily need to be replicated to.

function Framework.FireAllClientsNearby(position, radius, callback, moduleName, remoteName, ...)
	callback = callback or function(player, maxDistance)
		if not player.Character or not player.Character.PrimaryPart then
			return
		end
		
		local distance = (player.Character.PrimaryPart.Position - position).Magnitude
		
		return distance < maxDistance
	end
	
	for _, player in pairs(Players:GetPlayers()) do
		if callback(player, radius) then
			Framework.Fire(moduleName, remoteName, player, ...)
		end
	end
end

In code, the default callback just tests character primary part distance from the given position. If you want to use the default, just provide nil to the function. The rest of the arguments are passed to the usual Framework.Fire method.

2 Likes