Android 飞机小游戏(自定义ViewGroup+View)

源码
体验App(密码8686)

先上效果图


Android 飞机小游戏(自定义ViewGroup+View)_第1张图片
效果图

思路

  1. 能移动的飞行物(View)
  2. 容纳飞行物的容器 (ViewGroup)
  3. 控制飞行物在容器中的位置、移动、以及碰撞检测

飞行物

首先捋一捋创建一个飞行物所需要的属性

  1. 类型(我的飞机、敌机、我的子弹、敌机子弹、BOSS飞机、BOSS子弹,为了方便我把爆炸效果也归类到飞行物中)
  2. 视图(View)
  3. 坐标
  4. 宽高
  5. 血量
  6. 威力(敌机撞到我,敌机子弹撞到我等等......)
  7. 飞行物的死亡标记
  8. 飞行物的飞行速度
  9. 在内部算出他的中心X和中心Y
  10. context

好的,捋清楚了可以开始写了

首先创建飞行物的基类Fly

abstract class Fly {
    //飞行物类型
    var flyType: FlyType
    //飞行物展示的view
    var view: View
    //飞行物的的偏移值 xy
    var x = 0F
        set(value) {
            view.x = value
            cx = (view.x + w / 2)
            field = value
        }
    var y = 0F
        set(value) {
            view.y = value
            cy = (view.y + h / 2)
            field = value
        }
    //飞行物的宽高
    var w = 0
    var h = 0
    //飞行物的血量
    var HP = 100
    //飞行物的碰撞威力
    var power = 100
    //这个飞行物是否已经死亡
    var isBoom = false
    //飞行物的移动速度 越小越快
    var speed = 1
    //上下文
    var context: Context
    //飞行物的中心点
    var cx = 0F
    var cy = 0F
}

