MVC: A Practical Approach Towards Developing Games (And how to stop confusing yourself with ECS vs OOP)

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)

200px-MVC-Process.svg
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.

32 Likes

Love the conceptual breakdown! I think a massive reason the learning curve from ‘intermediate scripter who has a general understanding of programming on Roblox’ to ‘experienced scripter who has a strong understanding of how to program on Roblox’ can be so steep and difficult is a lack of theory resources on DevForum, like this. Particularly theory resources that are in conversation with other bits (in this case OOP and ECS).

Most developers don’t have strong resources for them to read that get them to think about programming (in the context of games) from a more conceptual stance and as a result either push ahead until they meet an obstacle (something we all do, but in this case these are obstacles from a general design philosophy POV) or they latch onto the few theory resources that are posted here and run with it until they hit the obstacle. These threads are so key, because general design philosophy obstacles can sometimes force a developer to rework their entire project (as mentioned in the post) or spend disproportionate amounts of effort to hackily circumvent the design limitation and force the feature through in a non-scalable fashion. All that is of course avoided by threads like these that present and detail different approaches to design and ultimately empower that developer to choose the best design path (or combination of paths) from the onset.

10 Likes

Well said. Especially on the point about design limitations.

Scripters sometimes make their code worse because they restrict themselves too much in one area, such as making code as consistent as possible or never using while loops because it ‘smells’, and they end up doing something that won’t scale well.

I remember many cases where I thought I was doing something clever when it would have been much more practical doing it simple.

3 Likes

I am surprised by the popularity of my post to be linked and paraphrased by others! I have no intention of commenting about MVC, I will leave that to others who find that interesting. However, I do want to clarify a few things about ECS.

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

In the academic sense I do agree that ECS and OOP are a false dichotomy but in practice they differ quite a lot in how they shape applications. ECS does promote composition first but it comes with powerful constructs to manage these compositions to effectively search for them which promotes better performance in terse indexing. Traditional OOP usually lacks in this department and end up quite naive in linear traversal.

Roblox already encourages its own paradigm for game development, which is the Event Driven Programming (EDP) paradigm.

I would say that event driven applications are very fragile, which in gameplay code can be detrimental when there is no recovery logic in place. As opposed to ECS that handles things on a frame-to-frame basis where things will just be reconciled by the next frame.

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.

Correct, we are on an engine that does clearly have an opinion in how data flow is handled, but analogously we see this in other engines such as Unreal Engine where the developers fight against the engine with some great success stories. This is in fact how things looked like in Unity as well before DOTS.

From what I am allowed to talk about, I can tell you there are good indicators to that Roblox is very friendly to the idea of using techniques inspired by ECS. We will hopefully see evidence of this “soon”. :slightly_smiling_face:

  • 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

I definitely understand your concern, but speaking of Matter I would like to address these points as a spokesperson of our organization.

  1. We are dedicated towards long term maintenance, we have almost a dozen of contributors as of recently and my intention is to ensure the project stays maintained with or without my presence in the future.
  2. We have an active channel in the Roblox-OSS server and we also have a very thriving server that extends support on specifically Matter, it is also a pretty cool place to hangout. And hey, even if you do not use ECS, you are welcome because we try to include as many perspectives as possible.
  3. In the case of an ECS, I would say it is most definitively one of the more transferrable things to learn in game development given how many engines extend support for it or have a community insane enough to retrofit one into it cough.

I am very biased as the current maintainer of Matter. But I truly believe it offers things other than just performance and great composition. It has a scheduler that topologically sorts your systems to create a deterministic data flow and a debugger that is great for profiling code and disabling buggy systems on runtime!

I also recently made a new ECS library called Jecs that features entity-entity relations as first class citizens. They are particularly useful in games because it helps you construct graphs of entities trivially, e.g. scene graphs, transform hierarchies, etc. I was recently met with a question of how one would make an inventory in an ECS and this is trivially solved with relations which are also much more performant than its counterpart of naively indexing!

local containedBy = world:component()
local function ContainedBy()
    return jecs.pair(containedBy, b)
end
world:set(toolId, ContainedBy(backpackId))

local __ = Wildcard
-- get any item contained by any backpack
query(ContainedBy(__))

-- if u have the backpack you can group the entities under it
local backpackId = world:get(player1, Backpack)
-- this only get items under player1s backpack
query(ContainedBy(backpackId))

Also check out this very long-winded paper I wrote on the Implementation of Entity Component System in Scripting. There’s only 12 pages, and it goes over the theory and concepts but also practical applications of them in the implementation.

I hope I was able to engage with this post without too much controversy but feel free to flame me if I said anything wrong or worthy of criticism :sweat_smile:

6 Likes

No bad vibes were received. Your comment is well phrased and addresses the ways I might have undermined Matter; I was focusing more on whether a scripter actually needs such a framework and how they might consider their game’s needs in order to justify installing such a dependency.

I definitely rather have Matter be a tool commonly known by the Roblox developer community rather than being a niche tool that no one uses. I will add a disclaimer addressing your points :+1:

1 Like

Hi, there! Loved this thread. I am also in my last year of college and was a software intern this summer (not at Amazon). In OOP systems (C++, Python) I find it easy to design programs as there are already good programming practices laid out but Roblox has a lot of different ways that people preach, but a lot of the design patterns don’t make sense in terms of scalability.

I think frameworks are definetley helpful, but relying on a single framework sometimes prevents system designers from making choices they understand and work well for the application. Ultimately, I agree simpler is better, and you should fully understand how to do good system design before adding additional dependencies.

Would love to connect with you sometime!

ok yfpro67

but what’s your opinion on MVC or the other paradigms

Event Driven Programmimg has always been what Roblox wants you to do in their engine, at the end of the day, your code connects to events and reacts to those events.

Yes you could have an active while true thread, but this is usually in one location of your codebase and if its running every frame, you instead use one of the RunService events.

OOP is redundant when you have CollectionService and write a 50-ish line module to hook tags to an initialiser. I dont think this can easily be expanded for UI, but it works for 3D objexts in the workspace.

As for MVC, one thing I like to do with plugins is to have the UI be completely independent from the backend, only accessed through APIs. This prevents the UI from getting too entangled with the internals of my code base.