View Javadoc

1   /*
2   
3       dsh-piccolo-state-machine-sprite  Piccolo2D state machine sprite and supporting classes.
4       Copyright (c) 2007-2013 held jointly by the individual authors.
5   
6       This library is free software; you can redistribute it and/or modify it
7       under the terms of the GNU Lesser General Public License as published
8       by the Free Software Foundation; either version 3 of the License, or (at
9       your option) any later version.
10  
11      This library is distributed in the hope that it will be useful, but WITHOUT
12      ANY WARRANTY; with out even the implied warranty of MERCHANTABILITY or
13      FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
14      License for more details.
15  
16      You should have received a copy of the GNU Lesser General Public License
17      along with this library;  if not, write to the Free Software Foundation,
18      Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA.
19  
20      > http://www.fsf.org/licensing/licenses/lgpl.html
21      > http://www.opensource.org/licenses/lgpl-license.php
22  
23  */
24  package org.dishevelled.piccolo.sprite.statemachine;
25  
26  import java.awt.image.BufferedImage;
27  
28  import java.awt.Image;
29  import java.awt.Graphics2D;
30  
31  import java.io.IOException;
32  
33  import java.util.HashMap;
34  import java.util.Iterator;
35  import java.util.Map;
36  
37  import javax.imageio.ImageIO;
38  
39  import org.piccolo2d.PNode;
40  
41  import org.piccolo2d.util.PBounds;
42  import org.piccolo2d.util.PPaintContext;
43  
44  import org.apache.commons.scxml.env.AbstractSCXMLListener;
45  import org.apache.commons.scxml.env.SimpleErrorHandler;
46  
47  import org.apache.commons.scxml.io.SCXMLParser;
48  
49  import org.apache.commons.scxml.model.ModelException;
50  import org.apache.commons.scxml.model.SCXML;
51  import org.apache.commons.scxml.model.State;
52  import org.apache.commons.scxml.model.TransitionTarget;
53  
54  import org.dishevelled.piccolo.sprite.Animation;
55  
56  import org.xml.sax.SAXException;
57  
58  /**
59   * Abstract Piccolo2D state machine sprite node.
60   *
61   * <p>
62   * This abstract sprite node utilizes a state machine to manage all its state transitions.  Consider the
63   * following simple state machine in <a href="http://www.w3.org/TR/scxml/">State Chart XML (SCXML)</a>
64   * format:
65   * <pre>
66   * &lt;scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initialstate="normal"&gt;
67   *   &lt;state id="normal"&gt;
68   *     &lt;transition event="walk" target="walking"/&gt;
69   *   &lt;/state&gt;
70   *   &lt;state id="walking"&gt;
71   *     &lt;transition event="stop" target="normal"/&gt;
72   *   &lt;/state&gt;
73   * &lt;/scxml&gt;
74   * </pre>
75   * </p>
76   * <p>
77   * Subclasses may provide state transition methods that fire an event
78   * to the underlying state machine.
79   * <pre>
80   * public void walk() {
81   *   fireStateMachineEvent("walk");
82   * }
83   * public void stop() {
84   *   fireStateMachineEvent("stop");
85   * }
86   * </pre>
87   * </p>
88   * <p>
89   * Subclasses may associate visual properties and behavior with states
90   * by providing private no-arg state methods which will be called via reflection
91   * on entry by the state machine engine.
92   * <pre>
93   * private void normal() {
94   *   walkingActivity.stop();
95   * }
96   * private void walking() {
97   *   walkingActivity.start();
98   * }
99   * </pre>
100  * <p>
101  * Animations can be associated with states by implementing the
102  * protected <code>createAnimation</code> method.  Create and
103  * return an animation for the specified state id, or return
104  * <code>null</code> if no such animation exists.
105  * <pre>
106  *   protected Animation createAnimation(final String id) {
107  *     Image image = loadImage(getClass(), id + ".png");
108  *     return Animations.createAnimation(image);
109  *   }
110  * </pre>
111  * </p>
112  * <p>
113  * Altogether, the typical implementation pattern for a subclass of this
114  * abstract sprite node looks like
115  * <pre>
116  * class MySprite extends AbstractStateMachineSprite {
117  *   // walking activity
118  *   private final WalkingActivity walkingActivity = ...;
119  *   // load the state machine backing all instances of this MySprite
120  *   private static final SCXML STATE_MACHINE = loadStateMachine(MySprite.class, "stateMachine.xml");
121  *
122  *   MySprite() {
123  *     super();
124  *     // initialize the state machine
125  *     initializeStateMachine(STATE_MACHINE);
126  *     // sprites have no bounds by default
127  *     setWidth(14.0d);
128  *     setHeight(24.0d);
129  *   }
130  *
131  *   protected Animation createAnimation(final String id) {
132  *     // load a single PNG image for each state id
133  *     Image image = loadImage(getClass(), id + ".png");
134  *     return Animations.createAnimation(image);
135  *   }
136  *
137  *   // methods to fire state transition events
138  *   public void walk() {
139  *     fireStateMachineEvent("walk");
140  *   }
141  *   public void stop() {
142  *     fireStateMachineEvent("stop");
143  *   }
144  *
145  *   // methods that receive notification of state transitions
146  *   private void normal() {
147  *     walkingActivity.stop();
148  *   }
149  *   private void walking() {
150  *     walkingActivity.start();
151  *   }
152  * }
153  * </pre>
154  * </p>
155  *
156  * @author  Michael Heuer
157  * @version $Revision$ $Date$
158  */
159 public abstract class AbstractStateMachineSprite
160     extends PNode
161 {
162     /** State machine support. */
163     private StateMachineSupport stateMachineSupport;
164 
165     /** Number of frames skipped. */
166     private int skipped;
167 
168     /** Number of frames to skip, default <code>0</code>. */
169     private int frameSkip;
170 
171     /** Current animation. */
172     private Animation currentAnimation;
173 
174     /** Map of animations keyed by state id. */
175     private final Map<String, Animation> animations;
176 
177 
178     /**
179      * Create a new abstract state machine sprite node.
180      */
181     protected AbstractStateMachineSprite()
182     {
183         animations = new HashMap<String, Animation>();
184     }
185 
186 
187     /**
188      * Create and return an animation for the specified state id, if any.
189      *
190      * @param id state id
191      * @return an animation for the specified state id, or <code>null</code> if
192      *    no such animation exists
193      */
194     protected abstract Animation createAnimation(final String id);
195 
196     /**
197      * Initialize the specified state machine.  Animations are loaded for all
198      * the state ids and the current animation is set to the initial target, if any.
199      *
200      * <p>
201      * <b>Note:</b> this method should be called from the constructor
202      * of a subclass after its state machine has been instantiated.
203      * </p>
204      *
205      * @param stateMachine state machine to initialize, must not be null
206      */
207     protected final void initializeStateMachine(final SCXML stateMachine)
208     {
209         if (stateMachine == null)
210         {
211             throw new IllegalArgumentException("stateMachine must not be null");
212         }
213         // load animations for state ids
214         for (Iterator<?> entries = stateMachine.getTargets().entrySet().iterator(); entries.hasNext(); )
215         {
216             Map.Entry<?, ?> entry = (Map.Entry<?, ?>) entries.next();
217             String id = (String) entry.getKey();
218             Object target = entry.getValue();
219             if (target instanceof State)
220             {
221                 Animation animation = createAnimation(id);
222                 if (animation != null)
223                 {
224                     animations.put(id, animation);
225                 }
226             }
227         }
228         // set the current animation to the initial target, if any
229         String initialTargetId = (stateMachine.getInitialTarget() == null) ? null : stateMachine.getInitialTarget().getId();
230         if (animations.containsKey(initialTargetId))
231         {
232             currentAnimation = animations.get(initialTargetId);
233         }
234         // create a state machine support class that delegates to this
235         stateMachineSupport = new StateMachineSupport(this, stateMachine);
236         // update current animation on entry to a new state
237         stateMachineSupport.getExecutor().addListener(stateMachine, new AbstractSCXMLListener()
238             {
239                 @Override
240                 public void onEntry(final TransitionTarget state)
241                 {
242                     Animation animation = animations.get(state.getId());
243                     if (animation != null)
244                     {
245                         currentAnimation = animation;
246                     }
247                 }
248             });
249     }
250 
251     /**
252      * Reset the state machine to its &quot;initial&quot; configuration.
253      */
254     protected final void resetStateMachine()
255     {
256         if (stateMachineSupport != null)
257         {
258             stateMachineSupport.resetStateMachine();
259         }
260     }
261 
262     /**
263      * Fire a state machine event with the specified event name.
264      *
265      * @param eventName event name, must not be null
266      */
267     protected final void fireStateMachineEvent(final String eventName)
268     {
269         if (stateMachineSupport != null)
270         {
271             stateMachineSupport.fireStateMachineEvent(eventName);
272         }
273     }
274 
275     /**
276      * Return the number of frames to skip.  Defaults to <code>0</code>.
277      *
278      * @return the number of frames to skip
279      */
280     protected final int getFrameSkip()
281     {
282         return frameSkip;
283     }
284 
285     /**
286      * Set the number of frames to skip to <code>frameSkip</code>.
287      *
288      * @param frameSkip number of frames to skip, must be <code>&gt;= 0</code>
289      */
290     protected final void setFrameSkip(final int frameSkip)
291     {
292         if (frameSkip < 0)
293         {
294             throw new IllegalArgumentException("frameSkip must be at least zero");
295         }
296         this.frameSkip = frameSkip;
297     }
298 
299     /**
300      * Return the current animation for this state machine sprite.
301      *
302      * @return the current animation for this state machine sprite
303      */
304     protected final Animation getCurrentAnimation()
305     {
306         return currentAnimation;
307     }
308 
309     //protected final State currentState() {} ?
310 
311     /**
312      * Advance this state machine sprite node one frame.
313      */
314     public final void advance()
315     {
316         if (skipped < frameSkip)
317         {
318             skipped++;
319         }
320         else
321         {
322             // advance the current animation
323             if (currentAnimation.advance())
324             {
325                 // and schedule a repaint
326                 repaint();
327             }
328             skipped = 0;
329         }
330     }
331 
332     @Override
333     public final void paint(final PPaintContext paintContext)
334     {
335         if (currentAnimation != null)
336         {
337             Graphics2D g = paintContext.getGraphics();
338             Image currentFrame = currentAnimation.getCurrentFrame();
339             PBounds bounds = getBoundsReference();
340 
341             double w = currentFrame.getWidth(null);
342             double h = currentFrame.getHeight(null);
343 
344             g.translate(bounds.getX(), bounds.getY());
345             g.scale(bounds.getWidth() / w, bounds.getHeight() / h);
346             g.drawImage(currentFrame, 0, 0, null);
347             g.scale(w / bounds.getWidth(), h / bounds.getHeight());
348             g.translate(-1 * bounds.getX(), -1 * bounds.getY());
349         }
350     }
351 
352     /**
353      * Load the state machine resource with the specified name, if any.  Any exceptions thrown
354      * will be ignored.
355      *
356      * @param cls class
357      * @param name name
358      * @return the state machine resource with the specified name, or <code>null</code>
359      *    if no such resource exists
360      */
361     protected static final <T> SCXML loadStateMachine(final Class<T> cls, final String name)
362     {
363         SCXML stateMachine = null;
364         try
365         {
366             stateMachine = SCXMLParser.parse(cls.getResource(name), new SimpleErrorHandler());
367         }
368         catch (IOException e)
369         {
370             // ignore
371         }
372         catch (SAXException e)
373         {
374             // ignore
375         }
376         catch (ModelException e)
377         {
378             // ignore
379         }
380         return stateMachine;
381     }
382 
383     /**
384      * Load the image resource with the specified name, if any.  Any exceptions thrown will be
385      * ignored.
386      *
387      * @param cls class
388      * @param name name
389      * @return the image resource with the specified name, or <code>null</code> if no such
390      *    resource exists
391      */
392     protected static final <T> BufferedImage loadImage(final Class<T> cls, final String name)
393     {
394         BufferedImage image = null;
395         try
396         {
397             image = ImageIO.read(cls.getResource(name));
398         }
399         catch (IOException e)
400         {
401             // ignore
402         }
403         return image;
404     }
405 }