安卓小游戏:飞机大战

前语

前面写了十二篇自定义view的博客,说实话写的仍是有点无聊了,最近调整了一下,觉得仍是要对开发有热心,就写了点小游戏,现在抽时刻把博客也写一写,希望读者喜爱。

需求

这儿便是飞机大战啊,很多人小时候都玩过,我这也比较简单还原了一下。中心思维如下:

  • 1,载入界面装备,设置游戏信息
  • 2,载入精灵装备,获取飞机、子弹、敌人掩图
  • 3,发动手势操控逻辑
  • 4,发动游戏controller,守时改写处理逻辑

效果图

效果图还阔以吧,便是掩图马虎了,直接再Android Studio里边找的vector的image,玩起来仍是有点小时候的感觉。

安卓小游戏:飞机大战

代码

import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.silencefly96.module_views.R
import java.lang.ref.WeakReference
import kotlin.math.pow
import kotlin.math.sqrt
/**
 * 飞机大战 GameView
 *
 * 1,载入界面装备,设置游戏信息
 * 2,载入精灵装备,获取飞机、子弹、敌人掩图
 * 3,发动手势操控逻辑
 * 4,发动游戏controller,守时改写处理逻辑
 *
 * @author silence
 * @date 2023-01-17
 */
class AirplaneFightGameView @JvmOverloads constructor(
    context: Context,
    attributeSet: AttributeSet? = null,
    defStyleAttr: Int = 0
): View(context, attributeSet, defStyleAttr) {
    companion object{
        // 游戏更新间隔,一秒20次
        const val GAME_FLUSH_TIME = 50L
        // 敌人增加隔时刻
        const val ADD_ENEMY_TIME = 1000L
        // 敌人更新隔时刻
        const val UPDATE_ENEMY_TIME = 200L
        // 敌人移动间隔
        const val ENEMY_MOVE_DISTANCE = 50
        // 子弹增加隔时刻
        const val ADD_BULLET_TIME = 150L
        // 子弹更新隔时刻
        const val UPDATE_BULLET_TIME = 100L
        // 子弹移动间隔
        const val BULLET_MOVE_DISTANCE = 50
        // 磕碰间隔
        const val COLLISION_DISTANCE = 100
        // 间隔核算公式
        fun getDistance(x1: Int, y1: Int, x2: Int, y2: Int): Float {
            return sqrt(((x1 - x2).toDouble().pow(2.0)
                    + (y1 - y2).toDouble().pow(2.0)).toFloat())
        }
    }
    // 默许生命值巨细
    private val mDefaultLiveSize: Int
    // 得分
    private var mScore: Int = 0
    // 飞机
    private val mAirPlane: Sprite = Sprite(0, 0)
    private val mAirPlaneMask: Bitmap?
    // 子弹序列
    private val mBulletList = ArrayList<Sprite>()
    private val mBulletMask: Bitmap?
    // 敌人序列
    private val mEnemyList = ArrayList<Sprite>()
    private val mEnemyMask: Bitmap?
    // 游戏操控器
    private val mGameController = GameController(this)
    // 画笔
    private val mPaint = Paint().apply {
        color = Color.WHITE
        strokeWidth = 3f
        style = Paint.Style.STROKE
        flags = Paint.ANTI_ALIAS_FLAG
        textAlign = Paint.Align.CENTER
        textSize = 30f
    }
    // 上一个接触点X的坐标
    private var mLastX = 0f
    init {
        // 读取装备
        val typedArray =
            context.obtainStyledAttributes(attributeSet, R.styleable.AirplaneFightGameView)
        mDefaultLiveSize =
            typedArray.getInteger(R.styleable.AirplaneFightGameView_liveSize, 3)
        // 得到的Bitmap为空
//        var resourceId =
//        typedArray.getResourceId(R.styleable.AirplaneFightGameView_airplane, 0)
//        mAirPlaneMask = if (resourceId != 0) {
//            BitmapFactory.decodeResource(resources, resourceId)
//        }else null
        // 留意Drawable也有类型
//        var drawable = typedArray.getDrawable(R.styleable.AirplaneFightGameView_airplane)
//        mAirPlaneMask = (drawable as BitmapDrawable).bitmap     //vector的xml不是BitmapDrawable
//        mAirPlaneMask = (drawable as VectorDrawable).toBitmap() //需求版本支持
        // 飞机掩图
        var drawable = typedArray.getDrawable(R.styleable.AirplaneFightGameView_airplane)
        mAirPlaneMask = if (drawable != null) drawableToBitmap(drawable, 2) else null
        // 子弹掩图
        drawable = typedArray.getDrawable(R.styleable.AirplaneFightGameView_bullet)
        mBulletMask = if (drawable != null) drawableToBitmap(drawable) else null
        // 敌人掩图
        drawable = typedArray.getDrawable(R.styleable.AirplaneFightGameView_enemy)
        mEnemyMask = if (drawable != null) drawableToBitmap(drawable, 2) else null
        typedArray.recycle()
    }
    private fun drawableToBitmap(drawable: Drawable, size: Int = 1): Bitmap? {
        val w = drawable.intrinsicWidth * size
        val h = drawable.intrinsicHeight * size
        val config = Bitmap.Config.ARGB_8888
        val bitmap = Bitmap.createBitmap(w, h, config)
        //留意,下面三行代码要用到,否则在View或许SurfaceView里的canvas.drawBitmap会看不到图
        val canvas = Canvas(bitmap)
        drawable.setBounds(0, 0, w, h)
        drawable.draw(canvas)
        return bitmap
    }
    // 完成测量开端游戏
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        // 开端游戏
        load(w, h)
    }
    // 加载
    private fun load(w: Int, h: Int) {
        mGameController.removeMessages(0)
        // 设置好飞机方位,坐标未中心坐标,方便核算磕碰
        mAirPlane.bitmap = mAirPlaneMask
        mAirPlane.live = mDefaultLiveSize
        mAirPlane.posX = w / 2 - mAirPlaneMask!!.width / 2
        mAirPlane.posY = h - mAirPlaneMask.height / 2 - 50
        mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
    }
    // 重新加载
    private fun reload(w: Int, h: Int) {
        mGameController.removeMessages(0)
        // 清空界面
        mBulletList.clear()
        mEnemyList.clear()
        // 重置好飞机方位、生命值、得分
        mScore = 0
        mAirPlane.live = mDefaultLiveSize
        mAirPlane.posX = w / 2 - mAirPlaneMask!!.width / 2
        mAirPlane.posY = h - mAirPlaneMask.height / 2 - 50
        mGameController.isGameOver = false
        mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
    }
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
            getDefaultSize(0, heightMeasureSpec))
    }
    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        // 制作顶部信息
        canvas.drawText("生命值:${mAirPlane.live}, 当时得分:$mScore",
            (width / 2).toFloat(), 50f, mPaint)
        // 制作敌人
        for (enemy in mEnemyList) {
            canvas.drawBitmap(mEnemyMask!!,
                enemy.posX.toFloat() - mEnemyMask.width / 2,
                enemy.posY.toFloat() - mEnemyMask.height / 2, mPaint)
        }
        // 制作子弹
        for (bullet in mBulletList) {
            canvas.drawBitmap(mBulletMask!!,
                bullet.posX.toFloat()- mBulletMask.width / 2,
                bullet.posY.toFloat()- mBulletMask.height / 2, mPaint)
        }
        // 制作飞机
        canvas.drawBitmap(mAirPlaneMask!!,
            mAirPlane.posX.toFloat() - mAirPlaneMask.width / 2,
            mAirPlane.posY.toFloat() - mAirPlaneMask.height / 2, mPaint)
    }
    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.x
            }
            MotionEvent.ACTION_MOVE -> {
                val len = event.x - mLastX
                val preX = mAirPlane.posX + len
                if (preX > mAirPlaneMask!!.width / 2 &&
                        preX < (width - mAirPlaneMask.width / 2)) {
                    mAirPlane.posX += len.toInt()
                    invalidate()
                }
                mLastX = event.x
            }
            MotionEvent.ACTION_UP -> {}
        }
        return true
    }
    private fun gameOver() {
        AlertDialog.Builder(context)
            .setTitle("持续游戏")
            .setMessage("请点击承认持续游戏")
            .setPositiveButton("承认") { _, _ -> reload(width, height) }
            .setNegativeButton("撤销", null)
            .create()
            .show()
    }
    // kotlin主动编译为Java静态类,控件引用运用弱引用
    class GameController(view: AirplaneFightGameView): Handler(Looper.getMainLooper()){
        // 控件引用
        private val mRef: WeakReference<AirplaneFightGameView> = WeakReference(view)
        // 子弹更新频率约束
        private var bulletCounter = 0
        // 子弹呈现频率操控
        private var bulletUpdateCounter = 0
        // 敌人更新频率操控
        private var enemyCounter = 0
        // 敌人呈现频率操控
        private var enemyUpdateCounter = 0
        // 游戏完毕标志
        internal var isGameOver = false
        override fun handleMessage(msg: Message) {
            mRef.get()?.let { gameView ->
                // 移动已有子弹
                bulletCounter++
                if (bulletCounter == (UPDATE_BULLET_TIME / GAME_FLUSH_TIME).toInt()) {
                    var mark = -1
                    for (i in 0 until gameView.mBulletList.size) {
                        val bullet = gameView.mBulletList[i]
                        bullet.posY -= BULLET_MOVE_DISTANCE
                        // 子弹出界
                        if (bullet.posY < 0) mark = i
                    }
                    // 移除出界子弹
                    if (mark >= 0) gameView.mBulletList.remove(gameView.mBulletList[mark])
                    bulletCounter = 0
                }
                // 移动敌人,并验证磕碰
                enemyUpdateCounter++
                if (enemyUpdateCounter == (UPDATE_ENEMY_TIME / GAME_FLUSH_TIME).toInt()) {
                    // 或许一起有子弹和敌人磕碰
                    val removeEnemyList = ArrayList<Sprite>()
                    val removeBulletList = ArrayList<Sprite>()
                    // 敌人和飞机磕碰标志
                    var mark = -1
                    for (i in 0 until gameView.mEnemyList.size) {
                        val enemy = gameView.mEnemyList[i]
                        enemy.posY += ENEMY_MOVE_DISTANCE
                        // 验证和子弹磕碰
                        for (j in 0 until gameView.mBulletList.size) {
                            val bullet = gameView.mBulletList[j]
                            if (getDistance(bullet.posX, bullet.posY, enemy.posX, enemy.posY)
                                <= COLLISION_DISTANCE) {
                                // 产生磕碰
                                removeEnemyList.add(enemy)
                                removeBulletList.add(bullet)
                                gameView.mScore++
                            }
                        }
                        //验证和飞机磕碰
                        if (getDistance(gameView.mAirPlane.posX, gameView.mAirPlane.posY,
                                enemy.posX, enemy.posY)
                            <= COLLISION_DISTANCE) {
                            // 产生磕碰
                            mark = i
                            if (--gameView.mAirPlane.live <= 0) {
                                isGameOver = true
                            }
                        }
                    }
                    // 统一移除
                    for (enemy in removeEnemyList) gameView.mEnemyList.remove(enemy)
                    for (bullet in removeBulletList) gameView.mBulletList.remove(bullet)
                    if (mark >= 0) gameView.mEnemyList.remove(gameView.mEnemyList[mark])
                    enemyUpdateCounter = 0
                }
                enemyCounter++
                if (enemyCounter == (ADD_ENEMY_TIME / GAME_FLUSH_TIME).toInt()) {
                    // 随机生成一个敌人
                    val x = (gameView.mEnemyMask!!.width / 2 +
                            Math.random() * (gameView.width - gameView.mEnemyMask.width)).toInt()
                    gameView.mEnemyList.add(
                        Sprite(x, gameView.mEnemyMask.height / 2, 1, gameView.mEnemyMask))
                    enemyCounter = 0
                }
                bulletUpdateCounter++
                if (bulletUpdateCounter == (ADD_BULLET_TIME / GAME_FLUSH_TIME).toInt()) {
                    // 增加新子弹,横向在飞机上居中
                    gameView.mBulletList.add(Sprite(gameView.mAirPlane.posX,
                        gameView.mAirPlane.posY, 1, gameView.mBulletMask))
                    bulletUpdateCounter = 0
                }
                // 循环发送音讯,改写页面
                gameView.invalidate()
                if (!isGameOver) {
                    gameView.mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
                }else {
                    gameView.gameOver()
                }
            }
        }
    }
    // 坐标运用中心坐标
    data class Sprite(var posX: Int, var posY: Int, var live: Int = 1, var bitmap: Bitmap? = null)
    /**
     * 供外部回收资源
     */
    fun recycle()  {
        mAirPlaneMask?.recycle()
        mBulletMask?.recycle()
        mEnemyMask?.recycle()
        mGameController.removeMessages(0)
    }
}

