pyxora.scene

  1from .display import Display
  2from .assets import Assets
  3from .camera import Camera as SceneCamera
  4from .utils import engine,asyncio
  5
  6from time import perf_counter as time
  7from typing import Dict,Tuple,Any,Type
  8
  9import pygame
 10
 11class classproperty(property):
 12    """@private class to create @classproperties"""
 13    def __get__(self, obj, cls):
 14        return self.fget(cls)
 15
 16class SceneManager:
 17    """The Main Manager of the Scenes."""
 18    scenes: Dict[str, Tuple[str, "Scene", Any]] = {}
 19    """A mapping of scene keys to (name, Scene object, additional data) tuples."""
 20
 21    selected: str
 22    """The currently selected scene"""
 23
 24    @classproperty
 25    def scene(cls) -> Tuple[str, "Scene", Any]:
 26        """Class Property to get the active scene."""
 27        return cls.scenes.get(cls.selected,(None,None,None))
 28
 29    # --- Scene Control ---
 30    @classmethod
 31    async def start(cls) -> None:
 32        """
 33        Start the main game loop.
 34        """
 35        engine.print_versions()
 36        while True:
 37            scene = cls.scene
 38            if not scene:
 39                engine.error(Exception("No scene selected"))
 40                engine.quit()
 41            scene_object = scene[1]
 42            await scene_object._Scene__run()
 43
 44    @classmethod
 45    def create(cls, name: str, **kwargs: Any) -> None:
 46        """
 47        Create a new scene instance.
 48
 49        Args:
 50            scene_class (str): The class of the scene.
 51            kwargs: Additional arguments passed to the scene's constructor.
 52        """
 53        scene = Assets.get("data","scenes",name)
 54        if not scene:
 55            engine.error(RuntimeError(f"Scene: {name}, not found in data/scenes folder"))
 56            engine.quit()
 57        cls.scene = (name, scene(**kwargs), kwargs)
 58
 59    @classmethod
 60    def change(cls, name: str, **kwargs) -> None:
 61        """
 62        Exit and change to a different scene.
 63
 64        Args:
 65            name (str): The name of the scene to switch to.
 66        """
 67        scene_obj = cls.scene[1]
 68        scene_obj._on_change()
 69        scene_obj._Scene__running = False
 70
 71        cls.create(name,**kwargs)
 72
 73    @classmethod
 74    def pause(cls) -> None:
 75        """
 76        Pause the current scene.
 77        """
 78        scene_obj = cls.scene[1]
 79        scene_obj._on_pause()
 80        scene_obj._Scene__paused = True
 81        scene_obj._Scene__pause_last_time = time()
 82
 83    @classmethod
 84    def resume(cls) -> None:
 85        """Resumes the current scene."""
 86        scene_obj = cls.scene[1]
 87        scene_obj._on_resume()
 88        scene_obj._Scene__paused = False
 89        scene_obj._Scene__dt = 0
 90
 91    @classmethod
 92    def restart(cls) -> None:
 93        """
 94        Restart the current scene.
 95        """
 96        scene_obj = cls.scene[1]
 97        scene_obj._on_restart()
 98        scene_obj._Scene__running = False
 99
100    @classmethod
101    def reset(cls) -> None:
102        """
103        Reset the current scene.
104        """
105        scene_obj = cls.scene[1]
106        scene_obj._on_reset()
107        scene_obj._Scene__running = False
108
109        # manual create a new scene
110        name, obj, kwargs = cls.scene
111        cls.create(name,**kwargs)
112
113    @classmethod
114    def quit(cls) -> None:
115        """
116        Quit the application through the current scene.
117        """
118        scene_obj = cls.scene[1]
119        scene_obj._on_quit()
120        engine.quit()
121
122class SceneEvent:
123    """
124    A helper class to create custom events for the Scenes.
125    """
126    def __init__(self,scene: "Scene") -> None:
127        """
128        @private
129        (Not include in the docs as it should be call only inside scene itself)
130        Updates all the custom events and it's properties at the scene state.
131
132        Initializes a Scene Event.
133
134        Args:
135            scene (SceneManager): The reference to the current Scene.
136        """
137        self.__data = {}
138        self.__scene = scene
139
140    def create(self,name: str,state: int = 0,**kwargs: Any) -> int:
141        """
142        Create and store a new custom event.
143
144        Args:
145            name (str): The name of the event.
146            state (int): The event state, where:
147                - 1: Runtime
148                - -1: Pause time
149                - 0: Both (default = 0)
150            **kwargs: Additional arguments passed to the event's info. Can be any extra data needed for the event.
151
152        Returns:
153            int: The ID of the new event.
154        """
155        event_id = pygame.event.custom_type()
156        create_time = self._now(state)
157        last_time = create_time
158        basic_argv = {
159            "name":name,"custom":True,
160            "create_time":create_time,"calls":0,
161            "last_time":last_time,"timer":None,
162            "loops":None,"state":state
163        }
164
165        kwargs.update(basic_argv)
166        event = pygame.event.Event(
167            event_id,
168            kwargs
169        )
170
171        self.__data[name] = event
172        return event_id
173
174    def get(self,name: str) -> pygame.event.Event:
175        """
176        Get a custom event by its name.
177
178        Args:
179            name (str): The name of the event.
180
181        Returns:
182            pygame.event.Event or None: The event with the specified name, or None if not found.
183        """
184        return self.__data.get(name,None)
185
186    def remove(self,name: str) -> "SceneEvent":
187        """
188        Remove a custom event by its name.
189
190        Args:
191            name (str): The name of the event.
192
193        Returns:
194            SceneEvent: The event that was removed
195        """
196        return self.__data.pop(event_name)
197
198    def post(self,name: str) -> bool:
199        """
200        Post a custom event by its name.
201
202        Args:
203            name (str): The name of the event.
204
205        Returns:
206            bool: returns a boolean on whether the event was posted or not
207        """
208        event = self.get(name)
209        return pygame.event.post(event)
210
211    def match(self,name: str,other_event: "SceneEvent") -> bool:
212        """
213        Check if a custom event matches by its name.
214
215        Args:
216            name (str): The name of the event.
217            other_event (SceneEvent): The event to compare against.
218
219        Returns:
220            bool: True if the events match, False otherwise.
221        """
222        event = self.get(name)
223        return event.type == other_event.type
224
225    # Handling events manually seems more flexible and easier for this use case.
226    def schedule(self,name: str,timer: int,loops: int = -1) -> None:
227        """
228        Shedule a custom event by it's name, for ms timer and loop times.
229
230        Args:
231            name (str): The name of the event.
232            timer (int): The time of the shedule in ms.
233            loops (int): The amount of loop times (default = -1, forever).
234        """
235        event = self.get(name)
236        event.timer = timer
237        event.loops = loops
238
239    # Update all the scene events and it properties
240    def update(self,state: int):
241        """
242        @private
243
244        (Not include in the doc as it should be call only inside scene itself)
245        Updates all the custom events and it's properties at the scene state.
246
247        Args:
248            state (int): The scene state. (default = -1, forever).
249        """
250        for event_name in self.__data:
251            event = self.get(event_name)
252
253            if not(self._is_state(state,event.state)):
254                continue
255
256            if not event.timer:
257                continue
258
259            if event.calls == event.loops:
260                continue
261
262            self._update_time(event)
263
264    def _now(self, state: int) -> float:
265        """Returns the current time based on the state.
266
267        Args:
268            state (int): Determines which time value to return.
269                - If 0: Returns runtime + pausetime.
270                - If >0: Returns runtime.
271                - If <0: Returns pausetime.
272
273        Returns:
274            float: The calculated current time value.
275        """
276        runtime = self.__scene.runtime
277        pausetime = self.__scene.pausetime
278        if state == 0:
279            return runtime + pausetime
280
281        now = runtime if state > 0 else pausetime
282        return now
283
284
285    def _update_time(self, event:"Event") -> None:
286        """Checks if an event should be triggered based on elapsed time.
287
288        If the time since the last event trigger exceeds the timer threshold,
289        the event is posted and its last_time is updated.
290
291        Args:
292            event (object): An object with the following attributes:
293                - state (int): State used to determine which time value to use.
294                - last_time (float): Timestamp of the last event trigger.
295                - timer (float): Timer threshold in milliseconds.
296                - name (str): Name of the event to post.
297
298        Side Effects:
299            Posts the event via self.post() if the condition is met.
300            Updates event.last_time.
301        """
302        now = self._now(event.state)
303        diff = (now - event.last_time) * 1000
304        is_time = diff >= event.timer
305        if not is_time:
306            return
307
308        self.post(event.name)
309        event.last_time = now
310        event.calls += 1
311
312
313    @staticmethod
314    def _is_state(state: int, event_state: int)-> bool:
315        """Checks if the event state matches the given state or is neutral (0).
316
317        Args:
318            state (int): Target state to compare against.
319            event_state (int): Current state of the event.
320
321        Returns:
322            bool: True if the event_state equals the state or is 0, False otherwise.
323        """
324        same = event_state == state
325        is_zero = event_state == 0
326        return (same or is_zero)
327
328class Scene:
329    """Represents a scene in the game."""
330    _global_runtime = _global_pausetime = 0
331    __global_start_time = time()
332    def __init__(self,**kwargs: Any) -> None:
333        """
334        Initializes a Scene object.
335
336        Args:
337            **kwargs: Additional arguments passed to the scene. Can be any extra data needed for the scene.
338
339        Raises:
340            RuntimeError: If the Display has not been initialized. Call Display.init() first.
341        """
342        if not self.display.window:
343            engine.error(RuntimeError("Display has not been initialized! Call Display.init first."))
344            self.quit()
345
346        self.__initialize(kwargs)
347
348    @classproperty
349    def manager(cls) -> Type[SceneManager]:
350        """Class Property to get the scene manager class"""
351        return SceneManager
352
353    @classproperty
354    def display(cls) -> Type[Display]:
355        """Class Property to get a direct reference to the engine Display class."""
356        return Display
357
358    @classproperty
359    def assets(cls) -> Type[Assets]:
360        """Class Property to get a direct reference to the engine Assets class."""
361        return Assets
362
363    @classproperty
364    def global_runtime(cls) -> float:
365        """Class Property to get the total run time across all scenes."""
366        return cls._global_runtime
367
368    @classproperty
369    def global_pausetime(cls) -> float:
370        """Class Property to get the total pause time across all scenes."""
371        return cls._global_pausetime
372
373    @property
374    def camera(self) -> SceneCamera:
375        """Property to get the event camera instance of the current scene."""
376        return self._camera
377
378    @property
379    def event(self) -> SceneEvent:
380        """Property to get the event handler instance of the current scene."""
381        return self._event
382
383    @property
384    def events(self) -> set:
385        """Property to get all the events of the current frame."""
386        return self._events
387
388    @property
389    def custom_events(self) -> set:
390        """Property to get all the custom events of the current frame."""
391        return self._custom_events
392
393    @property
394    def keys_pressed(self) -> set:
395        """Property to get the keys currently pressed of the current frame."""
396        return self._keys_pressed
397
398    @property
399    def dt(self) -> float:
400        """Property to get the time elapsed since the last frame."""
401        return self._dt
402
403    @property
404    def fps(self) -> float:
405        """Property to get the current frames per second."""
406        return self._fps
407
408    @property
409    def max_fps(self) -> int:
410        """Property to get the maximum frames per second limit."""
411        return self._max_fps
412
413    @max_fps.setter
414    def max_fps(self, value: int):
415        """Setter to set the maximum frames per second limit."""
416        self._max_fps = value
417
418    @property
419    def background_color(self) -> str | Tuple[int, int, int]:
420        """Property to get the background color"""
421        return self._background_color
422
423    @background_color.setter
424    def background_color(self, value: str | Tuple[int, int, int]):
425        """Setter to set the background color"""
426        self._background_color = value
427
428    @property
429    def runtime(self) -> float:
430        """Property to get the run time"""
431        return self._runtime
432
433    @property
434    def pausetime(self) -> float:
435        """Property to get the pause time"""
436        return self._pausetime
437
438    # Utils
439    def is_time(self,ms):
440        """Checks if a specified time interval has elapsed since the last frame."""
441        multiplier = 1/ms*1000
442        return int(self._runtime * multiplier) != int((self._runtime - self._dt) * multiplier)
443
444    def is_event(self,event_id):
445        """Checks if an event is happening during the frame. """
446        return event_id in self._events
447
448    def is_custom_event(self,event_name):
449        """Checks if a custom event is happening during the frame. """
450        return event_name in self._custom_events
451
452    def is_paused(self):
453        """Returns if the scene is paused."""
454        return self.__paused
455
456    # Lifecycle Methods
457    def _start(self) -> None:
458        """
459        @public
460        Called once at the start of the scene. You must Override this func in your subclass.
461
462        Raises:
463            NotImplementedError: If not overridden.
464        """
465        raise NotImplementedError("start() must be overridden in subclass.")
466
467    def _update(self) -> None:
468        """
469        @public
470        Called every frame to update scene logic. You must Override this func in your subclass.
471
472        Raises:
473            NotImplementedError: If not overridden.
474        """
475        raise NotImplementedError("update() must be overridden in subclass.")
476
477    def _draw(self) -> None:
478        """
479        @public
480        Called every frame to draw elements to the screen. You must Override this func in your subclass.
481
482        Raises:
483            NotImplementedError: If not overridden.
484        """
485        raise NotImplementedError("draw() must be overridden in subclass.")
486
487    # Paused Lifecycle Methods
488    def _paused_update(self) -> None:
489        """@public Called every paused frame to update scene logic. Override this func in your subclass."""
490        pass
491    def _paused_draw(self) -> None:
492        """@public Called every paused frame to draw elements to the screen. Override this func in your subclass."""
493        pass
494
495    # Scene State Change Methods
496    def _on_create(self) -> None:
497        """@public Called once at the scene creation "manager.create()". Override this func in your subclass to add code."""
498        pass
499    def _on_quit(self) -> None:
500        """@public Called once at every scene quit "manager.quit()". Override this func in your subclass to add code."""
501        pass
502    def _on_restart(self) -> None:
503        """@public Called once at every scene restart "manager.restart()". Override this func in your subclass to add code."""
504        pass
505    def _on_reset(self) -> None:
506        """@public Called once at the scene reset "manager.reset()". Override this func in your subclass to add code."""
507        pass
508    def _on_change(self) -> None:
509        """@public Called once at the scene change "manager.change()". Override this func in your subclass to add code."""
510        pass
511    def _on_resume(self) -> None:
512        """@public Called once at the scene resume "manager.resume()". Override this func in your subclass to add code."""
513        pass
514    def _on_pause(self) -> None:
515        """@public Called once at the scene pause "manager.pause()". Override this func in your subclass to add code."""
516        pass
517    def _on_error(self,error: BaseException) -> None:
518        """
519        @public
520        Called once at engine error "Scene.__handle_error()". Override this func in your subclass to add code.
521
522        Args:
523            error (BaseException): The engine error that occurred.
524        """
525        pass
526
527    # Scene Event/Input Methods
528    def _on_event(self, event: pygame.Event) -> None:
529        """
530        @public
531        Called every pygame event. Override this func in your subclass to add code.
532
533        Args:
534            event (pygame.Event): The pygame event that occurred.
535        """
536        pass
537    def _on_keydown(self,key: str) -> None:
538        """
539        @public
540        Called every keyboard keydown. Override this func in your subclass to add code.
541
542        Args:
543            key (str): The keyboard key.
544        """
545        pass
546    def _on_keyup(self,key: str) -> None:
547        """
548        @public
549        Called every keyboard keyup. Override this func in your subclass to add code.
550
551        Args:
552            key (str): The keyboard key.
553        """
554        pass
555    def _on_keypressed(self,key: str) -> None:
556        """
557        @public
558        Called every keypressed. Override this func in your subclass to add code.
559
560        Args:
561            key (str): The keyboard key.
562        """
563        pass
564    def _on_mousewheel(self,wheel: int) -> None:
565        """
566        @public
567        Called every mousewheel change. Override this func in your subclass to add code.
568
569        Args:
570            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
571        """
572        pass
573
574    # Paused Event/Input Methods
575    def _on_paused_event(self, event: pygame.event.Event) -> None:
576        """
577        @public
578        Called every paused pygame event. Override this func in your subclass to add code.
579
580        Args:
581            event (pygame.Event): The pygame event that occurred.
582        """
583        pass
584    def _on_paused_keydown(self,key: str) -> None:
585        """
586        @public
587        Called every paused keyboard keydown. Override this func in your subclass to add code.
588
589        Args:
590            key (str): The keyboard key.
591        """
592    def _on_paused_keyup(self,key: str) -> None:
593        """
594        @public
595        Called every paused keyboard keypressed. Override this func in your subclass to add code.
596
597        Args:
598            key (str): The keyboard key.
599        """
600        pass
601    def _on_paused_keypressed(self,key: str) -> None:
602        """
603        @public
604        Called every paused keypressed. Override this func in your subclass to add code.
605
606        Args:
607            key (str): The keyboard key.
608        """
609        pass
610    def _on_paused_mousewheel(self,wheel: int) -> None:
611        """
612        @public
613        Called every paused mousewheel change. Override this func in your subclass to add code.
614
615        Args:
616            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
617        """
618        pass
619
620    # Main Loop
621    # Async to support pygbag export
622    async def __run(self) -> None:
623        """
624        Starts the scene.
625
626        Raises:
627            Exception: If there is any error in the scene.
628        """
629        try:
630            self.__initialize_runtime()
631            self._start()
632
633            while self.__running:
634                self.__handle_events()
635                self.__update()
636                self.__render()
637                self.__flip()
638
639                await asyncio.sleep(0)
640
641        except Exception as e:
642            self.__handle_error(e)
643
644    # All the methods below are used to handle the scene frames.
645    def __initialize(self,kwargs):
646        try:
647            self._max_fps = 60
648            self._background_color = (0, 0, 0)
649
650            self.__running = True
651            self.__paused = False
652
653            # set it here because the assets are loaded after the scene is initialized
654            Display.set_icon(
655                Assets.get("engine","images","icon")
656            )
657
658            self.__event_handlers = {
659                True:  (self._on_paused_keydown, self._on_paused_keyup, self._on_paused_keypressed, self._on_paused_mousewheel, self._on_paused_event),
660                False: (self._on_keydown,        self._on_keyup,        self._on_keypressed,        self._on_mousewheel,        self._on_event)
661            }
662
663            # set manual the scene kwargs to the scene
664            for k, v in kwargs.items():
665                setattr(self, k, v)
666            self._on_create()
667        except Exception as e:
668            # expect on_create error as is not in the main loop
669            self.__handle_error(e)
670
671    def __initialize_runtime(self):
672        """Sets up initial basic runtime values."""
673        self._dt = self._fps = self._runtime = self._pausetime = 0
674        self._keys_pressed = set() # we manually keep track with the key_pressed every frame, set so no duplicates
675        self._events = set() # log events every frame
676        self._custom_events = set() # log custom events every frame
677
678        # Create custom Scene events
679        self._event = SceneEvent(self)
680        self._camera = SceneCamera()
681
682        self._start_time = time()
683        self.__running = True
684
685    def __handle_error(self,e):
686        """ Handles every possible error with a nice message."""
687        self._on_error(e)
688        engine.error(e)
689        engine.quit()
690
691    def __handle_events(self):
692        """Handles events during runtime or when paused."""
693        self._events.clear()
694        self._custom_events.clear()
695        self._event.update(-1 if self.__paused else 1)
696
697        on_keydown, on_keyup, on_keypressed, on_wheel, on_event = self.__event_handlers[self.__paused]
698        for event in pygame.event.get():
699            self._events.add(event.type)
700
701            if hasattr(event, "custom"):
702                self._custom_events.add(event.name)
703                continue
704
705            if event.type == pygame.QUIT:
706                self.manager.quit()
707
708            elif event.type == pygame.KEYDOWN:
709                key = pygame.key.name(event.key)
710                self._keys_pressed.add(key)
711                on_keydown(key)
712
713            elif event.type == pygame.KEYUP:
714                key = pygame.key.name(event.key)
715                self._keys_pressed.discard(key)
716                on_keyup(key)
717
718            elif event.type == pygame.MOUSEWHEEL:
719                wheel = "up" if event.y >= 1 else "down"
720                on_wheel(wheel)
721
722            elif event.type == pygame.VIDEORESIZE:
723                Display.set_res((event.w, event.h))
724                Display._dynamic_zoom and self.camera._dynamic_zoom()
725            on_event(event)
726
727        if self._keys_pressed:
728            on_keypressed(self._keys_pressed)
729
730
731    def __update(self):
732        """Update the scene and timers, depending on whether it's paused or active."""
733        update = self._paused_update if self.__paused else self._update
734        update_timers = self.__update_paused_timers if self.__paused else self.__update_timers
735        update()
736        update_timers()
737
738    def __update_timers(self):
739        """Updates global and local runtime timers."""
740        delta = time() - self._start_time
741        global_delta = time() - self.__global_start_time
742
743        self._runtime = delta - self._pausetime
744        self._global_runtime = global_delta - self._global_pausetime
745
746    def __update_paused_timers(self):
747        """Tracks time spent in pause mode."""
748
749        # That was the easiest way to track the duration during the pause
750        # but not the best :D
751        delta = time() - self.__pause_last_time
752
753        self._pausetime += delta
754        self._global_pausetime += delta
755
756        self.__pause_last_time = time()
757
758    # no point to update the fps every frame as is hard to tell with the eye
759    # maybe i should change it to average instead?
760    def __update_fps(self):
761        """Updates the current scene fps every 250ms."""
762        if self.is_time(250):
763            self._fps = Display.clock.get_fps()
764
765    def __render(self):
766        """Renders the scene."""
767        if not self.__paused: # skip background if paused to keep the last frame render
768            self.__draw_background()
769        (self._paused_draw if self.__paused else self._draw)()
770
771    def __draw_background(self):
772        """Clears the screen with the background color."""
773        Display.surface.fill(self._background_color)
774
775    def __draw_display(self):
776        """Draws the display."""
777        surf = Display.get_stretch_surf() if Display.is_resized() else Display.surface
778        Display.window.blit(surf,(0,0))
779
780    def __flip(self):
781        """Updates the display with the latest frame."""
782        self.__update_fps() # I think updating the fps before the flip is the best place?
783        self._dt = round(Display.clock.tick(self._max_fps) / 1000, 3) # Also take the dt
784        self.__draw_display()
785        pygame.display.flip()
class SceneManager:
 17class SceneManager:
 18    """The Main Manager of the Scenes."""
 19    scenes: Dict[str, Tuple[str, "Scene", Any]] = {}
 20    """A mapping of scene keys to (name, Scene object, additional data) tuples."""
 21
 22    selected: str
 23    """The currently selected scene"""
 24
 25    @classproperty
 26    def scene(cls) -> Tuple[str, "Scene", Any]:
 27        """Class Property to get the active scene."""
 28        return cls.scenes.get(cls.selected,(None,None,None))
 29
 30    # --- Scene Control ---
 31    @classmethod
 32    async def start(cls) -> None:
 33        """
 34        Start the main game loop.
 35        """
 36        engine.print_versions()
 37        while True:
 38            scene = cls.scene
 39            if not scene:
 40                engine.error(Exception("No scene selected"))
 41                engine.quit()
 42            scene_object = scene[1]
 43            await scene_object._Scene__run()
 44
 45    @classmethod
 46    def create(cls, name: str, **kwargs: Any) -> None:
 47        """
 48        Create a new scene instance.
 49
 50        Args:
 51            scene_class (str): The class of the scene.
 52            kwargs: Additional arguments passed to the scene's constructor.
 53        """
 54        scene = Assets.get("data","scenes",name)
 55        if not scene:
 56            engine.error(RuntimeError(f"Scene: {name}, not found in data/scenes folder"))
 57            engine.quit()
 58        cls.scene = (name, scene(**kwargs), kwargs)
 59
 60    @classmethod
 61    def change(cls, name: str, **kwargs) -> None:
 62        """
 63        Exit and change to a different scene.
 64
 65        Args:
 66            name (str): The name of the scene to switch to.
 67        """
 68        scene_obj = cls.scene[1]
 69        scene_obj._on_change()
 70        scene_obj._Scene__running = False
 71
 72        cls.create(name,**kwargs)
 73
 74    @classmethod
 75    def pause(cls) -> None:
 76        """
 77        Pause the current scene.
 78        """
 79        scene_obj = cls.scene[1]
 80        scene_obj._on_pause()
 81        scene_obj._Scene__paused = True
 82        scene_obj._Scene__pause_last_time = time()
 83
 84    @classmethod
 85    def resume(cls) -> None:
 86        """Resumes the current scene."""
 87        scene_obj = cls.scene[1]
 88        scene_obj._on_resume()
 89        scene_obj._Scene__paused = False
 90        scene_obj._Scene__dt = 0
 91
 92    @classmethod
 93    def restart(cls) -> None:
 94        """
 95        Restart the current scene.
 96        """
 97        scene_obj = cls.scene[1]
 98        scene_obj._on_restart()
 99        scene_obj._Scene__running = False
100
101    @classmethod
102    def reset(cls) -> None:
103        """
104        Reset the current scene.
105        """
106        scene_obj = cls.scene[1]
107        scene_obj._on_reset()
108        scene_obj._Scene__running = False
109
110        # manual create a new scene
111        name, obj, kwargs = cls.scene
112        cls.create(name,**kwargs)
113
114    @classmethod
115    def quit(cls) -> None:
116        """
117        Quit the application through the current scene.
118        """
119        scene_obj = cls.scene[1]
120        scene_obj._on_quit()
121        engine.quit()

The Main Manager of the Scenes.

scenes: Dict[str, Tuple[str, Scene, Any]] = {}

A mapping of scene keys to (name, Scene object, additional data) tuples.

selected: str

The currently selected scene

scene: Tuple[str, Scene, Any]
25    @classproperty
26    def scene(cls) -> Tuple[str, "Scene", Any]:
27        """Class Property to get the active scene."""
28        return cls.scenes.get(cls.selected,(None,None,None))

Class Property to get the active scene.

@classmethod
async def start(cls) -> None:
31    @classmethod
32    async def start(cls) -> None:
33        """
34        Start the main game loop.
35        """
36        engine.print_versions()
37        while True:
38            scene = cls.scene
39            if not scene:
40                engine.error(Exception("No scene selected"))
41                engine.quit()
42            scene_object = scene[1]
43            await scene_object._Scene__run()

Start the main game loop.

@classmethod
def create(cls, name: str, **kwargs: Any) -> None:
45    @classmethod
46    def create(cls, name: str, **kwargs: Any) -> None:
47        """
48        Create a new scene instance.
49
50        Args:
51            scene_class (str): The class of the scene.
52            kwargs: Additional arguments passed to the scene's constructor.
53        """
54        scene = Assets.get("data","scenes",name)
55        if not scene:
56            engine.error(RuntimeError(f"Scene: {name}, not found in data/scenes folder"))
57            engine.quit()
58        cls.scene = (name, scene(**kwargs), kwargs)

Create a new scene instance.

Arguments:
  • scene_class (str): The class of the scene.
  • kwargs: Additional arguments passed to the scene's constructor.
@classmethod
def change(cls, name: str, **kwargs) -> None:
60    @classmethod
61    def change(cls, name: str, **kwargs) -> None:
62        """
63        Exit and change to a different scene.
64
65        Args:
66            name (str): The name of the scene to switch to.
67        """
68        scene_obj = cls.scene[1]
69        scene_obj._on_change()
70        scene_obj._Scene__running = False
71
72        cls.create(name,**kwargs)

Exit and change to a different scene.

Arguments:
  • name (str): The name of the scene to switch to.
@classmethod
def pause(cls) -> None:
74    @classmethod
75    def pause(cls) -> None:
76        """
77        Pause the current scene.
78        """
79        scene_obj = cls.scene[1]
80        scene_obj._on_pause()
81        scene_obj._Scene__paused = True
82        scene_obj._Scene__pause_last_time = time()

Pause the current scene.

@classmethod
def resume(cls) -> None:
84    @classmethod
85    def resume(cls) -> None:
86        """Resumes the current scene."""
87        scene_obj = cls.scene[1]
88        scene_obj._on_resume()
89        scene_obj._Scene__paused = False
90        scene_obj._Scene__dt = 0

Resumes the current scene.

@classmethod
def restart(cls) -> None:
92    @classmethod
93    def restart(cls) -> None:
94        """
95        Restart the current scene.
96        """
97        scene_obj = cls.scene[1]
98        scene_obj._on_restart()
99        scene_obj._Scene__running = False

Restart the current scene.

@classmethod
def reset(cls) -> None:
101    @classmethod
102    def reset(cls) -> None:
103        """
104        Reset the current scene.
105        """
106        scene_obj = cls.scene[1]
107        scene_obj._on_reset()
108        scene_obj._Scene__running = False
109
110        # manual create a new scene
111        name, obj, kwargs = cls.scene
112        cls.create(name,**kwargs)

Reset the current scene.

@classmethod
def quit(cls) -> None:
114    @classmethod
115    def quit(cls) -> None:
116        """
117        Quit the application through the current scene.
118        """
119        scene_obj = cls.scene[1]
120        scene_obj._on_quit()
121        engine.quit()

Quit the application through the current scene.

class SceneEvent:
123class SceneEvent:
124    """
125    A helper class to create custom events for the Scenes.
126    """
127    def __init__(self,scene: "Scene") -> None:
128        """
129        @private
130        (Not include in the docs as it should be call only inside scene itself)
131        Updates all the custom events and it's properties at the scene state.
132
133        Initializes a Scene Event.
134
135        Args:
136            scene (SceneManager): The reference to the current Scene.
137        """
138        self.__data = {}
139        self.__scene = scene
140
141    def create(self,name: str,state: int = 0,**kwargs: Any) -> int:
142        """
143        Create and store a new custom event.
144
145        Args:
146            name (str): The name of the event.
147            state (int): The event state, where:
148                - 1: Runtime
149                - -1: Pause time
150                - 0: Both (default = 0)
151            **kwargs: Additional arguments passed to the event's info. Can be any extra data needed for the event.
152
153        Returns:
154            int: The ID of the new event.
155        """
156        event_id = pygame.event.custom_type()
157        create_time = self._now(state)
158        last_time = create_time
159        basic_argv = {
160            "name":name,"custom":True,
161            "create_time":create_time,"calls":0,
162            "last_time":last_time,"timer":None,
163            "loops":None,"state":state
164        }
165
166        kwargs.update(basic_argv)
167        event = pygame.event.Event(
168            event_id,
169            kwargs
170        )
171
172        self.__data[name] = event
173        return event_id
174
175    def get(self,name: str) -> pygame.event.Event:
176        """
177        Get a custom event by its name.
178
179        Args:
180            name (str): The name of the event.
181
182        Returns:
183            pygame.event.Event or None: The event with the specified name, or None if not found.
184        """
185        return self.__data.get(name,None)
186
187    def remove(self,name: str) -> "SceneEvent":
188        """
189        Remove a custom event by its name.
190
191        Args:
192            name (str): The name of the event.
193
194        Returns:
195            SceneEvent: The event that was removed
196        """
197        return self.__data.pop(event_name)
198
199    def post(self,name: str) -> bool:
200        """
201        Post a custom event by its name.
202
203        Args:
204            name (str): The name of the event.
205
206        Returns:
207            bool: returns a boolean on whether the event was posted or not
208        """
209        event = self.get(name)
210        return pygame.event.post(event)
211
212    def match(self,name: str,other_event: "SceneEvent") -> bool:
213        """
214        Check if a custom event matches by its name.
215
216        Args:
217            name (str): The name of the event.
218            other_event (SceneEvent): The event to compare against.
219
220        Returns:
221            bool: True if the events match, False otherwise.
222        """
223        event = self.get(name)
224        return event.type == other_event.type
225
226    # Handling events manually seems more flexible and easier for this use case.
227    def schedule(self,name: str,timer: int,loops: int = -1) -> None:
228        """
229        Shedule a custom event by it's name, for ms timer and loop times.
230
231        Args:
232            name (str): The name of the event.
233            timer (int): The time of the shedule in ms.
234            loops (int): The amount of loop times (default = -1, forever).
235        """
236        event = self.get(name)
237        event.timer = timer
238        event.loops = loops
239
240    # Update all the scene events and it properties
241    def update(self,state: int):
242        """
243        @private
244
245        (Not include in the doc as it should be call only inside scene itself)
246        Updates all the custom events and it's properties at the scene state.
247
248        Args:
249            state (int): The scene state. (default = -1, forever).
250        """
251        for event_name in self.__data:
252            event = self.get(event_name)
253
254            if not(self._is_state(state,event.state)):
255                continue
256
257            if not event.timer:
258                continue
259
260            if event.calls == event.loops:
261                continue
262
263            self._update_time(event)
264
265    def _now(self, state: int) -> float:
266        """Returns the current time based on the state.
267
268        Args:
269            state (int): Determines which time value to return.
270                - If 0: Returns runtime + pausetime.
271                - If >0: Returns runtime.
272                - If <0: Returns pausetime.
273
274        Returns:
275            float: The calculated current time value.
276        """
277        runtime = self.__scene.runtime
278        pausetime = self.__scene.pausetime
279        if state == 0:
280            return runtime + pausetime
281
282        now = runtime if state > 0 else pausetime
283        return now
284
285
286    def _update_time(self, event:"Event") -> None:
287        """Checks if an event should be triggered based on elapsed time.
288
289        If the time since the last event trigger exceeds the timer threshold,
290        the event is posted and its last_time is updated.
291
292        Args:
293            event (object): An object with the following attributes:
294                - state (int): State used to determine which time value to use.
295                - last_time (float): Timestamp of the last event trigger.
296                - timer (float): Timer threshold in milliseconds.
297                - name (str): Name of the event to post.
298
299        Side Effects:
300            Posts the event via self.post() if the condition is met.
301            Updates event.last_time.
302        """
303        now = self._now(event.state)
304        diff = (now - event.last_time) * 1000
305        is_time = diff >= event.timer
306        if not is_time:
307            return
308
309        self.post(event.name)
310        event.last_time = now
311        event.calls += 1
312
313
314    @staticmethod
315    def _is_state(state: int, event_state: int)-> bool:
316        """Checks if the event state matches the given state or is neutral (0).
317
318        Args:
319            state (int): Target state to compare against.
320            event_state (int): Current state of the event.
321
322        Returns:
323            bool: True if the event_state equals the state or is 0, False otherwise.
324        """
325        same = event_state == state
326        is_zero = event_state == 0
327        return (same or is_zero)

A helper class to create custom events for the Scenes.

def create(self, name: str, state: int = 0, **kwargs: Any) -> int:
141    def create(self,name: str,state: int = 0,**kwargs: Any) -> int:
142        """
143        Create and store a new custom event.
144
145        Args:
146            name (str): The name of the event.
147            state (int): The event state, where:
148                - 1: Runtime
149                - -1: Pause time
150                - 0: Both (default = 0)
151            **kwargs: Additional arguments passed to the event's info. Can be any extra data needed for the event.
152
153        Returns:
154            int: The ID of the new event.
155        """
156        event_id = pygame.event.custom_type()
157        create_time = self._now(state)
158        last_time = create_time
159        basic_argv = {
160            "name":name,"custom":True,
161            "create_time":create_time,"calls":0,
162            "last_time":last_time,"timer":None,
163            "loops":None,"state":state
164        }
165
166        kwargs.update(basic_argv)
167        event = pygame.event.Event(
168            event_id,
169            kwargs
170        )
171
172        self.__data[name] = event
173        return event_id

Create and store a new custom event.

Arguments:
  • name (str): The name of the event.
  • state (int): The event state, where:
    • 1: Runtime
    • -1: Pause time
    • 0: Both (default = 0)
  • **kwargs: Additional arguments passed to the event's info. Can be any extra data needed for the event.
Returns:

int: The ID of the new event.

def get(self, name: str) -> pygame.event.Event:
175    def get(self,name: str) -> pygame.event.Event:
176        """
177        Get a custom event by its name.
178
179        Args:
180            name (str): The name of the event.
181
182        Returns:
183            pygame.event.Event or None: The event with the specified name, or None if not found.
184        """
185        return self.__data.get(name,None)

Get a custom event by its name.

Arguments:
  • name (str): The name of the event.
Returns:

pygame.event.Event or None: The event with the specified name, or None if not found.

def remove(self, name: str) -> SceneEvent:
187    def remove(self,name: str) -> "SceneEvent":
188        """
189        Remove a custom event by its name.
190
191        Args:
192            name (str): The name of the event.
193
194        Returns:
195            SceneEvent: The event that was removed
196        """
197        return self.__data.pop(event_name)

Remove a custom event by its name.

Arguments:
  • name (str): The name of the event.
Returns:

SceneEvent: The event that was removed

def post(self, name: str) -> bool:
199    def post(self,name: str) -> bool:
200        """
201        Post a custom event by its name.
202
203        Args:
204            name (str): The name of the event.
205
206        Returns:
207            bool: returns a boolean on whether the event was posted or not
208        """
209        event = self.get(name)
210        return pygame.event.post(event)

Post a custom event by its name.

Arguments:
  • name (str): The name of the event.
Returns:

bool: returns a boolean on whether the event was posted or not

def match(self, name: str, other_event: SceneEvent) -> bool:
212    def match(self,name: str,other_event: "SceneEvent") -> bool:
213        """
214        Check if a custom event matches by its name.
215
216        Args:
217            name (str): The name of the event.
218            other_event (SceneEvent): The event to compare against.
219
220        Returns:
221            bool: True if the events match, False otherwise.
222        """
223        event = self.get(name)
224        return event.type == other_event.type

Check if a custom event matches by its name.

Arguments:
  • name (str): The name of the event.
  • other_event (SceneEvent): The event to compare against.
Returns:

bool: True if the events match, False otherwise.

def schedule(self, name: str, timer: int, loops: int = -1) -> None:
227    def schedule(self,name: str,timer: int,loops: int = -1) -> None:
228        """
229        Shedule a custom event by it's name, for ms timer and loop times.
230
231        Args:
232            name (str): The name of the event.
233            timer (int): The time of the shedule in ms.
234            loops (int): The amount of loop times (default = -1, forever).
235        """
236        event = self.get(name)
237        event.timer = timer
238        event.loops = loops

Shedule a custom event by it's name, for ms timer and loop times.

Arguments:
  • name (str): The name of the event.
  • timer (int): The time of the shedule in ms.
  • loops (int): The amount of loop times (default = -1, forever).
class Scene:
329class Scene:
330    """Represents a scene in the game."""
331    _global_runtime = _global_pausetime = 0
332    __global_start_time = time()
333    def __init__(self,**kwargs: Any) -> None:
334        """
335        Initializes a Scene object.
336
337        Args:
338            **kwargs: Additional arguments passed to the scene. Can be any extra data needed for the scene.
339
340        Raises:
341            RuntimeError: If the Display has not been initialized. Call Display.init() first.
342        """
343        if not self.display.window:
344            engine.error(RuntimeError("Display has not been initialized! Call Display.init first."))
345            self.quit()
346
347        self.__initialize(kwargs)
348
349    @classproperty
350    def manager(cls) -> Type[SceneManager]:
351        """Class Property to get the scene manager class"""
352        return SceneManager
353
354    @classproperty
355    def display(cls) -> Type[Display]:
356        """Class Property to get a direct reference to the engine Display class."""
357        return Display
358
359    @classproperty
360    def assets(cls) -> Type[Assets]:
361        """Class Property to get a direct reference to the engine Assets class."""
362        return Assets
363
364    @classproperty
365    def global_runtime(cls) -> float:
366        """Class Property to get the total run time across all scenes."""
367        return cls._global_runtime
368
369    @classproperty
370    def global_pausetime(cls) -> float:
371        """Class Property to get the total pause time across all scenes."""
372        return cls._global_pausetime
373
374    @property
375    def camera(self) -> SceneCamera:
376        """Property to get the event camera instance of the current scene."""
377        return self._camera
378
379    @property
380    def event(self) -> SceneEvent:
381        """Property to get the event handler instance of the current scene."""
382        return self._event
383
384    @property
385    def events(self) -> set:
386        """Property to get all the events of the current frame."""
387        return self._events
388
389    @property
390    def custom_events(self) -> set:
391        """Property to get all the custom events of the current frame."""
392        return self._custom_events
393
394    @property
395    def keys_pressed(self) -> set:
396        """Property to get the keys currently pressed of the current frame."""
397        return self._keys_pressed
398
399    @property
400    def dt(self) -> float:
401        """Property to get the time elapsed since the last frame."""
402        return self._dt
403
404    @property
405    def fps(self) -> float:
406        """Property to get the current frames per second."""
407        return self._fps
408
409    @property
410    def max_fps(self) -> int:
411        """Property to get the maximum frames per second limit."""
412        return self._max_fps
413
414    @max_fps.setter
415    def max_fps(self, value: int):
416        """Setter to set the maximum frames per second limit."""
417        self._max_fps = value
418
419    @property
420    def background_color(self) -> str | Tuple[int, int, int]:
421        """Property to get the background color"""
422        return self._background_color
423
424    @background_color.setter
425    def background_color(self, value: str | Tuple[int, int, int]):
426        """Setter to set the background color"""
427        self._background_color = value
428
429    @property
430    def runtime(self) -> float:
431        """Property to get the run time"""
432        return self._runtime
433
434    @property
435    def pausetime(self) -> float:
436        """Property to get the pause time"""
437        return self._pausetime
438
439    # Utils
440    def is_time(self,ms):
441        """Checks if a specified time interval has elapsed since the last frame."""
442        multiplier = 1/ms*1000
443        return int(self._runtime * multiplier) != int((self._runtime - self._dt) * multiplier)
444
445    def is_event(self,event_id):
446        """Checks if an event is happening during the frame. """
447        return event_id in self._events
448
449    def is_custom_event(self,event_name):
450        """Checks if a custom event is happening during the frame. """
451        return event_name in self._custom_events
452
453    def is_paused(self):
454        """Returns if the scene is paused."""
455        return self.__paused
456
457    # Lifecycle Methods
458    def _start(self) -> None:
459        """
460        @public
461        Called once at the start of the scene. You must Override this func in your subclass.
462
463        Raises:
464            NotImplementedError: If not overridden.
465        """
466        raise NotImplementedError("start() must be overridden in subclass.")
467
468    def _update(self) -> None:
469        """
470        @public
471        Called every frame to update scene logic. You must Override this func in your subclass.
472
473        Raises:
474            NotImplementedError: If not overridden.
475        """
476        raise NotImplementedError("update() must be overridden in subclass.")
477
478    def _draw(self) -> None:
479        """
480        @public
481        Called every frame to draw elements to the screen. You must Override this func in your subclass.
482
483        Raises:
484            NotImplementedError: If not overridden.
485        """
486        raise NotImplementedError("draw() must be overridden in subclass.")
487
488    # Paused Lifecycle Methods
489    def _paused_update(self) -> None:
490        """@public Called every paused frame to update scene logic. Override this func in your subclass."""
491        pass
492    def _paused_draw(self) -> None:
493        """@public Called every paused frame to draw elements to the screen. Override this func in your subclass."""
494        pass
495
496    # Scene State Change Methods
497    def _on_create(self) -> None:
498        """@public Called once at the scene creation "manager.create()". Override this func in your subclass to add code."""
499        pass
500    def _on_quit(self) -> None:
501        """@public Called once at every scene quit "manager.quit()". Override this func in your subclass to add code."""
502        pass
503    def _on_restart(self) -> None:
504        """@public Called once at every scene restart "manager.restart()". Override this func in your subclass to add code."""
505        pass
506    def _on_reset(self) -> None:
507        """@public Called once at the scene reset "manager.reset()". Override this func in your subclass to add code."""
508        pass
509    def _on_change(self) -> None:
510        """@public Called once at the scene change "manager.change()". Override this func in your subclass to add code."""
511        pass
512    def _on_resume(self) -> None:
513        """@public Called once at the scene resume "manager.resume()". Override this func in your subclass to add code."""
514        pass
515    def _on_pause(self) -> None:
516        """@public Called once at the scene pause "manager.pause()". Override this func in your subclass to add code."""
517        pass
518    def _on_error(self,error: BaseException) -> None:
519        """
520        @public
521        Called once at engine error "Scene.__handle_error()". Override this func in your subclass to add code.
522
523        Args:
524            error (BaseException): The engine error that occurred.
525        """
526        pass
527
528    # Scene Event/Input Methods
529    def _on_event(self, event: pygame.Event) -> None:
530        """
531        @public
532        Called every pygame event. Override this func in your subclass to add code.
533
534        Args:
535            event (pygame.Event): The pygame event that occurred.
536        """
537        pass
538    def _on_keydown(self,key: str) -> None:
539        """
540        @public
541        Called every keyboard keydown. Override this func in your subclass to add code.
542
543        Args:
544            key (str): The keyboard key.
545        """
546        pass
547    def _on_keyup(self,key: str) -> None:
548        """
549        @public
550        Called every keyboard keyup. Override this func in your subclass to add code.
551
552        Args:
553            key (str): The keyboard key.
554        """
555        pass
556    def _on_keypressed(self,key: str) -> None:
557        """
558        @public
559        Called every keypressed. Override this func in your subclass to add code.
560
561        Args:
562            key (str): The keyboard key.
563        """
564        pass
565    def _on_mousewheel(self,wheel: int) -> None:
566        """
567        @public
568        Called every mousewheel change. Override this func in your subclass to add code.
569
570        Args:
571            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
572        """
573        pass
574
575    # Paused Event/Input Methods
576    def _on_paused_event(self, event: pygame.event.Event) -> None:
577        """
578        @public
579        Called every paused pygame event. Override this func in your subclass to add code.
580
581        Args:
582            event (pygame.Event): The pygame event that occurred.
583        """
584        pass
585    def _on_paused_keydown(self,key: str) -> None:
586        """
587        @public
588        Called every paused keyboard keydown. Override this func in your subclass to add code.
589
590        Args:
591            key (str): The keyboard key.
592        """
593    def _on_paused_keyup(self,key: str) -> None:
594        """
595        @public
596        Called every paused keyboard keypressed. Override this func in your subclass to add code.
597
598        Args:
599            key (str): The keyboard key.
600        """
601        pass
602    def _on_paused_keypressed(self,key: str) -> None:
603        """
604        @public
605        Called every paused keypressed. Override this func in your subclass to add code.
606
607        Args:
608            key (str): The keyboard key.
609        """
610        pass
611    def _on_paused_mousewheel(self,wheel: int) -> None:
612        """
613        @public
614        Called every paused mousewheel change. Override this func in your subclass to add code.
615
616        Args:
617            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
618        """
619        pass
620
621    # Main Loop
622    # Async to support pygbag export
623    async def __run(self) -> None:
624        """
625        Starts the scene.
626
627        Raises:
628            Exception: If there is any error in the scene.
629        """
630        try:
631            self.__initialize_runtime()
632            self._start()
633
634            while self.__running:
635                self.__handle_events()
636                self.__update()
637                self.__render()
638                self.__flip()
639
640                await asyncio.sleep(0)
641
642        except Exception as e:
643            self.__handle_error(e)
644
645    # All the methods below are used to handle the scene frames.
646    def __initialize(self,kwargs):
647        try:
648            self._max_fps = 60
649            self._background_color = (0, 0, 0)
650
651            self.__running = True
652            self.__paused = False
653
654            # set it here because the assets are loaded after the scene is initialized
655            Display.set_icon(
656                Assets.get("engine","images","icon")
657            )
658
659            self.__event_handlers = {
660                True:  (self._on_paused_keydown, self._on_paused_keyup, self._on_paused_keypressed, self._on_paused_mousewheel, self._on_paused_event),
661                False: (self._on_keydown,        self._on_keyup,        self._on_keypressed,        self._on_mousewheel,        self._on_event)
662            }
663
664            # set manual the scene kwargs to the scene
665            for k, v in kwargs.items():
666                setattr(self, k, v)
667            self._on_create()
668        except Exception as e:
669            # expect on_create error as is not in the main loop
670            self.__handle_error(e)
671
672    def __initialize_runtime(self):
673        """Sets up initial basic runtime values."""
674        self._dt = self._fps = self._runtime = self._pausetime = 0
675        self._keys_pressed = set() # we manually keep track with the key_pressed every frame, set so no duplicates
676        self._events = set() # log events every frame
677        self._custom_events = set() # log custom events every frame
678
679        # Create custom Scene events
680        self._event = SceneEvent(self)
681        self._camera = SceneCamera()
682
683        self._start_time = time()
684        self.__running = True
685
686    def __handle_error(self,e):
687        """ Handles every possible error with a nice message."""
688        self._on_error(e)
689        engine.error(e)
690        engine.quit()
691
692    def __handle_events(self):
693        """Handles events during runtime or when paused."""
694        self._events.clear()
695        self._custom_events.clear()
696        self._event.update(-1 if self.__paused else 1)
697
698        on_keydown, on_keyup, on_keypressed, on_wheel, on_event = self.__event_handlers[self.__paused]
699        for event in pygame.event.get():
700            self._events.add(event.type)
701
702            if hasattr(event, "custom"):
703                self._custom_events.add(event.name)
704                continue
705
706            if event.type == pygame.QUIT:
707                self.manager.quit()
708
709            elif event.type == pygame.KEYDOWN:
710                key = pygame.key.name(event.key)
711                self._keys_pressed.add(key)
712                on_keydown(key)
713
714            elif event.type == pygame.KEYUP:
715                key = pygame.key.name(event.key)
716                self._keys_pressed.discard(key)
717                on_keyup(key)
718
719            elif event.type == pygame.MOUSEWHEEL:
720                wheel = "up" if event.y >= 1 else "down"
721                on_wheel(wheel)
722
723            elif event.type == pygame.VIDEORESIZE:
724                Display.set_res((event.w, event.h))
725                Display._dynamic_zoom and self.camera._dynamic_zoom()
726            on_event(event)
727
728        if self._keys_pressed:
729            on_keypressed(self._keys_pressed)
730
731
732    def __update(self):
733        """Update the scene and timers, depending on whether it's paused or active."""
734        update = self._paused_update if self.__paused else self._update
735        update_timers = self.__update_paused_timers if self.__paused else self.__update_timers
736        update()
737        update_timers()
738
739    def __update_timers(self):
740        """Updates global and local runtime timers."""
741        delta = time() - self._start_time
742        global_delta = time() - self.__global_start_time
743
744        self._runtime = delta - self._pausetime
745        self._global_runtime = global_delta - self._global_pausetime
746
747    def __update_paused_timers(self):
748        """Tracks time spent in pause mode."""
749
750        # That was the easiest way to track the duration during the pause
751        # but not the best :D
752        delta = time() - self.__pause_last_time
753
754        self._pausetime += delta
755        self._global_pausetime += delta
756
757        self.__pause_last_time = time()
758
759    # no point to update the fps every frame as is hard to tell with the eye
760    # maybe i should change it to average instead?
761    def __update_fps(self):
762        """Updates the current scene fps every 250ms."""
763        if self.is_time(250):
764            self._fps = Display.clock.get_fps()
765
766    def __render(self):
767        """Renders the scene."""
768        if not self.__paused: # skip background if paused to keep the last frame render
769            self.__draw_background()
770        (self._paused_draw if self.__paused else self._draw)()
771
772    def __draw_background(self):
773        """Clears the screen with the background color."""
774        Display.surface.fill(self._background_color)
775
776    def __draw_display(self):
777        """Draws the display."""
778        surf = Display.get_stretch_surf() if Display.is_resized() else Display.surface
779        Display.window.blit(surf,(0,0))
780
781    def __flip(self):
782        """Updates the display with the latest frame."""
783        self.__update_fps() # I think updating the fps before the flip is the best place?
784        self._dt = round(Display.clock.tick(self._max_fps) / 1000, 3) # Also take the dt
785        self.__draw_display()
786        pygame.display.flip()

Represents a scene in the game.

Scene(**kwargs: Any)
333    def __init__(self,**kwargs: Any) -> None:
334        """
335        Initializes a Scene object.
336
337        Args:
338            **kwargs: Additional arguments passed to the scene. Can be any extra data needed for the scene.
339
340        Raises:
341            RuntimeError: If the Display has not been initialized. Call Display.init() first.
342        """
343        if not self.display.window:
344            engine.error(RuntimeError("Display has not been initialized! Call Display.init first."))
345            self.quit()
346
347        self.__initialize(kwargs)

Initializes a Scene object.

Arguments:
  • **kwargs: Additional arguments passed to the scene. Can be any extra data needed for the scene.
Raises:
  • RuntimeError: If the Display has not been initialized. Call Display.init() first.
manager: Type[SceneManager]
349    @classproperty
350    def manager(cls) -> Type[SceneManager]:
351        """Class Property to get the scene manager class"""
352        return SceneManager

Class Property to get the scene manager class

display: Type[pyxora.display.Display]
354    @classproperty
355    def display(cls) -> Type[Display]:
356        """Class Property to get a direct reference to the engine Display class."""
357        return Display

Class Property to get a direct reference to the engine Display class.

assets: Type[pyxora.assets.Assets]
359    @classproperty
360    def assets(cls) -> Type[Assets]:
361        """Class Property to get a direct reference to the engine Assets class."""
362        return Assets

Class Property to get a direct reference to the engine Assets class.

global_runtime: float
364    @classproperty
365    def global_runtime(cls) -> float:
366        """Class Property to get the total run time across all scenes."""
367        return cls._global_runtime

Class Property to get the total run time across all scenes.

global_pausetime: float
369    @classproperty
370    def global_pausetime(cls) -> float:
371        """Class Property to get the total pause time across all scenes."""
372        return cls._global_pausetime

Class Property to get the total pause time across all scenes.

camera: pyxora.camera.Camera
374    @property
375    def camera(self) -> SceneCamera:
376        """Property to get the event camera instance of the current scene."""
377        return self._camera

Property to get the event camera instance of the current scene.

event: SceneEvent
379    @property
380    def event(self) -> SceneEvent:
381        """Property to get the event handler instance of the current scene."""
382        return self._event

Property to get the event handler instance of the current scene.

events: set
384    @property
385    def events(self) -> set:
386        """Property to get all the events of the current frame."""
387        return self._events

Property to get all the events of the current frame.

custom_events: set
389    @property
390    def custom_events(self) -> set:
391        """Property to get all the custom events of the current frame."""
392        return self._custom_events

Property to get all the custom events of the current frame.

keys_pressed: set
394    @property
395    def keys_pressed(self) -> set:
396        """Property to get the keys currently pressed of the current frame."""
397        return self._keys_pressed

Property to get the keys currently pressed of the current frame.

dt: float
399    @property
400    def dt(self) -> float:
401        """Property to get the time elapsed since the last frame."""
402        return self._dt

Property to get the time elapsed since the last frame.

fps: float
404    @property
405    def fps(self) -> float:
406        """Property to get the current frames per second."""
407        return self._fps

Property to get the current frames per second.

max_fps: int
409    @property
410    def max_fps(self) -> int:
411        """Property to get the maximum frames per second limit."""
412        return self._max_fps

Property to get the maximum frames per second limit.

background_color: Union[str, Tuple[int, int, int]]
419    @property
420    def background_color(self) -> str | Tuple[int, int, int]:
421        """Property to get the background color"""
422        return self._background_color

Property to get the background color

runtime: float
429    @property
430    def runtime(self) -> float:
431        """Property to get the run time"""
432        return self._runtime

Property to get the run time

pausetime: float
434    @property
435    def pausetime(self) -> float:
436        """Property to get the pause time"""
437        return self._pausetime

Property to get the pause time

def is_time(self, ms):
440    def is_time(self,ms):
441        """Checks if a specified time interval has elapsed since the last frame."""
442        multiplier = 1/ms*1000
443        return int(self._runtime * multiplier) != int((self._runtime - self._dt) * multiplier)

Checks if a specified time interval has elapsed since the last frame.

def is_event(self, event_id):
445    def is_event(self,event_id):
446        """Checks if an event is happening during the frame. """
447        return event_id in self._events

Checks if an event is happening during the frame.

def is_custom_event(self, event_name):
449    def is_custom_event(self,event_name):
450        """Checks if a custom event is happening during the frame. """
451        return event_name in self._custom_events

Checks if a custom event is happening during the frame.

def is_paused(self):
453    def is_paused(self):
454        """Returns if the scene is paused."""
455        return self.__paused

Returns if the scene is paused.

def _start(self) -> None:
458    def _start(self) -> None:
459        """
460        @public
461        Called once at the start of the scene. You must Override this func in your subclass.
462
463        Raises:
464            NotImplementedError: If not overridden.
465        """
466        raise NotImplementedError("start() must be overridden in subclass.")

Called once at the start of the scene. You must Override this func in your subclass.

Raises:
  • NotImplementedError: If not overridden.
def _update(self) -> None:
468    def _update(self) -> None:
469        """
470        @public
471        Called every frame to update scene logic. You must Override this func in your subclass.
472
473        Raises:
474            NotImplementedError: If not overridden.
475        """
476        raise NotImplementedError("update() must be overridden in subclass.")

Called every frame to update scene logic. You must Override this func in your subclass.

Raises:
  • NotImplementedError: If not overridden.
def _draw(self) -> None:
478    def _draw(self) -> None:
479        """
480        @public
481        Called every frame to draw elements to the screen. You must Override this func in your subclass.
482
483        Raises:
484            NotImplementedError: If not overridden.
485        """
486        raise NotImplementedError("draw() must be overridden in subclass.")

Called every frame to draw elements to the screen. You must Override this func in your subclass.

Raises:
  • NotImplementedError: If not overridden.
def _paused_update(self) -> None:
489    def _paused_update(self) -> None:
490        """@public Called every paused frame to update scene logic. Override this func in your subclass."""
491        pass

Called every paused frame to update scene logic. Override this func in your subclass.

def _paused_draw(self) -> None:
492    def _paused_draw(self) -> None:
493        """@public Called every paused frame to draw elements to the screen. Override this func in your subclass."""
494        pass

Called every paused frame to draw elements to the screen. Override this func in your subclass.

def _on_create(self) -> None:
497    def _on_create(self) -> None:
498        """@public Called once at the scene creation "manager.create()". Override this func in your subclass to add code."""
499        pass

Called once at the scene creation "manager.create()". Override this func in your subclass to add code.

def _on_quit(self) -> None:
500    def _on_quit(self) -> None:
501        """@public Called once at every scene quit "manager.quit()". Override this func in your subclass to add code."""
502        pass

Called once at every scene quit "manager.quit()". Override this func in your subclass to add code.

def _on_restart(self) -> None:
503    def _on_restart(self) -> None:
504        """@public Called once at every scene restart "manager.restart()". Override this func in your subclass to add code."""
505        pass

Called once at every scene restart "manager.restart()". Override this func in your subclass to add code.

def _on_reset(self) -> None:
506    def _on_reset(self) -> None:
507        """@public Called once at the scene reset "manager.reset()". Override this func in your subclass to add code."""
508        pass

Called once at the scene reset "manager.reset()". Override this func in your subclass to add code.

def _on_change(self) -> None:
509    def _on_change(self) -> None:
510        """@public Called once at the scene change "manager.change()". Override this func in your subclass to add code."""
511        pass

Called once at the scene change "manager.change()". Override this func in your subclass to add code.

def _on_resume(self) -> None:
512    def _on_resume(self) -> None:
513        """@public Called once at the scene resume "manager.resume()". Override this func in your subclass to add code."""
514        pass

Called once at the scene resume "manager.resume()". Override this func in your subclass to add code.

def _on_pause(self) -> None:
515    def _on_pause(self) -> None:
516        """@public Called once at the scene pause "manager.pause()". Override this func in your subclass to add code."""
517        pass

Called once at the scene pause "manager.pause()". Override this func in your subclass to add code.

def _on_error(self, error: BaseException) -> None:
518    def _on_error(self,error: BaseException) -> None:
519        """
520        @public
521        Called once at engine error "Scene.__handle_error()". Override this func in your subclass to add code.
522
523        Args:
524            error (BaseException): The engine error that occurred.
525        """
526        pass

Called once at engine error "Scene.__handle_error()". Override this func in your subclass to add code.

Arguments:
  • error (BaseException): The engine error that occurred.
def _on_event(self, event: pygame.event.Event) -> None:
529    def _on_event(self, event: pygame.Event) -> None:
530        """
531        @public
532        Called every pygame event. Override this func in your subclass to add code.
533
534        Args:
535            event (pygame.Event): The pygame event that occurred.
536        """
537        pass

Called every pygame event. Override this func in your subclass to add code.

Arguments:
  • event (pygame.Event): The pygame event that occurred.
def _on_keydown(self, key: str) -> None:
538    def _on_keydown(self,key: str) -> None:
539        """
540        @public
541        Called every keyboard keydown. Override this func in your subclass to add code.
542
543        Args:
544            key (str): The keyboard key.
545        """
546        pass

Called every keyboard keydown. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_keyup(self, key: str) -> None:
547    def _on_keyup(self,key: str) -> None:
548        """
549        @public
550        Called every keyboard keyup. Override this func in your subclass to add code.
551
552        Args:
553            key (str): The keyboard key.
554        """
555        pass

Called every keyboard keyup. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_keypressed(self, key: str) -> None:
556    def _on_keypressed(self,key: str) -> None:
557        """
558        @public
559        Called every keypressed. Override this func in your subclass to add code.
560
561        Args:
562            key (str): The keyboard key.
563        """
564        pass

Called every keypressed. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_mousewheel(self, wheel: int) -> None:
565    def _on_mousewheel(self,wheel: int) -> None:
566        """
567        @public
568        Called every mousewheel change. Override this func in your subclass to add code.
569
570        Args:
571            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
572        """
573        pass

Called every mousewheel change. Override this func in your subclass to add code.

Arguments:
  • wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
def _on_paused_event(self, event: pygame.event.Event) -> None:
576    def _on_paused_event(self, event: pygame.event.Event) -> None:
577        """
578        @public
579        Called every paused pygame event. Override this func in your subclass to add code.
580
581        Args:
582            event (pygame.Event): The pygame event that occurred.
583        """
584        pass

Called every paused pygame event. Override this func in your subclass to add code.

Arguments:
  • event (pygame.Event): The pygame event that occurred.
def _on_paused_keydown(self, key: str) -> None:
585    def _on_paused_keydown(self,key: str) -> None:
586        """
587        @public
588        Called every paused keyboard keydown. Override this func in your subclass to add code.
589
590        Args:
591            key (str): The keyboard key.
592        """

Called every paused keyboard keydown. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_paused_keyup(self, key: str) -> None:
593    def _on_paused_keyup(self,key: str) -> None:
594        """
595        @public
596        Called every paused keyboard keypressed. Override this func in your subclass to add code.
597
598        Args:
599            key (str): The keyboard key.
600        """
601        pass

Called every paused keyboard keypressed. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_paused_keypressed(self, key: str) -> None:
602    def _on_paused_keypressed(self,key: str) -> None:
603        """
604        @public
605        Called every paused keypressed. Override this func in your subclass to add code.
606
607        Args:
608            key (str): The keyboard key.
609        """
610        pass

Called every paused keypressed. Override this func in your subclass to add code.

Arguments:
  • key (str): The keyboard key.
def _on_paused_mousewheel(self, wheel: int) -> None:
611    def _on_paused_mousewheel(self,wheel: int) -> None:
612        """
613        @public
614        Called every paused mousewheel change. Override this func in your subclass to add code.
615
616        Args:
617            wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.
618        """
619        pass

Called every paused mousewheel change. Override this func in your subclass to add code.

Arguments:
  • wheel (int): The wheel position, wheel>0 = up, wheel<1 = down.