安卓小游戏:飞机大战
创始人
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进行绘制,性能都更好一些。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...