Canvasを使用してAndroid向けのゲームを作成しています

こんにちはHabr!

今日は、Canvasを使用してAndroid OS用のシンプルなロジックゲームを作成する方法についてお話します。 約5年前に携帯電話でこのゲームに出会いました。 名前は忘れられており、いくつかのテーマ別フォーラムで検索しても何ももたらされなかったため、このゲームの独自の実装を作成することにしました。 私は小さなプロジェクトに取り組むこともありますが、開発は私の趣味です。 少し考えてから、エンジンを使用せず、自分で書くことにしました。 この決定の理由:Canvasでの経験を得たいという願望。



一番下の行は...





チェス盤に似た競技場がありますが、セルを白黒に分割しません。 フィールドのサイズは任意ですが、2x3より小さいフィールドでプレイするのはあまり意味がありません。 10x10のフィールドでプレーするのが好きです。



プレイヤーの数は任意ですが、私の意見では、一緒にプレイするか4人でプレイする方が面白いと思います。 長方形のフィールドで3人で一緒にプレーすると、プレーヤーの1人が「空の」コーナーから遠くなるため、不均衡が発生します。 4人以上でプレイする場合、戦略を実行することは困難です。



各プレイヤーは、1つのオブジェクト(アトムと呼びます)をフリーセルまたはそのアトムが既に存在するセルに配置する必要があります。 「臨界質量」が隣接セルの数に等しいセルに蓄積すると、このセルの原子は隣接セルに移動しますが、隣接セルの原子は「捕獲」されます。 それらは現在、原子が散らばっているプレイヤーのものです。



ポイントを明確にするためのいくつかの写真。





各セルの臨界質量を示す空の4x4フィールド。





3番目の移動後のゲームの状況。





4回目の移動後のゲームの状況(最初は青くなります)。 重大な数の原子が左上隅に蓄積されていることがわかります。





ああ! それらは散乱し、セル[2] [1]に2つの青い原子を捕獲しました。 そして、このセル[0] [1]でも重要な量になりました。 連鎖反応!





拡張後の状況。 4番目の動きの終わり。 これで、青が5番目の動きになります。





実装。 グラフィック部分。





実装に取り​​かかりましょう。 Viewから派生したクラスを作成しましょう。

public class GameView extends View { private Bitmap mBitmap; private Canvas mCanvas; private Paint paint, mBitmapPaint; private float canvasSize; private final int horizontalCountOfCells, verticalCountOfCells; public GameView(Context context, AttributeSet attrs) { super(context, attrs); //   horizontalCountOfCells =10; verticalCountOfCells =10; // xml       300dp canvasSize=(int)convertDpToPixel(300, context); mBitmap = Bitmap.createBitmap((int) canvasSize, (int) canvasSize, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBitmap); mBitmapPaint = new Paint(Paint.DITHER_FLAG); //  ,       paint =new Paint(); paint.setAntiAlias(true); paint.setDither(true); paint.setColor(0xffff0505); paint.setStrokeWidth(5f); paint.setStyle(Paint.Style.STROKE); paint.setStrokeJoin(Paint.Join.ROUND); paint.setStrokeCap(Paint.Cap.ROUND); //  for(int x=0;x< horizontalCountOfCells +1;x++) mCanvas.drawLine((float)x* canvasSize / horizontalCountOfCells, 0, (float)x* canvasSize / horizontalCountOfCells, canvasSize, paint); for(int y=0;y< verticalCountOfCells +1;y++) mCanvas.drawLine(0, (float)y* canvasSize / verticalCountOfCells, canvasSize, (float)y* canvasSize / verticalCountOfCells, paint); } @Override protected void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); } // dp   public float convertDpToPixel(float dp,Context context){ Resources resources = context.getResources(); DisplayMetrics metrics = resources.getDisplayMetrics(); return dp * (metrics.densityDpi/160f); } }
      
      







ここでは、低品質コードを少し許可しました。つまり、プレイフィールドのセルとビューのサイズを分ける線の色が300 dpに等しいと見なされるコードにハードコードされています。 このサイズはAttributeSetクラスのattrsオブジェクトから取得できますが、コードが乱雑になることはありません。



また、アクティビティをすぐにスケッチして、すべてが美しく描画されるようにします。



 public class GameActivity extends Activity { Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
      
      







そして、マークアップmain.xml



