Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sprite scale: Preserve performance #2389

Merged
merged 12 commits into from
Oct 14, 2024
3 changes: 2 additions & 1 deletion arcade/examples/particle_fireworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,10 +385,11 @@ def rocket_smoke_mutator(particle: LifetimeParticle):
particle.scale = lerp(
0.5,
3.0,
particle.lifetime_elapsed / particle.lifetime_original # type: ignore
particle.lifetime_elapsed / particle.lifetime_original, # type: ignore
)



def main():
""" Main function """
# Create a window class. This is what actually shows up on screen
Expand Down
2 changes: 1 addition & 1 deletion arcade/examples/sprite_explosion_particles.py
DragonMoffon marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def update(self, delta_time: float = 1/60):
self.alpha -= int(SMOKE_FADE_RATE * time_step)
self.center_x += self.change_x * time_step
self.center_y += self.change_y * time_step
self.scale += SMOKE_EXPANSION_RATE * time_step
self.add_scale(SMOKE_EXPANSION_RATE * time_step)


class Particle(arcade.SpriteCircle):
Expand Down
4 changes: 1 addition & 3 deletions arcade/examples/sprite_move_animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ def setup(self):
# Set up the player
self.score = 0
self.player = PlayerCharacter(self.idle_texture_pair, self.walk_texture_pairs)

self.player.center_x = WINDOW_WIDTH // 2
self.player.center_y = WINDOW_HEIGHT // 2
self.player.position = self.center
self.player.scale = 0.8

self.player_list.append(self.player)
Expand Down
84 changes: 46 additions & 38 deletions arcade/sprite/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

from typing import TYPE_CHECKING, Any, Iterable, TypeVar

from pyglet.math import Vec2

import arcade
from arcade.color import BLACK, WHITE
from arcade.exceptions import ReplacementWarning, warning
Expand Down Expand Up @@ -262,49 +260,24 @@ def scale_y(self, new_scale_y: AsFloat):
sprite_list._update_size(self)

@property
def scale(self) -> Vec2:
def scale(self) -> Point2:
"""Get or set the x & y scale of the sprite as a pair of values.
You may set both the x & y with a single scalar, but scale will always return
a length 2 tuple of the x & y scale

You may set it to either a single value or a pair of values:

.. list-table::
:header-rows: 0

* - Single value
- ``sprite.scale = 2.0``

* - Tuple or :py:class:`~pyglet,math.Vec2`
- ``sprite.scale = (1.0, 3.0)``

The two-channel version is useful for making health bars and
other indicators.

.. note:: Returns a :py:class:`pyglet.math.Vec2` for
compatibility.

Arcade versions lower than 3,0 used one or both of the following
for scale:
See :py:attr:`.scale_x` and :py:attr:`.scale_y` for individual access.

* A single :py:class:`float` on versions <= 2.6
* A ``scale_xy`` property and exposing only the x component
on some intermediate dev releases

Although scale is internally stored as a :py:class:`tuple`, we
return a :py:class:`pyglet.math.Vec2` to allow the in-place
operators to work in addition to setting values directly:

* Old-style (``sprite.scale *= 2.0``)
* New-style (``sprite.scale *= 2.0, 2.0``)
See :py:meth:`.scale_multiply_uniform` for uniform scaling.

.. note:: Negative scale values are supported.

This applies to both single-axis and dual-axis.
Negatives will flip & mirror the sprite, but the
with will use :py:func:`abs` to report total width
and height instead of negatives.
This applies to both single-axis and dual-axis.
Negatives will flip & mirror the sprite, but the
with will use :py:func:`abs` to report total width
and height instead of negatives.

"""
return Vec2(*self._scale)
return self._scale

@scale.setter
def scale(self, new_scale: Point2 | AsFloat):
Expand Down Expand Up @@ -607,6 +580,41 @@ def update_animation(self, delta_time: float = 1 / 60, *args, **kwargs) -> None:

# --- Scale methods -----

def add_scale(self, factor: AsFloat) -> None:
"""Add to the sprite's scale by the factor.
This adds the factor to both the x and y scale values.

Args:
factor: The factor to add to the sprite's scale.
"""
self._scale = self._scale[0] + factor, self._scale[1] + factor
self._hit_box.scale = self._scale
tex_width, tex_height = self._texture.size
self._width = tex_width * self._scale[0]
self._height = tex_height * self._scale[1]

self.update_spatial_hash()
for sprite_list in self.sprite_lists:
sprite_list._update_size(self)

def multiply_scale(self, factor: AsFloat) -> None:
"""multiply the sprite's scale by the factor.
This multiplies both the x and y scale values by the factor.

Args:
factor: The factor to scale up the sprite by.
"""

self._scale = self._scale[0] * factor, self._scale[1] * factor
self._hit_box.scale = self._scale
tex_width, tex_height = self._texture.size
self._width = tex_width * factor
self._height = tex_height * factor

self.update_spatial_hash()
for sprite_list in self.sprite_lists:
sprite_list._update_size(self)

