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

Refactors sprites.BaseSprite #357

Merged
merged 25 commits into from
Aug 28, 2019
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bbf23b2
Renames Rotatable to RotatableMixin
pathunstrom Aug 24, 2019
60ca123
Moves RotatableMixin higher in file.
pathunstrom Aug 24, 2019
318af49
Adds stubs that throw a deprecation warning and replace BaseSprite.
pathunstrom Aug 24, 2019
32bd32d
Adds Square Shape Mixin to ppb.sprites.
pathunstrom Aug 24, 2019
f99aeff
Pulls out RenderableMixin from BaseSprite.
pathunstrom Aug 24, 2019
b3785be
Reorganizers Sprite.
pathunstrom Aug 24, 2019
f763c15
BaseSprite stub needs to call super().__init__ to work properly.
pathunstrom Aug 24, 2019
674f57a
Size calculations have to happen in the SquareShapeMixin init.
pathunstrom Aug 24, 2019
84e445e
Removes unittest test_pos
pathunstrom Aug 24, 2019
60c8a12
Removes TestBaseSprite.test_center in favor of hypothesis tests.
pathunstrom Aug 24, 2019
0d78032
Parametrizes test_sides_center_plus_equals
pathunstrom Aug 24, 2019
109a548
Refacters TestBaseSprite.test_left
pathunstrom Aug 24, 2019
66edcf5
Refactors TestBaseSprite.test_right
pathunstrom Aug 24, 2019
fa1e4a7
Refactors TestBaseSprite.test_top
pathunstrom Aug 24, 2019
23bda32
Refactors TestBaseSprite.test_bottom
pathunstrom Aug 24, 2019
0905c61
Adds test to confirm ppb.BaseSprite warns of deprecation.
pathunstrom Aug 24, 2019
77b2aec
Refactors TestBaseSprite tests related to top_left
pathunstrom Aug 24, 2019
89fede0
Refactors top_right tests.
pathunstrom Aug 24, 2019
ec30ee6
Refactors bottom_right tests.
pathunstrom Aug 24, 2019
7321a33
Refactor bottom_left tests.
pathunstrom Aug 24, 2019
d169650
Refactors side.center tests.
pathunstrom Aug 24, 2019
1fba4b7
Finishes refactor of sides API: removes unittest!
pathunstrom Aug 24, 2019
ce1fe44
Touch up API documentation for sprites.
pathunstrom Aug 24, 2019
d82a35b
Change documentation and change inheritence order on Sprite mixins.
pathunstrom Aug 24, 2019
6c7c449
Fixes instantiating BaseSprite in test_camera.
pathunstrom Aug 24, 2019
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
23 changes: 21 additions & 2 deletions ppb/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
import logging
import warnings
from typing import Callable

from ppb import events
from ppb_vector import Vector
from ppb.engine import GameEngine
from ppb.scenes import BaseScene
from ppb.sprites import BaseSprite
from ppb.sprites import Sprite
from ppb.systems import Image
from ppb.systems import Sound

__all__ = (
# Shortcuts
'Vector', 'BaseScene', 'BaseSprite', 'Image', 'Sound', 'events',
'Vector', 'BaseScene', 'BaseSprite', 'Image', 'Sprite', 'Sound',
'events',
# Local stuff
'run', 'make_engine',
)


class BaseSprite(Sprite):
"""
A stub that raises a deprecation warning when a user uses
``ppb.BaseSprite.``
"""
__warning = """Using ppb.BaseSprite is deprecated.

You probably want ppb.Sprite. If you're wanting to use BaseSprite and
mixins to change what features your sprites have, import
ppb.sprites.BaseSprite.
"""

def __init__(self, **kwargs):
pathunstrom marked this conversation as resolved.
Show resolved Hide resolved
warnings.warn(self.__warning, DeprecationWarning)
super().__init__(**kwargs)


