52. We're tasked with creating a function named getGameObjects
that receives a function that performs a test on an entity. If the entity passes the test then true is returned and that entity is added to a list that will be returned by getGameObjects
. This exercise is basically asking if you have a good understanding of what passing functions around to other functions as arguments can achieve.
The first thing we wanna do in getGameObjects
is go over all game objects of the Area
:
function Area:getGameObjects(filter)
for _, game_object in ipairs(self.game_objects) do
end
end
Now, for each game object, we want to apply the filter
function that is received. Since the user will always define that function as receiving an entity as argument, we can always pass game_object
to it:
xxxxxxxxxx
function Area:getGameObjects(filter)
for _, game_object in ipairs(self.game_objects) do
if filter(game_object) then
end
end
end
filter
will return true in case the game object passes the test and false otherwise, which means that writing it like that in a conditional makes sense. Now, inside the conditional, we want to add the game_object
that passed the test to a table, and then we want to return that table to the caller:
xxxxxxxxxx
function Area:getGameObjects(filter)
local out = {}
for _, game_object in ipairs(self.game_objects) do
if filter(game_object) then
table.insert(out, game_object)
end
end
return out
end
And so in this way we will get all game objects from an Area
that pass the test.
53. This exercise is asking if you understand how the and
and or
operators work and how they can be used to check for certain conditions.
For the variable a
the result is 2. Whenever two variables are true in an and
operation, the second variable will always be the returned result. This is useful in a number of situations, like in the previous exercise the line if e.hp and e.hp >= 50 then
is asking if e.hp
exists, and then if it does, checking if that value is over 50. If e.hp
doesn't exist, then that will be nil
and the next check won't even be run (and if it was run it would result in an an error, since it would be asking if nil >= 50
which can't be done). In this way we can check to see if a certain attribute exists and then use that attribute in some way all in the same line instead of doing something like this:
xxxxxxxxxx
if e.hp then
if e.hp >= 50 then
end
end
For the variable b
the result is nil
. In Lua only nil
and false
are false values, so there's no way for this to be anything else. Like I just said, if the first element of an and
operation is false, then the second one is not even checked, so in this case the value 2 is irrelevant.
For the variable c
the result is 3. Whenever two variables are true in an or
operation, the first variable will always be the returned result. This is useful in a number of situations, like whenever we do stuff like opts = opts or {}
. If the opts
that's passed in is nil
, then the or
will return the empty table. But if the opts
value is defined then it will just return opts
itself. Using or
like this we can check to see if some value that was passed in was actually passed in or not instead of doing something like this:
xxxxxxxxxx
if not opts then
opts = {}
end
For the variable d
the result is 4. If one of the elements in an or
operation is false then it will just return the one that's true. For the variable e
the result is 4 as well and the same logic can be used to explain it.
For the variable f
the result is 1. First, (4 > 3)
is true, so the whole thing parses out to true and 1 or 2
. and
takes precedence over or
, so first we parse true and 1
which returns 1. Now we have 1 or 2
left, which parses out to 1 again.
For the variable g
the result is 2. First, (3 > 4)
is false, so the whole thing parses out to false and 1 or 2
. Then we parse false and 1
which is false, and finally we're left with false or 2
, which is 2.
54. This question is asking for your understanding of the ...
construct in Lua. Whenever you define the arguments of a function as ...
, it means that the function can receive an unknown amount of arguments and that you'll deal with separating them inside. So, our function could look like this:
xxxxxxxxxx
function printAll(...)
local args = {...}
for _, arg in ipairs(args) do
print(arg)
end
end
So we just capture all the arguments passed in inside the table args
and then we simply go over that table and print each value.
55. We can use the same logic for this function, but instead of printing each value we just concatenate them together using ..
:
xxxxxxxxxx
function printText(...)
local args = {...}
local str = ''
for _, arg in ipairs(args) do
str = str .. arg
end
print(str)
end
The difference here is that we have to define an empty string str
and then when going over the args
table we concatenate all values to that string.
56. The collectgarbage
function can be used to do a number of things related to the garbage collector. As the link states, calling it like collectgarbage("collect")
will perform a full garbage collection cycle.
57. From the previous link we can see that calling collectgarbage("count")
will show the total amount of memory used by the Lua program in KBs.
58. We can use the error
function to do this. I personally rarely use this function in game code, but if you're building a library and you want to lock off certain paths of execution it's useful to halt execution when those paths are activated and tell the user how they used your library inappropriately. It's a way better thing to do than just failing silently.
59. First we can create a default Rectangle
class:
xRectangle = GameObject:extend()
function Rectangle:new(area, x, y, opts)
Rectangle.super.new(self, area, x, y, opts)
self.w, self.h = random(10, 50), random(10, 50)
end
function Rectangle:update(dt)
Rectangle.super.update(self, dt)
end
function Rectangle:draw()
love.graphics.rectangle('fill', self.x - self.w/2, self.y - self.h/2, self.w, self.h)
end
Then the question asks us to create 10 instances of the class at random positions of the screen with random size. We already took care of the size inside the class itself, so now for the rest:
xxxxxxxxxx
function Stage:new()
...
for i = 1, 10 do
self.area:addGameObject('Rectangle', random(0, 800), random(0, 600))
end
end
Then, whenever the d
key is pressed a random instance should be deleted:
xxxxxxxxxx
function Stage:new()
...
input:bind('d', 'removeRectangle')
end
function Stage:update(dt)
if input:pressed('removeRectangle') then
table.remove(self.area.game_objects, love.math.random(1, #self.area.game_objects))
end
end
Because every object in the Area
is an instance of the Rectangle
class we can just remove a random element from the area.game_objects
table directly instead of doing anything more complicated.
The next thing that's asked is that when the number of instances reaches 0, then 10 new instances should be created again like initially:
xxxxxxxxxx
function Stage:update(dt)
if input:pressed('removeRectangle') then
table.remove(self.area.game_objects, love.math.random(1, #self.area.game_objects))
if #self.area.game_objects == 0 then
for i = 1, 10 do
self.area:addGameObject('Rectangle', random(0, 800), random(0, 600))
end
end
end
end
Here we can just check the size of the game_objects
table and if it is 0 we just copy the code we used to spawn the Rectangle
instances initially.
60. First we create the default Circle
class:
xxxxxxxxxx
Circle = GameObject:extend()
function Circle:new(area, x, y, opts)
Circle.super.new(self, area, x, y, opts)
self.r = random(10, 50)
end
function Circle:update(dt)
Circle.super.update(self, dt)
end
function Circle:draw()
love.graphics.circle('fill', self.x, self.y, self.r)
end
Now we need to create 10 instances of this class, but with an interval of 0.25 between the creation of each instance:
xxxxxxxxxx
function Stage:new()
...
for i = 1, 10 do
timer:after(i*0.25, function()
self.area:addGameObject('Circle', random(0, 800), random(0, 600))
end)
end
end
Using the timer:after
call inside the for loop we can make it so that one instance will get spawned every 0.25 seconds by multiplying i
by 0.25 and using that as the argument for the after
call.
Now it says that after all the instances are created we need to delete a random one every [0.5, 1] second:
xxxxxxxxxx
function Stage:new()
...
timer:after(2.5, function()
timer:every(random(0.5, 1), function()
table.remove(self.area.game_objects, love.math.random(1, #self.area.game_objects))
end, 10)
end)
end
So, after the initial 2.5 seconds of creating instances, we create an every
timer that will run with an interval of between 0.5 and 1 second and will remove an instance each time it is run.
Now the last thing the exercise asks for is to restart the process again once the number of instances left reaches 0. Similarly to the previous exercise we can just check the number of entities left in game_objects
:
xxxxxxxxxx
function Stage:new()
...
timer:after(2.5, function()
timer:every(random(0.5, 1), function()
table.remove(self.area.game_objects, love.math.random(1, #self.area.game_objects))
if #self.area.game_objects == 0 then
-- restart
end
end)
end)
end
Now the question is what do we put in place of --restart
? The exercise asks for this process to repeat forever, so the first instinct would be to put this all into a function, and then call this function from within itself whenever it should be repeated. That would look something like this:
xxxxxxxxxx
function Stage:new()
local function process()
for i = 1, 10 do
timer:after(i*0.25, function()
self.area:addGameObject('Circle', random(0, 800), random(0, 600))
end)
end
timer:after(2.5, function()
timer:every(random(0.5, 1), function()
table.remove(self.area.game_objects, love.math.random(1, #self.area.game_objects))
if #self.area.game_objects == 0 then
process()
end
end)
end)
end
process()
end
The only problem with this approach is that the first time it is called it will create one every
timer, the second time it is called it will create another, and so on. In the end we will have multiple every
timers operating at the same time and that will lead to bugs. One easy way to fix it is to label the every
timer so that whenever it's called again, the previous one is cancelled.
xxxxxxxxxx
timer:every('process_every', random(0.5, 1), function()
...
end)
And another thing we need to do is to also cancel the previous every
timer whenever the process restarts again, or instances will be removed until the next every
is called, which is something we don't want:
xxxxxxxxxx
local function process()
timer:cancel('process_every')
...
end
This exercise is very very tricky and there are many ways to get lost. But this is also the kind of thing that you have to do all the time in games. Understanding and controlling things so that they happen in appropriate orders and at appropriate times is very important, so make sure you understand everything that's happening!
62. The first thing we need to do is to go over all game objects in an Area
and check to see if they are of the target classes we want:
xxxxxxxxxx
function Area:queryCircleArea(x, y, radius, object_types)
for _, game_object in ipairs(self.game_objects) do
if fn.any(object_types, game_object.class)
end
end
end
Here I make use of the .class
attribute, which holds the name of the class this object belongs to. We didn't define this previously but it can be easily added to every object that is inside the Area
in the addGameObject
function:
xxxxxxxxxx
function Area:addGameObject(game_object_type, ...)
...
game_object.class = game_object_type
table.insert(self.game_objects, game_object)
return game_object
end
This is mostly something convenient so we can use the any
function. We can achieve all this without this function and instead just looping over the object_types
list and using classic's is
function instead. Either way works but the first one feels cleaner.
Now that we know if the game object is or isn't of the type we're looking for, we can do the actual check to see if it's inside our determined radius or not. To do this we'll use a small function called distance
, which computes the distance between two points:
xxxxxxxxxx
function distance(x1, y1, x2, y2)
return math.sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2))
end
And so with that we can do this:
xxxxxxxxxx
if fn.any(object_types, game_object.class)
local d = distance(x, y, game_object.x, game_object.y)
if d <= radius then
end
end
Here we simply check the distance between the center of the radius and the center of the object. If that distance is lower than the radius then we'll add that object to a list, which we will then return once the loop is over:
xxxxxxxxxx
function Area:queryCircleArea(x, y, radius, object_types)
local out = {}
for _, game_object in ipairs(self.game_objects) do
if fn.any(object_types, game_object.class)
local d = distance(x, y, game_object.x, game_object.y)
if d <= radius then
table.insert(out, game_object)
end
end
end
return out
end
62. This one is very similar to the previous exercise, except that instead of returning all objects inside the circle, we just return the one that's closest to the target point. To start with, we can use the function we defined in the previous exercise to get all objects that are actually inside the circle:
xxxxxxxxxx
function Area:getClosestObject(x, y, radius, object_types)
local objects = self:queryCircleArea(x, y, radius, object_types)
end
Now what we need to do is to somehow sort this table so that the first objects in it are closer and the last ones are further away. An easy way to do that is using table.sort
:
xxxxxxxxxx
function Area:getClosestObject(x, y, radius, object_types)
local objects = self:queryCircleArea(x, y, radius, object_types)
table.sort(objects, function(a, b)
local da = distance(x, y, a.x, a.y)
local db = distance(x, y, b.x, b.y)
return da < db
end)
end
And so with this, we will place objects that have a smaller distance from the target first in the list. Since we know that closer objects are first, to get the closest one all we need to do is return the first object:
xxxxxxxxxx
function Area:getClosestObject(x, y, radius, object_types)
local objects = self:queryCircleArea(x, y, radius, object_types)
table.sort(objects, function(a, b)
local da = distance(x, y, a.x, a.y)
local db = distance(x, y, b.x, b.y)
return da < db
end)
return objects[1]
end
63. We can check if a method or attribute exists by just using a conditional. For instance, if we want to check if self
has the attribute damage
then we can do if self.damage then
. If we want to check if it has the attribute damage
and if that damage is higher than 10 then we can do if self.damage and self.damage > 10 then
. The use of the and
operator like this was explained in a previous exercise.
64. Suppose we have table a
and table b
and we want to copy table a
to table b
. To achieve that we'd do the following:
xxxxxxxxxx
for k, v in pairs(a) do
b[k] = v
end
Using the pairs
function we can go over all keys and indexes of the source table and then use those to directly set the appropriate values on the target table.