Skip to content
Advertisement

I am having trouble with enemies in pygame

I have been trying to create an enemy class for my snake game that I can execute in my pygame loop, but my fill_screen() function keeps it so that either one enemy is on the screen at a time, or no enemies are on the screen. I hope someone can help me with this issue, because I have been working on it for over 3 days and I have not found any practical solutions. An example would really help. My code is attached below.

import pygame,sys,random
from pygame.locals import *

pygame.init()
clock=pygame.time.Clock()

movement="nil"

enemy_list = []
enemy_img = pygame.image.load(r'C:UsershudsoDocumentsPyGame__pycache__Imagesmele_enemy.png')
SPAWNENEMY = pygame.USEREVENT
pygame.time.set_timer(SPAWNENEMY,1000)

cooled=False

food_created=False
food_counter=0

cooldown_tracker=0

window_size=[1000,1000]

screen=pygame.display.set_mode((window_size[0],window_size[1]),RESIZABLE)
display=pygame.Surface((400,400))

snake_health=100
snake_pos=[188,188]
enemy_pos=[100,100]

snake_img=pygame.image.load(r"C:UsershudsoDocumentsPyGame__pycache__Imagessnake.png")

def new_food_pos():
    food_x=random.randint(25,350)
    food_y=random.randint(25,350)
    food_coords=[food_x,food_y]
    return food_coords

class snake():
    def __init__(self,snake_position,snake_hitpoints):
        self.snake_position=snake_position
        self.snake_hp=snake_hitpoints

      
    def create_snake(self):
        global snake_img
        display.blit(snake_img,(self.snake_position[0],self.snake_position[1]))
        pygame.draw.rect(display,(0,0,0),[self.snake_position[0]-3,self.snake_position[1]-7,31,5])
        pygame.draw.rect(display,(212,23,48),[self.snake_position[0]-1,self.snake_position[1]-6,27,3])
        sub_hp=self.snake_hp-100
        take_health=abs(sub_hp)
        gray_health=(take_health*0.27)
        pygame.draw.rect(display,(40,40,40),[self.snake_position[0]-0+self.snake_hp*.27,self.snake_position[1]-6,gray_health,3])
    
    def move_snake(self,moving):
        global snake_img
        if moving=="up":
            snake_pos[1]-=1
        if moving=="down":
            snake_pos[1]+=1
        if moving=="right":
            snake_pos[0]+=1
        if moving=="left":
            snake_pos[0]-=1
        if moving=="nil":
            display.blit(snake_img,(self.snake_position[0],self.snake_position[1]))

food_pos=new_food_pos()

class food():
        
    def __init__(self):
        global food_pos
        self.food_location=food_pos
        self.food=pygame.image.load(r"C:UsershudsoDocumentsPyGame__pycache__Imagesapple.png")
        
    def create_food(self):
        display.blit(self.food,(self.food_location[0],self.food_location[1]))

class Enemys:
    def __init__(self):
            #self.xval = random.randint(0,700)
            self.size = random.randint(50,50)
    def create_enemy(self):
        Enemy = pygame.Rect(random.randint(20,480), random.randint(20,480), self.size,self.size)
        #enemy_updated = pygame.transform.scale(enemy_img,(self.size,self.size))
        enemy_list.append(Enemy)
        display.blit(enemy_img,Enemy)
        
    def draw(self):  # draw all enemies
        for e in enemy_list:
            display.blit(enemy_img,e)
    
    def move_enemy(self):
        if self.enemy_position[0]!=snake_pos[0]:
            if self.enemy_position[0]+15>snake_pos[0]:
                self.enemy_position[0]-=.5
            if self.enemy_position[0]-15<snake_pos[0]:
                self.enemy_position[0]+=.5
        if snake_pos[0]-16<self.enemy_position[0] and snake_pos[0]+16>self.enemy_position[0]:
            if self.enemy_position[1]!=snake_pos[1]:
                if self.enemy_position[1]-15>snake_pos[1]:
                    self.enemy_position[1]-=.5
                if self.enemy_position[1]+15<snake_pos[1]:
                    self.enemy_position[1]+=.5
        
def fill_screen():
    display.fill((0,0,0))
    pygame.draw.rect(display,(29,10,200),[2,2,396,396])
    pygame.draw.rect(display,(43,7,230),[3,3,394,394])

while True:    
    fill_screen()
    
    food().create_food()
    food_rect=pygame.Rect((food_pos[0],food_pos[1]),(25,25))
    
    snake(snake_pos,snake_health).create_snake()
    for event in pygame.event.get():
        if event.type==pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type==pygame.KEYDOWN:
            if event.key==K_RIGHT:
                movement="right"
            if event.key==K_LEFT:
                movement="left"
            if event.key==K_UP:
                movement="up"
            if event.key==K_DOWN:
                movement="down"
            if event.type == SPAWNENEMY:
                Enemys().create_enemy()
    
    snake(snake_pos,snake_health).move_snake(movement)
    
    snake_rect=pygame.Rect((snake_pos[0],snake_pos[1]),(25,25))
    
    food_eaten=snake_rect.colliderect(food_rect)
    
    if snake_health<=0:
        pygame.quit()
        sys.exit()
        print("You Died From Lack Of Living")
    
    if food_eaten:
        food_pos=new_food_pos()
        food_counter+=1
        snake_health+=5
    
    if snake_health>=100:
        snake_health=100
    
    surf=pygame.transform.scale(display,window_size)
    screen.blit(surf,(0,0))
    
    Enemys().draw()
    
    pygame.display.update()
    clock.tick(60)


Thx

Advertisement

Answer

This answer is going to be long, many thing to say about this piece of code. Don’t see this as harsh criticisms but rather, I hope it can guide you towards a somewhat better way of organizing your code and understanding OOP (Object Oriented Programming).

This is more or less a compete refactor of your code with explanations on why I did each change. I refactor what you wrote, but I did NOT finish the game, that’s left for you to do.

First thing I noticed was that you use classes but you don’t really seem to understand how there are meant to be use. For example, why having a snake class if you put variables such as “snake_health” or “snake_position” outside the class and then use some funky ways to get values back inside your classes.

Here is how I re-wrote the snake class :

class Snake():
    def __init__(self, pos):
        self.pos = pos
        self.lenght = 0
        self.health = 100
        self.direction = None
        self.img = pg.image.load("snake.png")
    
    def move(self):
        if self.direction=="up":
            self.pos[1]-=1
        elif self.direction=="down":
            self.pos[1]+=1
        elif self.direction=="right":
            self.pos[0]+=1
        elif self.direction=="left":
            self.pos[0]-=1
    
    def take_damage(self, damage):
        self.health -= damage

Quite a few points for you to take into account :

  1. Capitalize the first letter of your class names
  2. You don’t need to put the name of the class inside instance variables names since it’s redondant. So, for instance : snake_position simply becomes position (or pos as I personnally prefer to call it) since it is inside the snake class we already know it is link to the snake. Same goes for class methods, hence move_snake(...) becomes move(...) since you are going to call it with snake.move() anyway
  3. Notice how I remove everything that had to do with draw and blit ? Since you are a beginner, I would advice against any draw or blit inside classes until you really know what you are doing. For your first projects I would encourage you to have a single method called draw() that is called in the main loop and that draws everything.

Here is the draw function I just mentionned :

def draw():
    # Clear screen
    screen.fill((0,0,0))

    # Draw snake
    screen.blit(snake.img, snake.pos)

    # Draw hp bar
    snake_x, snake_y = snake.pos
    bar_width, bar_height = 30,5
    bar_x, bar_y = snake_x - bar_width // 2 + snake.img.get_width() // 2, snake_y - bar_height - snake.img.get_height() // 2

    # hp bar background
    pg.draw.rect(screen, (255,0,0), pg.Rect(bar_x, bar_y, bar_width, bar_height))
    # hp bar filled part
    pg.draw.rect(screen, (0,255,0), pg.Rect(bar_x, bar_y, bar_width * (snake.health / 100), bar_height))
    # hp bar contour
    pg.draw.rect(screen, (80,80,80), pg.Rect(bar_x, bar_y, bar_width, bar_height), 1)

    # Draw food
    screen.blit(food.img, food.pos)

    # Draw enemies
    for enemy in Enemy.LIST:
        screen.blit(enemy.img, enemy.pos)

To update your values and do a bit of game logic : group it in a function (most people will call it update() so let’s stick with that)

def update():
    # Move the snake every frame
    snake.move()

    # Check if snake ate the food
    snake_rect = pg.Rect(snake.pos, snake.img.get_size())
    food_rect = pg.Rect(food.pos, food.img.get_size())
    if snake_rect.colliderect(food_rect): # (if food_eaten)
        food.pos = new_food_pos()
        snake.lenght += 1
    
    # Move the enemies
    for enemy in Enemy.LIST:
        enemy.move(snake.pos)

        # Check if enemy reached snake
        enemy_rect = pg.Rect(enemy.pos, enemy.img.get_size())
        if snake_rect.colliderect(enemy_rect):
            snake.take_damage(10)
            enemy.kill()
            if snake.health <= 0:
                game_over()

A good rule of thumb, is to write your classes so that by calling methods in the update function it is really easy to understand the logic without having to look elsewhere in the code. For instance, if you look at :

if snake_rect.colliderect(enemy_rect):
        snake.take_damage(10)
        enemy.kill()
        if snake.health <= 0:
            game_over()

it is very clear that if snake and enemy collide, snake will take damage, enemy will be killed and after that if the snake health has reach 0 then it’s game over.

Here is how I re-wrote the food class :

class Food():
    def __init__(self, pos):
        self.pos = pos
        self.img = pg.image.load("apple.png")

So simple that you probably woudn’t need a class at all but I wanted to stick to your choice of writing a class for it. I think you saw but I replace the term “location” with “pos” as well. That’s another thing you should avoid : using different terms to say the same thing : for the position of the snake you used the term “position” and for the position of the food you used the term “location” both are correct names but choose one and use the same across all your classes otherwise, you just add confusion.

Now your enemy class :

class Enemy:
    LIST = []

    def __init__(self, pos):
            self.pos = pos
            self.img = pg.image.load("enemy.png")
            w = h = rd.randint(10, 20)
            self.size = (w, h)
            self.img = pg.transform.scale(self.img, self.size)

            # Add the enemy to the list
            Enemy.LIST.append(self)
    
    def move(self, snake_pos):
        enemy_x, enemy_y = self.pos
        snake_x, snake_y = snake_pos

        # Move enemy towards the player
        # (no need to check if position are equals since it will never happend -> as soon as rects collides)
        if enemy_x - snake_x > 0:
            enemy_x -= .5
        else:
            enemy_x += .5
        if enemy_y - snake_y > 0:
            enemy_y -= .5
        else:
            enemy_y += .5
        
        self.pos = (enemy_x, enemy_y)
    
    def kill(self):
        Enemy.LIST.remove(self)
  1. It’s almost always a bad idea to use plural for a class name, if you really wanted to have a class that represent mutliple enemies, you’d have to find a name like “GroupOfEnemy” for example. But for your game in it’s current state you don’t need that, you just want a class to describe what a single enemy look like and can do.
  2. For the enemies list we could have left it outside the class like you did, for this project it would have been totally ok but I just wanted to show you an alternative by putting it inside the class as a class variable (not instance variable or you are going to run into lots of trouble)
  3. Notice how in each of your class, I removed the create_… function ? That’s because it is not needed, if you want to execute code when an instance is created then just put it in the init function unless it prevents good readability.
  4. Note that each enemy has a position so you really need to put the position inside the class and not as a global variable like you did.

For the event handling part you were mostly right, there was an identation problem as @Rabbid76 mentionned and I wrote it a tiny bit different but close the original :

def handle_events():
    for evt in pg.event.get():
        if evt.type == pg.QUIT:
            exit()
        if evt.type == pg.KEYDOWN:
            if evt.key == pg.K_ESCAPE:
                exit()

        if evt.type == pg.KEYDOWN:
            if evt.key == pg.K_RIGHT:
                snake.direction = "right"
            elif evt.key == pg.K_LEFT:
                snake.direction = "left"
            elif evt.key == pg.K_UP:
                snake.direction = "up"
            elif evt.key == pg.K_DOWN:
                snake.direction = "down"

        if evt.type == SPAWN_ENEMY:
            Enemy((rd.randint(20, 480), rd.randint(20, 480)))

Now the only stuff that’s left are those 3 small functions :

def new_food_pos():
    return [rd.randint(25,350),rd.randint(25,350)]

def game_over():
    print("You Died From Lack Of Living")
    exit()

def exit():
    pg.quit()
    sys.exit()

I think they don’t need explanations.

One last crutial point is to understand that when you call a class name like so :

ClassName(...)

You create an instance of that class so every time in your code when you called snake(snake_position, snake_health) you were creating a new snake instance that shares nothing with all the snakes already created before that is why you still needed to use global variables and the class became completly pointless.

TO SUM UP : You struggle with OOP concepts, I’d suggest watching/ reading tutorials on the subject. If you find yourself having a hard time with understanding those tutorials, it might mean that OOP is too advance for you right now, consolidate the basics first and come back to OOP in a few weeks, it will go much smoother

Be careful about readability : be consistent with your naming habits, use more spaces. You can consult the zen of python (guidlines for python code) by openning your terminal, typing “python” then “import this”

Since you are a beginer, first get use to this structure for pygame projects :

    def update():
        pass

    def draw():
        pass

    def handle_events():
        pass

    while True:
        handle_events()
        update()
        draw()
        pg.display.update()
        clock.tick(FPS)

You will have all the time to experience with code structures later down the line. For now : update all your values and do game logic inside update(), render all your components inside draw() and manage every event inside handle_events(). That will make your learning of pygame much faster and easier.

Beside those functions that are a little bit special, try to follow the Single Responsibility Principle, which means that every function must have one job and one job only, it improves readability so much.

Most importantly : Don’t get discourage, we all need to start somewhere, even if many thing you wrote wouldn’t be consider “good code” you still manage to write code that does pretty much what you wanted and that’s already a big achivement you can be proud of. As you continue programming, your code will get better and better. I even learned something from your code (I started using pygame 5 years ago) : I didn’t know there was a “pygame.USEREVENT” this will be super handy for my future projects.

Happy programming, here is the full refactor in one piece :

# General import
import pygame as pg
import random as rd
import sys

# Classes
class Snake():
    def __init__(self, pos):
        self.pos = pos
        self.lenght = 0
        self.health = 100
        self.direction = None
        self.img = pg.image.load("snake.png")
    
    def move(self):
        if self.direction=="up":
            self.pos[1]-=1
        elif self.direction=="down":
            self.pos[1]+=1
        elif self.direction=="right":
            self.pos[0]+=1
        elif self.direction=="left":
            self.pos[0]-=1
    
    def take_damage(self, damage):
        self.health -= damage

class Food():
    def __init__(self, pos):
        self.pos = pos
        self.img = pg.image.load("apple.png")

