アニメーション

アニメーションの基礎

アニメーションは基本的にパラパラ漫画と同じ原理で行います. EZ Graphics では,EZ.addCircleEZ.addLineなどで 追加して返される実体の位置を変更し,EZ.refreshScreen()を呼び出すと画面を更新します.

また,Javaで一定時間スリープするのは,Thread.sleepメソッドを利用します. sleepメソッドに スリープする時間(ミリ秒)を渡します. 次の例では,100ミリ秒(0.1秒)スリープしています.

次の例で確認してみましょう.

import java.awt.Color;
public class RoundTrip{
  void run(){
    EZ.initialize(400, 400);
    EZCircle circle = EZ.addCircle(
        100, 100, 5, 5, Color.BLUE, true);
    this.roundTrip(circle);
  }
  void roundTrip(EZCircle circle){
    Integer deltaX = 10;
    while(true){ // 無限ループ
      Integer newX = circle.getXCenter() + deltaX;
      circle.translateTo(newX, circle.getYCenter());
      if(newX >= 400 || newX <= 0){
        deltaX = deltaX * -1;
      }
      EZ.refreshScreen();
      Thread.sleep(100);
    }
  }
}

これでコンパイルすると,次のようなコンパイルエラーが発生します.

prog/spring06/RoundTrip.java:17: エラー: 例外InterruptedExceptionは報告されません。スローするには、捕捉または宣言する必要があります
       Thread.sleep(100);
                    ^
エラー1個

例外機構(Exception Architecture)

例外(Exception)は,近年のプログラミング言語で採用されている実行時エラーの通知機構です.

従来のプログラミング言語,例えば,C言語の場合,fopenで開くファイルが見つからなかった場合, 返り値をNULLにすることでエラーを通知していました. この場合,プログラマが責任を持って, 返り値の値を確認して,正常か異常かを判断しなければいけませんでした.

一方の例外機構は,異常処理を行うための別の処理経路を作るものです. もし,プログラムの実行途中で何らかの異常が発生した時,それまでに行なっていた処理を中断し, 別の処理を行うようにする機構です.

graph LR; A[ファイルを開く] --> B{存在確認} B -->|存在する| C[正常処理] B -->|存在しない| D[異常処理]

C言語のようなエラー処理は上記のフローチャートのように, プログラマ自身がfopenの返り値を元に分岐処理によって,正常処理,異常処理を振り分ける必要があります.

graph LR; A[ファイルを開く] A --> C[正常処理] A -->|存在しない| D[異常処理]

対して,例外機構がサポートされている言語の場合,プログラマが異常,正常の分岐を明示的に書く必要はありません. 異常が起こった場合の処理の経路が決まっており,その経路に処理を書いておくことが異常処理を行うことになります. 正常処理の場合は,それまでの処理の続きにそのまま処理を書いていきます. これにより正常処理の見通しがよくなります.

そして,例外が投げられれば,何らかの異常が発生したと判断できるようになります. 逆に,例外が投げられなければ,データが変であろうが,正常な処理であると判断できます (もちろん,開発途中で変な場合はバグの可能性はありますが).

プログラマが明示的に投げる例外も存在しますが,多くの場合,システムが異常を検知し例外を投げます. 例えば,ファイルが見つからない場合,スリープ中に割り込みが入った場合などです. そのような例外が発生した時に,どのような対応をするのかをあらかじめ決めておく必要があります.

次のプログラムが,例外処理のイメージです.

void exceptionalMethod(){
  // 例外が発生する可能性のある処理
  someMethodCall();

  // 例外が起こらなかった場合の処理
  process();
}

例外が起こった場合は,exceptionalMethod の呼び出し元に通知されます.

検査例外と非検査例外

Javaの例外は,検査例外と非検査例外の2つに分類できます. 違いは次に挙げる通りです.

検査例外

例外が発生した時の処理をプログラム中に明示的に書いておかなければコンパイルエラーになる例外. 完全に防ぐことが不可能な例外(実行時の状態によって発生しうる例外).

例えば,実行時エラーは,プログラムでどれほど十分にチェックしたとしても,完全に回避することはできません.

  • 代表的な検査例外
    • IOException
      • 入出力時にエラーが発生した時.
    • InterruptedException
      • 割り込みが発生した時に発生する例外.

非検査例外

例外が発生した時の処理は書かなくてもコンパイルが通る例外.

  • 代表的な非検査例外
    • NullPointerException
      • 初期化されていない変数に対する処理を行なった場合に発生する例外.
    • ArrayIndexOutOfBoundsException
      • 配列の範囲を超えてアクセスしようとした時に発生する例外.
    • NumberFormatException
      • 数値の変換に失敗した時に投げられる例外.

非検査例外は,事前にチェックすることで,例外の発生を抑えられます. 逆に言えば,実行時に非検査例外が投げられた場合は,事前のチェックが不十分であるとも言えます. 例えば,配列の範囲を超えてアクセスすることはプログラム中で確認することで避けられます.

例外の責任転嫁

さて,例外が発生した時の対処法をプログラム中に書いておく必要があります. 取れる対処法は2つです.

  • 例外が投げられたら,その場で例外に対応する.
  • 例外が発生する可能性のあるメソッドを呼び出している元に対応を任せる.

