44. This questions asks for the implementation of 3 rooms and some way to change between them. From the example of a Stage
room defined at the start of the article we can see that rooms are just objects with an update
and draw
function, and that those functions are called if the room is currently active. So first, for the room definitions:
xCircleRoom = Object:extend()
function CircleRoom:new()
end
function CircleRoom:update(dt)
end
function CircleRoom:draw()
love.graphics.circle('fill', 400, 300, 50)
end
xxxxxxxxxx
RectangleRoom = Object:extend()
function RectangleRoom:new()
end
function RectangleRoom:update(dt)
end
function RectangleRoom:draw()
love.graphics.rectangle('fill', 400 - 100/2, 300 - 50/2, 100, 50)
end
xxxxxxxxxx
PolygonRoom = Object:extend()
function PolygonRoom:new()
end
function PolygonRoom:update(dt)
end
function PolygonRoom:draw()
love.graphics.polygon('fill', 400, 300 - 50, 400 + 50, 300, 400, 300 + 50, 400 - 50, 300)
end
These three should be created as CircleRoom.lua
, RectangleRoom.lua
and PolygonRoom.lua
in the rooms
folder. Then, similarly to how we automatically loaded the objects
folder we should do it for the rooms
one:
xxxxxxxxxx
function love.load()
local object_files = {}
recursiveEnumerate('objects', object_files)
requireFiles(object_files)
local room_files = {}
recursiveEnumerate('rooms', room_files)
requireFiles(room_files)
end
Then, finally, we can bind the keys with the functions needed to change from one room to another:
xxxxxxxxxx
function love.load()
input = Input()
current_room = nil
input:bind('f1', function() gotoRoom('CircleRoom') end)
input:bind('f2', function() gotoRoom('RectangleRoom') end)
input:bind('f3', function() gotoRoom('PolygonRoom') end)
end
And then pressing f1
, f2
or f3
should do this:
If the Simple Room setup is being used then whenever those keys are pressed, a new entire room is being created and the previous one is deleted. It's important to understand this idea.
45. In Unity Rooms are called Scenes. The functions around a Scene are here and they mostly appear to have the same functionality as explained previously. Although they have additional things such as merging scenes and asynchronous loading of scenes, things which we could do in Lua if necessary.
In Godot Rooms are also called Scenes. The functions around a Scene are here and they seem to be the same as well without anything that stands out. There are functions to change from scene to scene, as well as functions to reload scenes and then to communicate with the editor.
In HaxeFlixel Rooms are called States. The functions around a FlxState seem to be what would be expected, except with a few differences that are worth noting. For instance, it has persistentDraw
and persistentUpdate
functions, which will run even when the state is not an active one. This can be useful in a number of situations like the page explains.
The functions that switch/reload states are here in this sort of global object that has a bunch of assorted functionality in it. While a bit less organized than the previous engines, this generally works fine (and it's what we're doing as well, after all our state changing functions are global functions in a random file).
In Construct 2 Rooms are called Layouts. Construct 2 seems to be mostly UI based so it's hard to get a handle on how things work exactly, but changing between layouts can be done by using events, apparently. It seems hard to get more concrete information based on the documentation alone, though.
In Phaser Rooms are called States. And the functions that operate on states are on a StateManager. This is probably the most standard way of doing things I've seen. Note that each state here also has a reference to a Game
object as well as a World
object. The World object is the equivalent of an Area (although there's always only one world for Phaser, while in our case we can create multiple areas), and the Game object has no equivalent, but it would be everything that we defined and will define in the main.lua
file.
46. The first game I'll focus on is Rogue Legacy. It's very similar to The Binding of Isaac in a number of ways. This will be a simplified version of the game loop since depending on the situation it's a bit different but this is a good enough approximation. At first there's a starting screen:
Then you get to choose the character you wanna play with:
Then you get to spend gold on your skill tree:
After that you get to spend gold on other stuff (you can also go back to the skill tree screen from here):
And then once in the game you go from room to room inside a dungeon filled with rooms:
And you do that until you die:
After this death screen then the game loop restarts.
The way I would separate this in terms of rooms is: MainMenu
, CharacterSelect
, SkillTree
, PreCastle
and Castle
. Inside the castle, each room would be a persistent room of its own, like in Isaac, and then whenever the player reaches the end of one room that connects to another a transition to another room would be added. All rooms inside the castle are added previously with addRoom
. All other rooms that are not in the castle can be achieved by just creating a new one with the appropriate data each time the player enters it.
The next game is One Finger Death Punch. This game is rather simple and straightforward. First there's a main menu:
Then you get to pick which game mode you wanna play:
After that there's an overworld that acts as a level select of sorts (as you beat one node you can advance to nearby ones until you go through the whole map):
Then after that the gameplay itself happens:
And once that's over there's a score screen:
The way I would divide this in terms of rooms would be: MainMenu
, GameModeSelect
, LevelSelect
, Game
and Score
. The transition from each room to the next is rather straightforward and in the game it happens whenever the walls close in towards the center of the screen as a transition effect. All rooms can also use the simple mode instead of the persistent one and it would still work fine. The only room that could be persistent is the overworld map, but that can also work without being persistent so it's just a matter of preference really.
47. Whenever there are no more references pointing to a variable then eventually that variable will be collected. So if we want an object to be collected in Lua, all we have to do is set the variable that is holding that object to nil
. If the object is being held inside a table then all we have to do is remove it from the table. If the object is both inside a table and being pointed to with a variable, then we must do both. If we just remove it from the table or just set the variable to nil
then the object won't be collected.
Memory leaks can happen when the developer forgets to remove references to an object, like in the previous example of only removing it from the table that holds it or only setting the variable that points to it to nil
. Preventing this from happening is mostly a matter of paying attention and constantly checking to see if there are any execution paths that consistently lead to abnormal memory usage. Further on in the tutorial I'll go over some code that can help with this.
48. The first thing needed is the creation of the Stage
room with an Area
in it:
xxxxxxxxxx
Stage = Object:extend()
function Stage:new()
self.area = Area()
end
function Stage:update(dt)
self.area:update(dt)
end
function Stage:draw()
self.area:draw()
end
And then the creation of the Circle
game object:
xxxxxxxxxx
Circle = GameObject:extend()
function Circle:new(area, x, y, opts)
Circle.super.new(self, area, x, y, opts)
end
function Circle:update(dt)
Circle.super.update(self, dt)
end
function Circle:draw()
love.graphics.circle('fill', self.x, self.y, 50)
end
As required, it inherits from GameObject
, receives an area, position and optional arguments as arguments. Similarly, it calls the new
and update
functions from the its parent class. If you don't remember how that works go back to the OOP section in the last article.
After that we can add the functionality where the circle instance kills itself after 2-4 seconds. One of the challenges in doing this is that earlier I said we should only use love.math.random
for getting random numbers, but this function only returns integers if we pass in a range. We want a number between 2-4 seconds that includes all non-integer values as well. The way to solve this is through this function (place in utils.lua
):
xxxxxxxxxx
function random(min, max)
return love.math.random()*(max - min) + min
end
love.math.random()
returns a real number between 0 and 1 if no range is passed in, so what this function is doing is taking the min
and max
values, multiplying the 0-1 value by their difference and then offsetting by the lower amount. Essentially, lets say we called it as random(2, 4)
, then the love.math.random()*(max - min) + min
line parses as [real value between 0 and 1]*(2) + 2
, which means that if the value generated is 0.4, for instance, it would become 0.4*2 + 2 -> 2.8
, which is a value between 2 and 4 line we wanted.
Anyway, now that this is sorted out we can go back to the Circle
class:
xxxxxxxxxx
function Circle:new(area, x, y, opts)
Circle.super.new(self, area, x, y, opts)
self.timer:after(random(2, 4), function() self.dead = true end)
end
And so this makes it that the instance kills itself after 2-4 seconds. After that we need to create one instance of Circle
at a random position every 2 seconds:
xxxxxxxxxx
function Stage:new()
self.area = Area()
self.timer = Timer()
self.timer:every(2, function()
self.area:addGameObject('Circle', random(0, 800), random(0, 600))
end)
end
function Stage:update(dt)
...
self.timer:update(dt)
end
And then after that all that's left is activating the Stage
room in main.lua
with gotoRoom('Stage')
.
49. This exercise is essentially asking us to do the Area/GameObject binding manually from scratch. First, we need to create a Stage
room with no Area
in it:
xxxxxxxxxx
Stage = Object:extend()
function Stage:new()
end
function Stage:update(dt)
end
function Stage:draw()
end
After this we need to create a Circle
object that doesn't inherit from GameObject
:
xxxxxxxxxx
Circle = Object:extend()
function Circle:new(x, y)
self.x, self.y = x, y
end
function Circle:update(dt)
end
function Circle:draw()
love.graphics.circle('fill', self.x, self.y, 50)
end
This is essentially just creating an object normally. The only thing we do here already is to make sure that the object has x
and y
attributes and that it is drawn as a circle. Now we need to add the functionality that can make these circle objects be added and removed from the Stage
. This is just adding the same code that already exists in the Area
object, but doing it manually and directly in the Stage
room:
xxxxxxxxxx
function Stage:new()
self.game_objects = {}
end
function Stage:update(dt)
for i = #self.game_objects, 1, -1 do
self.game_objects[i]:update(dt)
if self.game_objects[i].dead then
table.remove(self.game_objects, i)
end
end
end
function Stage:draw()
for _, game_object in ipairs(self.game_objects) do
game_object:draw()
end
end
And with this, whenever dead
is set to true inside the circle, it will be removed from the game. We also need to add the dead
attribute to the Circle
class itself:
xxxxxxxxxx
function Circle:new(x, y)
self.x, self.y = x, y
self.dead = false
end
With this, we can now add an instance of Circle
to the Stage
at a random position every 2 seconds:
xxxxxxxxxx
function Stage:new()
self.game_objects = {}
self.timer:update(dt)
self.timer:every(2, function()
table.insert(self.game_objects, Circle(random(0, 800), random(0, 600)))
end)
end
function Stage:update(dt)
...
self.timer:update(dt)
end
And now to make the Circle
instance kill itself after a random amount between 2 and 4 seconds:
xxxxxxxxxx
function Circle:new(x, y)
...
self.timer:after(random(2, 4), function() self.dead = true end)
end
And then after that all that's left is activating the Stage
room in main.lua
with gotoRoom('Stage')
.
50. The random function right now looks like this:
xxxxxxxxxx
function random(min, max)
return love.math.random()*(max - min) + min
end
The first thing we wanna do is change it so that it can take only one value, and when only that value is taken then the number generated must be a random one between 0 and the value:
xxxxxxxxxx
function random(min, max)
if not max then
return love.math.random()*min
else
return love.math.random()*(max - min) + min
end
end
With this we get a value between 0 and min
if only one value is passed in. The if not max
conditional is simply checking to see if the max
value was defined or not. If we call random(2)
it means that max
wasn't defined and it's nil
, which means that not max
will return true, which is what we wanted in the first place. Now for the next thing the question asks, we need to augment the function so that min
and max
can be in any order:
xxxxxxxxxx
function random(min, max)
if not max then -- if max is nil then it means only one value was passed in
return love.math.random()*min
else
if min > max then min, max = max, min end
return love.math.random()*(max - min) + min
end
end
Here we simply add a conditional that checks if min
is bigger than max
, and if it is, then we just swap both values. In Lua you can perform an in-place swap of two variables because it supports multiple assignments.
51. local opts = opts or {}
serves to define the opts
variable locally even if it wasn't defined by the caller. For instance, the if caller does something like addGameObject('ClassName', x, y)
, the opts
table will be nil
, which is a problem because various things inside the GameObject
class definition assume that opts
exists as a table. To prevent complications from this we simply say that opts
will be the opts
variable that was passed in, or if none was passed in, then it will be an empty table. This works because of how the or
operator works in Lua (which is something we went over in previous exercises and will go over in the future too, so make sure you understand why this is the case).