##Prerequesites
Design patterns are ways to make your code more readable, maintable, and scalable. They can sometimes also solve commonly (but hard) recurring problems.
For starters, let us take a look into this code which is in charge of continuously processing a game as long as the game is running. A game loop.
local playerService = game:GetService("Players")
local isRunning = true
local day = 1
local playerCount = #playerService:GetPlayers()
while isRunning do
-- Run game...
-- Update run conditions
day += 1
playerCount = #playerService:GetPlayers()
isRunning = day <= 3 and playerCount >= 1
end
There are a few problems with this code
Imperative - We are keeping track of multiple conditions and updating them inside the game loop. This makes it less readable.
Exposed state - The day
and playerWon
conditions are exposed to anything inside the game loop, which is unnecessary and prone to bugs.
Although the issues listed above are negligible in this context and not meant to discourage anyone to implement a game loop like this, the repeated use of this pattern can spread horizontally to other parts of the codebase. More importantly, this can scale vertically, which you can imagine by making the game loop more complex in the future as more state is demanded to be managed.
Patterns which are likely to be ineffective and counterproductive are called anti-patterns.
Later in the tutorial, we will learn how we can implement a design pattern to improve the code in a way that benefits the developer and the development lifecycle of the game.
###What design patterns are NOT
###Why should I use design patterns?
##Refactoring the Game Loop Now that we know what design patterns are for, we can start refactoring the code presented earlier. Let us explain more thoroughly the 2 mentioned issues one at a time.
local playerService = game:GetService("Players")
local isRunning = true
local day = 1
local playerCount = #playerService:GetPlayers()
while isRunning do
-- Run game...
-- Update run conditions
day += 1
playerCount = #playerService:GetPlayers()
isRunning = day <= 3 and playerCount >= 1
end
The game loop presented earlier is imperative, meaning we are describing how the code is doing something. We are incrementing day every iteration, checking the player count to be greater than 0, and finally updating the isRunning condition to determine whether the game should continue running.
This is in contrast to declarative code, which describes what the code does. Declarative code is often preferred because of its readability and abstractions. While there should be a balance between imperative and declarative code, you should learn towards the latter if you can reap useful benefits out of it.
We are also exposing the isRunning
, day
, and playerCount
variables to the entire game loop. Sometimes we want to guarantee these variables are not mutated (but still accessed) by other blocks of code, which makes our code more predictable as state changes.
To solve these 2 problems at once, we will implement the iterator design pattern, which is used for handling complex sequences, including sequences dependent on state.
Lua provides generous support for iterators, one recognizable example is the ipairs
function.
function iter (a, i)
i = i + 1
local v = a[i]
if v then
return i, v
end
end
function ipairs (a)
return iter, a, 0
end
The iterator pattern deserves a tutorial for itself, so here we will emphasize its benefits.
Let us define a stateful iterator, gameLoop
, to manage the conditions mentioned.
local playerService = game:GetService("Players")
function gameLoop()
local day = 0
local playerCount = 0
return function ()
day += 1
playerCount = #playerService:GetPlayers()
if day <= 3 and playerCount >= 1 then
return day, playerCount
end
end
end
Now we can rewrite our original code as
for day, playerCount in gameLoop() do
-- Run game...
end
What just happened?
We abstracted away the management of the state that determines whether a game runs or not in a stateful iterator named gameLoop.
This makes our game loop more concise and readable, while also letting us write code that actually processes the game separated from the loop.
The day
and playerCount
internal state is now private from the block inside the for loop. This guarantees only the iterator can change that state.
This pattern is also scalable. Consider the addition of conditions such as checking whether it is day or night.
local playerService = game:GetService("Players")
local lightService = game:GetService("Lighting")
function gameLoop()
local day = 0
local playerCount
local clockTime
return function ()
day += 1
playerCount = #playerService:GetPlayers()
lightService.ClockTime += 1
clockTime = lightService.ClockTime
if day <= 3 and playerCount >= 0 and clockTime >= 7 and clockTime <= 17 then
return day, playerCount, clockTime
end
end
end
##Just the Beginning Now that you’ve taken a glimpse of the elegant solutions design patterns are, I ask you to join me on this series I’ve embarked on Lua Learning tutorials, with each introducing a new design pattern.
If this tutorial helped you, please contribute by upvoting, commenting, and/or awarding it! These tutorials take up much time, but an active community will surely give me reason to make more.
##Closing and Edit Notes The specific example here of using an iterator as a game loop is not necessarily the best implementation; it is purely for educational purposes. There are other methods of managing state and parts of code presented here are not necessarily the best practices.
It is important to emphasize that design patterns should be used selectively. It all depends on your specific problem and the result you want from the solution you ultimately choose to implement.
This tutorial is really just an introduction to a series of tutorials I will be making. It is meant for someone to read and learn new concepts instead of interactively coding along with the tutorial. However, the reader may research the topics mentioned and learn more about design patterns if they wish.