As you may all know by now, Knit is a framework that has been archived, and which has stopped receiving updates and support.
Despite this, almost on a bi-weekly basis new posts show up asking about the current state of this resource, and whether it should be used or not.
I have created this post as an attempt to answer some of these FAQs and to lend some advice going forward, given how frequent I come across them.
I also want to clarify that I’m clearly not an actual spokesperson and that these are mostly my own personal takes and advice surrounding this topic.
Feel free to click on these dropdrowns for some more details on these questions and some high quality yapping
Should I use Knit?
TLDR: If you don't care about autocomplete, feel free to use Knit
I would like to start by saying that Knit will not stop working anytime soon. Many, if not most top games are still using Knit, and while its true that there is a movement to push away from it, this wasn’t always the case.
Discussions about Knit being outdated didn’t begin until Roblox decided to implement Intellisense (luau autocomplete), which fully released around early 2022. This is to say that it is a relatively recent feature.
Despite this, as Roblox has kept frequently updating this feature and releasing new ones over the years (such as parallel luau), which are by nature incompatible with how Knit was written; You can start to get a picture of why this framework may no longer fit in.
The environment for developing Roblox games is simply heading towards a different direction than what Knit can offer.
Its creator talks about this with more depth in their blog.
Regardless, If you were to ask my opinion about this situation, I think that parallel luau has mostly niche use cases. So if you truly don’t care about autocomplete, then there’s no major reason to avoid using Knit.
(and don’t feel bad about it either, there’s a case to be made against luau’s autocomplete, but that’s a topic for another day)
Should I learn Knit?
TLDR: If you're learning how to roblox dev, sure!
The most common response I’ve come across for questions regarding using or learning Knit is a cold and unhelpful “just use ModuleScripts”.
Now, I absolutely agree with what is meant with this response. What they’re trying to say is that nothing of what Knit provides is something that a low level use of ModuleScripts can’t do.
This is simply true, as a very dumbed down explanation of what Knit “essentially does” is to require the ModuleScripts for you, and then store them in a table that you access through Knit.GetService()
.
This will error if Knit hasn’t “made sure they are ready” yet, or in other words, finished calling all the :KnitInit()
methods in your modulescripts.
This provides a nice, clean and ordered way to interact with your different modules, and allows for less thought put into how your modules can access logic from one another. It provides structure.
The problem is, How can you expect someone who has no background in any form of software development to know how to structure their games, let alone their modulescripts and code?!
This is where you can make a case for learning Knit. I don’t think you should seriously expect for young developers, that are probably still in highschool, to be nose deep in literature about Java frameworks and OOP to the extent that they can figure out intuitively what their modulescripts should do.
For love’s sake, they probably just learned to communicate between scripts via bindable functions. (also sorry, but you’re never using that again. seriously, no one uses that.)
So sure, in this way Knit can be educational in a sense, not that I’m arguing that its the definite way to learn roblox development. I don’t think there is such a thing.
Btw, if you want to work for someone else in the dev space you should definitely know Knit.
How do I move on?
I. Harnessing the power of "just use modulescripts"
So you’ve decided to take the modulescript pill and see what the fuss is all about. Great!
You see, the biggest realization regarding module scripts is noticing the full extent to which you can use this feature:
ModuleScripts run once and only once per Luau environment and return the exact same value for subsequent calls to require().
In simple terms this means that the result of a modulescript is always cached/stored after the first require()
:
--ModuleScript
local arbitrary_timestamp = os.time()
return arbitrary_timestamp
--script 1
local t = require(ModuleScript) --module first execute
print(t) --1744653460
--script 2
task.wait(10)
local t = require(ModuleScript) --gets first execute value
print(t) --1744653460 <- the same value!
This knowledge is insanely useful! as using this cache to your advantage allows you to simulate the functionality of access modifiers in luau.
Observe the following example of a “PlayerData” Service created entirely from a single modulescript.
--PlayerDataService modulescript
--//INIT Logic//
local Players = game:GetService('Players')
local Data = {}
Players.PlayerAdded:Connect(function(player)
local new_data = {}
new_data.coins = 0
Data[player] = new_data
end)
Players.PlayerRemoving:Connect(function(player)
if Data[player] then Data[player] = nil end
end)
--//
--//Service Logic// (actually called the "interface")
local service = {}
--return only the values
--so no one can directly modify the original table
function service.getCoins(player: Player)
return Data[player].coins
end
--only allow data to be edited
--through functions that validate the arguments
function service.addCoin(player: Player, amount: number)
assert(type(amount) == 'number' and amount == amount)
amount = math.abs(amount) --only allow positives
Data[player].coins += amount
end
return service
With this method, the Data
table that holds all of the player data from our game is never able to be accessed directly from a script! In other words, it is now private. You must use one of the functions from the “service” part of the code to interact with it (which is actually called the interface).
And now lets create a coin that adds +1 on pickup using our service!
local coinpart = script.Parent
local Players = game:GetService('Players')
--get our new service from a require
local PlayerDataService = require(Path.To.PlayerDataService)
coinpart.Touched:Connect(function(toucher)
local player = Players:GetPlayerFromCharacter(toucher.Parent)
if not player then return end
--add a coin!
PlayerDataService.addCoin(player, 1)
end)
And just like that, we have recreated some basic functionality of Knit.GetService()
and created our very own DataService!
“But arbi! What about the client?” I might hear you say.
“How can I do the networking like how Knit does it?”
“Can I just do service.Client.addCoin()?
”
Sadly, No, you cannot just do that. That is some unique built-in functionality of Knit that allows you to quickly create functions accesible from the client in such manner.
However, this is where using only modulescripts grants you the freedom to define your own networking solution. Which is actually one of the major reasons why developers eventually move away from Knit.
Theres an entire catalogue of networking libraries available to use, including Knit’s!
Refer to chapter II. for more details.
II. Choosing your networking library
Before I start, you could potentially be wondering what I’m even talking about and why use anything other than Remote Events.
At the end of the day, you are still going to use them, despite the complexity of any library that wraps ontop of them and whichever claims of what they do.
Knit uses a library named Comm
made by the same person. They also made another library named Net
. Other options available that I can list off the top of my head include Warp, Bytenet, Red, etc.
The reasons as to why these exist are varied, but there is one reason that all of these share that boils down to this:
Using child paths to reference RemoteEvent objects sucks. Having to create them in the editor and drag-and-drop them in ReplicatedStorage sucks, and attempting a code solution for this process sucks even more.
No one likes having to treat remote calls as instances of our game’s hierarchy! Its very bad for code modularity and portability, and its just lame and messy.
Instead, we would much rather just entirely handle requests through code, in a manner easily accessible for both server and client.
Having said that, I need to pick one of these libraries to showcase how to set them up in a knit-less project. It really doesn’t matter which you pick, but for the purposes of simplicity I’m choosing Net
. (because its way easier to set up than Comm
)
First, create a modulescript in ReplicatedStorage named network
, this is the module we will require each time we want to define a request.
within this modulescript, initialize the requirements for whichever network library you are using, in my case Net
:
-
use
net:RemoteEvent(name)
to create a remote event -
use
net:RemoteFunction(name)
to create a remote func
The code in my modulescript using Net
will look like this:
local net = require('./dependencies/Net')
return {
getCoins = net:RemoteFunction('getCoins'),
addCoins = net:RemoteEvent('addCoins')
}
Now we must bind these remotes to our service! (which we defined in Chapter I.)
--PlayerDataService
local Players = game:GetService('Players')
local Data = {}
--require our modulescript
local network = require(game.ReplicatedStorage.network)
Players.PlayerAdded:Connect(function(player)
...
end)
Players.PlayerRemoving:Connect(function(player)
...
end)
--//Service Logic//
local service = {}
function service.getCoins(player: Player)
return Data[player].coins
end
function service.addCoin(player: Player, amount: number)
assert(type(amount) == 'number' and amount == amount)
amount = math.abs(amount)
Data[player].coins += amount
end
--bind your logic to the remotes!
network.getCoins.OnServerInvoke = function(player)
return service.getCoins(player)
end
network.addCoins.OnServerEvent:Connect(function(player, amount)
service.addCoin(player, amount)
end)
return service
Given all of this, we can now use these remotes in our client!
--localscript UI
local textlabel = script.Parent
local network = require(game.ReplicatedStorage.network)
function updateCoins()
textlabel.Text = network.getCoins:Invoke()
end
updateCoins()
III. What about the lifecycle events?
If you’re a keen observer, you may be wondering about where the :KnitInit()
and :KnitStart()
functionality has gone, given that you’d like to make sure all your services are ready to be used when you fetch them.
With our service code so far, this is already the case, but perhaps not in a manner you’d like.
If for some reason the code within your service yields (like for example, it includes a task.wait(60)
). Then the entire thread which is requesting this modulescript will yield until it resolves.
Knit gets around this by providing a Promise
that resolves when all Services finish executing their :KnitInit()
methods. You may recall this as Knit.OnStart()
.
This was made using behavior which later became the Loader
library, which again, is by the same creator. This library loads all the modulescripts and its descendants under a specified folder, and then executes their lifecycle methods
Loader.SpawnAll(Loader.LoadDescendants(MyModules), "OnStart")
You could use this library to quickly replicate this behavior on our services, or alternatively, you could use Axis
.
Axis is a library made by the creator of Knit, which is literally just a module loader with lifecycle events and no dependencies.
This is interesting because if you’ve ever taken a peek into the Packages folder that comes bundled with Knit, you would have seen a whole lot of libraries which you may or may not use.
You will feel right at home with Axis if you’re coming from Knit. It has Axis:OnStart()
, services are called Providers
and instead of :KnitInit()
you :AxisPrepare()
.
Although, that’s all Axis is. It doesn’t come bundled with any additional libraries such as a networking library, meaning to use it you must consider the advice given on Chapters I. and II.
Finally, I would like to conclude by saying that there’s no arguably “best way” to develop games in Roblox, let alone games in general. Considering that Roblox grants us tools to directly use scripts as instances in our game’s hierarchy, it really opens up the door to the posibilities of how one may structure their game and code.
If you want to see some unconventional practices you could check out how the old Roblox gears were scripted, or read up on some obscure facts about Roblox’s Humanoids, like how parenting a StringValue under the character with certain special keywords will play an animation and then add them to debris (which is how the LinkedSword works.)
You could say its a coincidence that the direction the modern roblox dev environment is heading towards is that of Java Frameworks and their OOP patterns. The best advice I can give you is to learn a bite sized amount of everything. It will make you a better programmer at the end of the day.
Also, If you encounter any factual errors or otherwise within this post feel free to point them out.