Multithreading and animation

So here’s the situation. I currently have a character who performs a variety of different animations. To handle these I had the main class (which has it’s paintComponent method called) implement Runnable, put all the animations into the run() method and created a new Thread just for those. Now, I’m finally moving on to other aspects, mainly object movement. Now the problem I have is that because the main thread that draws all the graphics is also being used for character animation, any sleep calls will affect anything being drawn.

What I mean is that, for instance, I have a drawBullet() method which increments the bullets positions and draws them. I call this is the paintComponent method and notice that if the character is performing a certain animation with a sleep in it, the duration of the sleep affects when everything is drawn. Some animations which have short sleep times will cause the bullets to move faster as the drawBullet method is called more frequently while those with long sleeps will cause it to move slower. Obviously this is a problem as I want the bullets to move at a constant speed independent of whatever the character is doing. I’ve tried handling this is different ways

I tried creating a new class that implemented Runnable (and inherited some J component) but ran into trouble when I had to draw the bullets as the Graphics passed to each class was different. I tried remedying the problem by only having the new class change the bullets position and having a method in the original class do the drawing but ran into the same initial problem.

So, is there anyway for me to make a seperate Thread which somehow is able to use the same Graphic object as the original and paint on the same JPanel everything else is? I can’t think of anyway of having the bullets draw method unaffected by the sleep calls besides it being in another Thread with a different Runnable target

Hi, you have three problems here:

  1. you rendered in the animation thread, which you correctly solved by seperating drawing and update logic

  2. thread usage for such lightweight task as an animation is not good, threads are not that cheap (think about many animated objects at once), better to use some state machine (eg. have state for each animated object and update it atomically, updating the state so next run will continue correctly), or even better use Matthias Mann’s Continuations library (but read about the concept first and fully understand it as it’s somewhat advanced).

  3. sleep is very unaccurate for small values, use tick based timing for logic, eg. define one tick as 1/50 of second or so, and do something like this:


long time = System.currentTimeMillis(); // or nanoTime or whatever
while (lastTime + TICK_TIME < time) {
    update();
    lastTime += TICK_TIME;
}

