-
Notifications
You must be signed in to change notification settings - Fork 759
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split game logic into multiple sprites
- Loading branch information
Showing
17 changed files
with
547 additions
and
358 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
from .background import Background | ||
from .floor import Floor | ||
from .game_over import GameOver | ||
from .pipe import Pipe, Pipes | ||
from .player import Player, PlayerMode | ||
from .score import Score | ||
from .sprite import Sprite | ||
from .welcome_message import WelcomeMessage | ||
|
||
__all__ = [ | ||
"Background", | ||
"Floor", | ||
"Pipe", | ||
"Pipes", | ||
"Player", | ||
"Score", | ||
"Sprite", | ||
"WelcomeMessage", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from .sprite import Sprite | ||
|
||
|
||
class Background(Sprite): | ||
def setup(self) -> None: | ||
self.x = 0 | ||
self.y = 0 | ||
|
||
def tick(self) -> None: | ||
self.screen.blit(self.images.background, (self.x, self.y)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from .sprite import Sprite | ||
|
||
|
||
class Floor(Sprite): | ||
def setup(self) -> None: | ||
self.x = 0 | ||
self.y = self.window.play_area_height | ||
# amount to shift on each tick | ||
self.vel_x = 4 | ||
# amount by which floor can maximum shift to left | ||
self.x_extra = ( | ||
self.images.base.get_width() - self.images.background.get_width() | ||
) | ||
|
||
def stop(self) -> None: | ||
self.vel_x = 0 | ||
|
||
def tick(self) -> None: | ||
self.x = -((-self.x + self.vel_x) % self.x_extra) | ||
self.screen.blit(self.images.base, (self.x, self.y)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from .sprite import Sprite | ||
|
||
|
||
class GameOver(Sprite): | ||
def setup(self) -> None: | ||
self.x = int((self.window.width - self.images.gameover.get_width()) / 2) | ||
self.y = int(self.window.height * 0.2) | ||
|
||
def tick(self) -> None: | ||
self.screen.blit(self.images.gameover, (self.x, self.y)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import random | ||
from typing import List | ||
|
||
from pygame import Surface | ||
|
||
from .sprite import Sprite | ||
|
||
|
||
class Pipe(Sprite): | ||
def setup(self) -> None: | ||
self.x = 0 | ||
self.y = 0 | ||
self.set_image(self.images.pipe[0]) | ||
self.mid_x = self.x + self.images.pipe[0].get_width() / 2 | ||
# TODO: make this change with game progress | ||
self.vel_x = -5 | ||
|
||
def set_image(self, image: Surface) -> None: | ||
self.image = image | ||
self.width = self.image.get_width() | ||
self.height = self.image.get_height() | ||
|
||
def tick(self) -> None: | ||
self.x += self.vel_x | ||
self.mid_x = self.x + self.images.pipe[0].get_width() / 2 | ||
self.screen.blit(self.image, (self.x, self.y)) | ||
|
||
|
||
class Pipes(Sprite): | ||
upper: List[Pipe] | ||
lower: List[Pipe] | ||
|
||
def setup(self) -> None: | ||
# TODO: make this change with game progress | ||
self.pipe_gap = 120 | ||
self.top = 0 | ||
self.bottom = self.window.play_area_height | ||
self.reset() | ||
|
||
def reset(self) -> None: | ||
self.upper = [] | ||
self.lower = [] | ||
self.spawn_initial_pipes() | ||
|
||
def tick(self) -> None: | ||
if self.can_spawn_more(): | ||
self.spawn_new_pipes() | ||
self.remove_old_pipes() | ||
|
||
for up_pipe, low_pipe in zip(self.upper, self.lower): | ||
up_pipe.tick() | ||
low_pipe.tick() | ||
|
||
def stop(self) -> None: | ||
for pipe in self.upper + self.lower: | ||
pipe.vel_x = 0 | ||
|
||
def can_spawn_more(self) -> bool: | ||
# has 1 or 2 pipe and first pipe is almost about to exit the screen | ||
return 0 < len(self.upper) < 3 and 0 < self.upper[0].x < 5 | ||
|
||
def spawn_new_pipes(self): | ||
# add new pipe when first pipe is about to touch left of screen | ||
upper, lower = self.make_random_pipes() | ||
self.upper.append(upper) | ||
self.lower.append(lower) | ||
|
||
def remove_old_pipes(self): | ||
# remove first pipe if its out of the screen | ||
if ( | ||
len(self.upper) > 0 | ||
and self.upper[0].x < -self.images.pipe[0].get_width() | ||
): | ||
self.upper.pop(0) | ||
self.lower.pop(0) | ||
|
||
def spawn_initial_pipes(self): | ||
upper_1, lower_1 = self.make_random_pipes() | ||
upper_1.x = self.window.width + 100 | ||
lower_1.x = self.window.width + 100 | ||
|
||
upper_2, lower_2 = self.make_random_pipes() | ||
upper_2.x = self.window.width + 100 + (self.window.width / 2) | ||
lower_2.x = self.window.width + 100 + (self.window.width / 2) | ||
|
||
self.upper.append(upper_1) | ||
self.upper.append(upper_2) | ||
|
||
self.lower.append(lower_1) | ||
self.lower.append(lower_2) | ||
|
||
def make_random_pipes(self): | ||
"""returns a randomly generated pipe""" | ||
# y of gap between upper and lower pipe | ||
base_y = self.window.play_area_height | ||
|
||
gap_y = random.randrange(0, int(base_y * 0.6 - self.pipe_gap)) | ||
gap_y += int(base_y * 0.2) | ||
pipe_height = self.images.pipe[0].get_height() | ||
pipe_x = self.window.width + 10 | ||
|
||
upper_pipe = Pipe(*self._args) | ||
upper_pipe.x = pipe_x | ||
upper_pipe.y = gap_y - pipe_height | ||
upper_pipe.set_image(self.images.pipe[0]) | ||
|
||
lower_pipe = Pipe(*self._args) | ||
lower_pipe.x = pipe_x | ||
lower_pipe.y = gap_y + self.pipe_gap | ||
lower_pipe.set_image(self.images.pipe[1]) | ||
|
||
return upper_pipe, lower_pipe |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
from enum import Enum | ||
from itertools import cycle | ||
|
||
import pygame | ||
|
||
from ..utils import clamp, pixel_collision | ||
from .floor import Floor | ||
from .pipe import Pipe, Pipes | ||
from .sprite import Sprite | ||
|
||
|
||
class PlayerMode(Enum): | ||
SHM = "SHM" | ||
NORMAL = "NORMAL" | ||
CRASH = "CRASH" | ||
|
||
|
||
class Player(Sprite): | ||
def setup(self) -> None: | ||
self.img_idx = 0 | ||
self.img_gen = cycle([0, 1, 2, 1]) | ||
self.frame = 0 | ||
self.crashed = False | ||
self.crash_entity = None | ||
self.width = self.images.player[0].get_width() | ||
self.height = self.images.player[0].get_height() | ||
self.reset_pos() | ||
self.set_mode(PlayerMode.SHM) | ||
|
||
def set_mode(self, mode: PlayerMode) -> None: | ||
self.mode = mode | ||
if mode == PlayerMode.NORMAL: | ||
self.reset_vals_normal() | ||
self.sounds.wing.play() | ||
elif mode == PlayerMode.SHM: | ||
self.reset_vals_shm() | ||
elif mode == PlayerMode.CRASH: | ||
self.sounds.hit.play() | ||
if self.crash_entity == "pipe": | ||
self.sounds.die.play() | ||
self.reset_vals_crash() | ||
|
||
def reset_pos(self) -> None: | ||
self.x = int(self.window.width * 0.2) | ||
self.y = int( | ||
(self.window.height - self.images.player[0].get_height()) / 2 | ||
) | ||
self.mid_x = self.x + self.width / 2 | ||
self.mid_y = self.y + self.height / 2 | ||
|
||
def reset_vals_normal(self) -> None: | ||
self.vel_y = -9 # player's velocity along Y axis | ||
self.max_vel_y = 10 # max vel along Y, max descend speed | ||
self.min_vel_y = -8 # min vel along Y, max ascend speed | ||
self.acc_y = 1 # players downward acceleration | ||
|
||
self.rot = 45 # player's current rotation | ||
self.vel_rot = -3 # player's rotation speed | ||
self.rot_min = -90 # player's min rotation angle | ||
self.rot_max = 20 # player's max rotation angle | ||
|
||
self.flap_acc = -9 # players speed on flapping | ||
self.flapped = False # True when player flaps | ||
|
||
def reset_vals_shm(self) -> None: | ||
self.vel_y = 1 # player's velocity along Y axis | ||
self.max_vel_y = 4 # max vel along Y, max descend speed | ||
self.min_vel_y = -4 # min vel along Y, max ascend speed | ||
self.acc_y = 0.5 # players downward acceleration | ||
|
||
self.rot = 0 # player's current rotation | ||
self.vel_rot = 0 # player's rotation speed | ||
self.rot_min = 0 # player's min rotation angle | ||
self.rot_max = 0 # player's max rotation angle | ||
|
||
self.flap_acc = 0 # players speed on flapping | ||
self.flapped = False # True when player flaps | ||
|
||
def reset_vals_crash(self) -> None: | ||
self.acc_y = 2 | ||
self.vel_y = 7 | ||
self.max_vel_y = 15 | ||
|
||
def update_img_idx(self): | ||
self.frame += 1 | ||
if self.frame % 5 == 0: | ||
self.img_idx = next(self.img_gen) | ||
|
||
def tick_shm(self) -> None: | ||
if self.vel_y >= self.max_vel_y or self.vel_y <= self.min_vel_y: | ||
self.acc_y *= -1 | ||
self.vel_y += self.acc_y | ||
self.y += self.vel_y | ||
|
||
self.mid_x = self.x + self.width / 2 | ||
self.mid_y = self.y + self.height / 2 | ||
|
||
def tick_normal(self) -> None: | ||
if self.vel_y < self.max_vel_y and not self.flapped: | ||
self.vel_y += self.acc_y | ||
if self.flapped: | ||
self.flapped = False | ||
|
||
self.y += min( | ||
self.vel_y, self.window.play_area_height - self.y - self.height | ||
) | ||
|
||
self.mid_x = self.x + self.width / 2 | ||
self.mid_y = self.y + self.height / 2 | ||
|
||
def tick_crash(self) -> None: | ||
if self.y + self.height < self.window.play_area_height - 1: | ||
self.y += min( | ||
self.vel_y, self.window.play_area_height - self.y - self.height | ||
) | ||
|
||
# player velocity change | ||
if self.vel_y < self.max_vel_y: | ||
self.vel_y += self.acc_y | ||
|
||
# rotate only when it's a pipe crash | ||
if self.crash_entity != "floor": | ||
self.rotate() | ||
|
||
def rotate(self) -> None: | ||
self.rot = clamp(self.rot + self.vel_rot, self.rot_min, self.rot_max) | ||
|
||
def tick(self) -> None: | ||
self.update_img_idx() | ||
if self.mode == PlayerMode.SHM: | ||
self.tick_shm() | ||
elif self.mode == PlayerMode.NORMAL: | ||
self.tick_normal() | ||
self.rotate() | ||
elif self.mode == PlayerMode.CRASH: | ||
self.tick_crash() | ||
|
||
self.draw_player() | ||
|
||
def draw_player(self) -> None: | ||
player_surface = pygame.transform.rotate( | ||
self.images.player[self.img_idx], self.rot | ||
) | ||
self.screen.blit(player_surface, (self.x, self.y)) | ||
|
||
def flap(self) -> None: | ||
self.vel_y = self.flap_acc | ||
self.flapped = True | ||
self.rot = 45 | ||
self.sounds.wing.play() | ||
|
||
def crossed(self, pipe: Pipe) -> bool: | ||
return pipe.mid_x <= self.mid_x < pipe.mid_x + 4 | ||
|
||
def collided(self, pipes: Pipes, floor: Floor) -> bool: | ||
"""returns True if player collides with base or pipes.""" | ||
|
||
# if player crashes into ground | ||
if self.y + self.height >= floor.y - 1: | ||
self.crashed = True | ||
self.crash_entity = "floor" | ||
return True | ||
else: | ||
p_rect = pygame.Rect(self.x, self.y, self.width, self.height) | ||
|
||
for u_pipe, l_pipe in zip(pipes.upper, pipes.lower): | ||
# upper and lower pipe rects | ||
u_pipe_rect = pygame.Rect( | ||
u_pipe.x, u_pipe.y, u_pipe.width, u_pipe.height | ||
) | ||
l_pipe_rect = pygame.Rect( | ||
l_pipe.x, l_pipe.y, l_pipe.width, l_pipe.height | ||
) | ||
|
||
# player and upper/lower pipe hitmasks | ||
p_hit_mask = self.hit_mask.player[self.img_idx] | ||
u_hit_mask = self.hit_mask.pipe[0] | ||
l_hit_mask = self.hit_mask.pipe[1] | ||
|
||
# if bird collided with upipe or lpipe | ||
u_collide = pixel_collision( | ||
p_rect, u_pipe_rect, p_hit_mask, u_hit_mask | ||
) | ||
l_collide = pixel_collision( | ||
p_rect, l_pipe_rect, p_hit_mask, l_hit_mask | ||
) | ||
|
||
if u_collide or l_collide: | ||
self.crashed = True | ||
self.crash_entity = "pipe" | ||
return True | ||
|
||
return False |
Oops, something went wrong.