
GameObject class, we define the depth attribute, which is initially 50 for all entities. Then, if we want to define the constructor of each class, we can set the depth attribute for each object class ourselves. The idea is that objects with greater depth should be drawn from above, and smaller depth - at the bottom. That is, for example, if we want all effects to be drawn on top of everything else, then we can simply assign them to the depth attribute, for example, the value 75. function TickEffect:new(area, x, y, opts) TickEffect.super.new(self, area, x, y, opts) self.depth = 75 ... end game_objects list by the depth attribute of each object: function Area:draw() table.sort(self.game_objects, function(a, b) return a.depth < b.depth end) for _, game_object in ipairs(self.game_objects) do game_object:draw() end end table.sort to sort entities by their depth attribute. Entities with a smaller depth will move to the front of the table, that is, they will be drawn first (under the rest), and entities with greater depth will move to the end of the table and will be the last ones (on top of everything). If you try to set different depth values for different types of objects, you will see that it works.game_objects table, game_objects may occur. Flickering occurs because if objects have the same depth, then in one frame one object may be on top of another, but in the next frame, go down below it. The likelihood of this is small, but it can happen and we should prevent it. function 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 y . Entities with a larger value of y (that is, closer to the bottom of the screen) should be rendered last, and entities with a smaller value of y should be drawn first. What will the sort function look like in this case?
function Player:new(...) ... self.max_boost = 100 self.boost = self.max_boost end function Player:update(dt) ... self.boost = math.min(self.boost + 10*dt, self.max_boost) ... end boost is set to max_boost , that is, 100, and then we add to boost by 10 per second until the value max_boost . We can also implement rule 2 by simply subtracting 50 units per second when the player accelerates: function Player:update(dt) ... if input:down('up') then self.boosting = true self.max_v = 1.5*self.base_max_v self.boost = self.boost - 50*dt end if input:down('down') then self.boosting = true self.max_v = 0.5*self.base_max_v self.boost = self.boost - 50*dt end ... end self.boost -= 50*dt . Now, to check rule 4, we need to make sure that when boost reaches 0, the cooling will start for 2 seconds. This is a bit more complicated, because more moving parts are used here. The code looks like this: function Player:new(...) ... self.can_boost = true self.boost_timer = 0 self.boost_cooldown = 2 end can_boost will be used to report when acceleration can be performed. By default, it is true, because the player must be able to accelerate when starting the game. It is set to false when boost reaches 0, and then true in boost_cooldown seconds. The variable boost_timer will track how much time has passed after the boost reached 0, and when this variable exceeds boost_cooldown , can_boost will be set to true. function Player:update(dt) ... self.boost = math.min(self.boost + 10*dt, self.max_boost) self.boost_timer = self.boost_timer + dt if self.boost_timer > self.boost_cooldown then self.can_boost = true end self.max_v = self.base_max_v self.boosting = false if input:down('up') and self.boost > 1 and self.can_boost then self.boosting = true self.max_v = 1.5*self.base_max_v self.boost = self.boost - 50*dt if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 end end if input:down('down') and self.boost > 1 and self.can_boost then self.boosting = true self.max_v = 0.5*self.base_max_v self.boost = self.boost - 50*dt if self.boost <= 1 then self.boosting = false self.can_boost = false self.boost_timer = 0 end end self.trail_color = skill_point_color if self.boosting then self.trail_color = boost_color end end input:down , we also check that boost above 1 (rule 5) and that can_boost is true (rule 5). When boost reaches 0, we set the variables boosting and can_boost false, and then reset boost_timer to 0. Since boost_timer added to boost_timer , every two seconds it will set can_boost to true and we can accelerate again (rule 4). function Player:new(...) ... self.max_hp = 100 self.hp = self.max_hp self.max_ammo = 100 self.ammo = self.max_ammo end 
Ammo class and begin with the definitions: function Ammo:new(...) ... self.w, self.h = 8, 8 self.collider = self.area.world:newRectangleCollider(self.x, self.y, self.w, self.h) self.collider:setObject(self) self.collider:setFixedRotation(false) self.r = random(0, 2*math.pi) self.v = random(10, 20) self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) self.collider:applyAngularImpulse(random(-24, 24)) end function Ammo:draw() love.graphics.setColor(ammo_color) pushRotate(self.x, self.y, self.collider:getAngle()) draft:rhombus(self.x, self.y, self.w, self.h, 'line') love.graphics.pop() love.graphics.setColor(default_color) end setLinearVelocity and applyAngularImpulse . In addition, this object is drawn using the draft library. This is a small library that allows you to draw all sorts of shapes more conveniently than you would have done it yourself. In our case, we can simply draw the resource like any rectangle, but I decided to do it this way. I will assume that you have already installed the library yourself and have read the documentation, having learned about its capabilities. In addition, we will take into account the rotation of the physical object, using the getAngle result in pushRotate . function Stage:new() ... input:bind('p', function() self.area:addGameObject('Ammo', random(0, gw), random(0, gh)) end) end (collision classes) . For a start, we can define three classes of collisions for already existing objects: the player, shells, and resources. function Stage:new() ... self.area = Area(self) self.area:addPhysicsWorld() self.area.world:addCollisionClass('Player') self.area.world:addCollisionClass('Projectile') self.area.world:addCollisionClass('Collectable') ... end setCollisionClass (repeat this code in other files): function Player:new(...) ... self.collider:setCollisionClass('Player') ... end Collectable collision class so that it ignores the Player : self.area.world:addCollisionClass('Collectable', {ignores = {'Player'}}) addCollisionClass : function Stage:new() ... self.area.world:addCollisionClass('Player') self.area.world:addCollisionClass('Projectile', {ignores = {'Projectile'}}) self.area.world:addCollisionClass('Collectable', {ignores = {'Collectable', 'Projectile'}}) ... end enter : function Player:update(dt) ... if self.collider:enter('Collectable') then print(1) end end Ammo class is the slow motion of an object towards a player. The easiest way to do this is to add the behavior of Seek Behavior . My version of seek behavior is based on the book Programming Game AI by Example , in which there is a very good selection of general control behaviors. I will not explain the behavior in detail, because, frankly, I don’t remember how it works, so if you are interested, sort it out yourself: D function Ammo:update(dt) ... local target = current_room.player if target then local projectile_heading = Vector(self.collider:getLinearVelocity()):normalized() local angle = math.atan2(target.y - self.y, target.x - self.x) local to_target_heading = Vector(math.cos(angle), math.sin(angle)):normalized() local final_heading = (projectile_heading + 0.1*to_target_heading):normalized() self.collider:setLinearVelocity(self.v*final_heading.x, self.v*final_heading.y) else self.collider:setLinearVelocity(self.v*math.cos(self.r), self.v*math.sin(self.r)) end end target , if it exists, otherwise the resource will move in the initially specified direction. target contains a link to the player, which is set in the Stage as follows: function Stage:new() ... self.player = self.area:addGameObject('Player', gw/2, gh/2) end ProjectileDeathEffect object: a small white flash occurs, and then the actual color of the effect appears. The only difference here is that instead of drawing a square, we will draw a rhombus, that is, the same shape that we used to draw the actual ammunition resource. I will name this new object AmmoEffect . We will not consider it in detail, because it is similar to ProjectileDeathEffect . However, we call it as follows: function Ammo:die() self.dead = true self.area:addGameObject('AmmoEffect', self.x, self.y, {color = ammo_color, w = self.w, h = self.h}) for i = 1, love.math.random(4, 8) do self.area:addGameObject('ExplodeParticle', self.x, self.y, {s = 3, color = ammo_color}) end end AmmoEffect object and then from 4 to 8 ExplodeParticle objects, which we have already used in the effect of death Player. The die function of the Ammo object will be called when it collides with the Player: function Player:update(dt) ... if self.collider:enter('Collectable') then local collision_data = self.collider:getEnterCollisionData('Collectable') local object = collision_data.collider:getObject() if object:is(Ammo) then object:die() end end end getEnterCollisionData to retrieve collision data generated by the last enter collision event for the specified label. Then we use getObject to access the object attached to the collider participating in the collision event, which can be any object in the Collectable collision class. In our case, we only have an Ammo object, but if we had others, then it would be here that the code would distinguish them. And this is exactly what we do - to check if the object obtained from getObject is an Ammo class, we use the is function from the classic library. If this is actually an object of the Ammo class, then we call its function die . All this should look like this:
addAmmo function, which simply adds a specific value to the ammo variable and checks that it does not exceed max_ammo : function Player:addAmmo(amount) self.ammo = math.min(self.ammo + amount, self.max_ammo) end object:die() in the newly added code.
Boost class will be approximately the same as that of the Ammo class. It looks like this: function Boost:new(...) ... local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = random(48, gh - 48) self.w, self.h = 12, 12 self.collider = self.area.world:newRectangleCollider(self.x, self.y, self.w, self.h) self.collider:setObject(self) self.collider:setCollisionClass('Collectable') self.collider:setFixedRotation(false) self.v = -direction*random(20, 40) self.collider:setLinearVelocity(self.v, 0) self.collider:applyAngularImpulse(random(-24, 24)) end function Boost:update(dt) ... self.collider:setLinearVelocity(self.v, 0) end table.randomdefined utils.luaas follows: function table.random(t) return t[love.math.random(1, #t)] end -48or gw+48, that is, an object is created outside the screen, but close enough to its edge.-direction, because if the object is on the right, thendirectionequals 1; we want to move it to the left, so the speed must be negative (and vice versa for the opposite side). The component's velocity along the x axis is always assigned an attribute value v, and the y component is assigned a value of 0. We want the object to move along a horizontal line, so we set the velocity along y to 0. function Boost:draw() love.graphics.setColor(boost_color) pushRotate(self.x, self.y, self.collider:getAngle()) draft:rhombus(self.x, self.y, 1.5*self.w, 1.5*self.h, 'line') draft:rhombus(self.x, self.y, 0.5*self.w, 0.5*self.h, 'fill') love.graphics.pop() love.graphics.setColor(default_color) end AmmoEffect(however, it is a bit more complicated), and the second is used for text +BOOST. We'll start with one that looks like AmmoEffect and name it BoostEffect.AmmoEffectonly difference lies in the execution time of each phase: from 0.1 to 0.2 in the first phase and from 0.15 to 0.35 in the second: function BoostEffect:new(...) ... self.current_color = default_color self.timer:after(0.2, function() self.current_color = self.color self.timer:after(0.35, function() self.dead = true end) end) end visible, if true, the effect will be drawn, and if false, it will not. By changing the value of this variable, we will achieve the desired effect: function BoostEffect:new(...) ... self.visible = true self.timer:after(0.2, function() self.timer:every(0.05, function() self.visible = not self.visible end, 6) self.timer:after(0.35, function() self.visible = true end) end) end everyat intervals of 0.05 seconds, and after completion we finally make the effect visible. The effect “dies” after 0.55 seconds (because we set the deadvalue to true after 0.55 when setting the current color), so making it visible at the end is not very important. Now we can draw it as follows: function BoostEffect:draw() if not self.visible then return end love.graphics.setColor(self.current_color) draft:rhombus(self.x, self.y, 1.34*self.w, 1.34*self.h, 'fill') draft:rhombus(self.x, self.y, 2*self.w, 2*self.h, 'line') love.graphics.setColor(default_color) end function BoostEffect:new(...) ... self.sx, self.sy = 1, 1 self.timer:tween(0.35, self, {sx = 2, sy = 2}, 'in-out-cubic') end function BoostEffect:draw() ... draft:rhombus(self.x, self.y, self.sx*2*self.w, self.sy*2*self.h, 'line') ... end sxand sywill be increased to 2 for 0.35 seconds, that is, circuit, too, for the 0.35 second double. In the end, the result will look like this (I assume that you have already linked the function of diethis object to the collision event with the Player, as we did with the ammunition resource):

BoostEffect. Flicker uses the same logic as BoostEffect, that is, we have already considered it.characters, process this table, and then draw each character from the table on the screen with all modifications and effects.InfoText. We will call it like this: function Boost:die() ... self.area:addGameObject('InfoText', self.x, self.y, {text = '+BOOST', color = boost_color}) end text. Then the definition of the base class will look like this: function InfoText:new(...) ... self.depth = 80 self.characters = {} for i = 1, #self.text do table.insert(self.characters, self.text:utf8sub(i, i)) end end utf8. In general, it is a good idea to manipulate strings using a library that supports all types of characters, and as we will see soon, this is especially important for our object.Oin line +BOOSTmeans drawing at position initial_x_position + widthOf('+B'). In our case, the problem with getting the width +Bis that it depends on the font used, since we will use the function Font:getWidth, but have not yet set the font. However, we can easily solve this problem!resources/fontsand then upload it. I will leave the code needed to load it as an exercise for you, because it is somewhat similar to the code used to load class definitions from a folder objects(Exercise 14). By the end of this download process, we will have a global table fontsthat will contain all the downloaded fonts in the formatfontname_fontsize. In this example we will use m5x7_16: function InfoText:new(...) ... self.font = fonts.m5x7_16 ... end function InfoText:draw() love.graphics.setFont(self.font) for i = 1, #self.characters do local width = 0 if i > 1 then for j = 1, i-1 do width = width + self.font:getWidth(self.characters[j]) end end love.graphics.setColor(self.color) love.graphics.print(self.characters[i], self.x + width, self.y, 0, 1, 1, 0, self.font:getHeight()/2) end love.graphics.setColor(default_color) end love.graphics.setFontto specify the font that we want to use in the following drawing operations. Then we have to go through each of the characters, and then draw them. But first we need to calculate its position in x, which is the sum of the width of all characters before it. The internal loop that accumulates a variable widthdoes just that. It starts from 1 (the beginning of the line) to i-1 (the character before the current one) and adds the width of each character to the total width, that is, to the sum of all of them. Then we use love.graphics.printto draw each individual character in its corresponding position. We also shift each character half the height of the font (so that the characters are centered relative to the position given by us y).
function InfoText:new(...) ... self.visible = true self.timer:after(0.70, function() self.timer:every(0.05, function() self.visible = not self.visible end, 6) self.timer:after(0.35, function() self.visible = true end) end) self.timer:after(1.10, function() self.dead = true end) end afterfor 0.7 seconds, which we defined above. We will do this - every 0.035 seconds we will run a procedure that has a chance to change a character to another random character. It looks like this: self.timer:after(0.70, function() ... self.timer:every(0.035, function() for i, character in ipairs(self.characters) do if love.math.random(1, 20) <= 1 then -- change character else -- leave character as it is end end end) end) random_characters, which is a string containing all the characters that a character can change into. When a character needs to change, we randomly select a character from this string: self.timer:after(0.70, function() ... self.timer:every(0.035, function() local random_characters = '0123456789!@#$%¨&*()-=+[]^~/;?><.,|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWYXZ' 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 end end) end) 
background_colorsand foreground_colors. Each table is the same size as the table characters, and will simply contain the background and primary colors for each character. If no symbol is specified for a symbol in this table, it will by default use the primary color ( boost_color) and a transparent background. function InfoText:new(...) ... self.background_colors = {} self.foreground_colors = {} end function InfoText:draw() ... for i = 1, #self.characters do ... if self.background_colors[i] then love.graphics.setColor(self.background_colors[i]) love.graphics.rectangle('fill', self.x + width, self.y - self.font:getHeight()/2, self.font:getWidth(self.characters[i]), self.font:getHeight()) end love.graphics.setColor(self.foreground_colors[i] or self.color or default_color) love.graphics.print(self.characters[i], self.x + width, self.y, 0, 1, 1, 0, self.font:getHeight()/2) end end background_colors[i](background color for the current character), then for the background color we simply draw a rectangle in the appropriate position and the size of the current character. We change the main color by simply setting the setColorcolor of the current character using the color. If foreground_colors[i]not defined, then by default it is equal self.color, which for this object is always equal boost_color, since we are the ones that we transmit when we call it from the Boost object. But if self.colornot defined, then it defaults to white ( default_color). By itself, this code fragment does nothing, because we have not defined the values inside the tables background_colorsand foreground_colors. self.timer:after(0.70, function() ... self.timer:every(0.035, function() for i, character in ipairs(self.characters) do ... if love.math.random(1, 10) <= 1 then -- change background color else -- set background color to transparent end if love.math.random(1, 10) <= 2 then -- change foreground color else -- set foreground color to boost_color end end end) end) table.random. In addition, beyond this, we will define six more colors, which will be negatives of the six original ones. That is, if we have the original color 232, 48, 192, then its negative can be defined as 255-232, 255-48, 255-192. function InfoText:new(...) ... local default_colors = {default_color, hp_color, ammo_color, boost_color, skill_point_color} local 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]} } self.all_colors = fn.append(default_colors, negative_colors) ... end appendto combine them. Then now we can do something of a type table.random(self.all_colors)and get one random color out of ten defined in this table. That is, we can do the following: self.timer:after(0.70, function() ... self.timer:every(0.035, function() for i, character in ipairs(self.characters) do ... 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.background_colors[i] = nil end end end) end) 
addAmmoso that it supports the addition of negative values and does not allow the attribute ammoto fall below 0. Do the same for the functions addBoostand addHP(adding HP resource will be the task of another exercise).addResourceand removeResource?InfoTextchange the probability of changing the symbol by 20%, the probability of changing the primary color by 5%, and the probability of changing the background color by 30%.default_colors, negative_colorsAnd all_colorsin InfoTextnon-locally and globally.InfoTextso that it is created between -self.wand self.wover the x component, between -self.hand self.hover the y component. Attributes wand hrelate to the Boost object that creates the InfoText. function Area:getAllGameObjectsThat(filter) local out = {} for _, game_object in pairs(self.game_objects) do if filter(game_object) then table.insert(out, game_object) end end return out end function InfoText:new(...) ... local all_info_texts = self.area:getAllGameObjectsThat(function(o) if o:is(InfoText) and o.id ~= self.id then return true end end) end 

function Projectile:draw() love.graphics.setColor(default_color) pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) love.graphics.setLineWidth(self.s - self.s/4) love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y) love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y) love.graphics.setLineWidth(1) love.graphics.pop() end pushRotatewe use the speed of the projectile, so we can rotate it in accordance with the angle at which it moves. Then inside we use love.graphics.setLineWidthand set a value roughly proportional to the attribute s, but slightly smaller. This means that shells with sa large overall will be thicker. Then we draw the projectile with love.graphics.line. It is also important that we draw one line from -2*self.sto the center, and then one more from the center to 2*self.s. We do this because each attack will have its own color, and we will change the color of one of these lines, but not the second. That is, for example, if we do this: function Projectile:draw() love.graphics.setColor(default_color) pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) love.graphics.setLineWidth(self.s - self.s/4) love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y) love.graphics.setColor(hp_color) -- change half the projectile line to another color love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y) love.graphics.setLineWidth(1) love.graphics.pop() end 
globals.luaand for now it will look like this: attacks = { ['Neutral'] = {cooldown = 0.24, ammo = 0, abbreviation = 'N', color = default_color}, } Neutral. It will use the attack parameters that we already have in the game. Now we can define a function setAttackthat will replace one attack with another and use this global attack table: function Player:setAttack(attack) self.attack = attack self.shoot_cooldown = attacks[attack].cooldown self.ammo = self.max_ammo end function Player:new(...) ... self:setAttack('Neutral') ... end attackthat will contain the name of the current attack. This attribute will be used in the function shootto check the current active attack and determine how to create projectiles.shoot_cooldown. We have not created this attribute yet, but it will be similar to the attributes boost_timerand boost_cooldown. It will be used to control how often an action takes place, in our case an attack. We will delete this line: function Player:new(...) ... self.timer:every(0.24, function() self:shoot() end) ... end function Player:new(...) ... self.shoot_timer = 0 self.shoot_cooldown = 0.24 ... end function Player:update(dt) ... self.shoot_timer = self.shoot_timer + dt if self.shoot_timer > self.shoot_cooldown then self.shoot_timer = 0 self:shoot() end ... end shootso that it starts to take into account the existence of various attacks: function Player:shoot() local d = 1.2*self.w self.area:addGameObject('ShootEffect', self.x + d*math.cos(self.r), self.y + d*math.sin(self.r), {player = self, d = d}) if self.attack == 'Neutral' then self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r), self.y + 1.5*d*math.sin(self.r), {r = self.r}) end end if self.attack == 'Neutral'current attack with a conventional design . This function will gradually grow into a large chain of conditions, because we have to check all 16 attacks.Double. She looks like this:
ammo_color(I obtained these values by trial and error): attacks = { ... ['Double'] = {cooldown = 0.32, ammo = 2, abbreviation = '2', color = ammo_color}, } shoot: function Player:shoot() ... elseif self.attack == 'Double' then self.ammo = self.ammo - attacks[self.attack].ammo self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r + math.pi/12), self.y + 1.5*d*math.sin(self.r + math.pi/12), {r = self.r + math.pi/12, attack = self.attack}) self.area:addGameObject('Projectile', self.x + 1.5*d*math.cos(self.r - math.pi/12), self.y + 1.5*d*math.sin(self.r - math.pi/12), {r = self.r - math.pi/12, attack = self.attack}) end end attack. We will do this for each type of projectile, because it will help us determine what type of attack the projectile belongs to. This is useful for setting the appropriate color, as well as for changing behavior if necessary. The Projectile object now looks like this: function Projectile:new(...) ... self.color = attacks[self.attack].color ... end function Projectile:draw() pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()) love.graphics.setLineWidth(self.s - self.s/4) love.graphics.setColor(self.color) love.graphics.line(self.x - 2*self.s, self.y, self.x, self.y) love.graphics.setColor(default_color) love.graphics.line(self.x, self.y, self.x + 2*self.s, self.y) love.graphics.setLineWidth(1) love.graphics.pop() end colorcolor defined for this attack in the global table attacks. And in the draw function, we draw one part of the line with this color, which is an attribute color, and the other, which is default_color. For most types of shells, the scheme will be the same. function Player:shoot() ... elseif self.attack == 'Double' then self.ammo = self.ammo - attacks[self.attack].ammo ... end end ammodrops to 0, we change the current attack to Neutral: function Player:shoot() ... if self.ammo <= 0 then self:setAttack('Neutral') self.ammo = self.max_ammo end end shoot, because we do not want the player to shoot after the amount of ammunition drops to 0.
Triple. Its definition in the attack table looks like this: attacks['Triple'] = {cooldown = 0.32, ammo = 3, abbreviation = '3', color = boost_color} 
Double, but there is still an additional projectile, created in the middle (at the same angle as the attack projectile Neutral). Create this attack in the same way as Double.Rapid. Its definition in the attack table looks like this: attacks['Rapid'] = {cooldown = 0.12, ammo = 1, abbreviation = 'R', color = default_color} 
Spread. Its definition in the attack table: attacks['Spread'] = {cooldown = 0.16, ammo = 1, abbreviation = 'RS', color = default_color} 
all_colors(or through the frame, depending on how you feel better).Back. Its definition in the attack table looks like this: attacks['Back'] = {cooldown = 0.32, ammo = 2, abbreviation = 'Ba', color = skill_point_color} 
Side. Its definition in the attack table: attacks['Side'] = {cooldown = 0.32, ammo = 2, abbreviation = 'Si', color = boost_color} 
Attack. As Boostwith SkillPoint, the Attack resource is created in the left or right edge of the screen, and then very slowly moves inward. When a player interacts with an Attack resource, his attack with the function setAttackchanges to an attack that is contained in the resource.abbreviationin the table attacks. Here is what they look like:
Rock. It looks like this:
Boost, but with a few differences: function Rock:new(area, x, y, opts) Rock.super.new(self, area, x, y, opts) local direction = table.random({-1, 1}) self.x = gw/2 + direction*(gw/2 + 48) self.y = random(16, gh - 16) self.w, self.h = 8, 8 self.collider = self.area.world:newPolygonCollider(createIrregularPolygon(8)) self.collider:setPosition(self.x, self.y) self.collider:setObject(self) self.collider:setCollisionClass('Enemy') self.collider:setFixedRotation(false) self.v = -direction*random(20, 40) self.collider:setLinearVelocity(self.v, 0) self.collider:applyAngularImpulse(random(-100, 100)) end createIrregularPolygonto be defined in utils.lua. This function should return a list of vertices that make up an irregular rectangle. By an irregular rectangle, I mean one that looks like a circle, but each vertex of which may be slightly closer or farther from the center, and in which the angles between each of the vertices may also be slightly random.sizeand point_amount. The first will relate to the radius of the circle, and the second to the number of points that make up the polygon (polygon): function createIrregularPolygon(size, point_amount) local point_amount = point_amount or 8 end point_amountnot defined, then the default is 8.point_amount, in each iteration of which we will determine the next vertex based on the interval of angles. For example, to determine the position of the second point, we can say that its angle will be in the interval 2*angle_interval, where angle_intervalis the value 2*math.pi/point_amount. That is, in this case it will be approximately equal to 90 degrees. It is more logical to write this code, so: function createIrregularPolygon(size, point_amount) local point_amount = point_amount or 8 local points = {} for i = 1, point_amount do local angle_interval = 2*math.pi/point_amount local distance = size + random(-size/4, size/4) local angle = (i-1)*angle_interval + random(-angle_interval/4, angle_interval/4) table.insert(points, distance*math.cos(angle)) table.insert(points, distance*math.sin(angle)) end return points end angle_interval, as explained above, but also define distanceas being somewhere within the radius of a circle, but with a random offset from -size/4to +size/4. This means that each vertex will not be exactly on the circle of the circle, but somewhere nearby. We also randomly randomize a range of angles to create the same effect. Finally, we add the x and y components to the list of returned points. Notice that the polygon is created in local space (assuming that the center is at 0, 0), that is, to place the object in the right place, we will then have to use it setPosition.Enemy. Like all other classes of collisions, this one must also be defined before use: function Stage:new() ... self.area.world:addCollisionClass('Enemy') ... end love.graphics.polygon: function Rock:draw() love.graphics.setColor(hp_color) local points = {self.collider:getWorldPoints(self.collider.shapes.main:getPoints())} love.graphics.polygon('line', points) love.graphics.setColor(default_color) end PolygonShape:getPoints. The points are returned in local coordinates, and we need global ones, so we have to use Body:getWorldPointsto convert local coordinates to global ones. After that we can draw a polygon and it will behave as we expect. Note that since we get points directly from the collider, and the collider is a rotating polygon, we do not need to use pushRotateto rotate the object, as it did with the Boost object, because the resulting points already take into account the rotation of objects.
hpto the Rock class with an initial value of 100hit . :damage , , 100damage hp , hp 0 , Rock «»hp 0 , hit_flash true, 0,2 — false. hit_flash true, default_color , hp_color .EnemyDeathEffect. This effect is created when the enemy dies and behaves exactly like an object ProjectileDeathEffect, only it is larger and corresponds to the size of the Rock object. This object must be created when the attribute hpof the Rock object becomes 0 or lower.hitwith the amount of damage the projectile hits (by default, the projectiles will have an attribute damageinitially equal to 100). When hit, the projectile must also call its own function die.hit. This function should do the following:damage, and in the case when it is not defined, the default value is 10invincibleto true.ExplodeParticleaddHP(or removeHP, if you decide to add it) should receive the attribute damageand use it to reduce the HP of the Player object. Inside the function addHP(or removeHP), a method must be implemented to handle the situation when hpit becomes equal to or less than 0 and the player dies.invincible true, — false. , 0,2 6, , 0,5 0,25. , invisible true false 0,04 , invincible true. invisible true, Player .hitshould be called when a player collides with Enemy. In the event of a conflict with the enemy, the player must be dealt damage 30.
EnemyProjectile.Projectile. Both of these objects will have a lot of common code. We can abstract into a general object of shells that have a common behavior, but in fact this is not necessary, because there will be only two types of shells in the game. After copying we must make the following changes: function EnemyProjectile:new(...) ... self.collider:setCollisionClass('EnemyProjectile') end function Stage:new() ... self.area.world:addCollisionClass('EnemyProjectile', {ignores = {'EnemyProjectile', 'Projectile', 'Enemy'}}) end function EnemyProjectile:new(...) ... self.damage = 10 end function EnemyProjectile:update(dt) ... if self.collider:enter('Player') then local collision_data = self.collider:getEnterCollisionData('Player') ... end function EnemyProjectile:draw() love.graphics.setColor(hp_color) ... love.graphics.setColor(default_color) end 
function Shooter:new(...) ... self.w, self.h = 12, 6 self.collider = self.area.world:newPolygonCollider( {self.w, 0, -self.w/2, self.h, -self.w, 0, -self.w/2, -self.h}) end function Shooter:new(...) ... self.collider:setFixedRotation(false) self.collider:setAngle(direction == 1 and 0 or math.pi) self.collider:setFixedRotation(true) end direction == 1 and math.pi or 0is the implementation of the ternary operator in Lua. In other languages, it may look like (direction == 1) ? math.pi : 0. I think the exercises in parts 2 and 4 allowed you to examine them in detail. In essence, the following happens here: ifdirectionequal to 1 (the enemy appears to the right and is directed to the left), then the first conventional design is true, that is, we get true and math.pi or 0. Because of the order of execution andand or, the first will be true and math.pi, that is, as a result we will have math.pi or 0, which returns math.pi, because when both elements are true, then the orfirst one returns. On the other hand, if it directionis -1, then the first conditional is spars to false and we succeed false and math.pi or 0, that is false or 0, which leads us to 0, because when the first element is false, it orreturns the second.
function Player:new(...) ... self.timer:every(random(3, 5), function() -- spawn PreAttackEffect object with duration of 1 second self.timer:after(1, function() -- spawn EnemyProjectile end) end) end PreAttackEffectfor one second.TargetParticle. These particles will move to the point that we define as the target, and then die after some time, or when they reach the goal. function TargetParticle:new(area, x, y, opts) TargetParticle.super.new(self, area, x, y, opts) self.r = opts.r or random(2, 3) self.timer:tween(opts.d or random(0.1, 0.3), self, {r = 0, x = self.target_x, y = self.target_y}, 'out-cubic', function() self.dead = true end) end function TargetParticle:draw() love.graphics.setColor(self.color) draft:rhombus(self.x, self.y, 2*self.r, 2*self.r, 'fill') love.graphics.setColor(default_color) end d(or a random value from 0.1 to 0.3 seconds), the transition is performed by the function tween k target_x, target_y, and when the particle reaches this position, it dies. The particle is also drawn as a rhombus (as in one of the effects created earlier), but it can also be drawn as a circle or square, because it is rather small and becomes even smaller with time.PreAttackEffectas follows: function PreAttackEffect:new(...) ... self.timer:every(0.02, function() self.area:addGameObject('TargetParticle', self.x + random(-20, 20), self.y + random(-20, 20), {target_x = self.x, target_y = self.y, color = self.color}) end) end target_x, target_yvalue of the position of the effect itself (that is, on the nose of the ship).Shooterwe create PreAttackEffect like this: function Shooter:new(...) ... self.timer:every(random(3, 5), function() self.area:addGameObject('PreAttackEffect', self.x + 1.4*self.w*math.cos(self.collider:getAngle()), self.y + 1.4*self.w*math.sin(self.collider:getAngle()), {shooter = self, color = hp_color, duration = 1}) self.timer:after(1, function() end) end) end durationthat controls the lifetime of the PreAttackEffect object. Here we can do the following: function PreAttackEffect:new(...) ... self.timer:after(self.duration - self.duration/4, function() self.dead = true end) end durationbecause this is the object that I myself call the “controller object”. For example, it has nothing in the draw function, that is, we will never see it in the game. We see only the objects TargetParticlethat he orders to create. These objects have a random lifetime, from 0.1 to 0.3 seconds, that is, if we want the last particles to end immediately when the shell is fired, then this object will die in 0.1-0.3 seconds later than its duration in 1 second. I decided to make it equal to 0.75 (duration - duration / 4), but you can instead use another number, closer to 0.9 seconds.
shooterpointing to the Shooter object that created the PreAttackEffect object, so we can simply update the position of the PreAttackEffect based on the position of this parent object shooter: function PreAttackEffect:update(dt) ... if self.shooter and not self.shooter.dead then self.x = self.shooter.x + 1.4*self.shooter.w*math.cos(self.shooter.collider:getAngle()) self.y = self.shooter.y + 1.4*self.shooter.w*math.sin(self.shooter.collider:getAngle()) end end 
not self.shooter.dead. It may happen that when we refer to objects inside each other in a similar way, then when one object dies, another one will still keep a reference to it. For example, a PreAttackEffect object lives 0.75 seconds, but between its creation and disappearance, the Shooter object that created it can be killed by a player. If this happens, there may be a problem.colliderShooter object attribute , which is destroyed when the Shooter object dies. And if this object is destroyed, we can do nothing with it, because it no longer exists. So when we try to executegetAnglethen the game will fall out. We can work out a general system that solves such a problem, but in fact I don’t think it is necessary. For now, it is enough for us to simply be attentive to when we refer to objects in this way, so as not to try to gain access to objects that may already be dead. function Shooter:new(...) ... self.timer:every(random(3, 5), function() ... self.timer:after(1, function() self.area:addGameObject('EnemyProjectile', self.x + 1.4*self.w*math.cos(self.collider:getAngle()), self.y + 1.4*self.w*math.sin(self.collider:getAngle()), {r = math.atan2(current_room.player.y - self.y, current_room.player.x - self.x), v = random(80, 100), s = 3.5}) end) end) end rattribute) pointing in the direction of the player. In general, when we want to get the angle from sourceto target, we need to do the following: angle = math.atan2(target.y - source.y, target.x - source.x) 
dieboth objects is called and both are destroyed.directionin the class Shooter confuse us? If so, how should it be renamed? If not, why not?Source: https://habr.com/ru/post/349718/
All Articles