概要理解のために、最初に作った内容を以下に示します。
(細部で最新版と異なる部分があります。)

アニメーションライブラのリソース概要 

参考に下記ソースのクラス図を示します。

キャラクタの基底クラスになるクラスが「Sprite」の名前で用意されています。
つまりゲームに出現する画像がある場合、それらは「Sprite」クラスを継承して作る前提になっています。
ゲームの描画は、一定時間ごとで移動などの動作をして描画するための繰り返しがあります。
それを行うクラスが「SpriteThread」です。これに、「Sprite」の継承のインスタンスに追加することで、 指定時間ごとに「Sprite」のactionメソッドが実行呼び出されて、描画が行われます。

Spriteクラスソースコード

「Sprite」のコードは次のようになっており、 xとyが画像の左上の座標になっています。 描画イメージは、imageで管理します。

package sprite;//パッケージ名
import java.awt.*;//Graphics2Dなど用
import java.awt.image.*;//BufferedImage,ImageObserverなど用
import java.util.*;//ArrayListなど用
import java.awt.geom.*;//AffineTransform;

// アニメーション素材のメモリイメージを持ち、それに使う素材を複数管理するための抽象クラス
abstract public class Sprite
{
	protected Sprite parent;//親のSprite

	public double x = 0;	//位置(左上座標)
	public double y = 0;

	//描画で使われるオフスクリーンイメージ(コンストラクタで設定)
	protected Image image;

	//draw用の座標変換パラメタ用
	protected AffineTransform trans = AffineTransform.getTranslateInstance(0, 0);

	//この上記イメージに使われるSprite群
	protected ArrayList<Sprite> subSprite = new ArrayList<Sprite>();

	public void setSize(int width, int height){//サイズ変更
		this.image = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
	}
	
	public Dimension getSize(ImageObserver observer){//描画先を引数にしてサイズ取得する
		int width = this.image.getWidth(observer);
		int height = this.image.getHeight(observer);
		return new Dimension(width,height);
	}
	
	protected Sprite(int width, int height){//コンストラクタ
		setSize(width, height);
	}
	
	protected Sprite(Image img){//コンストラクタ
		this.image = img;
	}
	
	public void add(Sprite sprite){//描画素材の追加
		subSprite.add(sprite);
		sprite.parent = this;//親を指定
	}
	
	//この描画素材のイメージを返す
	abstract public Image getImage() throws Exception;

	//登録のアクションが終了したらtrue 
	abstract protected void action();
}

SpriteThreadクラスソースコード

やswingでは、paintComponentの内で描かないと、描画したものが残りません。 (そのためのプログラムが複雑であった。)
それで、Spriteの継承で画像や位置を記憶して、それをSpriteThreadに追加するだけで、 ライブラリ内で、自動的に適切なタイミングに描画してくれるようにしています。)
(自分で位置を指定するが描画命令を書かなくてよい。)

package sprite;
import java.awt.*;//Image,Graphics2D用
import javax.swing.*;//JComponent、SwingUtilities用
import java.awt.image.*;// BufferedImage、 ImageObserver用

//描画対象のパネル、描画とアクションのスレッドを持つクラス
public class SpriteThread extends SpriteBasic implements Runnable
{
	JComponent targetComponent;//描画対象
	
	Thread thread;//描画、アクション用スレッド

	private boolean loopFlag;//アスレッドを制御するフラグ
	private int animationInterval;//actionを実行させる間隔(ミリ秒)

	//描画サイズと、描画対象を指定するコンストラクタ
	public SpriteThread(int width, int height, JComponent component){
		super(width, height);
		targetComponent = component;
		targetComponent.setDoubleBuffered(true);//ダブルバッファリング使用
		targetComponent.setPreferredSize(new Dimension(width, height));
	}

	//描画とアクションのスレッドをスタート、描画とアクションの実行間隔(ms)を引数で指定
	public void start(int msec){
		this.animationInterval = msec;//ミリ秒
		this.loopFlag = true;
		this.thread = new Thread(this);
		this.thread.start();//スレットスタート
	}

