Open a327ex opened 6 years ago
Hello,
Just a small thing: When you introduce the PreAttackEffect, The first example has some code in Player:new(...) that really belongs ot Shooter:new()! This puzzled me for a while, so thought i should point it out =).
Thank you for this amazing Tutorial! (And also, the game is really fun too^^)
Introduction
In this article we'll go over the creation of a few enemies as well as the EnemyProjectile class, which is a projectile that some enemies can shoot to hurt the player. This article will be a bit shorter than others since we won't focus on creating all enemies now, only the basic behavior that will be shared among most of them.
Enemies
Enemies in this game will work in a similar way to how the resources we created in the last article worked, in the sense that they will be spawned at either left or right of the screen at a random y position and then they will slowly move inward. The code used to make that work will be exactly the same as the one used in each resource we implemented.
We'll get started with the first enemy which is called
Rock
. It looks like this:The constructor code for this object will be very similar to the
Boost
one, but with a few small differences:Here instead of the object using a RectangleCollider, it will use a PolygonCollider. We create the vertices of this polygon with the function
createIrregularPolygon
which will be defined inutils.lua
. This function should return a list of vertices that make up an irregular polygon. What I mean by an irregular polygon is one that is kinda like a circle but where each vertex might be a bit closer or further away from the center, and where the angles between each vertex may be a little random as well.To start with the definition of that function we can say that it will receive two arguments:
size
andpoint_amount
. The first will refer to the radius of the circle and the second will refer to the number of points that will compose the polygon:Here we also say that if
point_amount
isn't defined it will default to 8.The next thing we can do is start defining all the points. This can be done by going from 1 to
point_amount
and in each iteration defining the next vertex based on an angle interval. So, for instance, to define the position of the second point we can say that its angle will be somewhere around2*angle_interval
, whereangle_interval
is the value2*math.pi/point_amount
. So in this case it would be around 90 degrees. This probably makes more sense in code, so:And so here we define
angle_interval
like previously explained, but we also definedistance
as being around the radius of the circle, but with a random offset between-size/4
and+size/4
. This means that each vertex won't be exactly on the edge of the circle but somewhere around it. We also randomize the angle interval a bit to create the same effect. Finally, we add both x and y components to a list of points which is then returned. Note that the polygon is created in local space (assuming that the center is 0, 0), which means that we have to then usesetPosition
to place the object in its proper place.Another difference in the constructor of this object is the use of the
Enemy
collision class. Like all other collision classes, this one should be defined before it can be used:In general, new collision classes should be added for object types that will have different collision behaviors between each other. For instance, enemies will physically ignore the player but will not physically ignore projectiles. Because no other object type in the game follows this behavior, it means we need to add a new collision class to do it. If the Projectile collision class only ignored the player instead of also ignoring other projectiles, then enemies would be able to have their collision class be Projectile as well.
The last thing about the Rock object is how its drawn. Since it's just a polygon we can simply draw its points using
love.graphics.polygon
:We get its points first by using
PolygonShape:getPoints
. These points are returned in local coordinates, but we want global ones, so we have to useBody:getWorldPoints
to convert from local to global coordinates. Once that's done we can just draw the polygon and it will behave like expected. Note that because we're getting points from the collider directly and the collider is a polygon that is rotating around, we don't need to usepushRotate
to rotate the object like we did for the Boost object since the points we're getting are already accounting for the objects rotation.If you do all this it should look like this:
Enemies Exercises
110. Perform the following tasks:
hp
to the Rock class that initially is set to 100hit
to the Rock class. This function should do the following:damage
argument, and in case it isn't then it should default to 100damage
will be subtracted fromhp
and ifhp
hits 0 or lower then the Rock object will diehp
doesn't hit 0 or lower then an attribute namedhit_flash
will be set to true and then set to false 0.2 seconds later. In the draw function of the Rock object, wheneverhit_flash
is set to true the color of the object will be set todefault_color
instead ofhp_color
.111. Create a new class named
EnemyDeathEffect
. This effect gets created whenever an enemy dies and it behaves exactly like theProjectileDeathEffect
object, except that it is bigger according to the size of the Rock object. This effect should be created whenever the Rock object'shp
attribute hits 0 or lower.112. Implement the collision event between an object of the Projectile collision class and an object of the Enemy collision class. In this case, implement it in the Projectile's class update function. Whenever the projectile hits an object of the Enemy class, it should call the enemy's
hit
function with the amount of damage this projectile deals (by default, projectiles will have an attribute nameddamage
that is initially set to 100). Whenever a hit happens the projectile should also call its owndie
function.113. Add a function named
hit
to the Player class. This function should do the following things:damage
argument, and in case it isn't defined it should default to 10invincible
attribute is trueExplodeParticle
objects should be spawnedaddHP
(orremoveHP
function if you decided to add this one) should take thedamage
attribute and use it to remove HP from the Player. Inside theaddHP
(orremoveHP
) function there should be a way to deal with the case wherehp
hits or goes below 0 and the player dies.Additionally, the following conditional operations should hold:
invincible
attribute should be set to true and 2 seconds later it should be set to false. On top of that, the camera should shake with intensity 6 for 0.2 seconds, the screen should flash for 3 frames, and the game should be slowed to 0.25 for 0.5 seconds. Finally, aninvisible
attribute should alternate between true and false every 0.04 for the duration thatinvincible
is set to true, and additionally the Player's draw function shouldn't draw anything wheneverinvisible
is set to true.This
hit
function should be called whenever the Player collides with an Enemy. The player should be hit for 30 damage on enemy collision.After finishing these 4 exercises you should have completed everything needed for the interactions between Player, Projectile and Rock enemy to work like they should in the game. These interactions will hold true and be similar for other enemies as well. And it all should look like this:
EnemyProjectile
So, now we can focus on another part of making enemies which is creating enemies that can shoot projectiles. A few enemies will be able to do that and so we need to create an object, like the Projectile one, but that is used by enemies instead. For this we'll create the
EnemyProjectile
object.This object can be created at first by just copypasting the code for the
Projectile
one and changing it slightly. Both these objects will share a lot of the same code. We could somehow abstract them out into a general projectile-like object that has the common behavior, but that's really not necessary since these are the only two types of projectiles the game will have. After the copypasting is done the things we have to change are these:The collision class of an EnemyProjectile should also be EnemyProjectile. We want EnemyProjectiles objects to ignore other EnemyProjectiles, Projectiles and the Player. So we must add the collision class such that it fits that purpose:
The other main thing we have to change is the damage. A normal projectile shot by the player deals 100 damage, but a projectile shot by an enemy should deal 10 damage:
Another thing is that we want projectiles shot by enemies to collide with the Player but not with other enemies. So we can take the collision code that the Projectile object used and just turn it around on the Player instead:
Finally, we want this object to look completely red instead of half-red and half-white, so that the player can tell projectiles shot from an enemy to projectiles shot by himself:
With all these small changes we have successfully create the EnemyProjectile object. Now we need to create an enemy that will use it!
Shooter
This is what the Shooter enemy looks like:
As you can see, there's a little effect and then after a projectile is fired. The projectile looks just like the player one, except it's all red.
We can start making this enemy by copypasting the code from the Rock object. This enemy (and all enemies) will share the same property that they code from either left or right of then screen and then move inwards slowly, and since the Rock object already has that code taken care of we can start from there. Once that's done, we have to change a few things:
The width, height and vertices of the Shooter enemy are different from the Rock. With the rock we just created an irregular polygon, but here we need the enemy to have a well defined and pointy shape so that the player can instinctively tell where it will come from. The setup here for the vertices is similar to how we did it for designing ships, so if you want you can change the way this enemy looks and make it look cooler.
The other thing we need to change is that unlike the rock, setting the object's velocity is not enough. We must also set its angle so that physics collider points in the right direction. To do this we first need to disable its fixed rotation (otherwise setting the angle won't work), change the angle, and then set its fixed rotation back to true. The rotation is set to fixed again because we don't want the collider to spin around if something hits it, we want it to remain pointing to the direction it's moving towards.
The line
direction == 1 and math.pi or 0
is basically how you do a ternary operator in Lua. In other languages this would look like(direction == 1) ? math.pi : 0
. The exercises back in article 2 and 4 I think went over this in detail, but essentially what will happen is that ifdirection
is 1 (coming from the right and pointing to the left) then the first conditional will parse to true, which leaves us withtrue and math.pi or 0
. Because of precedence betweenand
andor
,true and math.pi
will go first, which leaves us withmath.pi or 0
, which will return math.pi, sinceor
returns the first element whenever both are true. On the other hand, ifdirection
is -1, then the first conditional will parse to false and we'll havefalse and math.pi or 0
, which will parse tofalse or 0
, which will parse to 0, sinceor
returns the second element whenever the first is false.With all this, we can spawn Shooter objects and they should look like this:
Now we need to create the pre-attack effect. Usually in most games whenever an enemy is about to attack something happens that tells the player that enemy is about to attack. Most of the time it's an animation, but it could also be an effect. In our case we'll use a simple "charging up" effect, where a bunch of particles are continually sucked into the point where the projectile will come from until the release happens.
At a high level this is how we'll do it:
So this means that with an interval of between 3 and 5 seconds each Shooter enemy will shoot a new projectile. This will happen after the
PreAttackEffect
effect is up for 1 second.The basic way effects like these that have to do with particles work is that, like with the trails, some type of particle will be spawned every frame or every other frame and that will make the effect work. In this case, we will spawn particles called
TargetParticle
. These particles will move towards a point we define as the target and then die after a duration or when they reach the point.Here each particle will be tweened towards
target_x, target_y
over ad
duration (or a random value between 0.1 and 0.3 seconds), and when that position is reached then the particle will die. The particle is also drawn as a rhombus (like one of the effects we made earlier), but it could be drawn as a circle or rectangle since it's small enough and gets smaller over the tween duration.The way we create these objects in
PreAttackEffect
looks like this:So here we spawn one particle every 0.02 seconds (almost every frame) in a random location around its position, and then we set the
target_x, target_y
attributes to the position of the effect itself (which will be at the tip of the ship).In
Shooter
, we create PreAttackEffect like this:The initial position we set should be at the tip of the Shooter object, and so we can use the general math.cos and math.sin pattern we've been using so far to achieve that and account for both possible angles (0 and math.pi). We also pass a
duration
attribute, which controls how long the PreAttackEffect object will stay alive for. Back there we can do this:The reason we don't use
duration
by itself here is because this object is what I call in my head a controller object. For instance, it doesn't have anything in its draw function, so we never actually see it in the game. What we see are theTargetParticle
objects that it commands to spawn. Those objects have a random duration of between 0.1 and 0.3 seconds each, which means if we want the last particles to end right as the projectile is being shot, then this object has to die between 0.1 and 0.3 seconds earlier than its 1 second duration. As a general case thing I decided to make this 0.75 (duration - duration/4), but it could be another number that is closer to 0.9 instead.In any case, if you run everything now it should look like this:
And this works well enough. But if you pay attention you'll notice that the target position of the particles (the position of the PreAttackEffect object) is staying still instead of following the Shooter. We can fix this in the same way we fixed the ShootEffect object for the player. We already have the
shooter
attribute pointing to the Shooter object that created the PreAttackEffect object, so we can just update PreAttackEffect's position based on the position of thisshooter
parent object:And so here every frame we're updating this objects position to be at the tip of the Shooter object that created it. If you run this it would look like this:
One important thing about the update code about is the
not self.shooter.dead
part. One thing that can happen when we reference objects within each other like this is that one object dies while another still holds a reference to it. For instance, the PreAttackEffect object lasts 0.75 seconds, but between its creation and its demise, the Shooter object that created it can be killed by the player, and if that happens problems can occur.In this case the problem is that we have to access the Shooter's
collider
attribute, which gets destroyed whenever the Shooter object dies. And if that object is destroyed we can't really do anything with it because it doesn't exist anymore, so when we try togetAngle
it that will crash our game. We could work out a general system that solves this problem but I don't really think that's necessary. For now we should just be careful whenever we reference objects like this to make sure that we don't access objects that might be dead.Now for the final part, which is the one where we create the EnemyProjectile object. For now we'll handle this relatively simply by just spawning it like we would spawn any other object, but with some specific attributes:
Here we create the projectile at the same position that we created the PreAttackEffect, and then we set its velocity to a random value between 80 and 100, and then its size to be slightly larger than the default value. The most important part is that we set its angle (
r
attribute) to point towards the player. In general, whenever you want something to get the angle of something fromsource
totarget
, you should do:And that's what we're doing here. After the object is spawned it will point itself towards the player and move there. It should look like this:
If you compare this to the initial gif on this section this looks a bit different. The projectiles there have a period where they slowly turn towards the player rather than coming out directly towards him. This uses the same piece of code as the homing passive that we will add eventually, so I'm gonna leave that for later.
One thing that will happen for the EnemyProjectile object is that eventually it will be filled with lots of functionality so that it can serve the purposes of lots of different enemies. All this functionality, however, will first be implemented in the Projectile object because it will serve as a passive to the player. So, for instance, there's a passive that makes projectiles circle around the player. Once we implement that, we can copypaste that code to the EnemyProjectile object and then implement an enemy that makes use of that idea and has projectiles that circle it instead. A number of enemies will be created in this way so those will be left as an exercise for when we implement passives for the player.
For now, we'll stay with those two enemies (Rock and Shooter) and with the EnemyProjectile object as it is and move on to other things, but we'll come back to create more enemies in the future as we add more functionality to the game.
EnemyProjectile/Shooter Exercises
114. Implement a collision event between Projectile and EnemyProjectile. In the EnemyProjectile class, make it so that whenever it hits an object of the Projectile class, both object's
die
function will be called and they will both be destroyed.115. Is the way the
direction
attribute is named confusing in the Shooter class? If so, what could it be named instead? If not, how is it not?BYTEPATH on Steam
Tutorial files