 <LinearLayout xmlns:android=«schemas.android.com/apk/res/android» android:orientation=«vertical» android:layout_width=«fill_parent» android:layout_height=«fill_parent» android:background="#aaa" android:gravity=«center_horizontal»> <com.devindi.chain.GameView android:layout_width=«300dp» android:layout_height=«300dp» android:id="@+id/game_view" android:layout_gravity=«center» android:background="#000"/> </LinearLayout>
      
      







このステップの後のコード。



さて、プレイフィールドのスケールを変更する機能を追加しましょう。サイズが小さいため、目的のセルを超えてミスが発生する可能性があるためです。 これを行うには、GameViewクラスのOnScaleGestureListenerインターフェイスのScaleGestureDetector.SimpleOnScaleGestureListenerを実装するために必要なメソッドを再定義します。



  private final ScaleGestureDetector scaleGestureDetector; private final int viewSize; private float mScaleFactor; public GameView(Context context, AttributeSet attrs) { // xml       300dp viewSize=(int)convertDpToPixel(300, context); mScaleFactor=1f;//    canvasSize=(int)(viewSize*mScaleFactor);//   … scaleGestureDetector=new ScaleGestureDetector(context, new MyScaleGestureListener()); } @Override protected void onDraw(Canvas canvas) { canvas.save(); canvas.scale(mScaleFactor, mScaleFactor);//  canvas.drawBitmap(mBitmap, 0, 0, mBitmapPaint); canvas.restore(); } //      MyScaleGestureListener @Override public boolean onTouchEvent(MotionEvent event) { scaleGestureDetector.onTouchEvent(event); return true; } //  ScaleGestureDetector.SimpleOnScaleGestureListener,       //  OnScaleGestureListener private class MyScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { // ""  @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { float scaleFactor=scaleGestureDetector.getScaleFactor();//      //    -    float focusX=scaleGestureDetector.getFocusX(); float focusY=scaleGestureDetector.getFocusY(); //               2  if(mScaleFactor*scaleFactor>1 && mScaleFactor*scaleFactor<2){ mScaleFactor *= scaleGestureDetector.getScaleFactor(); canvasSize =viewSize*mScaleFactor;//       //   //         . //  ,     // ,      //        ( ). int scrollX=(int)((getScrollX()+focusX)*scaleFactor-focusX); scrollX=Math.min( Math.max(scrollX, 0), (int) canvasSize -viewSize); int scrollY=(int)((getScrollY()+focusY)*scaleFactor-focusY); scrollY=Math.min( Math.max(scrollY, 0), (int) canvasSize -viewSize); scrollTo(scrollX, scrollY); } //   invalidate(); return true; } }
      
      







結果のコード



拡大値の境界が設定され(増加は1から2になります)、ゲームフィールドがズームポイント(指の間のポイント)までスクロールされ、プレイフィールドの外側の領域が表示されることに注意してください。 ズームポイント(焦点)へのスクロールは次のように実行されます-キャンバスの先頭(左上隅)に対する焦点の座標が計算され、ズーム係数が乗算され、ビューに対する点の座標が減算されます。 その後、競技場外へのスクロールを防ぐために、間隔[0、canvasSize -viewSize]から最も近い値を取得します。



ここで、スクロール、シングルタップ、ダブルタップの処理を記述します(ダブルタップは競技場の元のスケールに戻ります)。



  private final GestureDetector detector; public GameView(Context context, AttributeSet attrs) { ... detector=new GestureDetector(context, new MyGestureListener()); } //      Motion Event' MyGestureListener'  MyScaleGestureListener' @Override public boolean onTouchEvent(MotionEvent event) { detector.onTouchEvent(event); scaleGestureDetector.onTouchEvent(event); return true; } //  GestureDetector.SimpleOnGestureListener,     //    OnGestureListener private class MyGestureListener extends GestureDetector.SimpleOnGestureListener { //  (   ) @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //       if(getScrollX()+distanceX< canvasSize -viewSize && getScrollX()+distanceX>0){ scrollBy((int)distanceX, 0); } //       if(getScrollY()+distanceY< canvasSize -viewSize && getScrollY()+distanceY>0){ scrollBy(0, (int)distanceY); } return true; } //   @Override public boolean onSingleTapConfirmed(MotionEvent event){ //  ,    int cellX=(int)((event.getX()+getScrollX())/mScaleFactor); int cellY=(int)((event.getY()+getScrollY())/mScaleFactor); return true; } //   @Override public boolean onDoubleTapEvent(MotionEvent event){ //     mScaleFactor=1f; canvasSize =viewSize; scrollTo(0, 0);//,      . invalidate();//  return true; } }
      
      







結果のコード

シングルタップで、タップしたセルの座標を計算します。たとえば、左上のセルの座標は0,0になります。



アトム描画メソッドdrawAtomsを書きましょう。 メソッドのパラメーター-原子、色、原子の数を描画するセルの座標。



  void drawAtoms(int cellX, int cellY, int color, int count){ //    float x0=((1f/(2* horizontalCountOfCells))*viewSize+(1f/ horizontalCountOfCells)*cellX*viewSize); float y0=((1f/(2* verticalCountOfCells))*viewSize+(1f/ verticalCountOfCells)*cellY*viewSize); paint.setColor(color); switch (count){ //todo non-absolute values case 1: drawAtoms(cellX, cellY, color, 0);//   mCanvas.drawCircle(x0, y0, 3, paint);//      break; case 2: drawAtoms(cellX, cellY, color, 0); //        mCanvas.drawCircle(x0-7, y0, 3, paint); mCanvas.drawCircle(x0+7, y0, 3, paint); break; case 3: drawAtoms(cellX, cellY, color, 0); //            mCanvas.drawCircle(x0 - 7, y0 + 4, 3, paint); mCanvas.drawCircle(x0 + 7, y0 + 4, 3, paint); mCanvas.drawCircle(x0, y0-8, 3, paint); break; case 4: drawAtoms(cellX, cellY, color, 0); // 4          mCanvas.drawCircle(x0-7, y0-7, 3, paint); mCanvas.drawCircle(x0-7, y0+7, 3, paint); mCanvas.drawCircle(x0+7, y0+7, 3, paint); mCanvas.drawCircle(x0+7, y0-7, 3, paint); break; case 0: //    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); //   paint.setStyle(Paint.Style.FILL); //  ,       mCanvas.drawCircle(x0, y0, 17, paint); //   paint.setStyle(Paint.Style.STROKE); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); break; } invalidate();//  }
      
      







現時点では、この方法には原子のサイズと原子間の距離の絶対値という形の欠陥があります。 これは何につながりますか? フィールドサイズが10x10未満の場合、原子は小さく見え、フィールドサイズが大きいとセルに収まらない場合があります。



スクロールバーを追加するために残ります。 このプロセスはhabrahabr.ru/post/120931で詳しく説明されています



ゲームロジックの説明に進む前に、GameLogicタイプのグローバル変数(ロジックを説明するクラス)に名前logicを追加し、onSingleTapConfirmedをシングルタップの処理メソッドに追加します。

新しいアトムを追加する論理処理のfutureメソッドを呼び出します。

logic.addAtom(cellX、cellY);

この変数にはセッターも必要です。



コード。



実装。 ゲームロジック。





ゲームのロジックを記述するGameLogicクラスを作成します。 また、フィールドクラスセルのパラメーターを格納する内部クラスCellが必要です。



  public class GameLogic { private class Cell{ int player=0, countOfAtoms=0;// ,     final int maxCountOfAtoms;//  Cell(int maxCountOfAtoms){ this.maxCountOfAtoms=maxCountOfAtoms; } public int getCountOfAtoms() { return countOfAtoms; } public int getPlayer() { return player; } public void setPlayer(int player) { this.player = player; } public void resetCount() { this.countOfAtoms = 0; } public void addAtom(){ this.countOfAtoms++; } boolean isFilled(){ return this.countOfAtoms == this.maxCountOfAtoms; } } }
      
      







GameLogicクラス自体



  private final GameView view; private final GameActivity activity; private int moveNumber=0, currentPlayer=0; private final int COUNT_OF_PLAYERS, BOARD_WIDTH, BOARD_HEIGHT; private final Cell[][] cells; private final int[] colors={0xff1d76fc, 0xfffb1d76, 0xff76fb1d, 0xffa21cfb};//  private final Handler mHandler; public GameLogic(GameView view, GameActivity activity) { this.view = view; this.activity=activity; mHandler=new Handler(); //   ( ,  ) this.COUNT_OF_PLAYERS=2; this.BOARD_HEIGHT=10; this.BOARD_WIDTH=10; cells=new Cell[BOARD_WIDTH][BOARD_HEIGHT]; for(int x=0; x<BOARD_WIDTH; x++){ for(int y=0; y<BOARD_HEIGHT; y++){ if((x==0 || x==BOARD_WIDTH-1) && (y==0 || y==BOARD_HEIGHT-1)){ cells[x][y]=new Cell(2);//    2 }else if((x==0 || x==BOARD_WIDTH-1) || (y==0 || y==BOARD_HEIGHT-1)){ cells[x][y]=new Cell(3);//,    - 3 }else{ cells[x][y]=new Cell(4);// - 4 } } } } //      public void addAtom(final int cellX, final int cellY) { // ,    ,      -   . final Cell currentCell; try{ currentCell=cells[cellX][cellY]; }catch (IndexOutOfBoundsException ex){ return; } //        if(currentCell.getPlayer()==currentPlayer){ currentCell.addAtom(); view.drawAtoms(cellX, cellY, colors[currentPlayer], currentCell.getCountOfAtoms()); //   if(currentCell.isFilled()){ final List<Cell> nearby=new ArrayList<Cell>(4);//   selfAddCell(cellX, cellY-1, nearby); selfAddCell(cellX, cellY+1, nearby); selfAddCell(cellX-1, cellY, nearby); selfAddCell(cellX+1, cellY, nearby); for(Cell nearbyCell:nearby){ nearbyCell.setPlayer(currentPlayer);//     } delayedAddAtom(cellX, cellY-1); delayedAddAtom(cellX, cellY+1); delayedAddAtom(cellX-1, cellY); delayedAddAtom(cellX+1, cellY); //     run() mHandler.postDelayed(new Runnable() { @Override public void run() { //    () currentCell.setPlayer(-1); //  currentCell.resetCount(); view.drawAtoms(cellX, cellY, 0x000000, 0); } }, 1000); return; } }else if(currentCell.getPlayer()==-1){ currentCell.addAtom(); view.drawAtoms(cellX, cellY, colors[currentPlayer], currentCell.getCountOfAtoms()); currentCell.setPlayer(currentPlayer); }else{ return; } } //       private void delayedAddAtom(final int cellX, final int cellY){ mHandler.postDelayed(new Runnable() { @Override public void run() { addAtom(cellX, cellY); } }, 1000); } //   target   private void selfAddCell(int cellX, int cellY, List<Cell> target){ try{ target.add(cells[cellX][cellY]); }catch (IndexOutOfBoundsException ignore){} }
      
      







コード。



ここでは、Cellオブジェクトの2次元配列を作成するコンストラクターと、セルが眼球への原子で満たされている場合に、シングルタップビューとそれ自体から呼び出されるaddAtomメソッドを確認します。

これで、原子をセルに追加できます。原子がセルに蓄積すると、すぐにばらばらになります。 ただし、この2秒間に原子を追加できます。 isLockフラグ変数とisLock()、lock()、およびunlock()メソッドをGameViewクラスに追加して、これを取り除きます。



また、移動後にプレーヤーの変更を追加し、ゲームの終了をスコアリングして処理する必要があります(すべてのアトムが1人のプレーヤーに属し、各プレーヤーが少なくとも1回移動した場合)。

次のコードをaddAtom()メソッドの最後に追加します



  int[] score=scoring(); if(moveNumber==0){ endTurn(score); }else { //   .      endTurn()   //       int losersCount=0; for(int i=0; i<COUNT_OF_PLAYERS; i++){ if(score[i]==0) losersCount++; } if(losersCount+1==COUNT_OF_PLAYERS){ isEndGame=true; } if(!mHandler.hasMessages(0)){ view.unlock(); endTurn(score); } } } //   private void endTurn(int[] score){ if(!isEndGame){ if(currentPlayer == COUNT_OF_PLAYERS-1){ moveNumber++; currentPlayer=0; }else { currentPlayer++; } }else{ activity.endGame(currentPlayer, score[currentPlayer]); } // ,  ,   ,      activity.setMoveNumber(moveNumber); activity.setPlayerName(currentPlayer); activity.setScore(score); } //  int[] scoring(){ int[] score=new int[COUNT_OF_PLAYERS]; for(int x=0; x<BOARD_WIDTH; x++){ for(int y=0; y<BOARD_HEIGHT; y++){ if(cells[x][y].getPlayer()!=-1){ score[cells[x][y].getPlayer()]+=cells[x][y].getCountOfAtoms(); } } } return score; }
      
      







全コード

メソッドの実装を記述することは残っています

  void setPlayerName(int playerID){} void setScore(int[] score){} void setMoveNumber(int moveNumber){} void endGame(int winnerID, int score){}
      
      







これは宿題になります。 ここではすべてが簡単です。



組み立て後、ファイル





次に、ゲームをカスタマイズするアクティビティを作成する形で、結果のアプリケーションをわずかに仕上げる必要があります(プレイフィールドのサイズ、プレーヤーの数、アトムの色、プレーヤーの名前ですが、これはこの記事のトピックではありません。最終的なコードはgithubで確認できます。



この記事が誰かの役に立つことを願っています。

大きな石を投げないようにお願いします。 私はコードの品質が低いことを認め、たわごとを避けるよう努力することを約束します。 ご清聴ありがとうございました。



PSこれはHabrに関する私の最初の記事です。



All Articles