Foundation for all actions
Actions purpose is to modify along the time some trait of an object. The object that the action will modify is the action’s target. Usually the target will be an instance of some CocosNode subclass.
the target will move smoothly over the segment (target.position, action position parameter), reaching the final position after duration seconds have elapsed.
Cocos also provide some powerful operators to combine or modify actions, the more important s being:
sequence operator: action_1 + action_2 -> action_result
where action_result performs by first doing all that action_1 would do and then perform all that action_2 would do
move_2 = MoveTo((100, 100), 10) + MoveTo((200, 200), 15)
When activated, move_2 will move the target first to (100, 100), it will arrive there 10 seconds after departure; then it will move to (200, 200), and will arrive there 10 seconds after having arrived to (100, 100)
spawn operator: action_1 | action_2 -> action_result
where action_result performs by doing what would do action_1 in parallel with what would perform action_2
move_rotate = MoveTo((100,100), 10) | RotateBy(360, 5)
When activated, move_rotate will move the target from the position at the time of activation to (100, 100); also in the first 5 seconds target will be rotated 360 degrees
loop operator: action_1 * n -> action_result
Where n non negative integer, and action result would repeat n times in a row the same that action_1 would perform.
rotate_3 = RotateBy(360, 5) * 3
When activated, rotate_3 will rotate target 3 times, spending 5 sec in each turn.
Action instance roles¶
Action subclass: a detailed cualitative description for a change
An Action instance can play one of the following roles
The instance knows all the details to perform, except a target has not been set. In this role only __init__ and init should be called. It has no access to the concrete action target. The most usual way to obtain an action in the template mode is by calling the constructor of some Action subclass.
position = (100, 100); duration = 10 move = MoveTo(position, duration) move is playing here the template role.
Carry on with the changes desired when the action is initiated. You obtain an action in the worker role by calling the method do in a cocosnode instance, like:
worker_action = cocosnode.do(template_action, target=...)
The most usual is to call without the target kw-param, thus by default setting target to the same cocosnode that performs the do. The worker action begins to perform at the do call, and will carry on with the desired modifications to target in subsequent frames. If you want the capabilty to stop the changes midway, then you must retain the worker_action returned by the do and then, when you want stop the changes, call:
cocosnode.remove_action(worker_action) ( the cocosnode must be the same as in the do call )
Also, if your code need access to the action that performs the changes, have in mind that you want the worker_action (but this is discouraged,
position = (100, 100); duration = 10 move = MoveTo(position, duration) blue_bird = Bird_CocosNode_subclass(...) blue_move = blue_bird.do(move)
Here move plays the template role and blue_move plays the worker role. The target for blue_move has been set for the do method. When the do call omits the target parameter it defaults to the cocosnode where the do is called, so in the example the target for blue_move is blue_bird. In subsequents frames after this call, the blue_bird will move to the position (100, 100), arriving there 10 seconds after the do was executed.
From the point of view of a worker role action, the actions life can be mimicked by:
worker_action = deepcopy(template_action) worker_action.target = some_obj worker_action.start() for dt in frame.next(): worker_action.step(dt) if premature_termination() or worker_action.done(): break worker_action.stop()
Such an instance is created and stored into an Action class instance that implements an Action operator (a composite action). Carries on with the changes desired on behalf of the composite action. When the composite action is not instance of IntervalAction, the perceived life can be mimicked as in the worker role. When the composite action is instance of IntervalAction, special rules apply. For examples look at code used in the implementation of any operator, like Sequence_Action or Sequence_IntervalAction.
Restrictions and Capabilities for the current design and implementation¶
Multiple worker actions can be obtained from a single template action, and they wont interfere between them when applied to different targets.
position = (100, 0); duration = 10 move = MoveBy(position, duration) blue_bird = Sprite("blue_bird.png") blue_bird.position = (0, 100) blue_move = blue_bird.do(move) red_bird = Sprite("red_bird.png") red_bird.position = (0, 200) red_move = blue_bird.do(move) Here we placed two birds at the left screen border, separated vertically by 100. move is the template_action: full details on changes, still no target blue_move, red_move are worker_action 's: obtained by a node.do, have all the details plus the target; they will perform the changes along the time. What we see is both birds moving smooth to right by 100, taking 10 seconds to arrive at final position. Note that even if both worker actions derive for the same template, they don't interfere one with the other.
A worker action instance should not be used as a template¶
You will not get tracebacks, but the second worker action most surelly will have a corrupt workspace, that will produce unexpected behavior.
Posible fights between worker actions over a target member¶
If two actions that are active at the same time try to change the same target’s member(s), the resulting change is computationally well defined, but can be somewhat unexpected by the programmer.
guy = Sprite("grossini.png") guy.position = (100, 100) worker1 = guy.do(MoveTo((400, 100), 3)) worker2 = guy.do(MoveBy((0, 300), 3)) layer.add(guy)
Here the worker1 action will try to move to (400, 100), while the worker2 action will try to move 300 in the up direction. Both are changing guy.position in each frame.
What we see on screen, in the current cocos implementation, is the guy moving up, like if only worker2 were active. And, by physics, the programmer expectation probably guessed more like a combination of both movements.
Note that the unexpected comes from two actions trying to control the same target member. If the actions were changing diferent members, like position and rotation, then no unexpected can happen.
The fighting can result in a behavior that is a combination of both workers, not one a ‘winning’ one. It entirely depends on the implementation from each action. It is possible to write actions than in a fight will show additive behavoir, by example:
import cocos.euclid as eu class MoveByAdditive(ac.Action): def init( self, delta_pos, duration ): try: self.delta_pos = eu.Vector2(*delta_pos)/float(duration) except ZeroDivisionError: duration = 0.0 self.delta_pos = eu.Vector2(*delta_pos) self.duration = duration def start(self): if self.duration==0.0: self.target.position += self.delta_pos self._done = True def step(self, dt): old_elapsed = self._elapsed self._elapsed += dt if self._elapsed > self.duration: dt = self.duration - old_elapsed self._done = True self.target.position += dt*self.delta_pos guy = Sprite("grossini.png") guy.position = (100, 100) worker1 = guy.do(MoveByAdditive((300, 0), 3)) worker2 = guy.do(MoveByAdditive((0, 300), 3)) layer.add(guy)
Here the guy will mode in diagonal, ending 300 right and 300 up, the two actions have combined.
Action’s instances in the template role must be (really) deepcopyiable¶
Beginers note: if you pass in init only floats, ints, strings, dicts or tuples of the former you can skip this section and revisit later.
If the action template is not deepcopyiable, you will get a deepcopy exception, complaining it can’t copy something
If you cheat deepcopy by overriding __deepcopy__ in your class like:
def __deepcopy__(self): return self
you will not get a traceback, but the Worker Independence will broke, the Loop and Repeat operators will broke, and maybe some more.
The section name states a precise requeriment, but it is a bit concise. Let see some common situations where you can be in trouble and how to manage them.
- you try to pass a CocosNode instance in init, and init stores that in an action member
- you try to pass a callback f = some_cocosnode.a_method, with the idea that it shoud be called when some condition is meet, and init stores it in an action member
- You want the action access some big decision table, static in the sense it will not change over program execution. Even if is deepcopyable, there’s no need to deepcopy.
store the data that you do not want to deepcopy in some member in the cocosnode
use an init2 fuction to pass the params you want to not deepcopy:worker = node.do(template) worker.init2(another_cocosnode)
(see test_action_non_interval.py for an example)
Future: Next cocos version probably will provide an easier mechanism to designate some parameters as references.
Overview main subclasses¶
All action classes in cocos must be subclass of one off the following:
- IntervalAction (is itself subclass of Action)
- InstantAction (is itself subclass of IntervalAction)
The task that must perform happens in only one call, the start method. The duration member has the value zero. Examples:
Place(position) : does target.position <- position CallFunc(f, *args, **kwargs) : performs the call f(*args,**kwargs)
The task that must perform is spanned over a number of frames. The total time needed to complete the task is stored in the member duration. The action will cease to perform when the time elapsed from the start call reachs duration. A proper IntervalAction must adhere to extra rules, look in the details section Examples:
MoveTo(position, duration) RotateBy(angle, duration)
The most general posible action class. The task that must perform is spanned over a number of frames. The time that the action would perfom is undefined, and member duration has value None. Examples:
- selects a random point in the screen
- moves to it with the required fastness
This action will last forever.
- at each frame, move the target toward the chasee with the specified fastness.
- Declare the action as done when the distance from target to chasee is less than 10.
If fastness is greather than the chasee fastness this action will certainly terminate, but we dont know how much time when the action starts.
The most general action
False while the step method must be called.
Gets called by __init__ with all the parameteres received, At this time the target for the action is unknown. Typical use is store parameters needed by the action.
External code sets self.target and then calls this method. Perform here any extra initialization needed.
Gets called every frame. dt is the number of seconds that elapsed since the last call.
When the action must cease to perform this function is called by external code; after this call no other method should be called.
CocosNode object that is the target of the action
Interval Actions are the ones that have fixed duration, known when the worker instance is created, and, conceptually, the expected duration must be positive. Degeneratated cases, when a particular instance gets a zero duration, are allowed for convenience.
IntervalAction adds the method update to the public interfase, and it expreses the changes to target as a function of the time elapsed, relative to the duration, ie f(time_elapsed/duration). Also, it is guaranted that in normal termination .update(1.0) is called. Degenerate cases, when a particular instance gets a zero duration, also are guaranted to call .update(1.0) Note that when a premature termination happens stop will be called but update(1.0) is not called.
Examples: MoveTo , MoveBy , RotateBy are strictly Interval Actions, while Place, Show and CallFunc aren’t.
While RotateBy(angle, duration) will usually receive a positive duration, it will accept duration = 0, to ease on cases like action = RotateBy( angle, a-b )
When in the worker role, this method is reliable. When in the component role, if the composite spares the call to step this method cannot be relied (an then the composite must decide by itself when the action is done). Example of later situation is Sequence_IntervalAction.
Dont customize this method: it will not be called when in the component role for certain composite actions (like Sequence_IntervalAction). In such situation the composite will calculate the suitable t and directly call .update(t) You customize the action stepping by overriding .update
Gets called on every frame ‘t’ is the time elapsed normalized to [0, 1] If this action takes 5 seconds to execute, t will be equal to 0 at 0 seconds. t will be 0.5 at 2.5 seconds and t will be 1 at 5sec. This method must not use self._elapsed, which is not guaranted to be updated.
Instant actions are actions that promises to do nothing when the methods step, update, and stop are called. Any changes that the action must perform on his target will be done in the .start() method The interface must be keept compatible with IntervalAction to allow the basic operators to combine an InstantAction with an IntervalAction and give an IntervalAction as a result.
Here we must do out stuff
does nothing - dont override
does nothing - dont override
does nothing - dont override
- Repeats an action forever.
- Applied to InstantAction s means once per frame.
action = JumpBy( (200,0), 50,3,5) repeat = Repeat( action ) sprite.do( repeat )
Note: To repeat just a finite amount of time, just do action * times.
- action : Action instance
The action that will be repeated
Returns an action that runs first action_1 and then action_2 The returned action will be instance of the most narrow class posible in InstantAction, IntervalAction, Action
Returns an action that runs action_1 and action_2 in paralel. The returned action will be instance of the most narrow class posible in InstantAction, IntervalAction, Action
Reverses the behavior of the action
# rotates the sprite 180 degrees in 2 seconds counter clockwise action = Reverse( RotateBy( 180, 2 ) ) sprite.do( action )