先看效果图:
这是一个比较简易的射击小游戏,后期可以将圆球,炮筒用其它图片来替换,应该可以变得好看一些。我实现这个效果,主要是为了学习和巩固自定义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>