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 }