どちらがふさわしいかは場合により異なります. ここでは,呼び出し元に対応を任せましょう. そのために,呼び出し元に責任を丸投げするようにプログラム中に明示しておきましょう.

こうすることで,例外が発生した時はそのメソッドの呼び出し元に責任を転嫁し,正常処理のみに集中して処理を書くことができます.

本講義では,例外に対応する処理については省略します. 詳細を知りたい場合は,try-catch について調べてみてください.

InterruptedException の責任転嫁

さて,コンパイルエラーで,InterruptedExceptionが投げられる可能性があると述べられています. この例外は,スリープ中に割り込みが発生した時に発生する例外です. ここでは,呼び出し元に責任を転嫁しましょう. 以下のような対応になります.

  • Thread.sleep が例外を発生させた時,Thread.sleep の呼び出し元であるroundTripに対応が任されます.これが例外が投げられた,ということです.
  • そこで,roundTripの呼び出し元であるrunに対応を任せましょう.
  • さらに,runでも,呼び出し元であるmainに処理を任せる事にします.
  • 同じく,mainも呼び出し元に対応を任せましょう.
  • すると,実行環境が対応されなかった例外をスタックトレースという形で出力し,プログラムが終了するようになります.

throws 節

呼び出し元に対応を任せるには,メソッドのシグネチャthrows節を追加します.

throws 節は以下のように指定します. 以下のように書くことで,例外クラスが発生した時,methodNameの呼び出し元に責任を転嫁することができるようになります.

public class ClassName{
  void methodName() throws 例外クラス {
    // 検査例外が発生する可能性のある処理
  }
}

なお,複数の例外が発生する可能性のある場合,throws節に例外クラスの名前をコンマ区切りで指定できます.

例題3. アニメーション

すなわち,メソッドの宣言部分を以下のように変更してください.

public class RoundTrip{
  void run() throws InterruptedException{
    // ... 省略
    // roundTripで IntrruptedException が発生する可能性がある.
    this.roundTrip(circle);
  }
  void roundTrip(EZCircle circle)
        throws InterruptedException{
    // .... 省略
    // Thead.sleepの呼び出しで 
    // IntrruptedExceptionが発生する可能性がある.
    Thread.sleep(100);
  }
  public static void main(String[] args) throws InterruptedException{
    RoundTrip trip = new RoundTrip();
    // runでIntrruptedExceptionが発生する可能性がある.
    trip.run();
  }
}

このようにプログラムを変更し,コンパイルしてみましょう.今度はコンパイルできたはずです. 次のような実行結果となるはずです.

アニメーションの例

このように,アニメーションを行うには,スリープが必要です. 一方,スリープを行うには,例外への対応が必要になります. 今後,ファイルの入出力を扱う時にも例外機構は必要になってきますので, どのような機構であるのか,しっかりと押さえておいてください.

スリープを行わないと,環境によっては目にも留まらぬ速さでアニメーションが繰り広げられます.

例題4. 鉛直投げ上げ運動のアニメーション

以下の実行結果になるよう,鉛直投げ上げ運動のアニメーションを作成してみましょう. (アニメーションが途中で終わっているので変な挙動のように見えますが,バウンドし続ける動きになっています) クラス名は Bound としてください.

$y$方向の0が一番上,下方向がプラスになっていますので,通常の投げ上げとは方向が逆になっています(上方向に重力がかかっていると思ってください). 必要な式は次の通りです.

  • 時間$t$は0から始まり,0.1ずつ増加するものとする.
    • 投げ上げ運動,自由落下運動それぞれに切り替わるとき,$t=0$となる.
  • 重力加速度 $g=9.8$.
  • 投げ上げ運動のとき(下向きに動くとき)
    • 初速 $v_0 = 85.0$.
    • 初期位置 $y_0 = 0.0$.
    • $v<0$となったとき,自由落下運動に切り替わる.
    • 現在位置を求める.
      • $y = v_0 t - \frac{1}{2}gt^2$
    • その時の速度を求める.
      • $v = v_0 - gt$
  • 自由落下運動のとき(上向きに動くとき)
    • 初速 $v_0 = 0.0$
    • 初期位置 $y_0 = 368.62$
      • $y_0$ は投げ上げ運動の時の $v=0$となるときであるから,そのときの$t$は次の式で求められる.
        • $0 = v_0 - gt, v_0 = gt, t = \frac{v_0}{g}$,
        • この時の$t$を鉛直投げ上げの位置を求める式に代入し,
          • $y = v_0 \frac{v_0}{g} - \frac{1}{2}g (\frac{v_0}{g})^2 = \frac{2v_0^2 - v_0^2}{2g} = \frac{v_0^2}{2g} = \frac{85.0^2}{2 * 9.8} = 368.62$
    • $y < 5$となったとき,投げ上げ運動に切り替わる.
      • 本来の物理世界であれば,$v_{i -1}$に跳ね返り係数をかけ,$v_i$とするが,今回は跳ね返り係数を1としている.
    • 現在位置を求める.
      • $y = y_0 - (v_0 t - \frac{1}{2}gt^2)$.
    • その時の速度を求める.
      • $v = v_0 - gt$(投げ上げ運動と同じ)

跳ね返り