3
\$\begingroup\$

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() 
toolic
14.5k5 gold badges29 silver badges203 bronze badges
asked Aug 28, 2022 at 12:18
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

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)
answered Apr 20 at 15:47
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.