"""Zones are part of worlds"""
import math
import common
import serialize
import geometry
import profiler
pymunk = common.pymunk
class DuplicateActor(Exception): """An actor was already in the zone"""
class ActorNotFound(Exception): """Could not find the actor in the zone"""
# Use this to configure the Physics stepsize
PHYSICS_ITERATIONS = 10
[docs]class Zone(geometry.Rectangle, common.Loggable):
"""A zone
A zone is part of a world. It is a container for objects
and it controls whether objects will take part in world
updates.
"""
my_properties = (
serialize.B('active', False, 'whether the zone is active'),
serialize.L('actors', set(), 'the actors in this zone'),
serialize.F('physics_stepsize', 10.0, 'the size of physics steps in ms'),
serialize.L('global_force', (0,0), 'the global force for physics'),
serialize.F('_rtf', 1.0, 'debugging aid to slow down physics'),
)
def __init__(self):
"""Initialise the zone"""
super(Zone, self).__init__()
self.addLogger()
self.physics_stepsize = 10.0
self.global_force = (0,0)
self.active = False
self.setSpatial(-1000, -1000, 2000, 2000)
self.clearActors()
self._initPhysics()
self._rtf = 1.0 # A debugging aid to slow down physics
### Serializing ###
[docs] def init(self):
"""Initialise from serialized state"""
self.addLogger()
self.log.info('Initializing zone %s' % self)
super(Zone, self).init()
self._initPhysics()
for actor in self.actors:
actor.init()
if actor.getPhysical():
actor.getPhysical().init()
self._addPhysicalActor(actor)
### Zones ###
[docs] def updateZone(self, interval, world):
"""Update the objects in the zone"""
#
# Iterate through actors - use a list of the actors
# in case the actor wants to update the list of
# actors during this iteration
for actor in list(self.actors):
if actor.active:
profiler.PROFILER.start(actor, 'updateActor')
actor.updateActor(interval, world)
profiler.PROFILER.end()
#
# Do physics if we need to
if self._physics_objects:
self.updatePhysics(interval)
[docs] def wouldContain(self, actor):
"""Return True if this zone would contain the actor as it is right now
The base Zone implementation uses spatial overlapping as the criteria but you
can create custom zones that use other criteria to decide which actors should
be in the zone.
"""
return self.isOverlapping(actor)
[docs] def addActor(self, actor):
"""Add an actor to the zone"""
if actor in self.actors:
raise DuplicateActor('The actor %s is already in the zone' % actor)
else:
self.actors.add(actor)
if actor.getPhysical():
self._addPhysicalActor(actor)
[docs] def hasActor(self, actor):
"""Return True if the actor is in this zone"""
return actor in self.actors
[docs] def removeActor(self, actor):
"""Remove an actor from the zone"""
try:
self.actors.remove(actor)
except KeyError:
raise ActorNotFound('The actor %s was not in the zone' % actor)
else:
if actor in self._physics_objects:
self._physics_objects.remove(actor)
p = actor.getPhysical()
#
# The try-catch here is probably not required but if the game
# is playing around with the physics space then it might
# remove something without alerting the zone so we catch it
# here.
try:
self.space.remove(p.body)
if p.shape:
self.space.remove(p.shape)
except KeyError, err:
self.log.error('Actor %s already removed from physics space' % actor.getNiceName())
[docs] def clearActors(self):
"""Remove all actors"""
self.actors = set()
### Finding ###
[docs] def findActorByName(self, name):
"""Return the actor with the given name"""
for actor in self.actors:
if actor.name == name:
return actor
else:
raise ActorNotFound('Could not find actor "%s"' % name)
[docs] def findActorsByTag(self, tag):
"""Return all the actors with a certain tag"""
return [actor for actor in self.actors if actor.tag == tag]
[docs] def findFirstActorByTag(self, tag):
"""Return the first actor found with the given tag or raise an error"""
for actor in self.actors:
if actor.tag == tag:
return actor
else:
raise ActorNotFound('Could not find actor with tag "%s"' % tag)
[docs] def getActors(self):
"""Return all the actors"""
return self.actors
### Physics ###
def _initPhysics(self):
"""Initialize the physics engine"""
#
# Pymunk may not be installed - if so then we skip creating any physics context
if not common.PYMUNK_OK:
self.log.debug('No pymunk - physics disabled')
self._physics_objects = []
return
#
# Create a context for the physics
self.log.debug('Initializing physics engine with %d iterations' % PHYSICS_ITERATIONS)
self.space = pymunk.Space(PHYSICS_ITERATIONS)
self.space.add_collision_handler(2, 2, self._checkCollision, None, None, None)
#
# List of physics objects that we need to update
self._physics_objects = []
self._shape_dict = {}
def _checkCollision(self, space, arbiter):
"""Return True if the collision should occur"""
s1, s2 = arbiter.shapes[0], arbiter.shapes[1]
self._collisions.append((s1, s2))
return True
def _addPhysicalActor(self, actor):
"""Add an actor with physics to the zone"""
p = actor.getPhysical()
p.space = self.space
if p.shape:
self.space.add(p.body, p.shape)
self._shape_dict[p.shape] = actor
else:
self.space.add(p.body)
self._physics_objects.append(actor)
actor.syncPhysics()
[docs] def updatePhysics(self, interval):
"""Perform a step of the physics engine
You do not normally need to call this method as it is called by the
updateZone method. You may call this to advance the physics simulation
along without affecting other game elements.
"""
#
# Globally applied forces
self.space.gravity = self.global_force
#
# Do calculations
self._collisions = []
while interval > 0.0:
togo = min(self.physics_stepsize, interval)
self.space.step(togo/1000.0*self._rtf) # rtf is a debugging aid to go into slow motion mode
interval -= togo
#
# Apply all the collisions
for shape1, shape2 in self._collisions:
actor1, actor2 = self._shape_dict[shape1], self._shape_dict[shape2]
actor1.processEvent(('collision', actor2))
actor2.processEvent(('collision', actor1))
#
# Now update all the tracked objects in world space
for actor in self._physics_objects:
p = actor.getPhysical()
actor.moveTo(*p.body.position, no_sync=True, override_lock=True)
p.velocity = tuple(p.body.velocity)
if p.update_angle:
actor.setAngle(-math.degrees(p.body.angle), override_lock=True)
[docs] def setPhysicsStepsize(self, interval):
"""Set the maximum step size for physics calculations"""
self.physics_stepsize = interval
[docs] def setGlobalForce(self, force):
"""Set the global force for physics"""
self.global_force = force
[docs] def sleepActor(self, actor):
"""Tell the actor to go to sleep from a physics perspective
The actor will still be visible and will still be updated but it
will not update its physics. Useful for optimising when an actor
does not need to interact with the physics simulation for a while.
"""
actor.getPhysical().body.sleep()
[docs] def wakeActor(self, actor):
"""Tell the actor to go to wake up from a physics perspective
An actor that was put to sleep (via sleepActor) will be woken
up and take part in the physics simulation again.
"""
actor.getPhysical().body.activate()
class TagIncludeZone(Zone):
"""A zone that includes any actor with a tag chosen from a list"""
def __init__(self, tag_list):
"""Initialise the TagIncludeZone"""
super(TagIncludeZone, self).__init__()
self.tag_list = tag_list
def wouldContain(self, actor):
"""Return True if this actor has the right tag"""
return actor.tag in self.tag_list
class TagExcludeZone(Zone):
"""A zone that excludes any actor with a tag chosen from a list"""
def __init__(self, tag_list):
"""Initialise the TagExcludeZone"""
super(TagExcludeZone, self).__init__()
self.tag_list = tag_list
def wouldContain(self, actor):
"""Return True if this actor doesn't have a tag matching our list"""
return actor.tag not in self.tag_list