def rescale_relative_to_point(self, point: Point2, scale_by: AsFloat | Point2) -> None:
"""Rescale the sprite and its distance from the passed point.

Expand Down Expand Up @@ -695,7 +703,7 @@ def rescale_xy_relative_to_point(self, point: Point, factors_xy: Iterable[float]
Use :py:meth:`.rescale_relative_to_point` instead.

This was added during the 3.0 development cycle before scale was
made into a vector quantitity.
made into a vector quantity.

This method can scale by different amounts on each axis. To
scale along only one axis, set the other axis to ``1.0`` in
Expand Down
8 changes: 8 additions & 0 deletions benchmarks/sprite/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

# Sprite Benchmark

Contains performance comparison between two alternative implementations of
sprite handling scaling somewhat differently. This was done at the end
of arcade 3.0 to measure performance of scaling changes.

This can be changed and adjusted as needed to test different scenarios.
148 changes: 148 additions & 0 deletions benchmarks/sprite/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""
Quick and dirty system measuring differences between two sprite classes.
"""
import gc
import math
import timeit
import arcade
from itertools import cycle
from random import random

from sprite_alt import BasicSprite as SpriteA
from arcade import BasicSprite as SpriteB

random_numbers = cycle(tuple(random() + 0.1 for _ in range(1009)))

N = 100
MEASUREMENT_CONFIG = [
{"name": "populate", "number": N, "measure_method": "populate", "post_methods": ["flush"]},
{"name": "scale_set", "number": N, "measure_method": "scale_set", "post_methods": []},
{"name": "scale_set_uniform", "number": N, "measure_method": "scale_set_uniform", "post_methods": []},
{"name": "scale_mult", "number": N, "measure_method": "scale_mult", "post_methods": []},
{"name": "scale_mult_uniform", "number": N, "measure_method": "scale_mult_uniform", "post_methods": []},
]


class Measurement:
def __init__(self, avg=0.0, min=0.0, max=0.0):
self.avg = avg
self.min = min
self.max = max

@classmethod
def from_values(cls, values: list[float]) -> "Measurement":
return cls(avg=sum(values) / len(values), min=min(values), max=max(values))

# TODO: Compare measurements

def __str__(self):
return f"avg={self.avg}, min={self.min}, max={self.max}"


class SpriteCollection:
sprite_type = None
sprite_count = 100_000

def __init__(self):
self.spritelist = arcade.SpriteList(lazy=True, capacity=self.sprite_count)

def flush(self):
"""Remove all sprites from the spritelist."""
self.spritelist.clear()

def populate(self):
"""Populate the spritelist with sprites."""
texture = arcade.load_texture(":assets:images/items/coinBronze.png")
N = int(math.sqrt(self.sprite_count))
for y in range(N):
for x in range(N):
self.spritelist.append(
self.sprite_type(
texture=texture,
center_x=x * 64,
center_y=y * 64,
scale=(1.0, 1.0),
)
)

# Scale
def scale_set(self):
"""Set the scale of all sprites."""
for sprite in self.spritelist:
sprite.scale = next(random_numbers)

def scale_set_uniform(self):
"""Set the scale of all sprites."""
for sprite in self.spritelist:
sprite.scale_set_uniform(next(random_numbers))

def scale_mult_uniform(self):
"""Multiply the scale of all sprites."""
for sprite in self.spritelist:
sprite.scale_multiply_uniform(next(random_numbers))

def scale_mult(self):
"""Multiply the scale of all sprites uniformly."""
for sprite in self.spritelist:
sprite.multiply_scale(next(random_numbers, 1.0))

# Rotate
# Move
# Collision detection


class SpriteCollectionA(SpriteCollection):
sprite_type = SpriteA

class SpriteCollectionB(SpriteCollection):
sprite_type = SpriteB


def measure_sprite_collection(collection: SpriteCollection, number=10) -> dict[str, Measurement]:
"""Perform actions on the sprite collections and measure the time."""
print(f"Measuring {collection.__class__.__name__}...")
measurements: dict[str, Measurement] = {}

for config in MEASUREMENT_CONFIG:
name = config["name"]
number = config["number"]
measure_method = getattr(collection, config["measure_method"])
post_methods = [getattr(collection, method) for method in config.get("post_methods", [])]

results = []
try:
for _ in range(number):
results.append(timeit.timeit(measure_method, number=1))
for method in post_methods:
method()
measurement = Measurement.from_values(results)
measurements[name] = measurement
print(f"{name}: {measurement}")
except Exception as e:
print(f"Failed to measure {name}: {e}")

collection.flush()
collection.populate()
gc_until_nothing()

return measurements


def gc_until_nothing():
"""Run the garbage collector until no more objects are found."""
while gc.collect():
pass


def main():
a = SpriteCollectionA()
b = SpriteCollectionB()

m1 = measure_sprite_collection(a)
gc_until_nothing()
m2 = measure_sprite_collection(b)
# FIXME: Compare measurements


if __name__ == '__main__':
main()
Loading
Loading