Source code for gale.game

"""
This file contains the implementation of the class Game: the window,
virtual-resolution scaling, and game loop (a variable-rate
update/render pair driven by real elapsed time, plus a fixed_update
that steps at a constant rate regardless of frame rate) every gale
game is built on top of. Every constructor argument is optional and
falls back to gale.conf.settings (see gale.conf) when omitted, so a
project's settings.py is enough to configure it without passing
anything explicitly.

Importing this module calls pygame.init().

Author: Alejandro Mujica (aledrums@gmail.com)
"""

import sys

from typing import Optional, Any, Tuple, Dict

import pygame

from .conf import settings
from .memoize import Memo
from .timer import Timer
from .input_handler import InputHandler, InputListener, InputData, INPUT_EVENTS

pygame.init()


[docs] class Game(InputListener): """ Base class to implemente a game by using pygame. This class handles the window to show the game an a virtual screen with the resolution that you want to emulate. This also handles timer and the game loop. Usage example: class MyGame(Game): def init(self) -> None: # Set your own initial configuration of the game. self.player = Player() self.world = World() def on_input(self, input_id: str, input_data: InputData) -> None: # Make your action when an input is detected. if input_id == "quit" and input_data.pressed: self.quit() def update(self, dt: float) -> None: # Update of all your game elements here. # dt is the elapsed time in secconds. self.world.update(dt) self.player.update(dt) self.player.interact_with(self.world) def render(self, surface: pygame.Surface) -> None: # Render all of your game elements on the virtual # screen surface. self.world.render(surface) self.player.render(surface) game = MyGame(title='Title of my game') game.exec() fixed_update() is optional and only needed for logic that must advance by the same amount of time every call, independent of the frame rate (e.g. driving gale.physics.World.fixed_update() directly instead of its own internal accumulator, or a networked simulation tick): class MyGame(Game): def init(self) -> None: self.world = World() def fixed_update(self) -> None: self.world.fixed_update() """ def __init__( self, title: Optional[str] = None, window_width: Optional[int] = None, window_height: Optional[int] = None, virtual_width: Optional[int] = None, virtual_height: Optional[int] = None, fps: Optional[int] = None, fixed_timestep: Optional[float] = None, *args: Tuple[Any], **kwargs: Dict[str, Any], ) -> None: """ Set the basic elements of the game in their initial values. Every parameter below defaults to None, in which case it is resolved from gale.conf.settings instead: the project's own settings.py if it defines that setting, or gale.conf.global_settings otherwise. Pass a value explicitly to override settings for this instance specifically. :param title: Title of the game to show in the window title. Resolved from settings.TITLE, itself None by default, meaning 'Game'. :param window_width: Width of the window to show the game. Resolved from settings.WINDOW_WIDTH, itself 800 by default. :param window_height: Height of the window to show the game. Resolved from settings.WINDOW_HEIGHT, itself 600 by default. :param virtual_width: Width we're trying to emulate. Resolved from settings.VIRTUAL_WIDTH, itself None by default, meaning the same value as window_width. :param virtual_height: Height we're trying to emulate. Resolved from settings.VIRTUAL_HEIGHT, itself None by default, meaning the same value as window_height. :param fps: Number of frame per seconds. Resolved from settings.FPS, itself 60 by default. *args and **kwargs Any argument list of keyword arguments that are accepted by pygame.display.set_mode. :param fixed_timestep: Seconds between two fixed_update() calls. Resolved from settings.FIXED_TIMESTEP, itself 1/60 by default. Unrelated to fps: fixed_update() runs zero, one, or several times per frame so it always advances by exactly this much real time, regardless of the frame rate. """ self.window_width: int = ( window_width if window_width is not None else settings.WINDOW_WIDTH ) self.window_height: int = ( window_height if window_height is not None else settings.WINDOW_HEIGHT ) resolved_virtual_width = ( virtual_width if virtual_width is not None else settings.VIRTUAL_WIDTH ) resolved_virtual_height = ( virtual_height if virtual_height is not None else settings.VIRTUAL_HEIGHT ) self.virtual_width: int = resolved_virtual_width or self.window_width self.virtual_height: int = resolved_virtual_height or self.window_height self.fps = fps if fps is not None else settings.FPS self.fixed_timestep: float = ( fixed_timestep if fixed_timestep is not None else settings.FIXED_TIMESTEP ) if self.fixed_timestep <= 0: raise ValueError( f"fixed_timestep must be positive, got {self.fixed_timestep!r} -- " "a zero or negative value would make __update's accumulator loop " "run forever." ) self._accumulator: float = 0.0 # Setting the screen self.screen: pygame.Surface = pygame.display.set_mode( (self.window_width, self.window_height), *args, **kwargs ) self.title: str = title or settings.TITLE or "Game" pygame.display.set_caption(self.title) # Creating the virtual screen self.render_surface = pygame.Surface((self.virtual_width, self.virtual_height)) self.clock = pygame.time.Clock() self.running: bool = False InputHandler.register_listener(self) self.init()
[docs] def init(self) -> None: """ Empty. This should be implemented by the extension class. """ pass
[docs] def on_input(self, input_id: str, input_data: InputData) -> None: """ Empty. This should be implemented by the extension class. """ pass
[docs] def update(self, dt: float) -> None: """ Empty. This should be implemented by the extension class. :param dt: Time elapsed (in seconds) since the last time this function has been executed. """ pass
[docs] def fixed_update(self) -> None: """ Empty. Override for deterministic, frame-rate-independent logic (e.g. driving a gale.physics.World, or anything else that needs to advance by the same amount of time on every call regardless of how fast frames are rendering): unlike update(), this runs zero, one, or several times per frame so that it always advances the game by exactly fixed_timestep seconds each time it's called. """ pass
[docs] def render(self, surface: pygame.Surface) -> None: """ Empty. This should be implemented by the extension class. :param render_surface: The surface where you should render all of the game elements on. Its dimensions are virtual_width x virtual_height. """ pass
def __update(self, dt: float) -> None: """ Advance fixed_update() by as many fixed_timestep steps as the accumulated time covers, update the timer and the memoize cache registry, and call the method update that you should implement. """ self._accumulator += dt while self._accumulator >= self.fixed_timestep: self.fixed_update() self._accumulator -= self.fixed_timestep Timer.update(dt) Memo.update(dt) self.update(dt) def __render(self) -> None: """ Prepare screen for render and calls the method render that you should implement. """ self.render_surface.fill((0, 0, 0)) self.render(self.render_surface) self.screen.blit( pygame.transform.scale(self.render_surface, self.screen.get_size()), (0, 0) ) pygame.display.update()
[docs] def exec(self) -> None: """ Execute the game loop. """ self.running = True while self.running: for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() elif event.type in INPUT_EVENTS: InputHandler.handle_input(event) dt = self.clock.tick(self.fps) / 1000.0 self.__update(dt) self.__render() # pygame.quit() already uninitializes every subsystem # pygame.init() started (font and mixer included), so there's # no need to quit them individually first. pygame.quit()
[docs] def quit(self) -> None: """ Mark the game to exit and stop it from receiving further input, undoing the registration __init__ made with InputHandler. """ self.running = False InputHandler.unregister_listener(self)