In my current project, I try to implement the basic logic of the game "Vampire Survival" (Little impression of the first level can be seen in this video on Youtube).
Question
I want to know, if regarding to the concepts of OOP, I should restructure my code from the beginning or continue building on it like I started.
The whole structure looks like this at the moment:
project
│
└───main.py
│ - main()
│
└───enemy.py
│ class Enemy(Sprite):
│ - draw()
│ - update()
│
└───player.py
│ class Player(Sprite):
│ - draw()
│ - shoot()
│
└───bullet.py
│ class Bullet(Sprite):
│ - update()
│
└───functions.py
- get_df_from_json()
- convert_framelist_to_image_objects()
- create_player()
- create_enemy()
- create_bullet()
- spawn_enemies()
- scroll_background()
- levels_create_enemies()
- get_direction()
Functionality
I have 3 classes at the moment, Player
, Enemy
and Bullet
.
There is an infinitely field, where the main character is always in the middle of the screen, if he moves, the background is moving while him staying in the center.
I wrote a function scroll_background()
that does that for me, together with a infinity background image I have no "borders".
The enemies spawn in random coordinates outside of the screen. The enemies will always move towards the middle of the screen (= always to the player). The calculation for that is made in draw()
in the Enemy Sprite class. The amount of the enemies and which enemy should be time related and level related). I have all Players and Enemies of the original game as spritesheet with a big JSON file to access them. For the moment to get progress done, I just hardcoded some list of the Player and Enemy I want to keep it simple.
Every few seconds the player shoots a bullet (without a key event) and the bullet (starting at the middle of the screen) fires in the direction where the player is currently moving (8 directions). At the moment the collision with the enemies isn't implemented.
In the update()
function of the Enemy Sprite class the calculation and movement relative to the player is done: (it is called in every handling of the key press events of the 4 directions (left, right, top, bottom), signs and ax needed to call it for every direction.
For the moment I don't focus on how Enemies, Player and Bullets are created (I just wanted it to work for the basics) and also the way the so called "levels" are implemented. It's all about the way I handle all movements on the playground relative to the infinite scrolling background.
I get the feeling the way I structured my code will bite me in the future. The handling of different movements will be done at too many different places. Also it is very vulnerable to bugs or mistakes with all the relative coordinates. Every single new item, enemy, weapon, shield and anything else I will implement needs to be handled relatively to the middle of the screen (while keeping track of the real current x and y position of the player).
I don't know how else it is possible to have this infinite playfield with a static player in the mid else than I did it. The code gets messier the more I try to add and I don't know how to keep my code clean and more adjustable.
How should I handle all the other movements, any suggestions for me? Any ideas how to structure my code differently? The whole thing is (beside it's fun) to get a better understanding of OOP, I feel like I miss something here and don't take the advantages of it.
My code:
player.py
:
import pygame
from bullet import Bullet
class Player(pygame.sprite.Sprite):
def __init__(self,
x: int,
y: int,
images: list,
sheet: pygame.Surface,
screen: pygame.Surface):
super(Player, self).__init__()
self.moving_count = 0
self.spritesheet = sheet
self.images = images
self.standing = self.images[0]
self.moving = self.images[1:]
self.x = x
self.y = y
self.rect = self.standing.get_rect(topleft=(self.x, self.y))
self.screen = screen
def draw(self, moving: bool):
# choosing the animation images from list
rate = 20
num_images = len(self.moving)
animation_image_speed = num_images * rate
img_index = self.moving_count//rate
if self.moving_count + 1 >= animation_image_speed:
self.moving_count = 0
if moving:
self.screen.blit(self.moving[img_index],
self.screen.get_rect().center)
self.moving_count += 1
else:
self.screen.blit(self.standing, self.screen.get_rect().center)
self.rect.center = self.screen.get_rect().center
def shoot(self, bullet_group, image, bull_dx, bull_dy):
bull = Bullet((self.rect.centerx + (0.6 *
self.rect.size[0]), self.rect.centery), image, bull_dx, bull_dy)
bullet_group.add(bull)
# print(bull.time)
return bull
enemy.py
:
import math
from typing import Tuple
import pygame
class Enemy(pygame.sprite.Sprite):
def __init__(self,
images: list,
sheet: pygame.Surface,
spawn_coords: Tuple[int, int],
speed_factor: float,
field_moving_speed: int,
):
super(Enemy, self).__init__()
self.moving_count = 0
self.field_moving_speed = -field_moving_speed
self.speed_factor = speed_factor
self.spritesheet = sheet
self.images = images
self.image = self.images[0]
self.x, self.y = spawn_coords
self.rect = self.image.get_rect(topleft=(self.x, self.y))
def draw(self, win: pygame.Surface):
# MOVE towards player:
target_x, target_y = win.get_rect().center
radians = math.atan2(target_y - self.y, target_x - self.x)
dx = self.speed_factor * math.cos(radians)
dy = self.speed_factor * math.sin(radians)
self.x += dx
self.y += dy
#DRAW the enemy on the screen:
rate = 20
num_images = len(self.images)
animation_image_speed = num_images * rate
if self.moving_count + 1 >= animation_image_speed:
self.moving_count = 0
win.blit(self.images[self.moving_count//rate],(self.x, self.y))
self.moving_count += 1
self.rect = self.image.get_rect(topleft=(self.x, self.y))
def update(self,sign=1, ax='x'):
if ax=='x':
self.x += (sign * self.field_moving_speed)
else:
self.y += (sign * self.field_moving_speed)
self.rect = self.image.get_rect(topleft=(self.x, self.y))
bullet.py
:
import time
from typing import Tuple
import pygame
class Bullet(pygame.sprite.Sprite):
def __init__(self, xy: Tuple[int, int], image: pygame.Surface, bull_dx: float, bull_dy: float) -> None:
super().__init__()
self.x, self.y = xy
self.speed = 6
self.image = image
self.rect = self.image.get_rect()
self.rect.center = xy
self.time = time.time()
self.dx = bull_dx
self.dy = bull_dy
def update(self):
bullet_alive = time.time() - self.time
if bullet_alive >= 4:
self.kill()
self.x += int(self.speed * self.dx)
self.y += int(self.speed * self.dy)
self.rect.center = (self.x, self.y)
functions.py
:
import json
import math
import os
import random
from typing import Tuple
import pandas as pd
import pygame
from enemy import Enemy
def get_df_frames_from_json(folder: str, file: str) -> pd.DataFrame:
"""
Input: path, file
Return: DataFrame with information about the positions of each image in the big image
"""
with open(os.path.join(folder, file)) as f:
data = json.load(f)
df = pd.json_normalize(data['textures'][0]['frames'])
df = df.drop(df.columns[1:9],axis=1)
df['char'] = df['filename'].str.split('.',expand=True)[0]
return df
def convert_framelist_to_image_objects(list_of_lists: list, spritesheet: pygame.image) -> list:
"""
Input: list of frame information
Return: list of Image Objects
"""
image_list = []
for ls in list_of_lists:
x, y, w, h = ls
image = pygame.Surface((w, h)).convert_alpha()
image.blit(spritesheet, (0, 0), (x, y, w, h))
#image = pygame.transform.scale(image, (w * scale, h * scale))
image.set_colorkey((0, 0, 0))
image_list.append(image)
return image_list
def create_player(
folder: str,
file: str,
name: str,
spritesheet_characters: pygame.Surface
) -> list:
"""
from spreadsheet and its json data to list of images of one character
Returns:
list of lists with frame positions for all images with the searched character name
"""
# load json, convert to df and do some changes
df = get_df_frames_from_json(folder, file)
list_of_lists = (df[df['char'].str.startswith(name)]
.filter(like='frame')
.drop_duplicates(subset=['frame.x', 'frame.y', 'frame.w', 'frame.h'])
.values.tolist())
player_image_list = convert_framelist_to_image_objects(
list_of_lists, spritesheet_characters)
return player_image_list
def create_enemy(
folder: str,
file: str,
name: list,
spritesheet_enemies: pygame.Surface,
) -> list:
df = get_df_frames_from_json(folder, file)
list_of_lists = (df[df['filename'].isin(name)]
.filter(like='frame')
.drop_duplicates(subset=['frame.x', 'frame.y', 'frame.w', 'frame.h'])
.values.tolist())
enemy_image_list = convert_framelist_to_image_objects(
list_of_lists, spritesheet_enemies)
return enemy_image_list
def create_bullet(
folder: str,
file: str,
name: list,
spritesheet_weapons: pygame.Surface,
) -> list:
df = get_df_frames_from_json(folder, file)
list_of_lists = (df[df['filename'].isin(name)]
.filter(like='frame')
.drop_duplicates(subset=['frame.x', 'frame.y', 'frame.w', 'frame.h'])
.values.tolist())
bullet_image_list = convert_framelist_to_image_objects(
list_of_lists, spritesheet_weapons)
return bullet_image_list
def spawn_enemies() -> Tuple[int, int]:
"""
Enemies always spawn from outside of the screen in a given range so they don't appear all together at the same time
return: Tuple with x and y coordinates of Enemies
"""
spawn_x = random.choice(
[random.randint(-650, -150), random.randint(850, 1350)])
spawn_y = random.choice(
[random.randint(-660, -150), random.randint(850, 1350)])
return spawn_x, spawn_y
def scroll_background(screen: pygame.Surface, bkgd: pygame.Surface, x: int, y: int) -> Tuple[int, int]:
"""
Keep track of players real x and y coordinates while background is moving to keep player always in mid
return: Tuple with x and y coordinates of Player
"""
xOff = (0 - x % bkgd.get_rect().width)
yOff = (0 - y % bkgd.get_rect().height)
screen.blit(bkgd, [xOff, yOff])
screen.blit(bkgd, [xOff + bkgd.get_rect().width, yOff])
screen.blit(bkgd, [xOff, yOff + bkgd.get_rect().height])
screen.blit(bkgd, [xOff + bkgd.get_rect().width, yOff + bkgd.get_rect().height])
return x,y
def levels_create_enemies(level, total_time_game, dic, enemy_group, lst_names, all_enemies, moving_speed):
"""
creates enemies after given amount of time
Each level spawn different enemies
Informations of the different enemies are stored in the dictionary "dic"
Returns: Tuple with level and enemy_group (Sprites)
"""
if (level == 0 and total_time_game <= 30):
for _ in range(100):
enemy = Enemy(dic[lst_names[3]][0], all_enemies, spawn_enemies(
), dic[lst_names[3]][1], moving_speed)
enemy_group.add(enemy)
level += 1
elif (level == 1 and 30 < total_time_game <= 60):
for _ in range(100):
enemy = Enemy(dic[lst_names[1]][0], all_enemies, spawn_enemies(
), dic[lst_names[1]][1], moving_speed)
enemy_group.add(enemy)
level += 1
elif (level == 2 and 60 < total_time_game <= 90):
for _ in range(100):
enemy = Enemy(dic[lst_names[2]][0], all_enemies, spawn_enemies(
), dic[lst_names[2]][1], moving_speed)
enemy_group.add(enemy)
level += 1
elif (level == 3 and 90 < total_time_game <= 120):
for _ in range(100):
enemy = Enemy(dic[lst_names[3]][0], all_enemies, spawn_enemies(
), dic[lst_names[3]][1], moving_speed)
enemy_group.add(enemy)
level += 1
return level, enemy_group
def get_direction(keys: list) -> Tuple[float, float]:
"""
input: current key presses
return: Tuple with x,y for 8 directions each 45°
"""
diag = math.cos(math.radians(45))
if not any(list(keys)):
return 1, 0 # where to shoot when not moving
if keys[pygame.K_LEFT]:
if keys[pygame.K_UP]:
return -diag, -diag
elif keys[pygame.K_DOWN]:
return -diag, diag
else:
return -1, 0
elif keys[pygame.K_RIGHT]:
if keys[pygame.K_UP]:
return diag, -diag
elif keys[pygame.K_DOWN]:
return diag, diag
else:
return 1, 0
elif keys[pygame.K_UP]:
return 0, -1
elif keys[pygame.K_DOWN]:
return 0, 1
main.py
:
import os
import time
import pygame
from functions import (create_bullet, create_enemy, create_player,
get_direction, levels_create_enemies, scroll_background)
from player import Player
def main():
#Setup pygame
pygame.init()
CLOCK = pygame.time.Clock()
FPS = 60
SCREEN_WIDTH, SCREEN_HEIGHT = 700, 700
SCREEN = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('Custom Vampire_Survivals')
# path
game_folder = os.path.dirname(__file__)
img_folder = os.path.join(game_folder, 'assets', 'img')
# load background
BKGD = pygame.image.load(os.path.join(img_folder, 'bg_molise.png')).convert()
# load spritesheets
all_players = pygame.image.load(os.path.join(img_folder, 'characters.png')).convert_alpha()
all_enemies = pygame.image.load(os.path.join(img_folder, 'enemies.png')).convert_alpha()
vfx_png = pygame.image.load(os.path.join(img_folder, 'vfx.png')).convert_alpha()
# image lists of Player
player_image_list = create_player(
img_folder,
'characters.json',
'Antonio',
all_players
)
# dictionary of image lists of Enemies
# manually extract Enemies to keep things a little more simple
Bat1 = ['Bat1_0.png', 'Bat1_i01.png', 'Bat1_i02.png', 'Bat1_i03.png']
XLReaper = ['XLReaper_i01.png', 'XLReaper_i02.png', 'XLReaper_i03.png',
'XLReaper_i04.png', 'XLReaper_i05.png', 'XLReaper_i06.png', 'XLReaper_i08.png']
XLCount = ['XLCount2_i01.png', 'XLCount2_i02.png', 'XLCount2_i03.png',
'XLCount2_i04.png', 'XLCount2_i05.png', 'XLCount2_i06.png']
XLMummy = ['XLMummy_i01.png', 'XLMummy_i02.png', 'XLMummy_i03.png']
lst_enemies = [Bat1, XLReaper, XLCount, XLMummy]
lst_names = ['Bat1', 'XLReaper', 'XLCount', 'XLMummy']
speed_factor = [0.4, 0.6, 0.8, 1]
enemy_dict = {}
for name, en, speed_f in zip(lst_names, lst_enemies, speed_factor):
enemy_image_list = create_enemy(
img_folder,
'enemies.json',
en,
all_enemies)
enemy_dict[name] = (enemy_image_list, speed_f)
# image lists of bullets
bullet = ['ProjectileFireball2.png']
bull_image = create_bullet(
img_folder,
'vfx.json',
bullet,
vfx_png
)[0]
# Initialisation
start = time.time()
level = 0
bullet_counter = 0
moving_speed = 3
run = True
moving = False
# create sprite
player = Player(0, 0, player_image_list, all_players, SCREEN)
enemy_group = pygame.sprite.Group()
bullet_group = pygame.sprite.Group()
while run:
current_time = time.time()
total_time_game = current_time - start
if level <= 3:
level, enemy_group = levels_create_enemies(
level,
total_time_game,
enemy_dict,
enemy_group,
lst_names,
all_enemies,
moving_speed)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
pygame.display.quit()
quit()
player.x, player.y = scroll_background(
SCREEN, BKGD, player.x, player.y)
keys = pygame.key.get_pressed()
if any(list(keys)):
moving = True
else:
moving = False
if keys[pygame.K_LEFT]:
player.x -= moving_speed
enemy_group.update(sign=-1)
if len(bullet_group) > 0:
for bull in bullet_group:
bull.x += moving_speed
if keys[pygame.K_RIGHT]:
player.x += moving_speed
enemy_group.update()
if len(bullet_group) > 0:
for bull in bullet_group:
bull.x -= moving_speed
if keys[pygame.K_UP]:
player.y -= moving_speed
enemy_group.update(sign=-1, ax='y')
if len(bullet_group) > 0:
for bull in bullet_group:
bull.y += moving_speed
if keys[pygame.K_DOWN]:
player.y += moving_speed
enemy_group.update(ax='y')
if len(bullet_group) > 0:
for bull in bullet_group:
bull.y -= moving_speed
for sprite in enemy_group.sprites():
sprite.draw(SCREEN)
player.draw(moving)
bullet_counter += 1
if bullet_counter >= FPS * 5: # every 5 seconds
bull_dx, bull_dy = get_direction(keys)
player.shoot(bullet_group, bull_image, bull_dx, bull_dy)
bullet_counter = 0
bullet_group.update()
bullet_group.draw(SCREEN)
for enemy in enemy_group.sprites():
if player.rect.colliderect(enemy.rect):
pygame.sprite.Sprite.kill(enemy)
pygame.display.update()
CLOCK.tick(FPS)
if __name__ == '__main__':
main()
1 Answer 1
Efficiency
In main.py
, these separate if
statements:
if keys[pygame.K_LEFT]:
if keys[pygame.K_RIGHT]:
should be combined into an if/elif
:
if keys[pygame.K_LEFT]:
elif keys[pygame.K_RIGHT]:
The checks are mutually exclusive. This makes the code more efficient since you don't have to perform the 2nd check if the first is true, etc. Also, this more clearly shows the intent of the code.
DRY
Use a for
loop to create arrays like XLCount
.
This is repeated many times:
if len(bullet_group) > 0:
You should create a variable to reduce the repetition:
num_groups = len(bullet_group)
Then, the code is simpler as:
if num_groups:
since there is no need to compare to 0. In fact, there's
no need for if
line at all.
Naming
The variable start
is more meaningful as start_time
.
Documentation
The PEP 8 style guide recommends
adding docstrings for classes and functions. You have helpful
docstrings in your functions.py
file. You should add them to the others.
In enemy.py
, you should set this repeated expression to a variable:
(sign * self.field_moving_speed)