Let’s get down to business. Knit has its strengths and weaknesses. And what started out as a very simple game framework has expanded into something a little too complex for most use-cases.
Knit is the backbone of many Roblox games, contributing to millions of dollars of revenue every year. While I don’t have exact figures on that claim, I do know quite a few top-earning games that use Knit. This is not a bragging point, but rather a plea for people to listen and come along with me on a journey to understand how it came to be, where it is, where it could be today, and where I wish I could take it. As the developer of this framework, I feel that it is my responsibility to be its biggest critic, but also its biggest advocate.
History
Knit was not the first of its kind. In fact, not even the second. Knit is the third iteration of a framework structure that I started in 2015. Yeah, the third. There’s a good saying: “stop while you’re ahead.” Perhaps I should have heeded such advice. Back in 2015, I was fresh out of a 3-year internship (over the summers) at a large Fortune 500 company that loved its Java frameworks. This is where I got my first taste of MVC and service-controller architecture models. This is also where I realized that Roblox had no such frameworks.
The idea of a separation between the server and the client was, amazingly, a new concept in the Roblox ecosystem at this time (look up info on Roblox’s FilteringEnabled
property). There was a technical rift created between the server and client in Roblox games, and no one seemed to know how to best handle this. Roblox gave us new instances (RemoteEvents and RemoteFunctions) that had to be created and placed into the game hierarchy. It was maddening. In order to create a network bridge between the server and the client, I had to create a game object in my game’s hierarchy, and then write code to access it. What?! My Java-framework-filled brain went crazy. Surely this step could be skipped, right? No web framework required developers to place custom files into their project to represent web endpoints. You just write code to do it. So that’s when it clicked: I should create a module that creates those remote objects for the developer.
TCGF
A few days later, I had what I called the “Team Crazy Game Framework” (TCGF). Yeah, the name is a little verbose. But this is where I decided to pick the names “service” and “controller” for the core structure of the framework. The idea was simple: Services would act as singletons on the server (similar to built-in Roblox services, such as DataStoreService), and controllers would be the exact same thing, but on the client. The idea behind calling controllers “controllers” is that they help control client logic, and handle data input and output, similar to how a web framework’s controller would handle web endpoints. This isn’t always true in practice, but the naming distinction is helpful to quickly identify if we’re working on server-side or client-side code. I regret this naming schema, but I’ve yet to come up with anything better.
TCGF was, in retrospect, a respectable proof-of-concept. Albeit a trainwreck. But I’m a firm believer in trial by fire. The most important takeaway from TCGF is that it worked! Yes, it did exactly what I had hoped it would do. I didn’t have to create RemoteEvents or RemoteFunctions by hand! That ridiculous concept was swept away. Good riddance. The main issue, however, is that it didn’t allow for a clean startup procedure. If any service needed to access another service, it wasn’t possible to guarantee that said service was ready to be used. Let the race begin!
Race conditions are a terrible beast, and yet good coding practices make them disappear in the same way as moving your boat onto land will prevent sinking. Sure, you need to add wheels to your boat now, and a different engine, and all sorts of other things. But you’re not going to sink! You fixed the problem, right? Your boat-car is ready to go. In fact, you might as well have had a car to begin with. Do you see where I’m going with this? Things needed to be entirely reframed. I made a boat. It was sinking. So I took it onto land, and that’s where we got AeroGameFramework.
AGF
The successor to TCGF is the AeroGameFramework. Yes, it’s one word. Yes, it’s also verbose. Most people call it AGF instead, which is fair. AGF was an attempt to fix the pitfalls of TCGF by adding more structure. AGF had a strict service/controller model that enforced most code to always exist in one of these two structures. It also introduced lifecycles into the framework, which prevented the prior-mentioned race conditions with service-to-service or controller-to-controller communication. I had strapped wheels to my boat.
Along with the stricter model and lifecycle additions, AGF also had a robust networking setup, which made binding networking code to services super easy. Arguably too easy. But I saw how Spring Boot did this with their controllers, and I wanted in on the action.
AGF was also placed onto a public GitHub repository and introduced a lot of documentation. Oops. Little did I know that this would attract an audience. You see, I didn’t really advertise AGF. It was more of a personal project; again, a successor to TCGF. I did, however, post on my Twitter account. I posted how I was using AGF and how fun it was. People didn’t quite see that I had taken a boat out of the water and attached wheels to it. No, all that they saw was the shininess of a unique approach to Roblox game development. “What do you mean I don’t have to create remote objects myself?” gasped onlookers. “I can create my own services?” gawked by-passers. “Why on earth does that boat have whe—” NO, don’t look at that! It’s shiny, isn’t it? It even has a VSCode extension! The attention that AGF drew was too much for my ego. AGF was the way. Oh, you’re creating RemoteFunctions in Studio by hand? What are you, a noob? I bet you still think LUA is an acronym.
Then I did the unthinkable. I made a game. With my game framework. What a mistake that was. Can’t we just appreciate how shiny the boa — land vehicle is that I created? Nope. I made a game. What I learned about AGF was shocking (which is probably the click-bait title I could make if I turned this into a YouTube video).
From attempting to make a game with AGF, I quickly learned that the framework pigeon-holed developers into a very narrow band of programming design. If you were trying to write code that wasn’t in a service or controller, good luck. You might as well be writing code for an entirely different game at that point. Yes, this was AGF’s pitfall. Game code must exist within the AGF ecosystem. Trying to access AGF outside of AGF required going through the _G
global variable, which is basically a mortal sin in the Roblox game dev world. For every use of _G
, a baby zebra dies. Do you really want to live with that guilt?
But it was too late. Adoption set in. People were using it for real, profitable games. People were making money off of games that were built on AGF. What had I done? For a while, I debated this topic in my own head. “It’s ok, a rigid structure is actually good for game development,” I lied to myself. Eventually (after a couple years), it was clear that something needed to be done. But AGF was locked in. You couldn’t just unlock AGF from its strict boundaries. That’s like destroying the walls of a dam. The resulting flood would ruin everything. Ok, so we can’t destroy the dam. But, I thought, what if I could modularize AGF? What if the framework was just a simple ModuleScript that you could drop into your game and then build services/controllers off of? What if, I thought, I could attach wings to my demented car-boat and make it fly? And what if I gave it a trendy and short name that didn’t need to be shortened? And so Knit was born.
Knit
The name Knit came from the idea that you could “knit together” parts of your game with this fancy new module. In truth, the name came first, and then the “clever” explanation of the name came second, which is how any trendy open-source name should come to be.
Knit was a response to the shortcomings of AGF. See the trend here? Patches, band-aides, adjustments. Wheels on a boat. Wings on a car. I wasn’t considering what was needed, but rather what didn’t work. By stripping what didn’t work, I should arrive at what does work, right? Right??
One of the great things Knit offers is the modularity of the framework. Anyone coming from AGF will feel like they exited a coal mine with dead canary birds littered around and entered a meadow of wildflowers and dancing gazelles, with a cool breeze and singing birds all around. It was…refreshing.
So, you might be wondering, what’s the problem? It seems like Knit finally fixed some of the major problems. In fact, I did what any respectable programmer would do: I iterated; I refactored; I critiqued and changed; I rewrote; I took the bad out and put the good in. But the missing problem is that I had created a plane-car-boat. Yes, it doesn’t sink. Good. Yeah, it can fly. Sweet. But it’s still a boat at its core. If developers want a plane, then a plane should be created, not a boat with wheels that had wings strapped to it. It’s a bit hard to see this in Knit from afar, but it’s there.
A Problem
Let’s address a tangible problem with Knit and how to fix it. If you want intellisense for services/controllers in Knit, forget about it. You see, Knit was created before Roblox had proper intellisense built-in, and similarly before there were any VSCode extensions to help out with the same. But that changed shortly after release. This is incredibly frustrating. When I retrieve a service from Knit, it should be able to tell me what’s there. But it can’t. Because Knit loads all services/controllers into a generic table, which then is used to retrieve them by name. There’s no typing information on these objects. Please excuse me while I break my keyboard over my knee. Yes, there’s some hacks to do this, but it requires creating complex types and casting to said types every time you fetch a service from Knit. It’s not a clean or acceptable solution.
What is this, vanilla Lua 5.2? Give me my static analysis or I’ll throw a fit, or at least slump into a dark corner and contemplate how I’ll ever know what to type next. You expect me to open the file of a dependency to figure out what to write? We are an advanced species, an evolved society. No, the computer must tell me what’s in that file.
But in all seriousness, how do we fix it? Simple. Get rid of Knit and use ModuleScripts. Boom. Done. A flat table with methods/fields/etc. in a ModuleScript is already a singleton on its own. It’s arguably the same thing as a service at the core. Yes, it lacks lifecycles and built-in networking, but those can be added with some other modules with a lot of ease. The best part about this solution is that (1) it’s super simple, and (2) you get intellisense natively. The next best thing? Onboarding. Teaching developers on your team how to use such a system is incredibly simple. It’s just ModuleScripts.
That’s it! We’ve solved Knit’s greatest weakness. Now our service can simply require the ModuleScript of another service, and we’re good to go.
We could then slap on RbxUtil’s Loader and Net modules to add lifecycles and networking.
The Dream
A respectable service architecture framework would implement “dependency injection” (DI). This would help create a dependency inversion, which is the “D” in the SOLID programming principles. In simple terms, services and controllers would be able to define what services/controllers they want to utilize, but they would be injected by the framework code within the constructor of the service/controller. Ideally, this same method would be done for most (or all) dependencies within a module. This helps ensure loose coupling, and also makes testing way easier. Anyone who has tried to use Hoarcekat with Knit knows how awful Knit can be in a testing environment. Side effects…side effects everywhere.
In roblox-ts, the Flamework framework provides just that. Dependency injection is a built-in feature. But Knit is for the Luau world, not the roblox-ts world. So, we have a problem: the ability to cleanly inject dependencies based on interfaces or types necessitates features that Luau does not have — and may never have. That is, Luau does not have any sort of reflection metadata for types. Nor will it, because types are just for static analysis and in no way are included in the final compilation. Luau would need to have something built into the compiler to expose such information at runtime. I don’t think that’s ever going to happen. It’s just not within the goals of the language.
The alternative is to get hacky. Services could define some sort of array of strings that indicate the names of the desired services, and then could enforce a constructor method that feeds in said services. But this is not great, and we again lose out on intellisense unless we jump through some super messy hoops to add interface typings by hand. It’s just not worth it.
The dream is just that. It’s a dream. It’s a wonderful dream. A dream where IoC could be properly implemented with native Luau code. But at some point, you have to wake up and face the reality of your surroundings. It’s unlikely that such tooling will be provided.
A Case Against Knit
Just use ModuleScripts. If you want services/controllers without the framework malarky, just use ModuleScripts. Use my Loader module within RbxUtil to bootstrap them all together. Use my Net or Comm modules for networking. Done deal. You get intellisense, you get a simplified structure, and you get an ecosystem that you fully control.
Another case against Knit is in light of the heavy misuse of Knit. A lot of people use Knit the same way AGF was forced to be used: services and controllers for everything. This is not — I REPEAT — this is not how Knit is supposed to be used! Please, for the love of all your developers’ sanity, do not make everything a service or controller. Knit should be used to create top-level structures for your game, but not everything. It should be a piece of the puzzle, but not the whole. But for some people, trying to escape this logic is just too difficult to grasp when given tools to create simple wrapper classes that have built-in communication. If that’s the case for you or your developers, do something else.
One last case against Knit is the problem with singleton patterns to begin with. There are countless articles online that bash on singleton patterns. The main reason being the exploitation of them: so many people use singletons as just a secret global state container. Remember how I said _G
was evil? Well, if you use services/controllers like you would data in _G
, then what’s the difference? Evil! Unmanaged global state is a one-way ticket to the worst logic bugs you’ll ever encounter. If you’re not disciplined in how services and controllers operate, then it’s easy to fall into the pitfall of using them to simply store global state. A good test is to see how much data services and controllers are actually holding onto. Ideally, not much or any. Consumers of your services/controllers should be going through methods or events, not properties/fields. In short, this can become a footgun. Maybe an ECS framework would be better for you, but existing ECS frameworks (as of writing this) are early in design and not very ergonomic for the Roblox ecosystem at the moment.
A Case for Knit
Wow, that’s a lot of typing for me, and hardly any of it was code. Weird. Not sure how I feel about that. Anyway, I don’t want to bash on Knit too much, because it still excels at doing its core job well. That is, if you want services and controllers with built-in networking — and even middleware — then Knit might be the tool for you. However, I still think the example a couple sections above demonstrates that Knit isn’t even necessary to make this happen. It can be done with just a little bit of code.
But Knit is a battle-tested framework. It’s stood strong and has scaled well. Games with literally hundreds of thousands of concurrent players have utilized Knit without a hitch. While I may bash on it (and some others bash on it a little more), it’s hard to be a naysayer in light of what Knit has offered developers.
The Future
To be honest, I’ve transitioned most of my workflow to roblox-ts. The structured typings that TypeScript have to offer is simply amazing. But I still want to help others see how they can use Lua to create good structure for their games, without overusing said structure and turning their code into a prison. Let’s not be prisoners of our own devices. Let’s remove the wings and the wheels and admit that the boat was a boat the whole time. If it sinks, then maybe it was a bad boat to begin with. Adding wings just hid the flaws from us. Let’s design code that helps us excel at writing good, maintainable, readable, stable software. I will continue to support Knit and plan on laying out an LTS strategy, but I do not see a future where the current version of Knit should be recommended.
Lastly, I am always open to constructive criticism. Let’s have a conversation; a dialog. People who just say “Knit sucks” or whatever: you are not helping. And, to be honest, I’m just a lone developer that’s continuing to learn as I go. So instead of saying “Knit sucks,” say, “Knit sucks… because XYZ.” Thanks for the feedback.