You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Mar 8, 2021. It is now read-only.
In this tutorial we'll focus on getting more basics of gameplay down on the Player side of things. First we'll add the most fundamental stats: ammo, boost, HP and skill points. These stats will be used throughout the entire game and they're the main resources the player will use to do everything he can do. After that we'll focus on the creation of Resource objects, which are objects that the player can gather that contain the stats just mentioned. And finally after that we'll add the attack system as well as a few different attacks to the Player.
Draw Order
Before we go over to the main parts of this article, setting the game's draw order is something important that I forgot to mention in the previous article so we'll go over it now.
The draw order decides which objects will be drawn on top and which will be drawn behind which. For instance, right now we have a bunch of effects that are drawn when something happens. If the effects are drawn behind other objects like the Player then they either won't be visible or they will look wrong. Because of this we need to make sure that they are always drawn on top of everything and for that we need to define some sort of drawing order between objects.
The way we'll set this up is somewhat straight forward. In the GameObject class we'll define a depth attribute which will start at 50 for all entities. Then, in the definition of each class' constructors, we will be able to define the depth attribute ourselves for that class of objects if we want to. The idea is that objects with a higher depth will be drawn in front, while objects with a lower depth will be drawn behind. So, for instance, if we want to make sure that all effects are drawn in front of everything else, we can just set their depth attribute to something like 75.
functionTickEffect:new(area, x, y, opts)
TickEffect.super.new(self, area, x, y, opts)
self.depth=75...end
Now, the way that this works internally is that every frame we'll be sorting the game_objects list according to the depth attribute of each object:
functionArea:draw()
table.sort(self.game_objects, function(a, b)
returna.depth<b.depthend)
for_, game_objectinipairs(self.game_objects) dogame_object:draw() endend
Here, before drawing we simply use table.sort to sort the entities based on their depth attribute. Entities that have lower depth will be sorted to the front of the table and so will be drawn first (behind everything), and entities that have a higher depth will be sorted to the back of the table and so they will be drawn last (in front of everything). If you try setting different depth values to different types of objects you should see that this works out well.
One small problem that this approach has though is that some objects will have the same depth, and when this happens, depending on how the game_objects table is being sorted over time, flickering can occur. Flickering occurs because if objects have the same depth, in one frame one object might be sorted to be in front of another, but in another frame it might be sorted to be behind another. It's unlikely to happen but it can happen and we should take precautions against that.
One way to solve it is to define another sorting parameter in case objects have the same depth. In this case the other parameter I chose was the object's time of creation:
functionArea:draw()
table.sort(self.game_objects, function(a, b)
ifa.depth==b.depththenreturna.creation_time<b.creation_timeelsereturna.depth<b.depthendend)
for_, game_objectinipairs(self.game_objects) dogame_object:draw() endend
So if the depths are equal then objects that were created earlier will be drawn earlier, and objects that were created later will be drawn later. This is a reasonable solution and if you test it out you'll see that it also works!
Draw Order Exercises
93. Order objects so that ones with higher depth are drawn behind and ones with lower depth are drawn in front of others. In case the objects have the same depth, they should be ordered by their creation time. Objects that were created earlier should be drawn last, and objects that were created later should be drawn first.
94. In a top-downish 2.5D game like in the gif below, one of the things you have to do to make sure that the entities are drawn in the appropriate order is sort them by their y position. Entities that have a higher y position (meaning that they are closer to the bottom of the screen) should be drawn last, while entities that have a lower y position should be drawn first. What would the sorting function look like in that case?
Basic Stats
Now we'll start with stat building. The first stat we'll focus on is the boost one. The way it works now is that whenever the player presses up or down the ship will take a different speed based on the key pressed. The way it should work is that on top of this basic functionality, it should also be a resource that depletes with use and regenerates over time when not being used. The specific numbers and rules that will be used are these:
The player will have 100 boost initially
Whenever the player is boosting 50 boost will be removed per second
At all times 10 boost is generated per second
Whenever boost reaches 0 a 2 second cooldown is applied before it can be used again
Boosts can only happen when the cooldown is off and when the boost resource is above 0
These sound a little complicated but they're not. The first three are just number specifications, and the last two are to prevent boosts from never ending. When the resource reaches 0 it will regenerate to 1 pretty consistently and this can lead to a scenario where you can essentially use the boost forever, so a cooldown has to be added to prevent this from happening.
So with this we take care of rules 1 and 3. We start boost with max_boost, which is 100, and then we add 10 per second to boost while making sure that it doesn't go over max_boost. We can easily also get rule 2 done by simply decreasing 50 boost per second whenever the player is boosting:
Part of this code was already here from before, so the only lines we really added were the self.boost -= 50*dt ones. Now to check rule 4 we need to make sure that whenever boost reaches 0 a 2 second cooldown is started before the player can boost again. This is a bit more complicated because if involves more moving parts, but it looks like this:
At first we'll introduce 3 variables. can_boost will be used to tell when a boost can happen. By default it's set to true because the player should be able to boost at the start. It will be set to false once boost reaches 0 and then it will be set to true again boost_cooldown seconds after. The boost_timer variable will take care of tracking how much time it has been since boost reached 0, and if this variable goes above boost_cooldown then we will set can_boost to true.
This looks complicated but it just follows from what we wanted to achieve. Instead of just checking to see if a key is being pressed with input:down, we additionally also make sure that boost is above 1 (rule 5) and that can_boost is true (rule 5). Whenever boost reaches 0 we set boosting and can_boost to false, and then we reset boost_timer to 0. Since boost_timer is being added dt every frame, after 2 seconds it will set can_boost to true and we'll be able to boost again (rule 4).
The code above is also the way the boost mechanism should look like now in its complete state. One thing to note is that you might think that this looks ugly or unorganized or any number of combination of bad things. But this is just what a lot of code that takes care of certain aspects of gameplay looks like. It's multiple rules that are being followed and they sort of have to be followed all in the same place. It's important to get used to code like this, in my opinion.
In any case, out of the basic stats, boost was the only one that had some more involved logic to it. There are two more important stats: ammo and HP, but both are way simpler. Ammo will just get depleted whenever the player attacks and regained whenever a resource is collected in gameplay, and HP will get depleted whenever the player is hit and also regained whenever a resource is collected in gameplay. For now, we can just add them as basic stats like we did for the boost:
What I call resources are small objects that affect one of the main basic stats that we just went over. The game will have a total of 5 of these types of objects and they'll work like this:
Ammo resource restores 5 ammo to the player and is spawned on enemy death
Boost resource restores 25 boost to the player and is spawned randomly by the Director
HP resource restores 25 HP to the player and is spawned randomly by the Director
SkillPoint resource adds 1 skill point to the player and is spawned randomly by the Director
Attack resource changes the current player attack and is spawned randomly by the Director
The Director is a piece of code that handles the spawning of enemies as well as resources. I called it that because other games (like L4D) call it that and it just seems to fit. Because we're not going to work on that piece of code yet, for now we'll bind the creation of each resource to a key just to test that it works.
Ammo Resource
So let's get started on the ammo. The final result should be like this:
The little green rectangles are the ammo resource. When the player hits one of them with its body the resource is destroyed and the player gets 5 ammo. We can create a new class named Ammo and start with some definitions:
Ammo resources will be physics rectangles that start with some random and small velocity and rotation, set initially by setLinearVelocity and applyAngularImpulse. This object is also drawn using the draft library. This is a small library that lets you draw all sorts of shapes more easily than if you had to do it yourself. For this case you can just draw the resource as a rectangle if you want, but I'll choose to go with this. I'll also assume that you can already install the library yourself and read the documentation to figure out what it can and can't do. Additionally, we're also taking into account the rotation of the physics object by using the result from getAngle in pushRotate.
To test this all out, we can bind the creation of one of these objects to a key like this:
functionStage:new()
...input:bind('p', function()
self.area:addGameObject('Ammo', random(0, gw), random(0, gh))
end)
end
And if you run the game now and press p a bunch of times you should see these objects spawning and moving/rotating around.
The next thing we should do is create the collision interaction between player and resource. This interaction will hold true for all resources and will be mostly the same always. The first thing we want to do is make sure that we can capture an event when the player physics object collides with the ammo physics object. The easiest way to do this is through the use of collision classes. To start with we can define 3 collision classes for objects that are already exist: the Player, the projectiles and the resources.
And then in each one of those files (Player, Projectile and Ammo) we can set the collider's collision class using setCollisionClass (repeat the code below for the other files):
By itself this doesn't change anything, but it gives us a base to work with and to capture collision events between physics objects. For instance, if we change the Collectable collision class to ignore the Player like this:
Then if you run the game again you'll notice that the player is now physically ignoring the ammo resource objects. This isn't what we want to do in the end but it serves as a nice example of what we can do with collision classes. The rules we actually want these 3 collision classes to follow are the following:
Projectile will ignore Projectile
Collectable will ignore Collectable
Collectable will ignore Projectile
Player will generate collision events with Collectable
Rules 1, 2 and 3 can be satisfied by making small changes to the addCollisionClass calls:
It's important to note that the order of the declaration of collision classes matters. For instance, if we swapped the order of the Projectile and Collectable declarations a bug would happen because the Collectable collision class makes reference to the Projectile collision class, but since the Projectile collision class isn't yet defined it bugs out.
The fourth rule can be satisfied by using the enter call:
And if you run this you'll see that 1 will be printed to the console every time the player collides with an ammo resource.
Another piece of behavior we need to add to the Ammo class is that it needs to move towards the player slightly. An easy way to do this is to add the Seek Behavior to it. My version of the seek behavior was based on the book Programming Game AI by Example which has a very nice section on steering behaviors in general. I'm not going to explain it in detail because I honestly don't remember how it works, so I'll just assume if you're curious about it you'll figure it out :D
So here the ammo resource will head towards the target if it exists, otherwise it will just move towards the direction it was initially set to move towards. target contains a reference to the player, which was set in Stage like this:
functionStage:new()
...self.player=self.area:addGameObject('Player', gw/2, gh/2)
end
Finally, the only thing left to do is what happens when an ammo resource is collected. From the gif above you can see that a little effect plays (like the one for when a projectile dies), along with some particles, and then the player also gets +5 ammo.
Let's start with the effect. This effect follows the exact same logic as the ProjectileDeathEffect object, in that there's a little white flash and then the actual color of the effect comes on. The only difference here is that instead of drawing a square we will be drawing a rhombus, which is the same shape that we used to draw the ammo resource itself. I'll call this new object AmmoEffect and I won't really go over it in detail since it's the same as ProjectileDeathEffect. The way we call it though is like this:
Here we are creating one AmmoEffect object and then between 4 and 8 ExplodeParticle objects, which we already used before for the Player's death effect. The die function on an Ammo object will get called whenever it collides with the Player:
Here first we use getEnterCollisionData to get the collision data generated by the last enter collision event for the specified tag. After this we use getObject to get access to the object attached to the collider involved in this collision event, which could be any object of the Collectable collision class. In this case we only have the Ammo object to worry about, but if we had others here's where we'd place the code to separate between them. And that's what we do, to really check that the object we get from getObject is the of the Ammo class we use classic's is function. If it is really is an object of the Ammo class then we call its die function. All that should look like this:
One final thing we forgot to do is actually add +5 ammo to the player whenever an ammo resource is gathered. For this we'll define an addAmmo function which simply adds a certain amount to the ammo variable and checks that it doesn't go over max_ammo:
functionPlayer:addAmmo(amount)
self.ammo=math.min(self.ammo+amount, self.max_ammo)
end
And then we can just call this after object:die() in the collision code we just added.
Boost Resource
Now for the boost. The final result should look like this:
As you can see, the idea is almost the same as the ammo resource, except that the boost resource's movement is a little different, it looks a little different and the visual effect that happens when one is gathered is different as well.
So let's start with the basics. For every resource other than the ammo one, they'll be spawned either on the left or right of the screen and they'll move really slowly in a straight line to the other side. The same applies to the enemies. This gives the player enough time to move towards the resource to pick it up if he wants to.
The basic starting setup of the Boost class is about the same as the Ammo one and looks like this:
There are a few differences though. The 3 first lines in the constructor are picking the initial position of this object. The table.random function is defined in utils.lua as follows:
functiontable.random(t)
returnt[love.math.random(1, #t)]
end
And as you can see what it does is just pick a random element from a table. In this case, we're picking either -1 or 1 to signify the direction in which the object will be spawned. If -1 is picked then the object will be spawned to the left of the screen, and if 1 is picked then it will be spawned to the right. More precisely, the exact positions chosen for its chosen position will be either -48 or gw+48, so slightly offscreen but close enough to the edge.
After this we define the object mostly like the Ammo one, with a few differences again only when it comes to its velocity. If this object was spawned to the right then we want it to move left, and if it was spawned to the left then we want it to move right. So its velocity is set to a random value between 20 and 40, but also multiplied by -direction, since if the object was to the right, direction was 1, and if we want to move it to the left then the velocity has to be negative (and the opposite for the other side). The object's velocity is always set to the v attribute on the x component and set to 0 on the y component. We want the object to remain moving in a horizontal line no matter what so setting its y velocity to 0 will achieve that.
The final main difference is in the way its drawn:
Here instead of just drawing a single rhombus we draw one inner and one outer to be used as sort of an outline. You can obviously draw all these objects in whatever way you want, but this is what I personally decided to do.
Now for the effects. There are two effects being used here: one that is similar to AmmoEffect (although a bit more involved) and the one that is used for the +BOOST text. We'll start with the one that's similar to AmmoEffect and call it BoostEffect.
This effect has two parts to it, the center with its white flash and the blinking effect before it disappears. The center works in the same way as the AmmoEffect, the only difference is that the timing of each phase is different, from 0.1 to 0.2 in the first phase and from 0.15 to 0.35 in the second:
functionBoostEffect:new(...)
...self.current_color=default_colorself.timer:after(0.2, function()
self.current_color=self.colorself.timer:after(0.35, function()
self.dead=trueend)
end)
end
The other part of the effect is the blinking before it dies. This can be achieved by creating a variable named visible, which when set to true will draw the effect and when set to false will not draw the effect. By changing this variable from false to true really fast we can achieve the desired effect:
Here we use the every call to switch between visible and not visible six times, each with a 0.05 seconds delay in between, and after that's done we set it to be visible in the end. The effect will die after 0.55 seconds anyway (since we set dead to true after 0.55 seconds when setting the current color) so setting it to be visible in the end isn't super important to do. In any case, then we can draw it like this:
We're simply drawing both the inner and outer rhombus at different sizes. The exact numbers (1.34, 2) were reached through pretty much trial and error based on what looked best.
The final thing we need to do for this effect is to make the outer rhombus outline expand over the life of the object. We can do that like this:
functionBoostEffect:new(...)
...self.sx, self.sy=1, 1self.timer:tween(0.35, self, {sx=2, sy=2}, 'in-out-cubic')
end
With this, the sx and sy variables will grow to 2 over 0.35 seconds, which means that the outline rhombus will also grow to double its previous value over those 0.35 seconds. In the end the result looks like this (I'm assuming you already linked this object's die function to the collision event with the Player, like we did for the ammo resource):
Now for the other part of the effect, the crazy looking text. This text effect will be used throughout the game pretty much everywhere so let's make sure we get it right. Here's what the effect looks like again:
First let's break this effect down into its multiple parts. The first thing to notice is that it's simply a string being drawn to the screen initially, but then near its end it starts blinking like the BoostEffect object. That blinking part turns out to use exactly the same logic as the BoostEffect so we have that covered already.
What also happens though is that the letters of the string start changing randomly to other letters, and each character's background also changes colors randomly. This suggests that this effect is processing characters individually internally rather than operating on a single string, which probably means we'll have to do something like hold all characters in a characters table, operate on this table, and then draw each character on that table with all its modifications and effects to the screen.
So to start with this in mind we can define the basics of the InfoText class. The way we're gonna call it is like this:
functionBoost:die()
...self.area:addGameObject('InfoText', self.x, self.y, {text='+BOOST', color=boost_color})
end
And so the text attribute will contain our string. Then the basic definition of the class can look like this:
With this we simply define that this object will have depth of 80 (higher than all other objects, so will be drawn in front of everything) and then we separate the initial string into characters in a table. We use an utf8 library to do this. In general it's a good idea to manipulate strings with a library that supports all sorts of characters, and for this object it's really important that we do this as we'll see soon.
In any case, the drawing of these characters should also be done on an individual basis because as we figured out earlier, each character has its own background that can change randomly, so it's probably the case that we'll want to draw each character individually.
The logic used to draw characters individually is basically to go over the table of characters and draw each character at the x position that is the sum of all characters before it. So, for instance, drawing the first O in +BOOST means drawing it at something like initial_x_position + widthOf('+B'). The problem with getting the width of +B in this case is that it depends on the font being used, since we'll use the Font:getWidth function, and right now we haven't set any font. We can solve that easily though!
For this effect the font used will be m5x7 by Daniel Linssen. We can put this font in the folder resources/fonts and then load it. The code needed for loading it will be left as an exercise, since it's somewhat similar to the code used to load class definitions in the objects folder (exercise 14). By the end of this loading process we should have a global table called fonts that has all loaded fonts in the format fontname_fontsize. In this instance we'll use m5x7_16:
First we use love.graphics.setFont to set the font we want to use for the next drawing operations. After this we go over each character and then draw it. But first we need to figure out its x position, which is the sum of the width of the characters before it. The inner loop that accumulates on the variable width is doing just that. It goes from 1 (the start of the string) to i-1 (the character before the current one) and adds the width of each character to a total final width that is the sum of all of them. After that we use love.graphics.print to draw each individual character at its appropriate position. We also offset each character up by half the height of the font (so that the characters are centered around the y position we defined).
If we test all this out now it looks like this:
Which looks about right!
Now we can move on to making the text blink a little before disappearing. This uses the same logic as the BoostEffect object so we can just kinda copy it:
And if you run this you should see that the text stays normal for a while, starts blinking and then disappears.
Now the hard part, which is making each character change randomly as well as its foreground and background colors. This changing starts at about the same that the character starts blinking, so we'll place this piece of code inside the 0.7 seconds after call we just defined above. The way we'll do this is that every 0.035 seconds, we'll run a procedure that will have a chance to change a character to another random character. That looks like this:
self.timer:after(0.70, function()
...self.timer:every(0.035, function()
fori, characterinipairs(self.characters) doiflove.math.random(1, 20) <=1then-- change characterelse-- leave character as it isendendend)
end)
And so each 0.035 seconds, for each character there's a 5% probability that it will be changed to something else. We can complete this by adding a variable named random_characters which is a string that contains all characters a character might change to, and then when a character change is necessary we pick one at random from this string:
We can use the same logic we used here to change the character's colors as well as their background colors. For that we'll define two tables, background_colors and foreground_colors. Each table will be the same size of the characters table and will simply hold the background and foreground colors for each character. If a certain character doesn't have any colors set in these tables then it just defaults to the default color for the foreground (boost_color ) and to a transparent background.
For the background colors we simply draw a rectangle at the appropriate position and with the size of the current character if background_colors[i] (the background color for the current character) is defined. As for the foreground color, we simply set the color to draw the current character with using setColor. If foreground_colors[i] isn't defined then it defauls to self.color, which for this object should always be boost_color since that's what we're passing in when we call it from the Boost object. But if self.color isn't defined either then it defaults to white (default_color). By itself this piece of code won't really do anything, because we haven't defined any of the values inside the background_colors or the foreground_colors tables.
To do that we can use the same logic we used to change characters randomly:
self.timer:after(0.70, function()
...self.timer:every(0.035, function()
fori, characterinipairs(self.characters) do...iflove.math.random(1, 10) <=1then-- change background colorelse-- set background color to transparentendiflove.math.random(1, 10) <=2then-- change foreground colorelse-- set foreground color to boost_colorendendend)
end)
The code that changes colors around will have to pick between a list of colors. We defined a group of 6 colors globally and we could just put those all into a list and then use table.random to pick one at random. What we'll do is that but also define 6 more colors on top of it which will be the negatives of the 6 original ones. So say you have 232, 48, 192 as the original color, we can define its negative as 255-232, 255-48, 255-192.
So here we define two tables that contain the appropriate values for each color and then we use the append function to join them together. So now we can say something like table.random(self.all_colors) to get a random color out of the 10 defined in those tables, which means that we can do this:
And if we run the game now it should look like this:
And that's it. We'll improve it even more later on (and on the exercises) but it's enough for now. Lastly, the final thing we should do is make sure that whenever we collect a boost resource we actually add +25 boost to the player. This works exactly the same way as it did for the ammo resource so I'm going to skip it.
Resources Exercises
95. Make it so that the Projectile collision class will ignore the Player collision class.
96. Change the addAmmo function so that it supports the addition of negative values and doesn't let the ammo attribute go below 0. Do the same for the addBoost and addHP functions (adding the HP resource is an exercise defined below).
97. Following the previous exercise, is it better to handle positive and negative values on the same function or to separate between addResource and removeResource functions instead?
98. In the InfoText object, change the probability of a character being changed to 20%, the probability of a foreground color being changed to 5%, and the probability of a background color being changed to 30%.
99. Define the default_colors, negative_colors and all_colors tables globally instead of locally in InfoText.
100. Randomize the position of the InfoText object so that it is spawned between -self.w and self.w in its x component and between -self.h and self.h in its y component. The w and h attributes refer to the Boost object that is spawning the InfoText.
Which returns all game objects inside an Area that pass a filter function. And then assume that it's called like this inside InfoText's constructor:
functionInfoText:new(...)
...localall_info_texts=self.area:getAllGameObjectsThat(function(o)
ifo:is(InfoText) ando.id~=self.idthenreturntrueendend)
end
Which returns all existing and alive InfoText objects that are not this one. Now make it so that this InfoText object doesn't visually collide with any other InfoText object, meaning, it doesn't occupy the same space on the screen as another such that its text would become unreadable. You may do this in whatever you think is best as long as it achieves the goal.
102. (CONTENT) Add the HP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +25 to HP instead. The resource and effects look like this:
103. (CONTENT) Add the SP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +1 to SP instead. The SP resource should also be defined as a global variable for now instead of an internal one to the Player object. The resource and effects look like this:
Attacks
Alright, so now for attacks. Before anything else the first thing we're gonna do is change the way projectiles are drawn. Right now they're being drawn as circles but we want them as lines. This can be achieved with something like this:
In the pushRotate function we use the projectile's velocity so that we can rotate it towards the angle its moving at. Then inside we use love.graphics.setLineWidth and set it to a value somewhat proportional to the s attribute but slightly smaller. This means that projectiles with bigger s will be thicker in general. Then we draw the projectile using love.graphics.line and importantly, we draw one line from -2*self.s to the center and then another from the center to 2*self.s. We do this because each attack will have different colors, and what we'll do is change the color of one those lines but not change the color of another. So, for instance, if we do this:
functionProjectile: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 colorlove.graphics.line(self.x, self.y, self.x+2*self.s, self.y)
love.graphics.setLineWidth(1)
love.graphics.pop()
end
It will look like this:
In this way we can make each attack have its own color which helps with letting the player better understand what's going on on the screen.
The game will end up having 16 attacks but we'll cover only a few of them now. The way the attack system will work is very simple and these are the rules:
Attacks (except the Neutral one) consume ammo with every shot;
When ammo hits 0 the current attack is changed to Neutral;
New attacks can be obtained through resources that are spawned randomly;
When a new attack is obtained, the current attack is removed and ammo is fully regenerated;
Each attack consumes a different amount of ammo and has different properties.
The first we're gonna do is define a table that will hold information on each attack, such as their cooldown, ammo consumption and color. We'll define this in globals.lua and for now it will look like this:
The normal attack that we already have defined is called Neutral and it simply has the stats that the attack we had in the game had so far. Now what we can do is define a function called setAttack which will change from one attack to another and use this global table of attacks:
Here we simply change an attribute called attack which will contain the name of the current attack. This attribute will be used in the shoot function to check which attack is currently active and how we should proceed with projectile creation.
We also change an attribute named shoot_cooldown. This is an attribute that we haven't created yet, but similar to how the boost_timer and boost_cooldown attributes work, they will be used to control how often something can happen, in this case how often an attack happen. We will remove this line:
Finally, at the end of the setAttack function we also regenerate the ammo resource. With this we take care of rule 4. The next thing we can do is change the shoot function a little to start taking into account the fact that different attacks exist:
Before launching the projectile we check the current attack with the if self.attack == 'Neutral' conditional. This function will grow based on a big conditional chain like this where we'll be checking for all 16 attacks that we add.
So let's get started with adding one actual attack to see what it's like. The attack we'll add will be called Double and it looks like this:
And as you can see it shoots 2 projectiles at an angle instead of one. To get started with this first we'll add the attack's description to the global attacks table. This attack will have a cooldown of 0.32, cost 2 ammo, and its color will be ammo_color (these values were reached through trial and error):
Here we create two projectiles instead of one, each pointing with an angle offset of math.pi/12 radians, or 15 degrees. We also make it so that the projectile receives the attack attribute as the name of the attack. For each projectile type we'll do this as it will help us identify which attack this projectile belongs to. That is helpful for setting its appropriate color as well as changing its behavior when necessary. The Projectile object now looks like this:
In the constructor we set color to the color defined in the global attacks table for this attack. And then in the draw function we draw one part of the line with its color being the color attribute, and another being default_color. For most projectile types this drawing setup will hold.
The last thing we forgot to do is to make it so that this attack obeys rule 1, meaning that we forgot to add code to make it consume the amount of ammo it should consume. This is a pretty simple fix:
With this rule 1 (for the Double attack) will be followed. We can also add the code that will make rule 2 come true, which is that when ammo hits 0, we change the current attack to the Neutral one:
The angles on the projectile are exactly the same as Double, except that there's one extra projectile also being spawned along the middle (at the same angle that the Neutral projectile is spawned). Create this attack following the same steps that were used for the Double attack.
105. (CONTENT) Implement the Rapid attack. Its definition on the attacks table looks like this:
The angles used for the shots are a random value between -math.pi/8 and +math.pi/8. This attack's projectile color also works a bit differently. Instead of having one color only, the color changes randomly to one inside the all_colors list every frame (or every other frame depending on what you think is best).
107. (CONTENT) Implement the Back attack. Its definition on the attacks table looks like this:
109. (CONTENT) Implement the Attack resource. Like the Boost and SkillPoint resources, the Attack resource is spawned from either the left or right of the screen at a random y position, and then moves inward very slowly. When the player comes into contact with an Attack resource, his attack is changed to the attack that the resource contains using the setAttack function.
Attack resources look a bit different from the Boost or SkillPoint resources, but the idea behind it and its effects are pretty much the same. The colors used for each different attack are the same as the ones used for its projectiles and the identifying name used is the one that we called abbreviation in the attacks table. Here's what they look like:
Don't forget to create InfoText objects whenever a new attack is gathered by the player!
In Projectile:draw() you have pushRotate(self.x, self.y, Vector(self.collider:getLinearVelocity()):angle()), but the function name in hump seems to be angleTo.
(Again, great work and thanks for all this information! It's very helpful and motivating.)
@harrisi My version of hump.vector has a modification to it that adds the angle function, which looks like this:
functionvector:angle()
returnatan2(self.y, self.x)
end
It turns out that if you don't pass any arguments to angleTo it also does this so it works out, but I didn't notice it before so I added this new function. Sorry for the confusion.
Introduction
In this tutorial we'll focus on getting more basics of gameplay down on the Player side of things. First we'll add the most fundamental stats: ammo, boost, HP and skill points. These stats will be used throughout the entire game and they're the main resources the player will use to do everything he can do. After that we'll focus on the creation of Resource objects, which are objects that the player can gather that contain the stats just mentioned. And finally after that we'll add the attack system as well as a few different attacks to the Player.
Draw Order
Before we go over to the main parts of this article, setting the game's draw order is something important that I forgot to mention in the previous article so we'll go over it now.
The draw order decides which objects will be drawn on top and which will be drawn behind which. For instance, right now we have a bunch of effects that are drawn when something happens. If the effects are drawn behind other objects like the Player then they either won't be visible or they will look wrong. Because of this we need to make sure that they are always drawn on top of everything and for that we need to define some sort of drawing order between objects.
The way we'll set this up is somewhat straight forward. In the
GameObject
class we'll define adepth
attribute which will start at 50 for all entities. Then, in the definition of each class' constructors, we will be able to define thedepth
attribute ourselves for that class of objects if we want to. The idea is that objects with a higher depth will be drawn in front, while objects with a lower depth will be drawn behind. So, for instance, if we want to make sure that all effects are drawn in front of everything else, we can just set theirdepth
attribute to something like 75.Now, the way that this works internally is that every frame we'll be sorting the
game_objects
list according to thedepth
attribute of each object:Here, before drawing we simply use
table.sort
to sort the entities based on theirdepth
attribute. Entities that have lower depth will be sorted to the front of the table and so will be drawn first (behind everything), and entities that have a higher depth will be sorted to the back of the table and so they will be drawn last (in front of everything). If you try setting different depth values to different types of objects you should see that this works out well.One small problem that this approach has though is that some objects will have the same depth, and when this happens, depending on how the
game_objects
table is being sorted over time, flickering can occur. Flickering occurs because if objects have the same depth, in one frame one object might be sorted to be in front of another, but in another frame it might be sorted to be behind another. It's unlikely to happen but it can happen and we should take precautions against that.One way to solve it is to define another sorting parameter in case objects have the same depth. In this case the other parameter I chose was the object's time of creation:
So if the depths are equal then objects that were created earlier will be drawn earlier, and objects that were created later will be drawn later. This is a reasonable solution and if you test it out you'll see that it also works!
Draw Order Exercises
93. Order objects so that ones with higher depth are drawn behind and ones with lower depth are drawn in front of others. In case the objects have the same depth, they should be ordered by their creation time. Objects that were created earlier should be drawn last, and objects that were created later should be drawn first.
94. In a top-downish 2.5D game like in the gif below, one of the things you have to do to make sure that the entities are drawn in the appropriate order is sort them by their
y
position. Entities that have a higher y position (meaning that they are closer to the bottom of the screen) should be drawn last, while entities that have a lower y position should be drawn first. What would the sorting function look like in that case?Basic Stats
Now we'll start with stat building. The first stat we'll focus on is the boost one. The way it works now is that whenever the player presses up or down the ship will take a different speed based on the key pressed. The way it should work is that on top of this basic functionality, it should also be a resource that depletes with use and regenerates over time when not being used. The specific numbers and rules that will be used are these:
These sound a little complicated but they're not. The first three are just number specifications, and the last two are to prevent boosts from never ending. When the resource reaches 0 it will regenerate to 1 pretty consistently and this can lead to a scenario where you can essentially use the boost forever, so a cooldown has to be added to prevent this from happening.
Now to add this as code:
So with this we take care of rules 1 and 3. We start
boost
withmax_boost
, which is 100, and then we add 10 per second toboost
while making sure that it doesn't go overmax_boost
. We can easily also get rule 2 done by simply decreasing 50 boost per second whenever the player is boosting:Part of this code was already here from before, so the only lines we really added were the
self.boost -= 50*dt
ones. Now to check rule 4 we need to make sure that wheneverboost
reaches 0 a 2 second cooldown is started before the player can boost again. This is a bit more complicated because if involves more moving parts, but it looks like this:At first we'll introduce 3 variables.
can_boost
will be used to tell when a boost can happen. By default it's set to true because the player should be able to boost at the start. It will be set to false onceboost
reaches 0 and then it will be set to true againboost_cooldown
seconds after. Theboost_timer
variable will take care of tracking how much time it has been sinceboost
reached 0, and if this variable goes aboveboost_cooldown
then we will setcan_boost
to true.This looks complicated but it just follows from what we wanted to achieve. Instead of just checking to see if a key is being pressed with
input:down
, we additionally also make sure thatboost
is above 1 (rule 5) and thatcan_boost
is true (rule 5). Wheneverboost
reaches 0 we setboosting
andcan_boost
to false, and then we resetboost_timer
to 0. Sinceboost_timer
is being addeddt
every frame, after 2 seconds it will setcan_boost
to true and we'll be able to boost again (rule 4).The code above is also the way the boost mechanism should look like now in its complete state. One thing to note is that you might think that this looks ugly or unorganized or any number of combination of bad things. But this is just what a lot of code that takes care of certain aspects of gameplay looks like. It's multiple rules that are being followed and they sort of have to be followed all in the same place. It's important to get used to code like this, in my opinion.
In any case, out of the basic stats, boost was the only one that had some more involved logic to it. There are two more important stats: ammo and HP, but both are way simpler. Ammo will just get depleted whenever the player attacks and regained whenever a resource is collected in gameplay, and HP will get depleted whenever the player is hit and also regained whenever a resource is collected in gameplay. For now, we can just add them as basic stats like we did for the boost:
Resources
What I call resources are small objects that affect one of the main basic stats that we just went over. The game will have a total of 5 of these types of objects and they'll work like this:
The Director is a piece of code that handles the spawning of enemies as well as resources. I called it that because other games (like L4D) call it that and it just seems to fit. Because we're not going to work on that piece of code yet, for now we'll bind the creation of each resource to a key just to test that it works.
Ammo Resource
So let's get started on the ammo. The final result should be like this:
The little green rectangles are the ammo resource. When the player hits one of them with its body the resource is destroyed and the player gets 5 ammo. We can create a new class named
Ammo
and start with some definitions:Ammo resources will be physics rectangles that start with some random and small velocity and rotation, set initially by
setLinearVelocity
andapplyAngularImpulse
. This object is also drawn using thedraft
library. This is a small library that lets you draw all sorts of shapes more easily than if you had to do it yourself. For this case you can just draw the resource as a rectangle if you want, but I'll choose to go with this. I'll also assume that you can already install the library yourself and read the documentation to figure out what it can and can't do. Additionally, we're also taking into account the rotation of the physics object by using the result fromgetAngle
inpushRotate
.To test this all out, we can bind the creation of one of these objects to a key like this:
And if you run the game now and press p a bunch of times you should see these objects spawning and moving/rotating around.
The next thing we should do is create the collision interaction between player and resource. This interaction will hold true for all resources and will be mostly the same always. The first thing we want to do is make sure that we can capture an event when the player physics object collides with the ammo physics object. The easiest way to do this is through the use of
collision classes
. To start with we can define 3 collision classes for objects that are already exist: the Player, the projectiles and the resources.And then in each one of those files (Player, Projectile and Ammo) we can set the collider's collision class using
setCollisionClass
(repeat the code below for the other files):By itself this doesn't change anything, but it gives us a base to work with and to capture collision events between physics objects. For instance, if we change the
Collectable
collision class to ignore thePlayer
like this:Then if you run the game again you'll notice that the player is now physically ignoring the ammo resource objects. This isn't what we want to do in the end but it serves as a nice example of what we can do with collision classes. The rules we actually want these 3 collision classes to follow are the following:
Rules 1, 2 and 3 can be satisfied by making small changes to the
addCollisionClass
calls:It's important to note that the order of the declaration of collision classes matters. For instance, if we swapped the order of the Projectile and Collectable declarations a bug would happen because the Collectable collision class makes reference to the Projectile collision class, but since the Projectile collision class isn't yet defined it bugs out.
The fourth rule can be satisfied by using the
enter
call:And if you run this you'll see that 1 will be printed to the console every time the player collides with an ammo resource.
Another piece of behavior we need to add to the
Ammo
class is that it needs to move towards the player slightly. An easy way to do this is to add the Seek Behavior to it. My version of the seek behavior was based on the book Programming Game AI by Example which has a very nice section on steering behaviors in general. I'm not going to explain it in detail because I honestly don't remember how it works, so I'll just assume if you're curious about it you'll figure it out :DSo here the ammo resource will head towards the
target
if it exists, otherwise it will just move towards the direction it was initially set to move towards.target
contains a reference to the player, which was set inStage
like this:Finally, the only thing left to do is what happens when an ammo resource is collected. From the gif above you can see that a little effect plays (like the one for when a projectile dies), along with some particles, and then the player also gets +5 ammo.
Let's start with the effect. This effect follows the exact same logic as the
ProjectileDeathEffect
object, in that there's a little white flash and then the actual color of the effect comes on. The only difference here is that instead of drawing a square we will be drawing a rhombus, which is the same shape that we used to draw the ammo resource itself. I'll call this new objectAmmoEffect
and I won't really go over it in detail since it's the same asProjectileDeathEffect
. The way we call it though is like this:Here we are creating one
AmmoEffect
object and then between 4 and 8ExplodeParticle
objects, which we already used before for the Player's death effect. Thedie
function on an Ammo object will get called whenever it collides with the Player:Here first we use
getEnterCollisionData
to get the collision data generated by the last enter collision event for the specified tag. After this we usegetObject
to get access to the object attached to the collider involved in this collision event, which could be any object of the Collectable collision class. In this case we only have the Ammo object to worry about, but if we had others here's where we'd place the code to separate between them. And that's what we do, to really check that the object we get fromgetObject
is the of theAmmo
class we use classic'sis
function. If it is really is an object of the Ammo class then we call itsdie
function. All that should look like this:One final thing we forgot to do is actually add +5 ammo to the player whenever an ammo resource is gathered. For this we'll define an
addAmmo
function which simply adds a certain amount to theammo
variable and checks that it doesn't go overmax_ammo
:And then we can just call this after
object:die()
in the collision code we just added.Boost Resource
Now for the boost. The final result should look like this:
As you can see, the idea is almost the same as the ammo resource, except that the boost resource's movement is a little different, it looks a little different and the visual effect that happens when one is gathered is different as well.
So let's start with the basics. For every resource other than the ammo one, they'll be spawned either on the left or right of the screen and they'll move really slowly in a straight line to the other side. The same applies to the enemies. This gives the player enough time to move towards the resource to pick it up if he wants to.
The basic starting setup of the
Boost
class is about the same as theAmmo
one and looks like this:There are a few differences though. The 3 first lines in the constructor are picking the initial position of this object. The
table.random
function is defined inutils.lua
as follows:And as you can see what it does is just pick a random element from a table. In this case, we're picking either -1 or 1 to signify the direction in which the object will be spawned. If -1 is picked then the object will be spawned to the left of the screen, and if 1 is picked then it will be spawned to the right. More precisely, the exact positions chosen for its chosen position will be either
-48
orgw+48
, so slightly offscreen but close enough to the edge.After this we define the object mostly like the Ammo one, with a few differences again only when it comes to its velocity. If this object was spawned to the right then we want it to move left, and if it was spawned to the left then we want it to move right. So its velocity is set to a random value between 20 and 40, but also multiplied by
-direction
, since if the object was to the right,direction
was 1, and if we want to move it to the left then the velocity has to be negative (and the opposite for the other side). The object's velocity is always set to thev
attribute on the x component and set to 0 on the y component. We want the object to remain moving in a horizontal line no matter what so setting its y velocity to 0 will achieve that.The final main difference is in the way its drawn:
Here instead of just drawing a single rhombus we draw one inner and one outer to be used as sort of an outline. You can obviously draw all these objects in whatever way you want, but this is what I personally decided to do.
Now for the effects. There are two effects being used here: one that is similar to
AmmoEffect
(although a bit more involved) and the one that is used for the+BOOST
text. We'll start with the one that's similar to AmmoEffect and call itBoostEffect
.This effect has two parts to it, the center with its white flash and the blinking effect before it disappears. The center works in the same way as the
AmmoEffect
, the only difference is that the timing of each phase is different, from 0.1 to 0.2 in the first phase and from 0.15 to 0.35 in the second:The other part of the effect is the blinking before it dies. This can be achieved by creating a variable named
visible
, which when set to true will draw the effect and when set to false will not draw the effect. By changing this variable from false to true really fast we can achieve the desired effect:Here we use the
every
call to switch between visible and not visible six times, each with a 0.05 seconds delay in between, and after that's done we set it to be visible in the end. The effect will die after 0.55 seconds anyway (since we setdead
to true after 0.55 seconds when setting the current color) so setting it to be visible in the end isn't super important to do. In any case, then we can draw it like this:We're simply drawing both the inner and outer rhombus at different sizes. The exact numbers (1.34, 2) were reached through pretty much trial and error based on what looked best.
The final thing we need to do for this effect is to make the outer rhombus outline expand over the life of the object. We can do that like this:
And then update the draw function like this:
With this, the
sx
andsy
variables will grow to 2 over 0.35 seconds, which means that the outline rhombus will also grow to double its previous value over those 0.35 seconds. In the end the result looks like this (I'm assuming you already linked this object'sdie
function to the collision event with the Player, like we did for the ammo resource):Now for the other part of the effect, the crazy looking text. This text effect will be used throughout the game pretty much everywhere so let's make sure we get it right. Here's what the effect looks like again:
First let's break this effect down into its multiple parts. The first thing to notice is that it's simply a string being drawn to the screen initially, but then near its end it starts blinking like the
BoostEffect
object. That blinking part turns out to use exactly the same logic as the BoostEffect so we have that covered already.What also happens though is that the letters of the string start changing randomly to other letters, and each character's background also changes colors randomly. This suggests that this effect is processing characters individually internally rather than operating on a single string, which probably means we'll have to do something like hold all characters in a
characters
table, operate on this table, and then draw each character on that table with all its modifications and effects to the screen.So to start with this in mind we can define the basics of the
InfoText
class. The way we're gonna call it is like this:And so the
text
attribute will contain our string. Then the basic definition of the class can look like this:With this we simply define that this object will have depth of 80 (higher than all other objects, so will be drawn in front of everything) and then we separate the initial string into characters in a table. We use an
utf8
library to do this. In general it's a good idea to manipulate strings with a library that supports all sorts of characters, and for this object it's really important that we do this as we'll see soon.In any case, the drawing of these characters should also be done on an individual basis because as we figured out earlier, each character has its own background that can change randomly, so it's probably the case that we'll want to draw each character individually.
The logic used to draw characters individually is basically to go over the table of characters and draw each character at the x position that is the sum of all characters before it. So, for instance, drawing the first
O
in+BOOST
means drawing it at something likeinitial_x_position + widthOf('+B')
. The problem with getting the width of+B
in this case is that it depends on the font being used, since we'll use theFont:getWidth
function, and right now we haven't set any font. We can solve that easily though!For this effect the font used will be m5x7 by Daniel Linssen. We can put this font in the folder
resources/fonts
and then load it. The code needed for loading it will be left as an exercise, since it's somewhat similar to the code used to load class definitions in theobjects
folder (exercise 14). By the end of this loading process we should have a global table calledfonts
that has all loaded fonts in the formatfontname_fontsize
. In this instance we'll usem5x7_16
:And this is what the drawing code looks like:
First we use
love.graphics.setFont
to set the font we want to use for the next drawing operations. After this we go over each character and then draw it. But first we need to figure out its x position, which is the sum of the width of the characters before it. The inner loop that accumulates on the variablewidth
is doing just that. It goes from 1 (the start of the string) to i-1 (the character before the current one) and adds the width of each character to a total finalwidth
that is the sum of all of them. After that we uselove.graphics.print
to draw each individual character at its appropriate position. We also offset each character up by half the height of the font (so that the characters are centered around the y position we defined).If we test all this out now it looks like this:
Which looks about right!
Now we can move on to making the text blink a little before disappearing. This uses the same logic as the BoostEffect object so we can just kinda copy it:
And if you run this you should see that the text stays normal for a while, starts blinking and then disappears.
Now the hard part, which is making each character change randomly as well as its foreground and background colors. This changing starts at about the same that the character starts blinking, so we'll place this piece of code inside the 0.7 seconds
after
call we just defined above. The way we'll do this is that every 0.035 seconds, we'll run a procedure that will have a chance to change a character to another random character. That looks like this:And so each 0.035 seconds, for each character there's a 5% probability that it will be changed to something else. We can complete this by adding a variable named
random_characters
which is a string that contains all characters a character might change to, and then when a character change is necessary we pick one at random from this string:And if you run that now it should look like this:
We can use the same logic we used here to change the character's colors as well as their background colors. For that we'll define two tables,
background_colors
andforeground_colors
. Each table will be the same size of thecharacters
table and will simply hold the background and foreground colors for each character. If a certain character doesn't have any colors set in these tables then it just defaults to the default color for the foreground (boost_color
) and to a transparent background.For the background colors we simply draw a rectangle at the appropriate position and with the size of the current character if
background_colors[i]
(the background color for the current character) is defined. As for the foreground color, we simply set the color to draw the current character with usingsetColor
. Ifforeground_colors[i]
isn't defined then it defauls toself.color
, which for this object should always beboost_color
since that's what we're passing in when we call it from the Boost object. But ifself.color
isn't defined either then it defaults to white (default_color
). By itself this piece of code won't really do anything, because we haven't defined any of the values inside thebackground_colors
or theforeground_colors
tables.To do that we can use the same logic we used to change characters randomly:
The code that changes colors around will have to pick between a list of colors. We defined a group of 6 colors globally and we could just put those all into a list and then use
table.random
to pick one at random. What we'll do is that but also define 6 more colors on top of it which will be the negatives of the 6 original ones. So say you have232, 48, 192
as the original color, we can define its negative as255-232, 255-48, 255-192
.So here we define two tables that contain the appropriate values for each color and then we use the
append
function to join them together. So now we can say something liketable.random(self.all_colors)
to get a random color out of the 10 defined in those tables, which means that we can do this:And if we run the game now it should look like this:
And that's it. We'll improve it even more later on (and on the exercises) but it's enough for now. Lastly, the final thing we should do is make sure that whenever we collect a boost resource we actually add +25 boost to the player. This works exactly the same way as it did for the ammo resource so I'm going to skip it.
Resources Exercises
95. Make it so that the Projectile collision class will ignore the Player collision class.
96. Change the
addAmmo
function so that it supports the addition of negative values and doesn't let theammo
attribute go below 0. Do the same for theaddBoost
andaddHP
functions (adding the HP resource is an exercise defined below).97. Following the previous exercise, is it better to handle positive and negative values on the same function or to separate between
addResource
andremoveResource
functions instead?98. In the
InfoText
object, change the probability of a character being changed to 20%, the probability of a foreground color being changed to 5%, and the probability of a background color being changed to 30%.99. Define the
default_colors
,negative_colors
andall_colors
tables globally instead of locally inInfoText
.100. Randomize the position of the
InfoText
object so that it is spawned between-self.w
andself.w
in its x component and between-self.h
andself.h
in its y component. Thew
andh
attributes refer to the Boost object that is spawning the InfoText.101. Assume the following function:
Which returns all game objects inside an Area that pass a filter function. And then assume that it's called like this inside InfoText's constructor:
Which returns all existing and alive InfoText objects that are not this one. Now make it so that this InfoText object doesn't visually collide with any other InfoText object, meaning, it doesn't occupy the same space on the screen as another such that its text would become unreadable. You may do this in whatever you think is best as long as it achieves the goal.
102. (CONTENT) Add the HP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +25 to HP instead. The resource and effects look like this:
103. (CONTENT) Add the SP resource with all of its functionality and visual effects. It uses the exact same logic as the Boost resource, but instead adds +1 to SP instead. The SP resource should also be defined as a global variable for now instead of an internal one to the Player object. The resource and effects look like this:
Attacks
Alright, so now for attacks. Before anything else the first thing we're gonna do is change the way projectiles are drawn. Right now they're being drawn as circles but we want them as lines. This can be achieved with something like this:
In the
pushRotate
function we use the projectile's velocity so that we can rotate it towards the angle its moving at. Then inside we uselove.graphics.setLineWidth
and set it to a value somewhat proportional to thes
attribute but slightly smaller. This means that projectiles with biggers
will be thicker in general. Then we draw the projectile usinglove.graphics.line
and importantly, we draw one line from-2*self.s
to the center and then another from the center to2*self.s
. We do this because each attack will have different colors, and what we'll do is change the color of one those lines but not change the color of another. So, for instance, if we do this:It will look like this:
In this way we can make each attack have its own color which helps with letting the player better understand what's going on on the screen.
The game will end up having 16 attacks but we'll cover only a few of them now. The way the attack system will work is very simple and these are the rules:
The first we're gonna do is define a table that will hold information on each attack, such as their cooldown, ammo consumption and color. We'll define this in
globals.lua
and for now it will look like this:The normal attack that we already have defined is called
Neutral
and it simply has the stats that the attack we had in the game had so far. Now what we can do is define a function calledsetAttack
which will change from one attack to another and use this global table of attacks:And then we can call it like this:
Here we simply change an attribute called
attack
which will contain the name of the current attack. This attribute will be used in theshoot
function to check which attack is currently active and how we should proceed with projectile creation.We also change an attribute named
shoot_cooldown
. This is an attribute that we haven't created yet, but similar to how theboost_timer
andboost_cooldown
attributes work, they will be used to control how often something can happen, in this case how often an attack happen. We will remove this line:And instead do the timing of attacks manually like this:
Finally, at the end of the
setAttack
function we also regenerate the ammo resource. With this we take care of rule 4. The next thing we can do is change theshoot
function a little to start taking into account the fact that different attacks exist:Before launching the projectile we check the current attack with the
if self.attack == 'Neutral'
conditional. This function will grow based on a big conditional chain like this where we'll be checking for all 16 attacks that we add.So let's get started with adding one actual attack to see what it's like. The attack we'll add will be called
Double
and it looks like this:And as you can see it shoots 2 projectiles at an angle instead of one. To get started with this first we'll add the attack's description to the global attacks table. This attack will have a cooldown of 0.32, cost 2 ammo, and its color will be
ammo_color
(these values were reached through trial and error):Now we can add it to the
shoot
function as well:Here we create two projectiles instead of one, each pointing with an angle offset of math.pi/12 radians, or 15 degrees. We also make it so that the projectile receives the
attack
attribute as the name of the attack. For each projectile type we'll do this as it will help us identify which attack this projectile belongs to. That is helpful for setting its appropriate color as well as changing its behavior when necessary. The Projectile object now looks like this:In the constructor we set
color
to the color defined in the globalattacks
table for this attack. And then in the draw function we draw one part of the line with its color being thecolor
attribute, and another beingdefault_color
. For most projectile types this drawing setup will hold.The last thing we forgot to do is to make it so that this attack obeys rule 1, meaning that we forgot to add code to make it consume the amount of ammo it should consume. This is a pretty simple fix:
With this rule 1 (for the Double attack) will be followed. We can also add the code that will make rule 2 come true, which is that when
ammo
hits 0, we change the current attack to theNeutral
one:This must come at the end of the
shoot
function since we don't want the player to be able to shoot one extra time after his ammo resource hits 0.If you do all this and try running it it should look like this:
Attacks Exercises
104. (CONTENT) Implement the
Triple
attack. Its definition on the attacks table looks like this:And the attack itself looks like this:
The angles on the projectile are exactly the same as
Double
, except that there's one extra projectile also being spawned along the middle (at the same angle that theNeutral
projectile is spawned). Create this attack following the same steps that were used for the Double attack.105. (CONTENT) Implement the
Rapid
attack. Its definition on the attacks table looks like this:And the attack itself looks like this:
106. (CONTENT) Implement the
Spread
attack. Its definition on the attacks table looks like this:And the attack itself looks like this:
The angles used for the shots are a random value between -math.pi/8 and +math.pi/8. This attack's projectile color also works a bit differently. Instead of having one color only, the color changes randomly to one inside the
all_colors
list every frame (or every other frame depending on what you think is best).107. (CONTENT) Implement the
Back
attack. Its definition on the attacks table looks like this:And the attack itself looks like this:
108. (CONTENT) Implement the
Side
attack. Its definition on the attacks table looks like this:And the attack itself looks like this:
109. (CONTENT) Implement the
Attack
resource. Like theBoost
andSkillPoint
resources, the Attack resource is spawned from either the left or right of the screen at a random y position, and then moves inward very slowly. When the player comes into contact with an Attack resource, his attack is changed to the attack that the resource contains using thesetAttack
function.Attack resources look a bit different from the Boost or SkillPoint resources, but the idea behind it and its effects are pretty much the same. The colors used for each different attack are the same as the ones used for its projectiles and the identifying name used is the one that we called
abbreviation
in theattacks
table. Here's what they look like:Don't forget to create InfoText objects whenever a new attack is gathered by the player!
BYTEPATH on Steam
Tutorial files
The text was updated successfully, but these errors were encountered: