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 :
- Capitalize the first letter of your class names
- 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 becomesposition
(orpos
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, hencemove_snake(...)
becomesmove(...)
since you are going to call it withsnake.move()
anyway - 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)
- 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.
- 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)
- 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.
- 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):