From fe0709b74558137fffd59ec0cf0b78937aa9e61d Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 24 Dec 2023 18:11:44 +1300 Subject: [PATCH 01/10] Started working on Clock and Timer classes --- arcade/experimental/clock/clock.py | 73 +++++++++++++++++++++++ arcade/experimental/clock/clock_window.py | 0 arcade/experimental/clock/timer.py | 36 +++++++++++ 3 files changed, 109 insertions(+) create mode 100644 arcade/experimental/clock/clock.py create mode 100644 arcade/experimental/clock/clock_window.py create mode 100644 arcade/experimental/clock/timer.py diff --git a/arcade/experimental/clock/clock.py b/arcade/experimental/clock/clock.py new file mode 100644 index 000000000..533f62a48 --- /dev/null +++ b/arcade/experimental/clock/clock.py @@ -0,0 +1,73 @@ +from typing import Optional, Set, Callable, List, Dict, Any, TYPE_CHECKING + +if TYPE_CHECKING: + from arcade.experimental.clock.timer import Timer + + + +class Clock: + """ + A clock for managing elapsed time, delta time, and timers + + A clock has a tick method that can either be called directly + or is called by its parent clock. If a clock has a parent + DO NOT CALL its tick method. + + A clock can have any number of child timers or clocks. + + The tick speed of a clock is how fast time elapses for it. + + When a clock calls tick on its children it passes its modified delta_time + + Arcade's clocks are synchronous. No matter how long an update takes + all queries to a clock's elapsed time will be the same. This ensures + that two objects who have the same lifespan and are created at the + same time will die on the same frame in the future. + """ + + def __init__(self, *, + tick_speed: float = 1.0, + initial_elapsed_time: float = 0.0, + initial_tick_count: int = 0, + parent: Optional["Clock"] = None + ): + self._tick_speed: float = tick_speed + + self._elapsed_time: float = initial_elapsed_time + self._tick_count: int = initial_tick_count + + self._parent: Optional[Clock] = parent + + self._children: Set[Clock] = set() + self._timers: Set[Timer] = set() + + self._delta_time_raw: float = 0.0 + + def tick(self, delta_time: float): + self._tick_count += 1 + self._delta_time_raw = delta_time + + self._elapsed_time = self._delta_time_raw * self._tick_speed + + for child in tuple(self._children): + child.tick(self._delta_time_raw * self._tick_speed) + + for timer in tuple(self._timers): + if timer.complete: + timer.kill() + timer.check() + + def create_new_child(self, *, + tick_speed: float = 1.0, + inherit_elapsed: bool = False, + inherit_count: bool = False, + lifespan: float = 0.0 + ): + pass + + def create_new_timer(self, duration: float, callback: Callable, *, + callback_args: Optional[List[Any]] = None, + callback_kwargs: Optional[Dict[str, Any]] = None + ): + args = callback_args or list() + kwargs = callback_kwargs or dict() diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py new file mode 100644 index 000000000..e69de29bb diff --git a/arcade/experimental/clock/timer.py b/arcade/experimental/clock/timer.py new file mode 100644 index 000000000..1db6f1436 --- /dev/null +++ b/arcade/experimental/clock/timer.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from arcade.experimental.clock.clock import Clock + + +class Timer: + """ + A basic timer class which can be used to 'set and forget' a method call. + If you simply want to know how long since an event has elapsed you can + interact with arcade's clocks. + + Timers overlap heavily with pyglet's own clock system. If you want to + schedule a function that is irrespective of the arcade's clock time then + using pyglet's clock may better suit your needs. + + Arcade's clocks assure simultaneous timers. If two bullets are created on + the same frame they will both treat it as though they were spawned at the + same time. This is not true for events created by Pyglet's clocks. + """ + + @property + def complete(self): + return False + + def kill(self, no_call: bool = False): + pass + + def check(self, auto_kill: bool = True): + pass + + def arg_override(self, *args): + pass + + def kwarg_override(self, **kwargs): + pass From 987a23259e07029a67212e4628016232fde7988b Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Dec 2023 12:33:16 +1300 Subject: [PATCH 02/10] Mostly finished clock and timer --- arcade/experimental/clock/clock.py | 101 +++++++++++++++++--- arcade/experimental/clock/timer.py | 144 ++++++++++++++++++++++++++--- 2 files changed, 218 insertions(+), 27 deletions(-) diff --git a/arcade/experimental/clock/clock.py b/arcade/experimental/clock/clock.py index 533f62a48..5d870e59d 100644 --- a/arcade/experimental/clock/clock.py +++ b/arcade/experimental/clock/clock.py @@ -1,8 +1,5 @@ -from typing import Optional, Set, Callable, List, Dict, Any, TYPE_CHECKING - -if TYPE_CHECKING: - from arcade.experimental.clock.timer import Timer - +from typing import Optional, Set, Callable +from arcade.experimental.clock.timer import Timer class Clock: @@ -14,6 +11,8 @@ class Clock: DO NOT CALL its tick method. A clock can have any number of child timers or clocks. + The children and timers are stored in unordered sets, therefore + the properties which return them make no promises on the order. The tick speed of a clock is how fast time elapses for it. @@ -29,9 +28,11 @@ def __init__(self, *, tick_speed: float = 1.0, initial_elapsed_time: float = 0.0, initial_tick_count: int = 0, + frozen: bool = False, parent: Optional["Clock"] = None ): - self._tick_speed: float = tick_speed + self.tick_speed: float = tick_speed + self._frozen: bool = frozen self._elapsed_time: float = initial_elapsed_time self._tick_count: int = initial_tick_count @@ -45,12 +46,14 @@ def __init__(self, *, def tick(self, delta_time: float): self._tick_count += 1 + if self._frozen: + return self._delta_time_raw = delta_time - self._elapsed_time = self._delta_time_raw * self._tick_speed + self._elapsed_time = self._delta_time_raw * self.tick_speed for child in tuple(self._children): - child.tick(self._delta_time_raw * self._tick_speed) + child.tick(self._delta_time_raw * self.tick_speed) for timer in tuple(self._timers): if timer.complete: @@ -62,12 +65,80 @@ def create_new_child(self, *, inherit_elapsed: bool = False, inherit_count: bool = False, lifespan: float = 0.0 - ): + ) -> "Clock": + pass + + def create_new_timer(self, duration: float, callback: Callable, + *args, + **kwargs + ) -> Timer: + pass + + def add_clock(self, new_child: "Clock"): + pass + + + def add_timer(self, new_timer: Timer): + pass + + def free(self): + if self._parent: + self._parent.pop_child(self) + + def time_since(self, start: float): + return self._elapsed_time - start + + @property + def delta_time(self): + return self._delta_time_raw * self._tick_speed + + @property + def delta_time_raw(self): + return self._delta_time_raw + + @property + def elapsed(self): + return self._elapsed_time + + @property + def frozen(self): + return self._frozen + + def freeze(self): + self._frozen = True + + def unfreeze(self): + self._frozen = False + + def toggle_frozen(self) -> bool: + self._frozen = bool(1 - self._frozen) + return self._frozen + + @property + def children(self): + return tuple(self._children) + + def pop_child(self, child: "Clock"): + pass + + @property + def timers(self): + return tuple(self._timers) + + @property + def pop_timer(self, timer: Timer): + """ + Popping a timer allows you to remove a timer from a clock without destroying the timer. + """ + pass + + @property + def parent(self): + return self._parent + + def transfer_parent(self, new_parent): pass - def create_new_timer(self, duration: float, callback: Callable, *, - callback_args: Optional[List[Any]] = None, - callback_kwargs: Optional[Dict[str, Any]] = None - ): - args = callback_args or list() - kwargs = callback_kwargs or dict() + @property + def tick_count(self): + return self._tick_count \ No newline at end of file diff --git a/arcade/experimental/clock/timer.py b/arcade/experimental/clock/timer.py index 1db6f1436..633f3dd22 100644 --- a/arcade/experimental/clock/timer.py +++ b/arcade/experimental/clock/timer.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING +import weakref +from typing import Callable, List, Dict, Any, Optional, TYPE_CHECKING if TYPE_CHECKING: from arcade.experimental.clock.clock import Clock @@ -10,27 +11,146 @@ class Timer: If you simply want to know how long since an event has elapsed you can interact with arcade's clocks. - Timers overlap heavily with pyglet's own clock system. If you want to + Timers overlap heavily with Pyglet's own clock system. If you want to schedule a function that is irrespective of the arcade's clock time then - using pyglet's clock may better suit your needs. + using Pyglet's clock may better suit your needs. Arcade's clocks assure simultaneous timers. If two bullets are created on the same frame they will both treat it as though they were spawned at the same time. This is not true for events created by Pyglet's clocks. + + The timer makes no implicit expectations about the flow of time. Setting + a negative duration will make the timer expect the flow of time to be + reversed. If time is moving in the opposite direction to what the timer + expects it will not be active. + + If you want to get information from the timer and pass it to the function + you can. The possible options are: + 'duration' which gives the duration of the timer + 'elapsed' which gives the currently elapsed time of the parent clock + 'tick_count' which gives the tick_count of the parent clock + 'tick_speed' which gives the tick speed of the parent clock + 'timer' which gives the timer making the callback + 'clock' which gives the parent of the timer making the callback + simply add the option as a kwarg equal to none. ensure your callback function + accepts any you want to use as a kwarg. """ + def __init__(self, duration: float, parent: "Clock", callback: Callable, *args, + delay: float = 0.0, + reusable: bool = False, + **kwargs + ): + self._parent: "Clock" = parent + + self._duration: float = duration + self._delay: float = delay + + self._start_time: float = parent.elapsed + delay + + self._complete: bool = False + + self._callback: Optional[Callable] = weakref.proxy(callback) + self._args: List[Any] = args or list() + self._kwargs: Dict[str, Any] = kwargs or dict() + + self.reusable: bool = reusable + + def kill(self): + self.free() + self._start_time = 0.0 + + if not self.reusable: + self._duration = 0.0 + self._delay = 0.0 + self._callback = None + self._args = list() + self._kwargs = dict() + + def check(self, auto_kill: bool = True, auto_call: bool = True): + if not self._parent or not self._duration or not self.active: + return + + self._complete = abs(self._duration) <= abs(self._parent.time_since(self._start_time)) + + if not self._complete: + return + + if auto_call: + self.call() + + if auto_kill: + self.kill() + + def reuse(self, + new_parent: "Clock", + new_duration: Optional[float] = None, + new_delay: Optional[float] = None, + ): + new_parent.add_timer(self) + + self._duration = new_duration or self._duration + self._delay = new_delay or self._delay + + self._start_time = self._parent.elapsed + self._delay + + def arg_update(self, arg_index: int, value: Any): + self._args[arg_index] = value + + def args_override(self, *args): + self._args = args + + def kwarg_update(self, **kwargs): + self._kwargs = self._kwargs | kwargs + + def kwarg_override(self, **kwargs): + self._kwargs = kwargs + + def call(self, force_complete: bool = True): + if force_complete and not self._complete: + raise ValueError("The timer has not completed, but force_complete is active") + + if not self._callback: + return + + if 'duration' in self._kwargs and self._kwargs['duration'] is None: + self._kwargs['duration'] = self._duration + if 'timer' is self._kwargs and self._kwargs['timer'] is None: + self._kwargs['timer'] = self + + if self._parent: + if 'elapsed' in self._kwargs and self._kwargs['elapsed'] is None: + self._kwargs['elapsed'] = self._parent.elapsed + if 'tick_count' in self._kwargs and self._kwargs['tick_count'] is None: + self._kwargs['tick_count'] = self._parent.tick_count + if 'tick_speed' in self._kwargs and self._kwargs['tick_speed'] is None: + self._kwargs['tick_speed'] = self._parent.tick_speed + if 'clock' in self._kwargs and self._kwargs['clock'] is None: + self._kwargs['clock'] = self._parent + + self._callback(*self._args, **self._kwargs) + + def free(self): + if self._parent: + self._parent.pop_timer(self) + @property def complete(self): - return False + return self._complete - def kill(self, no_call: bool = False): - pass + @property + def active(self) -> bool: + if not self._parent or not self._duration or self._complete: + return False - def check(self, auto_kill: bool = True): - pass + if self._duration < 0: + return self._parent.elapsed <= self._start_time + else: + return self._start_time <= self._parent.elapsed - def arg_override(self, *args): - pass + @property + def percentage(self): + if not self.active: + return 0.0 - def kwarg_override(self, **kwargs): - pass + return self._parent.time_since(self._start_time) / self._duration From 80b96eaab9cc84e2d214bd7c76db3128a2c34787 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Tue, 26 Dec 2023 12:38:12 +1300 Subject: [PATCH 03/10] Created the updated window class The fixed update implementation is created, but classes to access the clocks and other variables is still necessary. --- arcade/experimental/clock/clock_window.py | 1156 +++++++++++++++++++++ 1 file changed, 1156 insertions(+) diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index e69de29bb..2b73f642a 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -0,0 +1,1156 @@ +""" +The main window class that all object-oriented applications should +derive from. + +THIS IS AN EXPERIMENTAL VERSION OF THE BASE WINDOW +""" +from __future__ import annotations + +import logging +import os +import time +from typing import Tuple, Optional + +import pyglet + +import pyglet.gl as gl +import pyglet.window.mouse +from pyglet.canvas.base import ScreenMode + +import arcade +from arcade import get_display_size +from arcade import set_viewport +from arcade import set_window +from arcade import NoOpenGLException +from arcade.color import TRANSPARENT_BLACK +from arcade.context import ArcadeContext +from arcade.types import Color, RGBA255, RGBA255OrNormalized +from arcade import SectionManager +from arcade.utils import is_raspberry_pi + +# NEW IMPORTS +from arcade.experimental.clock.clock import Clock + +LOG = logging.getLogger(__name__) + +_window: 'Window' + +__all__ = [ + "Window", + "View" +] + + +class Window(pyglet.window.Window): + """ + The Window class forms the basis of most advanced games that use Arcade. + It represents a window on the screen, and manages events. + + .. _pyglet_pg_window_size_position: https://pyglet.readthedocs.io/en/latest/programming_guide/windowing.html#size-and-position + .. _pyglet_pg_window_style: https://pyglet.readthedocs.io/en/latest/programming_guide/windowing.html#window-style + + :param width: Window width + :param height: Window height + :param title: Title (appears in title bar) + :param fullscreen: Should this be full screen? + :param resizable: Can the user resize the window? + :param update_rate: How frequently to run the on_update event. + :param draw_rate: How frequently to run the on_draw event. (this is the FPS limit) + :param antialiasing: Should OpenGL's anti-aliasing be enabled? + :param gl_version: What OpenGL version to request. This is ``(3, 3)`` by default \ + and can be overridden when using more advanced OpenGL features. + :param screen: Pass a pyglet :py:class:`~pyglet.canvas.Screen` to + request the window be placed on it. See `pyglet's window size & + position guide `_ to learn more. + :param style: Request a non-default window style, such as borderless. + Some styles only work in certain situations. See `pyglet's guide + to window style `_ to learn more. + :param visible: Should the window be visible immediately + :param vsync: Wait for vertical screen refresh before swapping buffer \ + This can make animations and movement look smoother. + :param gc_mode: Decides how OpenGL objects should be garbage collected ("context_gc" (default) or "auto") + :param center_window: If true, will center the window. + :param samples: Number of samples used in antialiasing (default 4). \ + Usually this is 2, 4, 8 or 16. + :param enable_polling: Enabled input polling capability. This makes the ``keyboard`` and ``mouse`` \ + attributes available for use. + """ + + def __init__( + self, + width: int = 800, + height: int = 600, + title: Optional[str] = 'Arcade Window', + fullscreen: bool = False, + resizable: bool = False, + update_rate: float = 1 / 60, + antialiasing: bool = True, + gl_version: Tuple[int, int] = (3, 3), + screen: Optional[pyglet.canvas.Screen] = None, + style: Optional[str] = pyglet.window.Window.WINDOW_STYLE_DEFAULT, + visible: bool = True, + vsync: bool = False, + gc_mode: str = "context_gc", + center_window: bool = False, + samples: int = 4, + enable_polling: bool = True, + gl_api: str = "gl", + draw_rate: float = 1 / 60, + fixed_update_rate: float = 1/60, + max_update_count: int = 10 + ): + # In certain environments we can't have antialiasing/MSAA enabled. + # Detect replit environment + if os.environ.get("REPL_ID"): + antialiasing = False + + # Detect Raspberry Pi and switch to OpenGL ES 3.1 + if is_raspberry_pi(): + gl_version = 3, 1 + gl_api = "gles" + + #: bool: If this is a headless window + self.headless = pyglet.options.get("headless") is True + + config = None + # Attempt to make window with antialiasing + if antialiasing: + try: + config = pyglet.gl.Config( + major_version=gl_version[0], + minor_version=gl_version[1], + opengl_api=gl_api, + double_buffer=True, + sample_buffers=1, + samples=samples, + depth_size=24, + stencil_size=8, + red_size=8, + green_size=8, + blue_size=8, + alpha_size=8, + ) + display = pyglet.canvas.get_display() + screen = display.get_default_screen() + if screen: + config = screen.get_best_config(config) + except pyglet.window.NoSuchConfigException: + LOG.warning("Skipping antialiasing due missing hardware/driver support") + config = None + antialiasing = False + # If we still don't have a config + if not config: + config = pyglet.gl.Config( + major_version=gl_version[0], + minor_version=gl_version[1], + opengl_api=gl_api, + double_buffer=True, + depth_size=24, + stencil_size=8, + red_size=8, + green_size=8, + blue_size=8, + alpha_size=8, + ) + try: + super().__init__( + width=width, + height=height, + caption=title, + resizable=resizable, + config=config, + vsync=vsync, + visible=visible, + style=style, + ) + self.register_event_type('on_update') + self.register_event_type('on_fixed_update') + except pyglet.window.NoSuchConfigException: + raise NoOpenGLException("Unable to create an OpenGL 3.3+ context. " + "Check to make sure your system supports OpenGL 3.3 or higher.") + if antialiasing: + try: + gl.glEnable(gl.GL_MULTISAMPLE_ARB) + except pyglet.gl.GLException: + LOG.warning("Warning: Anti-aliasing not supported on this computer.") + + # Arcade provides two clocks which are ticked every update and fixed update. + self._update_clock: Clock = Clock() + self._fixed_clock: Clock = Clock() + + # We don't call the set_draw_rate function here because unlike the updates, the draw scheduling + # is initially set in the call to pyglet.app.run() that is done by the run() function. + # run() will pull this draw rate from the Window and use it. Calls to set_draw_rate only need + # to be done if changing it after the application has been started. + self._draw_rate = draw_rate + self._fixed_update_rate = fixed_update_rate + self.set_update_rate(update_rate) + + # If updating physics takes longer than the time step it represents the engine can start to take + # longer and longer every frame. This caps the number of fixed updates that can happen. + self._max_update_count = max_update_count + + self.set_vsync(vsync) + + super().set_fullscreen(fullscreen, screen) + # This used to be necessary on Linux, but no longer appears to be. + # With Pyglet 2.0+, setting this to false will not allow the screen to + # update. It does, however, cause flickering if creating a window that + # isn't derived from the Window class. + # self.invalid = False + set_window(self) + + self._current_view: Optional[View] = None + self.current_camera: Optional[arcade.SimpleCamera] = None + self.textbox_time = 0.0 + self.key: Optional[int] = None + self.flip_count: int = 0 + self.static_display: bool = False + + self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) + set_viewport(0, self.width, 0, self.height) + self._background_color: Color = TRANSPARENT_BLACK + + # See if we should center the window + if center_window: + self.center_window() + + if enable_polling: + self.keyboard = pyglet.window.key.KeyStateHandler() + + if pyglet.options["headless"]: + self.push_handlers(self.keyboard) + + else: + self.mouse = pyglet.window.mouse.MouseStateHandler() + self.push_handlers(self.keyboard, self.mouse) + else: + self.keyboard = None + self.mouse = None + + @property + def current_view(self) -> Optional["View"]: + """ + This property returns the current view being shown. + To set a different view, call the + :py:meth:`arcade.Window.show_view` method. + + """ + return self._current_view + + @property + def ctx(self) -> ArcadeContext: + """ + The OpenGL context for this window. + + :type: :py:class:`arcade.ArcadeContext` + """ + return self._ctx + + def clear( + self, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, + viewport: Optional[Tuple[int, int, int, int]] = None, + ): + """Clears the window with the configured background color + set through :py:attr:`arcade.Window.background_color`. + + :param color: (Optional) override the current background color + with one of the following: + + 1. A :py:class:`~arcade.types.Color` instance + 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) + 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values + :param Tuple[int, int, int, int] viewport: The viewport range to clear + """ + color = color if color is not None else self.background_color + self.ctx.screen.clear(color, normalized=normalized, viewport=viewport) + + @property + def background_color(self) -> Color: + """ + Get or set the background color for this window. + This affects what color the window will contain when + :py:meth:`~arcade.Window.clear()` is called. + + Examples:: + + # Use Arcade's built in Color values + window.background_color = arcade.color.AMAZON + + # Set the background color with a custom Color instance + MY_RED = arcade.types.Color(255, 0, 0) + window.background_color = MY_RED + + # Set the backgrund color directly from an RGBA tuple + window.background_color = 255, 0, 0, 255 + + # (Discouraged) + # Set the background color directly from an RGB tuple + # RGB tuples will assume 255 as the opacity / alpha value + window.background_color = 255, 0, 0 + + :type: Color + """ + return self._background_color + + @property + def accumulated_time(self): + return self._accumulated_time + + @property + def excess_fraction(self): + return self._excess_fraction + + # I am unsure we should provide access to the clocks directly. + # I suspect it would be a bad idea. We could also use inheritance to make + # a stripped down clock similar to the basic camera. + @property + def update_clock(self): + return self._update_clock + + @property + def fixed_update_clock(self): + return self._fixed_clock + + @background_color.setter + def background_color(self, value: RGBA255): + self._background_color = Color.from_iterable(value) + + def run(self) -> None: + """ + Run the main loop. + After the window has been set up, and the event hooks are in place, this is usually one of the last + commands on the main program. This is a blocking function starting pyglet's event loop + meaning it will start to dispatch events such as ``on_draw`` and ``on_update``. + """ + arcade.run() + + def close(self): + """ Close the Window. """ + super().close() + # Make sure we don't reference the window anymore + set_window(None) + pyglet.clock.unschedule(self._dispatch_updates) + + def set_fullscreen(self, + fullscreen: bool = True, + screen: Optional['Window'] = None, + mode: Optional[ScreenMode] = None, + width: Optional[float] = None, + height: Optional[float] = None): + """ + Set if we are full screen or not. + + :param fullscreen: + :param screen: Which screen should we display on? See :func:`get_screens` + :param mode: + The screen will be switched to the given mode. The mode must + have been obtained by enumerating `Screen.get_modes`. If + None, an appropriate mode will be selected from the given + `width` and `height`. + :param width: + :param height: + """ + super().set_fullscreen(fullscreen, screen, mode, width, height) + + def center_window(self) -> None: + """ + Center the window on the screen. + """ + # Get the display screen using pyglet + screen_width, screen_height = get_display_size() + + window_width, window_height = self.get_size() + # Center the window + self.set_location((screen_width - window_width) // 2, (screen_height - window_height) // 2) + + def on_update(self, delta_time: float): + """ + Perform game logic which does not need regular interval updates. + For stability do not put physics updates such as collisions here. + + :param delta_time: Time interval since the last time the function was called. + """ + pass + + def on_fixed_update(self, delta_time: float): + """ + Perform game logic which needs regular interval updates. Put sprite movement + collisions, and physics updates here. + + :param delta_time: Time interval since the last time the function was called. + """ + pass + + def on_draw(self): + """ + Override this function to add your custom drawing code. + """ + pass + + def _dispatch_updates(self, delta_time: float): + """ + Internal function that is scheduled with Pyglet's clock, this function gets run by the clock, and + dispatches the on_update events. + """ + + # accumulate time, but cap it to protect against death spirals. This will cause the game to run slower, + # but it's worth it for stability. If you are seeing slowdowns either decrease the fixed update rate or + # optimise you physics. + self._accumulated_time = min(self._accumulated_time + delta_time, + self._fixed_update_rate * self._max_update_count) + while self._accumulated_time >= self._fixed_update_rate: + self._fixed_clock.tick(self._fixed_update_rate) + self.dispatch_event('on_fixed_update', self._fixed_update_rate) + self._accumulated_time -= self._fixed_update_rate + self._excess_fraction = self._accumulated_time / self._fixed_update_rate + + self._update_clock.tick(delta_time) + self.dispatch_event('on_update', delta_time) + + def set_update_rate(self, rate: float): + """ + Set how often the on_update function should be dispatched. + For example, self.set_update_rate(1 / 60) will set the update rate to 60 times per second. + + :param rate: Update frequency in seconds + """ + self._update_rate = rate + pyglet.clock.unschedule(self._dispatch_updates) + pyglet.clock.schedule_interval(self._dispatch_updates, rate) + + def set_draw_rate(self, rate: float): + """ + Set how often the on_draw function should be run. + For example, set.set_draw_rate(1 / 60) will set the draw rate to 60 frames per second. + """ + self._draw_rate = rate + pyglet.clock.unschedule(pyglet.app.event_loop._redraw_windows) + pyglet.clock.schedule_interval(pyglet.app.event_loop._redraw_windows, self._draw_rate) + + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): + """ + Called repeatedly while the mouse is moving over the window. + + Override this function to respond to changes in mouse position. + + :param x: x position of mouse within the window in pixels + :param y: y position of mouse within the window in pixels + :param dx: Change in x since the last time this method was called + :param dy: Change in y since the last time this method was called + """ + pass + + def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): + """ + Called once whenever a mouse button gets pressed down. + + Override this function to handle mouse clicks. For an example of + how to do this, see arcade's built-in :ref:`aiming and shooting + bullets ` demo. + + .. seealso:: :meth:`~.Window.on_mouse_release` + + :param x: x position of the mouse + :param y: y position of the mouse + :param button: What button was pressed. This will always be + one of the following: + + * ``arcade.MOUSE_BUTTON_LEFT`` + * ``arcade.MOUSE_BUTTON_RIGHT`` + * ``arcade.MOUSE_BUTTON_MIDDLE`` + + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + pass + + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int): + """ + Called repeatedly while the mouse moves with a button down. + + Override this function to handle dragging. + + :param x: x position of mouse + :param y: y position of mouse + :param dx: Change in x since the last time this method was called + :param dy: Change in y since the last time this method was called + :param buttons: Which button is pressed + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + self.on_mouse_motion(x, y, dx, dy) + + def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): + """ + Called once whenever a mouse button gets released. + + Override this function to respond to mouse button releases. This + may be useful when you want to use the duration of a mouse click + to affect gameplay. + + :param x: x position of mouse + :param y: y position of mouse + :param button: What button was hit. One of: + arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT, + arcade.MOUSE_BUTTON_MIDDLE + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + pass + + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): + """ + Called repeatedly while a mouse scroll wheel moves. + + Override this function to respond to scroll events. The scroll + arguments may be positive or negative to indicate direction, but + the units are unstandardized. How many scroll steps you recieve + may vary wildly between computers depending a number of factors, + including system settings and the input devices used (i.e. mouse + scrollwheel, touchpad, etc). + + .. warning:: Not all users can scroll easily! + + Only some input devices support horizontal + scrolling. Standard vertical scrolling is common, + but some laptop touchpads are hard to use. + + This means you should be careful about how you use + scrolling. Consider making it optional + to maximize the number of people who can play your + game! + + :param x: x position of mouse + :param y: y position of mouse + :param scroll_x: number of steps scrolled horizontally + since the last call of this function + :param scroll_y: number of steps scrolled vertically since + the last call of this function + """ + pass + + def set_mouse_visible(self, visible: bool = True): + """ + Set whether to show the system's cursor while over the window + + By default, the system mouse cursor is visible whenever the + mouse is over the window. To hide the cursor, pass ``False`` to + this function. Pass ``True`` to make the cursor visible again. + + The window will continue receiving mouse events while the cursor + is hidden, including movements and clicks. This means that + functions like :meth:`~.Window.on_mouse_motion` and + t':meth:`~.Window.on_mouse_press` will continue to work normally. + + You can use this behavior to visually replace the system mouse + cursor with whatever you want. One example is :ref:`a game + character that is always at the most recent mouse position in + the window`. + + .. note:: Advanced users can try using system cursor state icons + + It may be possible to use system icons representing + cursor interaction states such as hourglasses or resize + arrows by using features ``arcade.Window`` inherits + from the underlying pyglet window class. See the + `pyglet overview on cursors + `_ + for more information. + + :param visible: Whether to hide the system mouse cursor + """ + super().set_mouse_visible(visible) + + def on_key_press(self, symbol: int, modifiers: int): + """ + Called once when a key gets pushed down. + + Override this function to add key press functionality. + + .. tip:: If you want the length of key presses to affect + gameplay, you also need to override + :meth:`~.Window.on_key_release`. + + :param symbol: Key that was just pushed down + :param modifiers: Bitwise 'and' of all modifiers (shift, + ctrl, num lock) active during this event. + See :ref:`keyboard_modifiers`. + """ + try: + self.key = symbol + except AttributeError: + pass + + def on_key_release(self, symbol: int, modifiers: int): + """ + Called once when a key gets released. + + Override this function to add key release functionality. + + Situations that require handling key releases include: + + * Rythm games where a note must be held for a certain + amount of time + * 'Charging up' actions that change strength depending on + how long a key was pressed + * Showing which keys are currently pressed down + + :param symbol: Key that was just released + :param modifiers: Bitwise 'and' of all modifiers (shift, + ctrl, num lock) active during this event. + See :ref:`keyboard_modifiers`. + """ + try: + self.key = None + except AttributeError: + pass + + def on_resize(self, width: int, height: int): + """ + Override this function to add custom code to be called any time the window + is resized. The main responsibility of this method is updating + the projection and the viewport. + + If you are not changing the default behavior when overriding, make sure + you call the parent's ``on_resize`` first:: + + def on_resize(self, width: int, height: int): + super().on_resize(width, height) + # Add extra resize logic here + + :param width: New width + :param height: New height + """ + # NOTE: When a second window is opened pyglet will + # dispatch on_resize during the window constructor. + # The arcade context is not created at that time + if hasattr(self, "_ctx"): + # Retain projection scrolling if applied + original_viewport = self._ctx.projection_2d + self.set_viewport( + original_viewport[0], + original_viewport[0] + width, + original_viewport[2], + original_viewport[2] + height + ) + + def set_min_size(self, width: int, height: int): + """ Wrap the Pyglet window call to set minimum size + + :param width: width in pixels. + :param height: height in pixels. + """ + + if self._resizable: + super().set_minimum_size(width, height) + else: + raise ValueError('Cannot set min size on non-resizable window') + + def set_max_size(self, width: int, height: int): + """ Wrap the Pyglet window call to set maximum size + + :param width: width in pixels. + :param height: height in pixels. + :Raises ValueError: + + """ + + if self._resizable: + super().set_maximum_size(width, height) + else: + raise ValueError('Cannot set max size on non-resizable window') + + def set_size(self, width: int, height: int): + """ + Ignore the resizable flag and set the size + + :param width: + :param height: + """ + + super().set_size(width, height) + + def get_size(self) -> Tuple[int, int]: + """ + Get the size of the window. + + :returns: (width, height) + """ + + return super().get_size() + + def get_location(self) -> Tuple[int, int]: + """ + Return the X/Y coordinates of the window + + :returns: x, y of window location + """ + + return super().get_location() + + def set_visible(self, visible: bool = True): + """ + Set if the window is visible or not. Normally, a program's window is visible. + + :param visible: + """ + super().set_visible(visible) + + # noinspection PyMethodMayBeStatic + def set_viewport(self, left: float, right: float, bottom: float, top: float): + """ + Set the viewport. (What coordinates we can see. + Used to scale and/or scroll the screen). + + See :py:func:`arcade.set_viewport` for more detailed information. + + :param left: + :param right: + :param bottom: + :param top: + """ + set_viewport(left, right, bottom, top) + + # noinspection PyMethodMayBeStatic + def get_viewport(self) -> Tuple[float, float, float, float]: + """ Get the viewport. (What coordinates we can see.) """ + return self.ctx.projection_2d + + def use(self): + """Bind the window's framebuffer for rendering commands""" + self.ctx.screen.use() + + def test(self, frames: int = 10): + """ + Used by unit test cases. Runs the event loop a few times and stops. + + :param frames: + """ + start_time = time.time() + for _ in range(frames): + self.switch_to() + self.dispatch_events() + self.dispatch_event('on_draw') + self.flip() + current_time = time.time() + elapsed_time = current_time - start_time + start_time = current_time + if elapsed_time < 1. / 60.: + sleep_time = (1. / 60.) - elapsed_time + time.sleep(sleep_time) + self._dispatch_updates(1 / 60) + + def show_view(self, new_view: 'View'): + """ + Select the view to show in the next frame. + This is not a blocking call showing the view. + Your code will continue to run after this call + and the view will appear in the next dispatch + of ``on_update``/``on_draw```. + + Calling this function is the same as setting the + :py:attr:`arcade.Window.current_view` attribute. + + :param new_view: View to show + """ + if not isinstance(new_view, View): + raise TypeError( + f"Window.show_view() takes an arcade.View," + f"but it got a {type(new_view)}.") + + # Store the Window that is showing the "new_view" View. + if new_view.window is None: + new_view.window = self + elif new_view.window != self: + raise RuntimeError("You are attempting to pass the same view " + "object between multiple windows. A single " + "view object can only be used in one window.") + + # remove previously shown view's handlers + if self._current_view is not None: + self._current_view.on_hide_view() + if self._current_view.has_sections: + self.remove_handlers(self._current_view.section_manager) + self.remove_handlers(self._current_view) + + # push new view's handlers + self._current_view = new_view + if new_view.has_sections: + section_manager_managed_events = new_view.section_manager.managed_events + section_handlers = {event_type: getattr(new_view.section_manager, event_type, None) for event_type in + section_manager_managed_events} + if section_handlers: + self.push_handlers( + **section_handlers + ) + else: + section_manager_managed_events = set() + + # Note: Excluding on_show because this even can trigger multiple times. + # It should only be called once when the view is shown. + view_handlers = { + event_type: getattr(new_view, event_type, None) + for event_type in self.event_types + if event_type != 'on_show' and event_type not in section_manager_managed_events and hasattr(new_view, + event_type) + } + if view_handlers: + self.push_handlers( + **view_handlers + ) + self._current_view.on_show_view() + if self._current_view.has_sections: + self._current_view.section_manager.on_show_view() + + # Note: After the View has been pushed onto pyglet's stack of event handlers (via push_handlers()), pyglet + # will still call the Window's event handlers. (See pyglet's EventDispatcher.dispatch_event() implementation + # for details) + + def hide_view(self): + """ + Hide the currently active view (if any) returning us + back to ``on_draw`` and ``on_update`` functions in the window. + + This is not necessary to call if you are switching views. + Simply call ``show_view`` again. + """ + if self._current_view is None: + return + + self._current_view.on_hide_view() + if self._current_view.has_sections: + self._current_view.section_manager.on_hide_view() + self.remove_handlers(self._current_view.section_manager) + self.remove_handlers(self._current_view) + self._current_view = None + + def _create(self): + super()._create() + + def _recreate(self, changes): + super()._recreate(changes) + + def flip(self): + """ + Window framebuffers normally have a back and front buffer. + This method makes the back buffer visible and hides the front buffer. + A frame is rendered into the back buffer, so this method displays + the frame we currently worked on. + + This method also garbage collect OpenGL resources + before swapping the buffers. + """ + # Garbage collect OpenGL resources + num_collected = self.ctx.gc() + LOG.debug("Garbage collected %s OpenGL resource(s)", num_collected) + + # Attempt to handle static draw setups + if self.static_display: + if self.flip_count > 0: + return + else: + self.flip_count += 1 + + super().flip() + + def switch_to(self): + """ Switch to this window. """ + super().switch_to() + + def set_caption(self, caption): + """ Set the caption for the window. """ + super().set_caption(caption) + + def set_minimum_size(self, width: int, height: int): + """ Set smallest window size. """ + super().set_minimum_size(width, height) + + def set_maximum_size(self, width, height): + """ Set largest window size. """ + super().set_maximum_size(width, height) + + def set_location(self, x, y): + """ Set location of the window. """ + super().set_location(x, y) + + def activate(self): + """ Activate this window. """ + super().activate() + + def minimize(self): + """ Minimize the window. """ + super().minimize() + + def maximize(self): + """ Maximize the window. """ + super().maximize() + + def set_vsync(self, vsync: bool): + """ Set if we sync our draws to the monitors vertical sync rate. """ + super().set_vsync(vsync) + + def set_mouse_platform_visible(self, platform_visible=None): + """ + .. warning:: You are probably looking for + :meth:`~.Window.set_mouse_visible`! + + This method was implemented to prevent PyCharm from displaying + linter warnings. Most users will never need to set + platform-specific visibility as the defaults from pyglet will + usually handle their needs automatically. + + For more information on what this means, see the documentation + for :py:meth:`pyglet.window.Window.set_mouse_platform_visible`. + """ + super().set_mouse_platform_visible(platform_visible) + + def set_exclusive_mouse(self, exclusive=True): + """ Capture the mouse. """ + super().set_exclusive_mouse(exclusive) + + def set_exclusive_keyboard(self, exclusive=True): + """ Capture all keyboard input. """ + super().set_exclusive_keyboard(exclusive) + + def get_system_mouse_cursor(self, name): + """ Get the system mouse cursor """ + return super().get_system_mouse_cursor(name) + + def dispatch_events(self): + """ Dispatch events """ + super().dispatch_events() + + def on_mouse_enter(self, x: int, y: int): + """ + Called once whenever the mouse enters the window area on screen. + + This event will not be triggered if the mouse is currently being + dragged. + + :param x: + :param y: + """ + pass + + def on_mouse_leave(self, x: int, y: int): + """ + Called once whenever the mouse leaves the window area on screen. + + This event will not be triggered if the mouse is currently being + dragged. Note that the coordinates of the mouse pointer will be + outside of the window rectangle. + + :param x: + :param y: + """ + pass + + +class View: + """ + Support different views/screens in a window. + """ + + def __init__(self, + window: Optional[Window] = None): + + self.window = arcade.get_window() if window is None else window + self.key: Optional[int] = None + self._section_manager: Optional[SectionManager] = None + + @property + def section_manager(self) -> SectionManager: + """ lazy instantiation of the section manager """ + if self._section_manager is None: + self._section_manager = SectionManager(self) + return self._section_manager + + @property + def has_sections(self) -> bool: + """ Return if the View has sections """ + if self._section_manager is None: + return False + else: + return self.section_manager.has_sections + + def add_section(self, section, at_index: Optional[int] = None, at_draw_order: Optional[int] = None) -> None: + """ + Adds a section to the view Section Manager. + + :param section: the section to add to this section manager + :param at_index: inserts the section at that index for event capture and update events. If None at the end + :param at_draw_order: inserts the section in a specific draw order. Overwrites section.draw_order + """ + self.section_manager.add_section(section, at_index, at_draw_order) + + def clear( + self, + color: Optional[RGBA255OrNormalized] = None, + normalized: bool = False, + viewport: Optional[Tuple[int, int, int, int]] = None, + ): + """Clears the View's Window with the configured background color + set through :py:attr:`arcade.Window.background_color`. + + :param color: (Optional) override the current background color + with one of the following: + + 1. A :py:class:`~arcade.types.Color` instance + 2. A 4-length RGBA :py:class:`tuple` of byte values (0 to 255) + 3. A 4-length RGBA :py:class:`tuple` of normalized floats (0.0 to 1.0) + + :param normalized: If the color format is normalized (0.0 -> 1.0) or byte values + :param Tuple[int, int, int, int] viewport: The viewport range to clear + """ + self.window.clear(color, normalized, viewport) + + def on_update(self, delta_time: float): + """To be overridden""" + pass + + def on_fixed_update(self, delta_time: float): + """To be overridden""" + pass + + def on_draw(self): + """Called when this view should draw""" + pass + + def on_show_view(self): + """ + Called once when the view is shown. + + .. seealso:: :py:meth:`~arcade.View.on_hide_view` + """ + pass + + def on_hide_view(self): + """Called once when this view is hidden.""" + pass + + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int): + """ + Override this function to add mouse functionality. + + :param x: x position of mouse + :param y: y position of mouse + :param dx: Change in x since the last time this method was called + :param dy: Change in y since the last time this method was called + """ + pass + + def on_mouse_press(self, x: int, y: int, button: int, modifiers: int): + """ + Override this function to add mouse button functionality. + + :param x: x position of the mouse + :param y: y position of the mouse + :param button: What button was hit. One of: + arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT, + arcade.MOUSE_BUTTON_MIDDLE + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + pass + + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, _buttons: int, _modifiers: int): + """ + Override this function to add mouse button functionality. + + :param x: x position of mouse + :param y: y position of mouse + :param dx: Change in x since the last time this method was called + :param dy: Change in y since the last time this method was called + :param _buttons: Which button is pressed + :param _modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + self.on_mouse_motion(x, y, dx, dy) + + def on_mouse_release(self, x: int, y: int, button: int, modifiers: int): + """ + Override this function to add mouse button functionality. + + :param x: x position of mouse + :param y: y position of mouse + :param button: What button was hit. One of: + arcade.MOUSE_BUTTON_LEFT, arcade.MOUSE_BUTTON_RIGHT, + arcade.MOUSE_BUTTON_MIDDLE + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + pass + + def on_mouse_scroll(self, x: int, y: int, scroll_x: int, scroll_y: int): + """ + User moves the scroll wheel. + + :param x: x position of mouse + :param y: y position of mouse + :param scroll_x: ammout of x pixels scrolled since last call + :param scroll_y: ammout of y pixels scrolled since last call + """ + pass + + def on_key_press(self, symbol: int, modifiers: int): + """ + Override this function to add key press functionality. + + :param symbol: Key that was hit + :param modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + try: + self.key = symbol + except AttributeError: + pass + + def on_key_release(self, _symbol: int, _modifiers: int): + """ + Override this function to add key release functionality. + + :param _symbol: Key that was hit + :param _modifiers: Bitwise 'and' of all modifiers (shift, ctrl, num lock) + active during this event. See :ref:`keyboard_modifiers`. + """ + try: + self.key = None + except AttributeError: + pass + + def on_resize(self, width: int, height: int): + """ + Called when the window is resized while this view is active. + :py:meth:`~arcade.Window.on_resize` is also called separately. + By default this method does nothing and can be overridden to + handle resize logic. + """ + pass + + def on_mouse_enter(self, x: int, y: int): + """ + Called when the mouse was moved into the window. + This event will not be triggered if the mouse is currently being + dragged. + + :param x: x position of mouse + :param y: y position of mouse + """ + pass + + def on_mouse_leave(self, x: int, y: int): + """ + Called when the mouse was moved outside of the window. + This event will not be triggered if the mouse is currently being + dragged. Note that the coordinates of the mouse pointer will be + outside of the window rectangle. + + :param x: x position of mouse + :param y: y position of mouse + """ + pass From bfac73fdad54846b7ba073a96cc0aad23a0d7034 Mon Sep 17 00:00:00 2001 From: DragonMoffon Date: Sun, 3 Mar 2024 21:47:41 +1300 Subject: [PATCH 04/10] Tired best to fix linting. Can't fix a few issues due to it being an experimental view/window. --- arcade/experimental/clock/clock.py | 30 +++++++++++------------ arcade/experimental/clock/clock_window.py | 18 ++++++++------ arcade/experimental/clock/timer.py | 8 +++--- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/arcade/experimental/clock/clock.py b/arcade/experimental/clock/clock.py index 5d870e59d..f2de25204 100644 --- a/arcade/experimental/clock/clock.py +++ b/arcade/experimental/clock/clock.py @@ -1,4 +1,4 @@ -from typing import Optional, Set, Callable +from typing import Optional, Set from arcade.experimental.clock.timer import Timer @@ -60,24 +60,23 @@ def tick(self, delta_time: float): timer.kill() timer.check() - def create_new_child(self, *, - tick_speed: float = 1.0, - inherit_elapsed: bool = False, - inherit_count: bool = False, - lifespan: float = 0.0 - ) -> "Clock": - pass + #def create_new_child(self, *, + # tick_speed: float = 1.0, + # inherit_elapsed: bool = False, + # inherit_count: bool = False, + # lifespan: float = 0.0 + # ) -> "Clock": + # pass - def create_new_timer(self, duration: float, callback: Callable, - *args, - **kwargs - ) -> Timer: - pass + #def create_new_timer(self, duration: float, callback: Callable, + # *args, + # **kwargs + # ) -> Timer: + # pass def add_clock(self, new_child: "Clock"): pass - def add_timer(self, new_timer: Timer): pass @@ -125,7 +124,6 @@ def pop_child(self, child: "Clock"): def timers(self): return tuple(self._timers) - @property def pop_timer(self, timer: Timer): """ Popping a timer allows you to remove a timer from a clock without destroying the timer. @@ -141,4 +139,4 @@ def transfer_parent(self, new_parent): @property def tick_count(self): - return self._tick_count \ No newline at end of file + return self._tick_count diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index 2b73f642a..cca0b4b66 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -178,6 +178,9 @@ def __init__( self._update_clock: Clock = Clock() self._fixed_clock: Clock = Clock() + self._accumulated_time: float = 0.0 + self._excess_fraction: float = 0.0 + # We don't call the set_draw_rate function here because unlike the updates, the draw scheduling # is initially set in the call to pyglet.app.run() that is done by the run() function. # run() will pull this draw rate from the Window and use it. Calls to set_draw_rate only need @@ -297,12 +300,16 @@ def background_color(self) -> Color: """ return self._background_color + @background_color.setter + def background_color(self, value: RGBA255): + self._background_color = Color.from_iterable(value) + @property - def accumulated_time(self): + def accumulated_time(self) -> float: return self._accumulated_time @property - def excess_fraction(self): + def excess_fraction(self) -> float: return self._excess_fraction # I am unsure we should provide access to the clocks directly. @@ -316,10 +323,6 @@ def update_clock(self): def fixed_update_clock(self): return self._fixed_clock - @background_color.setter - def background_color(self, value: RGBA255): - self._background_color = Color.from_iterable(value) - def run(self) -> None: """ Run the main loop. @@ -958,8 +961,7 @@ class View: def __init__(self, window: Optional[Window] = None): - - self.window = arcade.get_window() if window is None else window + self.window: Window = arcade.get_window() if window is None else window self.key: Optional[int] = None self._section_manager: Optional[SectionManager] = None diff --git a/arcade/experimental/clock/timer.py b/arcade/experimental/clock/timer.py index 633f3dd22..06f53cca3 100644 --- a/arcade/experimental/clock/timer.py +++ b/arcade/experimental/clock/timer.py @@ -1,5 +1,5 @@ import weakref -from typing import Callable, List, Dict, Any, Optional, TYPE_CHECKING +from typing import Callable,List, Dict, Any, Optional, TYPE_CHECKING if TYPE_CHECKING: from arcade.experimental.clock.clock import Clock @@ -51,7 +51,7 @@ def __init__(self, duration: float, parent: "Clock", callback: Callable, *args, self._complete: bool = False self._callback: Optional[Callable] = weakref.proxy(callback) - self._args: List[Any] = args or list() + self._args: List[Any] = list(args) self._kwargs: Dict[str, Any] = kwargs or dict() self.reusable: bool = reusable @@ -113,9 +113,9 @@ def call(self, force_complete: bool = True): if not self._callback: return - if 'duration' in self._kwargs and self._kwargs['duration'] is None: + if 'duration' == self._kwargs and self._kwargs['duration'] is None: self._kwargs['duration'] = self._duration - if 'timer' is self._kwargs and self._kwargs['timer'] is None: + if 'timer' == self._kwargs and self._kwargs['timer'] is None: self._kwargs['timer'] = self if self._parent: From adf14ba409b72efb0071d02259641553dd0bc802 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:07:42 -0500 Subject: [PATCH 05/10] Add __init__.py for clock in experimental --- arcade/experimental/clock/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 arcade/experimental/clock/__init__.py diff --git a/arcade/experimental/clock/__init__.py b/arcade/experimental/clock/__init__.py new file mode 100644 index 000000000..6c00b1428 --- /dev/null +++ b/arcade/experimental/clock/__init__.py @@ -0,0 +1,21 @@ +"""Experimental fixed update support. + +.. warning:: The classes in this module have incomplete typing! + + Using them in your projects may make your IDE or type checker + complain. Pull requests welcome. + +See the following pull request for more information: +`https://github.com/pythonarcade/arcade/pull/1944`_ +""" +from arcade.experimental.clock.timer import Timer +from arcade.experimental.clock.clock import Clock +from arcade.experimental.clock.clock_window import View, Window + + +__all__ = [ + "Timer", + "Clock", + "View", + "Window" +] \ No newline at end of file From 484647f559606125313c379fa7ec0a98a3150022 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:08:41 -0500 Subject: [PATCH 06/10] Top-level docstring and experimental.clock.clock_window.Window docstring fixes --- arcade/experimental/clock/clock_window.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index cca0b4b66..bd45c1819 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -1,8 +1,11 @@ -""" -The main window class that all object-oriented applications should -derive from. +"""Clock variants of :py:class:`arcade.Window` and :py:class:`arcade.View`. + +Unlike the main versions, they add support for using a clock and fixed +updates. They use the following classes as dependencies: + +* :py:class:`arcade.experimental.clock.timer.Timer` +* :py:class:`arcade.experimental.clock.clock.Clock` -THIS IS AN EXPERIMENTAL VERSION OF THE BASE WINDOW """ from __future__ import annotations @@ -42,12 +45,12 @@ class Window(pyglet.window.Window): - """ - The Window class forms the basis of most advanced games that use Arcade. - It represents a window on the screen, and manages events. + """An experimental copy of :py:class:`arcade.Window` with timer features. + + .. warning:: This experiment has incomplete typing! - .. _pyglet_pg_window_size_position: https://pyglet.readthedocs.io/en/latest/programming_guide/windowing.html#size-and-position - .. _pyglet_pg_window_style: https://pyglet.readthedocs.io/en/latest/programming_guide/windowing.html#window-style + See the original :py:class:`arcade.Window` or the source code of the + class for more info. :param width: Window width :param height: Window height From 204431e95cfa9df47e4b2c918e37d8a6c3ac4568 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:10:18 -0500 Subject: [PATCH 07/10] Add fixed update None return annotations --- arcade/experimental/clock/clock_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index bd45c1819..16ef765e7 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -383,7 +383,7 @@ def on_update(self, delta_time: float): """ pass - def on_fixed_update(self, delta_time: float): + def on_fixed_update(self, delta_time: float) -> None: """ Perform game logic which needs regular interval updates. Put sprite movement collisions, and physics updates here. @@ -1018,7 +1018,7 @@ def on_update(self, delta_time: float): """To be overridden""" pass - def on_fixed_update(self, delta_time: float): + def on_fixed_update(self, delta_time: float) -> None: """To be overridden""" pass From 3c7b16b2e8ece1a775aa299109e0cca23a52279f Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:12:27 -0500 Subject: [PATCH 08/10] Fix missing annotation on TextureAnimationSprite._animation --- arcade/sprite/animated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite/animated.py b/arcade/sprite/animated.py index 95f89049e..28e5299d6 100644 --- a/arcade/sprite/animated.py +++ b/arcade/sprite/animated.py @@ -160,7 +160,7 @@ def __init__( center_y=center_y, ) self._time = 0.0 - self._animation = None + self._animation: Optional[TextureAnimation] = None if animation: self.animation = animation self._current_keyframe_index = 0 From 0cbd9e3c54b7093ecf0fc1cdf03cf9e73d4de1e2 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:22:20 -0500 Subject: [PATCH 09/10] Use nasty typing tricks to make mypy pass --- arcade/experimental/clock/clock_window.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/arcade/experimental/clock/clock_window.py b/arcade/experimental/clock/clock_window.py index 16ef765e7..97dbf7863 100644 --- a/arcade/experimental/clock/clock_window.py +++ b/arcade/experimental/clock/clock_window.py @@ -33,10 +33,17 @@ # NEW IMPORTS from arcade.experimental.clock.clock import Clock +from typing import Union LOG = logging.getLogger(__name__) -_window: 'Window' + +# Kludge to get typechecking to pass. Ideally, these should be +# a typing.Protocol if inheritance isn't an option. To paraphrase +# DragonMoffon from a question in Discord call whether avoiding +# inheriting was intentional: "Yeah, it changes a bunch of things". +AnyWindow = Union["Window", arcade.Window] +AnyView = Union["View", arcade.View] __all__ = [ "Window", @@ -751,7 +758,7 @@ def test(self, frames: int = 10): time.sleep(sleep_time) self._dispatch_updates(1 / 60) - def show_view(self, new_view: 'View'): + def show_view(self, new_view: AnyView): """ Select the view to show in the next frame. This is not a blocking call showing the view. @@ -962,9 +969,8 @@ class View: Support different views/screens in a window. """ - def __init__(self, - window: Optional[Window] = None): - self.window: Window = arcade.get_window() if window is None else window + def __init__(self, window: Optional[AnyWindow] = None): + self.window: AnyWindow = arcade.get_window() if window is None else window self.key: Optional[int] = None self._section_manager: Optional[SectionManager] = None @@ -972,7 +978,7 @@ def __init__(self, def section_manager(self) -> SectionManager: """ lazy instantiation of the section manager """ if self._section_manager is None: - self._section_manager = SectionManager(self) + self._section_manager = SectionManager(self) # type: ignore return self._section_manager @property From 678035ff74a8b2fa6697362533528876f8951518 Mon Sep 17 00:00:00 2001 From: pushfoo <36696816+pushfoo@users.noreply.github.com> Date: Sun, 3 Mar 2024 04:22:54 -0500 Subject: [PATCH 10/10] Fix no newline to make ruff pass --- arcade/experimental/clock/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/experimental/clock/__init__.py b/arcade/experimental/clock/__init__.py index 6c00b1428..24ab9e776 100644 --- a/arcade/experimental/clock/__init__.py +++ b/arcade/experimental/clock/__init__.py @@ -18,4 +18,4 @@ "Clock", "View", "Window" -] \ No newline at end of file +]