def _make_kwargs(setup, title, engine_opts):
kwargs = {
"resolution": (800, 600),
Expand Down
214 changes: 136 additions & 78 deletions ppb/sprites.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
from ppb.eventlib import EventMixin
from ppb.utils import FauxFloat

__all__ = (
"BaseSprite",
"Sprite",
"RotatableMixin",
"SquareShapeMixin",
"RenderableMixin",
)

TOP = "top"
BOTTOM = "bottom"
Expand All @@ -19,6 +26,113 @@
side_attribute_error_message = error_message.format


class BaseSprite(EventMixin):
"""
The base Sprite class. All sprites should inherit from this (directly or
indirectly).

The things that define a BaseSprite:

* The __event__ protocol (see ppb.eventlib.EventMixin)
* A position vector
* A layer
AstraLuma marked this conversation as resolved.
Show resolved Hide resolved

BaseSprite provides an __init__ method that sets attributes based on kwargs
to make rapid prototyping easier.
"""
#: (:py:class:`ppb.Vector`): Location of the sprite
position: Vector = Vector(0, 0)
#: The layer a sprite exists on.
layer: int = 0

def __init__(self, **kwargs):
super().__init__()

self.position = Vector(self.position)

# Initialize things
for k, v in kwargs.items():
# Abbreviations
if k == 'pos':
k = 'position'
# Castings
if k == 'position':
v = Vector(v)
setattr(self, k, v)


class RenderableMixin:
"""
A class implementing the API expected by ppb.systems.renderer.Renderer.

You should include RenderableMixin after BaseSprite in your parent class
pathunstrom marked this conversation as resolved.
Show resolved Hide resolved
definitions.
"""
#: (:py:class:`ppb.Image`): The image asset
image = None # TODO: Type hint appropriately

def __image__(self):
"""
Returns the sprite's image attribute if provided, or sets a default
one.
"""
if self.image is None:
klass = type(self)
prefix = Path(klass.__module__.replace('.', '/'))
try:
klassfile = getfile(klass)
except TypeError:
prefix = Path('.')
else:
if Path(klassfile).name != '__init__.py':
prefix = prefix.parent
if prefix == Path('.'):
self.image = ppb.Image(f"{klass.__name__.lower()}.png")
else:
self.image = ppb.Image(f"{prefix!s}/{klass.__name__.lower()}.png")
return self.image


class RotatableMixin:
"""
A simple rotation mixin. Can be included with sprites.
"""
_rotation = 0
# This is necessary to make facing do the thing while also being adjustable.
#: The baseline vector, representing the "front" of the sprite
basis = Vector(0, -1)
# Considered making basis private, the only reason to do so is to
# discourage people from relying on it as data.

@property
def facing(self):
"""
The direction the "front" is facing
"""
return Vector(*self.basis).rotate(self.rotation).normalize()

@facing.setter
def facing(self, value):
self.rotation = self.basis.angle(value)

@property
def rotation(self):
"""
The amount the sprite is rotated, in degrees
"""
return self._rotation

@rotation.setter
def rotation(self, value):
self._rotation = value % 360

def rotate(self, degrees):
"""
Rotate the sprite by a given angle (in degrees).
"""
self.rotation += degrees


class Side(FauxFloat):
"""
Acts like a float, but also has a variety of accessors.
Expand All @@ -30,7 +144,7 @@ class Side(FauxFloat):
BOTTOM: ('y', -1)
}

def __init__(self, parent: 'BaseSprite', side: str):
def __init__(self, parent: 'SquareShapeMixin', side: str):
self.side = side
self.parent = parent

Expand Down Expand Up @@ -165,74 +279,20 @@ def _attribute_gate(self, attribute, bad_sides):
raise AttributeError(message)


class Rotatable:
class SquareShapeMixin:
"""
A simple rotation mixin. Can be included with sprites.
"""
_rotation = 0
# This is necessary to make facing do the thing while also being adjustable.
#: The baseline vector, representing the "front" of the sprite
basis = Vector(0, -1)
# Considered making basis private, the only reason to do so is to
# discourage people from relying on it as data.

@property
def facing(self):
"""
The direction the "front" is facing
"""
return Vector(*self.basis).rotate(self.rotation).normalize()

@facing.setter
def facing(self, value):
self.rotation = self.basis.angle(value)

@property
def rotation(self):
"""
The amount the sprite is rotated, in degrees
"""
return self._rotation

@rotation.setter
def rotation(self, value):
self._rotation = value % 360

def rotate(self, degrees):
"""
Rotate the sprite by a given angle (in degrees).
"""
self.rotation += degrees

A mixin that applies square shapes to sprites.

class BaseSprite(EventMixin, Rotatable):
You should include SquareShapeMixin after ppb.sprites.BaseSprite in
pathunstrom marked this conversation as resolved.
Show resolved Hide resolved
your parent classes.
"""
The base Sprite class. All sprites should inherit from this (directly or
indirectly).
"""
#: (:py:class:`ppb.Image`): The image asset
image = None
#: (:py:class:`ppb.Vector`): Location of the sprite
position: Vector = Vector(0, 0)
#: The width/height of the sprite (sprites are square)
size: Union[int, float] = 1
#: The layer a sprite exists on.
layer: int = 0
#: Just here for typing and linting purposes. Your sprite should already have a position.
position: ppb_vector.Vector

def __init__(self, **kwargs):
super().__init__()

self.position = Vector(self.position)

# Initialize things
for k, v in kwargs.items():
# Abbreviations
if k == 'pos':
k = 'position'
# Castings
if k == 'position':
v = Vector(v)
setattr(self, k, v)
super().__init__(**kwargs)

# Trigger some calculations
self.size = self.size
Expand Down Expand Up @@ -296,19 +356,17 @@ def bottom(self, value):
def _offset_value(self):
return self.size / 2

def __image__(self):
if self.image is None:
klass = type(self)
prefix = Path(klass.__module__.replace('.', '/'))
try:
klassfile = getfile(klass)
except TypeError:
prefix = Path('.')
else:
if Path(klassfile).name != '__init__.py':
prefix = prefix.parent
if prefix == Path('.'):
self.image = ppb.Image(f"{klass.__name__.lower()}.png")
else:
self.image = ppb.Image(f"{prefix!s}/{klass.__name__.lower()}.png")
return self.image

class Sprite(BaseSprite, SquareShapeMixin, RenderableMixin, RotatableMixin):
"""
The default Sprite class.

Sprite includes:

* BaseSprite
* SquareShapeMixin
* RenderableMixin
* RotatableMixin

New in 0.7.0: Use this in place of BaseSprite in your games.
"""
2 changes: 2 additions & 0 deletions ppb/systems/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
DEFAULT_RESOLUTION = 800, 600


# TODO: Move Image out of the renderer so sprites can type hint
# appropriately.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been type-hinting with Asset to avoid this, but yeah, we have a circular dependency problem.

class Image(assets.Asset):
def background_parse(self, data):
return pygame.image.load(io.BytesIO(data), self.name).convert_alpha()
Expand Down
Loading