class PhysicallyMountableActor(AbstractMountableActor): """An physical actor that you can mount other physical actors to The other actors are located at a certain position relative to the position of this actor. You can use this actor to create clusters either visually or functionally. All actors must be under the control of the physics engine. """ my_properties = ( serialize.L('children', [], 'the child actors that we own'), serialize.L('_world', [], 'the world that we belong to'), serialize.D('_offsets', {}, 'the offsets to our children'), ) def __init__(self, tag, name='', mass=0.0, **kw): """Initialize the MountableActor""" if not mass: raise NoPhysicalConditions('Mass needs to be specified for mountable actor') super(PhysicallyMountableActor, self).__init__(tag, name, **kw) self.setPhysical(physical.PhysicalBody(mass=mass, update_angle=True)) self._joints = [] def init(self): """Initialise from serialized form""" self._joints = [] super(PhysicallyMountableActor, self).init() def mountActor(self, actor, (x, y), original_rotation=False, rotate_with_actor=True):
class AbstractMountableActor(CompositeActor): """An base class for actors that you can mount other actors to The other actors are located at a certain position relative to the position of this actor. You can use this actor to create clusters either visually or functionally. """ my_properties = ( serialize.L('children', [], 'the child actors that we own'), serialize.L('_world', [], 'the world that we belong to'), serialize.D('_offsets', {}, 'the offsets to our children'), ) def __init__(self, *args, **kw): """Initialize the MountableActor""" super(AbstractMountableActor, self).__init__(*args, **kw) self._offsets = {} def mountActor(self, actor, (x, y), original_rotation=False, rotate_with_actor=True):
class GeneralStore(serialize.Serializable): """Stores things""" my_properties = ( serialize.S('base_path', '', 'the base location to find files'), serialize.L('raw_items', [], 'the items we have registered'), ) def __init__(self): """Initialize the store""" self.items = {} self.raw_items = [] self.base_path = '' def init(self): """Initialise from serialized form""" self.items = {} old_items, self.raw_items = self.raw_items, [] for item in old_items: self.registerItem(*item) def setPath(self, path): """Set our base path to locate images""" if not os.path.isdir(path): raise BadPath('The path %s is not a directory' % path) self.base_path = path def _resolveFilename(self, name): """Return the name to a file""" if os.path.isfile(name): return name else: return os.path.join(self.base_path, name) def registerItem(self, name, *args, **kw): """Register an item""" # # Make sure we only register once if name in self.items: raise DuplicateItem('The item named "%s" is already registered' % name) # return self._registerItem(name, *args, **kw) def getItems(self): """Return all the items""" return self.items.values() def getItemDefinitions(self): """Return all the item definitions""" return self.raw_items def clearItems(self): """Clear all the items""" self.items = {} self.raw_items = [] def removeItem(self, name): """Remove the named item""" try: del(self.items[name]) except KeyError: raise UnknownItem('The item "%s" was not in the collection' % name) self.raw_items = [item for item in self.raw_items if item[0] != name] def getNames(self): """Return the names of all the items""" return self.items.keys() def getItem(self, name): """Return an item""" try: return self.items[name] except KeyError: raise UnknownItem('The item called "%s" could not be found' % name)
class Engine(common.Loggable, serialize.Serializable, common.EventAware): """The main Serge engine The engine manages a set of worlds and allows a single :doc:`world`, the current world, to be automatically updated on a certain time frequency. """ my_properties = ( serialize.L('_worlds', [], 'the worlds in this engine'), serialize.O('renderer', None, 'the renderer for this engine'), serialize.O('sprites', None, 'the sprite registry'), serialize.S('_current_world_name', '', 'the name of the current world'), serialize.L('_recent_worlds', [], 'the list of worlds recently visited'), serialize.B('fullscreen', False, 'whether to display in full screen or not'), ) def __init__(self, width=640, height=480, title='Serge', backcolour=(0,0,0), icon=None, fullscreen=False): """Initialise the engine :param width: width of the screen :param height: height of the screen """ self.title = title self.fullscreen = fullscreen self.addLogger() self.initEvents() self.log.info('Starting serge engine (v%s)' % common.version) SetCurrentEngine(self) super(Engine, self).__init__() self.clearWorlds() self.renderer = render.Renderer(width, height, title, backcolour, icon, fullscreen) self.sprites = visual.Register self._stop_requested = False self._current_world_name = '' self._builder = None self._keyboard = input.Keyboard() self._mouse = input.Mouse(self) self._stats = EngineStats() self._recent_worlds = [] def init(self): """Initialise ourself""" self.addLogger() self.log.info('Initializing serge engine (v%s)' % common.version) SetCurrentEngine(self) # # Prepare all the worlds for world in self._worlds.values(): world.init() self._current_world = None self._snapshots_enabled = True self._snapshot_count = 0 # # Recover the sprite registry from our own self.sprites.init() visual.Register = self.sprites self.setCurrentWorldByName(self._current_world_name) # self.renderer.init() # self._builder = None self._keyboard = input.Keyboard() self._mouse = input.Mouse(self) def addWorld(self, world): """Add a world to the engine :param world: the world instance to add """ if world.name in self._worlds: raise DuplicateWorld('A world named "%s" already exists' % world.name) if world in self._worlds.values(): raise DuplicateWorld('This world (named "%s") already exists' % world.name) self._worlds[world.name] = world world.setEngine(self) def removeWorld(self, world): """Remove a world from the engine :param world: the world instance to remove """ self.removeWorldNamed(world.name) def removeWorldNamed(self, name): """Remove a world with a given name :param name: the name of the world to remove """ try: del(self._worlds[name]) except KeyError: raise WorldNotFound('No world named "%s" in the worlds collection' % name) def clearWorlds(self): """Clear all the worlds""" self._worlds = {} self._current_world = None def getWorld(self, name): """Return the named world :param name: the name of the world to return """ try: return self._worlds[name] except KeyError: raise WorldNotFound('No world named "%s" in the worlds collection' % name) def getWorlds(self): """Return all the worlds""" return self._worlds.values() def getCurrentWorld(self): """Return the currently selected world""" if self._current_world: return self._current_world else: raise NoCurrentWorld('There is no current world') def setCurrentWorld(self, world): """Set the current world :param world: the world to set as the current world """ self.setCurrentWorldByName(world.name) def setCurrentWorldByName(self, name): """Set the current world to the one with the given name :param name: the name of the world to set as the current world """ self.log.info('Setting current world to %s' % name) if self._current_world_name: self._recent_worlds.append(self._current_world_name) new_world = self.getWorld(name) # # Send activation and deactivation callbacks to worlds to allow them to do # any housekeeping if new_world != self._current_world: if self._current_world: self._current_world.deactivateWorld() new_world.activateWorld() # self._current_world = new_world self._current_world_name = name return new_world def goBackToPreviousWorld(self, obj=None, arg=None): """Return to the world we were in before this one The arguments are never used and are just here to allow you to use this method as an event callback. """ try: name = self._recent_worlds.pop() except IndexError: raise WorldNotFound('There are no previous worlds') self.setCurrentWorldByName(name) # We will have pushed the current world onto the stack, which we don't want # so take it off again self._recent_worlds.pop() def updateWorld(self, interval): """Update the current world""" if self._current_world: self._current_world.updateWorld(interval) else: raise NoCurrentWorld('Cannot update when there is no current world') def run(self, fps, endat=None): """Run the updates at the specified frames per second until the optional endtime :param fps: the target frames per second (integer) :param endat: a time to stop the engine at (long), eg time.time()+60 to run for a minute """ self.log.info('Engine starting (requested fps=%d)' % fps) clock = pygame.time.Clock() self._stop_requested = False while True: # # Watch for ending conditions if self._stop_requested or (endat and time.time() >= endat): break # # Main render activity try: # # Pause clock.tick(fps) # # Do the update for our actors interval = clock.get_time() if self._current_world: self.updateWorld(interval) # # Do builder work if needed if self._builder: self._builder.updateBuilder(interval) # # Events that may have happened self._handleEvents() # Inputs self._mouse.update(interval) self._keyboard.update(interval) pygame.event.clear() # Sound sound.Music.update(interval) sound.Sounds.update(interval) if self._current_world: self.processEvents() # # Get ready to render self._stats.beforeRender() self.renderer.preRender() # # Render the active world if self._current_world: self._current_world.renderTo(self.renderer, interval) # # Render the builder if needed if self._builder: self._builder.renderTo(self.renderer, interval) # # And render all of our layers self.renderer.render() self.processEvent((events.E_AFTER_RENDER, self)) self._stats.afterRender() # # Show the screen pygame.display.flip() self._stats.recordFrame() # except NotImplementedError, err: self.log.error('Failed in main loop: %s' % err) # self.log.info('Engine stopping') self.processEvent((events.E_AFTER_STOP, self)) self.log.info('Engine info: %s' % (self._stats,))
class Renderer(common.Loggable, serialize.Serializable, common.EventAware): """The main rendering component""" my_properties = ( serialize.L('layers', [], 'the layers we render to'), serialize.I('width', 640, 'the width of the screen'), serialize.I('height', 480, 'the height of the screen'), serialize.S('title', 'Serge', 'the title of the main window'), serialize.L('backcolour', (0,0,0), 'the background colour'), serialize.O('camera', None, 'the camera for this renderer'), serialize.O('icon', None, 'the icon for the main window'), serialize.B('fullscreen', False, 'whether to display in full screen or not'), ) def __init__(self, width=640, height=480, title='Serge', backcolour=(0,0,0), icon=None, fullscreen=False): """Initialise the Renderer""" self.addLogger() self.initEvents() self.width = width self.height = height self.title = title self.layers = [] self.backcolour = backcolour self.fullscreen = fullscreen self.camera = camera.Camera() self.camera.setSpatial(0, 0, self.width, self.height) self.icon = icon self.init() ### Serializing ### def init(self): """Initialise from serialized state""" self.addLogger() self.initEvents() self._sort_needed = False pygame.display.set_caption(self.title) # # Tried the following with flags but no impact pygame.FULLSCREEN|pygame.HWSURFACE|pygame.DOUBLEBUF flags = pygame.FULLSCREEN if self.fullscreen else 0 self.surface = pygame.display.set_mode((self.width, self.height), flags ) for layer in self.layers: layer.setSurface(pygame.Surface((self.width, self.height), pygame.SRCALPHA, 32)) layer.init() self.camera.init() self.camera.resizeTo(self.width, self.height) if self.icon: pygame.display.set_icon(visual.Register.getItem(self.icon).raw_image) ### Layers ### def addLayer(self, layer): """Add a layer to the rendering""" self.log.info('Adding layer "%s" at %d' % (layer.name, layer.order)) if layer in self.layers: raise DuplicateLayer('The layer %s is already in the renderer' % layer) else: self.layers.append(layer) self._sort_needed = True self.resetSurfaces() return layer def getLayer(self, name): """Return the named layer""" for layer in self.layers: if layer.name == name: return layer else: raise UnknownLayer('No layer with name "%s" was found' % (name,)) def getLayerBefore(self, layer): """Return the layer before the specified one in terms of rendering order""" for test_layer in reversed(self.getLayers()): if test_layer.order < layer.order: return test_layer else: raise NoLayer('There is no layer before %s' % layer.getNiceName()) def resetSurfaces(self): """Recreate the surfaces for our layers When layers are added we sometimes need to reset the layers, for instance, virtual layers need to be shifted around so that they have the right order. """ self._sortLayers() for layer in self.getLayers(): layer.initSurface(self) def getLayers(self): """Return all the layers""" return self.layers def removeLayer(self, layer): """Remove the layer from the rendering""" try: self.layers.remove(layer) except ValueError: raise UnknownLayer('The layer %s was not found' % layer.getNiceName()) def removeLayerNamed(self, name): """Remove the layer with the specific name""" layer = self.getLayer(name) self.removeLayer(layer) def clearLayers(self): """Clear all the layers""" self.layers = [] def _sortLayers(self): """Sort the layers into the right order""" self.layers.sort(lambda l1, l2 : cmp(l1.order, l2.order)) self._sort_needed = False def orderActors(self, actors): """Return the list of actors sorted by who should be processed first to correctly render The actors are checked to see which layer they reside on and then this is used to order the returned list. """ # # Make a lookup table to quickly find layers layers = dict([(layer.name, layer.order) for layer in self.getLayers()]) actor_list = [(layers.get(actor.getLayerName(), 0), actor) for actor in actors] actor_list.sort() # return [actor for _, actor in actor_list] ### Rendering ### def clearSurface(self): """Clear the surface""" self.surface.fill(self.backcolour) def preRender(self): """Prepare for new rendering""" self.clearSurface() for layer in self.getLayers(): if layer.active: layer.clearSurface() layer.preRender() def render(self): """Render all the layers""" # # Post rendering events for layer in self.layers: if layer.active: layer.postRender() # # Put layers in the right order if self._sort_needed: self._sortLayers() # # Render all layers for layer in self.layers: if layer.active: layer.render(self.surface) # self.processEvent((events.E_AFTER_RENDER, self)) def getSurface(self): """Return the overall surface""" return self.surface ### Camera stuff ### def setCamera(self, camera): """Set our camera""" self.camera = camera def getCamera(self): """Return our camera""" return self.camera def getScreenSize(self): """Returns the screen size""" return (self.width, self.height)
class Rectangle(SpatialObject, serialize.Serializable): """Represents a rectangle""" my_properties = ( serialize.L('rect', (0, 0, 0, 0), 'the spatial extent of the actor'), ) def __init__(self, x=0, y=0, w=0, h=0): """Return a new object based on top left, top right, width and height""" self.rect = SimpleRect(x, y, w, h) def init(self): """Initialize from serialized""" if not hasattr(self, 'rect'): self.rect = SimpleRect(0, 0, 0, 0) else: self.rect = SimpleRect(*self.rect) @classmethod def fromCenter(cls, cx, cy, w, h): """Return a new rectangle giving the center x, y and width, height""" return cls(cx-w/2, cy-h/2, w, h) def isInside(self, other): """Return True if this object is inside another""" return other.rect.contains(self.rect) == 1 def isOverlapping(self, other): """Return True if this object overlaps another""" return other.rect.colliderect(self.rect) == 1 def setSpatial(self, x, y, w, h): """Set the spatial details of ourself""" self.rect = pygame.Rect(x, y, w, h) def setOrigin(self, x ,y): """Set the left and top coords""" self.rect.left = x self.rect.top = y def getSpatial(self): """Return spatial details""" return self.rect def setSpatialCentered(self, x, y, w, h): """Set the spatial details of ourself""" self.setSpatial(x-w/2, y-h/2, w, h) def getSpatialCentered(self): """Return spatial details""" x, y, w, h = self.getSpatial() return (x+w/2, y+h/2, w, h) def getRelativeLocation(self, other): """Return the relative location of another object""" return (other.rect.x - self.rect.x, other.rect.y - self.rect.y) def getRelativeLocationCentered(self, other): """Return the relative location of another object""" l1, l2 = self.getSpatialCentered(), other.getSpatialCentered() return (l2[0] - l1[0], l2[1] - l1[1]) def move(self, dx, dy): """Move the actor""" self.rect.move_ip(dx, dy) def moveTo(self, x, y): """Move the center of this object to the given location""" self.rect.x = x-self.rect.width/2 self.rect.y = y-self.rect.height/2 def resizeBy(self, w, h): """Resize the spatial by the given extent""" self.rect.inflate_ip(w, h) def resizeTo(self, w, h): """Resize the spatial by the given extent""" self.resizeBy(w-self.width, h-self.height) def scale(self, factor): """Rescale the spatial extent""" _, _, w, h = self.rect nw, nh = w*factor, h*factor self.resizeTo(nw, nh) def getArea(self): """Return the area of the shape""" return self.rect.width * self.rect.height ### Simple access ### @property def x(self): return self.rect.x+self.rect.width/2 @x.setter def x(self, value): self.moveTo(value, self.y) @property def y(self): return self.rect.y+self.rect.height/2 @y.setter def y(self, value): self.moveTo(self.x, value) @property def width(self): return self.rect.width @property def height(self): return self.rect.height
class CompositeActor(Actor): """An actor that can have children, which are also actors World operations on the parent, like adding and removing, will also apply to the children. If the children are removed from the parent then they are also removed from the world. """ # When serializing the children property can be needed (eg for the active and visible # properties) children = tuple() my_properties = ( serialize.L('children', [], 'the child actors that we own'), serialize.L('_world', [], 'the world that we belong to'), ) def __init__(self, *args, **kw): """Initialise the actor""" self.children = ActorCollection() self._active = True self._visible = True self._world = None super(CompositeActor, self).__init__(*args, **kw) ### World events ### def removedFromWorld(self, world): """Called when we are being removed from the world""" super(CompositeActor, self).removedFromWorld(world) for child in self.getChildren()[:]: world.removeActor(child) self._world = None def addedToWorld(self, world): """Called when we are being added to the world""" super(CompositeActor, self).addedToWorld(world) for child in self.getChildren(): world.addActor(child) self._world = world ### Children ### def addChild(self, actor): """Add a child actor""" self.children.append(actor) actor.linkEvent(events.E_REMOVED_FROM_WORLD, self._childRemoved) # # If we are already in the world then add this actor to the world also if self._world: try: self._world.addActor(actor) except world.DuplicateActor: # Ok if the actor is already there pass # return actor def removeChild(self, actor, leave_in_world=False): """Remove a child actor""" try: self.children.remove(actor) except ValueError: raise InvalidActor('The actor %s was not a child of %s' % (actor.getNiceName(), self.getNiceName())) # # Remove the child from the world if self._world and not leave_in_world: self._world.removeActor(actor) def removeChildren(self): """Remove all the children""" for actor in self.getChildren()[:]: self.removeChild(actor) def hasChild(self, actor): """Return True if this actor already has this actor as a child""" return actor in self.children def hasChildren(self): """Return True if this actor has children""" return len(self.children) != 0 def getChildren(self): """Return the list of children""" return self.children def getChildrenWithTag(self, tag): """Return all the children with a certain tag""" return [actor for actor in self.getChildren() if actor.tag == tag] def _childRemoved(self, child, arg): """A child was removed from the world""" if child in self.children: self.children.remove(child) # The active attribute should cascade to our children @property def active(self): return self._active @active.setter def active(self, value): """Set the active""" self._active = value for child in self.getChildren(): child.active = value # The visible attribute should cascade to our children @property def visible(self): return self._visible @visible.setter def visible(self, value): """Set the visible""" self._visible = value for child in self.getChildren(): child.visible = value
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 ### 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 ### 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) 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) 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) def hasActor(self, actor): """Return True if the actor is in this zone""" return actor in self.actors 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())
class World(common.Loggable, serialize.Serializable, common.EventAware): """The main world object The :doc:`engine` will control main worlds. Each world has a number of :doc:`zone` which contain :doc:`actor`. """ my_properties = ( serialize.S('name', '', 'the name of this world'), serialize.L('zones', set(), 'the zones in this world'), serialize.L('unzoned_actors', set(), 'the actors not in any zone in this world'), ) def __init__(self, name): """Initialise the World""" self.addLogger() self.initEvents() self.name = name self.engine = None self.zones = set() self.unzoned_actors = set( ) # Actors get put here if then end up in no zone self.event_handlers = {} self.init() ### Serializing ### def init(self): """Initialise from serialized state""" self.addLogger() self.initEvents() self.log.info('Initializing world %s' % self.name) super(World, self).__init__() self.engine = None # # This list is used to order the processing of actors in rendering. The # flag is used to tell us when we need to resort them self._sorted_actors = [] self._actors_need_resorting = False self._scheduled_deletions = set() # # Now process actors for zone in self.zones: zone.init() for actor in self.unzoned_actors: actor.init() ### Zones ### def addZone(self, zone): """Add a zone to the world""" if zone in self.zones: raise DuplicateZone('The zone %s is already in the world' % zone) else: self.zones.add(zone) self._actors_need_resorting = True def clearZones(self): """Remove all the zones""" self.zones = set() ### Main ### def updateWorld(self, interval): """Update the objects in the world""" for zone in self.zones: if zone.active: zone.updateZone(interval, self) # # Process any scheduled actor deletions while self._scheduled_deletions: try: self.removeActor(self._scheduled_deletions.pop()) except UnknownActor: # Ok, the actor must have been removed directly pass def setEngine(self, engine): """Set the engine that we are owned by""" self.engine = engine def getEngine(self): """Return the engine that we are owned by""" return self.engine def findActorsByTag(self, tag): """Return all the actors in all zones based on the tag""" results = actor.ActorCollection() for z in self.zones: results.extend(z.findActorsByTag(tag)) return results def findActorByName(self, name): """Return the actor with the give name in all zones""" for z in self.zones: try: return z.findActorByName(name) except zone.ActorNotFound: pass else: raise zone.ActorNotFound( 'Unable to find actor named "%s" in any zone' % name) def findActorsAt(self, x, y): """Return the actors at a certain location""" actors = actor.ActorCollection() test = geometry.Point(x, y) for the_actor in self.getActors(): if test.isInside(the_actor): actors.append(the_actor) return actors def getActors(self): """Return all the actors""" actors = actor.ActorCollection(self.unzoned_actors) for z in self.zones: actors.extend(z.getActors()) return actors def rezoneActors(self): """Move actors to the right zone based on their spatial location""" # # Start with a list of actors to find homes for based on any that # were not in any zones at all moved = self.unzoned_actors self.unzoned_actors = set() # # Find all the actors that are no longer in the right zone # and remove them from their current zone for z in self.zones: for actor in z.actors.copy(): if not actor.isOverlapping(z): z.removeActor(actor) moved.add(actor) # # Now find the place for the moved actors for actor in moved: self.addActor(actor) def clearActors(self): """Clear all the actors""" self.clearActorsExceptTags([]) def clearActorsExceptTags(self, tags): """Clear all actors except the ones with a tag in the list of tags""" for actor in self.getActors(): if actor.tag not in tags: try: self.removeActor(actor) except UnknownActor: # Can be called if a composite actor removes their own children pass for actor in list(self.unzoned_actors): if actor.tag not in tags: self.unzoned_actors.remove(actor) def clearActorsWithTags(self, tags): """Clear all actors with a tag in the list of tags""" for actor in self.getActors(): if actor.tag in tags: try: self.removeActor(actor) except UnknownActor: # Can be called if a composite actor removes their own children pass for actor in list(self.unzoned_actors): if actor.tag not in tags: self.unzoned_actors.remove(actor) def addActor(self, actor): """Add an actor to the world""" # self.log.debug('Adding %s to world %s' % (actor.getNiceName(), self.name)) # # Make sure the actor isn't already here if self.hasActor(actor): raise DuplicateActor('The actor %s is already in the world' % actor.getNiceName()) # # Try to put the actor in the right zone for z in self.zones: if z.wouldContain(actor): z.addActor(actor) break else: # The actor is not in any zones, store for later self.unzoned_actors.add(actor) # # Tell the actor about it actor.addedToWorld(self) # self._actors_need_resorting = True # return actor def removeActor(self, actor): """Remove the actor from the world""" self.log.debug('Removing "%s" actor (%s)' % (actor.tag, actor.getNiceName())) # self._actors_need_resorting = True # # Try to remove from zones for z in self.zones: if z.hasActor(actor): z.removeActor(actor) break else: # # We didn't find it in the zone - maybe in the unzoned if actor in self.unzoned_actors: self.unzoned_actors.remove(actor) else: raise UnknownActor('The actor %s was not found in the world' % actor) # # Tell the actor about it actor.removedFromWorld(self) def scheduleActorRemoval(self, actor): """Remove an actor at the end of the next update for the world This method can be used to safely remove an actor from the world during the execution of the world update. It can sometimes be useful to do this when inside logic that is iterating over actors or inside the updateWorld event loop. """ self._scheduled_deletions.add(actor) def hasActor(self, actor): """Return True if this actor is in the world""" # # Try to remove from zones for z in self.zones: if z.hasActor(actor): return True # # We didn't find it in the zone - maybe in the un-zoned return actor in self.unzoned_actors def requestResortActors(self): """Request that actors are resorted the next time we render Call this if you have adjusted the rendering order of actors """ self._actors_need_resorting = True def renderTo(self, renderer, interval): """Render all of our actors in active zones""" # # Watch out in case we need to reorder our actors if self._actors_need_resorting: self.log.debug('Sorting actors now') self._sorted_actors = renderer.orderActors(self.getActors()) self._actors_need_resorting = False # camera = renderer.getCamera() self.processEvent((events.E_BEFORE_RENDER, self)) # # Render all of the actors for actor in self._sorted_actors: if actor.active and actor.visible: profiler.PROFILER.start(actor, 'renderActor') try: actor.renderTo(renderer, interval) except Exception, err: self.log.error('Failed rendering "%s" actor "%s": %s' % (actor.tag, actor, err)) raise profiler.PROFILER.end() # self.processEvent((events.E_AFTER_RENDER, self))
class Engine(common.Loggable, serialize.Serializable): """The main Serge engine The engine essentially manages a set of world and allows a single world, the current world, to be automatically updated on a certain time frequency. """ my_properties = ( serialize.L('_worlds', [], 'the worlds in this engine'), serialize.O('renderer', None, 'the renderer for this engine'), serialize.O('sprites', None, 'the sprite registry'), serialize.S('_current_world_name', '', 'the name of the current world'), ) def __init__(self, width=640, height=480, title='Serge', backcolour=(0, 0, 0), icon=None): """Initialise the engine""" self.addLogger() self.log.info('Starting serge engine (v%s)' % common.version) SetCurrentEngine(self) super(Engine, self).__init__() self.clearWorlds() self.renderer = render.Renderer(width, height, title, backcolour, icon) self.sprites = visual.Register self._stop_requested = False self._current_world_name = '' self._builder = None self._keyboard = input.Keyboard() self._mouse = input.Mouse(self) self._stats = EngineStats() def init(self): """Initialise ourself""" self.addLogger() self.log.info('Initializing serge engine (v%s)' % common.version) SetCurrentEngine(self) # # Prepare all the worlds for world in self._worlds.values(): world.init() self._current_world = None self._snapshots_enabled = True self._snapshot_count = 0 # # Recover the sprite registry from our own self.sprites.init() visual.Register = self.sprites self.setCurrentWorldByName(self._current_world_name) # self.renderer.init() # self._builder = None self._keyboard = input.Keyboard() self._mouse = input.Mouse(self) def addWorld(self, world): """Add a world to the engine""" if world.name in self._worlds: raise DuplicateWorld('A world named "%s" already exists' % world.name) if world in self._worlds.values(): raise DuplicateWorld('This world (named "%s") already exists' % world.name) self._worlds[world.name] = world world.setEngine(self) def removeWorld(self, world): """Remove a world""" self.removeWorldNamed(world.name) def removeWorldNamed(self, name): """Remove a world with a given name""" try: del (self._worlds[name]) except KeyError: raise WorldNotFound( 'No world named "%s" in the worlds collection' % name) def clearWorlds(self): """Clear all the worlds""" self._worlds = {} self._current_world = None def getWorld(self, name): """Return the named world""" try: return self._worlds[name] except KeyError: raise WorldNotFound( 'No world named "%s" in the worlds collection' % name) def getWorlds(self): """Return all the worlds""" return self._worlds.values() def getCurrentWorld(self): """Return the current world""" if self._current_world: return self._current_world else: raise NoCurrentWorld('There is no current world') def setCurrentWorld(self, world): """Set the current world""" self.setCurrentWorldByName(world.name) def setCurrentWorldByName(self, name): """Set the current world to the one with the given name""" new_world = self.getWorld(name) # # Send activation and deactivation callbacks to worlds to allow them to do # any housekeeping if new_world != self._current_world: if self._current_world: self._current_world.deactivateWorld() new_world.activateWorld() # self._current_world = new_world self._current_world_name = name return new_world def updateWorld(self, interval): """Update the current world""" if self._current_world: self._current_world.updateWorld(interval) else: raise NoCurrentWorld( 'Cannot update when there is no current world') def run(self, fps, endat=None): """Run the updates at the specified frames per second until the optional endtime""" clock = pygame.time.Clock() self._stop_requested = False while True: # # Watch for ending conditions if self._stop_requested or (endat and time.time() >= endat): break # # Main render activity try: # # Pause clock.tick(fps) # # Do the update for our actors interval = clock.get_time() if self._current_world: self.updateWorld(interval) # # Do builder work if needed if self._builder: self._builder.updateBuilder(interval) # # Events that may have happened self._handleEvents() self._mouse.update(interval) self._keyboard.update(interval) if self._current_world: self.processEvents() # # Get ready to render self._stats.beforeRender() self.renderer.preRender() # # Render the active world if self._current_world: self._current_world.renderTo(self.renderer, interval) # # Render the builder if needed if self._builder: self._builder.renderTo(self.renderer, interval) # # And render all of our layers self.renderer.render() self._stats.afterRender() # # Show the screen pygame.display.flip() self._stats.recordFrame() # except NotImplementedError, err: self.log.error('Failed in main loop: %s' % err) # self.log.info('Engine stopping') self.log.info('Engine info: %s' % (self._stats, ))
class Zone(common.Loggable, geometry.Rectangle): """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'), ) def __init__(self): """Initialise the zone""" 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() ### Serializing ### 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 ### 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: actor.updateActor(interval, world) # # Do physics if we need to if self._physics_objects: self._updatePhysics(interval) 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) def hasActor(self, actor): """Return True if the actor is in this zone""" return actor in self.actors 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() self.space.remove(p.body, p.shape) def clearActors(self): """Remove all actors""" self.actors = set() ### Finding ### 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) def findActorsByTag(self, tag): """Return all the actors with a certain tag""" return [actor for actor in self.actors if actor.tag == tag] 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) 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.info('No pymunk - physics disabled') self._physics_objects = [] return # # Create a context for the physics self.log.info('Initializing physics engine') self.space = pymunk.Space() 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 self.space.add(p.body, p.shape) self._shape_dict[p.shape] = actor self._physics_objects.append(actor) actor.syncPhysics() def _updatePhysics(self, interval): """Perform a step of the physics engine""" # # 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) 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.shape.body.position, no_sync=True) p.velocity = tuple(p.shape.body.velocity) def setPhysicsStepsize(self, interval): """Set the maximum step size for physics calculations""" self.physics_stepsize = interval def setGlobalForce(self, force): """Set the global force for physics""" self.global_force = force
class PhysicalConditions(serialize.Serializable): """Represents physical parameters of an object This includes the mass, velocity, force applied, acceleration and the physical dimensions. """ my_properties = ( serialize.F('mass', 0.0, 'the mass of the object'), serialize.L('velocity', (0.0,0.0), 'the velocity of the object'), serialize.L('force', (0.0,0.0), 'the force on the object'), serialize.F('radius', 0.0, 'the radius of the object'), serialize.F('width', 0.0, 'the width of the object'), serialize.F('height', 0.0, 'the height of the object'), serialize.F('friction', 0.1, 'the friction the object'), serialize.F('elasticity', 1.0, 'the elasticity of the object'), serialize.I('layers', 0, 'the collision layers that we are in'), serialize.I('group', 0, 'the collision group that we are in'), serialize.B('fixed', False, 'whether the object is fixed in place'), serialize.B('update_angle', False, 'whether the rotation of the body should propagate to the actors visual'), serialize.B('visual_size', False, 'whether to set the size based on the visual element of our parent actor'), ) def __init__(self, mass=0.0, radius=0.0, velocity=(0.0, 0.0), force=(0.0, 0.0), width=0.0, height=0.0, fixed=False, friction=0.1, elasticity=1.0, group=0, layers=-1, update_angle=False, visual_size=False): """Initialise the conditions""" self.body = None if not mass and not fixed: raise InvalidMass('Mass must be specified unless the object is fixed in place') self.mass = mass if not fixed else pymunk.inf self.velocity = velocity self.force = force self.fixed = fixed self.friction = friction self.elasticity = elasticity self.update_angle = update_angle self.visual_size = visual_size self.group = group self.layers = layers self.space = None if not visual_size: self.setGeometry(radius, width, height) def init(self): """Initialize from serialized form""" super(PhysicalConditions, self).init() self.setGeometry(self.radius, self.width, self.height) self._createPhysicsObject() def setGeometry(self, radius=None, width=None, height=None): """Set the geometry You must specify either the radius or the width and height """ # # Reality check if radius and (width or height): raise InvalidDimensions('Must specify radius or width & height, not both') elif not radius and not (width and height): raise InvalidDimensions('Must specify width & height') # if radius: self.geometry_type = 'circle' else: self.geometry_type = 'rectangle' self.radius = radius self.width = width self.height = height self._createPhysicsObject() def _createPhysicsObject(self): """Return a new physics object""" if self.geometry_type == 'circle': inertia = pymunk.moment_for_circle(self.mass, 0, self.radius, (0,0)) else: inertia = pymunk.moment_for_box(self.mass, self.width, self.height) # body = pymunk.Body(self.mass, inertia) body.velocity = self.velocity body.force = self.force # if self.geometry_type == 'circle': shape = pymunk.Circle(body, self.radius, (0,0)) else: #shape = pymunk.Poly(body, [(0, 0), (self.width, 0), # (self.width, self.height), (0, self.height)]) w2, h2 = self.width/2, self.height/2 shape = pymunk.Poly(body, [(-w2,-h2), (+w2, -h2), (+w2, +h2), (-w2, +h2)]) # shape.elasticity = self.elasticity shape.collision_type = 2 shape.group = self.group shape.layers = self.layers shape.friction = self.friction self.shape = shape self.body = body def updateFrom(self, physical_conditions): """Update the properties and our physics object""" self.velocity = physical_conditions.velocity self.force = physical_conditions.force self.body.velocity = self.velocity self.body.force = self.force
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 ### 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 ### 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: actor.updateActor(interval, world) # # Do physics if we need to if self._physics_objects: self.updatePhysics(interval) 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) 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) def hasActor(self, actor): """Return True if the actor is in this zone""" return actor in self.actors 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() self.space.remove(p.body) if p.shape: self.space.remove(p.shape) def clearActors(self): """Remove all actors""" self.actors = set() ### Finding ### 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) def findActorsByTag(self, tag): """Return all the actors with a certain tag""" return [actor for actor in self.actors if actor.tag == tag] 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) 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() 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) def setPhysicsStepsize(self, interval): """Set the maximum step size for physics calculations""" self.physics_stepsize = interval def setGlobalForce(self, force): """Set the global force for physics""" self.global_force = force 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() 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 CompositeActor(Actor): """An actor that can have children, which are also actors World operations on the parent, like adding and removing, will also apply to the children. If the children are removed from the parent then they are also removed from the world. """ my_properties = ( serialize.L('children', [], 'the child actors that we own'), serialize.L('_world', [], 'the world that we belong to'), ) def __init__(self, *args, **kw): """Initialise the actor""" super(CompositeActor, self).__init__(*args, **kw) self.children = [] self._world = None ### World events ### def removedFromWorld(self, world): """Called when we are being removed from the world""" super(CompositeActor, self).removedFromWorld(world) for child in self.getChildren()[:]: world.removeActor(child) self._world = None def addedToWorld(self, world): """Called when we are being added to the world""" super(CompositeActor, self).addedToWorld(world) for child in self.getChildren(): world.addActor(child) self._world = world ### Children ### def addChild(self, actor): """Add a child actor""" self.children.append(actor) actor.linkEvent(events.E_REMOVED_FROM_WORLD, self._childRemoved) def removeChild(self, actor): """Remove a child actor""" try: self.children.remove(actor) except ValueError: raise InvalidActor('The actor %s was not a child of %s' % (actor.getNiceName(), self.getNiceName())) # # Remove the child from the world if self._world: self._world.removeActor(actor) def getChildren(self): """Return the list of children""" return self.children def _childRemoved(self, child, arg): """A child was removed from the world""" if child in self.children: self.children.remove(child)