--- a/sge/dsp.py +++ b/sge/dsp.py @@ -22,9 +22,11 @@ "fullscreen_mode_ok"] +import asyncio import math import os import sys +import time import traceback import warnings @@ -463,6 +465,12 @@ self.input_events = [] self.current_room = None + self._last_tick = None + self._was_paused = False + self._pause_sprite = None + # Hooks for add-ons that run modal dialogs (xsge_gui). + self.modal_active = lambda: False + self.modal_step = lambda time_passed: None r.game_display_surface = pygame.Surface((self.width, self.height)) _set_mode() @@ -470,6 +478,7 @@ r.music = None r.music_queue = [] r.game_running = False + r.game_paused = False r.game_clock = pygame.time.Clock() r.game_window_projections = [] self.mouse = Mouse() @@ -492,17 +501,34 @@ self.window_icon = None - def start(self): + async def start(self): """ Start the game. Should only be called once; the effect of any further calls is undefined. + + Web port (pygbag): this is a coroutine, and it yields to the + browser once per frame. ``pause`` and the xsge_gui dialogs no + longer run loops of their own; they are stepped from here. """ if self.start_room is not None: r.game_running = True self.start_room.start() - r.game_clock.tick() + self._tick_clock() while r.game_running: + # A paused game or a modal dialog takes the whole frame. + if r.game_paused: + await self._paused_frame() + continue + if self._was_paused: + self._was_paused = False + self.pump_input() + self.input_events = [] + if self.modal_active(): + time_passed = await self._wait_frame() + self.modal_step(time_passed) + continue + # Switch to new room (if one has been started) new_room = r.game_new_room if new_room is not None: @@ -538,7 +564,7 @@ # Prevent sudden movements from happening at the # start of a room due to delta timing, and make sure # transitions happen fully. - r.game_clock.tick() + self._tick_clock() # Input events self.pump_input() @@ -658,7 +684,7 @@ self.event_close() # Regulate speed - real_time_passed = self.regulate_speed() + real_time_passed = await self._wait_frame() if self.delta: time_passed = min(real_time_passed, 1000 / self.delta_min) @@ -777,148 +803,149 @@ font = gfx.Font("Droid Sans", size=64) sprite = gfx.Sprite.from_text(font, "Paused") + self._pause_sprite = sprite + self._was_paused = True r.game_paused = True - while r.game_paused and r.game_running: - # Input events - self.pump_input() - while self.input_events: - event = self.input_events.pop(0) - - if isinstance(event, sge.input.KeyPress): - self.event_paused_key_press(event.key, event.char) - self.current_room.event_paused_key_press(event.key, - event.char) - for obj in self.current_room.objects: - obj.event_paused_key_press(event.key, event.char) - elif isinstance(event, sge.input.KeyRelease): - self.event_paused_key_release(event.key) - self.current_room.event_paused_key_release(event.key) - for obj in self.current_room.objects: - obj.event_paused_key_release(event.key) - elif isinstance(event, sge.input.MouseMove): - self.event_paused_mouse_move(event.x, event.y) - self.current_room.event_paused_mouse_move(event.x, event.y) - for obj in self.current_room.objects: - obj.event_paused_mouse_move(event.x, event.y) - elif isinstance(event, sge.input.MouseButtonPress): - self.event_paused_mouse_button_press(event.button) - self.current_room.event_paused_mouse_button_press( - event.button) - for obj in self.current_room.objects: - obj.event_paused_mouse_button_press(event.button) - elif isinstance(event, sge.input.MouseButtonRelease): - self.event_paused_mouse_button_release(event.button) - self.current_room.event_paused_mouse_button_release( - event.button) - for obj in self.current_room.objects: - obj.event_paused_mouse_button_release(event.button) - elif isinstance(event, sge.input.MouseWheelMove): - self.event_paused_mouse_wheel_move(event.x, event.y) - self.current_room.event_paused_mouse_wheel_move(event.x, - event.y) - for obj in r._active_objects.copy(): - obj.event_paused_mouse_wheel_move(event.x, event.y) - elif isinstance(event, sge.input.JoystickAxisMove): - self.event_paused_joystick_axis_move( - event.js_name, event.js_id, event.axis, event.value) - self.current_room.event_paused_joystick_axis_move( - event.js_name, event.js_id, event.axis, event.value) - for obj in self.current_room.objects: - obj.event_paused_joystick_axis_move( - event.js_name, event.js_id, event.axis, - event.value) - elif isinstance(event, sge.input.JoystickHatMove): - self.event_paused_joystick_hat_move( - event.js_name, event.js_id, event.hat, event.x, - event.y) - self.current_room.event_paused_joystick_hat_move( - event.js_name, event.js_id, event.hat, event.x, - event.y) - for obj in self.current_room.objects: - obj.event_paused_joystick_hat_move( - event.js_name, event.js_id, event.hat, - event.x, event.y) - elif isinstance(event, sge.input.JoystickTrackballMove): - self.event_paused_joystick_trackball_move( - event.js_name, event.js_id, event.ball, event.x, - event.y) - self.current_room.event_paused_joystick_trackball_move( - event.js_name, event.js_id, event.ball, event.x, - event.y) - for obj in self.current_room.objects: - obj.event_paused_joystick_trackball_move( - event.js_name, event.js_id, event.ball, - event.x, event.y) - elif isinstance(event, sge.input.JoystickButtonPress): - self.event_paused_joystick_button_press( - event.js_name, event.js_id, event.button) - self.current_room.event_paused_joystick_button_press( - event.js_name, event.js_id, event.button) - for obj in self.current_room.objects: - obj.event_paused_joystick_button_press( - event.js_name, event.js_id, event.button) - elif isinstance(event, sge.input.JoystickButtonRelease): - self.event_paused_joystick_button_release( + async def _paused_frame(self): + # One iteration of what used to be the loop of pause(). + sprite = self._pause_sprite + # Input events + self.pump_input() + while self.input_events: + event = self.input_events.pop(0) + + if isinstance(event, sge.input.KeyPress): + self.event_paused_key_press(event.key, event.char) + self.current_room.event_paused_key_press(event.key, + event.char) + for obj in self.current_room.objects: + obj.event_paused_key_press(event.key, event.char) + elif isinstance(event, sge.input.KeyRelease): + self.event_paused_key_release(event.key) + self.current_room.event_paused_key_release(event.key) + for obj in self.current_room.objects: + obj.event_paused_key_release(event.key) + elif isinstance(event, sge.input.MouseMove): + self.event_paused_mouse_move(event.x, event.y) + self.current_room.event_paused_mouse_move(event.x, event.y) + for obj in self.current_room.objects: + obj.event_paused_mouse_move(event.x, event.y) + elif isinstance(event, sge.input.MouseButtonPress): + self.event_paused_mouse_button_press(event.button) + self.current_room.event_paused_mouse_button_press( + event.button) + for obj in self.current_room.objects: + obj.event_paused_mouse_button_press(event.button) + elif isinstance(event, sge.input.MouseButtonRelease): + self.event_paused_mouse_button_release(event.button) + self.current_room.event_paused_mouse_button_release( + event.button) + for obj in self.current_room.objects: + obj.event_paused_mouse_button_release(event.button) + elif isinstance(event, sge.input.MouseWheelMove): + self.event_paused_mouse_wheel_move(event.x, event.y) + self.current_room.event_paused_mouse_wheel_move(event.x, + event.y) + for obj in r._active_objects.copy(): + obj.event_paused_mouse_wheel_move(event.x, event.y) + elif isinstance(event, sge.input.JoystickAxisMove): + self.event_paused_joystick_axis_move( + event.js_name, event.js_id, event.axis, event.value) + self.current_room.event_paused_joystick_axis_move( + event.js_name, event.js_id, event.axis, event.value) + for obj in self.current_room.objects: + obj.event_paused_joystick_axis_move( + event.js_name, event.js_id, event.axis, + event.value) + elif isinstance(event, sge.input.JoystickHatMove): + self.event_paused_joystick_hat_move( + event.js_name, event.js_id, event.hat, event.x, + event.y) + self.current_room.event_paused_joystick_hat_move( + event.js_name, event.js_id, event.hat, event.x, + event.y) + for obj in self.current_room.objects: + obj.event_paused_joystick_hat_move( + event.js_name, event.js_id, event.hat, + event.x, event.y) + elif isinstance(event, sge.input.JoystickTrackballMove): + self.event_paused_joystick_trackball_move( + event.js_name, event.js_id, event.ball, event.x, + event.y) + self.current_room.event_paused_joystick_trackball_move( + event.js_name, event.js_id, event.ball, event.x, + event.y) + for obj in self.current_room.objects: + obj.event_paused_joystick_trackball_move( + event.js_name, event.js_id, event.ball, + event.x, event.y) + elif isinstance(event, sge.input.JoystickButtonPress): + self.event_paused_joystick_button_press( + event.js_name, event.js_id, event.button) + self.current_room.event_paused_joystick_button_press( + event.js_name, event.js_id, event.button) + for obj in self.current_room.objects: + obj.event_paused_joystick_button_press( event.js_name, event.js_id, event.button) - self.current_room.event_paused_joystick_button_release( + elif isinstance(event, sge.input.JoystickButtonRelease): + self.event_paused_joystick_button_release( + event.js_name, event.js_id, event.button) + self.current_room.event_paused_joystick_button_release( + event.js_name, event.js_id, event.button) + for obj in self.current_room.objects: + obj.event_paused_joystick_button_release( event.js_name, event.js_id, event.button) - for obj in self.current_room.objects: - obj.event_paused_joystick_button_release( - event.js_name, event.js_id, event.button) - elif isinstance(event, sge.input.JoystickEvent): - self.event_paused_joystick( - event.js_name, event.js_id, event.input_type, - event.input_id, event.value) - self.current_room.event_paused_joystick( + elif isinstance(event, sge.input.JoystickEvent): + self.event_paused_joystick( + event.js_name, event.js_id, event.input_type, + event.input_id, event.value) + self.current_room.event_paused_joystick( + event.js_name, event.js_id, event.input_type, + event.input_id, event.value) + for obj in r._active_objects.copy(): + obj.event_paused_joystick( event.js_name, event.js_id, event.input_type, event.input_id, event.value) - for obj in r._active_objects.copy(): - obj.event_paused_joystick( - event.js_name, event.js_id, event.input_type, - event.input_id, event.value) - elif isinstance(event, sge.input.KeyboardFocusGain): - self.event_paused_gain_keyboard_focus() - self.current_room.event_paused_gain_keyboard_focus() - elif isinstance(event, sge.input.KeyboardFocusLose): - self.event_paused_lose_keyboard_focus() - self.current_room.event_paused_lose_keyboard_focus() - elif isinstance(event, sge.input.MouseFocusGain): - self.event_paused_gain_mouse_focus() - self.current_room.event_paused_gain_mouse_focus() - elif isinstance(event, sge.input.MouseFocusLose): - self.event_paused_lose_mouse_focus() - self.current_room.event_paused_lose_mouse_focus() - elif isinstance(event, sge.input.WindowResize): - self.event_paused_window_resize() - elif isinstance(event, sge.input.QuitRequest): - self.current_room.event_paused_close() - self.event_paused_close() - - # Regulate speed - time_passed = self.regulate_speed() - - if self.delta: - t = min(time_passed, 1000 / self.delta_min) - delta_mult = t / (1000 / self.fps) - else: - delta_mult = 1 + elif isinstance(event, sge.input.KeyboardFocusGain): + self.event_paused_gain_keyboard_focus() + self.current_room.event_paused_gain_keyboard_focus() + elif isinstance(event, sge.input.KeyboardFocusLose): + self.event_paused_lose_keyboard_focus() + self.current_room.event_paused_lose_keyboard_focus() + elif isinstance(event, sge.input.MouseFocusGain): + self.event_paused_gain_mouse_focus() + self.current_room.event_paused_gain_mouse_focus() + elif isinstance(event, sge.input.MouseFocusLose): + self.event_paused_lose_mouse_focus() + self.current_room.event_paused_lose_mouse_focus() + elif isinstance(event, sge.input.WindowResize): + self.event_paused_window_resize() + elif isinstance(event, sge.input.QuitRequest): + self.current_room.event_paused_close() + self.event_paused_close() + + # Regulate speed + time_passed = await self._wait_frame() + + if self.delta: + t = min(time_passed, 1000 / self.delta_min) + delta_mult = t / (1000 / self.fps) + else: + delta_mult = 1 - self.event_paused_step(time_passed, delta_mult) - self.current_room.event_paused_step(time_passed, delta_mult) - for obj in self.current_room.objects[:]: - obj.event_paused_step(time_passed, delta_mult) - - # Project sprite - x = (self.width - sprite.width) / 2 - y = (self.height - sprite.height) / 2 - self.project_sprite(sprite, 0, x, y) + self.event_paused_step(time_passed, delta_mult) + self.current_room.event_paused_step(time_passed, delta_mult) + for obj in self.current_room.objects[:]: + obj.event_paused_step(time_passed, delta_mult) + + # Project sprite + x = (self.width - sprite.width) / 2 + y = (self.height - sprite.height) / 2 + self.project_sprite(sprite, 0, x, y) - # Refresh - self.refresh() - - self.pump_input() - self.input_events = [] + # Refresh + self.refresh() def unpause(self): """Unpause the game.""" @@ -1179,13 +1206,42 @@ else: fps = self.fps - tp = r.game_clock.tick(fps) + # Web port: never sleep (it would freeze the browser tab); the + # main loop waits with _wait_frame() instead. Code that still + # calls this directly just gets the time passed. + tp = self._tick_clock() + self._prune_cache(tp) + return tp + def _tick_clock(self): + # Milliseconds since the previous call (what Clock.tick() gave). + now = time.perf_counter() * 1000 + last = self._last_tick + self._last_tick = now + return 0 if last is None else int(now - last) + + def _prune_cache(self, tp): r.cache.prune_time += tp if r.cache.prune_time >= r.CACHE_PRUNE_TIME: r.cache.prune_time = 0 r.cache.prune() + async def _wait_frame(self, fps=None): + # Cooperative version of regulate_speed(): yield to the browser + # until a frame's worth of time has passed. + if fps is None: + if self.delta and self.delta_max is not None: + fps = self.delta_max + else: + fps = self.fps + target = 1000 / fps + last = self._last_tick + while True: + await asyncio.sleep(0) + if last is None or (time.perf_counter() * 1000 - last) >= target - 1: + break + tp = self._tick_clock() + self._prune_cache(tp) return tp def refresh(self):