View Javadoc
1   package org.newdawn.slick;
2   
3   import java.util.ArrayList;
4   import java.util.List;
5   
6   import org.lwjgl.Sys;
7   import org.newdawn.slick.util.Log;
8   
9   import javax.annotation.Nonnull;
10  import javax.annotation.Nullable;
11  
12  /**
13   * A utility to hold and render animations
14   *
15   * @author kevin
16   * @author DeX (speed updates)
17   */
18  public class Animation implements Renderable {
19      /** The list of frames to render in this animation */
20      private final List<Frame> frames = new ArrayList<>();
21      /** The frame currently being displayed */
22      private int currentFrame = -1;
23      /** The time the next frame change should take place */
24      private long nextChange = 0;
25      /** True if the animation is stopped */
26      private boolean stopped = false;
27      /** The time left til the next frame */
28      private long timeLeft;
29      /** The current speed of the animation */
30      private float speed = 1.0f;
31      /** The frame to stop at */
32      private int stopAt = -2;
33      /** The last time the frame was automagically updated */
34      private long lastUpdate;
35      /** True if this is the first update */
36      private boolean firstUpdate = true;
37      /** True if we should auto update the animation - default true */
38      private boolean autoUpdate = true;
39      /** The direction the animation is running */
40      private int direction = 1;
41      /** True if the animation in ping ponging back and forth */
42      private boolean pingPong;
43      /** True if the animation should loop (default) */
44      private boolean loop = true;
45      /** The spriteSheet backing this animation */
46      @Nullable
47      private SpriteSheet spriteSheet = null;
48      /** If this animation pauses after completing 1 full cycle. A value of 0 indicates no pausing (avoids an extra boolean)*/
49      private long pauseDuration = 0;
50  
51      /**
52       * Create an empty animation
53       */
54      private Animation() {
55          this(true);
56      }
57  
58      /**
59       * Create a new animation from a set of images
60       *
61       * @param frames The images for the animation frames
62       * @param duration The duration to show each frame
63       */
64      public Animation(@Nonnull Image[] frames, int duration) {
65          this(frames, duration, true);
66      }
67  
68      /**
69       * Create a new animation from a set of images
70       *
71       * @param frames The images for the animation frames
72       * @param durations The duration to show each frame
73       */
74      public Animation(@Nonnull Image[] frames, @Nonnull int[] durations) {
75          this(frames, durations, true);
76      }
77  
78      /**
79       * Create an empty animation
80       *
81       * @param autoUpdate True if this animation should automatically update. This means that the
82       * current frame will be caculated based on the time between renders
83       */
84      private Animation(boolean autoUpdate) {
85          currentFrame = 0;
86          this.autoUpdate = autoUpdate;
87      }
88  
89      /**
90       * Create a new animation from a set of images
91       *
92       * @param frames The images for the animation frames
93       * @param duration The duration to show each frame
94       * @param autoUpdate True if this animation should automatically update. This means that the
95       * current frame will be caculated based on the time between renders
96       */
97      private Animation(@Nonnull Image[] frames, int duration, boolean autoUpdate) {
98          for (Image frame : frames) {
99              addFrame(frame, duration);
100         }
101         currentFrame = 0;
102         this.autoUpdate = autoUpdate;
103     }
104 
105     /**
106      * Create a new animation from a set of images
107      *
108      * @param frames The images for the animation frames
109      * @param durations The duration to show each frame
110      * @param autoUpdate True if this animation should automatically update. This means that the
111      * current frame will be caculated based on the time between renders
112      */
113     private Animation(@Nonnull Image[] frames, @Nonnull int[] durations, boolean autoUpdate) {
114         this.autoUpdate = autoUpdate;
115         if (frames.length != durations.length) {
116             throw new RuntimeException("There must be one duration per frame");
117         }
118 
119         for (int i=0;i<frames.length;i++) {
120             addFrame(frames[i], durations[i]);
121         }
122         currentFrame = 0;
123     }
124 
125     /**
126      * Create a new animation based on the sprite from a sheet. It assumed that
127      * the sprites are organised on horizontal scan lines and that every sprite
128      * in the sheet should be used.
129      *
130      * @param frames The sprite sheet containing the frames
131      * @param duration The duration each frame should be displayed for
132      */
133     public Animation(@Nonnull SpriteSheet frames, int duration) {
134         this(frames, 0,0,frames.getHorizontalCount()-1,frames.getVerticalCount()-1,true,duration,true);
135     }
136 
137     /**
138      * Create a new animation based on a selection of sprites from a sheet
139      *
140      * @param frames The sprite sheet containing the frames
141      * @param x1 The x coordinate of the first sprite from the sheet to appear in the animation
142      * @param y1 The y coordinate of the first sprite from the sheet to appear in the animation
143      * @param x2 The x coordinate of the last sprite from the sheet to appear in the animation
144      * @param y2 The y coordinate of the last sprite from the sheet to appear in the animation
145      * @param horizontalScan True if the sprites are arranged in hoizontal scan lines. Otherwise
146      * vertical is assumed
147      * @param duration The duration each frame should be displayed for
148      * @param autoUpdate True if this animation should automatically update based on the render times
149      */
150     private Animation(@Nullable SpriteSheet frames, int x1, int y1, int x2, int y2, boolean horizontalScan, int duration, boolean autoUpdate) {
151         this.spriteSheet = frames;
152         this.autoUpdate = autoUpdate;
153 
154         if (!horizontalScan) {
155             for (int x=x1;x<=x2;x++) {
156                 for (int y=y1;y<=y2;y++) {
157                     //addFrame(frames.getSprite(x, y), duration);
158                     addFrame(duration, x, y);
159                 }
160             }
161         } else {
162             for (int y=y1;y<=y2;y++) {
163                 for (int x=x1;x<=x2;x++) {
164                     //addFrame(frames.getSprite(x, y), duration);
165                     addFrame(duration, x, y);
166                 }
167             }
168         }
169     }
170 
171     /**
172      * Creates a new Animation where each frame is a sub-image of <tt>SpriteSheet</tt> ss.
173      * @param ss The <tt>SpriteSheet</tt> backing this animation
174      * @param frames An array of coordinates of sub-image locations for each frame
175      * @param duration The duration each frame should be displayed for
176      */
177     public Animation(@Nullable SpriteSheet ss, @Nonnull int[] frames, int[] duration){
178         spriteSheet = ss;
179         int x;
180         int y;
181 
182         for(int i = 0; i < frames.length/2; i++){
183            x = frames[i*2];
184            y = frames[i*2 + 1];
185            addFrame(duration[i], x, y);
186         }
187     }
188 
189     /**
190      * Add animation frame to the animation.
191      * @param duration The duration to display the frame for
192      * @param x The x location of the frame on the <tt>SpriteSheet</tt>
193      * @param y The y location of the frame on the <tt>spriteSheet</tt>
194      */
195     void addFrame(int duration, int x, int y){
196        if (duration == 0) {
197           Log.error("Invalid duration: "+duration);
198           throw new RuntimeException("Invalid duration: "+duration);
199        }
200 
201         if (frames.isEmpty()) {
202           nextChange = (int) (duration / speed);
203        }
204 
205        frames.add(new Frame(duration, x, y));
206        currentFrame = 0;
207     }
208 
209     /**
210      * Indicate if this animation should automatically update based on the
211      * time between renders or if it should need updating via the update()
212      * method.
213      *
214      * @param auto True if this animation should automatically update
215      */
216     public void setAutoUpdate(boolean auto) {
217         this.autoUpdate = auto;
218     }
219 
220     /**
221      * Indicate if this animation should ping pong back and forth
222      *
223      * @param pingPong True if the animation should ping pong
224      */
225     public void setPingPong(boolean pingPong) {
226         this.pingPong = pingPong;
227     }
228 
229     /**
230      * Check if this animation has stopped (either explictly or because it's reached its target frame)
231      *
232      * @see #stopAt
233      * @return True if the animation has stopped
234      */
235     public boolean isStopped() {
236         return stopped;
237     }
238 
239     /**
240       * Adjust the overall speed of the animation.
241       *
242       * @param spd The speed to run the animation. Default: 1.0
243       */
244     public void setSpeed(float spd) {
245         if (spd > 0) {
246             // Adjust nextChange
247             nextChange = (long) (nextChange * speed / spd);
248 
249             speed = spd;
250         }
251     }
252 
253     /**
254      * Returns the current speed of the animation.
255      *
256      * @return The speed this animation is being played back at
257      */
258     public float getSpeed() {
259        return speed;
260     }
261 
262 
263     /**
264      * Stop the animation
265      */
266     public void stop() {
267         if (frames.size() == 0) {
268             return;
269         }
270         timeLeft = nextChange;
271         stopped = true;
272     }
273 
274     /**
275      * Start the animation playing again
276      */
277     public void start() {
278         if (!stopped) {
279             return;
280         }
281         if (frames.size() == 0) {
282             return;
283         }
284         stopped = false;
285         nextChange = timeLeft;
286     }
287 
288     /**
289      * Restart the animation from the beginning
290      */
291     public void restart() {
292         if (frames.size() == 0) {
293             return;
294         }
295         stopped = false;
296         currentFrame = 0;
297         nextChange = (int) (frames.get(0).duration / speed);
298         firstUpdate = true;
299         lastUpdate = 0;
300     }
301 
302     /**
303      * Add animation frame to the animation
304      *
305      * @param frame The image to display for the frame
306      * @param duration The duration to display the frame for
307      */
308     void addFrame(Image frame, int duration) {
309         if (duration == 0) {
310             Log.error("Invalid duration: "+duration);
311             throw new RuntimeException("Invalid duration: "+duration);
312         }
313 
314         if (frames.isEmpty()) {
315             nextChange = (int) (duration / speed);
316         }
317 
318         frames.add(new Frame(frame, duration));
319         currentFrame = 0;
320     }
321 
322     /**
323      * Draw the animation to the screen
324      */
325     public void draw() {
326         draw(0,0);
327     }
328 
329     /**
330      * Draw the animation at a specific location
331      *
332      * @param x The x position to draw the animation at
333      * @param y The y position to draw the animation at
334      */
335     public void draw(float x,float y) {
336         draw(x,y,getWidth(),getHeight());
337     }
338 
339     /**
340      * Draw the animation at a specific location
341      *
342      * @param x The x position to draw the animation at
343      * @param y The y position to draw the animation at
344      * @param filter The filter to apply
345      */
346     public void draw(float x,float y, Color filter) {
347         draw(x,y,getWidth(),getHeight(), filter);
348     }
349 
350     /**
351      * Draw the animation
352      *
353      * @param x The x position to draw the animation at
354      * @param y The y position to draw the animation at
355      * @param width The width to draw the animation at
356      * @param height The height to draw the animation at
357      */
358     void draw(float x, float y, float width, float height) {
359         draw(x,y,width,height,Color.white);
360     }
361 
362     /**
363      * Draw the animation
364      *
365      * @param x The x position to draw the animation at
366      * @param y The y position to draw the animation at
367      * @param width The width to draw the animation at
368      * @param height The height to draw the animation at
369      * @param col The colour filter to use
370      */
371     void draw(float x, float y, float width, float height, Color col) {
372         if (frames.size() == 0) {
373             return;
374         }
375 
376         if (autoUpdate) {
377             long now = getTime();
378             long delta = now - lastUpdate;
379             if (firstUpdate) {
380                 delta = 0;
381                 firstUpdate = false;
382             }
383             lastUpdate = now;
384             nextFrame(delta);
385         }
386 
387         Frame frame = frames.get(currentFrame);
388         frame.image.draw(x,y,width,height, col);
389     }
390 
391     /**
392      * Render the appropriate frame when the spriteSheet backing this Animation is in use.
393      * @param x The x position to draw the animation at
394      * @param y The y position to draw the animation at
395      */
396     public void renderInUse(int x, int y){
397        if (frames.size() == 0) {
398           return;
399        }
400 
401        if (autoUpdate) {
402           long now = getTime();
403           long delta = now - lastUpdate;
404           if (firstUpdate) {
405              delta = 0;
406              firstUpdate = false;
407           }
408           lastUpdate = now;
409           nextFrame(delta);
410        }
411 
412        Frame frame = frames.get(currentFrame);
413 
414        spriteSheet.renderInUse(x, y, frame.x, frame.y);
415     }
416 
417     /**
418      * Render the appropriate frame when the spriteSheet backing this Animation is in use.
419      * @param x The x position to draw the animation at
420      * @param y The y position to draw the animation at
421      * @param rot the rotation to do on this animation in degrees.
422      */
423     public void renderInUse(int x, int y, float rot){
424        if (frames.size() == 0) {
425           return;
426        }
427 
428        if (autoUpdate) {
429           long now = getTime();
430           long delta = now - lastUpdate;
431           if (firstUpdate) {
432              delta = 0;
433              firstUpdate = false;
434           }
435           lastUpdate = now;
436           nextFrame(delta);
437        }
438 
439 
440 
441        Frame frame = frames.get(currentFrame);
442        spriteSheet.renderInUse(x, y, rot, frame.x, frame.y);
443     }
444 
445     /**
446      * Get the width of the current frame
447      *
448      * @return The width of the current frame
449      */
450     int getWidth() {
451         return frames.get(currentFrame).image.getWidth();
452     }
453 
454     /**
455      * Get the height of the current frame
456      *
457      * @return The height of the current frame
458      */
459     int getHeight() {
460         return frames.get(currentFrame).image.getHeight();
461     }
462 
463     /**
464      * Draw the animation
465      *
466      * @param x The x position to draw the animation at
467      * @param y The y position to draw the animation at
468      * @param width The width to draw the animation at
469      * @param height The height to draw the animation at
470      */
471     public void drawFlash(float x,float y,float width,float height) {
472         drawFlash(x,y,width,height, Color.white);
473     }
474 
475     /**
476      * Draw the animation
477      *
478      * @param x The x position to draw the animation at
479      * @param y The y position to draw the animation at
480      * @param width The width to draw the animation at
481      * @param height The height to draw the animation at
482      * @param col The colour for the flash
483      */
484     void drawFlash(float x, float y, float width, float height, @Nonnull Color col) {
485         if (frames.size() == 0) {
486             return;
487         }
488 
489         if (autoUpdate) {
490             long now = getTime();
491             long delta = now - lastUpdate;
492             if (firstUpdate) {
493                 delta = 0;
494                 firstUpdate = false;
495             }
496             lastUpdate = now;
497             nextFrame(delta);
498         }
499 
500         Frame frame = frames.get(currentFrame);
501         frame.image.drawFlash(x,y,width,height,col);
502     }
503 
504     /**
505      * Update the animation cycle without draw the image, useful
506      * for keeping two animations in sync
507      *
508      * @deprecated
509      */
510     public void updateNoDraw() {
511         if (autoUpdate) {
512             long now = getTime();
513             long delta = now - lastUpdate;
514             if (firstUpdate) {
515                 delta = 0;
516                 firstUpdate = false;
517             }
518             lastUpdate = now;
519             nextFrame(delta);
520         }
521     }
522 
523     /**
524      * Update the animation, note that this will have odd effects if auto update
525      * is also turned on
526      *
527      * @see #autoUpdate
528      * @param delta The amount of time thats passed since last update
529      */
530     public void update(long delta) {
531         nextFrame(delta);
532     }
533 
534     /**
535      * Get the index of the current frame
536      *
537      * @return The index of the current frame
538      */
539     public int getFrame() {
540         return currentFrame;
541     }
542 
543     /**
544      * Set the current frame to be rendered
545      *
546      * @param index The index of the frame to rendered
547      */
548     public void setCurrentFrame(int index) {
549         currentFrame = index;
550     }
551 
552     /**
553      * Get the image assocaited with a given frame index
554      *
555      * @param index The index of the frame image to retrieve
556      * @return The image of the specified animation frame
557      */
558     public Image getImage(int index) {
559         Frame frame = frames.get(index);
560         return frame.image;
561     }
562 
563     /**
564      * Get the number of frames that are in the animation
565      *
566      * @return The number of frames that are in the animation
567      */
568     public int getFrameCount() {
569         return frames.size();
570     }
571 
572     /**
573      * Get the image associated with the current animation frame
574      *
575      * @return The image associated with the current animation frame
576      */
577     public Image getCurrentFrame() {
578         Frame frame = frames.get(currentFrame);
579         return frame.image;
580     }
581 
582     /**
583      * Check if we need to move to the next frame
584      *
585      * @param delta The amount of time thats passed since last update
586      */
587     private void nextFrame(long delta) {
588         if (stopped) {
589             return;
590         }
591         if (frames.size() == 0) {
592             return;
593         }
594 
595         nextChange -= delta;
596 
597         while (nextChange < 0 && (!stopped)) {
598             if (currentFrame == stopAt) {
599                 stopped = true;
600                 break;
601             }
602             if ((currentFrame == frames.size() - 1) && (!loop) && (!pingPong)) {
603                 stopped = true;
604                 break;
605             }
606             currentFrame = (currentFrame + direction) % frames.size();
607 
608             if (pingPong) {
609                 if (currentFrame <= 0) {
610                     currentFrame = 0;
611                     direction = 1;
612                     if (!loop) {
613                         stopped = true;            
614                         break;     
615                     }       
616                 }
617                 else if (currentFrame >= frames.size()-1) {
618                     currentFrame = frames.size()-1;
619                     direction = -1;
620                 }
621             }
622             int realDuration = (int) (frames.get(currentFrame).duration/ speed);
623             nextChange = nextChange + realDuration;
624             //System.out.println("Before next: " + nextChange);
625             //Pause after the loop is completed - display the first frame for the pauseDuration time.
626             if(pauseDuration >= 0 && currentFrame == 0)
627                 nextChange += pauseDuration;
628 
629             //System.out.println("after next: " + nextChange);
630         }
631     }
632 
633     /**
634      * Indicate if this animation should loop or stop at the last frame
635      *
636      * @param loop True if this animation should loop (true = default)
637      */
638     public void setLooping(boolean loop) {
639         this.loop = loop;
640     }
641 
642     /**
643      * Get the accurate system time
644      *
645      * @return The system time in milliseconds
646      */
647     private long getTime() {
648         return (Sys.getTime() * 1000) / Sys.getTimerResolution();
649     }
650 
651     /**
652      * Indicate the animation should stop when it reaches the specified
653      * frame index (note, not frame number but index in the animation
654      *
655      * @param frameIndex The index of the frame to stop at
656      */
657     public void stopAt(int frameIndex) {
658         stopAt = frameIndex;
659     }
660 
661     /**
662      * Get the duration of a particular frame
663      *
664      * @param index The index of the given frame
665      * @return The duration in (ms) of the given frame
666      */
667     int getDuration(int index) {
668         return frames.get(index).duration;
669     }
670 
671     /**
672      * Set the duration of the given frame
673      *
674      * @param index The index of the given frame
675      * @param duration The duration in (ms) for the given frame
676      */
677     public void setDuration(int index, int duration) {
678         frames.get(index).duration = duration;
679     }
680 
681     /**
682      * Get the durations of all the frames in this animation
683      *
684      * @return The durations of all the frames in this animation
685      */
686     @Nonnull
687     public int[] getDurations() {
688         int[] durations = new int[frames.size()];
689         for (int i=0;i<frames.size();i++) {
690             durations[i] = getDuration(i);
691         }
692 
693         return durations;
694     }
695 
696     /**
697      * Returns if this animation is pausing or not.
698      * @return True if this animation will pause after it completes 1 full cycle of animation. False if it does not.
699      */
700     public boolean isPause ()
701     {
702         return pauseDuration > 0;
703     }
704 
705 
706     /**
707      * Gets the pauseDuration (MS) of this animation.
708      * @return Returns the pauseDuration.
709      */
710     public long getPauseDuration ()
711     {
712         return pauseDuration;
713     }
714 
715     /**
716      * Sets the pauseDuration of this animation in MS.
717      * @param pauseDuration The pauseDuration to set (in MS).
718      */
719     public void setPauseDuration ( long pauseDuration )
720     {
721         this.pauseDuration = pauseDuration;
722     }
723 
724 
725     /**
726      * @see java.lang.Object#toString()
727      */
728     @Nonnull
729     public String toString() {
730         String res = "[Animation ("+frames.size()+") ";
731         for (Frame frame : frames) {
732             res += frame.duration + ",";
733         }
734 
735         res += "]";
736         return res;
737     }
738 
739     /**
740      * Create a copy of this animation. Note that the frames
741      * are not duplicated but shared with the original
742      *
743      * @return A copy of this animation
744      */
745     @Nonnull
746     public Animation copy() {
747         Animation copy = new Animation();
748 
749         copy.spriteSheet = spriteSheet;
750         copy.frames.addAll(frames);
751         copy.autoUpdate = autoUpdate;
752         copy.direction = direction;
753         copy.loop = loop;
754         copy.pingPong = pingPong;
755         copy.speed = speed;
756 
757         return copy;
758     }
759 
760     /**
761      * A single frame within the animation
762      *
763      * @author kevin
764      */
765     private class Frame {
766         /** The image to display for this frame */
767         public final Image image;
768         /** The duration to display the image fro */
769         public int duration;
770         /** The x location of this frame on a SpriteSheet*/
771         public int x = -1;
772         /** The y location of this frame on a SpriteSheet*/
773         public int y = -1;
774 
775         /**
776          * Create a new animation frame
777          *
778          * @param image The image to display for the frame
779          * @param duration The duration in millisecond to display the image for
780          */
781         public Frame(Image image, int duration) {
782             this.image = image;
783             this.duration = duration;
784         }
785 
786         /**
787          * Creates a new animation frame with the frames image location on a sprite sheet
788          * @param duration The duration in millisecond to display the image for
789          * @param x the x location of the frame on the <tt>SpriteSheet</tt>
790          * @param y the y location of the frame on the <tt>SpriteSheet</tt>
791          */
792         public Frame(int duration, int x, int y) {
793             this.image = spriteSheet.getSubImage(x, y);
794             this.duration = duration;
795             this.x = x;
796             this.y = y;
797         }
798     }
799 }