这里飞行物基类创建好了之后开始派生子类

  1. 飞机(Plane.kt
  2. 子弹(Bullet.kt
  3. 爆炸(Boom.kt

创建飞行物的枚举类型FlyType

enum class FlyType {
    PLANE_GCD,  //我军飞机
    PLANE_GMD,  //敌军飞机
    PLANE_BOSS, //boss飞机
    BULLET_GCD, //我军子弹
    BULLET_GMD, //敌军子弹
    BULLET_BOSS,//boss子弹
    BOOM, //爆炸类型
    OTHER //预留
}

创建飞行物的视图(View)
作图是不可能作图的,这辈子都不可能作图,只能靠onDraw维持生活这样子

  1. 飞机PlaneView (我机和敌机都用这个view,如果是敌机就给他canvas.rotate(180)
    竖着draw一个rect为飞机主体,横着draw两个rect为飞机的翅膀 ,再在尾部绘制火焰效果(火焰效果看这里,飞机就完成啦(点我看图),代码我就不贴了。

  2. 子弹BulletView (我机和敌机一样都用这个View)
    竖着draw一个rect为子弹主体,横着draw一个rect为子弹尾翼,再在尾部绘制火焰效果

  3. 爆炸效果BoomView
    实现的思路就是在view的中心绘制12个rect,之后用动画将这个12个rect沿着同一个方向移动,
    在onDraw的过程中,每绘制一个之后,canvas.rotate(30),这样就有扩散效果了

Android 飞机小游戏(自定义ViewGroup+View)_第2张图片
画好了的样子

容器

MapView

class MapView : ViewGroup {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)

    private var w = 0
    private var h = 0
    private var loadFinish = false
    var onMeasureFinishListener: OnViewLoadFinishListener? = null

    /**
     * 指定飞行物的位置
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child != null) {
                val childW = child.measuredWidth
                val childH = child.measuredHeight
                child.layout(0, 0, childW, childH)
            }

        }
    }

    /**
     * 测量
     */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        measureChildren(widthMeasureSpec, heightMeasureSpec)
        w = measuredWidth
        h = measuredHeight
        if (!loadFinish) {
            onMeasureFinishListener?.onFinish(w, h)
            loadFinish = true
        }
    }

    /**
     * 添加飞行物
     */
    fun addFly(fly: Fly) {
        addView(fly.view)

    }

    /**
     * 删除飞行物
     */
    fun removeFly(fly: Fly) {
        removeView(fly.view)
    }

    interface OnViewLoadFinishListener {
        fun onFinish(w: Int, h: Int)
    }

}

控制

首先提供一个简单的工厂类来获取飞机、子弹、爆炸

object FlyFactory{
  /**
     * 获取飞机的Fly
     */
    fun getPlane(context: Context, flyType: FlyType): Plane?{......}
 /**
     * 获取子弹的Fly
     */
    fun getBullet(context: Context, flyType: FlyType): Bullet?{......}
  /**
     * 获取爆炸效果的Fly
     */
    fun getBoom(context: Context, flyType: FlyType): Boom? {......}
  /**
     * 获取爆炸View
     */
    private fun getBoomView(context: Context, flyType: FlyType): View?{......}
  /**
     * 获取飞机或子弹的View
     */
    private fun getView(context: Context, flyType: FlyType): View?{......}
}

主要的逻辑控制类FlyController

飞行物管理的集合

为了避免ConcurrentModificationException异常,这里用CopyOnWriteArrayList

      /**
     * 我军飞机
     */
    private var gcdPlanes: CopyOnWriteArrayList = CopyOnWriteArrayList()
    /**
     * 敌军飞机集合
     */
    private var gmdPlanes: CopyOnWriteArrayList = CopyOnWriteArrayList()
    /**
     * 我军子弹集合
     */
    private var gcdBullets: CopyOnWriteArrayList = CopyOnWriteArrayList()
    /**
     * 敌军子弹集合
     */
    private var gmdBullets: CopyOnWriteArrayList = CopyOnWriteArrayList()

创建飞机

    private fun createPlane(flyType: FlyType) {
        val plane = FlyFactory.getPlane(activity, flyType)!!
        if (flyType == FlyType.PLANE_GCD) {
            //我的飞机的y在屏幕的最下方,x在屏幕中心位置
            plane.x = w / 2F - plane.w / 2
            plane.y = h - plane.h.toFloat()
        } else if (flyType == FlyType.PLANE_GMD) {
            //敌机y在屏幕的最上方,x则是容器的w以内的随机位置
            plane.x = (w - plane.w) * random.nextFloat()
            plane.y = -plane.h * 2F
        } 
    }

添加飞机

/**
     * 添加Fly到List和mapView
     */
    private fun addFly(fly: Fly) {
        when (fly.flyType) {
            FlyType.PLANE_GCD -> {
                gcdPlanes.add(fly)
            }
            FlyType.PLANE_GMD -> {
                gmdPlanes.add(fly)
            }
            ......省略代码
        }
        mapView.addFly(fly)
      
    }

删除飞行物

    /**
     * 从集合和mapView中删除fly
     */
    private fun removeFly(fly: Fly) {
        when (fly.flyType) {
            FlyType.PLANE_GMD -> {
                createPlane(FlyType.PLANE_GMD)
                gmdPlanes.remove(fly)
            }
            FlyType.PLANE_GCD -> {
                gcdPlanes.remove(fly)
            }
            ......省略代码
        }
        mapView.removeFly(fly)
       //删除的时候标记为死亡状态
        fly.boom()
    }

创建子弹

   /**
     *子弹的初始位置为发射子弹飞机的当前位置,所以需要传入plane
     */
    private fun createBullet(plane: Plane) {
        val bullet = FlyFactory.getBullet(activity, plane.flyType)!!

        bullet.x = (plane.cx - bullet.w / 2)

        bullet.y = (plane.cy - bullet.h / 2)
 
    }

启动线程为飞机创建子弹

    /**
     * 我的飞机自动发射子弹线程
     */
    private fun startGcdPlaneShotThread() {
        Thread {
            while (true) {
                activity.runOnUiThread {
                    if (gcdPlanes.size != 0) {
                        shot(gcdPlanes[0] as Plane)
                    }
                }
                SystemClock.sleep(200)
            }
        }.start()
    }

    /**
     * 敌机自动发射子弹线程
     */
    private fun startGmdPlaneShotThread() {
        Thread {
            while (true) {
                //遍历敌机集合,并随机确定是否为飞机创建子弹
                gmdPlanes.forEach {
                    activity.runOnUiThread {
                        if (random.nextInt(3) % 3 == 0 )
                            shot(it as Plane)
                    }
                }
                SystemClock.sleep(2000)

            }
        }.start()
    }

移动飞机和子弹

/**
     * y轴移动Fly
     */
    private fun moveFlyY(fly: Fly) {
        //如果fly已经死亡
        if (fly.isBoom) {
            return
        }
        //移动的起始位置是自己的y
        var start = fly.cy
       //移动的目标位置,如果是敌机和敌机子弹,则是屏幕的最下方再加自身两个高度 ,我的子弹则相反
        var end = when (fly.flyType) {
            FlyType.BULLET_GCD -> {
                -fly.h * 2
            }
            FlyType.PLANE_GCD -> {
                return
            }
            FlyType.PLANE_GMD -> {
                h + fly.h * 2
            }
            FlyType.BULLET_GMD -> {
                h + fly.h * 2
            }
        ......省略代码
        }

        ValueAnimator.ofFloat(start, end.toFloat())
            .apply {
                addUpdateListener {
                    //在动画过程中,如果经过碰撞检测判定飞行器已经死亡,则马上取消动画
                    if (fly.isBoom) {
                        cancel()
                        return@addUpdateListener
                    }

                    //更改飞行物的y值以移动飞行物
                    fly.y = (it.animatedValue as Float)

                   ......省略代码
                }
                //动画的时间取值是起始距离到结束距离的值作为毫秒数,再乘以飞行器的速度
                //speed越高则飞行越慢,在之前给敌机的speed赋值的时候可以使用随机数,让敌机飞行有快有慢
                duration = (abs(start - end)).toLong() * fly.speed
                interpolator = LinearInterpolator()
                start()
            }
    }

飞出屏幕检测和碰撞检测

经过上面过程之后,飞机和子弹就动起来了。接下来在动画中,需要做飞出屏幕的检测和碰撞检测

首先飞出屏幕检测

/**
     * 飞行物位置检测 如果已经超出屏幕则删除
     */
    private fun checkFlyPosition(fly: Fly): Boolean {
       //如果view已经不再屏幕内了 删除它
        if (fly.x + fly.w <= -fly.w ||

            fly.x >= w + fly.w ||

            fly.y + fly.h <= -fly.h ||

            fly.y >= h + fly.h
        ) {
            removeFly(fly)
            return false
        }
        return true
    }

碰撞检测

    /**
     * 通过flyType来决定选择哪个集合中的元素与它进行碰撞检测
     */
    private fun selectHitAndBeHit(fly: Fly) {
        when (fly.flyType) {
            FlyType.BULLET_GCD -> {
                //如果是我的子弹,则与敌机集合检测
                hitAndBeHit(fly, gmdPlanes)
            }
            FlyType.BULLET_GMD -> {
                //如果是敌机的子弹则与我机检测
                hitAndBeHit(fly, gcdPlanes)
            }
            FlyType.PLANE_GMD -> {
                //...
                hitAndBeHit(fly, gcdPlanes)
            }
            FlyType.PLANE_BOSS -> {
                //...
                hitAndBeHit(fly, gcdPlanes)
            }
            FlyType.BULLET_BOSS -> {
                //...
                hitAndBeHit(fly, gcdPlanes)
            }
        }
    }

    /**
     * 碰撞和被碰撞的集合
     */
    private fun hitAndBeHit(hitFly: Fly, beHitFlys: CopyOnWriteArrayList) {
        for (beHitFly in beHitFlys) {
            //碰撞之后跳出循环
            if (isCollision(
                    hitFly.x,
                    hitFly.y,
                    hitFly.w.toFloat(),
                    hitFly.h.toFloat(),
                    beHitFly.x,
                    beHitFly.y,
                    beHitFly.w.toFloat(),
                    beHitFly.h.toFloat()
                )
            ) {
                sellingHP(hitFly, beHitFly)
                break
            }
        }
    }

    /**
     * 碰撞检测
     */
    private fun isCollision(
        x1: Float,
        y1: Float,
        w1: Float,
        h1: Float,
        x2: Float,
        y2: Float,
        w2: Float,
        h2: Float
    ): Boolean {
        if (x1 > x2 + w2 || x1 + w1 < x2 || y1 > y2 + h2 || y1 + h1 < y2) {
            return false
        }
        return true
    }


    /**
     * 碰撞扣除HP
     */
    private fun sellingHP(fly1: Fly, fly2: Fly) {
        fly1.HP -= fly2.power
        fly2.HP -= fly1.power
        isDie(fly1)
        isDie(fly2)
    }

    /**
     * 飞行物是否死亡
     */
    private fun isDie(fly: Fly) {
        if (fly.HP <= 0) {
          //创建爆炸效果
            createBoom(fly)
            removeFly(fly)
        }
    }

创建爆炸效果

当碰撞发生时 如果有飞行器死亡,则会触发一个爆炸效果,位置就在飞行器死亡的位置

 /**
     * 创建爆炸效果
     */
    private fun createBoom(fly: Fly) {
        val boom = FlyFactory.getBoom(activity, fly.flyType)!!
        boom.x = (fly.cx - boom.w / 2)
        boom.y = (fly.cy - boom.h / 2)
        mapView.addFly(boom)

        val flyBoomView = boom.view as FlyBoomView
        //爆炸动画结束的时候将其从mapView中删除
        flyBoomView.animatorListener = object : FlyBoomView.AnimatorListener {
            override fun onAnimationEnd() {
                removeFly(boom)
            }
        }
    }

添加Boss飞机

Boss飞机的创建与普通的飞机创建是一样的,传入不同的大小和颜色用来区分。Boss的y轴移动目标点只在屏幕的最上方,从负屏移动到0的位置即停止,并开始x轴的左右移动。需要要为Boss添加x轴的移动动画,以下代码

    /**
     * X轴移动Fly
     */
    private fun moveFlyX(fly: Fly) {
        //如果fly已经死亡
        if (fly.isBoom) {
            return
        }
            
        var start = fly.x
        //左移动或者右移动
        var end = if (fly.x < w - fly.w) {
            w.toFloat() - fly.w
        } else {
            0F
        }
        ValueAnimator.ofFloat(start, end).apply {
            addUpdateListener {
                //如果fly已经死亡
                if (fly.isBoom) {
                    cancel()
                    return@addUpdateListener
                }
                fly.x = it.animatedValue as Float
                //超出屏幕检测
                if (checkFlyPosition(fly)) {
                    selectHitAndBeHit(fly)
                }
            }
            duration = (abs(start - end)).toLong() * fly.speed
            interpolator = LinearInterpolator()
            
            doOnEnd {
                //递归调用,实现左右不停移动
                moveFlyX(fly)
            }
            start()
        }
    }

滚动的星空背景

用自定义View不停的画圆实现

  1. 定义一个集合,准备存储圆的xy
  2. 心里将屏幕x轴定4个点,这四个点就是圆的中心
  3. 在屏幕的最上方,在x轴4个点随机取一个点,存入集合,并用这个点画一个圆。
  4. 接着开启一个动画,在动画中遍历集合累加y,这样圆就开始向下移动了
  5. 在移动的过程中判断y的移动距离,每到一定的距离就重复在最上方添加圆,这样就完成了
/**
 * 滚动的星空背景
 */
class ScrollStarsView : BaseView {
    /**
     * 圆形集合
     */
    private var circleStars = CopyOnWriteArrayList()
    /**
     * 圆形移动动画
     */
    private var moveRectAnim: ValueAnimator? = null

    private var random = Random()
    /**
     * 圆形直径
     */
    private var diameter = 0

    private var paint = Paint().apply {
        isAntiAlias = true
        style = Paint.Style.FILL

    }

    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        diameter = (w / 3).toInt()
        addRect()
        moveRect()
    }

    /**
     * 添加圆形
     */
    private fun addRect() {
        circleStars.add(
            CircleStar(
                (random.nextInt(4) * diameter).toFloat(),
                -diameter.toFloat(),
                diameter / 2F * (random.nextFloat() + 0.1F)
            )
        )
    }

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.let { c ->
            /**
             * 绘制背景矩形
             */
            paint.color = Color.parseColor("#0F2332")
            c.drawRect(0F, 0F, w, h, paint)

            /**
             * 绘制圆形
             */
            paint.color = Color.parseColor("#374060")
            circleStars.forEach {
                c.drawCircle(it.x, it.y, it.r, paint)
            }
        }
    }


    /**
     *  移动圆形  每走diameter*2的距离 触发一次添加
     *
     */
    private fun moveRect() {
        if (moveRectAnim == null) {
            var oldAnimateValue = 0
            moveRectAnim = ValueAnimator.ofInt(1, diameter * 2)
                .apply {
                    addUpdateListener { it ->
                        val animateValue = it.animatedValue as Int

                        //repeat的时候重置oldAnimateValue
                        if (oldAnimateValue > animateValue) {
                            oldAnimateValue = 0
                            addRect()
                        }

                        circleStars.forEach {
                            it.y += animateValue - oldAnimateValue
                            //如果已经走出了屏幕外则删除它
                            if (it.y > h + diameter) {
                                circleStars.remove(it)
                            }
                        }

                        oldAnimateValue = animateValue
                        invalidate()
                    }

                    repeatCount = ValueAnimator.INFINITE
                    interpolator = LinearInterpolator()
                    duration = 2000
                    start()
                }
        }
    }

    override fun onDetachedFromWindow() {
        super.onDetachedFromWindow()
        moveRectAnim?.cancel()
        moveRectAnim = null
    }

    data class CircleStar(var x: Float, var y: Float, var r: Float)

}

完!

你可能感兴趣的:(Android 飞机小游戏(自定义ViewGroup+View))