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.
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.