Android游戏程:子弹和爆炸效果
子弹和爆炸都有相应的实体类用于记录数据,都有各种的管理类用于管理这些实体类。而且这两块内容实现起来并不复杂。
子弹的实体类除了记录子弹的坐标和位图外还判断了子弹的矩形是否和玩家或程序飞机的矩形相交,即子弹是否击中了飞机。我这里的子弹比较简单,没有复杂的数学计算,子弹的飞行方向不是往上就是往下。
class Bullet {
private lateinit var bmpBullet: Bitmap
private var level = 1 // 子弹等级(范围1,2,3)
private var x = 0f
private var y = 0f
private var speed = 3f
private var direction = "up"
var flagFree = true // 闲置标记,子弹飞出边界就算闲置,对象会被重置使用
fun setBitmap(imgUrl: String) {
this.bmpBullet = BitmapCache.loadBitmap(imgUrl)
}
fun getBitmap() = bmpBullet
fun buildBmpLevel(level: Int) {
this.level = level
setBitmap("myBullet$level.png")
}
fun getDirection() = this.direction
fun setDirection(direction: String) {
this.direction = direction
}
fun getY() = this.y
fun getX() = this.x
fun draw(canvas: Canvas) {
canvas.drawBitmap(bmpBullet, x, y, null)
}
fun moveTop() {
y -= speed
flagFree = (y <= -bmpBullet.height)
}
fun moveBottom() {
y += speed
flagFree = (y >= AppHelper.bound.bottom)
}
fun setLocation(x: Float, y: Float) {
this.x = x
this.y = y
}
/**
* 判断子弹是否击中敌机
*/
fun hitEnemy() {
val left = x.toInt()
val right = (x + bmpBullet.width).toInt()
val top = y.toInt()
val bottom = (y + bmpBullet.height).toInt()
val result = EnemyPlaneManager.getInst().isHit(Rect(left, top, right, bottom), level)
if (result == null) return
else {
flagFree = true
if ((result.x + result.y) >= 0) {
BombManager.getInst().obtain(result.x.toFloat(), result.y.toFloat())
}
}
}
/**
* 判断子弹是否击中玩家
*/
fun hitPlayer() {
val left = x.toInt()
val right = (x + bmpBullet.width).toInt()
val top = y.toInt()
val bottom = (y + bmpBullet.height).toInt()
val bulletRect = Rect(left, top, right, bottom)
val isHit = MathUtils.cross(bulletRect, PlayerPlane.getInst().getRect())
if (isHit) {
flagFree = true
PlayerPlane.hit()
}
}
}
接着我们再设计一个子弹管理类,在内部维护着两个List集合,一个保存玩家发射的所有子弹,另一个保存程序飞机发射的全部子弹。然后再需要两上线程,在线程中通过循环不断的遍历这些子弹对象并更新对象的数据。其原理和上回我们设计的程序飞机的原理是一样的。
class BulletManager private constructor() {
private val playerBullets = mutableListOf<Bullet>()
private val enemyBullets = mutableListOf<Bullet>()
private lateinit var executors: ExecutorService
private object Holder {
val instance = BulletManager()
}
companion object {
var level = 1
fun getInst() = Holder.instance
fun init() = getInst().apply { init() }
fun sendPlayerBullet(x: Int, y: Int, width: Int) {
getInst().obtainPlayerBullet(x, y, width)
}
fun sendBossBullet(x: Int, y: Int, width: Int, height: Int) {
getInst().obtainBossBullet(x, y, width, height)
}
}
private fun init() {
// 我方子弹的运动循环
executors = Executors.newFixedThreadPool(2)
executors.submit {
while (AppHelper.isRunning) {
if (AppHelper.isPause) continue
playerBullets.filter { !it.flagFree }.forEach {
it.moveTop()
it.hitEnemy()
}
// 运动延时
Thread.sleep(1)
}
}
// 处理敌方子弹的线程
executors.submit {
// 敌方子弹的运动循环
while (AppHelper.isRunning) {
if (AppHelper.isPause) continue
enemyBullets.filter { !it.flagFree }.forEach {
when (it.getDirection()) {
"down" -> it.moveBottom()
"up" -> it.moveTop()
}
it.hitPlayer()
}
// 运动延时
Thread.sleep(5)
}
}
}
fun release() {
executors.shutdown()
}
/**
* 生成玩家子弹对象
* @param width:玩家飞机的宽度
*/
fun obtainPlayerBullet(x: Int, y: Int, width: Int) {
var bullet = playerBullets.find { it.flagFree }
if (bullet == null) {
bullet = Bullet()
playerBullets += bullet
}
bullet.buildBmpLevel(level)
bullet.flagFree = false
// 计算子弹位于飞机的中间位置
val centerX = x + (width / 2) - (bullet.getBitmap().width / 2).toFloat()
bullet.setLocation(centerX, y.toFloat())
}
fun drawPlayerBullet(canvas: Canvas?) {
if (canvas == null) return
playerBullets.filter { !it.flagFree }.forEach { it.draw(canvas) }
}
fun drawBossBullet(canvas: Canvas?) {
if (canvas == null) return
enemyBullets.filter { !it.flagFree }.forEach { it.draw(canvas) }
}
/**
* 生成敌方子弹对象
* @param width:敌机的宽度
* @param height:敌机的高度
*/
fun obtainBossBullet(x: Int, y: Int, width: Int, height: Int) {
var bullet = enemyBullets.find { it.flagFree }
if (bullet == null) {
bullet = Bullet().apply {
this.setBitmap("BossBullet.png")
}
enemyBullets += bullet
}
bullet.let {
// 设置子弹的起始位置,位于飞机的中央
val centerX = x + (width / 2) - (it.getBitmap().width / 2).toFloat()
val centerY = y + (height / 2) - (it.getBitmap().height / 2).toFloat()
it.setLocation(centerX, centerY)
it.flagFree = false
it.setDirection(getPlayerDirection(it))
}
}
private fun getPlayerDirection(bullet: Bullet): String {
val py = PlayerPlane.getInst().getCenter().y
val by = bullet.getY()
return if ((py - by) > 0) "down" else "up"
}
fun clearBullet() {
enemyBullets.forEach { it.flagFree = true }
}
}
飞出屏幕的子弹会被打上标记,然后在下一轮循环时被重置数据并重新使用。
然后再说一下爆炸效果,我爆炸效果是由6张图片按顺序显示的结果 。所以我我们也需要先设计一个实体类,在这个实体类中保存一个Bitmap类型的集合,然后还要保存当前帧的索引,爆炸的位置,爆炸的大小和爆炸的中心,爆炸位置是根据飞机的位置而来,爆炸大小和爆炸中心则是换算出来的。详细的可以看源码。
class Bomb {
private var bmpBombList = mutableListOf<Bitmap>()
private var frame = 0
private var bmpX = 0f
private var bmpY = 0f
private var centerX = 0f
private var centerY = 0f
private var radius = 100f // 爆炸半径
private var paintOval = Paint()
var isPlaying = false
init {
// 从assets里读取图片文件到列表
repeat(6) {
bmpBombList.add(BitmapCache.loadBitmap("bomb_enemy_$it.png"))
}
}
fun draw(canvas: Canvas) {
// 绘制冲击波效果
if (frame > 0) {
paintOval.let {
it.alpha = 255 / (frame + 1)
val gradient = RadialGradient(
centerX, centerY, (frame * radius / 2) + 1,
intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.WHITE), null,
Shader.TileMode.CLAMP
)
it.setShader(gradient)
}
canvas.drawCircle(centerX, centerY, frame * radius / 2, paintOval)
}
// 绘制爆炸图片
val src = Rect(0, 0, bmpBombList[frame].width, bmpBombList[frame].height)
val dst = Rect(
bmpX.toInt(), bmpY.toInt(),
(radius * 2 + bmpX).toInt(),
(radius * 2 + bmpY).toInt()
)
canvas.drawBitmap(bmpBombList[frame], src, dst, null)
}
/**
* 开始播放爆炸
*/
fun play(x: Float, y: Float, radius: Float) {
isPlaying = true
frame = 0
this.centerX = x
this.centerY = y
this.bmpX = x - radius
this.bmpY = y - radius
}
fun loopFrame() {
frame++
if (frame >= bmpBombList.size) {
frame = 0
isPlaying = false
}
}
}
老规矩,我们依据需要一个管理类来对所有的爆炸对象进行管理,而这种管理仍旧是在线程中遍历集合来实现的,因为原理比较简单就直接上源码了。
class BombManager private constructor() {
private val bombList = mutableListOf<Bomb>()
private lateinit var executors: ExecutorService
private object Holder {
const val DELAY = 80L
val instance = BombManager()
}
companion object {
fun getInst() = Holder.instance
fun init() = getInst().apply { init() }
}
private fun init() {
executors = Executors.newSingleThreadExecutor()
executors.submit {
while (AppHelper.isRunning) {
if (AppHelper.isPause) continue
bombList.filter { it.isPlaying }
.forEach { it.loopFrame() }
Thread.sleep(Holder.DELAY)
}
}
}
fun release() {
executors.shutdown()
}
/**
* 生成一个对象
* @param x:爆炸中心X
* @param y: 爆炸中心Y
* @param radius:爆炸半径
*/
fun obtain(x: Float, y: Float, radius: Float = 100f) {
var bomb = bombList.find { !it.isPlaying }
if (bomb == null) {
bomb = Bomb()
bombList += bomb
}
bomb.play(x, y, radius)
}
fun drawAll(canvas: Canvas?) {
if (canvas == null) return
bombList.filter { it.isPlaying }.forEach { it.draw(canvas) }
}
}
至此,我们的飞机游戏教程就算完成了。这个游戏的特点是多线程的操作, 主界面的SurfaceView通过循环不断的重绘背景、飞机、子弹和爆炸,然后飞机、子弹和爆炸对象都有各种的实体类和管理类,在各自的管理类都有线程,通过线程再不断的遍历各自管理的对象或集合。简单归纳一下:
- SurfaceView里的线程循环主要负责画面的渲染。
- 管理类中的线程循环主要负责对象数据的更新。
需要说明的是这两种线程都需要访问实体对象,尤其是管理类中的线程还涉及到对数据的写操作,这就需要在项目使用CopyOnWriteArrayList来保证并发操作,不过自从Kotlin引入了协程以后我们又多了一种选择,直接用协程替代SurfaceView里的线程,因此我们在项目中可以直接用使用List集合而不会出现并发异常。
关于这个项目就写到这里了,如果对象我的小游戏感兴趣可以直接加我的QQ号52137124或我的QQ群联系我。
github完整源码:https://github.com/greentea107/PlaneGame
传送门 - Android开发:不用游戏引擎也能做游戏
传送门 - Android游戏教程:从SurfaceView开始
传送门 - Android游戏教程:背景卷轴
传送门 - Android游戏教程:玩家飞机
传送门 - Android游戏教程:程序飞机
点击链接加入QQ群聊:https://jq.qq.com/?_wv=1027&k=5z4fzdT
或关注微信公众号:口袋里的安卓