A game with only a single enemy type is doomed to be boring and lackluster. In this Pygame RPG Tutorial, we’ll be adding a new enemy type in our game. Not only will it look completely different, we will also change it’s way of attack as well from melee to ranged.
I created the below enemy, putting in a little more effort that I did with the first one. It’s nothing special, but looks good enough for our 2D game.
As I mentioned earlier, this is going to be an enemy that fires out ranged attacks at our player from a distance. The code for this enemy class is going to be pretty long, so it will take a few tutorials to fully complete it.
Creating the New Enemy
I know this isn’t very clean, and that we aren’t following the OOP model much, but some things are hard to show in text. Later on, I plan on releasing a video version of this RPG tutorial, from start to finish. It will feature some bonus content, and will be structured better (multiple header files) and will follow the OOP model (inheritance) much better.
For now just look at the concept behind the Enemy, and the unique twists and turns we will be putting on it in this tutorial and the next.
Here is the start of the New Enemy Class. It’s mostly the same as before, with two minor changes. We increased the amount of mana that can be generated from 1 – 3 to 2 – 3 and lowered the speed a bit. This is a ranged enemy, so doesn’t make sense for it to be very fast.
class Enemy2(pygame.sprite.Sprite): def __init__(self): super().__init__() self.pos = vec(0,0) self.vel = vec(0,0) self.direction = random.randint(0,1) # 0 for Right, 1 for Left self.vel.x = random.randint(2,6) / 3 # Randomized velocity of the generated enemy self.mana = random.randint(2, 3) # Randomized mana amount obtained upon
In the second half of the init function, we’ll decide which image to load into the enemy sprite. I have two images, which are identical, but one is flipped 180 degrees to point in the other direction. With our previous enemy, his look remained the same regardless of which direction, but with this we need two images.
if self.direction == 0: self.image = pygame.image.load("enemy2.png") if self.direction == 1: self.image = pygame.image.load("enemy2_L.png") self.rect = self.image.get_rect() # Sets the initial position of the enemy if self.direction == 0: self.pos.x = 0 self.pos.y = 250 if self.direction == 1: self.pos.x = 700 self.pos.y = 250
Towards the end of the init function, we place the enemy on either the left or right side of the screen, depending on his direction.
This is the move function, where the code for this enemy begins to differ greatly from the first enemy.
(If you’re wondering what the
cursor.wait code is, go back and read the pause button tutorial)
def move(self): if cursor.wait == 1: return # Causes the enemy to change directions upon reaching the end of screen if self.pos.x >= (WIDTH-20): self.direction = 1 elif self.pos.x <= 0: self.direction = 0
This second code block is where the difference lies. What I want is for the enemy to move a bit, then pause for around a second, fire at the Player and then repeat the entire process again. For now we will work on getting it to move and stop appropriately.
I have used a simple system, where every 50 frames, the enemy will stop. And then stay still for another 50 frames (in which he will attack). Then he will proceed to move forward for another 50 frames.
Every time the move function is called, we increment the
self.wait counter, and when this goes above 50, we set the
wait_status to true.
# Updates position with new values if self.wait > 50: self.wait_status = True elif int(self.wait) <= 0: self.wait_status = False if self.wait_status == True: self.wait -= 1
Now if wait status is true, then the Enemy will not move, instead it will go into the if block where the decrement
self.wait. This will continue until self.wait is at 0 or less.
This final part of the
move() is pretty simple. It’s almost identical to the first Enemy’s move function, but with additional lines for
self.wait. This is where the value of
self.wait builds up. For every pixel the enemy moves, the value of
self.wait is increment by 1.
elif self.direction == 0: self.pos.x += self.vel.x self.wait += self.vel.x elif self.direction == 1: self.pos.x -= self.vel.x self.wait += self.vel.x self.rect.topleft = self.pos # Updates rect
If he moves 10 pixels forward, it is incremented by 10 until it has reached 50, which is when the wait mode activates.
For this, we will simply copy paste the update function from the first enemy. There is only one small difference. We remove the last two lines, which would call the
player_hit() function if the Player and Enemy were colliding, but the Player was not attacking.
This was purely my choice, as I view this enemy as a ranged attacker, unable to perform close quarter hits. So the only way he can damage the player is by firing projectiles.
def update(self): # Checks for collision with the Player hits = pygame.sprite.spritecollide(self, Playergroup, False) # Checks for collision with Fireballs f_hits = pygame.sprite.spritecollide(self, Fireballs, False) # Activates upon either of the two expressions being true if hits and player.attacking == True or f_hits: self.kill() handler.dead_enemy_count += 1 if player.mana < 100: player.mana += self.mana # Release mana player.experiance += 1 # Release expeiriance rand_num = numpy.random.uniform(0, 100) item_no = 0 if rand_num >= 0 and rand_num <= 5: # 1 / 20 chance for an item (health) drop item_no = 1 elif rand_num > 5 and rand_num <= 15: item_no = 2 if item_no != 0: # Add Item to Items group item = Item(item_no) Items.add(item) # Sets the item location to the location of the killed enemy item.posx = self.pos.x item.posy = self.pos.y
And finally, we create the draw function, which we will call later on to render our Enemy object to the screen.
def render(self): # Displays the enemy on screen displaysurface.blit(self.image, self.rect)
Just like how we had an event detection code block for the first enemy type, we need to do the same for the new enemy type.
if event.type == handler.enemy_generation2: if handler.enemy_count < handler.stage_enemies[handler.stage - 1]: enemy = Enemy2() Enemies.add(enemy) handler.enemy_count += 1
We’ll add it into the same Enemies group though, so we can access both types at once while iterating over the Enemeies group.
You’ll find the relevant images for the new Enemy type available for download in the next tutorial. We’ll be working on actually improving the movement for the enemy, and allowing it to turn and aim at our Player while shooting.
This marks the end of the Pygame RPG – New Enemy Tutorial. Any suggestions or contributions for CodersLegacy are more than welcome. Questions regarding the tutorial content can be asked in the comments section below.