tkinter-space-game/game.py
2023-01-05 11:35:28 +00:00

310 lines
8.8 KiB
Python

from random import randint, random
from tkinter import Canvas, PhotoImage, Tk
from typing import List
from config import Config
from font import Font
from frame_counter import FrameCounter
from inputs import InputController
from sprite import Sprite
from textures import TextureFactory
class Game:
"""A generic game object"""
def __init__(self) -> None:
"""Initialise the game
"""
self.win = Tk()
game_width, game_height = (
Config.WIDTH*Config.SCALE, Config.HEIGHT*Config.SCALE
)
self.w, self.h = Config.WIDTH, Config.HEIGHT
self.win.geometry(f"{game_width}x{game_height}")
self.canvas = Canvas(self.win, width=game_width,
height=game_height, bg="#000")
self.canvas.pack()
self.texture_factory = TextureFactory(scale=Config.SCALE)
self.effect_player = EffectPlayer(self)
self.frame_counter = FrameCounter(self.canvas, Config.FPS)
self.inputs = InputController(self)
self.sprites = []
self.score = 0
self.alpha = 0
def start(self):
"""Start the game"""
self.loop()
self.win.mainloop()
def tick(self):
"""Update the game's sprites"""
for sprite in self.sprites:
sprite.tick()
self.effect_player.tick()
def loop(self):
"""Loop the game at a set framerate"""
self.alpha += 1
self.tick()
self.frame_counter.next_frame(self.loop)
def clear_all(self):
"""Remove all game sprites"""
for sprite in self.sprites:
sprite.destroy()
self.sprites = []
class GameSprite(Sprite):
"""A sprite which belongs to a game"""
def __init__(self, game: Game, image: PhotoImage):
"""Initialise the sprite
:param game: The game which this belongs to
:type game: Game
:param image: The image to use for the sprite
:type image: PhotoImage
"""
self.game = game
super().__init__(game.canvas, image, (0, 0))
def move(self, x, y):
"""Move the sprite by a certain amount
:param x: Amount of pixels to move right
:param y: Amount of pixels to move down
"""
# if the object needs to move less than a pixel
# only move it every few frames to create this effect
if abs(x) >= 1:
self.x += x
elif x != 0:
if self.game.alpha % (1/x) == 0:
self.x += 1
if abs(y) >= 1:
self.y += y
elif y != 0:
if self.game.alpha % (1/y) == 0:
self.y += 1
self.update_position()
class GameEffect(GameSprite):
"""An effect that can be played within game"""
def __init__(self, game: Game, image: PhotoImage,
duration=10, momentum=(0, 0)):
"""Initialise the game effect
:param game: The game which this belongs to
:type game: Game
:param image: The image to use for this
:type image: PhotoImage
:param duration: How long this effect should last for
:param momentum: Which direction to move this effect
"""
self.start_time = game.alpha
self.duration = duration
self.velocity_x, self.velocity_y = momentum
super().__init__(game, image)
def tick(self):
"""Move the effect by its momentum and remove it if its over"""
super().tick()
self.move(self.velocity_x, self.velocity_y)
alpha = self.game.alpha - self.start_time
if self.duration != -1 and alpha > self.duration:
self.destroy()
class AnimatedEffect(GameEffect):
"""An effect which involves animating an image"""
def __init__(self, game: Game, images: List[PhotoImage],
frame_time=1, momentum=(0, 0)):
"""Initialise the effect
:param game: The game which this belongs to
:type game: Game
:param images: The images to use for this animation
:type images: List[PhotoImage]
:param frame_time: Length of each frame of the animation
:param momentum: Direction to move this effect
"""
self.start_time = game.alpha
self.frame_time = frame_time
self.images = images
self.frame_start = game.alpha
super().__init__(game, images[0], duration=len(
images)*frame_time, momentum=momentum)
def tick(self):
"""Animate the effect"""
super().tick()
alpha = self.game.alpha - self.start_time
i = int(alpha // self.frame_time)
if i < len(self.images):
self.set_image(self.images[i])
else:
self.destroy()
class EffectPlayer:
"""An object which concerns itself with managing the effects"""
def __init__(self, game: Game) -> None:
"""Initialise the
:param game: The game which this belongs to
:type game: Game
"""
self.sprites = []
self.game = game
self.explosion_frames = []
self.star_image: PhotoImage
def load_textures(self):
"""Load effect textures"""
self.explosion_frames = [
self.game.texture_factory.get_image(f"explosion{i+1}")
for i in range(3)
]
self.star_image = self.game.texture_factory.get_image("star")
def tick(self):
"""Update all effects"""
for sprite in self.sprites:
sprite.tick()
self.sprites = Sprite.remove_destroyed(self.sprites)
def create_stars(self):
"""Initialise the stars in the background"""
for _ in range(100):
self.create_star(True)
def create_star(self, new=False):
"""Add a star to the background
:param new: Whether this star should be added at
the top of the screen or anywhere
"""
x = randint(0, self.game.w)
if new:
y = randint(0, self.game.h)
else:
y = -1
speed = randint(1, 4) * 0.1
duration = 2*self.game.h / speed
star = GameEffect(
self.game,
self.star_image,
duration=int(duration),
momentum=(0, speed)
)
star.set_pos((x, y))
star.send_to_back()
star.show()
self.sprites.append(star)
def create_explosion(self, position=(0, 0)):
"""Create an explosion effect
:param position: location of the explosion
"""
for _ in range(randint(1, 3)):
m = ((random()*2)-1, (random()*2)-1)
explosion_sprite = AnimatedEffect(
self.game, self.explosion_frames, frame_time=5, momentum=m)
explosion_sprite.set_pos(position)
explosion_sprite.show()
self.sprites.append(explosion_sprite)
def splash_text(self, text, duration=50):
"""splash_text.
:param text:
:param duration:
"""
text_img = Font.load_text(self.game.texture_factory, text)
position = (
(self.game.w-Font.FONT_WIDTH*len(text)) // 2,
(self.game.h-Font.FONT_SIZE) // 3
)
text_sprite = GameEffect(
self.game, text_img, duration=duration)
text_sprite.set_pos(position)
text_sprite.show()
self.sprites.append(text_sprite)
class DamageableSprite(GameSprite):
"""Sprite with health points """
def __init__(self, game: Game, image_name: str, hp=3):
"""Initialise the sprite
:param game: The game which this belongs to
:type game: Game
:param image_name: The name of the image to use for this sprite
:type image_name: str
:param hp: The number of hit points this sprite spawns with
"""
self.image = game.texture_factory.get_image(image_name)
self.white_image = game.texture_factory.get_image(
f"{image_name}:white")
self.hp = hp
self.animation_frame = 0
super().__init__(game, self.image)
def damage(self, amount=1):
"""Decrease number of hit points by an amount
:param amount:
"""
if not self.destroyed:
self.hp -= amount
self.animation_frame = 5
if self.hp <= 0:
self.hp = 0
self.destroy()
self.game.effect_player.create_explosion(self.get_pos())
def tick(self):
"""Update the sprite"""
super().tick()
if self.animation_frame > 0:
self.animation_frame -= 1
if self.animation_frame % 2 == 0:
self.set_image(self.image)
else:
self.set_image(self.white_image)
if __name__ == "__main__":
print("!!!")
print("This is not the main file!")
print("Pleae run\n\tpython main.py\ninstead!")
print("!!!")