	public void stop()
	{//描画とアクションのスレッドを停止させる
		this.loopFlag = false;
	}

	public void paintTo(Graphics g)	{//描画対象に描画するメソッド(paintComponent内で利用するメソッド)
		try	{
			Graphics2D g2 = (Graphics2D)g;
			Image img = this.getImage();//描画すべきイメージを取得
			g2.drawImage(img, 0, 0, targetComponent);//描画
		}
		catch (Exception e){
			e.printStackTrace();
			System.exit(1);
		}
	}

	public void run(){//描画とアクションのスレッド
		while(loopFlag){
			try{		
				this.action();//アクション

				//描画用スレッドの実行
				SwingUtilities.invokeLater(new SpriteDrawThread(targetComponent));
				
				//指定間隔までスリープさせる
				long nextTimer = System.currentTimeMillis() + this.animationInterval;
				while (loopFlag && nextTimer > System.currentTimeMillis()){
					Thread.sleep(1);//0.001秒間、他のスレッドを実行
				}
			}		
			catch(Exception e){
				e.printStackTrace();
				loopFlag = false;
			}
		}
	}
}

//Swing (シングルスレッドなので)専用の描画スレット実装クラス
class SpriteDrawThread implements Runnable 
{
	private JComponent target;// 描画対象

	SpriteDrawThread(JComponent target){//描画対象をコンストラクタで指定
		this.target = target;
	}
	
	public void run(){//描画スレッド
		target.repaint();
	}
}

Swingオブジェクトの起動スレッドと異なるスレッドで、repaintなど描画処理 を行う場合、SwingUtilities.invokeLaterで指定する スレッドに任せなければならない規則があります。 (Swingオブジェクトは単一スレッドで管理されなければならず、Swing起動スレッドと 同じスレッドと同期を取るためです。)

SpriteBasicクラスソースコード

package sprite;
import java.awt.*;//Image,Graphics2D用
import java.awt.geom.*;//Point2DやAffineTransfor用;
import java.awt.image.*;//ImageObserver用

// 基本操作を実装したSprite 
public class SpriteBasic extends Sprite 
{
	public SpriteBasic(int width, int height){//サイズ指定コンストラクタ
		super(width,height);
	}

	public SpriteBasic(Image img){//イメージ指定コンストラクタ
		super(img);
	}

	//Spriteのメソッド実装:各素材のアクションをそれぞれ実行 
	protected void action(){
		for (int i = 0; i < subSprite.size(); i++){
			Sprite sprite = subSprite.get(i);
			sprite.action();
		}
	}

	//Spriteのメソッド実装:イメージを取得(subSpriteがある場合は再帰的に行う)
	public Image getImage() throws Exception{
		if (this.image instanceof BufferedImage == false){
			return this.image;//編集してはいけないイメージ
		}
		Graphics2D g2 = (Graphics2D)this.image.getGraphics();
		for (int i = 0; i < subSprite.size(); i++){
			Sprite sprite = subSprite.get(i);//各素材取得
			Image img = sprite.getImage();
			sprite.setDrawParameter();//描画に使うパラメタとして設定
			g2.drawImage(img, sprite.trans, null);//各素材をメモリイメージへ描画
		}
		return this.image;//描画し終わったメモリイメージを戻す
	}

	//内部情報を、直接描画で使う各種パラメタに変換(原点への描画が、this.x, this.yに描画となるための処理)
	public void setDrawParameter(){
		this.trans = AffineTransform.getTranslateInstance(this.x, this.y);//移動パラメタ生成
	}
}

SpriteBasicでは、getImageのイメージの取得で、 自身を描画するためのイメージを描画して作成します。
それは、ArrayListに記憶される各素材(Sprite)を drawImageで行いますが、その移動描画で、 AffineTransformのインスタンス変数 transを利用します。そのtransを設定するのがsetDrawParameterメソッドです。