Source code for rubato.game
"""
An abstraction surrounding the main game loop in rubato.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sdl2, sdl2.sdlttf
import sys
from . import Time, Display, Radio, Events, Font, PrintError, IdError, Draw, InitError, Camera
if TYPE_CHECKING:
from . import Scene
# THIS IS A STATIC CLASS
[docs]class Game:
"""
A static class controlling rubato game flow.
"""
RUNNING = 1
STOPPED = 2
PAUSED = 3
debug: bool = False
"""Whether to use debug-mode."""
show_fps: bool = False
"""Whether to show fps."""
debug_font: Font
"""What font to draw debug text in."""
state: int = STOPPED
"""
The state of the game. The game states are::
Game.RUNNING
Game.STOPPED
Game.PAUSED
"""
_initialized = False
_scenes: dict[str, Scene] = {}
_scene_id: int = 0
_current: str = ""
_zero_cam: Camera = Camera()
def __init__(self) -> None:
raise InitError(self)
[docs] @classmethod
def current(cls) -> Scene:
"""
The current scene of the game.
Returns:
The current Scene.
"""
scene = cls._scenes.get(cls._current)
if scene:
return scene
raise ValueError("The current scene is invalid or not set. Make sure to create a scene or switch to it.")
[docs] @classmethod
def set_scene(cls, scene_id: str):
"""
Changes the current scene. Takes effect on the next frame.
Args:
scene_id: The id of the new scene.
"""
cls._current = scene_id
cls.current().on_switch()
@classmethod
def _add(cls, scene: Scene, name: str | None) -> str: # test: skip
"""
Add a scene to the game. Also set the current scene if this is the first added scene.
Args:
scene: The scene to add.
name: The name of the scene. If None, a unique name is generated.
Raises:
IdError: The given scene id is already used.
"""
if name is None:
name = "scene" + str(cls._scene_id)
if name in cls._scenes:
raise IdError(f"A scene with name '{name}' has already been added.")
cls._scenes[name] = scene
if len(cls._scenes) == 1:
cls.set_scene(name)
cls._scene_id += 1
return name
[docs] @classmethod
def quit(cls):
"""Quit the game and close the python process."""
Radio.broadcast(Events.EXIT)
cls.state = cls.STOPPED
sdl2.sdlttf.TTF_Quit()
sdl2.SDL_Quit()
sys.exit(0)
@classmethod
def _start(cls) -> None:
"""
Starts the main game loop. Called automatically by :meth:`rubato.begin`.
"""
cls.state = cls.RUNNING
try:
while True:
cls._tick()
except KeyboardInterrupt:
cls.quit()
except PrintError as e:
raise e
except (Exception,) as e:
raise type(e)(
str(e) + "\nRubato Error-ed. Was it our fault? Issue tracker: "
"https://github.com/rubatopy/rubato/issues"
).with_traceback(sys.exc_info()[2])
@classmethod
def _tick(cls):
# start a new frame
Time._start_frame()
if cls.state == cls.STOPPED:
sdl2.SDL_PushEvent(sdl2.SDL_Event(sdl2.SDL_QUIT))
# Pump SDL events
sdl2.SDL_PumpEvents()
# Event handling
if Radio._handle():
cls.quit()
# process delayed calls
Time._process_calls()
cls.update()
curr = cls._scenes.get(cls._current)
if curr: # pylint: disable=using-constant-test
if cls.state == Game.PAUSED:
# process user set pause update
curr._paused_update()
else:
# normal update
curr._update()
# fixed update
Time._physics_counter += Time.delta_time
while Time._physics_counter >= Time.fixed_delta:
curr._fixed_update()
Time._physics_counter -= Time.fixed_delta
curr._draw()
else:
Draw.clear()
cls.draw()
Draw._dump()
if cls.show_fps:
Draw._draw_fps(cls.debug_font)
# update renderers
Display.renderer.present()
# end frame
Time._end_frame()
[docs] @staticmethod
def update(): # test: skip
"""An overrideable method for updating the game. Called once per frame, before the current scene updates."""
pass
[docs] @staticmethod
def draw(): # test: skip
"""An overrideable method for drawing the game. Called once per frame, before the draw queue is dumped."""
pass