Android実例剖析ノート(七)


前の文章はSnakeのインタフェースLayoutの実現を分析して、本文はゲームのメインインタフェースというViewがどのように実現したのかに注目して、そして私のいくつかの困惑したところを提出して、友达が困惑を解くことができることを望んでいます.
SnakeというプロジェクトはメインインタフェースをインタフェースUIとゲームロジックの2つの層に分割し、最も基礎的なインタフェースUI部分は親のTileViewで表され、サブクラスSnakeViewはTileViewのUIに基づいて、対応するゲーム制御ロジックを加え、両者の分離を実現し、ゲームの修正に非常に役立つ.
UI実装部
まずインタフェースUIの部分を見てみると、画面全体を2次元配列と見なし、各要素をブロックと見なすことができるので、各グリッドはゲームの過程で異なる状態になることができます.例えば、空き、壁、リンゴ、ヘビ(蛇の体や蛇の頭)を貪ることができます.私たちはゲームを操作する過程で、実際には対応する四角形の状態を絶えず修正してから、View全体に自分自身を再描画させる(もちろん、ゲームの現在の状態(失敗または成功)の判定メカニズムを追加する必要があります).
TileViewのデータ・メンバーは次のとおりです.

  
  
  
  
  1. //  
  2. protected static int mTileSize;      
  3. //  
  4. protected static int mXTileCount;  
  5. protected static int mYTileCount;  
  6. //xy  
  7. private static int mXOffset;  
  8. private static int mYOffset;  
  9. //  
  10. private Bitmap[] mTileArray;   
  11. //  
  12. private int[][] mTileGrid;  

