2011年12月27日火曜日

Android SDK r16にハマる

android sdk r16がリリースされましたので早速使ってみることにしました。違いは android 4.0.3対応のようです。いつものようにsdkをダウンロードして、次にsdk/tools/androidを実行して必要なファイルをダウンロードします。すべてダウンロードするには結構な時間が掛かりました。
ここまでは、問題ありません。ハマるのはこれからでした。
まず開発中のゲームをビルドしてみます。自分はantを使ってビルドしているので。

% ant clean
% ant debug とします。問題なくビルドできましたので、開発作業を再開します。
(% はマンドプロンプトです。csh系の場合は'%'、bashなどのsh系は'$'、rootシェルの場合は'#'と表記します。自分はcsh系が好きなので tcsh を使っています。因みに開発環境は Ubuntu 10.04)

ところが、プログラムを書き始めると「あれ?」、「何か間違えたかな?」... 5分ほど何が起きたのか
判りませんでしたが修正箇所が反映されていないようです。antの出力をよく見ると。

-dex:
      [dex] No new compiled code. No need to convert bytecode to dalvik format.
ソースコードを書き換えているのにdexファイルが作られていないようです。という事はapkファイルは?

-package:
[apkbuilder] No changes. No need to create apk.
やっぱりapkファイルも新しく作られていません。
一旦sdk 16を使うのを中止。r15に戻して作業を続ける事にしました。

さて、開発中のゲームが完成してリリース準備ができたので、放置していたr16でビルドできない問題を調べる事にします。
まず、ソースコードをダウンロードしておきます。リポジトリに android-4.0.3_r1 というのがあるのでこれを取ってきてビルドしておきます。

次にdexファイルを作るdxコマンドが動くかどうかチェックします。
% dx --dex --verbose --output bin/claases.dex bin/classes libs
問題ありません。ちゃんとclasses.dexができました。

次は ant関連を調べます。普段antを使っているのですが、いままでどのようにantでビルドされているのかちゃんと調べたことがなかったので調べる事から開始です。
sdkの下に tools/ant/build.xml というファイルがありました。これを見ると次のような記述があります。

<path id="android.antlibs">
    <pathelement path="${sdk.dir}/tools/lib/anttasks.jar" />
    


これがandroidのビルドをする為のanttaskのjarファイルです。
androidのビルドをするためのanttask.jarがsdkに含まれているものとソースからビルドしたものが違うとソースを見ても意味がありませんから、同じかどうかチェックしておきます。
先ほどソースからビルドしたanttasks.jarと配布されているsdkのディレクトリのanttasks.jarのファイルサイズは同じなので同じ物のようです。

さらにbuild.xmlには、下記のような記述があります。
<taskdef name="dex"
        classname="com.android.ant.DexExecTask"
        classpathref="android.antlibs" />

名前からこのクラスがdexファイルを作るクラスのように見えます。先ほどダウンロードしておいたソースに DexExecTask.java ファイルがないか探します。
% find . -name DexExecTask.java
./sdk/anttasks/src/com/android/ant/DexExecTask.java
原始的な事をしましたけど、@checkelaさんがもっと便利なやり方を紹介しています。

DexExecTask.javaファイルに"No new compiled code. No need to convert bytecode to dalvik format."というメッセージを出している所がないかと探すと、ありますあります。すぐに見つかりました。(笑)
if (initDependencies(depFile, inputPaths) && dependenciesHaveChanged() == false) {
    System.out.println(
             "No new compiled code. No need to convert bytecode to dalvik format.");
    return;
}

ここを直せばよいような気がします。
この調子でソースを追っていくと、 dexファイルを作る為の依存関係にあるファイルとして libs/ ディレクトリの下にあるファイルと bin/classesディレクトリをチェックしているのが分かります。そしてファイルの先頭にドットがあるファイルを無視したいように書かれています。そのファイルを無視する判定メソッドがInputPath.javaにあります。
ここが間違っています。 trueとfalseを逆にして返してしまうという単純なミス。
これでソースファイルを変更したのにdexファイルが作られない現象が直りました。
以前載せていたのはリバースド・パッチになっていました、訂正してあります。恥ずかしい...

