📜 ⬆️ ⬇️

Writing a game for Android using Canvas

Hi Habr!
Today I want to talk about how to write a simple puzzle game for Android OS using Canvas. I got acquainted with this game about 5 years ago on my phone. The name was forgotten, and searches on several thematic forums resulted in nothing, and I decided to write my own implementation of this game. Development for me is rather a hobby, although sometimes I take on small projects. After some thought, I decided not to use the engine, but to write on my own. The reasons for this decision: the desire to gain experience with Canvas.

The bottom line is ...



There is a playing field like a chessboard, but without dividing the cells into black and white. The size of the field can be arbitrary, but playing on a field smaller than 2x3 does not make much sense. I like to play 10x10 on the pitch.

The number of players can be arbitrary, but in my opinion it is more interesting to play together or four together. If three of us play on a rectangular field, then an imbalance occurs, since one of the players will be distant from the “empty” corner. If you play a company more than 4 people, it is difficult to implement your strategy.
')
Each player in turn must put one object (let's call it an atom) in a free cell or in a cell where its atoms already exist. If a “critical mass” accumulates in the cell, which is equal to the number of neighboring cells, then the atoms from this cell move to the neighboring cells, while the atoms in the neighboring cells are “trapped”, i.e. now they belong to the player whose atoms have scattered.

A couple of pictures to explain the essence.



Empty 4x4 field indicating the critical mass for each cell.


Game situation after the third move.


The game situation after the fourth move (the first go blue). It can be seen that a critical number of atoms has accumulated in the upper left corner.


Bah! They scattered, capturing 2 blue atoms in the cell [0] [1]. And in this cell [0] [1] is now also a critical amount. Chain reaction!


The situation after the expansion. The end of the fourth move. Blue will now make the fifth move.


Implementation. The grafical part.



Let's start the implementation. Create a derived class from 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); } } 


Here I made a bit of bad quality govnod code, namely, in the code, the color of the lines separating the cells of the playing field and the size of the view is considered equal to 300 dp. This size can be obtained from the attrs object of the AttributeSet class, but we will not clutter up the code.

Also immediately throw an Activity in order to make sure that everything is drawn beautifully.

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


And the main.xml markup

 <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> 


Code after this stage.

Now we add the ability to change the scale of the playing field, since there may be misses past the desired cell due to their small size. To do this, we override the methods we need to implement the ScaleGestureDetector.SimpleOnScaleGestureListener of the OnScaleGestureListener interface in our GameView class.

  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; } } 


The resulting code

Note that the boundaries of the magnification value are set (the magnification will be between 1 and 2), and the playing field is scrolled to the zoom point (the point is in the middle between the fingers) with a check to show the area outside the playing field. The scroll to the zoom point (focal point) is performed as follows - the coordinates of the focal point relative to the beginning of the canvas (the upper left corner) are calculated, multiplied by the zoom factor and the coordinates of the point relative to the viewpoint are subtracted. After that, we take the closest value from the interval [0, canvasSize -viewSize] to prevent scrolling outside the playing field.

Now we will write the scrolling, single tap and double tap processing (double tap will return to the original scale of the playing field).

  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; } } 


The resulting code
With a single tap, we calculate the coordinates of the cell by which we taped, for example, the upper left cell will have coordinates of 0.0.

Let's write a method for drawing atoms to drawAtoms. The method parameters are the coordinates of the cell in which we draw atoms, the color and the number of atoms.

  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();//  } 


At the moment, the method has a flaw in the form of absolute values ​​of atomic sizes and distances between them. What can this lead to? When the field size is less than 10x10, the atoms will look small, and with a larger field size they may not fit into the cell.

It remains to add scrollbars. This process is well described here habrahabr.ru/post/120931 .

Before proceeding to the description of the game logic, add a global variable of type GameLogic (our class that describes logic) named logic and add onSingleTapConfirmed to the single-tap processing method
call the future method of logical processing of the addition of a new atom.
logic.addAtom (cellX, cellY);
A setter is also required for this variable.

Code.

Implementation. Game logic.



Create a GameLogic class that describes the logic of the game. We also need the inner Cell class, which stores the cell parameters of the playing field.

  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 class itself

  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){} } 


Code.

Here we see a constructor that creates a two-dimensional array of Cell objects and the addAtom method, called from a single tap view and from itself, provided that the cell is filled with atoms to the eye.
Now you can add atoms to the cells, and when a critical number of atoms accumulate in the cell, they will fly apart in a second. However, it is now possible to add atoms during this second. Get rid of this by adding the isLock flag variable and the isLock (), lock (), and unlock () methods to the GameView class.

You also need to add a player change after the turn, scoring and processing the end of the game (when all atoms belong to one player and each player has completed at least 1 turn).
Add the following code to the end of the addAtom () method

  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; } 


All code
It remains to write the implementation of the methods
  void setPlayerName(int playerID){} void setScore(int[] score){} void setMoveNumber(int moveNumber){} void endGame(int winnerID, int score){} 


This will be homework. It's all trivial.

After assembly process file



Next, you need a little doping of the resulting application in the form of creating an Activity to customize the game (the size of the playing field, the number of players, the color of the atoms and the names of the players, but this is not the topic of this article. You can see the final code on the github , try out the game on Google Play

I hope someone article will help.
Please do not throw big stones. I realize the poor quality of the code and promise to try to avoid govnokoda. Thanks for attention.

PS This is my first article on Habré.

Source: https://habr.com/ru/post/151492/


All Articles