remember to initialize lastTime to current time before start
(maybe there is a better method for tick based timing, feel free to correct me :slight_smile:

you need to use some interpolation when drawing, or have some commands like moveTo, etc, which will be handled for each visible frame and not tick, so you get smooth animation

Hey there. Try out my AnimationSet class.


package mm;

import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Iterator;

/**
 * An AnimationSet is a single collection of Animations that one Entity might be
 * using. It also handles all logic for when one Animation should end or when
 * one should replace another. In terms of structure, an AnimationSet is simply
 * a HashMap that contains all passed Animations. There will always be a default
 * Animation, however, so that it can appear if no other Animations have been
 * specified to play.
 * 
 * @author Eli Delventhal
 */
public class AnimationSet
{
	/**
	 * The map that contains all the animations, accessed by a String name.
	 */
	private HashMap<String,Animation> animations;
	/**
	 * The name (key) of the current animation.
	 */
	private String currentAnimation;
	/**
	 * The name (key) of the default animation.
	 */
	private String defaultName;
	
	/**
	 * Creates an AnimationSet with only a default Animation.
	 * Subsequent Animations must be added using the addAnimation() method.
	 * @param defaultNom	The name (key) of the default Animation.
	 * @param defaultAnimation	The default Animation. Note that its priority should be very low.
	 */
	public AnimationSet(String defaultNom, Animation defaultAnimation)
	{
		animations = new HashMap<String,Animation>();
		defaultName = defaultNom;
		currentAnimation = defaultName;
		animations.put(defaultName, defaultAnimation);
	}
	
	/**
	 * Adds an Animation to the map.
	 * @param name		The name (key) of the Animation.
	 * @param animation	The Animation to add.
	 */
	public void addAnimation(String name, Animation animation)
	{
		animations.put(name,animation);
	}
	
	/**
	 * Changes current the Animation to the specified one. This will
	 * only be done, however, if this is allowed priority-wise.
	 * @param name	The name (key) of this Animation.
	 */
	public void setAnimation(String name)
	{
		//If the passed Animation doesn't exist, return with an error message.
		Animation newAnimation = animations.get(name);
		if (newAnimation == null)
		{
			System.err.println("Animation \"" + name + "\" not found!");
			return;
		}
		
		//If the current Animation doesn't exist or is done, don't bother to
		//check priority - instead start up the new animation immediately.
		Animation current = animations.get(currentAnimation);
		if (current == null || current.isDone())
		{
			startAnimation(name);
			return;
		}
		
		//If the new Animation has a higher priority than the current one, then
		//it's allowed to interrupt it and start.
		if (newAnimation.getPriority() > current.getPriority())
		{
			startAnimation(name);
			return;
		}
	}
	
	/**
	 * Starts a specified Animation by setting it as the current one and by
	 * resetting it back to the start. Warning: this doesn't check to see if
	 * the map contains the passed name; NullPointerException can result.
	 * @param name	The name of the Animation to start.
	 */
	private void startAnimation(String name)
	{
		currentAnimation = name;
		animations.get(name).restart();
	}
	
	/**
	 * Tells this AnimationSet to "tick." By doing so, it will advance the current
	 * Animation, and if it's done, it will set the default Animation immediately.
	 */
	public void tick()
	{
		Animation current = animations.get(currentAnimation);
		current.advance();
		
		if (current.isDone())
			startAnimation(defaultName);
	}
	
	/**
	 * Preloads all the Animations in the map.
	 */
	public void preloadAll()
	{
		for (Iterator<Animation> i = animations.values().iterator(); i.hasNext(); i.next().preloadAll());
	}
	
	/**
	 * Returns the current image in the current Animation.
	 */
	public BufferedImage getCurrentImage()
	{
		return animations.get(currentAnimation).getCurrentImage();
	}
	
	/**
	 * Finishes the current animation by turning on the default, regardless of priority.
	 */
	public void finishAnimation()
	{
		currentAnimation = defaultName;
		animations.get(currentAnimation).restart();
	}
	
	/**
	 * An Animation holds information for drawing all the different progressions
	 * of images - the image prefixes, the number of images, and the delays.
	 * In addition, each Animation has a priority; higher priority animations will
	 * interrupt lower priority animations.
	 * Currently, Animation only supports PNG (because that's all I need), but it
	 * would be trivial to allow other file types.
	 * 
	 * @author Eli Delventhal
	 */
	public static class Animation
	{
		private String prefix;
		private String soundPrefix;
		private int nFrames;
		private int delay;
		private int endDelay;
		private int priority;
		
		private int currentFrame;
		private int lastAdvance;
		
		/**
		 * Constructs an Animation.
		 * @param name	The name of the folder the images are contained in (with /), or the prefix of the image.
		 * @param sound	The name of the folder the sounds are contained in (with /), or the prefix of the sound.
		 * @param num	The number of images in this animation.
		 * @param pause	The pause between the changing of each image (in turns).
		 * @param endPause	The pause after the animation finishes (in turns).
		 * @param prioritee	The priority this animation has.
		 */
		public Animation(String name, String sound, int num, int pause, int endPause, int prioritee)
		{
			prefix = name;
			soundPrefix = sound;
			nFrames = num;
			delay = pause;
			endDelay = endPause;
			priority = prioritee;
			currentFrame = 0;
		}
		
		public BufferedImage getCurrentImage()
		{
			return ImageManager.getImage(prefix + currentFrame + ".png");
		}
		
		/**
		 * Advances the animation. The frame will only increment if it has been
		 * a long enough period of time since the last increment. 
		 */
		public void advance()
		{
			int now = Globals.getCurrentTurn();
			
			//Only advance if there has been a long enough pause and
			//we're not at the last frame yet.
			if (now - lastAdvance >= delay && currentFrame < nFrames-1)
			{
				currentFrame++;
				lastAdvance = now;
				if (soundPrefix.length() > 0)
					SoundManager.playSound(soundPrefix + currentFrame + ".wav");
			}
		}
		
		/**
		 * Returns whether or not this Animation has finished, length and end delay
		 * both considered.
		 * @return	True / false if this Animation is finished.
		 */
		public boolean isDone()
		{
			int now = Globals.getCurrentTurn();
			
			//If we're at the last frame and the pause has been long enough,
			//return true to show that the animation is finished.
			if (now - lastAdvance >= Math.max(delay,endDelay) && currentFrame >= nFrames-1)
				return true;
			return false;
		}
		
		/**
		 * Gives the priority of the Animation
		 * @return	The priority.
		 */
		public int getPriority()
		{
			return priority;
		}
		
		/**
		 * Restarts the Animation.
		 */
		public void restart()
		{
			currentFrame = 0;
			lastAdvance = Globals.getCurrentTurn();
			if (soundPrefix.length() > 0)
				SoundManager.playSound(soundPrefix + currentFrame + ".wav");
		}
		
		/**
		 * Preloads all the frames in this Animation.
		 */
		public void preloadAll()
		{
			for (int i = 0; i < nFrames; i++)
			{
				ImageManager.preloadImage(prefix + i + ".png");
				if (soundPrefix.length() > 0)
					SoundManager.preloadSound(soundPrefix + i + ".wav");
			}
		}
	}
}

You give each object its own AnimationSet, and create it upon construction. Then you simply call tick() every time you draw. The beauty of using this is that no animations can supersede the others, and you can give them separate priorities.

Oh yeah, I should also note that this enables you to put in images and sounds, and obviously I have ImageManager and SoundManager classes. The AnimationSet allows you to preload all of its images and sounds before use.

The way the images and sounds work is you specify a prefix (like “Images/Dog/walk”) and then every subsequent image has a number value representing the current frame, and is a PNG (although this can be easily changed). So, you would have: Images/Dog/walk0.png Images/Dog/walk1.png Images/Dog/walk2.png Images/Dog/walk3.png etc.

Thanks for the responses guys, I’ve started incorporating your ideas into my code. It actually makes sense having a constant frame rate (the normal main Thread response time between painting) while making each animation simply hold off it’s next picture for it’s own specified time. Also, thanks for the code Demonpants but I think i’ll write my own animation class. For what I need of it, it’ll be different from yours but use the same basic idea.

Cool, well I’m glad I got your mind in the correct direction. It’s much better for you to write your own code, anyway.