InputPath.javaのパッチ。
diff -r -c source-4.0.3_r1/sdk/anttasks/src/com/android/ant/InputPath.java source/sdk/anttasks/src/com/android/ant/InputPath.java
*** source-4.0.3_r1/sdk/anttasks/src/com/android/ant/InputPath.java     2012-01-07 23:18:45.718828865 +0900
--- source/sdk/anttasks/src/com/android/ant/InputPath.java      2011-12-27 18:13:11.843712831 +0900
***************
*** 76,82 ****
       */
      public boolean ignores(File file) {
          // always ignore hidden files/folders.
!         return file.getName().startsWith(".") == false;
      }

      /**
--- 76,82 ----
       */
      public boolean ignores(File file) {
          // always ignore hidden files/folders.
!         return file.getName().startsWith(".");
      }

      /**

2011年12月6日火曜日

「androidで動くゲームプログラミング入門」 その3

「androidで動くゲームプログラミング入門」 その2の続きです。

[複数の解像度への対応]
Androidは色々な解像度の端末に対応できるように作られています。その機能を活かすには、Android.xmlのmanifestに以下のように書きます。

  <supports-screens
     android:smallScreens="true"
     android:normalScreens="true"
     android:largeScreens="true"
     android:xlargeScreens="true"
     android:anyDensity="true"/>

この説明はyanzmさんのblogに詳しく書いてあります。

ここでは画像データの対応について書きます。画像データはres/drawableの下に置くのですが、解像度と画面密度別に複数のデータを置けるようにできています。
画面サイズはsmall,normal,large,xlargeの4種類。
画面密度はldpi,mdpi,hdpi,xhdpiの4種類あります。このうちxlargeとxhdpiはandroid 2.3以降の対応になります。
携帯端末では480x320はmdpi、8??x480はhdpi、1280x720はxhdpiになるはずです。
drawable-hdpiに800x480向けの画像データを入れておくと読み込んだ時に、mdpi端末では縮小され、xhdpiでは拡大され、ちょっとボケますが拡大縮小などせずにそのまま使えます。
ここでちょっと落とし穴があって、拡大/縮小の具合は画面密度でされるのです。解像度ではありません。自分は勘違いしていて解像度に合わせて拡大/縮小してくれるのだと思っていました。だからタブレット端末だと画像を拡大してくれるのかと思っていたのです。とろこがタブレット端末は高解像度(1280x768ぐらい)ですが、画面密度は中程度なのです。ですからmdpi扱い、つまり縮小されてしまいました。画面が大きくなって画像が縮小されてしまって、小さい画像が表示されて困ってしまいました。解決方法はAndroidディベロッパーラボ東京の時に教えていただきました。
解決方法はタブレット端末の画像は drawable-xlarge-mdpi に入れるでした。1280x768用のデータを作りdrawable-xlarge-mdpiに置き無事解決。
言語毎にデータを分けることもできます。
データを置くディレクトリ名のルールは drawable-言語-解像度-画面密度 となります。
これはlayoutでも同様で layout-言語-解像度-画面密度となります。(試していません、間違っていたらごめんなさい)

例えば、現在売られている端末に合わせて画像データを作るとします。画像制作の手間を減らしたいのでなるべく種類は少なくします。解像度が低い端末向けには用意しません。androidによって縮小された画像を使うようにします。画像の縮小なら劣化はあまり目立ちません、ただしOut of memoryには注意です。目立つのは画像の拡大ですので大きな画像を用意します。
800x480(携帯)向け、1280x768(タブレット)向け、1280x720(携帯向け)。このうち1280x720と1280x768は同じデータで済めば楽だと思います。

ディレクトリ構成は以下のようになります。
drawable-hdpi  (800x480) 各言語共通
drawable-xhdpi (1280x720) 各言語共通
drawable-xlarge-mdpi (1280x768) 各言語共通
drawable-ja-hdpi  (800x480) 日本語用
drawable-ja-xhdpi (1280x720) 日本語用
drawable-ja-xlarge-mdpi (1280x768) 日本語用

例えばこのように動作します。800x480の端末で言語を日本語にすると drawable-ja-hdpi 以下のデータが読み込まれ、もしなければdrawable-hdpiのデータが読み込まれます。
言語設定が日本語以外ならdrawable-hdpi以下のデータが読み込まれます。
このように作るとandroidの「画面サイズが違っても表示される大きさは同じ」という考えと合いません。「高解像度の端末だと大きく表示される」になります。ゲームは絵が大きい方が見やすいですし、楽しいですから。
実際にこのように作ったのがこれです。よかったらダウンロードして遊んでみてください。タブレットでも画像がボケずに大きく綺麗になっています。

[まだ作っていないところ]
今回作ったのはサンプル的なもので、実装レベルもまだまだです。たとえばセーブ機能がありません。プログラムの状態が保存できないので端末の向きを変えるとゲームは最初からやり直しになります。端末を回転させると、ActivityのonDestory()が呼ばれ、onCreate()からやり直しだからです。そうならない為にはセーブしておかなければなりませんが、実装していません。セーブはonPause()で行うのが基本です。
ゲームの基本的な動きは作ったつもりですが、商品レベルにするにはまだまだ沢山作りこまなくてはなりません。UIをまともに作っていないのでちゃんと作る。見た目を格好良くする。ランキングシステムはどうする。広告はいれる? などなど。

コードの説明は androidに関連する部分だけにしてゲーム部分の説明はしませんでした。たぶん読んでも眠くなるだけですし。
質問、コメントや間違いの指摘を寄せていただいたら有難いです。

「androidで動くゲームプログラミング入門」 その1
「androidで動くゲームプログラミング入門」 その2
「androidで動くゲームプログラミング入門」 その3

2011年11月30日水曜日

「androidで動くゲームプログラミング入門」 その2

「androidで動くゲームプログラミング入門」 その1の続きです。
ここからコードの説明に入ります。blogに載せたコードは説明と合わせて見やすいように骨組みだけにしています。実際のコードはgithubから取ってきてください。

[プログラムの初期化]
androidアプリが起動されるとまず、Activityの初期化でonCreate()が呼ばれます。onCreateではViewを初期化してActivityにセットするだけです。この部分で注意する事はViewの初期化に時間を掛けないようにする事です。やってはいけない代表的な例はネットワーク経由でデータをダウンロードする(ほんの少しでもダメです)。大量のデータをロードする事です。データをダウンロードしようとすると電波状況が悪いと5秒ぐらいすぐ経って、ANRが発生してしまいます。地下鉄に乗っている時にアプリの起動ができないというのも困ります。Viewの初期化や起動時に表示させる画像データをロードする程度に留めます。

public class SlidePuzzle1Activity extends Activity {
    private GameView gameView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gameView = new GameView(this); //ここは速く
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(gameView);
    }

    @Override
    public void onStart() {
        super.onStart();
    }

    @Override
    public void onPause() {
        super.onPause();
    }

    @Override
    public void onStop() {
        super.onStop();
    }

    @Override
    public void onResume() {
        super.onResume();
    }
}

onCreate()からの流れを説明します。onCreate()が呼ばれるとSurefaceViewから派生したGameViewクラスを作り、setContentView()でGameViewクラスをセットします。ここでなにか画面に表示できそうな気がしますが、まだViewは作られていませので画面には何も表示されません。
この後 onStart(),onResume()が順に呼ばれ、その後にGameViewのsurfaceCreated()が呼ばれます。surfaceCreated()が呼ばれたときに初めて画面に何かを表示される事が出来ます。

[GameViewクラスの初期化]
GameViewクラスはSurfaceViewクラスを継承したものです。ゲームスレッドはこのクラスに置きます。GestureDetector.OnGestureListenerとSurfaceHolder.Callbackの実装も入れます。

public class GameView extends SurfaceView implements
                                          GestureDetector.OnGestureListener,
                                          SurfaceHolder.Callback,
                                          Runnable {
    public GameView(Activity activity) {
        super(activity);
        init();
    }

    private void init() {
        holder = getHolder();
        holder.addCallback(this);

        surfaceCreated = false;
        thread         = null;
        loader         = new DataLoader(this);
        loaded         = false;
        status         = StatusInit;

        Resources res = getContext().getResources(); 
        logoImg = BitmapFactory.decodeResource(res,R.drawable.fjtn);

        thread = new Thread(this);
        thread.start();
    }

説明するほどの事はないのですが、コンストラクタではinit()を呼び出してaddCallback()を呼びSurfaceViewのsurfaceChanged(),surfaceCreated(),surfaceDestroyed()が呼ばれるようにします。次に画像データの読み込みに使うDataLoaderクラスのインスタンスを作り。起動時に表示する画像データを読み込み、最後にスレッドを生成して初期化終了となります。
DataLoaderクラスの説明はしません。ソースコードを見れば何をしているかすぐ解ると思います。

    public void surfaceCreated(SurfaceHolder holder) {
        gestureDetector = new GestureDetector(this);
        surfaceCreated = true;

        if (initializing) {
            //起動時
            repaint();
        }

        if (!loaded) {
            status = StatusLoading;
            wakeup();
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        surfaceCreated  = false;
        gestureDetector = null;
        loaded = false;
        free();
    }

surfaceCreated()は起動時にViewが作られる時以外にも、ポーズ状態(ホームボタンを押された)後の復帰時にも呼ばれます。その時の流れは Activityの onPause(),onStop()が呼ばれ、復帰時には onRestart(),onStart(),onResume(),SurfaceCreated()の順に呼ばれます。
surfaceCreated()ではGestureDetector()のインスタンスを作った後、描画をします。起動時なら起動時画像が表示され、ポーズ後ならゲーム画面を表示します。
最後にデータがロードされていないならstatusをロード中に書き換え、止めてあったスレッドを動かします。

surfaceDestroyed()はActivityのonPause()の後に呼ばれます。ここで読みこんである画像データを呼んで開放します(free()が画像データ開放メソッド)。Bitmapクラスのインスタンスはrecycle()を呼んで明示的にメモリを開放しないと解放されません。ゲームの多くでは画像データが一番メモリを消費するのではないかと思います。
ここで画像データを開放してしまうと、次にsurfaceCreated()が呼ばれた時に、再度画像をロードしなくてはいけないので再開が遅くなってしまいます。開放しないと他のアプリが動いた時にメモリが足らなくなりアプリが終了させられる可能性も高くなります。どちらを取るかは迷うところです。

ゲームスレッドの骨組みはこのようになっています。
    public void run() {
        while (thread != null) {
            if (status == StatusLoading) {
                sleep(100);
                if (loader.loadImages(getContext())) {
                    if (initializing) {
                        init2();
                        bitmapAlpha = 255;
                        status = StatusOpening;
                        initializing = false;
                    } else {
                        status = prevStatus;
                    }
                    loaded = true;
                    repaint();
                } else {
                    status = StatusError;
                }
            } else if (status == StatusOpening) {
                //オープニング表示
            } else if (status == StatusDisplayAnswer) {
                //答えの表示
            } else if (status == StatusComplete) {
                //パズル完成
            } else if (status == StatusFling) {
                //onFling()を受けて駒を動かす
            } else {
                synchronized (this) {
                    try {
                        wait();
                    } catch (Exception e) {
                    }
                }
            }
        }
    }
}

コンストラクタでstatus=StatusInitとしているのでwait()を呼び出しすぐに休眠状態に入ります。スレッドは用もないのにぐるぐるループしないように必要がない時は止めておきます。
最初のsurfaceCreated()が呼ばれるとstatus==StatusLoadingとなるので、ここでデータをロードします。最初にsleep(100)とあるのは100ミリ秒待つことで起動時画像の描画を邪魔しないようにしています。あまりスマートな方法ではないですが、簡単なやり方でよいかなと..
Activityの起動時は変数initiazilingがtrueになっていてデータをロードした後にオープニングの演出(数字がフェードアウトする)が入りますが、ポーズの後に復帰する時はstatus変数を戻してゲームが再開するようになっています。

    private void wakeup() {
        synchronized(this) {
            notifyAll();
        }
    }


これはwait()で止まっているスレッドを起こすメソッドです。いちいち synchronizedブロックを書くのが面倒なのでメソッドにしています。

[描画について]
描画については前述のように自分で行うのですが、描画をするエリアを絞って必要な部分だけ描くようにします。

    public void repaint() {
        Canvas canvas = null;

        if (!surfaceCreated) {
            return;
        }

        try {
            canvas = holder.lockCanvas();
            synchronized (holder) {
                onDraw(canvas);
            }
        } catch (Exception e) {
        } finally {
            if (canvas != null) {
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }

    public void repaint(int x,int y,int w,int h) {
        Canvas canvas = null;

        if (!surfaceCreated) {
            return;
        }

        try {
            canvas = holder.lockCanvas();
            canvas.save(Canvas.CLIP_SAVE_FLAG);
            canvas.clipRect(x,y,x+w,y+h);

            synchronized (holder) {
                onDraw(canvas);
            }

            canvas.restore();
        } catch (Exception e) {
        } finally {
            if (canvas != null) {
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }

surfaceCreatedという変数をチェックしていますが、GameViewのsurfaceCreated()が呼ばれる前にrepaint()が呼ばれるとholder.lockCanvas()でエラーが起きるためチェックしています。
描画エリアを絞ったrepaint(int x,int y,int w,int h)では、canvas.save()でCanvasの状態をまずセーブしてからクリッピングエリアを変更します。描画の後にcanvas.restore()で元の状態に戻しています。
当たり前ですが、描画エリアを絞ったほうがずっと速く動きます。どの部分だけ再描画が必要か調べるのは面倒な時もありますが効果は大きいのでやったほうがよいでしょう。

[イベント処理について]
イベント処理ではGestureDetector.OnGestureListenerを実装してみます。これでシングルタップやフリックの判定処理を自分で作らずに済みます。オーバーライドするメソッドは下記の6つあります。

onDown() タッチスクリーンに触れられた時に呼ばれる。

onFling() フリックされた時に呼ばれる。

onLongPress() 長押しされた時に呼ばれる。

onScroll() スクロール判定がされた時に呼ばれる。

onShowPress() onDownの後MotionEventのACTION_MOVEやACTION_UPの前に呼ばれる。

onSingleTapUp() シングルタップされた時に呼ばれる。

イベントの起こり方は、例えばonLongPress()だと、onDown(),onShowPress(),onLongPress()の順に呼ばれます。今回使うのはonSingleTapUp()(onDown() -> onSingleTapUp())と
onFling()(onDown() -> onScroll() -> onFling())です。
GestureDetectorを使うにはonTouchEvent()内で下記のようにgestureDetector.onTouchEvent()を呼びます。

    public boolean onTouchEvent(MotionEvent event) {
        boolean rc = false;

        if (gestureDetector == null) {
            return false;
        }

        if (gestureDetector.onTouchEvent(event)) {
            rc = true;
        } else {
            rc = false;
        }

        if (event.getAction() == MotionEvent.ACTION_UP) {
            return onActionUp(event);
        }

        if (status == StatusReady) {
            //処理
            rc = true;
        } else if (status == StatusPlaying) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                //処理
            } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
                //処理
            }
            rc = true;
        } else if (status == StatusComplete) {
            //処理
            rc = true;
        }
        
        return rc;
    }


GestureDetectorを使う上で困るなと思う点があります。拾ったイベントを観察すると分かるのですが、速めに指を動かすとonTouchEventのMotionEvent.ACTION_DOWNの後MotionEvent.ACTION_MOVEが来て、次にonScroll()が呼ばれ、さらに速く動かすとonFling()が呼ばれます。これは理解しやすいです。
ところが指をゆっくり動かし始めるとonTouchEventのMotionEvent.ACTION_DOWNの後MotionEvent.ACTION_MOVEまでは一緒ですが、その後に指の動きを加速させてもonScroll()もonFling()も呼ばれません。これでは最初にタッチして指をゆっくりした動きから速い動きに変えた時にはスクロールやフリック動作をさせられません。実際に作るとわかりますが、結構ぎこちない動きになってしまいます。自分は結局GestureDetectorに頼らないで自作のスクロールやフリックの実装を使っています。作るアプリによってはGestureDetectorは使わない方が却って楽だと思います。

今回はサンプルアプリという事で GestureDetectorを使いました。駒の移動量が少ないのでonTouchEvent()でMotionEvent.ACTION_MOVEだけ拾って作ってもあまりぎこちない動きにはなりませんでした。実装例ですので onFling()も実装してみました。結果はonFling()を使った方が動きが気持よくなりました。ソースのonFling()の実装部分を全部コメントアウトして比べてみると分かると思います。

    public boolean onFling(MotionEvent e1, MotionEvent e2,
                           float velocityXX, float velocityYY) {

        //移動できる方向と逆にフリックされたら無視
        if (movableDirection == Answer.DirectionUp && velocityYY > 0) {
            return false;
        } else if(movableDirection == Answer.DirectionDown && velocityYY < 0) {
            return false;
        } else if (movableDirection == Answer.DirectionLeft && velocityXX > 0) {
            return false;
        } else if (movableDirection == Answer.DirectionRight && velocityXX < 0) {
            return false;
        }

        velocityX = velocityY = 0;
        if (movableDirection == Answer.DirectionUp) {
            velocityY = -movingLength;
        } else if(movableDirection == Answer.DirectionDown) {
            velocityY = movingLength;
        } else if (movableDirection == Answer.DirectionLeft) {
            velocityX = -movingLength;
        } else if (movableDirection == Answer.DirectionRight) {
            velocityX = movingLength;
        }

        status = StatusFling;
        wakeup();

        return false;
    }


onFling()では移動できる方向以外にフリックされたら無視するようにして、それ以外なら速度を与えてstatus変数をStatusFlingにしてwait()しているスレッドを起こします。
駒の移動は長い時間の掛かる処理の部類に入りますので、ここでは行わずゲームスレッドに任せます。動きがよく見えるように駒が次の位置まで移動するのに10フレーム(numMoveToNext=10としています)掛けてゆっくり移動しています。実際のゲームはもっと速く動かすでしょう。

    public boolean onSingleTapUp(MotionEvent e) {
        boolean rc = false;

        if (status == StatusReady) {
            makeGame();
            status = StatusDisplayAnswer;
            wakeup();
            rc = true;
        }

        return rc;
    }


onSingleTapUp()は見た通りなのですが、ゲームを開始できる時(StatusがStatusReady)に問題を作り(makeGame()) statusをStatusDisplayAnswerにしてwait()しているスレッドを起こします。ゲームスレッドは問題を作る過程(コマを1つ空けて、1駒づつ動かしていく)を表示します。解答を見せてしまうためとても簡単にパズルが解けます(笑)。 実際のゲームを作るときは難易度の設定ができて、「簡単モード」だとこのように解答が見られるようにするのかなと思っています。

コードの説明は androidに関連する部分だけにしてゲーム部分の説明はしませんでした。たぶん読んでも眠くなるだけですし。
次回は色々な解像度に対応するデータの持ち方の説明をして終了します。


「androidで動くゲームプログラミング入門」 その1
「androidで動くゲームプログラミング入門」 その2
「androidで動くゲームプログラミング入門」 その3

2011年11月24日木曜日

「androidで動くゲームプログラミング入門」 その1

[はじめに]
今回は「androidで動くゲームプログラミング入門」というタイトルで書いてみます。少し長くなりそうなので何回かに分けて書く予定です。
ちょっと普通のアプリと作り方が違うかと思いますのでSurfaceViewを使ったパズルゲームを作って、そのコードを追いながらandroidの基本的なゲーム・プログラミングのやり方を解説していきたいと思います。作るのはスライドパズルです。なぜスライドパズルかというと作るのが簡単だからという理由です。(笑) ゲームといえばOpenGLを使うケースが多いかと思いますが、今回はCanvasを使います。Canvasでもスライドパズルなら充分な速度が出ます。
ソースコードはgithubに置いておきます。
linuxとMacOSな人は
git clone git@github.com:michiro/SlidePuzzle.git でソースコード一式取得できます。
windowsな他人はリポジトリをgit@github.com:michiro/SlidePuzzle.gitとしてください。
質問、コメントや間違いの指摘を寄せていただいたら感謝します。

[ゲームの処理とマルチスレッド]
ゲームの処理をどこで行うか考えてみます。ゲームですので画面の表示はユーザが何かをした時以外にも変わってきます。例えばアクションゲームではユーザが何もしなくてもゲームはどんどん進んでいきます、それに伴い画面もどんどん描き変えてきます。こういった長い処理はメインスレッドでは行うことが出来ません。androidの場合はメインスレッドを5秒以上止めてしまいますとANR(Application Not Responding)エラーが出てプログラムが終了させられます。android以外のシステムだとしても、例えばwindowシステム(WindowsやX-Window Sysetmなど)のメインループを止めてしますような事をしますと画面が書き換えられなかったり、プログラムが反応しなくなったりします。
そこでゲームの主な処理はメインスレッドを専有しないようにしなくてはいけません。方法としてはタイマーを使って一定間隔でゲーム処理部分を呼び出す方法や別のスレッドを作ってそこで処理を行う方法があります。今回のプログラムは別のスレッドを作る方法で書いて行きます。以後の説明では、この別のスレッドをゲームスレッドと呼ぶことにします。

[Viewについて]
今回作るゲームではSurfaceViewを使います。理由はViewクラスを使うより速いからです。もう一つ、androidのUI操作はスレッドセーフに作られていません。したがって描画はメインスレッドで行わなければならないのです。具体的にはViewクラスでは描画はview.invalidate()を呼ぶのですが、これはメインスレッドから呼ばなくてはなりません。メインスレッドから呼ぶには、下記のようなコードになります。
public void repaint() {
    new Thread(new Runnable() {
            @Override public void run() {
                handler.post(new Runnable() {
                        @Override public void run() {
                            view.invalidate()
                        }
                    });
            }
        }).start();
}

protected onDraw(Canvas canvas) {
    //ここで描画をする
}

この repaint()メソッド(Java2MEと同じメソッド名にしました)はメインスレッドの待ち行列に処理を登録するだけで、すぐに実行されるわけではありません。
repaint()を呼んだ後にonDraw()が呼び出されるので、そこで描画を行います。下記のコードではrepaint()で描画しているのですが、おそらく描画が完了する前にnextProcess()が呼び出されます。遅くなりますし、描画の終了を知るのにひと工夫要ります。
public void myFunction() {
    repaint();
    nextProcess(); //描画が完了する前に、ここに来てしまう
}

androidアプリのUIがシングルスレッドという事に関してはadamrockerさんがblogに詳しく書かれています。

これに対してSurfaceViewを使った場合は、自分でonDraw()を呼び出し描画をします。コードは下記のようになります。
public void repaint() {
    Canvas canvas = null;
    try {
        canvas = holder.lockCanvas();
        synchronized (holder) {
            onDraw(canvas);
        }
    } catch (Exception e) {
    } finally {
        if (canvas != null) {
            holder.unlockCanvasAndPost(canvas);
        }
    }
}

この場合はrepaint()を呼び出せばすぐに描画が始まり。repaint()が終了したときは描画も終わっています。こちらの方が速く、repaint()が帰ってきた時には描画は終わっています。
注意する点はメインスレッドとゲームスレッド両方で色々な処理をする事になると思いますので競合が起きないようにしないように注意深くプログラミングする必要があります。


次回はコードの説明に入ります。
「androidで動くゲームプログラミング入門」 その1
「androidで動くゲームプログラミング入門」 その2
「androidで動くゲームプログラミング入門」 その3



2011年10月26日水曜日

android-sdk r14への移行

android SDKをr14に移行してみた。r13までのように、SDKを入れ替えてツールをダウンロードすれば終わりかと思ったらそうではなかったのでここに書いておく。

android-sdkを所定の位置に置いた後で、androidコマンドを起動してツール類を全部インストールする。ここまでは問題なし。
次にちゃんと動くかどうかコンパイルしてみる。

% ant debugとすると
BUILD FAILED
/xxxxx//build.xml:65:

Error. You are using an obsolete build.xml
You need to delete it and regenerate it using
        android update project
あらら、build.xmlファイルを新しくしなくてはいけないようだ。
android update projectとしろと書いてあるのでそうする。

% rm build.xml
% ant update project -p ./
No project name specified, using Activity name 'XXXActivity'.
If you wish to change it, edit the first line of build.xml.
Added file ./build.xml
プロジェクト名を入れなかったのでactivityの名前でプロジェクト名が作られた。
build.xmlのを書き換えてこれでコンパイルできるかと思うと...

% and debug
BUILD FAILED
/usr/local/java/android-sdk/tools/ant/build.xml:466: The following error occurred while executing this line:
/xxxxxx/OpenFeintAPI/build.xml:65:
Error. You are using an obsolete build.xml
You need to delete it and regenerate it using
        android update project
また同じエラーが出てしまった。
OpenFeintを使っているので、そちらも更新しろと。

OpenFeintもandroid update project -p ./ としてコンパイルできるようになったが。

build.xml:600: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
という警告が出る。これはr14以前から出ていたもので、目障りだから消しておく。
今回はandroid-sdk/tools/ant/build.xmlを直接書き換えてしまった。
antタスクののプロパティに includeantruntime="false"を追加しておく。
これで終了。

r14に移行のドキュメントはちゃんと書かれていて、探すと見つかったので読んでおく。
antでのプロジェクトの作り方やプロジェクトの更新方法のドキュメントもこの機会に読んでおく。

まとめ、ライブラリプロジェクト(今回はOpenFeint)のほうは、API level7対応なので
% android update lib-project --path ./ --target 5

自分のコードの方は
android update project --path ./ --name ProjectName

もともとあった、default.propertiesはproject.propertiesに置き換えられ、build.propertiesはant.propertiesに変えられた。
% ant release としてリリースビルドもできたので大丈夫なようだ。

2011年10月25日火曜日

プログラムの起動

ここでは自分のandroidプログラミングのスタイルを書き連ねていこうかと思っています。入門程度の難易度になるかと思うけど参考になる人もいるかもしれないと思う。現在の仕事はandroid向けのゲームを作るのが主になっているので、ここでもゲームプログラミングの内容が多くなる予定です。

1. プログラムの起動まで

ここでは、よくあるようにHello Worldを作りますが
1. ViewはSurfaceViewを使う
2. プログラムの起動時にスレッドを作っておく
SurfaceViewを使うのはandroid.widget以下のViewを使うより速いからです。もうひとつ、android.widget以下のViewだと描画はメインスレッドで行われなくてはならないのですが、ゲームの処理はゲーム用のスレッド(以下ゲームスレッドと呼びます)でも行ないますし、描画もゲームスレッドで行いたいからです。ただし、メインスレッドとゲームスレッドで描画が競合しないように作らなくてはいけません。

スレッドについてですが、これから作っていくプログラムではデータのローディングから処理のほとんどを自分で生成したゲームスレッドで行います。長い時間の掛かる描画やデータのロードをメインスレッドで行うようにすると、ANR (Application Not Responding)で落ちる場合があります。時間の掛かる処理はメインスレッドでは何をするかの指示をするだけで実際の処理はゲームスレッドで行うようにします。

サンプルコードを書きます。
package jp.co.mekira.android.examples.helloworld1;

import android.os.Bundle;
import android.app.Activity;
import android.view.Window;
import android.util.Log;

public class HelloWorld1Activity extends Activity {
    private GameView gameView;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        gameView = new GameView(this); // <-- ここは遅くちゃダメ
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(gameView);
    }

    @Override
    public void onDestroy() {
        if (gameView != null) {
            gameView.finish();
        }
        super.onDestroy();
    }
}


最初にactivityの生成。 onCreate()をオーバーライドしてactivityを作る数行の処理ですが、ここでの肝はonCreate()は速く終了するという事。その為に14行目のGameVew()のインスタンスを作る処理は速く終わる必要があります。
andoridに限らずdocomoのiアプリでも、Windowシステム向けのアプリケーション(X Window SystemやMacintoshやWindows)でもこの部分は速く終わらせてシステムのメインループに戻す必要があります。
やってはいけない代表的な事がネットワークを介してデータを読み込む(少しでもダメです)とか,大量のデータを読み込む(ディスクなどの速い媒体からでも)事です。

package jp.co.mekira.android.examples.helloworld1;

import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.util.Log;

public class GameView extends SurfaceView implements
                                          SurfaceHolder.Callback,
                                          Runnable {
    private SurfaceHolder holder;
    private boolean       surfaceCreated;
    private Thread        thread;
    private int           status;

    private Paint         bgPaint;
    private Paint         textPaint;
    private int           textSize;

    public static final int StatusNOP  = 0;
    public static final int StatusDraw = 0;

    public GameView(Activity activity) {
        super(activity);
        init();

        thread = new Thread(this);
        thread.start();
    }

    private void init() {
        holder = getHolder();
        holder.addCallback(this);

        surfaceCreated = false;
        thread         = null;
        status         = StatusNOP;

        bgPaint = new Paint();
        bgPaint.setStyle(Paint.Style.FILL);
        bgPaint.setARGB(0xff,0xff,0xff,0xff); //背景は白

        textSize = 24;
        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setStyle(Paint.Style.FILL);
        textPaint.setARGB(0xff,0,0,0); //文字の色は黒
        textPaint.setTextSize(textSize);
    }

    public void surfaceChanged(SurfaceHolder holder,
                               int format, int width, int height) {
    }

    public void surfaceCreated(SurfaceHolder holder) {
        surfaceCreated = true;
        repaint();
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        surfaceCreated  = false;
    }

    public void run() {
        while (thread != null) {
            if (status == StatusDraw) {
                repaint();
            } else {
                synchronized (this) {
                    try {
                        wait(); //止めてしまう
                    } catch (Exception e) {
                    }
                }
            }
        }
    }

    public void finish() {
        thread = null;
        wakeup();
    }

    private void wakeup() {
        synchronized(this) {
            notifyAll();
        }
    }

    public void repaint() {
        Canvas canvas = null;

        if (!surfaceCreated) {
            //surfaceCreated()より前に呼び出された場合は何もしない
            return;
        }

        try {
            canvas = holder.lockCanvas();
            synchronized (holder) {
                paint(canvas);
            }
        } catch (Exception e) {
        } finally {
            if (canvas != null) {
                holder.unlockCanvasAndPost(canvas);
            }
        }
    }

    protected void paint(Canvas c) {
        //背景を塗りつぶす
        c.drawRect(0,0,getWidth(),getHeight(),bgPaint);

        String str = "ハローワールド";
        Rect   bounds = new Rect();
        int    xx,yy;

        //文字列の描画範囲を取得する
        textPaint.getTextBounds(str,0,str.length(),bounds);
        xx = (getWidth() - bounds.width()) / 2;
        yy = (getHeight() - textSize) / 2;
        c.drawText(str,xx,yy,textPaint);
    }
}


SurfaceViewを継承したGameViewは、初期化時にゲームスレッドを生成します。今回はプログラムの骨組みを作るだけなのでゲームスレッドは何もしないでいきなりwait()を呼び出して止めてしまいます。用がないのにCPUを使いたくありませんから、電池も消耗しますし。yield()やsleep()ではなくてwait()で完全に止めます。プログラム終了時にはnotifyAll()メソッドを呼び出しスレッドを起こして終了させます。
描画にはrepaint()を呼び出しますが、この名前はjava.awtやjavax.microedition.lcdui.Canvasクラスと同じ名前を使っているだけです。

ソースコード一式はここ
githubにもおいてあります。 git clone git@github.com:michiro/HelloWorld.git でソースコード一式取得できます。

2011年10月17日月曜日

最初の一歩

今更ながらblogを書いてみる。主にandroidでのプログラムの作り方などを書いてみようかと思う。
できれば実際に動くコードとその説明も書いてみたい。