Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/code_quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,12 @@ jobs:
if: ${{ !cancelled() }}
run: uv run make.py ruff-check

# Type checking doesn't fail the check yet: there are existing errors
- name: Type Checking (MyPy)
if: ${{ !cancelled() }}
continue-on-error: true
run: uv run make.py mypy

- name: Type Checking (Pyright)
if: ${{ !cancelled() }}
continue-on-error: true
run: uv run make.py pyright

# This is a second job instead of an extra step because it takes the longest
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Fixed collision checks with `CollisionMethod.GPU` (also chosen automatically for lists of over 1500 sprites without a spatial hash) missing hit boxes that reach beyond a sprite's drawn size, such as a melee attack's reach. The GPU only knows the sprites' drawn sizes, so it now also uses the checking sprite's hit box, and each `SpriteList` keeps track of how far its sprites' hit boxes reach beyond their sizes.
- Fixed drawing inside `TextureAtlas.render_into()` when the atlas had to rebuild to make room, for example to add a new texture being drawn. A full atlas with freed space rebuilds instead of growing, which moved the texture being drawn into, so the drawing landed on other textures and what was drawn before was lost. While rendering into it, the atlas now grows instead if it can, and a rebuild keeps what was drawn and moves the drawing area with the texture.
- Fixed a `SpriteList` on WebGL only drawing its first 256 sprites when its storage was created with a capacity over 256: for example `SpriteList(capacity=1000)`, `clear()` on a list that had grown past 256 sprites, or a lazy list with more than 256 sprites before it was first drawn ([#2909](https://github.com/pythonarcade/arcade/issues/2909)).
- Fixed platformer tutorial steps 7 to 19, the `custom_sprite` example, and the lights tutorial crashing on startup with `AttributeError: property 'camera' ... has no setter`. They stored their camera as `self.camera` on the window, which is the default camera pyglet draws with. Their camera is now `self.world_camera`, and assigning `Window.camera` gives an error saying to use another name.
- Fixed `arcade.gui.experimental.pixelated_ui()` raising `AttributeError`. It now turns off text anti-aliasing and makes font glyphs use nearest-neighbor filtering, as documented.
- Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded.

### New Features
- Added `arcade.sweep_sprite(sprite, dx, dy, sprite_list)`, which checks the whole path of a moving sprite and returns a `SweepInfo` for the first sprite it would hit (the `sprite`, how far along the move as a `fraction` and `distance`, and the surface `normal`), or `None`. Fast sprites can't pass through thin walls this way. A sprite that already overlaps one is an immediate hit. Added the `sprite_bullets_sweep` example comparing it with a plain collision check.
Expand All @@ -43,6 +46,7 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- Tile maps no longer use Pillow's `Image.getdata()`, which is deprecated and will be removed in Pillow 14, to apply an image layer's transparent color. It's done with Pillow operations instead, which is also faster (a 1024x600 image went from 76 ms to 4 ms).
- Sped up removing many sprites from a large `SpriteList`, such as every bullet that hit something this frame. `SpriteList.remove()`, and so `Sprite.remove_from_sprite_lists()` and `kill()`, used to search the whole list for each sprite. Removals now wait until the list is next used, and many are applied in one pass: removing 100 sprites from 41,000 went from 19 ms to 3.3 ms, and 500 from 166,000 went from 630 ms to 17 ms. Drawing order is kept, and a few removals cost the same as before.
- Documented choosing a spatial hash cell size (`spatial_hash_cell_size`) in the performance tips and the `SpriteList` docs: about the size of the sprites in the list. With 4 pixel sprites, collision checks were 16 times faster with 8 pixel cells than with the default of 128.
- Type checking with mypy and pyright now fails the CI check, like formatting and linting. Their existing errors are fixed.

## 4.0.0.dev8

Expand Down
22 changes: 17 additions & 5 deletions arcade/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
if is_pyodide:
pyglet.options.backend = "webgl"

import pyglet.config
import pyglet.config.gl
import pyglet.window.mouse
from pyglet.config import GraphicsAPI
from pyglet.display.base import Screen, ScreenMode
from pyglet.enums import GraphicsAPI
from pyglet.event import EVENT_HANDLE_STATE, EVENT_UNHANDLED
from pyglet.window import MouseCursor

Expand Down Expand Up @@ -223,7 +223,7 @@ def __init__(
graphics_api = _GL_API_MAP[gl_api]
if antialiasing:
try:
config = pyglet.config.OpenGLUserConfig(
config = pyglet.config.gl.OpenGLUserConfig(
major_version=gl_version[0],
minor_version=gl_version[1],
api=graphics_api,
Expand All @@ -243,7 +243,7 @@ def __init__(
antialiasing = False
# If we still don't have a config
if not config:
config = pyglet.config.OpenGLUserConfig(
config = pyglet.config.gl.OpenGLUserConfig(
major_version=gl_version[0],
minor_version=gl_version[1],
api=graphics_api,
Expand Down Expand Up @@ -1099,7 +1099,9 @@ def default_camera(self) -> DefaultProjector:
"""
return self._ctx._default_camera

@property
# pyglet types this as one of its own cameras, but it needs to be arcade's
# DefaultProjector, so the override doesn't match
@property # type: ignore[override]
def camera(self) -> DefaultProjector:
"""
Alias for :py:attr:`default_camera`.
Expand All @@ -1109,9 +1111,19 @@ def camera(self) -> DefaultProjector:
set, so this needs to resolve to the same :py:class:`DefaultProjector`
to keep pyglet's internal drawing (e.g. :py:class:`~pyglet.text.Label`)
going through arcade's projection/scissor handling.

It can't be assigned. To keep your own camera on a window subclass,
give it another name, such as ``self.world_camera``.
"""
return self._ctx._default_camera

@camera.setter
def camera(self, value: object) -> None:
raise AttributeError(
"Window.camera is the default camera pyglet draws with, and can't be assigned. "
"Store your camera under another name, such as self.world_camera."
)

@property
def current_camera(self) -> Projector:
"""
Expand Down
3 changes: 2 additions & 1 deletion arcade/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ def __init__(self):
self.description: str = "Arcade Game Library CLI"

def register_command(self, command_class: Type[BaseCommand]) -> None:
command = command_class() # type: ignore BaseCommand has different constructor than it's implementations
# BaseCommand has a different constructor than its implementations
command = command_class() # type: ignore[call-arg]
self.commands[command.name] = command

def create_parser(self) -> argparse.ArgumentParser:
Expand Down
2 changes: 1 addition & 1 deletion arcade/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def reset(self) -> None:
self.projection_matrix = Mat4.orthogonal_projection(
0, self.window.width, 0, self.window.height, -100, 100
)
self._default_camera: DefaultProjector = DefaultProjector(context=self)
self._default_camera = DefaultProjector(context=self)
self.current_camera = self._default_camera
self.enable_only(self.BLEND)
self.blend_func = self.BLEND_DEFAULT
Expand Down
13 changes: 7 additions & 6 deletions arcade/examples/easing_example_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@

import arcade
from arcade.anim import ease, Easing
from arcade.anim.easing import EasingFunction

# --- Constants ---
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
WINDOW_TITLE = "Easing Example 1"

X_START = 40
X_END = 1200
X_START = 40.0
X_END = 1200.0
Y_INTERVAL = 60
BALL_RADIUS = 13
LINE_WIDTH = 1.0
Expand Down Expand Up @@ -48,8 +49,8 @@
class EasingCircle(arcade.SpriteCircle):
"""A ball that eases along the x-axis using a specific curve."""

def __init__(self, radius: int, color: arcade.types.RGBOrA255,
ease_function: Easing):
def __init__(self, radius: int, color: arcade.types.Color,
ease_function: EasingFunction):
super().__init__(radius, color)
self.ease_function = ease_function
self.start_time = 0.0
Expand All @@ -61,12 +62,12 @@ class GameView(arcade.View):
def __init__(self):
super().__init__()
self.background_color = BACKGROUND_COLOR
self.ball_list: arcade.SpriteList[EasingCircle] | None = None
self.ball_list: arcade.SpriteList[EasingCircle] = arcade.SpriteList()
self.time_elapsed = 0.0

def setup(self):
"""Create one ball per easing function."""
self.ball_list = arcade.SpriteList()
self.ball_list.clear()
self.time_elapsed = 0.0

for index, (label, ease_func) in enumerate(EASING_LIST):
Expand Down
11 changes: 5 additions & 6 deletions arcade/examples/easing_example_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,10 @@ def __init__(self):
super().__init__()
self.background_color = arcade.color.BLACK

self.ship_sprite: arcade.Sprite | None = None
self.ship_sprite = arcade.Sprite(
":resources:images/space_shooter/playerShip1_orange.png",
scale=0.5,
)
self.mode = 1
self.time_elapsed = 0.0

Expand All @@ -90,10 +93,6 @@ def __init__(self):

def setup(self):
"""Set up the game."""
self.ship_sprite = arcade.Sprite(
":resources:images/space_shooter/playerShip1_orange.png",
scale=0.5,
)
self.ship_sprite.center_x = WINDOW_WIDTH / 2
self.ship_sprite.center_y = WINDOW_HEIGHT / 2
self.time_elapsed = 0.0
Expand Down Expand Up @@ -154,7 +153,7 @@ def _start_position_ease(self):
def on_update(self, delta_time: float):
"""Update ship angle and/or position based on current mode."""
self.time_elapsed += delta_time
ease_func = MODE_EASING.get(self.mode)
ease_func = MODE_EASING.get(self.mode, Easing.LINEAR)

if self.mode == 1:
# Instant angle — always face the target directly
Expand Down
8 changes: 4 additions & 4 deletions arcade/examples/gl/custom_sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class GeoSprites(arcade.Window):

def __init__(self):
super().__init__(800, 600, "Custom Sprites", resizable=True)
self.camera = arcade.Camera2D()
self.world_camera = arcade.Camera2D()
self.program = self.ctx.program(
vertex_shader="""
#version 330
Expand Down Expand Up @@ -150,19 +150,19 @@ def __init__(self):

def on_resize(self, width: int, height: int):
super().on_resize(width, height)
self.camera.match_window(position=True)
self.world_camera.match_window(position=True)

def on_draw(self):
self.clear()
self.camera.use()
self.world_camera.use()
# Bind our sprite texture to channel 0
self.texture.use(unit=0)
# Render the sprite data with our shader
self.geometry.render(self.program)

def on_mouse_drag(self, x: float, y: float, dx: float, dy: float, buttons: int, modifiers: int):
"""Make it easier to explore the geometry by scrolling"""
self.camera.position = self.camera.position[0] - dx, self.camera.position[1] - dy
self.world_camera.position = self.world_camera.position[0] - dx, self.world_camera.position[1] - dy

def gen_sprites(self, count: int):
"""Quickly generate some random sprite data"""
Expand Down
11 changes: 5 additions & 6 deletions arcade/examples/gui/exp_animations_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,11 @@ def main():


if __name__ == "__main__":
import pyglet
# pyglet.options.text_antialiasing = False
#
from pyglet.graphics.api.gl import GL_NEAREST
pyglet.font.base.Font.texture_min_filter = GL_NEAREST
pyglet.font.base.Font.texture_mag_filter = GL_NEAREST
import pyglet.font.base
from pyglet.enums import TextureFilter

# Keep the pixel font sharp when it's scaled
pyglet.font.base.Font.filters = TextureFilter.NEAREST
resources.load_kenney_fonts()

main()
8 changes: 4 additions & 4 deletions arcade/examples/platform_tutorial/07_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(self):
self.wall_list = None

# A variable to store our camera object
self.camera = None
self.world_camera = None

def setup(self):
"""Set up the game here. Call this function to restart the game."""
Expand Down Expand Up @@ -96,7 +96,7 @@ def setup(self):
)

# Initialize our camera, setting a viewport the size of our window.
self.camera = arcade.Camera2D()
self.world_camera = arcade.Camera2D()

self.background_color = arcade.csscolor.CORNFLOWER_BLUE

Expand All @@ -107,7 +107,7 @@ def on_draw(self):
self.clear()

# Activate our camera before drawing
self.camera.use()
self.world_camera.use()

# Draw our sprites
self.player_list.draw()
Expand All @@ -120,7 +120,7 @@ def on_update(self, delta_time):
self.physics_engine.update()

# Center our camera on the player
self.camera.position = self.player_sprite.position
self.world_camera.position = self.player_sprite.position

def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed."""
Expand Down
8 changes: 4 additions & 4 deletions arcade/examples/platform_tutorial/08_coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(self):
self.coin_list = None

# A variable to store our camera object
self.camera = None
self.world_camera = None

def setup(self):
"""Set up the game here. Call this function to restart the game."""
Expand Down Expand Up @@ -108,7 +108,7 @@ def setup(self):
)

# Initialize our camera, setting a viewport the size of our window.
self.camera = arcade.Camera2D()
self.world_camera = arcade.Camera2D()

self.background_color = arcade.csscolor.CORNFLOWER_BLUE

Expand All @@ -119,7 +119,7 @@ def on_draw(self):
self.clear()

# Activate our camera before drawing
self.camera.use()
self.world_camera.use()

# Draw our sprites
self.player_list.draw()
Expand All @@ -143,7 +143,7 @@ def on_update(self, delta_time):
coin.remove_from_sprite_lists()

# Center our camera on the player
self.camera.position = self.player_sprite.position
self.world_camera.position = self.player_sprite.position

def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed."""
Expand Down
8 changes: 4 additions & 4 deletions arcade/examples/platform_tutorial/09_sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(self):
self.coin_list = None

# A variable to store our camera object
self.camera = None
self.world_camera = None

# Load sounds
self.collect_coin_sound = arcade.load_sound(":resources:sounds/coin1.wav")
Expand Down Expand Up @@ -112,7 +112,7 @@ def setup(self):
)

# Initialize our camera, setting a viewport the size of our window.
self.camera = arcade.Camera2D()
self.world_camera = arcade.Camera2D()

self.background_color = arcade.csscolor.CORNFLOWER_BLUE

Expand All @@ -123,7 +123,7 @@ def on_draw(self):
self.clear()

# Activate our camera before drawing
self.camera.use()
self.world_camera.use()

# Draw our sprites
self.player_list.draw()
Expand All @@ -148,7 +148,7 @@ def on_update(self, delta_time):
arcade.play_sound(self.collect_coin_sound)

# Center our camera on the player
self.camera.position = self.player_sprite.position
self.world_camera.position = self.player_sprite.position

def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed."""
Expand Down
8 changes: 4 additions & 4 deletions arcade/examples/platform_tutorial/10_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(self):
self.coin_list = None

# A variable to store our camera object
self.camera = None
self.world_camera = None

# A variable to store our gui camera object
self.gui_camera = None
Expand Down Expand Up @@ -121,7 +121,7 @@ def setup(self):
)

# Initialize our camera, setting a viewport the size of our window.
self.camera = arcade.Camera2D()
self.world_camera = arcade.Camera2D()

# Initialize our gui camera, initial settings are the same as our world camera.
self.gui_camera = arcade.Camera2D()
Expand All @@ -141,7 +141,7 @@ def on_draw(self):
self.clear()

# Activate our camera before drawing
self.camera.use()
self.world_camera.use()

# Draw our sprites
self.player_list.draw()
Expand Down Expand Up @@ -174,7 +174,7 @@ def on_update(self, delta_time):
self.score_text.text = f"Score: {self.score}"

# Center our camera on the player
self.camera.position = self.player_sprite.position
self.world_camera.position = self.player_sprite.position

def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed."""
Expand Down
Loading
Loading