class OneShot(Effect): """ This class implements an Effect that has a simple one-play cycle. With this class, you call go( source, target ). It will clean itself up and when completely finished, and its self.going flag will be reset. maxDuration is an important parameter, it makes sure that OneShots do not build up in the world. In some cases, Effect files cannot work out their duration (e.g. if a continuously time-triggered particle effect is referenced in the effect.) In these cases, maxDuration is used to cap the time the effect is in use. If this behaviour is not desired, you should use FX.Persistent instead, and manage the lifetime of the effect yourself. This type of Effect does not keep a permanent assocation with any source. """ def __init__(self, fileName=None, maxDuration=10.0, prereqs=None): Effect.__init__(self, fileName) Effect._create(self, prereqs) self.timer = EventTimer() self.timers = {} for actorName in self.actors.keys(): self.timers[actorName] = EventTimer() self.maxDuration = maxDuration if self.maxDuration < 0.0: WARNING_MSG('maxDuration was negative! setting to 10', self.maxDuration) self.maxDuration = 10.0 def _playEvents(self, eventTiming, source, target=None, callbackFn=None, **kargs): """ This method plays all the events that match the TRANSFORM_DEPENDENT_EVENT timing flag. This first pass through ( eventTiming = 0 ) happens during the attach frame. this is so particles can be cleared as they are attached (so they don't draw in their last position for one frame.) The second pass through ( eventTiming = 1 ) happens after the transforms have been set, usually the next frame. This is so that particles etc. can be spawned at the correct location. The third pass through ( eventTiming =2 ) happens after the total duration of the effect is known. This duration is put into the kargs. This is for events that either begin some stage through the effect, or need to end at the correct time (for example turning off time triggered particles for a nice fade-out, or setting up a colour animation that is timed exactly.) """ for actorName, event in self.events: if event.eventTiming() == eventTiming: try: actor = self.actors[actorName] except: actor = None duration = min(event.go(self, actor, source, target, **kargs), self.maxDuration) if actorName != '': if not self.lengths.has_key(actorName): self.lengths[actorName] = duration else: self.lengths[actorName] = max(self.lengths[actorName], duration) self.totalDuration = max(duration, self.totalDuration) return self.totalDuration def go(self, source, target=None, callbackFn=None, **kargs): """ This method should be the only method called on this Effect. Once fully finished, the self.going flag will be reset, and you can call go() again. If you pass in a callback function, it will be called upon full completion of the effect. """ if self.timer.going(): return self.timer.reserve() self.lengths = {} self.totalDuration = 0.0 for actorName, attacher in self.joints.items(): attacher.attach(self.actors[actorName], source, target) self._playEvents(IMMEDIATE_EVENT, source, target, callbackFn, **kargs) BigWorld.callback( 0.001, lambda: self._go2(source, target, callbackFn, **kargs)) def _go2(self, source, target, callbackFn, **kargs): """ This method is internal - once attached and have waited a frame, spawn all transform dependent events. """ self._playEvents(TRANSFORM_DEPENDENT_EVENT, source, target, callbackFn, **kargs) for actorName, duration in self.lengths.items(): if duration <= 0.0: duration = self.maxDuration self.totalDuration = self.maxDuration self.timers[actorName].begin( duration, partial(self.detach, actorName, source, target)) if self.totalDuration > self.maxDuration or self.totalDuration <= 0.0: WARNING_MSG( 'using maxDuration for this effect. perhaps this was unexpected?', self, source, target) self.totalDuration = self.maxDuration kargs['totalDuration'] = self.totalDuration kargs['actorDurations'] = self.lengths self._playEvents(DURATION_DEPENDENT_EVENT, source, target, callbackFn, **kargs) self.timer.begin(self.totalDuration, lambda: self.stop(source, target, callbackFn)) self.timer.release() def detach(self, actorName, source, target): """ This method should be considered as private for the OneShot effect. It is called automatically in response to the effect ending. """ self.joints[actorName].detach(self.actors[actorName], source, target) def stop(self, source, target, callbackFn): if callbackFn: callbackFn() def _extendTime(self, event, duration): for actorName, ev in self.events: if event == ev: self.timers[actorName].extend(duration) self.timer.extend(duration)
class Persistent(Effect): """ This class implements an Effect that persist on an object. With this class, you must call attach(), then go(),stop(),go(),stop() as many times as you like, then detach(). Once attached, there is a persistent association with the source. Thus when calling go(), you need not pass in the source again. You can pass in a callback fn to go(), which is called back when the effect has properly finished one pass through for all its events. """ def __init__(self, fileName=None, prereqs=None): Effect.__init__(self, fileName) Effect._create(self, prereqs) self.timer = EventTimer() self.source = None self.target = None return def attach(self, source): """ This method attaches the effect to the source. The source may be a model or an entity, depending on the effect. It is usually safe to pass in an Entity, since the Joints associated with the effect can find the appropriate model and/or node from an entity, however Joints cannot derive the entity given a model or a node. """ self.source = source for actor, attacher in self.joints.items(): attacher.attach(self.actors[actor], source) def go(self, target=None, callbackFn=None, **kargs): """ This method starts the effect playing, once it is attached. You may call go() and stop() any number of times in between attach() and detach() pairs. """ self.totalDuration = 0.0 self.target = target for actorName, event in self.events: if event.eventTiming() == 0: try: actor = self.actors[actorName] except: actor = None self.totalDuration = max( self.totalDuration, event.go(self, actor, self.source, target, **kargs)) BigWorld.callback(0.001, lambda: self._go2(target, callbackFn, **kargs)) return def _go2(self, target=None, callbackFn=None, **kargs): for actorName, event in self.events: if event.eventTiming() > 0: try: actor = self.actors[actorName] except: actor = None self.totalDuration = max( self.totalDuration, event.go(self, actor, self.source, target, **kargs)) if callbackFn: self.timer.begin(self.totalDuration + 0.001, callbackFn) return def stop(self): """ This method stops the effect playing. Note that it will do this gracefully, i.e. it allows the effect to die out rather than pop out. For this reason, a stopTime is returned and the effect should not be started again, nor should it be detached, until this time has been has been allowed to pass. """ stopTime = 0.0 for actorName, event in self.events: try: actor = self.actors[actorName] except KeyError: actor = None eventStopTime = event.stop(actor, self.source, self.target) stopTime = max(stopTime, eventStopTime) return stopTime def detach(self): """ This method detaches the effect from the source, removing it and all its actors from the world. The effect remembers where it was attached to begin with, so no arguments need be passed in. """ for actor, attacher in self.joints.items(): attacher.detach(self.actors[actor], self.source) self.source = None self.target = None return
class OneShot(Effect): """ This class implements an Effect that has a simple one-play cycle. With this class, you call go( source, target ). It will clean itself up and when completely finished, and its self.going flag will be reset. maxDuration is an important parameter, it makes sure that OneShots do not build up in the world. In some cases, Effect files cannot work out their duration (e.g. if a continuously time-triggered particle effect is referenced in the effect.) In these cases, maxDuration is used to cap the time the effect is in use. If this behaviour is not desired, you should use FX.Persistent instead, and manage the lifetime of the effect yourself. This type of Effect does not keep a permanent assocation with any source. """ def __init__(self, fileName = None, maxDuration = 10.0, prereqs = None): Effect.__init__(self, fileName) Effect._create(self, prereqs) self.timer = EventTimer() self.timers = {} for actorName in self.actors.keys(): self.timers[actorName] = EventTimer() self.maxDuration = maxDuration if self.maxDuration < 0.0: WARNING_MSG('maxDuration was negative! setting to 10', self.maxDuration) self.maxDuration = 10.0 def _playEvents(self, eventTiming, source, target = None, callbackFn = None, **kargs): """ This method plays all the events that match the TRANSFORM_DEPENDENT_EVENT timing flag. This first pass through ( eventTiming = 0 ) happens during the attach frame. this is so particles can be cleared as they are attached (so they don't draw in their last position for one frame.) The second pass through ( eventTiming = 1 ) happens after the transforms have been set, usually the next frame. This is so that particles etc. can be spawned at the correct location. The third pass through ( eventTiming =2 ) happens after the total duration of the effect is known. This duration is put into the kargs. This is for events that either begin some stage through the effect, or need to end at the correct time (for example turning off time triggered particles for a nice fade-out, or setting up a colour animation that is timed exactly.) """ for actorName, event in self.events: if event.eventTiming() == eventTiming: try: actor = self.actors[actorName] except: actor = None duration = min(event.go(self, actor, source, target, **kargs), self.maxDuration) if actorName != '': if not self.lengths.has_key(actorName): self.lengths[actorName] = duration else: self.lengths[actorName] = max(self.lengths[actorName], duration) self.totalDuration = max(duration, self.totalDuration) return self.totalDuration def go(self, source, target = None, callbackFn = None, **kargs): """ This method should be the only method called on this Effect. Once fully finished, the self.going flag will be reset, and you can call go() again. If you pass in a callback function, it will be called upon full completion of the effect. """ if self.timer.going(): return self.timer.reserve() self.lengths = {} self.totalDuration = 0.0 for actorName, attacher in self.joints.items(): attacher.attach(self.actors[actorName], source, target) self._playEvents(IMMEDIATE_EVENT, source, target, callbackFn, **kargs) BigWorld.callback(0.001, lambda : self._go2(source, target, callbackFn, **kargs)) def _go2(self, source, target, callbackFn, **kargs): """ This method is internal - once attached and have waited a frame, spawn all transform dependent events. """ self._playEvents(TRANSFORM_DEPENDENT_EVENT, source, target, callbackFn, **kargs) for actorName, duration in self.lengths.items(): if duration <= 0.0: duration = self.maxDuration self.totalDuration = self.maxDuration self.timers[actorName].begin(duration, partial(self.detach, actorName, source, target)) if self.totalDuration > self.maxDuration or self.totalDuration <= 0.0: WARNING_MSG('using maxDuration for this effect. perhaps this was unexpected?', self, source, target) self.totalDuration = self.maxDuration kargs['totalDuration'] = self.totalDuration kargs['actorDurations'] = self.lengths self._playEvents(DURATION_DEPENDENT_EVENT, source, target, callbackFn, **kargs) self.timer.begin(self.totalDuration, lambda : self.stop(source, target, callbackFn)) self.timer.release() def detach(self, actorName, source, target): """ This method should be considered as private for the OneShot effect. It is called automatically in response to the effect ending. """ self.joints[actorName].detach(self.actors[actorName], source, target) def stop(self, source, target, callbackFn): if callbackFn: callbackFn() def _extendTime(self, event, duration): for actorName, ev in self.events: if event == ev: self.timers[actorName].extend(duration) self.timer.extend(duration)
class Persistent(Effect): """ This class implements an Effect that persist on an object. With this class, you must call attach(), then go(),stop(),go(),stop() as many times as you like, then detach(). Once attached, there is a persistent association with the source. Thus when calling go(), you need not pass in the source again. You can pass in a callback fn to go(), which is called back when the effect has properly finished one pass through for all its events. """ def __init__(self, fileName = None, prereqs = None): Effect.__init__(self, fileName) Effect._create(self, prereqs) self.timer = EventTimer() self.source = None self.target = None return def attach(self, source): """ This method attaches the effect to the source. The source may be a model or an entity, depending on the effect. It is usually safe to pass in an Entity, since the Joints associated with the effect can find the appropriate model and/or node from an entity, however Joints cannot derive the entity given a model or a node. """ self.source = source for actor, attacher in self.joints.items(): attacher.attach(self.actors[actor], source) def go(self, target = None, callbackFn = None, **kargs): """ This method starts the effect playing, once it is attached. You may call go() and stop() any number of times in between attach() and detach() pairs. """ self.totalDuration = 0.0 self.target = target for actorName, event in self.events: if event.eventTiming() == 0: try: actor = self.actors[actorName] except: actor = None self.totalDuration = max(self.totalDuration, event.go(self, actor, self.source, target, **kargs)) BigWorld.callback(0.001, lambda : self._go2(target, callbackFn, **kargs)) return def _go2(self, target = None, callbackFn = None, **kargs): for actorName, event in self.events: if event.eventTiming() > 0: try: actor = self.actors[actorName] except: actor = None self.totalDuration = max(self.totalDuration, event.go(self, actor, self.source, target, **kargs)) if callbackFn: self.timer.begin(self.totalDuration + 0.001, callbackFn) return def stop(self): """ This method stops the effect playing. Note that it will do this gracefully, i.e. it allows the effect to die out rather than pop out. For this reason, a stopTime is returned and the effect should not be started again, nor should it be detached, until this time has been has been allowed to pass. """ stopTime = 0.0 for actorName, event in self.events: try: actor = self.actors[actorName] except KeyError: actor = None eventStopTime = event.stop(actor, self.source, self.target) stopTime = max(stopTime, eventStopTime) return stopTime def detach(self): """ This method detaches the effect from the source, removing it and all its actors from the world. The effect remembers where it was attached to begin with, so no arguments need be passed in. """ for actor, attacher in self.joints.items(): attacher.detach(self.actors[actor], self.source) self.source = None self.target = None return