Mercurial > parpg-core
changeset 27:09b581087d68
Added base files for grease
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/__init__.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,75 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__versioninfo__ = (0, 3, 0) +__version__ = '.'.join(str(n) for n in __versioninfo__) + +__all__ = ('BaseWorld', 'Entity', 'System', 'Renderer') + +import grease.component +import grease.geometry +import grease.collision +from grease.entity import Entity +from grease.world import BaseWorld + +import abc + +class System(object): + """Grease system abstract base class. Systems define behaviorial aspects + of a |World|. All systems must define a :meth:`step` + method that is invoked by the world each timestep. User-defined systems + are not required to subclass this class. + + See :ref:`an example system from the tutorial <tut-system-example>`. + """ + __metaclass__ = abc.ABCMeta + + world = None + """The |World| this system belongs to""" + + def set_world(self, world): + """Bind the system to a world""" + self.world = world + + @abc.abstractmethod + def step(self, dt): + """Execute a time step for the system. Must be defined + by all system classes. + + :param dt: Time since last step invocation + :type dt: float + """ + +class Renderer(object): + """Grease renderer abstract base class. Renderers define the presentation + of a |World|. All renderers must define a :meth:`draw` + method that is invoked by the world when the display needs to be redrawn. + User-defined renderers are not required to subclass this class. + + See :ref:`an example renderer from the tutorial <tut-renderer-example>`. + """ + __metaclass__ = abc.ABCMeta + + world = None + """The |World| this renderer belongs to""" + + def set_world(self, world): + """Bind the system to a world""" + self.world = world + + @abc.abstractmethod + def draw(self): + """Issue drawing commands for this renderer. Must be defined + for all renderer classes. + """ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/collision.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,528 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +""" +**Grease collision detection systems** + +Grease uses two-phase broad and narrow collision detection. *Broad-phase* +collision systems are used to efficiently identify pairs that may be colliding +without resorting to a brute-force check of all possible pairs. *Narrow-phase* +collision systems use the pairs generated by the broad-phase and perform more +precise collision tests to determine if a collision has actually occurred. The +narrow-phase system also calculates more details about each collision, +including collision point and normal vector for use in collision response. + +A typical collision detection system consists of a narrow-phase system that +contains a broad-phased system. The narrow-phase system is usually the only + +one that the application directly interacts with, though the application is +free to use the broad-phased system directly if desired. This could be +useful in cases where speed, rather than precision is paramount. + +The narrow-phase system can be assigned handler objects to run after +collision detection. These can perform tasks like handling collision response +or dispatching collision events to application handlers. + +Note that broad-phase systems can return false positives, though they should +never return false negatives. Do not assume that all pairs returned by a +broad-phase system are actually in collision. +""" + +__version__ = '$Id$' + +from grease.geometry import Vec2d +from bisect import bisect_right + + +class Pair(tuple): + """Pair of entities in collision. This is an ordered sequence of two + entities, that compares and hashes unordered. + + Also stores additional collision point and normal vectors + for each entity. + + Sets of ``Pair`` objects are exposed in the ``collision_pairs`` + attribute of collision systems to indicate the entity pairs in + collision. + """ + info = None + """A sequence of (entity, collision point, collision normal) + for each entity in the pair + """ + + def __new__(cls, entity1, entity2, point=None, normal=None): + pair = tuple.__new__(cls, (entity1, entity2)) + return pair + + def __hash__(self): + return hash(self[0]) ^ hash(self[1]) + + def __eq__(self, other): + other = tuple(other) + return tuple(self) == other or (self[1], self[0]) == other + + def __repr__(self): + return '%s%r' % (self.__class__.__name__, tuple(self)) + + def set_point_normal(self, point0, normal0, point1, normal1): + """Set the collision point and normal for both entities""" + self.info = ( + (self[0], point0, normal0), + (self[1], point1, normal1), + ) + + +class BroadSweepAndPrune(object): + """2D Broad-phase sweep and prune bounding box collision detector + + This algorithm is efficient for collision detection between many + moving bodies. It has linear algorithmic complexity and takes + advantage of temporal coherence between frames. It also does + not suffer from bad worst-case performance (like RDC can). + Unlike spacial hashing, it does not need to be optimized for + specific space and body sizes. + + Other algorithms may be more efficient for collision detection with + stationary bodies, bodies that are always evenly distributed, or ad-hoc + queries. + + :param collision_component: Name of the collision component used by this + system, defaults to 'collision'. This component supplies each + entities' aabb and collision masks. + :type collision_component: str + """ + world = None + """|World| object this system belongs to""" + + collision_component = None + """Name of world's collision component used by this system""" + + LEFT_ATTR = "left" + RIGHT_ATTR = "right" + TOP_ATTR = "top" + BOTTOM_ATTR = "bottom" + + def __init__(self, collision_component='collision'): + self.collision_component = collision_component + self._by_x = None + self._by_y = None + self._collision_pairs = None + + def set_world(self, world): + """Bind the system to a world""" + self.world = world + + def step(self, dt): + """Update the system for this time step, updates and sorts the + axis arrays. + """ + component = getattr(self.world.components, self.collision_component) + LEFT = self.LEFT_ATTR + RIGHT = self.RIGHT_ATTR + TOP = self.TOP_ATTR + BOTTOM = self.BOTTOM_ATTR + if self._by_x is None: + # Build axis lists from scratch + # Note we cache the box positions here + # so that we can perform hit tests efficiently + # it also isolates us from changes made to the + # box positions after we run + by_x = self._by_x = [] + append_x = by_x.append + by_y = self._by_y = [] + append_y = by_y.append + for data in component.itervalues(): + append_x([data.aabb.left, LEFT, data]) + append_x([data.aabb.right, RIGHT, data]) + append_y([data.aabb.bottom, BOTTOM, data]) + append_y([data.aabb.top, TOP, data]) + else: + by_x = self._by_x + by_y = self._by_y + removed = [] + for entry in by_x: + entry[0] = getattr(entry[2].aabb, entry[1]) + for entry in by_y: + entry[0] = getattr(entry[2].aabb, entry[1]) + # Removing entities is inefficient, but expected to be rare + if component.deleted_entities: + deleted_entities = component.deleted_entities + deleted_x = [] + deleted_y = [] + for i, (_, _, data) in enumerate(by_x): + if data.entity in deleted_entities: + deleted_x.append(i) + deleted_x.reverse() + for i in deleted_x: + del by_x[i] + for i, (_, _, data) in enumerate(by_y): + if data.entity in deleted_entities: + deleted_y.append(i) + deleted_y.reverse() + for i in deleted_y: + del by_y[i] + # Tack on new entities + for entity in component.new_entities: + data = component[entity] + by_x.append([data.aabb.left, LEFT, data]) + by_x.append([data.aabb.right, RIGHT, data]) + by_y.append([data.aabb.bottom, BOTTOM, data]) + by_y.append([data.aabb.top, TOP, data]) + + # Tim-sort is highly efficient with mostly sorted lists. + # Because positions tend to change little each frame + # we take advantage of this here. Obviously things are + # less efficient with very fast moving, or teleporting entities + by_x.sort() + by_y.sort() + self._collision_pairs = None + + @property + def collision_pairs(self): + """Set of candidate collision pairs for this timestep""" + if self._collision_pairs is None: + if self._by_x is None: + # Axis arrays not ready + return set() + + LEFT = self.LEFT_ATTR + RIGHT = self.RIGHT_ATTR + TOP = self.TOP_ATTR + BOTTOM = self.BOTTOM_ATTR + # Build candidates overlapping along the x-axis + component = getattr(self.world.components, self.collision_component) + xoverlaps = set() + add_xoverlap = xoverlaps.add + discard_xoverlap = xoverlaps.discard + open = {} + for _, side, data in self._by_x: + if side is LEFT: + for open_entity, (from_mask, into_mask) in open.iteritems(): + if data.from_mask & into_mask or from_mask & data.into_mask: + add_xoverlap(Pair(data.entity, open_entity)) + open[data.entity] = (data.from_mask, data.into_mask) + elif side is RIGHT: + del open[data.entity] + + if len(xoverlaps) <= 10 and len(xoverlaps)*4 < len(self._by_y): + # few candidates were found, so just scan the x overlap candidates + # along y. This requires an additional sort, but it should + # be cheaper than scanning everyone and its simpler + # than a separate brute-force check + entities = set([entity for entity, _ in xoverlaps] + + [entity for _, entity in xoverlaps]) + by_y = [] + for entity in entities: + data = component[entity] + # We can use tuples here, which are cheaper to create + by_y.append((data.aabb.bottom, BOTTOM, data)) + by_y.append((data.aabb.top, TOP, data)) + by_y.sort() + else: + by_y = self._by_y + + # Now check the candidates along the y-axis + open = set() + add_open = open.add + discard_open = open.discard + self._collision_pairs = set() + add_pair = self._collision_pairs.add + for _, side, data in by_y: + if side is BOTTOM: + for open_entity in open: + pair = Pair(data.entity, open_entity) + if pair in xoverlaps: + discard_xoverlap(pair) + add_pair(pair) + if not xoverlaps: + # No more candidates, bail + return self._collision_pairs + add_open(data.entity) + elif side is TOP: + discard_open(data.entity) + return self._collision_pairs + + def query_point(self, x_or_point, y=None, from_mask=0xffffffff): + """Hit test at the point specified. + + :param x_or_point: x coordinate (float) or sequence of (x, y) floats. + + :param y: y coordinate (float) if x is not a sequence + + :param from_mask: Bit mask used to filter query results. This value + is bit ANDed with candidate entities' ``collision.into_mask``. + If the result is non-zero, then it is considered a hit. By + default all entities colliding with the input point are + returned. + + :return: A set of entities where the point is inside their bounding + boxes as of the last time step. + """ + if self._by_x is None: + # Axis arrays not ready + return set() + if y is None: + x, y = x_or_point + else: + x = x_or_point + LEFT = self.LEFT_ATTR + RIGHT = self.RIGHT_ATTR + TOP = self.TOP_ATTR + BOTTOM = self.BOTTOM_ATTR + x_index = bisect_right(self._by_x, [x]) + x_hits = set() + add_x_hit = x_hits.add + discard_x_hit = x_hits.discard + if x_index <= len(self._by_x) // 2: + # closer to the left, scan from left to right + while (x == self._by_x[x_index][0] + and self._by_x[x_index][1] is LEFT + and x_index < len(self._by_x)): + # Ensure we hit on exact left edge matches + x_index += 1 + for _, side, data in self._by_x[:x_index]: + if side is LEFT and from_mask & data.into_mask: + add_x_hit(data.entity) + else: + discard_x_hit(data.entity) + else: + # closer to the right + for _, side, data in reversed(self._by_x[x_index:]): + if side is RIGHT and from_mask & data.into_mask: + add_x_hit(data.entity) + else: + discard_x_hit(data.entity) + if not x_hits: + return x_hits + + y_index = bisect_right(self._by_y, [y]) + y_hits = set() + add_y_hit = y_hits.add + discard_y_hit = y_hits.discard + if y_index <= len(self._by_y) // 2: + # closer to the bottom + while (y == self._by_y[y_index][0] + and self._by_y[y_index][1] is BOTTOM + and y_index < len(self._by_y)): + # Ensure we hit on exact bottom edge matches + y_index += 1 + for _, side, data in self._by_y[:y_index]: + if side is BOTTOM: + add_y_hit(data.entity) + else: + discard_y_hit(data.entity) + else: + # closer to the top + for _, side, data in reversed(self._by_y[y_index:]): + if side is TOP: + add_y_hit(data.entity) + else: + discard_y_hit(data.entity) + if y_hits: + return x_hits & y_hits + else: + return y_hits + + +class Circular(object): + """Basic narrow-phase collision detector which treats all entities as + circles with their radius defined in the collision component. + + :param handlers: A sequence of collision handler functions that are invoked + after collision detection. + :type handlers: sequence of functions + + :param collision_component: Name of collision component for this system, + defaults to 'collision'. This supplies each entity's collision + radius and masks. + :type collision_component: str + + :param position_component: Name of position component for this system, + defaults to 'position'. This supplies each entity's position. + :type position_component: str + + :param update_aabbs: If True (the default), then the entities' + `collision.aabb` fields will be updated using their position + and collision radius before invoking the broad phase system. + Set this False if another system updates the aabbs. + :type update_aabbs: bool + + :param broad_phase: A broad-phase collision system to use as a source + for collision pairs. If not specified, a :class:`BroadSweepAndPrune` + system will be created automatically. + """ + world = None + """|World| object this system belongs to""" + + position_component = None + """Name of world's position component used by this system""" + + collision_component = None + """Name of world's collision component used by this system""" + + update_aabbs = True + """Flag to indicate whether the system updates the entities' `collision.aabb` + field before invoking the broad phase collision system + """ + + handlers = None + """A sequence of collision handler functions invoke after collision + detection + """ + + broad_phase = None + """Broad phase collision system used as a source for collision pairs""" + + def __init__(self, handlers=(), position_component='position', + collision_component='collision', update_aabbs=True, broad_phase=None): + self.handlers = tuple(handlers) + if broad_phase is None: + broad_phase = BroadSweepAndPrune(collision_component) + self.collision_component = collision_component + self.position_component = position_component + self.update_aabbs = bool(update_aabbs) + self.broad_phase = broad_phase + self._collision_pairs = None + + def set_world(self, world): + """Bind the system to a world""" + self.world = world + self.broad_phase.set_world(world) + for handler in self.handlers: + if hasattr(handler, 'set_world'): + handler.set_world(world) + + def step(self, dt): + """Update the collision system for this time step and invoke + the handlers + """ + if self.update_aabbs: + for position, collision in self.world.components.join( + self.position_component, self.collision_component): + aabb = collision.aabb + x, y = position.position + radius = collision.radius + aabb.left = x - radius + aabb.right = x + radius + aabb.bottom = y - radius + aabb.top = y + radius + self.broad_phase.step(dt) + self._collision_pairs = None + for handler in self.handlers: + handler(self) + + @property + def collision_pairs(self): + """The set of entity pairs in collision in this timestep""" + if self._collision_pairs is None: + position = getattr(self.world.components, self.position_component) + collision = getattr(self.world.components, self.collision_component) + pairs = self._collision_pairs = set() + for pair in self.broad_phase.collision_pairs: + entity1, entity2 = pair + position1 = position[entity1].position + position2 = position[entity2].position + radius1 = collision[entity1].radius + radius2 = collision[entity2].radius + separation = position2 - position1 + if separation.get_length_sqrd() <= (radius1 + radius2)**2: + normal = separation.normalized() + pair.set_point_normal( + normal * radius1 + position1, normal, + normal * -radius2 + position2, -normal) + pairs.add(pair) + return self._collision_pairs + + def query_point(self, x_or_point, y=None, from_mask=0xffffffff): + """Hit test at the point specified. + + :param x_or_point: x coordinate (float) or sequence of (x, y) floats. + + :param y: y coordinate (float) if x is not a sequence + + :param from_mask: Bit mask used to filter query results. This value + is bit ANDed with candidate entities' ``collision.into_mask``. + If the result is non-zero, then it is considered a hit. By + default all entities colliding with the input point are + returned. + + :return: A set of entities where the point is inside their collision + radii as of the last time step. + + """ + if y is None: + point = Vec2d(x_or_point) + else: + point = Vec2d(x_or_point, y) + hits = set() + position = getattr(self.world.components, self.position_component) + collision = getattr(self.world.components, self.collision_component) + for entity in self.broad_phase.query_point(x_or_point, y, from_mask): + separation = point - position[entity].position + if separation.get_length_sqrd() <= collision[entity].radius**2: + hits.add(entity) + return hits + + +def dispatch_events(collision_system): + """Collision handler that dispatches `on_collide()` events to entities + marked for collision by the specified collision system. The `on_collide()` + event handler methods are defined by the application on the desired entity + classes. These methods should have the following signature:: + + def on_collide(self, other_entity, collision_point, collision_normal): + '''Handle A collision between this entity and `other_entity` + + - other_entity (Entity): The other entity in collision with + `self` + + - collision_point (Vec2d): The point on this entity (`self`) + where the collision occurred. Note this may be `None` for + some collision systems that do not report it. + + - collision_normal (Vec2d): The normal vector at the point of + collision. As with `collision_point`, this may be None for + some collision systems. + ''' + + Note the arguments to `on_collide()` are always passed positionally, so you + can use different argument names than above if desired. + + If a pair of entities are in collision, then the event will be dispatched + to both objects in arbitrary order if all of their collision masks align. + """ + collision = getattr(collision_system.world.components, + collision_system.collision_component) + for pair in collision_system.collision_pairs: + entity1, entity2 = pair + if pair.info is not None: + args1, args2 = pair.info + else: + args1 = entity1, None, None + args2 = entity2, None, None + try: + on_collide = entity1.on_collide + masks_align = collision[entity2].from_mask & collision[entity1].into_mask + except (AttributeError, KeyError): + pass + else: + if masks_align: + on_collide(*args2) + try: + on_collide = entity2.on_collide + masks_align = collision[entity1].from_mask & collision[entity2].into_mask + except (AttributeError, KeyError): + pass + else: + if masks_align: + on_collide(*args1) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/color.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,64 @@ + +class RGBA(object): + """Four channel color representation. + + RGBA colors are floating point color representations with color channel + values between (0..1). Colors may be initialized from 3 or 4 floating + point numbers or a hex string:: + + RGBA(1.0, 1.0, 1.0) # Alpha defaults to 1.0 + RGBA(1.0, 1.0, 0, 0.5) + RGBA("#333") + RGBA("#7F7F7F") + + Individual color channels can be accessed by attribute name, or the + color object can be treated as a sequence of 4 floats. + """ + + def __init__(self, r_or_colorstr, g=None, b=None, a=None): + if isinstance(r_or_colorstr, str): + assert g is b is a is None, "Ambiguous color arguments" + self.r, self.g, self.b, self.a = self._parse_colorstr(r_or_colorstr) + elif g is b is a is None: + try: + self.r, self.g, self.b, self.a = r_or_colorstr + except ValueError: + self.r, self.g, self.b = r_or_colorstr + self.a = 1.0 + else: + self.r = r_or_colorstr + self.g = g + self.b = b + self.a = a + if self.a is None: + self.a = 1.0 + + def _parse_colorstr(self, colorstr): + length = len(colorstr) + if not colorstr.startswith("#") or length not in (4, 5, 7, 9): + raise ValueError("Invalid color string: " + colorstr) + if length <= 5: + parsed = [int(c*2, 16) / 255.0 for c in colorstr[1:]] + else: + parsed = [int(colorstr[i:i+2], 16) / 255.0 for i in range(1, length, 2)] + if len(parsed) == 3: + parsed.append(1.0) + return parsed + + def __len__(self): + return 4 + + def __getitem__(self, item): + return (self.r, self.g, self.b, self.a)[item] + + def __iter__(self): + return iter((self.r, self.g, self.b, self.a)) + + def __eq__(self, other): + return tuple(self) == tuple(other) + + def __repr__(self): + return "%s(%.2f, %.2f, %.2f, %.2f)" % (self.__class__.__name__, + self.r, self.g, self.b, self.a) + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/component/__init__.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,149 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Components store all entity data in a given |World|. You can +think of components as tables with entities as their primary keys. Like +database tables, components are defined with a "schema" that specifies +the data fields. Each field in a component has a name and a type. + +Component objects themselves have a dict-like interface with entities +as keys and data records as values. An application will typically +interact with components via entity attributes, entity extents or +by joining them. For more information see: + +- :class:`~grease.entity.Entity` class. +- :class:`~grease.world.EntityExtent` class. +- :meth:`~grease.world.ComponentParts.join` method of ComponentParts. + +See also :ref:`defining custom components in the tutorial <custom-component-example>`. +""" + +__version__ = '$Id$' + +__all__ = ('Component', 'ComponentError', 'Position', 'Transform', 'Movement', + 'Shape', 'Renderable', 'Collision') + +from grease.component.general import Component +from grease.geometry import Vec2d, Vec2dArray, Rect +from grease import color + + +class ComponentError(Exception): + """General component error""" + + +class Position(Component): + """Predefined component that stores position and orientation info for + entities. + + Fields: + + - **position** (Vec2d) -- Position vector + - **angle** (float) -- Angle, in degrees + """ + + def __init__(self): + Component.__init__(self, position=Vec2d, angle=float) + + +class Transform(Component): + """Predefined component that stores offset, shear, + rotation and scale info for entity shapes. + + Fields: + + - **offset** (Vec2d) + - **shear** (Vec2d) + - **rotation** (float) + - **scale** (float, default 1.0) + """ + + def __init__(self): + Component.__init__(self, offset=Vec2d, shear=Vec2d, rotation=float, scale=float) + self.fields['scale'].default = lambda: 1.0 + + +class Movement(Component): + """Predefined component that stores velocity, + acceleration and rotation info for entities. + + Fields: + + - **velocity** (Vec2d) -- Rate of change of entity position + - **accel** (Vec2d) -- Rate of change of entity velocity + - **rotation** (Vec2d) -- Rate of change of entity angle, in degrees/time + """ + + def __init__(self): + Component.__init__(self, velocity=Vec2d, accel=Vec2d, rotation=float) + + +class Shape(Component): + """Predefined component that stores shape vertices for entities + + - **closed** (bool) -- If the shapes is closed implying an edge between + last and first vertices. + - **verts** (Vec2dArray) -- Array of vertex points + """ + + def __init__(self): + Component.__init__(self, closed=int, verts=Vec2dArray) + self.fields['closed'].default = lambda: 1 + + +class Renderable(Component): + """Predefined component that identifies entities to be + rendered and provides their depth and color. + + - **depth** (float) -- Drawing depth, can be used to determine z-order + while rendering. + - **color** (color.RGBA) -- Color used for entity. The effect of this + field depends on the renderer. + """ + + def __init__(self): + Component.__init__(self, depth=float, color=color.RGBA) + self.fields['color'].default = lambda: color.RGBA(1,1,1,1) + + +class Collision(Component): + """Predefined component that stores collision masks to determine + which entities can collide. + + Fields: + + - **aabb** (Rect) -- The axis-aligned bounding box for the entity. + This is used for broad-phase collision detection. + + - **radius** (float) -- The collision radius of the entity, used for narrow-phase + collision detection. The exact meaning of this value depends on the collision + system in use. + + - **from_mask** (int) -- A bitmask that determines what entities this object + can collide with. + + - **into_mask** (int) -- A bitmask that determines what entities can collide + with this object. + + When considering an entity A for collision with entity B, A's ``from_mask`` is + bit ANDed with B's ``into_mask``. If the result is nonzero (meaning 1 or more + bits is set the same for each) then the collision test is made. Otherwise, + the pair cannot collide. + + The default value for both of these masks is ``0xffffffff``, which means that + all entities will collide with each other by default. + """ + def __init__(self): + Component.__init__(self, aabb=Rect, radius=float, from_mask=int, into_mask=int) + self.fields['into_mask'].default = lambda: 0xffffffff + self.fields['from_mask'].default = lambda: 0xffffffff +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/component/base.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,74 @@ +class ComponentBase(object): + """Component abstract base class + + Strictly speaking you do not need to derive from this class to create your + own components, but it does serve to document the full interface that a + component implements and it provides some basic implementations for + certain methods + """ + + ## Optional attributes and methods ## + + def set_manager(self, manager): + """Set the manager of this component. If this method exists it will be + automatically called when the component is added to a manager. + + This method stores the manager and allows the component to be added + only once to a single manager. + """ + assert getattr(self, 'manager', None) is None, 'Component cannot be added to multiple managers' + self.manager = manager + + def __del__(self): + """Break circrefs to allow faster collection""" + if hasattr(self, 'manager'): + del self.manager + + ## Mandatory methods ## + + def add(self, entity_id, data=None, **data_kw): + """Add a data entry in the component for the given entity. Additional + data (if any) for the entry can be provided in the data argument or as + keyword arguments. Additional data is optional and if omitted then + suitable defaults will be used. Return an entity data object + for the new entity entry. + + The semantics of the data arguments is up to the component. + + An entity_id is a unique key, thus multiple separate data entries for + a given entity are not allowed. Components can indivdually decide + what to do if an entity_id is added multiple times to the component. + Potential options include, raising an exception, replacing the + existing data or coalescing it somehow. + """ + + def remove(self, entity_id): + """Remove the entity data entry from the component. If the + entity is not in the component, raise KeyError + """ + + def __delitem_(self, entity_id): + """Same as remove()""" + + def __len__(self): + """Return the number of entities in the component""" + raise NotImplementedError() + + def __iter__(self): + """Return an iterator of entity data objects in this component + + No order is defined for these data objects + """ + raise NotImplementedError() + + def __contains__(self, entity_id): + """Return True if the entity is contained in the component""" + raise NotImplementedError() + + def __getitem__(self, entity_id): + """Return the entity data object for the given entity. + The entity data object returned may be mutable, immutable or a + mutable copy of the data at the discretion of the component + """ + raise NotImplementedError() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/component/field.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,301 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__version__ = '$Id$' + +import operator +from grease.geometry import Vec2d, Vec2dArray, Rect +from grease import color + +# Allowed field types -> default values +types = {int:lambda: 0, + float:lambda: 0.0, + bool:lambda: False, + str:lambda:"", + object:lambda:None, + Vec2d:lambda: Vec2d(0,0), + Vec2dArray:lambda: Vec2dArray(), + color.RGBA: lambda: color.RGBA(0.0, 0.0, 0.0, 0.0), + Rect: lambda: Rect(0.0, 0.0, 0.0, 0.0)} + +class Schema(dict): + """Field schema definition for custom components""" + + def __init__(self, **fields): + for ftype in fields.values(): + assert ftype in types, fname + " has an illegal field type" + self.update(fields) + + +class FieldAccessor(object): + """Facade for manipulating a field for a set of entities""" + + __field = None + __entities = None + __attrs = None + __getter = None + __parent_getters = () + + def __init__(self, field, entities, attrs=()): + self.__field = field + self.__entities = entities + field_getter = operator.attrgetter(field.name) + self.__attrs = attrs + if attrs: + getters = [field_getter] + [operator.attrgetter(attr) for attr in attrs] + def get(entity): + value = entity + for getter in getters: + value = getter(value) + return value + self.__getter = get + self.__parent_getters = getters[:-1] + else: + self.__getter = field_getter + + def __getattr__(self, name): + """Return a FieldAccessor for the child attribute""" + return self.__class__(self.__field, self.__entities, self.__attrs + (name,)) + + def __setattr__(self, name, value): + if value is self: + return # returned by mutators + if hasattr(self.__class__, name): + # Set local attr + self.__dict__[name] = value + elif not name.startswith('_'): + getattr(self, name).__set__(value) + else: + raise AttributeError("Cannot set field attribute: %s" % name) + + @property + def __setter(self): + """Return the proper setter function for setting the field value""" + if not self.__attrs: + return setattr + else: + parent_getters = self.__parent_getters + def setter(data, name, value): + for getter in parent_getters: + data = getter(data) + setattr(data, name, value) + self.__setter = setter + return setter + + def __set__(self, value): + """Set field values en masse""" + # Mass set field attr + setter = self.__setter + component = self.__field.component + if self.__attrs: + name = self.__attrs[-1] + else: + name = self.__field.name + if isinstance(value, FieldAccessor): + # Join set between two entity sets + if not self.__attrs: + cast = self.__field.cast + else: + cast = lambda x: x + for entity in self.__entities: + try: + setter(component[entity], name, cast(value[entity])) + except KeyError: + pass + else: + if not self.__attrs: + value = self.__field.cast(value) + for entity in self.__entities: + try: + setter(component[entity], name, value) + except KeyError: + pass + + def __getitem__(self, entity): + """Return the field value for a single entity (used for joins)""" + if entity in self.__entities: + return self.__getter(self.__field.component[entity]) + raise KeyError(entity) + + def __contains__(self, entity): + return entity in self.__entities + + def __repr__(self): + return '<%s %s @ %x>' % ( + self.__class__.__name__, + '.'.join((self.__field.name,) + self.__attrs), id(self)) + + def __nonzero__(self): + return bool(self.__entities) + + def __iter__(self): + """Return an iterator of all field values in the set""" + component = self.__field.component + getter = self.__getter + for entity in self.__entities: + try: + data = component[entity] + except KeyError: + continue + yield getter(data) + + ## batch comparison operators ## + + def __match(self, value, op): + component = self.__field.component + getter = self.__getter + matches = set() + add = matches.add + if isinstance(value, FieldAccessor): + # Join match between entity sets + for entity in self.__entities: + try: + data = component[entity] + other = value[entity] + except KeyError: + continue + if op(getter(data), other): + add(entity) + else: + for entity in self.__entities: + try: + data = component[entity] + except KeyError: + continue + if op(getter(data), value): + add(entity) + return matches + + def __eq__(self, value): + """Return an entity set of all entities with a matching field value""" + return self.__match(value, operator.eq) + + def __ne__(self, value): + """Return an entity set of all entities not matching field value""" + return self.__match(value, operator.ne) + + def __gt__(self, value): + """Return an entity set of all entities with a greater field value""" + return self.__match(value, operator.gt) + + def __ge__(self, value): + """Return an entity set of all entities with a greater or equal field value""" + return self.__match(value, operator.ge) + + def __lt__(self, value): + """Return an entity set of all entities with a lesser field value""" + return self.__match(value, operator.lt) + + def __le__(self, value): + """Return an entity set of all entities with a lesser or equal field value""" + return self.__match(value, operator.le) + + def _contains(self, values): + """Return an entity set of all entities with a field value contained in values""" + return self.__match(values, operator.contains) + + ## Batch in-place mutator methods + + def __mutate(self, value, op): + component = self.__field.component + if self.__attrs: + name = self.__attrs[-1] + else: + name = self.__field.name + getter = self.__getter + setter = self.__setter + if isinstance(value, FieldAccessor): + # Join between entity sets + for entity in self.__entities: + try: + data = component[entity] + other = value[entity] + except KeyError: + continue + setter(data, name, op(getter(data), other)) + else: + for entity in self.__entities: + try: + data = component[entity] + except KeyError: + continue + setter(data, name, op(getter(data), value)) + return self + + def __iadd__(self, value): + return self.__mutate(value, operator.iadd) + + def __isub__(self, value): + return self.__mutate(value, operator.isub) + + def __imul__(self, value): + return self.__mutate(value, operator.imul) + + def __idiv__(self, value): + return self.__mutate(value, operator.idiv) + + def __itruediv__(self, value): + return self.__mutate(value, operator.itruediv) + + def __ifloordiv__(self, value): + return self.__mutate(value, operator.ifloordiv) + + def __imod__(self, value): + return self.__mutate(value, operator.imod) + + def __ipow__(self, value): + return self.__mutate(value, operator.ipow) + + def __ilshift__(self, value): + return self.__mutate(value, operator.ilshift) + + def __irshift__(self, value): + return self.__mutate(value, operator.irshift) + + def __iand__(self, value): + return self.__mutate(value, operator.iand) + + def __ior__(self, value): + return self.__mutate(value, operator.ior) + + def __ixor__(self, value): + return self.__mutate(value, operator.ixor) + + +class Field(object): + """Component field metadata and accessor interface""" + + def __init__(self, component, name, type, accessor_factory=FieldAccessor): + self.component = component + self.name = name + self.type = type + self.default = types.get(type) + self.accessor_factory = accessor_factory + + def cast(self, value): + """Cast value to the appropriate type for thi field""" + if self.type is not object: + return self.type(value) + else: + return value + + def accessor(self, entities=None): + """Return the field accessor for the entities in the component, + or all entities in the set specified that are also in the component + """ + if entities is None or entities is self.component.entities: + entities = self.component.entities + else: + entities = entities & self.component.entities + return self.accessor_factory(self, entities) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/component/general.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,143 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__version__ = '$Id$' + +from grease.component import base +from grease.component import field +from grease.entity import ComponentEntitySet + + +class Component(dict): + """General component with a configurable schema + + The field schema is defined via keyword args where the + arg name is the field name and the value is the type object. + + The following types are supported for fields: + + - :class:`int` + - :class:`float` + - :class:`bool` + - :class:`str` + - :class:`object` + - |Vec2d| + - |Vec2dArray| + - |RGBA| + - |Rect| + """ + + deleted_entities = () + """List of entities deleted from the component since the last time step""" + + new_entities = () + """List of entities added to the component since the last time step""" + + def __init__(self, **fields): + self.fields = {} + for fname, ftype in fields.items(): + assert ftype in field.types, fname + " has an illegal field type" + self.fields[fname] = field.Field(self, fname, ftype) + self.entities = ComponentEntitySet(self) + self._added = [] + self._deleted = [] + + def set_world(self, world): + self.world = world + + def step(self, dt): + """Update the component for the next timestep""" + delitem = super(Component, self).__delitem__ + for entity in self._deleted: + delitem(entity) + self.new_entities = self._added + self.deleted_entities = self._deleted + self._added = [] + self._deleted = [] + + def set(self, entity, data=None, **data_kw): + """Set the component data for an entity, adding it to the + component if it is not already a member. + + If data is specified, its data for the new entity's fields are + copied from its attributes, making it easy to copy another + entity's data. Keyword arguments are also matched to fields. + If both a data attribute and keyword argument are supplied for + a single field, the keyword arg is used. + """ + if data is not None: + for fname, field in self.fields.items(): + if fname not in data_kw and hasattr(data, fname): + data_kw[fname] = getattr(data, fname) + data = self[entity] = Data(self.fields, entity, **data_kw) + return data + + def __setitem__(self, entity, data): + assert entity.world is self.world, "Entity not in component's world" + if entity not in self.entities: + self._added.append(entity) + self.entities.add(entity) + super(Component, self).__setitem__(entity, data) + + def remove(self, entity): + if entity in self.entities: + self._deleted.append(entity) + self.entities.remove(entity) + return True + return False + + __delitem__ = remove + + def __repr__(self): + return '<%s %x of %r>' % ( + self.__class__.__name__, id(self), getattr(self, 'world', None)) + + +class Singleton(Component): + """Component that may contain only a single entity""" + + def add(self, entity_id, data=None, **data_kw): + if entity_id not in self._data: + self.entity_id_set.clear() + self._data.clear() + Component.add(self, entity_id, data, **data_kw) + + @property + def entity(self): + """Return the entity in the component, or None if empty""" + if self._data: + return self.manager[self._data.keys()[0]] + + +class Data(object): + + def __init__(self, fields, entity, **data): + self.__dict__['_Data__fields'] = fields + self.__dict__['entity'] = entity + for field in fields.values(): + if field.name in data: + setattr(self, field.name, data[field.name]) + else: + setattr(self, field.name, field.default()) + + def __setattr__(self, name, value): + if name in self.__fields: + self.__dict__[name] = self.__fields[name].cast(value) + else: + raise AttributeError("Invalid data field: " + name) + + def __repr__(self): + return '<%s(%r)>' % (self.__class__.__name__, self.__dict__) + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/component/schema.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,1 @@ +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/controller/__init__.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,4 @@ + +__all__ = ('EulerMovement',) + +from grease.controller.integrator import EulerMovement
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/controller/integrator.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,42 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__version__ = '$Id$' + + +class EulerMovement(object): + """System that applies entity movement to position using Euler's method + + :param position_component: Name of :class:`grease.component.Position` + component to update. + :param movement_component: Name of :class:`grease.component.Movement` + component used to update position. + """ + + def __init__(self, position_component='position', movement_component='movement'): + self.position_component = position_component + self.movement_component = movement_component + + def set_world(self, world): + """Bind the system to a world""" + self.world = world + + def step(self, dt): + """Apply movement to position""" + assert self.world is not None, "Cannot run with no world set" + for position, movement in self.world.components.join( + self.position_component, self.movement_component): + movement.velocity += movement.accel * dt + position.position += movement.velocity * dt + position.angle += movement.rotation * dt +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/entity.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,212 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Grease entities are useful as actionable, interactive +game elements that are often visible to the player. + +You might use entities to represent: + +- Characters +- Bullets +- Particles +- Pick-ups +- Space Ships +- Weapons +- Trees +- Planets +- Explosions + +See :ref:`an example entity class in the tutorial <tut-entity-example>`. +""" + +__version__ = '$Id$' + +__all__ = ('Entity', 'EntityComponentAccessor', 'ComponentEntitySet') + + +class EntityMeta(type): + """The entity metaclass enforces fixed slots of `entity_id` and `world` + for all subclasses. This prevents accidental use of other entity instance + attributes, which may not be saved. + + Class attributes are not affected by this restriction, but subclasses + should be careful not to cause name collisions with world components, + which are exposed as entity attributes. Using a naming convention for + class attributes, such as UPPER_CASE_WITH_UNDERSCORES is recommended to + avoid name clashes. + + Note as a result of this, entity subclasses are not allowed to define + `__slots__`, and doing so will cause a `TypeError` to be raised. + """ + + def __new__(cls, name, bases, clsdict): + if '__slots__' in clsdict: + raise TypeError('__slots__ may not be defined in Entity subclasses') + clsdict['__slots__'] = ('world', 'entity_id') + return type.__new__(cls, name, bases, clsdict) + + +class Entity(object): + """Base class for grease entities. + + Entity objects themselves are merely identifiers within a :class:`grease.world.World`. + They also provide a facade for convenient entity-wise access of component + data. However, they do not contain any data themselves other than an + entity id. + + Entities must be instantiated in the context of a world. To instantiate an + entity, you must pass the world as the first argument to the constructor. + Subclasses that implement the :meth:`__init__()` method, must accept the world + as their first argument (after ``self``). Other constructor arguments can be + specified arbitarily by the subclass. + """ + __metaclass__ = EntityMeta + + def __new__(cls, world, *args, **kw): + """Create a new entity and add it to the world""" + entity = object.__new__(cls) + entity.world = world + entity.entity_id = world.new_entity_id() + world.entities.add(entity) + return entity + + def __getattr__(self, name): + """Return an :class:`EntityComponentAccessor` for this entity + for the component named. + + Example:: + + my_entity.movement + """ + component = getattr(self.world.components, name) + return EntityComponentAccessor(component, self) + + def __setattr__(self, name, value): + """Set the entity data in the named component for this entity. + This sets the values of the component fields to the values of + the matching attributes of the value provided. This value must + have attributes for each of the component fields. + + This allows you to easily copy component data from one entity + to another. + + Example:: + + my_entity.position = other_entity.position + """ + if name in self.__class__.__slots__: + super(Entity, self).__setattr__(name, value) + else: + component = getattr(self.world.components, name) + component.set(self, value) + + def __delattr__(self, name): + """Remove this entity and its data from the component. + + Example:: + + del my_entity.renderable + """ + component = getattr(self.world.components, name) + del component[self] + + def __hash__(self): + return self.entity_id + + def __eq__(self, other): + return self.world is other.world and self.entity_id == other.entity_id + + def __repr__(self): + return "<%s id: %s of %s %x>" % ( + self.__class__.__name__, self.entity_id, + self.world.__class__.__name__, id(self.world)) + + def delete(self): + """Delete the entity from its world. This removes all of its + component data. If then entity has already been deleted, + this call does nothing. + """ + self.world.entities.discard(self) + + @property + def exists(self): + """True if the entity still exists in the world""" + return self in self.world.entities + + +class EntityComponentAccessor(object): + """A facade for accessing specific component data for a single entity. + The implementation is lazy and does not actually access the component + data until needed. If an attribute is set for a component that the + entity is not yet a member of, it is automatically added to the + component first. + + :param component: The :class:`grease.Component` being accessed + :param entity: The :class:`Entity` being accessed + """ + + # beware, name mangling ahead. We want to avoid clashing with any + # user-configured component field names + __data = None + + def __init__(self, component, entity): + clsname = self.__class__.__name__ + self.__dict__['_%s__component' % clsname] = component + self.__dict__['_%s__entity' % clsname] = entity + + def __nonzero__(self): + """The accessor is True if the entity is in the component, + False if not, for convenient membership tests + """ + return self.__entity in self.__component + + def __getattr__(self, name): + """Return the data for the specified field of the entity's component""" + if self.__data is None: + try: + data = self.__component[self.__entity] + except KeyError: + raise AttributeError(name) + clsname = self.__class__.__name__ + self.__dict__['_%s__data' % clsname] = data + return getattr(self.__data, name) + + def __setattr__(self, name, value): + """Set the data for the specified field of the entity's component""" + if self.__data is None: + clsname = self.__class__.__name__ + if self.__entity in self.__component: + self.__dict__['_%s__data' % clsname] = self.__component[self.__entity] + else: + self.__dict__['_%s__data' % clsname] = self.__component.set(self.__entity) + setattr(self.__data, name, value) + + +class ComponentEntitySet(set): + """Set of entities in a component, can be queried by component fields""" + + _component = None + + def __init__(self, component, entities=()): + self.__dict__['_component'] = component + super(ComponentEntitySet, self).__init__(entities) + + def __getattr__(self, name): + if self._component is not None and name in self._component.fields: + return self._component.fields[name].accessor(self) + raise AttributeError(name) + + def __setattr__(self, name, value): + if self._component is not None and name in self._component.fields: + self._component.fields[name].accessor(self).__set__(value) + raise AttributeError(name) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/geometry.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,600 @@ +__version__ = "$Id$" +__docformat__ = "reStructuredText" + +import operator +import math +import ctypes + +class Vec2d(ctypes.Structure): + """2d vector class, supports vector and scalar operators, + and also provides a bunch of high level functions + """ + __slots__ = ['x', 'y'] + + @classmethod + def from_param(cls, arg): + return cls(arg) + + def __init__(self, x_or_pair, y = None): + + if y == None: + self.x = x_or_pair[0] + self.y = x_or_pair[1] + else: + self.x = x_or_pair + self.y = y + + def __len__(self): + return 2 + + def __getitem__(self, key): + if key == 0: + return self.x + elif key == 1: + return self.y + else: + raise IndexError("Invalid subscript "+str(key)+" to Vec2d") + + def __setitem__(self, key, value): + if key == 0: + self.x = value + elif key == 1: + self.y = value + else: + raise IndexError("Invalid subscript "+str(key)+" to Vec2d") + + # String representaion (for debugging) + def __repr__(self): + return 'Vec2d(%s, %s)' % (self.x, self.y) + + # Comparison + def __eq__(self, other): + if hasattr(other, "__getitem__") and len(other) == 2: + return self.x == other[0] and self.y == other[1] + else: + return False + + def __ne__(self, other): + if hasattr(other, "__getitem__") and len(other) == 2: + return self.x != other[0] or self.y != other[1] + else: + return True + + def __nonzero__(self): + return self.x or self.y + + # Generic operator handlers + def _o2(self, other, f): + "Any two-operator operation where the left operand is a Vec2d" + if isinstance(other, Vec2d): + return Vec2d(f(self.x, other.x), + f(self.y, other.y)) + elif (hasattr(other, "__getitem__")): + return Vec2d(f(self.x, other[0]), + f(self.y, other[1])) + else: + return Vec2d(f(self.x, other), + f(self.y, other)) + + def _r_o2(self, other, f): + "Any two-operator operation where the right operand is a Vec2d" + if (hasattr(other, "__getitem__")): + return Vec2d(f(other[0], self.x), + f(other[1], self.y)) + else: + return Vec2d(f(other, self.x), + f(other, self.y)) + + def _io(self, other, f): + "inplace operator" + if (hasattr(other, "__getitem__")): + self.x = f(self.x, other[0]) + self.y = f(self.y, other[1]) + else: + self.x = f(self.x, other) + self.y = f(self.y, other) + return self + + # Addition + def __add__(self, other): + if isinstance(other, Vec2d): + return Vec2d(self.x + other.x, self.y + other.y) + elif hasattr(other, "__getitem__"): + return Vec2d(self.x + other[0], self.y + other[1]) + else: + return Vec2d(self.x + other, self.y + other) + __radd__ = __add__ + + def __iadd__(self, other): + if isinstance(other, Vec2d): + self.x += other.x + self.y += other.y + elif hasattr(other, "__getitem__"): + self.x += other[0] + self.y += other[1] + else: + self.x += other + self.y += other + return self + + # Subtraction + def __sub__(self, other): + if isinstance(other, Vec2d): + return Vec2d(self.x - other.x, self.y - other.y) + elif (hasattr(other, "__getitem__")): + return Vec2d(self.x - other[0], self.y - other[1]) + else: + return Vec2d(self.x - other, self.y - other) + def __rsub__(self, other): + if isinstance(other, Vec2d): + return Vec2d(other.x - self.x, other.y - self.y) + if (hasattr(other, "__getitem__")): + return Vec2d(other[0] - self.x, other[1] - self.y) + else: + return Vec2d(other - self.x, other - self.y) + def __isub__(self, other): + if isinstance(other, Vec2d): + self.x -= other.x + self.y -= other.y + elif (hasattr(other, "__getitem__")): + self.x -= other[0] + self.y -= other[1] + else: + self.x -= other + self.y -= other + return self + + # Multiplication + def __mul__(self, other): + if isinstance(other, Vec2d): + return Vec2d(self.x*other.y, self.y*other.y) + if (hasattr(other, "__getitem__")): + return Vec2d(self.x*other[0], self.y*other[1]) + else: + return Vec2d(self.x*other, self.y*other) + __rmul__ = __mul__ + + def __imul__(self, other): + if isinstance(other, Vec2d): + self.x *= other.x + self.y *= other.y + elif (hasattr(other, "__getitem__")): + self.x *= other[0] + self.y *= other[1] + else: + self.x *= other + self.y *= other + return self + + # Division + def __div__(self, other): + return self._o2(other, operator.div) + def __rdiv__(self, other): + return self._r_o2(other, operator.div) + def __idiv__(self, other): + return self._io(other, operator.div) + + def __floordiv__(self, other): + return self._o2(other, operator.floordiv) + def __rfloordiv__(self, other): + return self._r_o2(other, operator.floordiv) + def __ifloordiv__(self, other): + return self._io(other, operator.floordiv) + + def __truediv__(self, other): + return self._o2(other, operator.truediv) + def __rtruediv__(self, other): + return self._r_o2(other, operator.truediv) + def __itruediv__(self, other): + return self._io(other, operator.floordiv) + + # Modulo + def __mod__(self, other): + return self._o2(other, operator.mod) + def __rmod__(self, other): + return self._r_o2(other, operator.mod) + + def __divmod__(self, other): + return self._o2(other, divmod) + def __rdivmod__(self, other): + return self._r_o2(other, divmod) + + # Exponentation + def __pow__(self, other): + return self._o2(other, operator.pow) + def __rpow__(self, other): + return self._r_o2(other, operator.pow) + + # Bitwise operators + def __lshift__(self, other): + return self._o2(other, operator.lshift) + def __rlshift__(self, other): + return self._r_o2(other, operator.lshift) + + def __rshift__(self, other): + return self._o2(other, operator.rshift) + def __rrshift__(self, other): + return self._r_o2(other, operator.rshift) + + def __and__(self, other): + return self._o2(other, operator.and_) + __rand__ = __and__ + + def __or__(self, other): + return self._o2(other, operator.or_) + __ror__ = __or__ + + def __xor__(self, other): + return self._o2(other, operator.xor) + __rxor__ = __xor__ + + # Unary operations + def __neg__(self): + return Vec2d(operator.neg(self.x), operator.neg(self.y)) + + def __pos__(self): + return Vec2d(operator.pos(self.x), operator.pos(self.y)) + + def __abs__(self): + return Vec2d(abs(self.x), abs(self.y)) + + def __invert__(self): + return Vec2d(-self.x, -self.y) + + # vectory functions + def get_length_sqrd(self): + """Get the squared length of the vector. + It is more efficent to use this method instead of first call + get_length() or access .length and then do a sqrt(). + + :return: The squared length + """ + return self.x**2 + self.y**2 + + def get_length(self): + """Get the length of the vector. + + :return: The length + """ + return math.sqrt(self.x**2 + self.y**2) + def __setlength(self, value): + length = self.get_length() + self.x *= value/length + self.y *= value/length + length = property(get_length, __setlength, doc = """Gets or sets the magnitude of the vector""") + + def rotate(self, angle_degrees): + """Rotate the vector by angle_degrees degrees clockwise.""" + radians = -math.radians(angle_degrees) + cos = math.cos(radians) + sin = math.sin(radians) + x = self.x*cos - self.y*sin + y = self.x*sin + self.y*cos + self.x = x + self.y = y + + def rotated(self, angle_degrees): + """Create and return a new vector by rotating this vector by + angle_degrees degrees clockwise. + + :return: Rotated vector + """ + radians = -math.radians(angle_degrees) + cos = math.cos(radians) + sin = math.sin(radians) + x = self.x*cos - self.y*sin + y = self.x*sin + self.y*cos + return Vec2d(x, y) + + def get_angle(self): + if (self.get_length_sqrd() == 0): + return 0 + return math.degrees(math.atan2(self.y, self.x)) + def __setangle(self, angle_degrees): + self.x = self.length + self.y = 0 + self.rotate(angle_degrees) + angle = property(get_angle, __setangle, doc="""Gets or sets the angle of a vector""") + + def get_angle_between(self, other): + """Get the angle between the vector and the other in degrees + + :return: The angle + """ + cross = self.x*other[1] - self.y*other[0] + dot = self.x*other[0] + self.y*other[1] + return math.degrees(math.atan2(cross, dot)) + + def normalized(self): + """Get a normalized copy of the vector + + :return: A normalized vector + """ + length = self.length + if length != 0: + return self/length + return Vec2d(self) + + def normalize_return_length(self): + """Normalize the vector and return its length before the normalization + + :return: The length before the normalization + """ + length = self.length + if length != 0: + self.x /= length + self.y /= length + return length + + def perpendicular(self): + return Vec2d(-self.y, self.x) + + def perpendicular_normal(self): + length = self.length + if length != 0: + return Vec2d(-self.y/length, self.x/length) + return Vec2d(self) + + def dot(self, other): + """The dot product between the vector and other vector + v1.dot(v2) -> v1.x*v2.x + v1.y*v2.y + + :return: The dot product + """ + return float(self.x*other[0] + self.y*other[1]) + + def get_distance(self, other): + """The distance between the vector and other vector + + :return: The distance + """ + return math.sqrt((self.x - other[0])**2 + (self.y - other[1])**2) + + def get_dist_sqrd(self, other): + """The squared distance between the vector and other vector + It is more efficent to use this method than to call get_distance() + first and then do a sqrt() on the result. + + :return: The squared distance + """ + return (self.x - other[0])**2 + (self.y - other[1])**2 + + def projection(self, other): + other_length_sqrd = other[0]*other[0] + other[1]*other[1] + projected_length_times_other_length = self.dot(other) + return other*(projected_length_times_other_length/other_length_sqrd) + + def cross(self, other): + """The cross product between the vector and other vector + v1.cross(v2) -> v1.x*v2.y - v2.y-v1.x + + :return: The cross product + """ + return self.x*other[1] - self.y*other[0] + + def interpolate_to(self, other, range): + return Vec2d(self.x + (other[0] - self.x)*range, self.y + (other[1] - self.y)*range) + + def convert_to_basis(self, x_vector, y_vector): + return Vec2d(self.dot(x_vector)/x_vector.get_length_sqrd(), self.dot(y_vector)/y_vector.get_length_sqrd()) + + # Extra functions, mainly for chipmunk + def cpvrotate(self, other): + return Vec2d(self.x*other.x - self.y*other.y, self.x*other.y + self.y*other.x) + def cpvunrotate(self, other): + return Vec2d(self.x*other.x + self.y*other.y, self.y*other.x - self.x*other.y) + + # Pickle, does not work atm. + def __getstate__(self): + return [self.x, self.y] + + def __setstate__(self, dict): + self.x, self.y = dict + def __newobj__(cls, *args): + return cls.__new__(cls, *args) +Vec2d._fields_ = [ + ('x', ctypes.c_double), + ('y', ctypes.c_double), + ] + + +class Vec2dArray(list): + + def __init__(self, iterable=()): + list.__init__(self, (Vec2d(i) for i in iterable)) + + def __setitem__(self, index, value): + list.__setitem__(self, index, Vec2d(value)) + + def append(self, value): + """Append a vector to the array""" + list.append(self, Vec2d(value)) + + def insert(self, index, value): + """Insert a vector into the array""" + list.insert(self, index, Vec2d(value)) + + def transform(self, offset=Vec2d(0,0), angle=0, scale=1.0): + """Return a new transformed Vec2dArray""" + offset = Vec2d(offset) + angle = math.radians(-angle) + rot_vec = Vec2d(math.cos(angle), math.sin(angle)) + xformed = Vec2dArray() + for vec in self: + xformed.append(vec.cpvrotate(rot_vec) * scale + offset) + return xformed + + def segments(self, closed=True): + """Generate arrays of line segments connecting adjacent vetices + in this array, exploding the shape into it's constituent segments + """ + if len(self) >= 2: + last = self[0] + for vert in self[1:]: + yield Vec2dArray((last, vert)) + last = vert + if closed: + yield Vec2dArray((last, self[0])) + elif self and closed: + yield Vec2dArray((self[0], self[0])) + + + +class Rect(ctypes.Structure): + """Simple rectangle. Will gain more functionality as needed""" + _fields_ = [ + ('left', ctypes.c_double), + ('top', ctypes.c_double), + ('right', ctypes.c_double), + ('bottom', ctypes.c_double), + ] + + def __init__(self, rect_or_left, bottom=None, right=None, top=None): + if bottom is not None: + assert right is not None and top is not None, "No enough arguments to Rect" + self.left = rect_or_left + self.bottom = bottom + self.right = right + self.top = top + else: + self.left = rect_or_left.left + self.bottom = rect_or_left.bottom + self.right = rect_or_left.right + self.top = rect_or_left.top + + @property + def width(self): + """Rectangle width""" + return self.right - self.left + + @property + def height(self): + """Rectangle height""" + return self.top - self.bottom + + +######################################################################## +## Unit Testing ## +######################################################################## +if __name__ == "__main__": + + import unittest + import pickle + + #################################################################### + class UnitTestVec2d(unittest.TestCase): + + def setUp(self): + pass + + def testCreationAndAccess(self): + v = Vec2d(111, 222) + self.assert_(v.x == 111 and v.y == 222) + v.x = 333 + v[1] = 444 + self.assert_(v[0] == 333 and v[1] == 444) + + def testMath(self): + v = Vec2d(111,222) + self.assertEqual(v + 1, Vec2d(112, 223)) + self.assert_(v - 2 == [109, 220]) + self.assert_(v * 3 == (333, 666)) + self.assert_(v / 2.0 == Vec2d(55.5, 111)) + #self.assert_(v / 2 == (55, 111)) # Not supported since this is a c_float structure in the bottom + self.assert_(v ** Vec2d(2, 3) == [12321, 10941048]) + self.assert_(v + [-11, 78] == Vec2d(100, 300)) + #self.assert_(v / [11,2] == [10,111]) # Not supported since this is a c_float structure in the bottom + + def testReverseMath(self): + v = Vec2d(111, 222) + self.assert_(1 + v == Vec2d(112, 223)) + self.assert_(2 - v == [-109, -220]) + self.assert_(3 * v == (333, 666)) + #self.assert_([222,999] / v == [2,4]) # Not supported since this is a c_float structure in the bottom + self.assert_([111, 222] ** Vec2d(2, 3) == [12321, 10941048]) + self.assert_([-11, 78] + v == Vec2d(100, 300)) + + def testUnary(self): + v = Vec2d(111, 222) + v = -v + self.assert_(v == [-111, -222]) + v = abs(v) + self.assert_(v == [111, 222]) + + def testLength(self): + v = Vec2d(3,4) + self.assert_(v.length == 5) + self.assert_(v.get_length_sqrd() == 25) + self.assert_(v.normalize_return_length() == 5) + self.assertAlmostEquals(v.length, 1) + v.length = 5 + self.assert_(v == Vec2d(3, 4)) + v2 = Vec2d(10, -2) + self.assert_(v.get_distance(v2) == (v - v2).get_length()) + + def testAngles(self): + v = Vec2d(0, 3) + self.assertEquals(v.angle, 90) + v2 = Vec2d(v) + v.rotate(-90) + self.assertEqual(v.get_angle_between(v2), 90) + v2.angle -= 90 + self.assertEqual(v.length, v2.length) + self.assertEquals(v2.angle, 0) + self.assertEqual(v2, [3, 0]) + self.assert_((v - v2).length < .00001) + self.assertEqual(v.length, v2.length) + v2.rotate(300) + self.assertAlmostEquals(v.get_angle_between(v2), -60, 5) # Allow a little more error than usual (floats..) + v2.rotate(v2.get_angle_between(v)) + angle = v.get_angle_between(v2) + self.assertAlmostEquals(v.get_angle_between(v2), 0) + + def testHighLevel(self): + basis0 = Vec2d(5.0, 0) + basis1 = Vec2d(0, .5) + v = Vec2d(10, 1) + self.assert_(v.convert_to_basis(basis0, basis1) == [2, 2]) + self.assert_(v.projection(basis0) == (10, 0)) + self.assert_(basis0.dot(basis1) == 0) + + def testCross(self): + lhs = Vec2d(1, .5) + rhs = Vec2d(4, 6) + self.assert_(lhs.cross(rhs) == 4) + + def testComparison(self): + int_vec = Vec2d(3, -2) + flt_vec = Vec2d(3.0, -2.0) + zero_vec = Vec2d(0, 0) + self.assert_(int_vec == flt_vec) + self.assert_(int_vec != zero_vec) + self.assert_((flt_vec == zero_vec) == False) + self.assert_((flt_vec != int_vec) == False) + self.assert_(int_vec == (3, -2)) + self.assert_(int_vec != [0, 0]) + self.assert_(int_vec != 5) + self.assert_(int_vec != [3, -2, -5]) + + def testInplace(self): + inplace_vec = Vec2d(5, 13) + inplace_ref = inplace_vec + inplace_src = Vec2d(inplace_vec) + inplace_vec *= .5 + inplace_vec += .5 + inplace_vec /= (3, 6) + inplace_vec += Vec2d(-1, -1) + alternate = (inplace_src*.5 + .5)/Vec2d(3, 6) + [-1, -1] + self.assertEquals(inplace_vec, inplace_ref) + self.assertEquals(inplace_vec, alternate) + + def testPickle(self): + return # pickling does not work atm + testvec = Vec2d(5, .3) + testvec_str = pickle.dumps(testvec) + loaded_vec = pickle.loads(testvec_str) + self.assertEquals(testvec, loaded_vec) + + #################################################################### + unittest.main() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/impl/__init__.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,22 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__versioninfo__ = (0, 3, 0) +__version__ = '.'.join(str(n) for n in __versioninfo__) + +__all__ = ('Mode', 'World') + +from mode import Mode +from world import World + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/impl/controls.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,216 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Control systems for binding controls to game logic""" + +import grease +from pyglet.window import key + +class KeyControls(grease.System): + """System that maps subclass-defined action methods to keys. + + Keys may be mapped in the subclass definition using decorators + defined here as class methods or at runtime using the ``bind_key_*`` + instance methods. + + See :ref:`an example implementation in the tutorial <tut-controls-example>`. + """ + MODIFIER_MASK = ~(key.MOD_NUMLOCK | key.MOD_SCROLLLOCK | key.MOD_CAPSLOCK) + """The MODIFIER_MASK allows you to filter out modifier keys that should be + ignored by the application. By default, capslock, numlock, and scrolllock + are ignored. + """ + + world = None + """:class:`grease.World` object this system is bound to""" + + def __init__(self): + self._key_press_map = {} + self._key_release_map = {} + self._key_hold_map = {} + for name in self.__class__.__dict__: + member = getattr(self, name) + if hasattr(member, '_grease_hold_key_binding'): + for binding in member._grease_hold_key_binding: + self.bind_key_hold(member, *binding) + if hasattr(member, '_grease_press_key_binding'): + for binding in member._grease_press_key_binding: + self.bind_key_press(member, *binding) + if hasattr(member, '_grease_release_key_binding'): + for binding in member._grease_release_key_binding: + self.bind_key_release(member, *binding) + self.held_keys = set() + + ## decorator methods for binding methods to key input events ## + + @classmethod + def key_hold(cls, symbol, modifiers=0): + """Decorator to bind a method to be executed where a key is held down""" + def bind(f): + if not hasattr(f, '_grease_hold_key_binding'): + f._grease_hold_key_binding = [] + f._grease_hold_key_binding.append((symbol, modifiers & cls.MODIFIER_MASK)) + return f + return bind + + @classmethod + def key_press(cls, symbol, modifiers=0): + """Decorator to bind a method to be executed where a key is initially depressed""" + def bind(f): + if not hasattr(f, '_grease_press_key_binding'): + f._grease_press_key_binding = [] + f._grease_press_key_binding.append((symbol, modifiers & cls.MODIFIER_MASK)) + return f + return bind + + @classmethod + def key_release(cls, symbol, modifiers=0): + """Decorator to bind a method to be executed where a key is released""" + def bind(f): + if not hasattr(f, '_grease_release_key_binding'): + f._grease_release_key_binding = [] + f._grease_release_key_binding.append((symbol, modifiers & cls.MODIFIER_MASK)) + return f + return bind + + ## runtime binding methods ## + + def bind_key_hold(self, method, key, modifiers=0): + """Bind a method to a key at runtime to be invoked when the key is + held down, this replaces any existing key hold binding for this key. + To unbind the key entirely, pass ``None`` for method. + """ + if method is not None: + self._key_hold_map[key, modifiers & self.MODIFIER_MASK] = method + else: + try: + del self._key_hold_map[key, modifiers & self.MODIFIER_MASK] + except KeyError: + pass + + def bind_key_press(self, method, key, modifiers=0): + """Bind a method to a key at runtime to be invoked when the key is initially + pressed, this replaces any existing key hold binding for this key. To unbind + the key entirely, pass ``None`` for method. + """ + if method is not None: + self._key_press_map[key, modifiers & self.MODIFIER_MASK] = method + else: + try: + del self._key_press_map[key, modifiers & self.MODIFIER_MASK] + except KeyError: + pass + + def bind_key_release(self, method, key, modifiers=0): + """Bind a method to a key at runtime to be invoked when the key is releaseed, + this replaces any existing key hold binding for this key. To unbind + the key entirely, pass ``None`` for method. + """ + if method is not None: + self._key_release_map[key, modifiers & self.MODIFIER_MASK] = method + else: + try: + del self._key_release_map[key, modifiers & self.MODIFIER_MASK] + except KeyError: + pass + + def step(self, dt): + """invoke held key functions""" + already_run = set() + for key in self.held_keys: + func = self._key_hold_map.get(key) + if func is not None and func not in already_run: + already_run.add(func) + func(dt) + + def on_key_press(self, key, modifiers): + """Handle pyglet key press. Invoke key press methods and + activate key hold functions + """ + key_mod = (key, modifiers & self.MODIFIER_MASK) + if key_mod in self._key_press_map: + self._key_press_map[key_mod]() + self.held_keys.add(key_mod) + + def on_key_release(self, key, modifiers): + """Handle pyglet key release. Invoke key release methods and + deactivate key hold functions + """ + key_mod = (key, modifiers & self.MODIFIER_MASK) + if key_mod in self._key_release_map: + self._key_release_map[key_mod]() + self.held_keys.discard(key_mod) + + +if __name__ == '__main__': + import pyglet + + class TestKeyControls(KeyControls): + + MODIFIER_MASK = ~(key.MOD_NUMLOCK | key.MOD_SCROLLLOCK | key.MOD_CTRL) + + remapped = False + + @KeyControls.key_hold(key.UP) + @KeyControls.key_hold(key.W) + def up(self, dt): + print 'UP!' + + @KeyControls.key_hold(key.LEFT) + @KeyControls.key_hold(key.A) + def left(self, dt): + print 'LEFT!' + + @KeyControls.key_hold(key.RIGHT) + @KeyControls.key_hold(key.D) + def right(self, dt): + print 'RIGHT!' + + @KeyControls.key_hold(key.DOWN) + @KeyControls.key_hold(key.S) + def down(self, dt): + print 'DOWN!' + + @KeyControls.key_press(key.SPACE) + def fire(self): + print 'FIRE!' + + @KeyControls.key_press(key.R) + def remap_keys(self): + if not self.remapped: + self.bind_key_hold(None, key.W) + self.bind_key_hold(None, key.A) + self.bind_key_hold(None, key.S) + self.bind_key_hold(None, key.D) + self.bind_key_hold(self.up, key.I) + self.bind_key_hold(self.left, key.J) + self.bind_key_hold(self.right, key.L) + self.bind_key_hold(self.down, key.K) + else: + self.bind_key_hold(None, key.I) + self.bind_key_hold(None, key.J) + self.bind_key_hold(None, key.K) + self.bind_key_hold(None, key.L) + self.bind_key_hold(self.up, key.W) + self.bind_key_hold(self.left, key.A) + self.bind_key_hold(self.right, key.D) + self.bind_key_hold(self.down, key.S) + self.remapped = not self.remapped + + + window = pyglet.window.Window() + window.clear() + controls = TestKeyControls() + window.push_handlers(controls) + pyglet.clock.schedule_interval(controls.step, 0.5) + pyglet.app.run() +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/impl/mode.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,203 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +""" +Modes manage the state and transition between different application modes. +Typically such modes are presented as different screens that the user can +navigate between, similar to the way a browser navigates web pages. Individual +modes may be things like: + +- Title screen +- Options dialog +- About screen +- In-progress game +- Inventory interface + +The modal framework provides a simple mechanism to ensure that modes are +activated and deactivated properly. An activated mode is running and receives +events. A deactivated mode is paused and does not receive events. + +Modes may be managed as a *last-in-first-out* stack, or as a list, or ring +of modes in sequence, or some combination of all. + +For example usage see: :ref:`the mode section of the tutorial <tut-mode-section>`. +""" + +__version__ = '$Id$' + +import abc +import pyglet +from grease.mode import * + +class PygletManager(BaseManager): + """Mode manager abstract base class using pyglet. + + The mode manager keeps a stack of modes where a single mode + is active at one time. As modes are pushed on and popped from + the stack, the mode at the top is always active. The current + active mode receives events from the manager's event dispatcher. + """ + + event_dispatcher = None + """:class:`pyglet.event.EventDispatcher` object that the + active mode receive events from. + """ + + def activate_mode(self, mode): + """Perform actions to activate a node + + :param mode: The :class: 'Mode' object to activate + """ + BaseManager.activate_mode(self, mode) + self.event_dispatcher.push_handlers(mode) + + def deactivate_mode(self, mode): + """Perform actions to deactivate a node + + :param mode: The :class: 'Mode' object to deactivate + """ + BaseManager.deactivate_mode(self, mode) + self.event_dispatcher.remove_handlers(mode) + +class Manager(PygletManager): + """A basic mode manager that wraps a single + :class:`pyglet.event.EventDispatcher` object for use by its modes. + """ + + def __init__(self, event_dispatcher): + self.modes = [] + self.event_dispatcher = event_dispatcher + + +class ManagerWindow(PygletManager, pyglet.window.Window): + """An integrated mode manager and pyglet window for convenience. + The window is the event dispatcher used by modes pushed to + this manager. + + Constructor arguments are identical to :class:`pyglet.window.Window` + """ + + def __init__(self, *args, **kw): + super(ManagerWindow, self).__init__(*args, **kw) + self.modes = [] + self.event_dispatcher = self + + def on_key_press(self, symbol, modifiers): + """Default :meth:`on_key_press handler`, pops the current mode on ``ESC``""" + if symbol == pyglet.window.key.ESCAPE: + self.pop_mode() + + def on_last_mode_pop(self, mode): + """Hook executed when the last mode is popped from the manager. + When the last mode is popped from a window, an :meth:`on_close` event + is dispatched. + + :param mode: The :class:`Mode` object just popped from the manager + """ + self.dispatch_event('on_close') + + +class Mode(BaseMode): + """Application mode abstract base class using pyglet + + Subclasses must implement the :meth:`step` method + + :param step_rate: The rate of :meth:`step()` calls per second. + + :param master_clock: The :class:`pyglet.clock.Clock` interface used + as the master clock that ticks the world's clock. This + defaults to the main pyglet clock. + """ + clock = None + """The :class:`pyglet.clock.Clock` instance used as this mode's clock. + You should use this clock to schedule tasks for this mode, so they + properly respect when the mode is active or inactive + + Example:: + + my_mode.clock.schedule_once(my_cool_function, 4) + """ + + def __init__(self, step_rate=60, master_clock=pyglet.clock, + clock_factory=pyglet.clock.Clock): + BaseMode.__init__(self) + self.step_rate = step_rate + self.time = 0.0 + self.master_clock = master_clock + self.clock = clock_factory(time_function=lambda: self.time) + self.clock.schedule_interval(self.step, 1.0 / step_rate) + + def on_activate(self): + """Being called when the Mode is activated""" + self.master_clock.schedule(self.tick) + + def on_deactivate(self): + """Being called when the Mode is deactivated""" + self.master_clock.unschedule(self.tick) + + def tick(self, dt): + """Tick the mode's clock. + + :param dt: The time delta since the last tick + :type dt: float + """ + self.time += dt + self.clock.tick(poll=False) + + @abc.abstractmethod + def step(self, dt): + """Execute a timestep for this mode. Must be defined by subclasses. + + :param dt: The time delta since the last time step + :type dt: float + """ + +class Multi(BaseMulti, Mode): + """A mode with multiple submodes. One submode is active at one time. + Submodes can be switched to directly or switched in sequence. If + the Multi is active, then one submode is always active. + + Multis are useful when modes can switch in an order other than + a LIFO stack, such as in "hotseat" multiplayer games, a + "wizard" style ui, or a sequence of slides. + + Note unlike a normal :class:`Mode`, a :class:`Multi` doesn't have it's own + :attr:`clock` and :attr:`step_rate`. The active submode's are used + instead. + """ + + def __init__(self, submodes): + BaseMulti.__init__(self, submodes) + self.time = 0.0 + + + def _set_active_submode(self, submode): + BaseMulti._set_active_submode(self, submode) + self.master_clock = submode.master_clock + self.clock = submode.clock + + def clear_subnode(self): + """Clear any subnmode data""" + BaseMulti.clear_subnode(self) + self.master_clock = None + self.clock = None + + def tick(self, dt): + """Tick the active submode's clock. + + :param dt: The time delta since the last tick + :type dt: float + """ + self.time += dt + if self.active_submode is not None: + self.active_submode.clock.tick(poll=False) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/impl/world.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,129 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Worlds are environments described by a configuration of components, systems and +renderers. These parts describe the data, behavioral and presentation aspects +of the world respectively. + +The world environment is the context within which entities exist. A typical +application consists of one or more worlds containing entities that evolve +over time and react to internal and external interaction. + +See :ref:`an example of world configuration in the tutorial <tut-world-example>`. +""" + +__version__ = '$Id$' + +import itertools +import pyglet +from pyglet import gl +from grease.world import * +from grease.impl import Mode + +class World(Mode, BaseWorld): + """A coordinated collection of components, systems and entities + + A world is also a mode that may be pushed onto a + :class:`grease.mode.Manager` + + :param step_rate: The rate of :meth:`step()` calls per second. + + :param master_clock: The :class:`pyglet.clock.Clock` interface used + as the master clock that ticks the world's clock. This + defaults to the main pyglet clock. + """ + + clock = None + """:class:`pyglet.clock` interface for use by constituents + of the world for scheduling + """ + + time = None + """Current clock time of the world, starts at 0 when the world + is instantiated + """ + + running = True + """Flag to indicate that the world clock is running, advancing time + and stepping the world. Set running to False to pause the world. + """ + + def __init__(self, step_rate=60, master_clock=pyglet.clock, + clock_factory=pyglet.clock.Clock): + Mode.__init__(self, step_rate, master_clock, clock_factory) + BaseWorld.__init__(self) + + def activate(self, manager): + """Activate the world/mode for the given manager, if the world is already active, + do nothing. This method is typically not used directly, it is called + automatically by the mode manager when the world becomes active. + + The systems of the world are pushed onto `manager.event_dispatcher` + so they can receive system events. + + :param manager: :class:`mode.BaseManager` instance + """ + if not self.active: + for system in self.systems: + manager.event_dispatcher.push_handlers(system) + super(World, self).activate(manager) + + def deactivate(self, manager): + """Deactivate the world/mode, if the world is not active, do nothing. + This method is typically not used directly, it is called + automatically by the mode manager when the world becomes active. + + Removes the system handlers from the `manager.event_dispatcher` + + :param manager: :class:`mode.BaseManager` instance + """ + for system in self.systems: + manager.event_dispatcher.remove_handlers(system) + super(World, self).deactivate(manager) + + def tick(self, dt): + """Tick the mode's clock, but only if the world is currently running + + :param dt: The time delta since the last tick + :type dt: float + """ + if self.running: + super(World, self).tick(dt) + + def step(self, dt): + """Execute a time step for the world. Updates the world `time` + and invokes the world's systems. + + Note that the specified time delta will be pinned to 10x the + configured step rate. For example if the step rate is 60, + then dt will be pinned at a maximum of 0.1666. This avoids + pathological behavior when the time between steps goes + much longer than expected. + + :param dt: The time delta since the last time step + :type dt: float + """ + dt = min(dt, 10.0 / self.step_rate) + for component in self.components: + if hasattr(component, "step"): + component.step(dt) + for system in self.systems: + if hasattr(system, "step"): + system.step(dt) + + def on_draw(self, gl=pyglet.gl): + """Clear the current OpenGL context, reset the model/view matrix and + invoke the `draw()` methods of the renderers in order + """ + gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) + gl.glLoadIdentity() + BaseWorld.draw_renderers(self) \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/mode.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,392 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +""" +Modes manage the state and transition between different application modes. +Typically such modes are presented as different screens that the user can +navigate between, similar to the way a browser navigates web pages. Individual +modes may be things like: + +- Title screen +- Options dialog +- About screen +- In-progress game +- Inventory interface + +The modal framework provides a simple mechanism to ensure that modes are +activated and deactivated properly. An activated mode is running and receives +events. A deactivated mode is paused and does not receive events. + +Modes may be managed as a *last-in-first-out* stack, or as a list, or ring +of modes in sequence, or some combination of all. + +For example usage see: :ref:`the mode section of the tutorial <tut-mode-section>`. +""" + +__version__ = '$Id$' + +import abc + + +class BaseManager(object): + """Mode manager abstract base class. + + The mode manager keeps a stack of modes where a single mode + is active at one time. As modes are pushed on and popped from + the stack, the mode at the top is always active. The current + active mode receives events from the manager's event dispatcher. + """ + + modes = () + """The mode stack sequence. The last mode in the stack is + the current active mode. Read-only. + """ + + @property + def current_mode(self): + """The current active mode or ``None``. Read-only""" + try: + return self.modes[-1] + except IndexError: + return None + + def on_last_mode_pop(self, mode): + """Hook executed when the last mode is popped from the manager. + Implementing this method is optional for subclasses. + + :param mode: The :class:`Mode` object just popped from the manager + """ + + def activate_mode(self, mode): + """Perform actions to activate a node + + :param mode: The :class: 'Mode' object to activate + """ + mode.activate(self) + + def deactivate_mode(self, mode): + """Perform actions to deactivate a node + + :param mode: The :class: 'Mode' object to deactivate + """ + mode.deactivate(self) + + + def push_mode(self, mode): + """Push a mode to the top of the mode stack and make it active + + :param mode: The :class:`Mode` object to make active + """ + current = self.current_mode + if current is not None: + self.deactivate_mode(current) + self.modes.append(mode) + self.activate_mode(mode) + + def pop_mode(self): + """Pop the current mode off the top of the stack and deactivate it. + The mode now at the top of the stack, if any is then activated. + + :param mode: The :class:`Mode` object popped from the stack + """ + mode = self.modes.pop() + mode.deactivate(self) + self.event_dispatcher.remove_handlers(mode) + current = self.current_mode + if current is not None: + self.activate_mode(current) + else: + self.on_last_mode_pop(mode) + return mode + + def swap_modes(self, mode): + """Exchange the specified mode with the mode at the top of the stack. + This is similar to popping the current mode and pushing the specified + one, but without activating the previous mode on the stack or + executing :meth:`on_last_mode_pop()` if there is no previous mode. + + :param mode: The :class:`Mode` object that was deactivated and replaced. + """ + old_mode = self.modes.pop() + self.deactivate_mode(old_mode) + self.modes.append(mode) + self.activate_mode(mode) + return old_mode + + def remove_mode(self, mode): + """Remove the specified mode. If the mode is at the top of the stack, + this is equivilent to :meth:`pop_mode()`. If not, no other modes + are affected. If the mode is not in the manager, do nothing. + + :param mode: The :class:`Mode` object to remove from the manager. + """ + if self.current_mode is mode: + self.pop_mode() + else: + try: + self.modes.remove(mode) + except ValueError: + pass + +class BaseMode(object): + """Application mode very abstract base class + """ + __metaclass__ = abc.ABCMeta + + manager = None + """The :class:`BaseManager` that manages this mode""" + + def __init__(self): + self.active = False + + def on_activate(self): + """Being called when the Mode is activated""" + pass + + def activate(self, mode_manager): + """Activate the mode for the given mode manager, if the mode is already active, + do nothing + + The default implementation schedules time steps at :attr:`step_rate` per + second, sets the :attr:`manager` and sets the :attr:`active` flag to True. + """ + if not self.active: + self.on_activate() + self.manager = mode_manager + self.active = True + + def on_deactivate(self): + """Being called when the Mode is deactivated""" + pass + + def deactivate(self, mode_manager): + """Deactivate the mode, if the mode is not active, do nothing + + The default implementation unschedules time steps for the mode and + sets the :attr:`active` flag to False. + """ + self.on_deactivate() + self.active = False + + +class BaseMulti(BaseMode): + """A mode with multiple submodes. One submode is active at one time. + Submodes can be switched to directly or switched in sequence. If + the Multi is active, then one submode is always active. + + Multis are useful when modes can switch in an order other than + a LIFO stack, such as in "hotseat" multiplayer games, a + "wizard" style ui, or a sequence of slides. + + Note unlike a normal :class:`Mode`, a :class:`Multi` doesn't have it's own + :attr:`clock` and :attr:`step_rate`. The active submode's are used + instead. + """ + active_submode = None + """The currently active submode""" + + def __init__(self, *submodes): + # We do not invoke the superclass __init__ intentionally + self.active = False + self.submodes = list(submodes) + + def add_submode(self, mode, before=None, index=None): + """Add the submode, but do not make it active. + + :param mode: The :class:`Mode` object to add. + + :param before: The existing mode to insert the mode before. + If the mode specified is not a submode, raise + ValueError. + + :param index: The place to insert the mode in the mode list. + Only one of ``before`` or ``index`` may be specified. + + If neither ``before`` or ``index`` are specified, the + mode is appended to the end of the list. + """ + assert before is None or index is None, ( + "Cannot specify both 'before' and 'index' arguments") + if before is not None: + index = self.submodes.index(mode) + if index is not None: + self.submodes.insert(index, mode) + else: + self.submodes.append(mode) + + def remove_submode(self, mode=None): + """Remove the submode. + + :param mode: The submode to remove, if omitted the active submode + is removed. If the mode is not present, do nothing. If the + mode is active, it is deactivated, and the next mode, if any + is activated. If the last mode is removed, the :class:`Multi` + is removed from its manager. + """ + # TODO handle multiple instances of the same subnode + if mode is None: + mode = self.active_submode + elif mode not in self.submodes: + return + next_mode = self.activate_next() + self.submodes.remove(mode) + if next_mode is mode: + if self.manager is not None: + self.manager.remove_mode(self) + self._deactivate_submode() + + def activate_subnode(self, mode, before=None, index=None): + """Activate the specified mode, adding it as a subnode + if it is not already. If the mode is already the active + submode, do nothing. + + :param mode: The mode to activate, and add as necesary. + + :param before: The existing mode to insert the mode before + if it is not already a submode. If the mode specified is not + a submode, raise ValueError. + + :param index: The place to insert the mode in the mode list + if it is not already a submode. Only one of ``before`` or + ``index`` may be specified. + + If the mode is already a submode, the ``before`` and ``index`` + arguments are ignored. + """ + if mode not in self.submodes: + self.add_submode(mode, before, index) + if self.active_submode is not mode: + self._activate_submode(mode) + + def activate_next(self, loop=True): + """Activate the submode after the current submode in order. If there + is no current submode, the first submode is activated. + + Note if there is only one submode, it's active, and `loop` is True + (the default), then this method does nothing and the subnode remains + active. + + :param loop: When :meth:`activate_next` is called + when the last submode is active, a True value for ``loop`` will + cause the first submode to be activated. Otherwise the + :class:`Multi` is removed from its manager. + :type loop: bool + + :return: + The submode that was activated or None if there is no + other submode to activate. + """ + assert self.submodes, "No submode to activate" + next_mode = None + if self.active_submode is None: + next_mode = self.submodes[0] + else: + last_mode = self.active_submode + index = self.submodes.index(last_mode) + 1 + if index < len(self.submodes): + next_mode = self.submodes[index] + elif loop: + next_mode = self.submodes[0] + self._activate_submode(next_mode) + return next_mode + + def activate_previous(self, loop=True): + """Activate the submode before the current submode in order. If there + is no current submode, the last submode is activated. + + Note if there is only one submode, it's active, and `loop` is True + (the default), then this method does nothing and the subnode remains + active. + + :param loop: When :meth:`activate_previous` is called + when the first submode is active, a True value for ``loop`` will + cause the last submode to be activated. Otherwise the + :class:`Multi` is removed from its manager. + :type loop: bool + + :return: + The submode that was activated or None if there is no + other submode to activate. + """ + assert self.submodes, "No submode to activate" + prev_mode = None + if self.active_submode is None: + prev_mode = self.submodes[-1] + else: + last_mode = self.active_submode + index = self.submodes.index(last_mode) - 1 + if loop or index >= 0: + prev_mode = self.submodes[index] + self._activate_submode(prev_mode) + return prev_mode + + def _set_active_submode(self, submode): + self.active_submode = submode + self.step_rate = submode.step_rate + + def _activate_submode(self, submode): + """Activate a submode deactivating any current submode. If the Multi + itself is active, this happens immediately, otherwise the actual + activation is deferred until the Multi is activated. If the submode + is None, the Mulitmode is removed from its manager. + + If submode is already the active submode, do nothing. + """ + if self.active_submode is submode: + return + assert submode in self.submodes, "Unknown submode" + self._deactivate_submode() + self._set_active_submode(submode) + if submode is not None: + if self.active: + self.manager.activate_mode(submode) + else: + if self.manager is not None: + self.manager.remove_mode(self) + + def clear_subnode(self): + """Clear any subnmode data""" + self.active_submode = None + self.step_rate = None + + def _deactivate_submode(self, clear_subnode=True): + """Deactivate the current submode, if any. if `clear_subnode` is + True, `active_submode` is always None when this method returns + """ + if self.active_submode is not None: + if self.active: + self.manager.deactivate_mode(self.active_submode) + if clear_subnode: + self.clear_subnode() + + def activate(self, mode_manager): + """Activate the :class:`Multi` for the specified manager. The + previously active submode of the :class:`Multi` is activated. If there + is no previously active submode, then the first submode is made active. + A :class:`Multi` with no submodes cannot be activated + """ + assert self.submodes, "No submode to activate" + self.manager = mode_manager + if self.active_submode is None: + self._set_active_submode(self.submodes[0]) + else: + self._set_active_submode(self.active_submode) + self.manager.activate_mode(self.active_submode) + super(BaseMulti, self).activate(mode_manager) + + def deactivate(self, mode_manager): + """Deactivate the :class:`Multi` for the specified manager. + The `active_submode`, if any, is deactivated. + """ + self._deactivate_submode(clear_subnode=False) + super(BaseMulti, self).deactivate(mode_manager) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/renderer/__init__.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,25 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Renderers define world presentation. This module contains the +built-in renderer classes. + +See also: + +- :class:`~grease.Renderer` abstract base class. +- :ref:`Example renderer class in the tutorial <tut-renderer-example>` +""" + +__all__ = ('Vector', 'Camera') + +from grease.renderer.vector import Vector +from grease.renderer.camera import Camera
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/renderer/camera.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,39 @@ +import pyglet + +class Camera(object): + """Sets the point of view for further renderers by altering the + model/view matrix when it is drawn. It does not actually perform + any drawing itself. + + :param position: The position vector for the camera. Sets the center of the view. + :type position: Vec2d + :param angle: Camera rotation in degrees about the z-axis. + :type angle: float + :param zoom: Scaling vector for the coordinate axis. + :type zoom: Vec2d + :param relative: Flag to indicate if the camera settings are relative + to the previous view state. If ``False`` the view state is reset before + setting the camera view by loading the identity model/view matrix. + + At runtime the camera may be manipulated via attributes with the + same names and functions as the parameters above. + """ + + def __init__(self, position=None, angle=None, zoom=None, relative=False): + self.position = position + self.angle = angle + self.zoom = zoom + self.relative = relative + + def draw(self, gl=pyglet.gl): + if not self.relative: + gl.glLoadIdentity() + if self.position is not None: + px, py = self.position + gl.glTranslatef(px, py, 0) + if self.angle is not None: + gl.glRotatef(self.angle, 0, 0, 1) + if self.zoom is not None: + sx, sy = self.zoom + gl.glScalef(sx, sy ,0) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/renderer/vector.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,166 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# + +__version__ = '$Id$' + +__all__ = ('Vector',) + +from grease.geometry import Vec2d +import ctypes +from math import sin, cos, radians +import pyglet + + +class Vector(object): + """Renders shapes in a classic vector graphics style + + :param scale: Scaling factor applied to shape vertices when rendered. + + :param line_width: The line width provided to ``glLineWidth`` before rendering. + If not specified or None, ``glLineWidth`` is not called, and the line + width used is determined by the OpenGL state at the time of rendering. + + :param anti_alias: If ``True``, OpenGL blending and line smoothing is enabled. + This allows for fractional line widths as well. If ``False``, the blending + and line smoothing modes are unchanged. + + :param corner_fill: If true (the default), the shape corners will be filled + with round points when the ``line_width`` exceeds 2.0. This improves + the visual quality of the rendering at larger line widths at some + cost to performance. Has no effect if ``line_width`` is not specified. + + :param position_component: Name of :class:`grease.component.Position` + component to use. Shapes rendered are offset by the entity positions. + + :param renderable_component: Name of :class:`grease.component.Renderable` + component to use. This component specifies the entities to be + rendered and their base color. + + :param shape_component: Name of :class:`grease.component.Shape` + component to use. Source of the shape vertices for each entity. + + The entities rendered are taken from the intersection of he position, + renderable and shape components each time :meth:`draw` is called. + """ + + CORNER_FILL_SCALE = 0.6 + CORNER_FILL_THRESHOLD = 2.0 + + def __init__(self, scale=1.0, line_width=None, anti_alias=True, corner_fill=True, + position_component='position', + renderable_component='renderable', + shape_component='shape'): + self.scale = float(scale) + self.corner_fill = corner_fill + self.line_width = line_width + self.anti_alias = anti_alias + self._max_line_width = None + self.position_component = position_component + self.renderable_component = renderable_component + self.shape_component = shape_component + + def set_world(self, world): + self.world = world + + def _generate_verts(self): + """Generate vertex and index arrays for rendering""" + vert_count = sum(len(shape.verts) + 1 + for shape, ignored, ignored in self.world.components.join( + self.shape_component, self.position_component, self.renderable_component)) + v_array = (CVertColor * vert_count)() + if vert_count > 65536: + i_array = (ctypes.c_uint * 2 * vert_count)() + i_size = pyglet.gl.GL_UNSIGNED_INT + else: + i_array = (ctypes.c_ushort * (2 * vert_count))() + i_size = pyglet.gl.GL_UNSIGNED_SHORT + v_index = 0 + i_index = 0 + scale = self.scale + rot_vec = Vec2d(0, 0) + for shape, position, renderable in self.world.components.join( + self.shape_component, self.position_component, self.renderable_component): + shape_start = v_index + angle = radians(-position.angle) + rot_vec.x = cos(angle) + rot_vec.y = sin(angle) + r = int(renderable.color.r * 255) + g = int(renderable.color.g * 255) + b = int(renderable.color.b * 255) + a = int(renderable.color.a * 255) + for vert in shape.verts: + vert = vert.cpvrotate(rot_vec) * scale + position.position + v_array[v_index].vert.x = vert.x + v_array[v_index].vert.y = vert.y + v_array[v_index].color.r = r + v_array[v_index].color.g = g + v_array[v_index].color.b = b + v_array[v_index].color.a = a + if v_index > shape_start: + i_array[i_index] = v_index - 1 + i_index += 1 + i_array[i_index] = v_index + i_index += 1 + v_index += 1 + if shape.closed and v_index - shape_start > 2: + i_array[i_index] = v_index - 1 + i_index += 1 + i_array[i_index] = shape_start + i_index += 1 + return v_array, i_size, i_array, i_index + + def draw(self, gl=pyglet.gl): + vertices, index_size, indices, index_count = self._generate_verts() + if index_count: + if self.anti_alias: + gl.glEnable(gl.GL_LINE_SMOOTH) + gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) + gl.glEnable(gl.GL_BLEND) + gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) + gl.glPushClientAttrib(gl.GL_CLIENT_VERTEX_ARRAY_BIT) + gl.glEnableClientState(gl.GL_VERTEX_ARRAY) + gl.glEnableClientState(gl.GL_COLOR_ARRAY) + gl.glVertexPointer( + 2, gl.GL_FLOAT, ctypes.sizeof(CVertColor), ctypes.pointer(vertices)) + gl.glColorPointer( + 4, gl.GL_UNSIGNED_BYTE, ctypes.sizeof(CVertColor), + ctypes.pointer(vertices[0].color)) + if self.line_width is not None: + gl.glLineWidth(self.line_width) + if self._max_line_width is None: + range_out = (ctypes.c_float * 2)() + gl.glGetFloatv(gl.GL_ALIASED_LINE_WIDTH_RANGE, range_out) + self._max_line_width = float(range_out[1]) * self.CORNER_FILL_SCALE + if self.corner_fill and self.line_width > self.CORNER_FILL_THRESHOLD: + gl.glEnable(gl.GL_POINT_SMOOTH) + gl.glPointSize( + min(self.line_width * self.CORNER_FILL_SCALE, self._max_line_width)) + gl.glDrawArrays(gl.GL_POINTS, 0, index_count) + gl.glDrawElements(gl.GL_LINES, index_count, index_size, ctypes.pointer(indices)) + gl.glPopClientAttrib() + + +class CVert(ctypes.Structure): + _fields_ = [("x", ctypes.c_float), ("y", ctypes.c_float)] + +class CColor(ctypes.Structure): + _fields_ = [ + ("r", ctypes.c_ubyte), + ("g", ctypes.c_ubyte), + ("b", ctypes.c_ubyte), + ("a", ctypes.c_ubyte), + ] + +class CVertColor(ctypes.Structure): + _fields_ = [("vert", CVert), ("color", CColor)] +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/grease/world.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,302 @@ +############################################################################# +# +# Copyright (c) 2010 by Casey Duncan and contributors +# All Rights Reserved. +# +# This software is subject to the provisions of the MIT License +# A copy of the license should accompany this distribution. +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# +############################################################################# +"""Worlds are environments described by a configuration of components, systems and +renderers. These parts describe the data, behavioral and presentation aspects +of the world respectively. + +The world environment is the context within which entities exist. A typical +application consists of one or more worlds containing entities that evolve +over time and react to internal and external interaction. + +See :ref:`an example of world configuration in the tutorial <tut-world-example>`. +""" + +__version__ = '$Id$' + +import itertools +from grease import mode +from grease.component import ComponentError +from grease.entity import Entity, ComponentEntitySet + + +class BaseWorld(object): + """A coordinated collection of components, systems and entities + + A world is also a mode that may be pushed onto a + :class:`grease.mode.Manager` + """ + + components = None + """:class:`ComponentParts` object containing all world components. + :class:`grease.component.Component` objects define and contain all entity data + """ + + systems = None + """:class:`Parts` object containing all world systems. + :class:`grease.System` objects define world and entity behavior + """ + + renderers = None + """:class:`Parts` object containing all world renderers. + :class:`grease.Renderer` objects define world presentation + """ + + entities = None + """Set of all entities that exist in the world""" + + def __init__(self): + self.components = ComponentParts(self) + self.systems = Parts(self) + self.renderers = Parts(self) + self.new_entity_id = itertools.count().next + self.new_entity_id() # skip id 0 + self.entities = WorldEntitySet(self) + self._full_extent = EntityExtent(self, self.entities) + self._extents = {} + self.configure() + + def configure(self): + """Hook to configure the world after construction. This method + is called immediately after the world is initialized. Override + in a subclass to configure the world's components, systems, + and renderers. + + The default implementation does nothing. + """ + + def __getitem__(self, entity_class): + """Return an :class:`EntityExtent` for the given entity class. This extent + can be used to access the set of entities of that class in the world + or to query these entities via their components. + + Examples:: + + world[MyEntity] + world[...] + + :param entity_class: The entity class for the extent. + + May also be a tuple of entity classes, in which case + the extent returned contains union of all entities of the classes + in the world. + + May also be the special value ellipsis (``...``), which + returns an extent containing all entities in the world. This allows + you to conveniently query all entities using ``world[...]``. + """ + if isinstance(entity_class, tuple): + entities = set() + for cls in entity_class: + if cls in self._extents: + entities |= self._extents[cls].entities + return EntityExtent(self, entities) + elif entity_class is Ellipsis: + return self._full_extent + try: + return self._extents[entity_class] + except KeyError: + extent = self._extents[entity_class] = EntityExtent(self, set()) + return extent + + def draw_renderers(self): + """Draw all renderers""" + for renderer in self.renderers: + renderer.draw() + +class WorldEntitySet(set): + """Entity set for a :class:`World`""" + + def __init__(self, world): + self.world = world + + def add(self, entity): + """Add the entity to the set and all necessary class sets + Return the unique entity id for the entity, creating one + as needed. + """ + super(WorldEntitySet, self).add(entity) + for cls in entity.__class__.__mro__: + if issubclass(cls, Entity): + self.world[cls].entities.add(entity) + + def remove(self, entity): + """Remove the entity from the set and, world components, + and all necessary class sets + """ + super(WorldEntitySet, self).remove(entity) + for component in self.world.components: + try: + del component[entity] + except KeyError: + pass + for cls in entity.__class__.__mro__: + if issubclass(cls, Entity): + self.world[cls].entities.discard(entity) + + def discard(self, entity): + """Remove the entity from the set if it exists, if not, + do nothing + """ + try: + self.remove(entity) + except KeyError: + pass + + +class EntityExtent(object): + """Encapsulates a set of entities queriable by component. Extents + are accessed by using an entity class as a key on the :class:`World`:: + + extent = world[MyEntity] + """ + + entities = None + """The full set of entities in the extent""" + + def __init__(self, world, entities): + self.__world = world + self.entities = entities + + def __getattr__(self, name): + """Return a queriable :class:`ComponentEntitySet` for the named component + + Example:: + + world[MyEntity].movement.velocity > (0, 0) + + Returns a set of entities where the value of the :attr:`velocity` field + of the :attr:`movement` component is greater than ``(0, 0)``. + """ + component = getattr(self.__world.components, name) + return ComponentEntitySet(component, self.entities & component.entities) + + +class Parts(object): + """Maps world parts to attributes. The parts are kept in the + order they are set. Parts may also be inserted out of order. + + Used for: + + - :attr:`World.systems` + - :attr:`World.renderers` + """ + + _world = None + _parts = None + _reserved_names = ('entities', 'entity_id', 'world') + + def __init__(self, world): + self._world = world + self._parts = [] + + def _validate_name(self, name): + if (name in self._reserved_names or name.startswith('_') + or hasattr(self.__class__, name)): + raise ComponentError('illegal part name: %s' % name) + return name + + def __setattr__(self, name, part): + if not hasattr(self.__class__, name): + self._validate_name(name) + if not hasattr(self, name): + self._parts.append(part) + else: + old_part = getattr(self, name) + self._parts[self._parts.index(old_part)] = part + super(Parts, self).__setattr__(name, part) + if hasattr(part, 'set_world'): + part.set_world(self._world) + elif name.startswith("_"): + super(Parts, self).__setattr__(name, part) + else: + raise AttributeError("%s attribute is read only" % name) + + def __delattr__(self, name): + self._validate_name(name) + part = getattr(self, name) + self._parts.remove(part) + super(Parts, self).__delattr__(name) + + def insert(self, name, part, before=None, index=None): + """Add a part with a particular name at a particular index. + If a part by that name already exists, it is replaced. + + :arg name: The name of the part. + :type name: str + + :arg part: The component, system, or renderer part to insert + + :arg before: A part object or name. If specified, the part is + inserted before the specified part in order. + + :arg index: If specified, the part is inserted in the position + specified. You cannot specify both before and index. + :type index: int + """ + assert before is not None or index is not None, ( + "Must specify a value for 'before' or 'index'") + assert before is None or index is None, ( + "Cannot specify both 'before' and 'index' arguments when inserting") + self._validate_name(name) + if before is not None: + if isinstance(before, str): + before = getattr(self, before) + index = self._parts.index(before) + if hasattr(self, name): + old_part = getattr(self, name) + self._parts.remove(old_part) + self._parts.insert(index, part) + super(Parts, self).__setattr__(name, part) + if hasattr(part, 'set_world'): + part.set_world(self._world) + + def __iter__(self): + """Iterate the parts in order""" + return iter(tuple(self._parts)) + + def __len__(self): + return len(self._parts) + + +class ComponentParts(Parts): + """Maps world components to attributes. The components are kept in the + order they are set. Components may also be inserted out of order. + + Used for: :attr:`World.components` + """ + + def join(self, *component_names): + """Join and iterate entity data from multiple components together. + + For each entity in all of the components named, yield a tuple containing + the entity data from each component specified. + + This is useful in systems that pull data from multiple components. + + Typical Usage:: + + for position, movement in world.components.join("position", "movement"): + # Do something with each entity's position and movement data + """ + if component_names: + components = [getattr(self, self._validate_name(name)) + for name in component_names] + if len(components) > 1: + entities = components[0].entities & components[1].entities + for comp in components[2:]: + entities &= comp.entities + else: + entities = components[0].entities + for entity in entities: + yield tuple(comp[entity] for comp in components) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/mode.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,21 @@ + +from grease.mode import * +from fife.extensions.basicapplication import ApplicationBase +import abc + +class FifeManager(BaseManager, ApplicationBase): + + def __init__(self, TDS): + ApplicationBase.__init__(self, TDS) + self.modes = [] + self._settings = TDS + + def _pump(self): + if self.current_mode: + self.current_mode.pump(self.engine.getTimeManager().getTimeDelta() / 1000) + +class FifeMode(BaseMode): + + @abc.abstractmethod + def pump(self, dt): + """Performs actions every frame""" \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/parpg/world.py Tue Jul 12 10:16:48 2011 +0200 @@ -0,0 +1,12 @@ +from grease.world import * +from mode import FifeMode + +class World(FifeModeMode, BaseWorld): + + def pump(self, dt): + for component in self.components: + if hasattr(component, "step"): + component.step(dt) + for system in self.systems: + if hasattr(system, "step"): + system.step(dt)