11-1. Androidプログラムの並列化

Android のスレッド管理

スマートフォンもマルチコア化されて来たため、アプリケーションで効率的に スレッドを使うと高速化できます。 Android の開発マニュアルの 「プロセスとスレッド」 にあるように、 メインスレッド(UIスレッド)にしか許されていない操作があります。 例えば、シューティングゲームで自機の操作をスレッド化すると、ユーザの操 作へのAPIをメインスレッド以外のスレッドからアクセスすることになるので 異常終了してしまいます。 そのため、異常終了を回避するテクニックを考えるか、スレッド化の切り分け を変える必要があります。

git://edu.net.c.dendai.ac.jp/git/spro/11/1 にサンプルプログラムを示し ます。 これはシューティングゲームですが、View クラスでスレッドを作っています。

リアルタイムゲームの基本的な動作は、画面のフレームを書き換えていくこと です。 つまり、例えば、1/30秒以内に、1/30秒後の画面を計算して描画することを繰 り返します。 描画は一定間隔で行わなければなり ませんが、各シーンで必要な処理量は一定ではありません。 そのため、処理ループ内で、各1ステップの処理を行うことと、描画を続け てしまうと、処理量が多くなることで、描画が遅くなったり、プレイヤーには 独立して動作すると思われている動きに影響が出たりします。 これをコマ落ちとか、動作が思いとか表現されたりします。 さらに、Android 端末では、アーキテクチャは一定でも端末間に性能差がある ため、それを吸収するようなプログラミングをしなければなりません。

今、プレイヤー操作を読み取ってstep処理へ指示をだすmove()、各ステッ プの操作を step() とし、描画を draw() 、 時間調整を wait() とする と、安易なゲームプログラムは次のようなプログラムになります。

private Player player;
void main(){
  while(!gameover()){
    player.move();
    step();
    draw();
    wait();
  }
}

しかし、一般的なオブジェクト指向環境では、ユーザ操作はイベント処理 で行われますので、次のような形態になっているかも知れません。

private Player player;
void main(){
  while(!gameover()){
    step();
    draw();
    wait();
  }
}
@Override
public void onUserEvent(Event e){
  player.move(); 
}

さて、このような状況で、安定した動作と、並列化による高速化を考えま す。 まず、step ですが、これは前述通り、一定の時間で終わるわけではなく、 ゲームの局面で処理時間が変化します。 そこで、通常は画面の座標は整数ですが、これを小数点以下まで表せる型 で表現します。 そして、 step を引数 t をとり、 t 時間進めるように変更します。 そして、時間を測定し、前回の step からの時間からの経過時間を与えま す。

private Player player;
void main(){
  double previous = System.currentTimeMillis();
  double now;
  while(!gameover()){
    now = System.currentTimeMillis();
    step(now-previous);
    previous = now;
    draw();
    wait();
  }
}
@Override
public void onUserEvent(Event e){
  player.move(); 
}

次に並列化します。 onStart で動作させ、 onStop メソッドで停止できるよう、共通の変数を持ち、制御をします。 shutdown 変数は boolean でtrue になったらすべて のスレッドが停止するように動作します。 異なるスレッドから変更できるよう volatile 宣言します。

なお、並列化するにあたり、座標を変更する途中で描画するのを避けるために、 排他制御を行います。 lock 変数は Object のインスタンスで、 synchronized の引数として用い、 キャラクタの移動、描画が同時に行われないように制御します。 なお、いずれの並列処理においても、待ち時間を入れ、常にビジーな状態が続 かないようにします。

private Player player;
private Thread tstep;
private Thread tdraw;
private volatile boolean shutdown;
private Object lock;
void init(){
  tstep = new TStep();
  tdraw = new TDraw();
  lock = new Object();
}
@Override
public void onStart(){
  super.onStart();
  shutdown = false;
  tstep.start();
  tdraw.start();
}
@Override
public void onStop(){
  super.onStop(); 
  shutdown = true;
  try{
    tstep.join();
  }catch(Execption e){
  }
  try{
    tdraw.join();
  }catch(Exception e){
  }
}
class TStep extends Thread {
  @Override
  public void run(){
    double previous = System.currentTimeMillis();
    double now;
    while(!shutdown){
      synchronized(lock){
        now = System.currentTimeMillis();
        step(now-previous);
        previous = now;
      }
      wait();
      if(!shutdown){
        shutdown = gameover();
      }
    }
  }
}
class TDraw extends Thread {
  @Override
  public void run(){
    while(!shutdown){
      synchronized(lock){
        draw();
      }
      wait();
    }
  }
}
@Override
public void onUserEvent(Event e){
  player.move(); 
}

演習11-1

ShootingSample を取得し、シーンを追加する

プログラムの取得

  1. File→New→Project From Version Control→Gitを選ぶ
  2. URLにgit://edu.net.c.dendai.ac.jp/git/spro/11/1を入れ、Clone ボタンを押す

テスト

プログラムを起動し、正常に表示されるかを確認する。

mono/Teki2.java

  1. Teki.java をコピーペーストして Teki2.java を作成する。 以下、Teki2.java 内を編集する。
  2. init メソッド内で、 dps = new Vect[]{new Vect(0,1)}; に変更する。
  3. 
    public double getInterval() {
            return 12;
    }
    

TekiLogic.java

次のように修正します

  1. フィールドに次を増やす
          
    private static double period2 = 500;
    private double tic2;    
    
  2. コンストラクタ内にtic2 = 0;を増やす
  3. 次のメソッドを増やす
    
    private Mono createTeki2() {
            return new Teki2(context, (int)(Math.random()*400), 30);
    }      
    
  4. stepメソッドに次のコードを追加する
    
      tic2 += tstep;
      while (tic2 > period2) {
        list.add(createTeki2());
        tic2 -= period2;
      }
    

テスト

プログラムを起動し、垂直に降りてくる敵が出現することを確認する。