安卓小游戏:飞机大战
创始人
2024-05-23 20:05:57
0

安卓小游戏:飞机大战

前言

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

需求

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

  • 1,载入界面配置,设置游戏信息
  • 2,载入精灵配置,获取飞机、子弹、敌人掩图
  • 3,启动手势控制逻辑
  • 4,启动游戏controller,定时刷新处理逻辑

效果图

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

airplane

代码

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()private val mBulletMask: Bitmap?// 敌人序列private val mEnemyList = ArrayList()private val mEnemyMask: Bitmap?// 游戏控制器private val mGameController = GameController(this)// 画笔private val mPaint = Paint().apply {color = Color.WHITEstrokeWidth = 3fstyle = Paint.Style.STROKEflags = Paint.ANTI_ALIAS_FLAGtextAlign = Paint.Align.CENTERtextSize = 30f}// 上一个触摸点X的坐标private var mLastX = 0finit {// 读取配置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 nulltypedArray.recycle()}private fun drawableToBitmap(drawable: Drawable, size: Int = 1): Bitmap? {val w = drawable.intrinsicWidth * sizeval h = drawable.intrinsicHeight * sizeval config = Bitmap.Config.ARGB_8888val 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 = mAirPlaneMaskmAirPlane.live = mDefaultLiveSizemAirPlane.posX = w / 2 - mAirPlaneMask!!.width / 2mAirPlane.posY = h - mAirPlaneMask.height / 2 - 50mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)}// 重新加载private fun reload(w: Int, h: Int) {mGameController.removeMessages(0)// 清空界面mBulletList.clear()mEnemyList.clear()// 重置好飞机位置、生命值、得分mScore = 0mAirPlane.live = mDefaultLiveSizemAirPlane.posX = w / 2 - mAirPlaneMask!!.width / 2mAirPlane.posY = h - mAirPlaneMask.height / 2 - 50mGameController.isGameOver = falsemGameController.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 - mLastXval preX = mAirPlane.posX + lenif (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 = WeakReference(view)// 子弹更新频率限制private var bulletCounter = 0// 子弹出现频率控制private var bulletUpdateCounter = 0// 敌人更新频率控制private var enemyCounter = 0// 敌人出现频率控制private var enemyUpdateCounter = 0// 游戏结束标志internal var isGameOver = falseoverride fun handleMessage(msg: Message) {mRef.get()?.let { gameView ->// 移动已有子弹bulletCounter++if (bulletCounter == (UPDATE_BULLET_TIME / GAME_FLUSH_TIME).toInt()) {var mark = -1for (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()val removeBulletList = ArrayList()// 敌人和飞机碰撞标志var mark = -1for (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 = iif (--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




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

res -> drawable -> ic_airplane.xml



res -> drawable -> ic_bullet.xml



res -> drawable -> ic_enemy.xml



主要问题

下面简单讲讲吧。

资源加载

这里用了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进行绘制,性能都更好一些。

相关内容

热门资讯

重阳节主持词 重阳节主持词(精选13篇)  主持词分为会议主持词、晚会主持词、活动主持词、婚庆主持词等。在人们越来...
商场活动主持词   商场活动主持词  亲爱的顾客朋友:  大家下午好!  “金猪报捷去,玉鼠送春来”。欢迎大家在这个...
主婚人简短婚礼致辞 主婚人简短婚礼致辞  结婚是件大事,那么主婚人如何向新人们致辞呢?怎么做致辞才简短又大气呢?下面我们...
六一儿童节活动主持词 六一儿童节活动主持词集锦  众所周知,六一儿童节是孩子们开心玩耍的节日。小编今天为大家带来六一儿童节...
公司会抽奖环节主持词 公司会抽奖环节主持词  根据活动对象的不同,需要设置不同的主持词。在现在的社会生活中,司仪等是很多场...
迎新年晚会主持词 2022迎新年晚会主持词例文(精选11篇)  过去的一年,对于我们每一个中国人来说都是难忘的一年,我...
女儿出嫁父亲婚礼致辞 女儿出嫁父亲婚礼致辞范文(通用13篇)  无论是在学校还是在社会中,大家总少不了要接触或使用致辞吧,...
结婚当天新娘的致辞 结婚当天新娘的致辞(精选8篇)  新人致辞是婚礼上不可缺少的重要环节,那么如何表达能 显得既大方得体...
毕业典礼主持词 毕业典礼主持词(合集15篇)  主持人在台上表演的灵魂就表现在主持词中。在当今不断发展的世界,主持成...
新婚回门的宴会主持词 新婚回门的宴会主持词尊敬的各位来宾:  大家好!  今天是20XX年2月13日,农历:乙卯年正月十一...
春季订货会的主持词 春季订货会的主持词各位领导、各位经理、来宾、朋友们:  今天我们相聚在这里,共同迎来了四海商贸公司的...
电影《画壁》经典台词 电影《画壁》经典台词  1、牡丹:如果我愿意跟你一起走,你会和我一生一世吗?  2、牡丹:没有爱 会...
中秋对客户的感谢词 中秋对客户的感谢词亲爱的客户朋友们:  金秋九月,硕果累累,又是一年一度的中秋佳节,又是一年一度的月...
致新生的欢迎词 致新生的欢迎词亲爱的新同学:你们好!秋风送爽,丹桂飘香,又是一个流金岁月,又是一个收获季节。踏着习习...
追悼会家属答谢词 追悼会家属答谢词尊敬的各位领导,各位亲朋好友,感谢各位今天出席亡母的追悼会。在母亲生病住院期间,承蒙...
重阳节活动主持词开场白 重阳节活动主持词开场白  在这金秋送爽,硕果累累的时节,我们迎来了又一个九九重阳节。下面是小编精心为...
郭德纲相声小段子台词 郭德纲相声小段子台词  相声,一种民间说唱曲艺。它以说,学,逗,唱为形式,突出其特点。下面是小编整理...
《将夜》经典台词 《将夜》经典台词  1.这片海洋,当时这里还有日出,在阳光的照射下,这片海洋是透明的',看上去就像是...
晚会主持词 【实用】晚会主持词(精选17篇)  主持词是主持人在节目进行过程中用于串联节目的串联词。在如今这个时...
春节晚会主持词 给力春节晚会主持词(通用3篇)  主持词的写作需要将主题贯穿于所有节目之中。在现今人们越来越重视活动...