"""
The main game module. It controls everything in the game.
"""
from __future__ import annotations
import sys
import sdl2, sdl2.ext, sdl2.sdlttf
from contextlib import suppress
from typing import TYPE_CHECKING
from . import Time, Display, Vector, Color, Input, Radio, Events, Font, Draw, Debug
if TYPE_CHECKING:
from . import SceneManager, Camera
[docs]class GameProperties(type):
"""
Defines static property methods for Game.
Warning:
This is only a metaclass for the class below it, so you wont be able to access this class.
To use the property methods here, simply access them as you would any other Game property.
"""
@property
def state(cls) -> int:
"""
The state of the game.
The game states are::
Game.RUNNING
Game.STOPPED
Game.PAUSED
"""
return cls._state
@state.setter
def state(cls, new: int):
cls._state = new
if cls._state == Game.STOPPED:
sdl2.SDL_PushEvent(sdl2.SDL_Event(sdl2.SDL_QUIT))
@property
def camera(cls) -> Camera:
"""
A shortcut getter allowing easy access to the current camera.
This is a get-only property.
Note:
Returns a pointer to the current camera object.
This is so you can access/change the current camera properties faster, but you'd still need to
use :func:`Game.scenes.current.camera <rubato.classes.scene.Scene.camera>` to access the camera directly.
Returns:
Camera: The current scene's camera
"""
return cls.scenes.current.camera
# THIS IS A STATIC CLASS
[docs]class Game(metaclass=GameProperties):
"""
The main game class.
Attributes:
name (str): The title of the game window.
scenes (SceneManager): The global scene manager.
background_color (Color): The background color of the window.
border_color (Color): The color of the borders of the window.
debug (bool): Turn on debug rendering for everything in the game.
"""
RUNNING = 1
STOPPED = 2
PAUSED = 3
name: str = ""
border_color: Color = Color(0, 0, 0)
background_color: Color = Color(255, 255, 255)
debug: bool = False
show_fps: bool = False
debug_font: Font
_state: int = STOPPED
scenes: SceneManager = None
initialized = False
[docs] @classmethod
def constant_loop(cls): # test: skip
"""
The constant game loop. Should only be called by :meth:`rubato.begin`.
"""
cls.state = cls.RUNNING
try:
while True:
cls.update()
except (Exception,) as e: # add possible exceptions here if there are more needed
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])
[docs] @classmethod
def update(cls): # test: skip
"""
The update loop for the game. Called automatically every frame.
Handles the game states.
Will always process timed calls.
"""
# start timing the update loop
frame_start = sdl2.SDL_GetTicks64()
# Event handling
for event in sdl2.ext.get_events():
sdl2.SDL_PumpEvents()
if event.type == sdl2.SDL_QUIT:
Radio.broadcast(Events.EXIT)
sdl2.sdlttf.TTF_Quit()
sdl2.SDL_Quit()
sys.exit()
if event.type == sdl2.SDL_WINDOWEVENT:
if event.window.event == sdl2.SDL_WINDOWEVENT_RESIZED:
Radio.broadcast(
Events.RESIZE, {
"width": event.window.data1,
"height": event.window.data2,
"old_width": Display.window_size.x,
"old_height": Display.window_size.y
}
)
Display.window_size = Vector(
event.window.data1,
event.window.data2,
)
if event.type in (sdl2.SDL_KEYDOWN, sdl2.SDL_KEYUP):
key_info, unicode = event.key.keysym, ""
with suppress(ValueError):
unicode = chr(key_info.sym)
if event.type == sdl2.SDL_KEYUP:
event_name = Events.KEYUP
else:
event_name = (Events.KEYDOWN, Events.KEYHOLD)[event.key.repeat]
Radio.broadcast(
event_name,
{
"key": Input.get_name(key_info.sym),
"unicode": unicode,
"code": int(key_info.sym),
"mods": key_info.mod,
},
)
if event.type in (sdl2.SDL_MOUSEBUTTONDOWN, sdl2.SDL_MOUSEBUTTONUP):
mouse_button = None
if event.button.state == sdl2.SDL_BUTTON_LEFT:
mouse_button = "mouse 1"
elif event.button.state == sdl2.SDL_BUTTON_MIDDLE:
mouse_button = "mouse 2"
elif event.button.state == sdl2.SDL_BUTTON_RIGHT:
mouse_button = "mouse 3"
elif event.button.state == sdl2.SDL_BUTTON_X1:
mouse_button = "mouse 4"
elif event.button.state == sdl2.SDL_BUTTON_X2:
mouse_button = "mouse 5"
if event.type == sdl2.SDL_MOUSEBUTTONUP:
event_name = Events.MOUSEUP
else:
event_name = Events.MOUSEDOWN
#
Radio.broadcast(
event_name,
{
"mouse_button": mouse_button,
"x": event.button.x,
"y": event.button.y,
"clicks": event.button.clicks,
"which": event.button.which,
"windowID": event.button.windowID,
"timestamp": event.button.timestamp,
},
)
# process delayed calls
Time.process_calls()
if cls.state == Game.PAUSED:
# process user set pause update
cls.scenes.paused_update()
else:
# fixed update
Time.physics_counter += Time.delta_time
while Time.physics_counter >= Time.fixed_delta:
if cls.state != Game.PAUSED:
cls.scenes.fixed_update()
Time.physics_counter -= Time.fixed_delta
# normal update
cls.scenes.update()
# Draw Loop
Display.renderer.clear(cls.border_color.to_tuple())
Display.renderer.fill(
(0, 0, *Display.renderer.logical_size),
cls.background_color.to_tuple(),
)
cls.scenes.draw()
Debug.clear_queue()
if cls.show_fps:
fs = str(int(Time.smooth_fps))
h = Display.res.y // 40
p = h // 4
p2 = p + p
Draw.rect(
Vector(p2 + (h * len(fs)) / 2, p2 + h / 2),
h * len(fs) + p2,
h + p2,
Color(a=180),
fill=Color(a=180),
)
Draw.text(fs, font=cls.debug_font, pos=Vector(p2, p2), align=Vector(1, 1))
# update renderers
Display.renderer.present()
# use delay to cap the fps if need be
if Time.capped:
delay = Time.normal_delta - (1000 * Time.delta_time)
if delay > 0:
sdl2.SDL_Delay(int(delay))
# dont allow updates to occur more than once in a millisecond
# this will likely never occur but is a failsafe
while sdl2.SDL_GetTicks64() == frame_start:
sdl2.SDL_Delay(1)
# clock the time the update call took
Time.delta_time = (sdl2.SDL_GetTicks64() - frame_start) / 1000