93. This one is asking for the reverse of what the article talked about, which means it's simply a matter of reversing the comparisons wherever necessary:
xfunction Area:draw()
table.sort(self.game_objects, function(a, b)
if a.depth == b.depth then return a.creation_time > b.creation_time
else return a.depth > b.depth end
end)
for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end
94. This exercise is asking that instead of using the depth
attribute as the parameter to be compared, we use the y
attribute. On top of that, it's asking us to use the y
attribute and make it so that the objects with higher y drawn last. This is the same ordering idea as the one used in the article (and the opposite of the one used in the last exercise), so:
xxxxxxxxxx
function Area:draw()
table.sort(self.game_objects, function(a, b)
if a.y == b.y then return a.creation_time < b.creation_time
else return a.y < b.y end
end)
for _, game_object in ipairs(self.game_objects) do game_object:draw() end
end
We can also keep the line that sorts by creation_time
here since sometimes in a 2.5D game like that objects can have the same y
attribute as well and flickering can also occur.
95. This is simply a matter of change the definition of the Projectile class with the addCollisionClass
call:
xxxxxxxxxx
function Stage:new()
...
self.area.world:addCollisionClass('Projectile', {ignores = {'Projectile', 'Player'}})
...
end
It's important to note that we can't change the Player projectile class instead and make it ignore Projectile (even though the end result would be the same), because the Projectile collision class is defined only after the Player collision class. This is a small quirk of the physics library we're using.
96. The addAmmo
function looks like this right now:
xxxxxxxxxx
function Player:addAmmo(amount)
self.ammo = math.min(self.ammo + amount, self.max_ammo)
end
The question is asking that we take into account the possibility that the value in amount
might be negative, and take precautions against ammo
going below 0. We can easily do that by wrapping out initial calculation into a math.max
call:
xxxxxxxxxx
function Player:addAmmo(amount)
self.ammo = math.max(math.min(self.ammo + amount, self.max_ammo), 0)
end
In this way, the result of the addition of ammo
and amount
will first be checked so that it doesn't go above max_ammo
, and immediately after that checked so that it doesn't go below 0. The same applies to the other functions:
xxxxxxxxxx
function Player:addBoost(amount)
self.boost = math.max(math.min(self.boost + amount, self.max_boost), 0)
end
xxxxxxxxxx
function Player:addHP(amount)
self.hp = math.max(math.min(self.hp + amount, self.max_hp), 0)
end
97. This is entirely up to preference. The only change I'd make is that if we're handling the values in the same function then it might make more sense to name that function changeResource
instead of addResource
, since we would be adding and removing from some resource in the same function.
98. This exercise is a matter of changing the values around the love.math.random
calls in the constructor of the InfoText
class. This is what it looks like now:
xxxxxxxxxx
for i, character in ipairs(self.characters) do
if love.math.random(1, 20) <= 1 then
local r = love.math.random(1, #random_characters)
self.characters[i] = random_characters:utf8sub(r, r)
else self.characters[i] = character end
if love.math.random(1, 10) <= 1 then
self.background_colors[i] = table.random(self.all_colors)
else self.background_colors[i] = nil end
if love.math.random(1, 10) <= 2 then
self.foreground_colors[i] = table.random(self.all_colors)
else self.foreground_colors[i] = nil end
end
So to change the probability of a character being changed to 20%, we need to change the first if love.math.random(1, 20) <= 1
, which is a 5%, to something like love.math.random(1, 5) <= 1
or love.math.random(1, 10) <= 2
, which would be 20%. And to change the probability of the foreground color being changed to 5% we need to change it from love.math.random(1, 10) <= 1
, which is 10%, to something like love.math.random(1, 20) <= 1
. And finally, to change the probability of a background color being changed to 30% we need to change it from love.math.random(1, 10) <= 2
, which is 20%, to something like love.math.random(1, 10) <= 3
.
So the final result would look like this:
xxxxxxxxxx
for i, character in ipairs(self.characters) do
if love.math.random(1, 5) <= 1 then
local r = love.math.random(1, #random_characters)
self.characters[i] = random_characters:utf8sub(r, r)
else self.characters[i] = character end
if love.math.random(1, 20) <= 1 then
self.background_colors[i] = table.random(self.all_colors)
else self.background_colors[i] = nil end
if love.math.random(1, 10) <= 3 then
self.foreground_colors[i] = table.random(self.all_colors)
else self.foreground_colors[i] = nil end
end
99. This is simply a matter of defining those tables in globals.lua
:
xxxxxxxxxx
default_colors = {default_color, hp_color, ammo_color, boost_color, skill_point_color}
negative_colors = {
{255-default_color[1], 255-default_color[2], 255-default_color[3]},
{255-hp_color[1], 255-hp_color[2], 255-hp_color[3]},
{255-ammo_color[1], 255-ammo_color[2], 255-ammo_color[3]},
{255-boost_color[1], 255-boost_color[2], 255-boost_color[3]},
{255-skill_point_color[1], 255-skill_point_color[2], 255-skill_point_color[3]}
}
all_colors = fn.append(default_colors, negative_colors)
And then also changing all references to from self.all_colors
to justall_colors
.
100. The current way the InfoText
object is spawned from the Boost
object looks like this:
xxxxxxxxxx
function Boost:die()
...
self.area:addGameObject('InfoText', self.x, self.y, {color = boost_color, text = '+BOOST'})
end
To randomize its starting position based on the constraints the exercise asks for we can do something like this:
xxxxxxxxxx
function Boost:die()
...
self.area:addGameObject('InfoText',
self.x + random(-self.w, self.w), self.y + random(-self.h, self.h),
{color = boost_color, text = '+BOOST'})
end
And this would solve the exercise itself. However, upon testing this looks a bit off sometimes. A slightly better solution, in my opinion, is something like this:
xxxxxxxxxx
function Boost:die()
...
self.area:addGameObject('InfoText',
self.x + table.random({-1, 1})*self.w, self.y + table.random({-1, 1})*self.h,
{color = boost_color, text = '+BOOST'})
end
In this way instead of randomizing to a position between some width and height, we always go for either the position to one of the extremes of that range. This makes the results more predictable and prevents the problem of the text appearing on top of the boost resource, which would make it unreadable.
101. This exercise is very involved and can be solved in any number of ways. The solution outlined here is only one possible out of many!
The question starts with the assumption that you have all InfoText
objects that are currently alive in a table named all_info_texts
and then asks you to make it so that the current InfoText object doesn't visually collide with any other. This is an important problem to solve because like in the last exercise, if the text becomes unreadable at any point then it stops serving its purpose. And if multiple texts are being printed on top of each other the chances that they'll become unreadable are high, so we want to prevent that from happening.
The first thing we need to do to get InfoText objects to not visually collide with each other is to define their sizes. Their width is defined by the width of the text when using the font this object uses, and their height is defined by the height of this font:
xxxxxxxxxx
function InfoText:new(...)
...
self.font = fonts.m5x7_16
self.w, self.h = self.font:getWidth(self.text), self.font:getHeight()
...
end
After we have the position and size of each InfoText object, we wanna go through the list containing all of them and seeing if it collides with the current InfoText object. We'll wrap this into a local function called collidesWithOtherInfoText
which returns true when a collision is happening and false otherwise. Note that here a collision refers to the overlapping of rectangles that defines each object.
xxxxxxxxxx
function InfoText:new(...)
...
local all_info_texts = self.area:getAllGameObjectsThat(function(o)
if o:is(Ammo) and o.id ~= self.id then return true end
end)
local collidesWithOtherInfoText = function()
for _, info_text in ipairs(all_info_texts) do
return areRectanglesOverlapping(
self.x, self.y, self.x + self.w, self.y + self.h,
info_text.x, info_text.y, info_text.x + info_text.w, info_text.y + info_text.h
)
end
end
...
end
Here we introduce a function named areRectanglesOverlapping
, which takes in the positions of two rectangles and returns true if they're overlapping. A check to see if rectangles overlap can be found with some simple googling, like on this StackOverflow answer. And so according to that answer, we can create the function in utils.lua
and it looks like this:
xxxxxxxxxx
function areRectanglesOverlapping(x1, y1, x2, y2, x3, y3, x4, y4)
return not (x3 > x2 or x4 < x1 or y3 > y2 or y4 < y1)
end
Finally, after collidesWithOtherInfoText
is defined what we can do is just call it, see if it returns true, and if it does then we move this InfoText object to a nearby position somewhat randomly. Then we call this check again to see if it still collides with another, and if it does, we move it randomly again. And we repeat this until this InfoText object isn't colliding with any other one. However intuitive this might seem or that it might get stuck in an infinite loop, this solution actually works out pretty well. The way I decided to do it was just with a while loop, and the amount I decided to move the InfoText by was by its width/height either left/right or up/down, as you can see below:
xxxxxxxxxx
function InfoText:new(...)
...
while collidesWithOtherInfoText() do
self.x = self.x + table.random({-1, 0, 1})*self.w
self.y = self.y + table.random({-1, 0, 1})*self.h
end
...
end
And so with this we end up solving the problem. Moving the InfoText object by in self.w
and self.h
steps both leads to more predictable results, but also leads to fewer iterations of the than if we just moved it by a random amount.