SurfaceViewで高速描画する(1)に引き続き、SurfaceViewでの描画方法です。第2回ではゲームプログラミングを意識して、処理落ちを考慮した描画テクニックを紹介します。
右側の図は、SurfaceViewを使って画面上から下へ、Bitmapを移動させたキャプチャです(わかりやすく背景をクリアせずに残像を残しています。残像の理由は記事の最後の章”描画関数”で)。実際は、コマ落ちを考慮したアニメーションができます。
紹介するのはインベーダーゲーム・マリオブラザーズのようにユーザーのアクションが無くても時間が経過していくアクションゲームに適したロジック(の基本)です。ポイントは以下の2つ。
- Threadを使った連続描画(無限ループ)
- 処理落ちを考慮した移動量調整
なお、SurfaceViewで高速描画する(1)で解説した内容をベースに記述していますので未読の方は(1)も合わせてどうぞ。またAndroidにおけるThreadのあつかいなどはTimerを使って定期実行するで、少し触れています。こちらも参考程度に。
ではサンプルコードは続きから。
(要点を先取りしたい人は無限ループの導入を読み飛ばして、次章の無限ループの実装をみてください)
無限ループの導入(Threadの追加)
描画処理をループで処理するための準備をします。ユーザのアクション(タッチ・キー操作)をトリガとしたイベント処理は今回は未考慮です。経過時間に応じて描画を更新します。
public class sampleSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable{ private Bitmap mImage; private SurfaceHolder mHolder; private Thread mLooper; (省略) }
SurfaceHolder.Callback以外にも、Thread処理用のRunnableを実装します。
surfaceCreated()
//SurfaceView生成時に呼び出される public void surfaceCreated(SurfaceHolder holder) { //スレッドの生成 mHolder = holder; mLooper = new Thread(this); }
surface生成のタイミングでスレッドも同時に作成します。描画のためにはSurfaceHolder(でのcanvasロック)も必要なので一緒に保存しておきます。
surfaceDestroyed()
//SurfaceView破棄時に呼び出される public void surfaceDestroyed(SurfaceHolder holder) { //スレッドを削除 mLooper = null; }
スレッドを削除します。サンプルコードではスレッドをmLooperメンバに保持しています。null代入されて参照を失ったスレッド資源はAndroidのメモリ管理システムによって回収されます。
surfaceChanged()
private int mHeight; //画面の高さ private int mPositionTop = 0; //表示位置(TOP:Y座標) private int mPositionLeft = 0; //表示位置(LEFT:X座標) private long mTime =0; //一つ前の描画時刻 private long mLapTime =0; //画面上部から下部に到達するまでの時間 //SurfaceView変更時に呼び出される public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { //スレッド処理を開始 if(mLooper != null ){ mHeight = height; mTime = System.currentTimeMillis(); mLooper.start(); } }
スレッド処理を開始します。1行目から5行目のメンバを覚えておきます。
無限ループの実装
無限ループのサンプルソースは以下の通りです。doDraw()は描画用の関数(後述)です。今回のポイントはSystem.currentTimeMillis()メソッドを利用して時刻に応じた描画位置を求めている箇所(16行目)です。
//スレッドによるSurfaceView更新処理 public void run() { while (mLooper != null) { //描画処理 doDraw(); //位置更新処理 //処理落ちによるスローモーションをさけるため現在時刻を取得 long delta = System.currentTimeMillis() - mTime; mTime = System.currentTimeMillis(); //次の描画位置 int nextPosition = (int)( ( delta / 1000.0 ) * 200 ); //1秒間に200px動くとして //描画範囲の設定 if(mPositionTop + nextPosition < mHeight ){ mPositionTop += nextPosition; }else{ //画面の縦移動が終わるまでの時間計測(一定であることが期待値) Log.d("VIEW","mLapTime:" + (mTime - mLapTime) ); mLapTime = mTime; //位置の初期化 mPositionTop = 0; } } }
NexusOneでは描画が上から下までアニメーションする時間mLapTimeは2.7秒程度でした。
時間を係数にコマ落ちを考慮した処理(16行目)を入れると、タッチ操作など画面描画が遅延した場合でもmLapTimeはほぼ一定を保てます。
09-21 03:25:28.451: DEBUG/VIEW(12336): mLapTime:2766
09-21 03:25:31.161: DEBUG/VIEW(12336): mLapTime:2701
09-21 03:25:33.861: DEBUG/VIEW(12336): mLapTime:2701
09-21 03:25:36.601: DEBUG/VIEW(12336): mLapTime:2744
09-21 03:25:39.321: DEBUG/VIEW(12336): mLapTime:2724
09-21 03:25:42.011: DEBUG/VIEW(12336): mLapTime:2684
描画関数
//描画関数 private void doDraw(){ //Canvasの取得(マルチスレッド環境対応のためLock) Canvas canvas = mHolder.lockCanvas(); Paint paint = new Paint(); //描画処理(Lock中なのでなるべく早く) canvas.drawColor(Color.GRAY); canvas.drawBitmap(mImage, mPositionLeft, mPositionTop, paint); //LockしたCanvasを解放、ほかの描画処理スレッドがあればそちらに。 mHolder.unlockCanvasAndPost(canvas); }
8行目のcanvas.drawColor(Color.GRAY);が無いと、この記事の冒頭で紹介したような画像になります。SurfaceViewは前回の描画内容をリセットせずに次の描画を行っているからです。サンプルコードでは塗りつぶしましたが、必要な箇所だけクリアできれば処理を高速化できます。