对应style装备

res -> values -> airplane_fight_game_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name ="AirplaneFightGameView">
        <attr name="liveSize" format="integer"/>
        <attr name="airplane" format="reference"/>
        <attr name="bullet" format="reference"/>
        <attr name="enemy" format="reference"/>
    </declare-styleable>
</resources>

三个掩图也给一下吧,当然你找点美观的图片替代下会更好!

res -> drawable -> ic_airplane.xml

<vector android:height="24dp" android:tint="#01A5FD"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M22,16v-2l-8.5,-5V3.5C13.5,2.67 12.83,2 12,2s-1.5,0.67 -1.5,1.5V9L2,14v2l8.5,-2.5V19L8,20.5L8,22l4,-1l4,1l0,-1.5L13.5,19v-5.5L22,16z"/>
</vector>

res -> drawable -> ic_bullet.xml

<vector android:height="24dp" android:tint="#6F6A6A"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

res -> drawable -> ic_enemy.xml

<vector android:height="24dp" android:tint="#DB172F"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M5,16c0,3.87 3.13,7 7,7s7,-3.13 7,-7v-4L5,12v4zM16.12,4.37l2.1,-2.1 -0.82,-0.83 -2.3,2.31C14.16,3.28 13.12,3 12,3s-2.16,0.28 -3.09,0.75L6.6,1.44l-0.82,0.83 2.1,2.1C6.14,5.64 5,7.68 5,10v1h14v-1c0,-2.32 -1.14,-4.36 -2.88,-5.63zM9,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

