6. This question is asking for a specific implementation of some class. We can use the examples from rxi/classic to figure out how to do everything we need. First, let's create the skeleton of the class in Circle.lua
:
xCircle = Object:extend()
function Circle:new()
end
Then the question mentions how the constructor should receive x
, y
and radius
arguments, and how those should also be the attributes the class will have. Based on the Creating a new class
example on the github page, we can come up with this:
xxxxxxxxxx
Circle = Object:extend()
function Circle:new(x, y, radius)
self.x, self.y, self.radius = x, y, radius
self.creation_time = love.timer.getTime()
end
The question then says how there should be update
and draw
methods:
xxxxxxxxxx
Circle = Object:extend()
function Circle:new(x, y, radius)
self.x, self.y, self.radius = x, y, radius
self.creation_time = love.timer.getTime()
end
function Circle:update(dt)
end
function Circle:draw()
end
And then we can implement the main function of drawing the circle:
xxxxxxxxxx
function Circle:draw()
love.graphics.circle('fill', self.x, self.y, self.radius)
end
To instantiate an object of this class we need to go back to main.lua
, create the object there and then update and draw it:
xxxxxxxxxx
function love.load()
circle = Circle(400, 300, 50)
end
function love.update(dt)
circle:update(dt)
end
function love.draw()
circle:draw()
end
Here the circle
variable is an instance of the Circle
class. All of this can be done by looking at the examples in the github page as well as some searching around the LÖVE wiki.
7. This question is asking for more of the same, but now so that we can learn how classic
deals with inheritance. Inheritance will be used very sparingly throughout the game (I think there's only really 1 class that other classes inherit from if I remember correctly), but it's still a necessary thing to know how to do. Based on the logic used for the first question, we can look at the examples in the github page and arrive at this:
xxxxxxxxxx
HyperCircle = Circle:extend()
function HyperCircle:new(x, y, radius, line_width, outer_radius)
HyperCircle.super.new(self, x, y, radius)
self.line_width = line_width
self.outer_radius = outer_radius
end
function HyperCircle:update(dt)
HyperCircle.super.update(self, dt)
end
function HyperCircle:draw()
HyperCircle.super.draw(self)
love.graphics.setLineWidth(self.line_width)
love.graphics.circle('line', self.x, self.y, self.outer_radius)
love.graphics.setLineWidth(1)
end
The way inheritance works with this library is that whenever you want to execute functionality from your child class, you need to add the line ClassName.super.methodName(self)
to what you're doing. So, for instance, in the draw
function, HyperCircle.super.draw(self)
draws the inner filled circle, and then the next 3 lines draw the outer circle that the HyperCircle
class is concerned with.
8. The :
operator in Lua is used as a shorthand for passing self
as the first argument of a function when calling it. For instance, both of these lines are the same thing:
xxxxxxxxxx
instance.method(instance, ...)
instance:method(...)
This is a very common pattern in Lua when you have functions that are operating on some object that are also defined on that object, which happens often with object oriented programming. For instance, let's say we have a table that contains fields x
, y
and add
in it. x
and y
are values, and add
is a function adds those two values together and returns them. One way to define this would be like this:
xxxxxxxxxx
local t = {}
t.x = 5
t.y = 4
t.add = function(self)
return self.x + self.y
end
Note that the add
function receives a self
argument. This argument corresponds to a table that contains that x
and y
attributes which we want to add. If we call it like this:
xxxxxxxxxx
t:add()
Then we're passing the t
table itself to the add
function, and so the return result will be 9. Similarly, we could call it like this:
xxxxxxxxxx
t.add(t)
And we'd achieve the same result. We could also define another table u
with different x
and y
values, and then call t.add(u)
and we'd get a different result other than 9, however this is generally not what people do. In general the way objects are built in Lua are like more like the first example where we called t:add()
, since the table will be operating on its own values instead of on another table's values.
9. This question expands on the previous question to test if you really understood the idea that tables can have functions defined in them that will change the table's own attributes.
The value of counter_table.value
is initially what it was defined to be, which is 1. And then after the counter_table:increment()
call it got changed to 2. counter_table:increment()
is the same as counter_table.increment(counter_table)
, which means that the self
variable in that function definition, in this example, corresponds to the counter_table
variable itself, which is why counter_table.value
was able to be incremented in the first place.
If we had defined another table called counter_table_2
which had its .value
attribute defined to 5, and then called counter_table.increment(counter_table_2)
, counter_table.value
would still be 1 and counter_table_2.value
would be 6.
10. This question expands even further on the same concept to really make sure you get it. If you had to get help from the answers for the previous two questions and you couldn't get close to the answer by yourself then you need to spend more time on this idea before you move on. Understanding this is truly essential to understanding how Lua works.
The question is simply asking for a construct similar to the one used in the previous exercise. First it asks for a function that returns a table that contains attributes a
, b
, c
and sum
. Those should be initialized, respectively, to 1, 2, 3 and sum
should be a function:
xxxxxxxxxx
function createSumTable()
return {
a = 1,
b = 2,
c = 3,
sum = function()
end,
}
end
After this it says that sum
should add the previous 3 attributes together, with the final result being stored in the c
attribute. This means that like the previous exercise, the sum
function will receive some table that we'll name self
, and it could be any other table, but we'll most likely call the sum
function from that contains that function definition itself:
xxxxxxxxxx
function createSumTable()
return {
a = 1,
b = 2,
c = 3,
sum = function(self)
self.c = self.a + self.b + self.c
end,
}
end
t = createSumTable()
t:sum()
print(t.c) --> prints 6
11. It is possible for a table to have a method and an attribute of the same name, however things won't work as expected. Either the method or the attribute will overwrite what was there before and then that identifier will only function as one of them. For instance, if the method t.x
is defined first and it's a function that does something, but then the attribute t.x
is defined as the value 5, in the end it will be the value 5 and not the function, since the value was defined later and overwrote the function definition.
Because objects will be tables in Lua, this means that method names cannot be the same as attribute names, and that you can't have multiple methods with the same name, or multiple attributes with the same name in a Lua object. Of course, because Lua is flexible, there are ways to change this behavior using metatables and do a bunch of tricky things that to the end user will make it seem like you can have multiple definitions under the same name, but no Lua OOP library I know of does that and we won't really need it.
12. The global table is Lua is named _G
and it holds references to all global variables in Lua. Whenever you defined a new variable, like a = 5
, what you're really doing is saying _G['a'] = 5
, and you can access the a
variable by saying _G.a
, for instance (on top of just saying a
normally).
13. There is no guarantee that ParentClass
is already defined in that situation. Based on the way we defined the loading of classes, the only thing that we know for sure is that classes are loaded in alphabetical order, other than that the order in which they're loaded is undefined. This means that with the way we do it now, there's no way to guarantee one class is always loaded before the other.
One way to solve this is to simply manually load classes that other classes depend on and them automatically load all other classes. In the game there are only a few classes that fit this definition so this is a suitable solution.
14. In this new way of doing it, the class is returned instead of automatically being assigned to a global variable. So the only thing we have to change in the requireFiles
function is making sure that assignment to a global variable happens. The way the current function looks is like this:
xxxxxxxxxx
function requireFiles(files)
for _, file in ipairs(files) do
local file = file:sub(1, -5)
require(file)
end
end
If we print the file
variable we'll get strings like this, for instance: objects/ObjectName
, where ObjectName
will be the name of some class, and there will be multiple of those strings. What we want to do is take ObjectName
out of this full objects/ObjectName
string, and then use that as the name of our global variable, so essentially we'll be doing _G[ObjectName] = require(file)
. The only thing we have to do then is successfully remove the class name from the full path that we have in the file
variable.
To achieve this we can first find the last /
character. It has to be the last because the full path can have other folders in it in the future, like objects/Enemies/EnemyName
. To do this I just googled "find last specific character in string Lua" and I got this page https://stackoverflow.com/questions/14554193/last-index-of-character-in-string where someone already had the answer for the exact character we need even. So applying that to our case:
xxxxxxxxxx
local file = file:sub(1, -5)
local last_forward_slash_index = file:find("/[^/]*$")
local class_name = file:sub(last_forward_slash_index+1, #file)
First we find the index of the last forward slash using the code from the StackOverflow question. Then we take that index and use the string.sub
function to split the string, from the index found + 1 (because we don't want the forward slash in the final string) to the end of the string. This gets us exactly what we want and so the class_name
variable will contain the name of the class we wanted. Now it's just a matter of loading that globally:
xxxxxxxxxx
local file = file:sub(1, -5)
local last_forward_slash_index = file:find("/[^/]*$")
local class_name = file:sub(last_forward_slash_index+1, #file)
_G[class_name] = require(file)
Another thing that can be done here is that unload a class if it was previously loaded. This might never be useful but it's useful to know that it can be done: by calling package.loaded[file] = nil
before the require(file)
call, we can clear the cache that holds all files that have been previously loaded and force a full reload of that file. This is useful in a number of situations, like if we wanted to implement hot-reloading of our code.
15. For this question all that really has to be done is to test the code and see that whenever mouse1
is pressed (not released nor held) the function runs. This is an alternate thing you can do with the bind
function, which is to just bind a key to a function that will be executed when the key is pressed.
16. For this question we need to first bind the keypad +
key to an action named add
. To do that we need to figure out what's the string used to represent keypad +
. The github page links to this page for key constants and says that for the keyboard they are the same, which means that keypad +
is kp+
. And so the code looks like this:
xxxxxxxxxx
function love.load()
input = Input()
input:bind('kp+', 'add')
end
Then the question asks to increment a sum
variable every 0.25
seconds when the add
action key is held down and to print the result to the console. This is simply an application of the pressRepeat
function:
xxxxxxxxxx
function love.load()
input = Input()
input:bind('kp+', 'add')
sum = 0
end
function love.update(dt)
if input:pressRepeat('add', 0.25) then
sum = sum + 1
print(sum)
end
end
17. Multiple keys can be bound to the same action. What will happen when an action is checked for is that all the keys will be checked for at the same time, so if any of the keys was pressed an event will be generated for that action. Similarly, multiple actions can be bound to the same key and whenever the key is pressed, all actions bound to it will have an event generated for them.
18. This one is a matter of simply applying everything we went over so far, just using gamepad constants instead of mouse or keyboard ones:
xxxxxxxxxx
function love.load()
...
input:bind('fleft', 'left')
input:bind('fright', 'right')
input:bind('fup', 'up')
input:bind('fdown', 'down')
end
function love.update(dt)
...
if input:pressed('left') then print('left') end
if input:pressed('right') then print('right') end
if input:pressed('up') then print('up') end
if input:pressed('down') then print('down') end
end
19. Here we see how values that are not booleans can be used. In the case of triggers, down
can be used to access the value which will be somewhere between 0 and 1, depending on how hard the trigger is being pressed.
xxxxxxxxxx
function love.load()
...
input:bind('l2', 'trigger')
end
function love.update(dt)
...
local left_trigger_value = input:down('trigger')
print(left_trigger_value)
end
20. Same exercise as the previous, but for vertical/horizontal stick positions:
xxxxxxxxxx
function love.load()
...
input:bind('leftx', 'left_horizontal')
input:bind('lefty', 'left_vertical')
input:bind('rightx', 'right_horizontal')
input:bind('righty', 'right_vertical')
end
function love.update(dt)
...
local left_stick_horizontal = input:down('left_horizontal')
local left_stick_vertical = input:down('left_vertical')
local right_stick_horizontal = input:down('right_horizontal')
local right_stick_vertical = input:down('right_vertical')
print(left_stick_horizontal, left_stick_vertical)
print(right_stick_horizontal, right_stick_vertical)
end
21. For this question we need to print 10 random numbers with a 0.5 interval between each print using only a for
and an after
call inside that loop. The thing to do on instinct is something like this:
xxxxxxxxxx
for i = 1, 10 do
timer:after(0.5, function() print(love.math.random()) end)
end
But this will print 10 numbers exactly at the same time after an initial interval of 0.5 seconds, which is not what we wanted. What we did here is just call timer:after
10 times. Another thing one might try is to somehow chain after
calls together like this:
xxxxxxxxxx
timer:after(0.5, function()
print(love.math.random())
timer:after(0.5, function()
print(love.math.random())
...
end)
end)
And then somehow translate that into a for, but there's no reasonable way to do that. The solution lies in figuring out that if you use the i
index from the loop and multiply that by the 0.5 delay, you'll get delays of 0.5, then 1, then 1.5, ... until you get to the last value of 5. And that looks like this:
xxxxxxxxxx
for i = 1, 10 do
timer:after(0.5*i, function() print(love.math.random()) end)
end
The first number is printed after 0.5 seconds and then the others follow. If we needed the first number to printed immediately (instead of with an initial 0.5 seconds delay) then we needed to use i-1
instead of i
.
22. This question asks for a bunch of tweens that happens in sequence after one another. This is just a simple application of the tween function using the optional last argument to chain tweens together. That looks like this:
xxxxxxxxxx
timer:tween(1, rect_1, {w = 0}, 'in-out-cubic', function()
timer:tween(1, rect_2, {h = 0}, 'in-out-cubic', function()
timer:tween(2, rect_1, {w = 50}, 'in-out-cubic')
timer:tween(2, rect_2, {h = 50}, 'in-out-cubic')
end)
end)
23. For this one we need to define two structures, one for which will be the front layer and one that will the background layer of the HP bar. We'll define them simply like tables:
xxxxxxxxxx
function love.load()
...
hp_bar_bg = {x = gw/2, y = gh/2, w = 200, h = 40}
hp_bar_fg = {x = gw/2, y = gh/2, w = 200, h = 40}
end
Here we simply define the x, y
position and w, h
size of the rectangle for both the front and back layer. To draw them we can simply use love.graphics.rectangle
:
xxxxxxxxxx
function love.draw()
...
love.graphics.setColor(222, 64, 64)
love.graphics.rectangle('fill', hp_bar_bg.x, hp_bar_bg.y - hp_bar_bg.h/2, hp_bar_bg.w, hp_bar_bg.h)
love.graphics.setColor(222, 96, 96)
love.graphics.rectangle('fill', hp_bar_fg.x, hp_bar_fg.y - hp_bar_fg.h/2, hp_bar_fg.w, hp_bar_fg.h)
love.graphics.setColor(255, 255, 255)
end
After drawing it, all we need to do is bind the d
key to make the front layer move, and then after a small delay make the background layer move as well. We can use the tween function from a timer to achieve this:
xxxxxxxxxx
function love.keypressed(key)
if key == 'd' then
timer:tween('fg', 0.5, hp_bar_fg, {w = hp_bar_fg.w - 25}, 'in-out-cubic')
timer:after('bg_after', 0.25, function()
timer:tween('bg_tween', 0.5, hp_bar_bg, {w = hp_bar_bg.w - 25}, 'in-out-cubic')
end)
end
end
And so at first we decrease the width of the front bar by 25, and then after 0.25 seconds we do the same for the background bar. One important thing to notice is that all calls that use the timer have a name. This is because whenever we press the key and another previous tween is happening (like if you press the key multiple times really fast), then we want to cancel the previous tween so that two tweens aren't operating on the same variable at once.
24. The easiest way to do this is to change the timer:after
call to be timer:every
instead:
xxxxxxxxxx
function love.load()
...
timer:every(12, function()
timer:tween(6, circle, {radius = 96}, 'in-out-cubic', function()
timer:tween(6, circle, {radius = 24}, 'in-out-cubic')
end)
end)
end
The duration of the every
call should be the sum of the duration of both the expand and shrink tween inside it, so in this case it goes on for 12 seconds, since each tween goes for 6 seconds.
25. The only difference with this exercise is that instead of using every
we have to use multiple after
calls to simulate every
, which is something that the timer library supports:
xxxxxxxxxx
function love.load()
...
timer:after(0, function(f)
timer:tween(6, circle, {radius = 96}, 'in-out-cubic', function()
timer:tween(6, circle, {radius = 24}, 'in-out-cubic')
end)
timer:after(12, f)
end)
end
26. This question asks for the application of the naming system in a tween, like in question 23. So the easiest way to do that is to break down each part of the behavior (expand, shrink) and tie it to the different keys:
xxxxxxxxxx
function love.keypressed(key)
if key == 'e' then
timer:tween('expand', 6, circle, {radius = 96}, 'in-out-cubic')
elseif key == 's' then
timer:tween('shrink', 6, circle, {radius = 24}, 'in-out-cubic')
end
end
With this, whenever the user presses either e
or s
, the circle will expand or shrink appropriately. However, if the other key is pressed while the tween from the previous press is happening, then two tweens will be operating on the same variable at the same time and bugs will happen. For instance, if you press e
and then 2 seconds later you press s
, for 4 seconds both tweens will be active.
To fix this we need to actively cancel other tweens that might be active. In the case of expand, cancel shrink, and in the case of shrink, cancel expand:
xxxxxxxxxx
function love.keypressed(key)
if key == 'e' then
timer:cancel('shrink')
timer:tween('expand', 6, circle, {radius = 96}, 'in-out-cubic')
elseif key == 's' then
timer:cancel('expand')
timer:tween('shrink', 6, circle, {radius = 24}, 'in-out-cubic')
end
end
We don't need to also call the cancel function for the same tag (i.e. calling timer:cancel('expand')
when e
is pressed) because when the tween call has a tag attached to it it automatically does that once the tag is repeated in another call.
27. The problem this question is asking about is how to change a variable that is not inside a table with the tween function. The second argument of the tween method receives a table, and then in the next argument after that the attribute that is to be changed can be specified. But how do we do this if a variable is not in a table, like a free floating global variable? The answer is to remember that all global variables in Lua are inside the _G
table:
xxxxxxxxxx
timer:tween(1, _G, {a = 20}, 'linear')
In this way, the a
variable will have its value changed in the way the question asked. It's worth noting that I don't think there's a way of doing this if the variable was defined locally, since in that case the variable wouldn't be in the global environment table.
For the answers for the table exercises, it's assumed that the moses library was initiated to the fn
global variable.
28. This is an application of the each
function as it is shown in the examples:
xxxxxxxxxx
fn.each(a, print)
29. This is an application of the count
function as it is shown in the examples:
xxxxxxxxxx
fn.count(b, 1)
30. This is an application of the map
function as it is shown in the examples:
xxxxxxxxxx
fn.map(d, function(_, v) return v+1 end)
31. This is an application of the map
function but is a bit more involved:
xxxxxxxxxx
fn.map(a, function(_, v)
if type(v) == 'number' then return 2*v
elseif type(v) == 'string' then return v .. 'xD'
elseif type(v) == 'boolean' then return not v
elseif type(v) == 'table' then end
end)
Here we make use of the type function, which returns the type of a value as a string. After verifying which type we're dealing with we proceed with what's asked for each type.
32. This is an application of the reduce
function as it is shown in the examples:
xxxxxxxxxx
fn.reduce(d, function(memo, v) return memo+v end)
33. This is an application of the include
function as it is shown in the examples. I use this a lot but I usually use the any
alias instead, since I remember what it does better with that name:
xxxxxxxxxx
if fn.any(b, 9) then
print('table contains the value 9')
end
34. This is an application of the detect
function as it is shown in the examples:
xxxxxxxxxx
fn.detect(c, 7)
35. This is an application of the select
or filter
function as it is shown in the examples:
xxxxxxxxxx
fn.filter(d, function(_, v) return v < 5 end)
This is the same as:
xxxxxxxxxx
fn.filter(d, function(_, v)
if v < 5 then
return true
end
end)
So what this function should do is return true
for values that should stay and false
or nil
otherwise. The reject
function works the same, except with the opposite values being returned: values that should be removed should return true
instead.
36. Same as the previous exercise:
xxxxxxxxxx
fn.filter(c, function(_, v) return type(v) == 'string' end)
37. This is an application of the all
function as it is shown in the examples:
xxxxxxxxxx
fn.all(c, function(_, v) return type(v) == 'number' end)
fn.all(d, function(_, v) return type(v) == 'number' end)
38. This is an application of the shuffle
function as it is shown in the examples:
xxxxxxxxxx
local shuffled_d = fn.shuffle(d)
Note that the d
list isn't actually shuffled in place. A new shuffled table is returned and then you have to go from there. In a lot of functions this is the case but in some it isn't, so generally you should test to see if the function you're using modifies the table in place or not.
39. This is an application of the reverse
function as it is shown in the examples:
xxxxxxxxxx
fn.reverse(d)
40. This is an application of the pull
or remove
function as it is shown in the examples:
xxxxxxxxxx
fn.remove(d, 1, 4)
41. This is an application of the union
function as it is shown in the examples:
xxxxxxxxxx
fn.union(b, c, d)
42. This is an application of the intersection
function as it is shown in the examples:
xxxxxxxxxx
fn.intersection(b, d)
43. This is an application of the append
function as it is shown in the examples:
xxxxxxxxxx
fn.append(b, d)