先看效果图:

Android 自定义画射击靶 自定义射击游戏_安卓

这是一个比较简易的射击小游戏,后期可以将圆球,炮筒用其它图片来替换,应该可以变得好看一些。我实现这个效果,主要是为了学习和巩固自定义View的一些知识点。下面我来讲述一下本游戏的设计思路

从图上我们可以看到,我们需要一个炮筒,炮筒里可以发出许多的子弹,然后天上有很多的敌人,我们需要用子弹去碰撞到敌人,从而达到消灭敌人的效果。所有我们首先就需要有炮筒,子弹,敌人这三个类

大炮类:

public class Artillery {
    private Matrix matrix;  //大炮的变换矩阵
    private Paint paint;    //大炮的画笔
    private Bitmap bitmap;  //大炮的图片
    private int centerX,centerY;    //大炮中心点
    public Artillery(Matrix matrix, Paint paint, Bitmap bitmap) {
        this.matrix = matrix;
        this.paint = paint;
        this.bitmap = bitmap;
    }

    public Matrix getMatrix() {
        return matrix;
    }

    public void setMatrix(Matrix matrix) {
        this.matrix = matrix;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPaint(Paint paint) {
        this.paint = paint;
    }

    public Bitmap getBitmap() {
        return bitmap;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    public int getCenterX() {
        return centerX;
    }

    public void setCenterX(int centerX) {
        this.centerX = centerX;
    }

    public int getCenterY() {
        return centerY;
    }

    public void setCenterY(int centerY) {
        this.centerY = centerY;
    }

    public void setCenter(int centerX, int centerY){
        this.centerX=centerX;
        this.centerY=centerY;
    }
}

因为子弹是从大炮的中心发出的,所以在大炮类中我们需要记录大炮的中心点(centerX,centerY)

子弹类:

public class Bullet {
    private Paint paint;    //子弹的画笔
    private int radius;     //子弹的半径
    private float moveStep; //每次移动的步长

    public Bullet(Paint paint,int radius,int moveStep){
        this.paint=paint;
        this.radius=radius;
        this.moveStep=moveStep;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPaint(Paint paint) {
        this.paint = paint;
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }

    public float getMoveStep() {
        return moveStep;
    }

    public void setMoveStep(float moveStep) {
        this.moveStep = moveStep;
    }
}

敌人类:

public class Enemy {
    private Paint paint;        //敌人的画笔
    private float moveStep;     //敌人每次移动的步长
    private int radius;         //敌人的半径(圆)
    public Enemy(Paint paint, int radius, float moveStep) {
        this.paint = paint;
        this.moveStep = moveStep;
        this.radius=radius;
    }

    public Paint getPaint() {
        return paint;
    }

    public void setPaint(Paint paint) {
        this.paint = paint;
    }

    public float getMoveStep() {
        return moveStep;
    }

    public void setMoveStep(float moveStep) {
        this.moveStep = moveStep;
    }

    public int getRadius() {
        return radius;
    }

    public void setRadius(int radius) {
        this.radius = radius;
    }
}

由图上可以发现,大炮会随着我们的点击发生旋转,这里我们需要计算一下,假设我们点击的坐标为P0:(x0,y0),大炮的中心点为P1:(x1,y1),那么P0,P1连成的直线与水平轴会产生一个夹角θ。这个θ就是我们想要大炮旋转的角度

直线P0P1的斜率:tanθ=(y1-y0)/(x0-x1)

注意在android里面,是以屏幕左上方为坐标原点,且以屏幕下方为y轴正方向,越往屏幕下方y值越大。而我们这里是以大炮中心点为原点建立的坐标系来进行计算的。所以是y1-y0

根据斜率公式,就可以得到θ=arctan((y1-y0)/(x0-x1))

相应的在代码中的写法如下:

Math.toDegrees(Math.atan((arti.getCenterY()-event.getY())/(event.getX()-arti.getCenterX())));

其中arti是大炮对象,P0为(arti.getCenterX(),arti.getCenterY()),P1为(event.getX(),event.getY())

atan()是反三角计算方法,返回值为弧度

toDegrees()是将弧度转化为角度

我们大炮的旋转问题解决了,然后我们需要去实现点击某一位置时,生成一颗子弹,并让子弹打向那个位置的功能

1、点击监听的方法是onTouchEvent,我们重写这个方法以后就可以获取点击时的坐标

2、由于我可以不断的点击,屏幕里会出现非常多的子弹,每个子弹应该有他自己的位置和运动方向。所以,我们要创建一个新的类MyPoint,来记录每个点的位置和运动方向(之后的敌人位置也是使用这个类)

MyPoint类:

public class MyPoint {
    private int x;
    private int y;
    private double angle;   //角度

    public MyPoint(int x, int y, double angle) {
        this.x = x;
        this.y = y;
        this.angle = angle;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public double getAngle() {
        return angle;
    }

    public void setAngle(double angle) {
        this.angle = angle;
    }

    public void set(int x,int y){
        this.x=x;
        this.y=y;
    }

    //点移动的方法
    /*
    * moveStep:步长
    * boundWidth:点所有在区域的宽度
    * boundHeight:点所在区域的高度
    * */
    public void move(float moveStep,boolean isEnemy){
        double moveY=0,moveX=0;
        if(getAngle()>=0){  //子弹在右上方
            moveX=moveStep*Math.cos(getAngle());
            moveY=moveStep*Math.sin(getAngle());
        }
        else{               //子弹在左上方(角度为负数)
            moveX=-moveStep*Math.cos(-getAngle());
            moveY=moveStep*Math.sin(-getAngle());
        }
        if(!isEnemy)set((int)(getX()+moveX),(int)(getY()-moveY));
        else set((int)(getX()+moveX),(int)(getY()+moveY));
    }

    //是否离开该区域
    public boolean isOutOfBounds(int boundWidth,int boundHeight){
        if(getX()>boundWidth || getX()<0 )return true;
        else if(getY()>boundHeight || getY()<0)return true;
        else return false;
    }

    //是否离开该区域,忽略顶部
    public boolean isOutOfBoundsWithOutTop(int boundWidth,int boundHeight){
        if(getX()<0 || getX()>boundWidth)return true;
        else if(getY()>boundHeight)return true;
        else return false;
    }

    //是否发生碰撞
    public boolean isCollider(MyPoint point,int bulletRadius,int enemyRadius){
        //两个圆的圆心的距离小于两个圆的半径之和时,说明两个圆发生碰撞
        return (getX()-point.getX())*(getX()-point.getX())+(getY()-point.getY())*(getY()-point.getY()) <= (bulletRadius+enemyRadius)*(bulletRadius+enemyRadius);
    }
}

这里用角度angle来代替运动方向

所有子弹的初始位置都是大炮的中心点(arti.getCenterX(),arti.getCenterY())

 我们用一个List集合(bulletPoints)来存放所有的点。

//将点击的位置放入点的集合中
bulletPoints.add(new MyPoint(arti.getCenterX(),arti.getCenterY(),Math.toRadians(currentRotate)));

3、我们在onDraw()绘制方法里循环遍历bulletPoints集合,画出每一颗子弹,并修改下一次运动到的位置。

for(int i=0;i<bulletPoints.size();i++){ //移动所有的点
        canvas.drawCircle(bulletPoints.get(i).getX(),bulletPoints.get(i).getY(),bullet.getRadius(),bullet.getPaint());
        bulletPoints.get(i).move(bullet.getMoveStep(),false);
    }
//点移动的方法
/*
* moveStep:步长
* isEnemy:是否为敌人的位置
* */
public void move(float moveStep,boolean isEnemy){
    double moveY=0,moveX=0;
    if(getAngle()>=0){  //子弹在右上方
        moveX=moveStep*Math.cos(getAngle());
        moveY=moveStep*Math.sin(getAngle());
    }
    else{               //子弹在左上方(角度为负数)
        moveX=-moveStep*Math.cos(-getAngle());
        moveY=moveStep*Math.sin(-getAngle());
    }
    if(!isEnemy)set((int)(x+moveX),(int)(y-moveY));
    else set((int)(x+moveX),(int)(y+moveY));
}

public void set(int x,int y){
    this.x=x;
    this.y=y;
}

我方的子弹已经生成完毕,最后,让我们生成敌人。

1、我们先写一个生成敌人的方法,并让它每隔一段时间被调用一次:

private Runnable spawnRunnable=new Runnable() {
    @Override
    public void run() {
        instantiateEnemy();
    }
};

//生成敌人
private void instantiateEnemy(){
    //将敌人位置保存到集合中
    enemyPoints.add(new MyPoint(positionRand.nextInt(Constants.SCREEN_WIDTH),-5,Math.toRadians(-90)));
    isSpawning=false;
}

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //......
    //如果敌人未到达最大数量,并且不在生成敌人,则继续生成敌人
    if(enemyPoints.size()<maxEnemyNum&&!isSpawning){
        isSpawning=true;
        postDelayed(spawnRunnable,300);
    }
    //......
}

2、敌人移动的方法(与子弹的方法一样):

for(int i=0;i<enemyPoints.size();i++){
    canvas.drawCircle(enemyPoints.get(i).getX(),enemyPoints.get(i).getY(),enemy.getRadius(),enemy.getPaint());
    enemyPoints.get(i).move(enemy.getMoveStep(),true);
}

3、敌人与子弹碰撞的方法:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //......
    //是否发生碰撞
    for(int j=0;j<enemyPoints.size();j++){
        //如果子弹和敌人发生碰撞
        if(bulletPoints.get(i).isCollider(enemyPoints.get(j),bullet.getRadius(),enemy.getRadius())){
            //移除子弹
            bulletPoints.remove(i--);
            //移除敌人
            enemyPoints.remove(j);
            //发生监听事件
            if(beatEnemyListener!=null)
                beatEnemyListener.onBeatEnemy();
            break;
        }
    }
    //......
}

//是否发生碰撞
/*
* point:另一个点的位置
* bulletRaidus:子弹的半径
* enemyRadius:敌人的半径
* */
public boolean isCollider(MyPoint point,int bulletRadius,int enemyRadius){
    //两个圆的圆心的距离小于两个圆的半径之和时,说明两个圆发生碰撞
    return (x-point.getX())*(x-point.getX())+(y-point.getY())*(y-point.getY()) <= (bulletRadius+enemyRadius)*(bulletRadius+enemyRadius);
}

最后,呈上完整自定义View源码:

import android.content.Context;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.hualinfo.bean.circlecollider.Artillery;
import com.hualinfo.bean.circlecollider.Bullet;
import com.hualinfo.bean.circlecollider.MyPoint;
import com.hualinfo.bean.circlecollider.Enemy;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import androidx.annotation.Nullable;

public class MyCircleColliderView extends View {
    private Bullet bullet;  //子弹对象
    private Artillery arti; //大炮对象
    private Enemy enemy;    //敌人对象
    private int maxEnemyNum=30; //敌人的最大数量
    private boolean isSpawning=false;   //正在生成敌人
    private float currentRotate=-90;    //当前大炮的旋转方向
    private List<MyPoint> bulletPoints=new ArrayList<>(); //每一个子弹的坐标点
    private List<MyPoint> enemyPoints=new ArrayList<>();  //每一个敌人的坐标点
    private Random positionRand=new Random();
    private BeatEnemyListener beatEnemyListener;
    public MyCircleColliderView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init(){
        arti=new Artillery(new Matrix(),new Paint(),BitmapFactory.decodeResource(getResources(),R.mipmap.arti));
        arti.setCenter(Constants.SCREEN_WIDTH/2,Constants.SCREEN_HEIGHT-arti.getBitmap().getHeight()/2);
        Paint bulletPaint=new Paint();
        bulletPaint.setColor(Color.RED);
        bullet=new Bullet(bulletPaint,25,20);
        Paint enemyPaint=new Paint();
        enemyPaint.setColor(Color.BLUE);
        enemy=new Enemy(enemyPaint,25,2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        arti.getMatrix().reset();
        arti.getMatrix().postTranslate(arti.getCenterX()-arti.getBitmap().getWidth()/2,arti.getCenterY()-arti.getBitmap().getHeight()/2);
        arti.getMatrix().postRotate(-currentRotate,arti.getCenterX(),arti.getCenterY());
        canvas.drawBitmap(arti.getBitmap(),arti.getMatrix(),arti.getPaint());
        for(int i=0;i<bulletPoints.size();i++){ //移动所有的点
            canvas.drawCircle(bulletPoints.get(i).getX(),bulletPoints.get(i).getY(),bullet.getRadius(),bullet.getPaint());
            bulletPoints.get(i).move(bullet.getMoveStep(),false);
            //是否发生碰撞
            for(int j=0;j<enemyPoints.size();j++){
                //如果子弹和敌人发生碰撞
                if(bulletPoints.get(i).isCollider(enemyPoints.get(j),bullet.getRadius(),enemy.getRadius())){
                    //移除子弹
                    bulletPoints.remove(i--);
                    //移除敌人
                    enemyPoints.remove(j);
                    //发生监听事件
                    if(beatEnemyListener!=null)
                        beatEnemyListener.onBeatEnemy();
                    break;
                }
            }
        }
        for(int i=0;i<bulletPoints.size();i++){ //移除离开屏幕区域的点
            if(bulletPoints.get(i).isOutOfBounds(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT)){
                bulletPoints.remove(i--);
            }
        }

        for(int i=0;i<enemyPoints.size();i++){
            canvas.drawCircle(enemyPoints.get(i).getX(),enemyPoints.get(i).getY(),enemy.getRadius(),enemy.getPaint());
            enemyPoints.get(i).move(enemy.getMoveStep(),true);
        }
        for(int i=0;i<enemyPoints.size();i++){ //移除离开屏幕区域的点
            if(enemyPoints.get(i).isOutOfBoundsWithOutTop(Constants.SCREEN_WIDTH,Constants.SCREEN_HEIGHT)){
                enemyPoints.remove(i--);
            }
        }
        //如果敌人未到达最大数量,并且不在生成敌人,继续生成敌人
        if(enemyPoints.size()<maxEnemyNum&&!isSpawning){
            isSpawning=true;
            postDelayed(spawnRunnable,300);
        }
        postInvalidateDelayed(5);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //计算水平轴与 大炮中心和点击的位置连成的直线 之间的夹角
                currentRotate=(float) Math.toDegrees(Math.atan((arti.getCenterY()-event.getY())/(event.getX()-arti.getCenterX())));
                //将点击的位置放入点的集合中
                bulletPoints.add(new MyPoint(arti.getCenterX(),arti.getCenterY(),Math.toRadians(currentRotate)));
                break;
        }
        return true;
    }

    //生成敌人
    private void instantiateEnemy(){
        //将敌人位置保存到集合中
        enemyPoints.add(new MyPoint(positionRand.nextInt(Constants.SCREEN_WIDTH),-5,Math.toRadians(-90)));
        isSpawning=false;
    }

    private Runnable spawnRunnable=new Runnable() {
        @Override
        public void run() {
            instantiateEnemy();
        }
    };

    //设计击中敌人时的监听
    public interface BeatEnemyListener{
        void onBeatEnemy();
    }

    public void setBeatEnemyListener(BeatEnemyListener listener){
        this.beatEnemyListener=listener;
    }
}

Activity类:

public class MyCircleColliderAct extends AppCompatActivity {
    private MyCircleColliderView colliderView;
    private TextView tv_score;
    private int score=0;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_circle_collider);
        init();
    }

    private void init(){
        tv_score=findViewById(R.id.tv_score);
        colliderView=findViewById(R.id.collider_view);
        colliderView.setBeatEnemyListener(new MyCircleColliderView.BeatEnemyListener() {
            @Override
            public void onBeatEnemy() {
                tv_score.setText("干掉了"+(++score)+"个敌人");
            }
        });
    }
}

xml布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <TextView
        android:id="@+id/tv_score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="干掉0个敌人"
        android:textSize="25sp"
        android:layout_centerInParent="true"
        android:textColor="@color/black"/>

    <com.hualinfo.myviewtext.MyCircleColliderView
        android:id="@+id/collider_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>