class Enemy:
    LIST = []

    def __init__(self, pos):
            self.pos = pos
            self.img = pg.image.load("enemy.png")
            w = h = rd.randint(10, 20)
            self.size = (w, h)
            self.img = pg.transform.scale(self.img, self.size)

            # Add the enemy to the list
            Enemy.LIST.append(self)
    
    def move(self, snake_pos):
        enemy_x, enemy_y = self.pos
        snake_x, snake_y = snake_pos

        # Move enemy towards the player
        # (no need to check if position are equals since it will never happend -> as soon as rects collides)
        if enemy_x - snake_x > 0:
            enemy_x -= .5
        else:
            enemy_x += .5
        if enemy_y - snake_y > 0:
            enemy_y -= .5
        else:
            enemy_y += .5
        
        self.pos = (enemy_x, enemy_y)
    
    def kill(self):
        Enemy.LIST.remove(self)

# Init
pg.init()

# Display
screen = pg.display.set_mode((500, 500))
pg.display.set_caption('Snake - fix')
FPS = 60
clock = pg.time.Clock()

# Set up custom event
SPAWN_ENEMY = pg.USEREVENT
pg.time.set_timer(SPAWN_ENEMY, 1000)

# Main functions
def update():
    # Move the snake every frame
    snake.move()

    # Check if snake ate the food
    snake_rect = pg.Rect(snake.pos, snake.img.get_size())
    food_rect = pg.Rect(food.pos, food.img.get_size())
    if snake_rect.colliderect(food_rect): # (if food_eaten)
        food.pos = new_food_pos()
        snake.lenght += 1
    
    # Move the enemies
    for enemy in Enemy.LIST:
        enemy.move(snake.pos)

        # Check if enemy reached snake
        enemy_rect = pg.Rect(enemy.pos, enemy.img.get_size())
        if snake_rect.colliderect(enemy_rect):
            snake.take_damage(10)
            enemy.kill()
            if snake.health <= 0:
                game_over()

def draw():
    # Clear screen
    screen.fill((0,0,0))

    # Draw snake
    screen.blit(snake.img, snake.pos)

    # Draw hp bar
    snake_x, snake_y = snake.pos
    bar_width, bar_height = 30,5
    bar_x, bar_y = snake_x - bar_width // 2 + snake.img.get_width() // 2, snake_y - bar_height - snake.img.get_height() // 2

    # hp bar background
    pg.draw.rect(screen, (255,0,0), pg.Rect(bar_x, bar_y, bar_width, bar_height))
    # hp bar filled part
    pg.draw.rect(screen, (0,255,0), pg.Rect(bar_x, bar_y, bar_width * (snake.health / 100), bar_height))
    # hp bar contour
    pg.draw.rect(screen, (80,80,80), pg.Rect(bar_x, bar_y, bar_width, bar_height), 1)

    # Draw food
    screen.blit(food.img, food.pos)

    # Draw enemies
    for enemy in Enemy.LIST:
        screen.blit(enemy.img, enemy.pos)

def handle_input():
    for evt in pg.event.get():
        if evt.type == pg.QUIT:
            exit()
        if evt.type == pg.KEYDOWN:
            if evt.key == pg.K_ESCAPE:
                exit()

        if evt.type == pg.KEYDOWN:
            if evt.key == pg.K_RIGHT:
                snake.direction = "right"
            elif evt.key == pg.K_LEFT:
                snake.direction = "left"
            elif evt.key == pg.K_UP:
                snake.direction = "up"
            elif evt.key == pg.K_DOWN:
                snake.direction = "down"

        if evt.type == SPAWN_ENEMY:
            Enemy((rd.randint(20, 480), rd.randint(20, 480)))

def new_food_pos():
    return [rd.randint(25,350),rd.randint(25,350)]

def game_over():
    print("You Died From Lack Of Living")
    exit()

def exit():
    pg.quit()
    sys.exit()

# Main loop
if __name__ == "__main__":
    snake = Snake([100, 100])
    food = Food(new_food_pos())

    while True:
        handle_input()
        update()
        draw()
        pg.display.update()
        clock.tick(FPS)

Here are the 3 images I used (for enemy.png, apple.png and snake.png): enemy.png apple.png snake.png

User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement