Java超|スレッドでアニメーションを作る

止まっていた画面に、時間の流れを吹き込む。スレッドを使ったアニメーションを学ぶと、JavaのGUIアプリは「表示する画面」から「動き続ける画面」へ進化します。

これまでのGUIアプリケーションでは、ウィンドウを表示し、部品を配置し、画像を表示し、マウス操作に応じて画面を描き変えるところまで学んできました。

Frame で画面の土台を作る。
Label や Button を置く。
MouseEvent から座標を取得する。
repaint を呼んで画面を描き直す。
paint の中で文字や図形を描く。

ここまで来ると、JavaのGUIアプリはかなり表情豊かになります。

しかし、さらに一歩進むと、ユーザーが操作したときだけではなく、時間の経過に合わせて画面を変化させることができます。

これがアニメーションです。

たとえば、戦士本部の重力修行場に「修行段階」を表示する大きな案内盤があるとします。

最初は、次のように表示されます。

修行段階 0 に到達

1秒後に、次のように変わります。

修行段階 1 に到達

さらに1秒後に、次のように変わります。

修行段階 2 に到達

このように、一定時間ごとに表示内容を更新し、画面を描き直すことで、静止した画面に時間の流れが生まれます。

ドラゴンボール風にたとえると、重力修行場の表示盤が、戦士の気の高まりに合わせて段階をひとつずつ進めていくようなものです。

GokuWarrior が集中を高める。
GohanAnalyzer が修行記録を確認する。
表示盤には「修行段階 0」「修行段階 1」「修行段階 2」と順番に映し出される。

このように、画面が時間に合わせて変化すると、GUIアプリは静かな掲示板ではなく、動きのある修行装置のように見えてきます。

Javaでこのようなアニメーションを作るときに大切になるのが、スレッドです。

スレッドを使うことで、一定間隔で値を更新し、repaint を呼び、少し待って、また値を更新するという流れを作れます。

この記事では、具体的な例示プログラムとして Sample7.java を使い、スレッドでアニメーションを作る基本を、ドラゴンボールの世界観でやわらかく整理していきます。

アニメーションとは何か

アニメーションとは、画面の表示内容を少しずつ変化させ、それを連続して見せることで動いているように見せる仕組みです。

今回のような基本例では、図形を動かすのではなく、表示する数値を一定時間ごとに変化させます。

流れは次のようになります。

手順内容
1表示する値を変える
2repaint で再描画を依頼する
3paint が呼ばれて画面を描き直す
4Thread.sleep で少し待つ
5また値を変える
6この流れを繰り返す

この繰り返しによって、画面の表示が時間とともに変化します。

ドラゴンボール風にたとえると、重力修行場の案内盤に表示される「修行段階」が、一定時間ごとに上がっていくイメージです。

表示のタイミング表示内容
起動直後修行段階 0 に到達
約1秒後修行段階 1 に到達
約2秒後修行段階 2 に到達
約3秒後修行段階 3 に到達

このように、表示される値が少しずつ変わり、それが一定間隔で描き直されることで、画面に時間の流れが生まれます。

なぜスレッドを使うのか

アニメーションでは、画面を1回描いて終わりではありません。

一定の間隔で、何度も画面を描き直す必要があります。

そのためには、次のような処理を繰り返す必要があります。

値を変える
  ↓
画面を描き直す
  ↓
少し待つ
  ↓
また値を変える

この「時間に沿って繰り返す処理」を担当するために、スレッドを使います。

スレッドを使うと、画面更新のための処理を、独立した流れとして動かせます。

ドラゴンボール風にたとえると、修行表示盤には「表示を更新する係」がついているようなものです。

その係は、1回だけ数字を書いて終わるのではありません。

1秒ごとに修行段階を確認し、表示盤を書き換え続けます。

Javaの要素ドラゴンボール風のイメージ
Thread時間に沿って動く更新係
run更新係が実行する作業内容
repaint表示盤に描き直しを依頼する合図
sleep次の更新まで一呼吸置く時間
paint表示盤に実際の文字を描く係

アニメーションは、スレッドだけで完成するわけではありません。

スレッドが時間の流れを作り、値を変え、repaint で描き直しを依頼します。

そして、paint がその時点の値を画面に描きます。

この役割分担が、スレッドを使ったGUIアニメーションの基本です。

図:スレッドでアニメーションを作る全体像

この図が示していること

この図は、スレッドでアニメーションを作る基本構造を表しています。

