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 * <scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0" initialstate="normal">
67 * <state id="normal">
68 * <transition event="walk" target="walking"/>
69 * </state>
70 * <state id="walking">
71 * <transition event="stop" target="normal"/>
72 * </state>
73 * </scxml>
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 "initial" 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>>= 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 }