This post is in response to the popular All about Entity Component System and All about Object Oriented Programming articles, which have revolutionized the way Roblox developers create games. In here I am not introducing a new concept, but an old one. Model View Controller (MVC) is a way to architecture your game’s code in a way that it separates business logic, code logic, and the presentation layer. But most importantly, I am going to explain why approaches such as OOP, ECS, and MVC should remain as concepts, and not exact implementations.
Disclaimer
This post is not meant to discourage or discredit any open source framework or technology in particular. The points I make about not using dependencies apply to ALL of them, not just one.
The Problem
Let’s focus on a 14 year old boy, Lee, who found an interest to make Roblox games. He knows he has to learn scripting, and after watching AlvinBlox’s series, he gets a feel for using Lua and the Roblox API. But then his Roblox scripter friend tells Lee all about OOP and that he should start using it because of how it helps with abstraction, encapsulation, inheritance, and polymorphism. These concepts seem revolutionary to Lee and he starts using OOP for everything in his games, from representing zombie NPCs with a Zombie
class, to managing the game’s entire state.
Lee realizes his game’s codebase has become messy and unmaintainable, so he searches the Roblox devforum and finds the All about Entity Component System article and comes to the conclusion that ECS is better than OOP, so he starts using Matter in his game. Lee takes a break from developing for a few months, and when he comes back, he struggles to read and understand his code the way he used to before.
The thing about OOP, ECS, and design patterns is that they all theoretical concepts by nature. They are supposed to be applicable in any programming language and in any application. However, too much theory does not convert well when you apply it in practice. Lee learned this when he approached his entire game with OOP, and when he switched to ECS. He learned the theory well, but when he put it into practice, he found some cases where he could not figure out a good way to do it. He also started confusing terminologies and formed misconceptions about these paradigms.
Misconceptions
OOP and ECS are exclusive
Most well built applications use a multi-paradigm approach. They do not limit themselves to either OOP or ECS, and it’s a good idea to experiment using a combination of paradigms and see what works best. For example, composition is the concept of being able to reuse small components in multiple entities. However, this is also a common practice in OOP (Dependency Injection).
Lua OOP is bad because it's mostly unnecessary
I agree that using OOP for most things in Roblox Lua is unnecessary, but there are good cases of when to use it. Your job as a software developer is to figure out what cases those are, and why you should use one paradigm over the other in one specific case. I find OOP to serve well with NPCs since you can abstract a lot of their state, and it’s encapsulated into one object. For organizing player data, however, I find it better to do it at an eagle’s-eyed view, similar to ECS.
Learning frameworks makes you a better developer
There are a few reasons why I like to keep external modules at a minimal.
- There is a chance the module is no longer maintained, or becomes obsolete (Datastore2)
- You are likely to run into bugs or problems that are specific to the framework/dependency and it will not have as many forum threads for developer support
- Knowledge you gain from learning the framework is not very transferrable to other frameworks
That being said, I use a dependency such as react-lua because it is meant to behave as close as possible to React, one of the most supported libraries in the world, and there are endless stack overflow threads for it.
The Solution (Something that took me years to realize)
An idiot admires complexity, but a genius admires simplicity.
There are many things Lee can do to improve his code. He could learn about cleaner code patterns and sophisticated design patterns, but there is one golden rule that with little effort, could make his code more readable, maintainable, and performant: simplicity over complexity.
You have to realize that Roblox already encourages its own paradigm for game development, which is the Event Driven Programming (EDP) paradigm. This generally means that paradigms that complement EDP usually work well with Roblox games. In fact, Matter does this by merging the Event → Action flow through its useEvent
hook.
You can do the same in react-lua using a custom hook:
function useRemote(eventName: string)
local values, setValues = React.useState({})
local event = events[eventName] :: RemoteEvent
React.useEffect(function()
local onClient = event.OnClientEvent:Connect(function(...)
setValues({...})
end)
return function() onClient:Disconnect() end
end, {})
return table.unpack(values)
end
function MyComponent()
local health = useRemote('HealthSync')
-- ...
end
Knowing that you should follow the EDP paradigm, you can leverage any other paradigm to support that. This is where Model View Controller shines, since it is a simple, matured, and scalable approach to use with EDP.
Model View Controller (MVC)
MVC is a design pattern that is meant to put code into different categories:
Controller
The controller is meant to represent your game’s behavior. If a player attempts to buy an item without enough money, does it prevent them from buying it? Does it put them into debt? Does it trigger a message saying “Not enough funds”?
This layer is mainly concerned with updating the internal state of the game.
Model
The model is meant to represent the internal state of your game. What items does the player have in the inventory? What armor does the player have equipped?
This layer is mainly concerned with storing the current state of the game.
View
The view layer, also called the presentation layer, is what the player sees. Models, particle effects, GUI.
This layer is derived from the model layer.
Theory vs Implementation
A lot of theoretical concepts have just been explained, but differentiating the theory from the implementation is the main topic of this post. We learned that OOP, ECS, and MVC are mostly theory and it takes a bit more from the programmer to apply them well.
When applying these concepts, it is important to revisit your goals. Lee ultimately wants a clean and maintainable code base, so he realizes that a multi-paradigm approach will fit his game the best.
Lee figures out that in his game, he will need to track the player’s inventory, their health and mana stats, their loot drops, their equipped items, and their levels. Instead of keeping all of that state into a PlayerObject
, he makes a separate module for each of those states, such as InventoryService
and LootService
, and he queries each item and each loot drop using a generated unique Id, similar to entities in ECS.
NPCs don’t have as much state as players do, so Lee makes an NPC
class and inherits it for an Enemy
class.
Lee finds a common pattern while scripting his game, which is Listening to Events → Using Services to manipulate state → Syncing the state to the client. He realizes he can simplify this pattern using the old MVC paradigm using just modules and the native Roblox API. He realizes that he doesn’t need a framework like Matter, and he realizes he doesn’t need to stick to one single paradigm.
For Lee’s own specific game, he can listen to events inside Controller modules such as LootController
and CharacterController
. Inside these controller modules, he worries only about business logic and delegates low level code logic to single-responsibility services such as InventoryService
and LootService
, which are responsible for manipulating the Model of the game. He approaches the client side in a similar approach.
Conclusion
OOP, ECS, and MVC are all powerful, but theoretical concepts. The step of implementing them is just as important as learning the concept. An implementation should be practical while still meeting all of your goals for your game. It is up to you what paradigms to use, and your game will most likely need a multi-paradigm approach.
FAQ
What is a ‘single-responsibility service’?
A single-responsibility service is a module that is meant to be responsible for only one thing. For example, a LootService
is responsible for managing the state of players’ loot drops. This can be easily implemented using a module:
LootService.lua
local module = {}
module.LootDrops = {}
function module.SpawnLoot(origin, lootItems)
-- ...
end
function module.GetLootFromItemId(itemId)
-- ...
end
function module.LootItem(itemId, player)
-- ...
end
return module
Isn’t MVC an OOP pattern?
It is often explained in the context of OOP, but you can implement it using Lua modules as well without using Lua OOP. I find it works well for Roblox games.
How do I decide which paradigms are best for my game?
Through experience, you will figure out what a paradigm does best. Whenever you are about to implement something, make a pros and cons list for each paradigm and choose accordingly. OOP is known for abstracting logic and encapsulating state. ECS is known for composing state and querying entities. MVC is known for separating business logic, code logic, and the presentation layer.
Who is Lee?
Lee is supposed to be someone who went through similar experiences I did throughout the years of scripting in Roblox and educating myself in other areas of software development. Currently I am in my last year of college and I am a software development intern at Amazon.