スレッドは、一定間隔で step の値を更新します。

step の値を変えたあと、repaint を呼んで画面の再描画を依頼します。

その結果、paint が呼ばれ、現在の step の値を使って画面に文字を描きます。

図の要素意味
Thread時間の流れを作る処理
runスレッドが実行する処理
step表示する修行段階を表す値
repaint画面を描き直す依頼
paint実際に画面へ描画する処理
Thread.sleep次の更新まで待つ処理

この図から分かることは、アニメーションは「値の更新」「再描画」「待機」を繰り返すことで成り立つということです。

スレッドを使ったアニメーションの例

ここでは、1秒ごとに修行段階の表示が変わるプログラムを見ていきます。

ファイル名:Sample7.java

import java.awt.*;
import java.awt.event.*;

public class Sample7 extends Frame implements Runnable
{
    int step;

    public static void main(String[] args)
    {
        Sample7 sm = new Sample7();
    }

    public Sample7()
    {
        super("修行カウント表示");

        addWindowListener(new SampleWindowListener());

        // 新しいスレッドを開始する
        Thread th;
        th = new Thread(this);
        th.start();

        setSize(420, 320);
        setVisible(true);
    }

    public void run()
    {
        try {
            for (int i = 0; i < 10; i++) {
                // 表示する段階を更新する
                step = i;

                // 画面を再描画する
                repaint();

                // 1秒待つ
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
        }
    }

    public void paint(Graphics g)
    {
        g.setFont(new Font("SansSerif", Font.BOLD, 26));
        g.drawString("修行段階 " + step + " に到達", 90, 150);
    }

    class SampleWindowListener extends WindowAdapter
    {
        public void windowClosing(WindowEvent e)
        {
            System.exit(0);
        }
    }
}

このプログラムでできること

このプログラムを実行すると、「修行カウント表示」というタイトルのウィンドウが表示されます。

画面の中央付近には、次のような文字が表示されます。

修行段階 0 に到達

約1秒後には、表示が次のように変わります。

修行段階 1 に到達

さらに約1秒後には、次のように変わります。

修行段階 2 に到達

このように、表示が順番に変化していきます。

最終的には、修行段階 9 まで進みます。

このプログラムで行っていることを整理すると、次のようになります。

できること内容
ウィンドウを表示するFrame を継承して画面を作る
スレッドを開始するThread を作り start を呼ぶ
スレッドで処理を動かすrun の中で繰り返し処理を行う
表示する値を変えるstep に 0 から 9 まで代入する
画面を描き直すrepaint を呼ぶ
表示間隔を作るThread.sleep(1000) で1秒待つ
文字を描画するpaint の中で drawString を使う
閉じる操作に対応するWindowAdapter と windowClosing を使う

ドラゴンボール風にたとえると、戦士本部の修行表示盤が、1秒ごとに修行段階を更新していくアプリです。

GokuWarrior が修行を進めるたびに、表示盤の段階が 0 から 1、1 から 2 へと進んでいくように見えます。

Sample7 は Frame であり Runnable でもある

クラス宣言は次のようになっています。

public class Sample7 extends Frame implements Runnable

この1行には、2つの役割が含まれています。

指定役割
extends Frameウィンドウとして動けるようにする
implements Runnableスレッドで実行する処理を持てるようにする

Sample7 は Frame を継承しているため、ウィンドウとして表示できます。

同時に Runnable を実装しているため、run メソッドを持ち、Thread に渡してスレッド処理として動かせます。

ドラゴンボール風にたとえると、Sample7 は「修行表示盤」そのものでありながら、「自分で表示を更新する係」も兼ねています。

Javaの役割ドラゴンボール風のイメージ
Frame修行表示盤の本体
Runnable自動更新の任務を持つ約束
run表示更新係が行う作業
Threadその作業を時間に沿って動かす流れ

このように、Sample7 は画面としての役割と、アニメーション処理を持つ役割の両方を担当しています。

Runnable を実装する意味

Runnable は、スレッドで動かしたい処理を持たせるためのインターフェイスです。

Runnable を実装するクラスでは、run メソッドを書きます。

public void run()
{
    // スレッドで実行したい処理
}

今回の Sample7 では、run の中にアニメーションの中心となる処理を書いています。

public void run()
{
    try {
        for (int i = 0; i < 10; i++) {
            step = i;
            repaint();
            Thread.sleep(1000);
        }
    } catch (InterruptedException e) {
    }
}

つまり、Runnable を実装することで、Sample7 は「スレッドで実行する処理」を持てるようになります。

ただし、Runnable を実装しただけではスレッドは始まりません。

Thread オブジェクトを作り、start を呼ぶ必要があります。

段階内容
implements Runnableスレッドで動かす処理を持つ
run を定義する実際の処理を書く
new Thread(this)このオブジェクトの run を動かす Thread を作る
start を呼ぶ新しいスレッドを開始する

ドラゴンボール風にたとえると、Runnable は「自動更新任務を持つ」という約束です。

しかし、任務内容を持っているだけでは動きません。

Thread という実行係に渡して、start で任務を開始させる必要があります。

Thread を作って start する流れ

コンストラクタの中では、次のようにスレッドを作って開始しています。

Thread th;
th = new Thread(this);
th.start();

この3行を分けて見ると、役割が分かりやすくなります。

コード役割
Thread th;Thread 型の変数を用意する
th = new Thread(this);この Sample7 オブジェクトをもとに Thread を作る
th.start();スレッドを開始する

ここで出てくる this は、現在の Sample7 オブジェクト自身を表します。

Sample7 は Runnable を実装しているため、Thread に渡せます。

th = new Thread(this);

これは、次のような意味です。

この Sample7 オブジェクトの run を、新しいスレッドで動かしてください。

そして、start を呼びます。

th.start();

start が呼ばれると、新しいスレッドが開始され、その中で run が実行されます。

ここで重要なのは、run を直接呼ばないことです。

呼び方動き
run を直接呼ぶ普通のメソッド呼び出しになる
start を呼ぶ新しいスレッドが始まり、その中で run が動く

ドラゴンボール風にたとえると、run は任務内容が書かれた巻物です。

start は、その任務を実際に開始する出撃命令です。

巻物を読むだけでは、時間に沿った自動更新は始まりません。

start によって、修行表示盤の自動更新係が動き始めます。

図:Thread を開始して run が動く流れ

この図が示していること

この図は、Sample7 から Thread を作り、start によって run が動き始める流れを表しています。

Sample7 は Runnable を実装しているため、run メソッドを持っています。

コンストラクタで new Thread(this) と書くことで、Sample7 自身を Thread に渡します。

そして th.start を呼ぶと、新しいスレッドが開始され、run の中の処理が実行されます。

図の要素意味
Sample7ウィンドウ本体であり、Runnable を実装したオブジェクト
this現在の Sample7 オブジェクト自身
new Thread(this)Sample7 の run を動かす Thread を作る
th.start新しいスレッドを開始する
runアニメーションの更新処理を書く場所

この図から分かることは、Runnable を実装しただけでは動かず、Thread に渡して start を呼ぶことで初めてスレッド処理が始まるということです。

run でアニメーションの流れを作る

アニメーションの中心になるのが run メソッドです。

public void run()
{
    try {
        for (int i = 0; i < 10; i++) {
            // 表示する段階を更新する
            step = i;

            // 画面を再描画する
            repaint();

            // 1秒待つ
            Thread.sleep(1000);
        }
    } catch (InterruptedException e) {
    }
}

この中では、次の3つの処理を繰り返しています。

処理内容
step = i表示する段階を更新する
repaint画面を描き直すように依頼する
Thread.sleep(1000)1秒待つ

この3つが、アニメーションの基本のリズムです。

値を変える
  ↓
描き直す
  ↓
少し待つ
  ↓
次の値へ進む

ドラゴンボール風にたとえると、修行表示盤の更新係が次のように動いています。

流れドラゴンボール風の動き
step = i修行段階を記録カードに書く
repaint表示盤に描き直しを命じる
Thread.sleep(1000)次の段階まで一呼吸置く

この流れを繰り返すことで、修行段階が時間とともに進んで見えます。

for 文が修行段階を進める

run の中では、for 文を使っています。

for (int i = 0; i < 10; i++) {

この for 文により、i の値は 0 から 9 まで順番に変わります。

繰り返しi の値step の値
1回目00
2回目11
3回目22
4回目33
.........
10回目99

for 文の中で、i を step に代入しています。

step = i;

これにより、表示に使う step の値が 0、1、2、3 と進んでいきます。

ドラゴンボール風にたとえると、for 文は修行段階の進行表です。

修行段階 0 から始まり、1、2、3 と順番に上がっていきます。

そのたびに表示盤が描き直されるため、画面上でも段階が進んで見えます。

repaint は画面更新の依頼

step の値を変えただけでは、画面の見た目がすぐに変わるとは限りません。

そこで repaint を呼びます。

repaint();

repaint は、画面をもう一度描き直してほしいと依頼するメソッドです。

実際に描画するのは paint です。

メソッド役割
repaint再描画を依頼する
paint実際に画面へ描く

今回の流れでは、step の値を変えたあとに repaint を呼びます。

step = i;
repaint();

これにより、Javaは必要なタイミングで paint を呼び、最新の step の値を画面に描きます。

ドラゴンボール風にたとえると、BulmaSupport が「現在の修行段階は 3 です」と記録カードを更新しただけでは、表示盤の見た目はまだ古いままです。

支援係が「表示盤を描き直してください」と合図を出す必要があります。

その合図が repaint です。

Thread.sleep で表示の間隔を作る

run の中では、repaint のあとに Thread.sleep を呼んでいます。

Thread.sleep(1000);

1000 はミリ秒です。

つまり、Thread.sleep(1000) は、現在のスレッドを約1秒間一時停止するという意味です。

指定待つ時間
5000.5秒
10001秒
20002秒

この待ち時間があることで、表示の変化が見やすくなります。

もし Thread.sleep(1000) がなければ、for 文は一瞬で進み、0 から 9 までの変化がほとんど見えません。

アニメーションとして見せるには、変化の間に時間差が必要です。

ドラゴンボール風にたとえると、修行段階を一瞬で 0 から 9 まで進めてしまうと、戦士は何が起きたのか分かりません。

1段階ごとに一呼吸置くことで、修行の進み方が見えるようになります。

InterruptedException に備える

Thread.sleep を使うときは、InterruptedException に備える必要があります。

そのため、run の中では try-catch を使っています。

try {
    for (int i = 0; i < 10; i++) {
        step = i;
        repaint();
        Thread.sleep(1000);
    }
} catch (InterruptedException e) {
}

InterruptedException は、スレッドが待機中に割り込みを受ける可能性があるために扱う例外です。

学習段階では、まず次のように理解すると分かりやすいです。

要素内容
Thread.sleepスレッドを一定時間待たせる
InterruptedException待っている途中で割り込みが起きる可能性に備える例外
try-catchその可能性に対応するための書き方

ドラゴンボール風にたとえると、更新係が1秒休んでいる間に、急な通信が届く可能性に備えているようなものです。

paint で現在の step を描画する

画面に実際の文字を描いているのは paint メソッドです。

public void paint(Graphics g)
{
    g.setFont(new Font("SansSerif", Font.BOLD, 26));
    g.drawString("修行段階 " + step + " に到達", 90, 150);
}

この中では、まずフォントを設定しています。

g.setFont(new Font("SansSerif", Font.BOLD, 26));

これは、SansSerif の太字、26ポイントの文字で描くという意味です。

次に、drawString で文字列を描いています。

g.drawString("修行段階 " + step + " に到達", 90, 150);

ここで step の値が文字列に組み込まれています。

step の値画面に表示される文字
0修行段階 0 に到達
1修行段階 1 に到達
2修行段階 2 に到達
9修行段階 9 に到達

paint は、呼ばれた時点の step の値を見て描画します。

つまり、run が step を更新し、repaint が paint を呼び、paint が現在の step を画面に表示する、という流れです。

スレッドと描画の役割分担

ここで大切なのは、スレッドが直接画面に文字を描いているわけではないことです。

スレッドの役割は、時間の流れを作り、step を更新し、repaint を呼ぶことです。

実際の描画は paint が担当します。

役割担当
時間の流れを作るThread
表示する値を変えるrun
画面更新を依頼するrepaint
実際に描画するpaint
表示間隔を作るThread.sleep

この分担が分かると、アニメーションの構造が整理しやすくなります。

ドラゴンボール風にたとえると、次のようになります。

Javaの処理ドラゴンボール風の役割
Thread修行の時間管理係
run段階更新の任務内容
step現在の修行段階
repaint表示盤への描き直し命令
paint表示盤に文字を書く係
sleep次の段階までの待機時間

スレッドは、表示を更新するためのきっかけを作ります。

paint は、そのきっかけを受けて、現在の状態を画面に描きます。

図:アニメーションが成立する仕組み

この図が示していること

この図は、アニメーションが成立する理由を表しています。

アニメーションは、1回だけ描画して終わるものではありません。

step を更新し、repaint で再描画を依頼し、paint が現在の step を描き、Thread.sleep で一定時間待つ。

この流れを繰り返すことで、画面の表示が少しずつ変化して見えます。

図の要素意味
stepを更新表示する内容を変える
repaint画面を描き直すように依頼する
paint現在の状態を画面に描く
Thread.sleep次の表示まで間隔を作る
ループこの流れを繰り返す

この図から分かることは、アニメーションは「値の変化」「再描画」「待機時間」の3つが組み合わさって成立するということです。

アニメーションが成立する理由

今回のプログラムでアニメーションらしく見える理由は、次の3つがそろっているからです。

必要な要素内容
表示内容が変わるstep が 0 から 9 まで変化する
毎回描き直すrepaint によって paint が呼ばれる
変化の間隔があるThread.sleep(1000) で約1秒待つ

この3つのうち、どれかが欠けると、アニメーションとして見えにくくなります。

たとえば、step を変えなければ、毎回同じ文字が表示されます。

repaint を呼ばなければ、値が変わっても画面に反映されにくくなります。

Thread.sleep がなければ、変化が一瞬で終わってしまい、ユーザーには流れとして見えにくくなります。

欠けているもの起こりやすいこと
step の更新画面の内容が変わらない
repaint画面が更新されない
Thread.sleep一瞬で終わって変化が見えにくい

ドラゴンボール風にたとえると、修行段階を変える記録、表示盤への描き直し命令、次の段階までの一呼吸。

この3つがそろって、修行の進行が画面に表れます。

もっと発展させると何ができるか

今回のプログラムでは、文字の中の数字を変えるだけのシンプルなアニメーションを作っています。

しかし、この考え方を広げると、さまざまな動きに応用できます。

応用例できること
位置を変える図形や画像を横に動かす
色を変える一定時間ごとに色を切り替える
サイズを変える気のしるしが大きくなる表現を作る
複数の値を使う位置、色、大きさを同時に変える
画像を切り替える戦士アイコンの動きのように見せる
メーターを増やす修行ゲージが上がる表現を作る

たとえば、step の値を文字表示だけでなく、図形の位置に使うこともできます。

g.fillOval(50 + step * 20, 150, 30, 30);

このようにすれば、step が増えるたびに丸が右へ動いていきます。

ドラゴンボール風にたとえると、修行段階が進むたびに、気のしるしが表示盤の上を少しずつ移動していくような表現です。

はじめて学ぶときに押さえたい見方

スレッドと描画が同時に出てくると、最初は少し複雑に見えるかもしれません。

そのときは、次の4つに分けて考えると分かりやすくなります。

見る場所確認すること
時間の流れを作る場所run と Thread.sleep
値を変える場所for 文の中の step = i
画面更新を依頼する場所repaint
実際に描く場所paint と drawString

特に大切なのは、run と paint の役割を混同しないことです。

メソッド役割
run時間に沿って値を変え、repaintを呼ぶ
paint現在の値を使って画面を描く

run はアニメーションの進行役です。

paint は画面表示の担当です。

repaint は、その2つをつなぐ合図です。

ドラゴンボール風にたとえると、run は修行段階を進める記録係、paint は表示盤にその段階を書く描画係、repaint は「表示を更新してください」という通信札です。

この内容でつかんでおきたいこと

今回のテーマでいちばん大切なのは、スレッドを使うことで、一定時間ごとに画面を更新できるということです。

ただし、スレッドだけで画面を描くわけではありません。

スレッドが値を変え、repaint で再描画を依頼し、paint が画面に描きます。

ポイント内容
Runnableスレッドで動かす処理を持たせるための仕組み
Thread新しく動く処理の流れを作る
startスレッドを開始する
runスレッドで実行する処理を書く
step表示する修行段階を保存する
for 文step を 0 から 9 まで進める
repaint画面の再描画を依頼する
paint実際に画面を描く
drawString文字列を描画する
Thread.sleep表示の間隔を作る
InterruptedExceptionsleep 中の割り込みに備える例外

この流れがつかめると、JavaのGUIは「押したら変わる画面」だけではなく、「時間とともに動く画面」として見えてきます。

ドラゴンボール風にたとえると、修行表示盤はもう静かな掲示板ではありません。

時間の流れに合わせて修行段階を進め、1秒ごとに表示を更新し、戦士の成長を映し出す生きた修行装置になります。