では、ゲームが正式に開始される前に、まず初期化作業を行い、Viewが最初にロードされたときにonSizeChangedを呼び出すのが最善のタイミングです.

  
  
  
  
  1. @Override 
  2. protected void onSizeChanged(int w, int h, int oldw, int oldh)   
  3. {  
  4.         //  
  5.         mXTileCount = (int) Math.floor(w / mTileSize);  
  6.         mYTileCount = (int) Math.floor(h / mTileSize);  
  7.         mXOffset = ((w - (mTileSize * mXTileCount)) / 2);  
  8.         mYOffset = ((h - (mTileSize * mYTileCount)) / 2);  
  9.         mTileGrid = new int[mXTileCount][mYTileCount];  
  10.         clearTiles();  

なお、シミュレータ画面のデフォルト画素は320である×400で、コードのデフォルトの四角形のサイズは12なので、画面に配置されている四角形の数は26です.×40,スクリーンを大きく分割した後,対応する2次元int型配列を1つ設けて各格子の状態を記録し,格子の状態に応じてmTileArrayが保存したアイコンファイルから対応する状態アイコンを読み取ることができる.
最初にonSizeChangedを呼び出すと、最初にonDrawを呼び出してView自体を描画します.もちろん、この場合はすべてのグリッドの状態が0なので、画面上では何も描画されません.

  
  
  
  
  1.  public void onDraw(Canvas canvas)   
  2.   {  
  3.       super.onDraw(canvas);  
  4.       for (int x = 0; x < mXTileCount; x += 1)  
  5.       {  
  6.           for (int y = 0; y < mYTileCount; y += 1)  
  7.           {  
  8.               if (mTileGrid[x][y] > 0)  
  9.               {  
  10.                   canvas.drawBitmap(mTileArray[mTileGrid[x][y]],   
  11.                           mXOffset + x * mTileSize,  
  12.                           mYOffset + y * mTileSize,  
  13.                           mPaint);  
  14.               }  
  15.           }  
  16.       }  

onDrawが行う作業は非常に簡単です.各グリッドをスキャンし、グリッドの現在の状態に基づいて、アイコンファイルから対応するアイコンを選択してこのグリッドに描画します.もちろんこのonDrawはゲーム中に呼び出され続け、インタフェースが更新されます.
ゲームロジック
サブクラスSnakeViewがどのように親クラスTileViewに基づいて、特定のゲームロジックを加えてSnakeというプログラムを完了したのかを見てみましょう.

  
  
  
  
  1. private ArrayList<Coordinate> mSnakeTrail = new ArrayList<Coordinate>();//  
  2. private ArrayList<Coordinate> mAppleList = new ArrayList<Coordinate>();//  

SnakeViewはTileViewから継承されているため、この2次元四角形地図を持っていると言える(ただし、このときの地図のすべての四角形状態は0である).では、このような2次元の四角い地図があります.どうやってこの地図を初期化しますか?これはinitNewGame関数で実現される.

  
  
  
  
  1. private void initNewGame()  
  2.   {  
  3.       //  
  4.       mSnakeTrail.clear();  
  5.       mAppleList.clear();  
  6.       // ,  
  7.       mSnakeTrail.add(new Coordinate(77));  
  8.       mSnakeTrail.add(new Coordinate(67));  
  9.       mSnakeTrail.add(new Coordinate(57));  
  10.       mSnakeTrail.add(new Coordinate(47));  
  11.       mSnakeTrail.add(new Coordinate(37));  
  12.       mSnakeTrail.add(new Coordinate(27));  
  13.       mNextDirection = NORTH;  
  14.  
  15.       //  
  16.       for (int i = 0; i < nApples; ++i)  
  17.       {  
  18.           addRandomApple();  
  19.       }  
  20.       //  
  21.       mMoveDelay = 600;  
  22.       mScore = 0;  

ゲーム画面全体を撮影し、次の状態で写真を撮ることを想像すると、2枚の写真の違いはどのように発生しますか?システムにとって、onDrawを呼び出し続けることしか知られていません.後者は画面全体を描くことを担当しています.それは2つの画面の違いを生み出し、いくつかのデータ構造(ここの2次元四角形地図など)を調整する必要があります(ユーザーの制御命令、タイマーなど).その後、次のonDrawになると、これらの変更がインタフェースに反映されます.   ここではprivate long mMoveDelay=600について重点的に説明する.このメンバー変数は、目立たないが、その役割をよく考えてみると面白い.では、その大きさを変えることで、ゲームが速くなったり遅くなったりすることをどのように感じているのだろうか.
簡単な例として、時刻0でゲームが開始され、まず蛇とリンゴの位置をグリッドマップにマークし、update関数で蛇の体を修正して蛇を北に進ませることができますが、この変更は内部のコアデータ構造(すなわち2次元グリッドマップ)にとどまり、インタフェースに表示されていません.もちろん、この変更を表示させるには、onDrawを呼び出して描画させておけばいいのではないかとすぐに考えました.しかし問題は、システムがどのくらいの時間を隔ててonDraw関数を呼び出すのか分からないことであり、mMoveDelayはこの時点で機能し、それを通じて休眠時間を設定することができ、時間が来るとすぐにSnakeViewに再描画を通知することができる.mMoveDelayの数値を大きくしてみると、私が上記した「写真を撮る」効果がわかります.
Handlerの使用
JavaScriptやActionScriptを書いた開発者は、setIntervalの使い方をよく知っています.では、AndroidでsetIntervalをどのように実現するかというと、似たような機能を実現する方法が2つあります.そのうちの1つはスレッドでHandlerメソッドを呼び出し、もう1つはアプリケーションTimerです.Snakeでは前者を使用しています

  
  
  
  
  1. class RefreshHandler extends Handler   
  2.    {  
  3.        @Override 
  4.        public void handleMessage(Message msg)   
  5.        {//“ ”  
  6.           SnakeView.this.update();  
  7.           SnakeView.this.invalidate();  
  8.        }  
  9.        public void sleep(long delayMillis)   
  10.        {// delayMillis  
  11.            this.removeMessages(0);  
  12.            sendMessageDelayed(obtainMessage(0), delayMillis);  
  13.        }  

実際に呼び出された処理関数updateはゲーム全体のエンジンと言えるが、その仕事(蛇とリンゴの状態を新しい状態に修正し、自分を休眠し、目が覚めるとHandlerで前回修正した2次元ブロックマップをシステム領域に描画させ、updateを再び呼び出し、このように繰り返し、生きている)これにより、ゲームがどんどん進められるようになったので、「エンジン」と比べると過言ではありません.

  
  
  
  
  1. public void update()  
  2.   {  
  3.       if (mMode == RUNNING)  
  4.       {  
  5.           long now = System.currentTimeMillis();  
  6.           if (now - mLastMove > mMoveDelay)   
  7.           {  
  8.               clearTiles();  
  9.               updateWalls();  
  10.               updateSnake();  
  11.               updateApples();  
  12.               mLastMove = now;  
  13.           }  
  14.           mRedrawHandler.sleep(mMoveDelay);  
  15.       }  
  16.   } 

      updateがゲームの動力である以上、ゲームを停止させるにはupdateを呼び出さなければよい(この時点では画面が静止しているため)ため、ゲームは一時停止に入る(この状態では「運転」に移行することもできるが、実際には修正を継続して描き直すことができる)、失敗に入ると(実はこの時点で二次元ブロックマップが最後の画面に留まっているのも、最初に地図全体をクリアしなければならない理由です)【これは、ゲームに失敗した後に、再び新しいゲームを開始することができ、その際に設定されたブレークポイントで前回のゲームの実行時の下位データを観察することができます】.
少し戸惑う      しかし、個人的にはSnakeの次のコードは少し変だと思います.「鶏が先にいるのか、卵が先にあるのか」という問題のように、私の思考論理に「怪圏」が現れました.

  
  
  
  
  1. public void handleMessage(Message msg)   
  2. {  
  3.         SnakeView.this.update();  
  4.         SnakeView.this.invalidate();  
  5.  } 

このコードの意味から、スリープの時間が来たら、まずupdateを呼び出します.つまり、次の描画の準備をしてから、自分をスリープさせ、最後にシステムに自分を再描画するように通知します.
まあ、これは私に理解しにくいですが、やはり時刻0の例に戻って、時刻0の時に蛇の体を北に一歩前進させ(底の2次元の四角い地図の修正を指して、インタフェースではありません)、それから自分を0.6ミリ秒休眠させて、時間になったら、まずupdateの方法を呼び出して、それではまた蛇の体に修正をさせて、つまり前回まだ描いていないカバーを落としました(では前回の修正は無駄ではなかったのか、まだ描いていないのか)、ましてやupdateで自分を休眠させる(まだinvalidateを呼び出していないのに、どうしてまた休眠したのか?)、どうして私のonDrawメソッドを呼び出すようにシステムに知らせることができるのか.つまりinvalidateは実行されていないのか.??
私の理解では、順番を逆さまにして、まずシステムにonDrawメソッドを呼び出して再描画するように通知し、前回の下位の2次元四角形地図の修正を表示させ、それから次の修正の準備をして、最後に自分を休眠させ、目が覚めるのを待つように繰り返します.実験は、逆さまにするのも正しいことを証明していますが、この謎については私を惑わすところ、友达が私を教えてくれることを望んでいます!
javascriptでsetIntervalを使うときも、まず処理ロジックを書いて、それから末尾にsetInterval(これも私の習慣的な考え方です)を書くのを覚えていますが、googleの上でこのような書き方は何の意味がありますか?
      また、壁を描くたびに壁を描き直すのは、壁に何の変化もないので、時間がもったいないような気がします.また、mLastMoveという変数設定の初心は、現在の時間点が前回の変化からmMoveDelayミリ秒を過ぎたことを保証することですが、sleepメカニズムを使っている以上、この時間差を使う必要はありません.