首要问题

下面简单讲讲吧。

资源加载

这儿用了styleable来装备游戏所需的资源,关于styleable我前面转载了一篇博文,别人写的很好,我就不再赘述。在init函数里边加载了生命值和掩图的资源id,留意这儿没对id验证,假如没有设置或许设置不对或许会闪退,可是我这小游戏你不设置也没必要玩。。

掩图加载

这儿我需求经过资源id拿到掩图资源,并转化为bitmap,这儿踩了挺多坑。首要我是经过BitmapFactory对拿到的resourceId直接decode,结果拿到的bitmap为空,为此我还调试了一下,也欠好确认原因,原因在后边也呈现了,便是我这用的是vector的资源,并不能转换为BitmapDrawable,拿不到bitmap。后边还试了拿到Drawable,强制转换成BitmapDrawable拿到bitmap,也是不行,转成VectorDrawable需求改最低的api,最后仍是凭借canvas拿到的bitmap。

加载游戏

我这儿是在onSizeChanged里边开端的,onSizeChanged会在第一次onMeasure后调用(条件是尺寸不产生变化),这儿能够拿到控件宽高,正好对游戏进行一些装备,并发送空音讯给handler,敞开守时改写。

游戏逻辑

onSizeChanged后敞开守时改写,游戏逻辑写在handler里就能够了,还能够依据改写频率略微操控下飞机、子弹、敌人的频率,我这儿用了很多操控变量,当然小游戏能够这么写,复杂点的仍是收拾下吧。

游戏逻辑无非便是移动、磕碰、死亡几个,很简单,看代码就行,不多讲。这儿稍稍留意下对列表数据的移除,我这儿写的也不是很好,应该用iterator去移除的。

飞机移动逻辑

这儿飞机移动逻辑和游戏逻辑别离了,直接在onTouchEvent中移动飞机,并经过invalidate改写。飞机移动就没有必要和游戏改写频率有关了吧,直接invalidate看起来舒畅点。

画面制作

画面制作没得说,只要把飞机、子弹、敌人制作出来就行了,逻辑和制作别离。一开端我还想着又怎样移动,又怎样制作,运用动画移动子弹什么的,太复杂了,反而不现实。

资源回收

这儿用到了bitmap,最好要做下bitmap的回收。

主意的失误

一开端我是想把飞机、子弹、敌人当成控件,游戏当成viewgroup的,然后对控件进行操作,面向对象嘛,也许有道理,可是为什么不直接运用ondraw进行制作,功能都更好一些。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。