Making greedy snake

Posted by dacio on Wed, 23 Feb 2022 08:35:08 +0100

Imitation of Russian design to make greedy snake

  1. Basic design ideas

    Draw a grid, make a map,

    Four buttons to control the direction, up, down, left and right,

    A snake is composed of a snake body and a snake head - all of which are squares of the same size

    Box attribute - x-y color - can be added when drawing

    The change in the movement xy of the snake up, down, left and right,

    The production of food, snakes eat food;

  2. Problems to be solved

    • Boundary treatment

      The snake's head touches the border and dies or passes through the wall,

    • Meet yourself

      A snake's head touches its body

    • Random production of food

    • The length of food itself increases

    • Up, down, left and right movement processing

  3. Creation of block entity class

    Write the basic attributes and methods of the box. At present, only the basic attributes are written, and the method will be written later.

    public class BlockUnit {
        public static  final int UNIT_SIZE=50; 
        public static final int BEGIN=10;
       // public int color;
        public int x,y;
        public BlockUnit() {
        }
        public BlockUnit( int x, int y) {
            this.x = x;
            this.y = y;
           // this.color = color;
        }
    
  4. Creation of snakes

    Set the basic properties and color of the snake, and the method to obtain the snake

    public class SnakeBlock {
        private static final int SnakeSize=7;
        //private int HandColor,SnakeColo ,x,y;
        private int x,y;
        public SnakeBlock() {
        }
        public SnakeBlock(int x, int y) {
            this.x = x;
            this.y = y;
        }
        public List<BlockUnit> getUnit(int x,int y){
            this.x=x;
            this.y=y;
            return returnSnake();
        }
        private List<BlockUnit> returnSnake() {
            List<BlockUnit> units=new ArrayList<BlockUnit>();
            units.clear();
            units.add(new BlockUnit(x-0*BlockUnit.UNIT_SIZE,y+0*BlockUnit.UNIT_SIZE));
            units.add(new BlockUnit(x-1*BlockUnit.UNIT_SIZE,y+0*BlockUnit.UNIT_SIZE));
            units.add(new BlockUnit(x-2*BlockUnit.UNIT_SIZE,y+0*BlockUnit.UNIT_SIZE));
            units.add(new BlockUnit(x-3*BlockUnit.UNIT_SIZE,y+0*BlockUnit.UNIT_SIZE));
            return units;
        }
    }
    
  5. The design of the main interface of the game is very similar to the Tetris game interface designed before. First, create a brush object and initialize it. Initialize the brush in the construction method,

    First initialize the game map, that is, the background brush, and set the brush color, mode or type. When the brush style is stroke, set the thickness of the brush

    Initialize the snake brush and instantiate the snake class

    public SnakeView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        if (paintWall==null){  //Background brush initialization
            paintWall=new Paint();
            paintWall.setColor(Color.LTGRAY);
            paintWall.setStyle(Paint.Style.STROKE);  //hollow
            paintWall.setStrokeWidth(BOUND_WIDTH_OF_WALL+1);}
        if (paintBlock == null) {// Initialize background wall brush
            paintBlock = new Paint();
            paintBlock.setColor(Color.parseColor("#FF6600")); }
         snakeBlock= new SnakeBlock();}
    
  6. Draw game map in onDraw

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        max_x=getWidth(); //Get the maximum value of the game interface
        max_y=getHeight();
        RectF rel;  //Rectangular class
        num_y=0;
        //Draw game grid
        for (int y=begin;y<max_y-BlockUnit.UNIT_SIZE;y+=BlockUnit.UNIT_SIZE){
            num_x=0;
            for (int x=begin;x<max_x-BlockUnit.UNIT_SIZE;x+=BlockUnit.UNIT_SIZE){
                rel=new RectF(x,y,x+BlockUnit.UNIT_SIZE,y+BlockUnit.UNIT_SIZE);  //Four coordinates of the rectangle
                canvas.drawRoundRect(rel,8,8,paintWall);//Draw a rounded rectangle with an arc of 8
                num_x++;//Record the number of rows and columns of the map
            }
            num_y++;//Record the number of rows and columns of the map
        }
    
  7. Draw the snake's body and set the color of the snake's head to be different from that of the snake's body, which is convenient to distinguish.

       //Draw the body of a snake
            int len = blockUnits.size();
            // Draw square
            for (int i = 1; i < len; i++) {
                int x = blockUnits.get(i).x;
                int y = blockUnits.get(i).y;
              //  int c=blockUnits.get(i).color;
                // Sets the color of the current square
                paintBlock.setColor(Color.BLUE);
                rel = new RectF(x + BOUND_WIDTH_OF_WALL, y + BOUND_WIDTH_OF_WALL,
                        x + BlockUnit.UNIT_SIZE - BOUND_WIDTH_OF_WALL, y + BlockUnit.UNIT_SIZE - BOUND_WIDTH_OF_WALL);
                canvas.drawRoundRect(rel, 8, 8, paintBlock);
                paintBlock.setColor(Color.RED);
                rel = new RectF(blockUnits.get(0).x + BOUND_WIDTH_OF_WALL, blockUnits.get(0).y + BOUND_WIDTH_OF_WALL,
                        blockUnits.get(0).x + BlockUnit.UNIT_SIZE - BOUND_WIDTH_OF_WALL, blockUnits.get(0).y + BlockUnit.UNIT_SIZE - BOUND_WIDTH_OF_WALL);
                canvas.drawRoundRect(rel, 8, 8, paintBlock);
            }
        }
    
  8. Main page layout - the game custom view and a button can be moved by clicking the button snake first, and the default is to move to the right

    <com.example.mysnakegame.SnakeView
        android:id="@+id/sv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginRight="120dp"
        android:layout_marginBottom="200dp"/>
     <Button
         android:id="@+id/gameStart"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentRight="true"
         android:layout_centerVertical="true"
         android:text="start"
         />
    

    In mainactivity Binding controls and event listening in Java

    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
        private Button gameStart;
        private SnakeView snakeView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initView();}
        private void initView() {
            gameStart=(Button) findViewById(R.id.gameStart);
            snakeView=(SnakeView) findViewById(R.id.sv);
            snakeView.setFather(this);
            gameStart.setOnClickListener(this);}
        @Override
        public void onClick(View view) {
            switch (view.getId()){
                case R.id.gameStart:
                    snakeView.gameStart();
                    break;}}}
    

    The event calls the gameStart() method of the custom control. The gameStart() method is set similar to that in the Russian demo to start the thread and enter the loop to make the snake move

    public void gameStart() {
        GameStart=true;
        Running=true;
        if (mainThread==null || !mainThread.isAlive()){
            getSnake();  //Produce a snake
            mainThread=new Thread(new MainThread());  //Create thread object
            mainThread.start();  //Start thread
            // invalidate();
        }
    }
    

    According to the online query, most snakes are divided into snake head, snake body, snake tail, and each part of the snake body is the inflection point. This is a little cumbersome. I set it to let the snake head move, and the remaining blocks inherit the coordinate value of the previous block to form the purpose of moving

    Write the right shift method in the block basic class,

    public static boolean toRight(List<BlockUnit> blockUnits) {
            for (int i = blockUnits.size() - 1; i > 0; i--) {
                blockUnits.get(i).x = blockUnits.get(i - 1).x;
                blockUnits.get(i).y = blockUnits.get(i - 1).y;
            }
            blockUnits.get(0).x += BlockUnit.UNIT_SIZE;
            return true;
    }
    

    Call the shift right method in the thread and constantly redraw to make the snake move

    public class MainThread implements Runnable{
        @Override
        public void run() {
            while(GameStart){
                while(Running){
                            BlockUnit.toRight(blockUnits);
                    father.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            invalidate();}});
                    try {
                        Thread.sleep(300);
                    }catch (InterruptedException e){
                        e.printStackTrace();}} } }}
    
  9. The snake moves up, down, left and right in a certain way, which is similar to the right shift above. Three new buttons are created in the layout, which are up, down and left, respectively binding events for them,

    case R.id.toDown:
        snakeView.toDown();
        break;
    case R.id.toUp:
        snakeView.toUp();
        break;
    case R.id.toLeft:
            snakeView.toLeft();
        break;
    case R.id.toRight:
        snakeView.toRight();
        break;
    
  10. Move method in custom view

    public void toDown(){
                mDirection = DOWN;}
    public void toUp(){
                mDirection = UP;}
    public void toLeft(){
                    mDirection = LEFT;}
    public void toRight(){
                mDirection = RIGHT;}
    

    Loop in thread

    switch (mDirection) {
        case LEFT:
            BlockUnit.toLeft(blockUnits);
            break;
        case RIGHT:
           BlockUnit.toRight(blockUnits);
            break;
        case DOWN:
            BlockUnit.toDown(blockUnits);
            break;
        case UP:
            BlockUnit.toUp(blockUnits);
            break;
    }
    

    Basic methods in block class

    public static boolean toLeft(List<BlockUnit> blockUnits) {
        for (int i = blockUnits.size() - 1; i > 0; i--) {
            blockUnits.get(i).x = blockUnits.get(i - 1).x;
            blockUnits.get(i).y = blockUnits.get(i - 1).y;}
        blockUnits.get(0).x -= BlockUnit.UNIT_SIZE;
        return true;}
    public static boolean toRight(List<BlockUnit> blockUnits) {
            for (int i = blockUnits.size() - 1; i > 0; i--) {
                blockUnits.get(i).x = blockUnits.get(i - 1).x;
                blockUnits.get(i).y = blockUnits.get(i - 1).y;}
            blockUnits.get(0).x += BlockUnit.UNIT_SIZE;
            return true;}
    public static boolean toUp(List<BlockUnit> blockUnits) {
        for (int i = blockUnits.size() - 1; i > 0; i--) {
            blockUnits.get(i).x = blockUnits.get(i - 1).x;
            blockUnits.get(i).y = blockUnits.get(i - 1).y;}
        blockUnits.get(0).y -= BlockUnit.UNIT_SIZE;
        return true;}
    public static boolean toDown(List<BlockUnit> blockUnits) {
        for (int i = blockUnits.size() - 1; i > 0; i--) {
            blockUnits.get(i).x = blockUnits.get(i - 1).x;
            blockUnits.get(i).y = blockUnits.get(i - 1).y;}
        blockUnits.get(0).y += BlockUnit.UNIT_SIZE;
        return true;}
    
  11. Limit the boundary, set the judgment statement to touch the boundary, and the game ends

    Write a judgment method in the basic class of the box. In the loop, judge before moving, return true to execute the move statement, and return false to call the end method to end the game.

    public static boolean canMove(List<BlockUnit> blockUnits ,int max_x,int max_y ) {
       int x=blockUnits.get(0).x;
       int y=blockUnits.get(0).y;
        if (x+BlockUnit.UNIT_SIZE>=max_x ||x-BlockUnit.UNIT_SIZE<=0 || y+BlockUnit.UNIT_SIZE>=max_y || y-BlockUnit.UNIT_SIZE<=0){
            return false;
        }
        for (int i=1;i<blockUnits.size();i++){
            if (x==blockUnits.get(i).x&&y==blockUnits.get(i).y){
                return false;
            }
        }
        return true;
    }
    
  12. For the modification of small defects, the direction of snake head will be modified by the button before the game starts. It is set that the button will not work when the game does not start

    Add judgment before the statement of changing the direction of the snake head. If the game is not started and running, the direction of the snake head cannot be changed.

  13. Food production, food is randomly generated on the map

    Create food classes, create methods to get food,

    public class Food {
        private int begin=10;
        private int x,y;
        public Food() {}
        public Food(int x, int y) {
            this.x = x;
            this.y = y;}
        public List<BlockUnit> getFood(List<BlockUnit> blockUnits,int num_x,int num_y){
            List<BlockUnit> food=new ArrayList<BlockUnit>();
            int x=(int)(Math.random()*num_x)*BlockUnit.UNIT_SIZE+begin;
            int y=(int)(Math.random()*num_y)*BlockUnit.UNIT_SIZE+begin;
            Log.i("food.x",String.valueOf(x));
            Log.i("food.y",String.valueOf(y));
            food.clear();
            food.add(new BlockUnit(x,y));
           return food;}}
    

    Call in the game start method, automatically create a food at the beginning of the game.

  14. After creating food, you need to judge whether the food produced does not coincide with the snake;

    Add judgment to the method of obtaining food. If it coincides, call the method again until it does not coincide

    for (int i=0; i<blockUnits.size();i++){
        if (x==blockUnits.get(i).x&&y==blockUnits.get(i).y)
            getFood(blockUnits,num_x,num_y);
    }
    
  15. Snake eating food event

    Judgment: when the snake head coincides with the food, it means that the snake has eaten the food. The creation of the judgment method requires the snake head data and food data, so two parameters are required temporarily, and they can be added later if necessary.

    public static boolean EatFood(List<BlockUnit> blockUnits,List<BlockUnit> foodUnit) {
        int bx=blockUnits.get(0).x;
        int by=blockUnits.get(0).y;
        int fx=foodUnit.get(0).x;
        int fy=foodUnit.get(0).y;
        if (bx==fx &&by==fy){
            return true;
        }
        return false;
    }
    

    After the snake eats the food, the original food disappears, obtains a new food, and the snake's body needs to be increased by one.

    How to add snakes:

    public List<BlockUnit> addUnit(List<BlockUnit> blockUnits){
        int x=blockUnits.get(blockUnits.size()-1).x;
        int y=blockUnits.get(blockUnits.size()-1).y;
        blockUnits.add(new BlockUnit(x,y));
        return blockUnits;
    }
    

    Then it is realized in the thread. When eating food, the food disappears and a new food is created. The length of the snake's body is added by one. Here we add the tail.

    if (BlockUnit.EatFood(blockUnits,foodUnits)){
        Log.i("eat","Eat food");
        foodUnits.clear();
        father.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                getFood();
               blockUnits=snakeBlock.addUnit(blockUnits);
               invalidate();
            }
        });
    }
    
  16. Pop up processing at the end of the game

    Judge whether to continue the game by pop-up after the game

    father.runOnUiThread(new Runnable() {
        @Override
        public void run() {
           gamePaush();
          //  Log.i("1234","1111");
            new AlertDialog.Builder(father).setIcon(R.drawable.snake_icon_128)
                    .setTitle("You're dead!! game over")
                    .setMessage("Do you want to start over??")
                    .setNegativeButton("cancel", new DialogInterface.OnClickListener(){
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i){
                            gameOver();} })
                    .setPositiveButton("determine", new DialogInterface.OnClickListener(){
                        @Override
                        public void onClick(DialogInterface dialogInterface, int i){
                            gameStart();}
                    }).create().show();}});
    

    There is an error in the operation. Click OK to confirm that the game does not restart, but the pop-up window appears again. After searching, it is found that the snake list has not been cleared, so there is an error.

    In addition, there are still problems after clearing the snake list, and there is no restart. Emptying the food list and running again still failed. It is speculated that it is a thread problem, which will be solved later

  17. End of the game, pause, continue button implementation

    • game over

      Add a button to the layout. No code will be posted here

      Control binding and event binding,

      For the writing of the game end method, first stop the game, release the thread and refresh the page

      public void gameOver(){
         // Stop the game and release the main thread of the game
         Running= false;
         GameStart = false;
         mainThread.interrupt();
         blockUnits.clear();
        // bottomBlockUnits.clear();
        // score = 0;
         invalidate();}
      
    • Game pause

      When the game is suspended, set the running flag bit to false and a window pops up. Click Cancel to end the game. Click OK to call the continue method and set the running flag bit to true

      public void gamePaush(){
          Running=false;
          new AlertDialog.Builder(father)
          .setIcon(R.drawable.snake_icon_128)
                  .setTitle("Game pause!!!!")
                  .setMessage("The game is suspended. Do you want to continue")
                  .setNegativeButton("cancel", new DialogInterface.OnClickListener() {
                      @Override
                      public void onClick(DialogInterface dialogInterface, int i) {
                          gameOver();}})
                  .setPositiveButton("determine", new DialogInterface.OnClickListener() {
                      @Override
                      public void onClick(DialogInterface dialogInterface, int i) {
                          gameContinue();}
                  }).create().show();}
      
  18. Score records, eating a food record is very important

    And update the game home page to increase the score after judging that the food is eaten

    if (BlockUnit.EatFood(blockUnits,foodUnits)){
        score+=10;
        Log.i("eat","Eat food");
        foodUnits.clear();
        father.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                father.tvScore.setText(""+score);
                getFood();
               blockUnits=snakeBlock.addUnit(blockUnits);
               invalidate();}});}
    

    Clear the score after the game decision

  19. For the storage and reading of the highest score, use the lightweight storage class SharePreferences provided by Android to save data in XML format. First, create the auxiliary class SharedHelper of SharedPreferences to define the saving and reading methods

     //Define a method to save data
        public void save(String key, String value) {
            SharedPreferences sp = mContext.getSharedPreferences("score", Context.MODE_PRIVATE);
            SharedPreferences.Editor editor = sp.edit();
            editor.putString(key,value);
            editor.commit();
        }
        //Define a method to read SP files
        public String read(String key ) {
            Map<String, String> data = new HashMap<String, String>();
            SharedPreferences sp = mContext.getSharedPreferences("score", Context.MODE_PRIVATE);
            return sp.getString(key,"");
        }
    
  20. In mainactivity The onStart method is created in Java, and the highest score is read at the beginning of the Activity.

    @Override
    protected void onStart() {
        super.onStart();
        sh=new SharedHelper(MainActivity.this);
        String max_score = sh.read("max_score");
        MaxScore.setText(max_score);
        try {
            mScore = Integer.parseInt(max_score);
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
    }
    
  21. At the end of the game, the score obtained by the game will be compared and replaced with the highest score

    Log.i("mScore", String.valueOf(father.mScore));
    if (father.mScore<score){
        father.setMAX_score(score);
        Toast.makeText(father,"The highest score has been replaced",Toast.LENGTH_LONG).show();
    }
    
  22. For level adjustment, the level of each game will be set to zero. When the snake's body is longer and longer, the level will increase. The increase rule is to update when the score reaches a multiple of 100

    father.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            int dengji=score/100;
            father.Dengji.setText(" "+dengji);
        }
    });
    
  23. Speed update

    Set the speed gear, change the sleep time of the thread, and change the moving speed of the snake,

     speed=score/50;
    father.Speed.setText(""+speed);
    
    switch (speed){
        case 0:
            Thread.sleep(300);
            break;
        case 1:
            Thread.sleep(290);
            break;
        case 2:
            Thread.sleep(270);
            break;
        case 3:
            Thread.sleep(240);
            break;
        case 4:
            Thread.sleep(210);
            break;
        case 5:
            Thread.sleep(180);
            break;
        default:
            Thread.sleep(120);
            break;}
    
  24. So far, the greedy snake design is over. Although some small bug s have not been solved, the basic functions and game logic have been completed.

Topics: Android