Pygame RPG Tutorial – Creating a new Enemy type

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.

Pygame New Enemy Type

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.


Move Function

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.


Update Function

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)

Game Loop

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.


Next Section

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.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments