From a942b02819d1a68696300e75ff2009ba51bc280b Mon Sep 17 00:00:00 2001 From: Calvin Spealman Date: Sun, 24 May 2020 12:47:53 -0400 Subject: [PATCH 1/2] Various render and event loop optimizations to share --- ppb/assetlib.py | 10 ++-- ppb/engine.py | 8 +-- ppb/scenes.py | 20 ++++++- ppb/systems/clocks.py | 2 +- ppb/systems/renderer.py | 117 ++++++++++++++++++++++++++-------------- 5 files changed, 108 insertions(+), 49 deletions(-) diff --git a/ppb/assetlib.py b/ppb/assetlib.py index 4d7bb580..60376b41 100644 --- a/ppb/assetlib.py +++ b/ppb/assetlib.py @@ -212,9 +212,13 @@ def load(self, timeout: float = None): Will block until the data is loaded. """ # NOTE: This is called by FreeingMixin.__del__() - if not self.is_loaded() and not _executor.running(): - logger.warning(f"Waited on {self!r} outside of the engine") - return self._future.result(timeout) + try: + return self._cached_result + except AttributeError: + if not self.is_loaded() and not _executor.running(): + logger.warning(f"Waited on {self!r} outside of the engine") + self._cached_result = self._future.result(timeout) + return self._cached_result class ChainingMixin(BackgroundMixin): diff --git a/ppb/engine.py b/ppb/engine.py index 60fd8089..a78d3fd5 100644 --- a/ppb/engine.py +++ b/ppb/engine.py @@ -226,7 +226,7 @@ def publish(self): callback(event) event_handler_name = _get_handler_name(type(event).__name__) - for obj in self.walk(): + for obj in self.get_objects_for_handler(event_handler_name): method = getattr(obj, event_handler_name, None) if callable(method): try: @@ -333,9 +333,9 @@ def _flush_events(self): """ self.events = deque() - def walk(self): + def get_objects_for_handler(self, event_handler_name): """ - Walk the object tree. + Walk the object tree for a specific event handler name. Publication order: The :class:`GameEngine`, the :class:`~ppb.systemslib.System` list, the current @@ -346,4 +346,4 @@ def walk(self): yield from self.systems yield self.current_scene if self.current_scene is not None: - yield from self.current_scene + yield from self.current_scene.get_objects_for_handler(event_handler_name) diff --git a/ppb/scenes.py b/ppb/scenes.py index d3d8f4eb..c634af87 100644 --- a/ppb/scenes.py +++ b/ppb/scenes.py @@ -11,6 +11,9 @@ from ppb.camera import Camera +get_layer = lambda s: getattr(s, "layer", 0) + + class GameObjectCollection(Collection): """A container for game objects.""" @@ -111,6 +114,7 @@ def __init__(self, *, setattr(self, k, v) self.game_objects = self.container_class() + self.event_handler_cache = {} if set_up is not None: set_up(self) @@ -120,6 +124,18 @@ def __contains__(self, item: Hashable) -> bool: def __iter__(self) -> Iterator: return (x for x in self.game_objects) + + def get_objects_for_handler(self, event_handler_name): + try: + return self.event_handler_cache[event_handler_name] + except: + results = [] + for obj in self: + method = getattr(obj, event_handler_name, None) + if callable(method): + results.append(obj) + self.event_handler_cache[event_handler_name] = results + return results @property def kinds(self): @@ -158,6 +174,7 @@ def add(self, game_object: Hashable, tags: Iterable=())-> None: scene.add(MyGameObject(), tags=("red", "blue") """ self.game_objects.add(game_object, tags) + self.event_handler_cache = {} def get(self, *, kind: Type=None, tag: Hashable=None, **kwargs) -> Iterator: """ @@ -192,6 +209,7 @@ def remove(self, game_object: Hashable) -> None: scene.remove(my_game_object) """ self.game_objects.remove(game_object) + self.event_handler_cache = {} def sprite_layers(self) -> Iterator: """ @@ -205,4 +223,4 @@ def sprite_layers(self) -> Iterator: This function exists primarily to assist the Renderer subsystem, but will be left public for other creative uses. """ - return sorted(self, key=lambda s: getattr(s, "layer", 0)) + return sorted(self, key=get_layer) diff --git a/ppb/systems/clocks.py b/ppb/systems/clocks.py index 0d7d2f95..ec03b821 100644 --- a/ppb/systems/clocks.py +++ b/ppb/systems/clocks.py @@ -7,7 +7,7 @@ class Updater(System): - def __init__(self, time_step=0.016, **kwargs): + def __init__(self, time_step=0.1, **kwargs): self.accumulated_time = 0 self.last_tick = None self.start_time = None diff --git a/ppb/systems/renderer.py b/ppb/systems/renderer.py index 79e88b70..04aad423 100644 --- a/ppb/systems/renderer.py +++ b/ppb/systems/renderer.py @@ -1,4 +1,5 @@ import ctypes +from functools import lru_cache import io import logging import random @@ -139,6 +140,9 @@ def __init__( self.target_frame_rate = target_frame_rate self.target_frame_length = 1 / self.target_frame_rate self.target_clock = get_time() + self.target_frame_length + self.last_opacity = 255 + self.last_opacity_mode = OPACITY_MODES[flags.BlendModeNone] + self.last_tint = (255, 255, 255) self._texture_cache = ObjectSideData() @@ -240,12 +244,17 @@ def prepare_resource(self, game_object): return None if not hasattr(game_object, '__image__'): - return + return None - image = game_object.__image__() - if image is None: + try: + image = game_object.__image__() + except AttributeError: return None + else: + if image is flags.DoNotRender or image is None: + return None + # Can change for animated objects, cannot reliable cache surface = image.load() try: texture = self._texture_cache[surface] @@ -261,24 +270,56 @@ def prepare_resource(self, game_object): opacity_mode = OPACITY_MODES[opacity_mode] tint = getattr(game_object, 'tint', (255, 255, 255)) - sdl_call( - SDL_SetTextureAlphaMod, texture.inner, opacity, - _check_error=lambda rv: rv < 0 - ) + if self.last_opacity != opacity: + self.last_opacity = opacity + sdl_call( + SDL_SetTextureAlphaMod, texture.inner, opacity, + _check_error=lambda rv: rv < 0 + ) - sdl_call( - SDL_SetTextureBlendMode, texture.inner, opacity_mode, - _check_error=lambda rv: rv < 0 - ) + if self.last_opacity_mode != opacity_mode: + self.last_opacity_mode = opacity_mode + sdl_call( + SDL_SetTextureBlendMode, texture.inner, opacity_mode, + _check_error=lambda rv: rv < 0 + ) - sdl_call( - SDL_SetTextureColorMod, texture.inner, tint[0], tint[1], tint[2], - _check_error=lambda rv: rv < 0 - ) + if self.last_tint != tint: + self.last_tine = tint + sdl_call( + SDL_SetTextureColorMod, texture.inner, tint[0], tint[1], tint[2], + _check_error=lambda rv: rv < 0 + ) return texture + _compute_cache = {} def compute_rectangles(self, texture, game_object, camera): + if hasattr(game_object, 'width'): + obj_w = game_object.width + obj_h = game_object.height + else: + obj_w, obj_h = game_object.size + + rect = getattr(game_object, 'rect', None) + key = (rect, id(texture), tuple(game_object.position), obj_w, obj_h, camera.pixel_ratio,) + + try: + win_w, win_h, src_rect, dest_rect = self._compute_cache[key] + except KeyError: + self._compute_cache[key] = self._compute_rectangles( + rect, + texture, + game_object.position, + obj_w, obj_h, camera.pixel_ratio, + camera, + ) + win_w, win_h, src_rect, dest_rect = self._compute_cache[key] + + return src_rect, dest_rect, ctypes.c_double(-game_object.rotation) + + @staticmethod + def _compute_rectangles(rect, texture, position, obj_width, obj_height, pixel_ratio, camera): flags = sdl2.stdinc.Uint32() access = ctypes.c_int() img_w = ctypes.c_int() @@ -289,17 +330,27 @@ def compute_rectangles(self, texture, game_object, camera): _check_error=lambda rv: rv < 0 ) - src_rect = SDL_Rect(x=0, y=0, w=img_w, h=img_h) - - if hasattr(game_object, 'width'): - obj_w = game_object.width - obj_h = game_object.height + if rect: + src_rect = SDL_Rect(*rect) + win_w = int(rect[2] * obj_width) + win_h = int(rect[3] * obj_height) else: - obj_w, obj_h = game_object.size - - win_w, win_h = self.target_resolution(img_w.value, img_h.value, obj_w, obj_h, camera.pixel_ratio) - - center = camera.translate_point_to_screen(game_object.position) + src_rect = SDL_Rect(x=0, y=0, w=img_w, h=img_h) + + if not obj_width: + print("no width") + ratio = img_h / (pixel_ratio * obj_height) + elif not obj_height: + print("no height") + ratio = img_w.value / (pixel_ratio * obj_width) + else: + ratio_w = img_w.value / (pixel_ratio * obj_width) + ratio_h = img_h.value / (pixel_ratio * obj_height) + ratio = min(ratio_w, ratio_h) # smaller value -> less reduction + + win_w, win_h = round(img_w.value / ratio), round(img_h.value / ratio) + + center = camera.translate_point_to_screen(position) dest_rect = SDL_Rect( x=int(center.x - win_w / 2), y=int(center.y - win_h / 2), @@ -307,18 +358,4 @@ def compute_rectangles(self, texture, game_object, camera): h=win_h, ) - return src_rect, dest_rect, ctypes.c_double(-game_object.rotation) - - @staticmethod - def target_resolution(img_width, img_height, obj_width, obj_height, pixel_ratio): - if not obj_width: - print("no width") - ratio = img_height / (pixel_ratio * obj_height) - elif not obj_height: - print("no height") - ratio = img_width / (pixel_ratio * obj_width) - else: - ratio_w = img_width / (pixel_ratio * obj_width) - ratio_h = img_height / (pixel_ratio * obj_height) - ratio = min(ratio_w, ratio_h) # smaller value -> less reduction - return round(img_width / ratio), round(img_height / ratio) + return win_w, win_h, src_rect, dest_rect From e3c4b84acd18c0ea2070a0fd9c60eaf03b21d505 Mon Sep 17 00:00:00 2001 From: Calvin Spealman Date: Sun, 24 May 2020 13:03:02 -0400 Subject: [PATCH 2/2] un-nerf update speed --- ppb/systems/clocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ppb/systems/clocks.py b/ppb/systems/clocks.py index ec03b821..0d7d2f95 100644 --- a/ppb/systems/clocks.py +++ b/ppb/systems/clocks.py @@ -7,7 +7,7 @@ class Updater(System): - def __init__(self, time_step=0.1, **kwargs): + def __init__(self, time_step=0.016, **kwargs): self.accumulated_time = 0 self.last_tick = None self.start_time = None