上回我们实现了玩家可控的飞机玩家飞机
。这回我们要实现程序操控的飞机,程序飞机要实现的功能如下:
- 能循环不断的在屏幕上生成各种飞机。
- 能对玩家飞机做碰撞检测。
- 能够发射子弹。
- 飞机的样式、速度和飞机方向是不同的。
所以在代码上我们需要设计一个EnemyPlane类,并继承自Plane类,然后在其中实现所需要的功能;
class EnemyPlane : Plane() {
var flagUsed = false // 占用标记,飞出屏幕后设为false,对象可被重复使用
var direction = "down" // 敌机的飞行方向
var score = 1 // 敌机的分数
var isTrack = false // 是否会根据玩家的位置进行左右偏移
var type = 1
init {
setEnableOut(true) // 敌机可以飞机屏幕边界再消失
}
/**
* 飞机的构造方法
* @param imgUrl:飞机图片
* @param speed:速度
* @param score:分数
* @param hp:血量
* @param direction:飞机的飞行方向
* @param isTrack:是否会根据玩家飞机的位置做偏移飞行
*/
fun buildPlane(
imgUrl: String,
speed: Float,
score: Int = 1,
hp: Int = 1,
direction: String = "down",
isTrack: Boolean = false
) {
setPlaneImage(imgUrl)
this.speed = speed
this.score = score
this.hp = hp
this.direction = direction
this.isTrack = isTrack
}
fun offsetLeft(offset: Float) {
this.x -= offset
}
fun offsetRight(offset: Float) {
this.x += offset
}
fun getPlaneSpeed() = this.speed
/**
* 获取飞机位于屏幕上的坐标
*/
fun getRect(): Rect {
val right = x.toInt() + getPlaneImage().width
val bottom = y.toInt() + getPlaneImage().height
// 矩形框内缩1/5,减少碰撞的难度
val sw = getPlaneImage().width / 5
val sh = getPlaneImage().height / 5
return Rect(x.toInt() + sw, y.toInt() + sh, right - sw, bottom - sh)
}
}
buildPlane可以设置飞机的各种参数,这里引出一个问题,为什么要写一个这样的方法,而不是直接在构造器中直接设置?这主要是考虑到这个飞机对象在飞出屏幕后只需要重新设置一下参数,就可以被再次使用,以便减少创建和销毁对象的开销。
有了敌机对象后还需要一个敌机管理类,在这个类中会有一个List集合存放所有的敌机,然后再用线程不断的遍历集合,并对集中的飞机数据进行管理。这就个线程也就是整个管理对象的核心部分。
private val enemyList = mutableListOf<EnemyPlane>()
private lateinit var executor: ExecutorService
private fun init() {
executor = Executors.newSingleThreadExecutor()
executor.submit {
while (AppHelper.isRunning) {
private val enemyList = mutableListOf<EnemyPlane>()
private lateinit var executor: ExecutorService
private fun init() {
executor = Executors.newSingleThreadExecutor()
executor.submit {
while (AppHelper.isRunning) {
buildEnemyPlane() // 生成敌机
motionPlane() // 对敌机列表遍历,作运动处理
sendBullet() // 让指定的飞机发射子弹
Thread.sleep(Holder.DELAY) // 这个延时能决定飞机的移动的快慢
}
}
}
我们在init方法中开启了一个线程,然后在线程内循环执行buildEnemyPlane、motionPlane和sendBullet方法。而这三些方法内部都会对enemyList集合做遍历操作,尤其是buildEnemyPlane方法还具有往集全中添加对象的作用;
fun buildEnemyPlane() {
getInst().obtain()
}
...
fun obtain() {
var plane = enemyList.find { !it.flagUsed }
if (plane == null && enemyList.size < Holder.ENEMY_MAX) {
plane = EnemyPlane()
enemyList += plane
}
// 随机生成敌机类型
var enemyType = Random().nextInt(11)
// 控制大号机的数
val count = enemyList.count { it.type in (6..10) && it.flagUsed }
if (count >= 5) enemyType -= 5
plane?.let {
it.flagUsed = true
when (enemyType) {
0 -> it.buildPlane("ep0.png", 1.0f, 1)
1 -> it.buildPlane("ep1.png", 1.1f, 1)
2 -> it.buildPlane("ep2.png", 1.5f, 1, isTrack = true)
3 -> it.buildPlane("ep3.png", 1.2f, 2, isTrack = true)
4 -> it.buildPlane("ep4.png", 0.8f, 2)
5 -> it.buildPlane("ep5.png", 0.8f, 1, 3)
6 -> it.buildPlane("ep6.png", 0.3f, 5, 20, "up")
7 -> it.buildPlane("ep7.png", 0.4f, 4, 10)
8 -> it.buildPlane("ep8.png", 0.2f, 5, 12, "up")
9 -> it.buildPlane("ep9.png", 0.35f, 8, 20)
10 -> it.buildPlane("ep10.png", 0.37f, 9, 20)
}
it.type = enemyType
// 随机设置敌机的起始位置
val x = Random().nextInt(AppHelper.widthSurface - it.getPlaneImage().width).toFloat()
val y = if (enemyType in arrayOf(6, 8)) {
it.getPlaneImage().height * 2f + AppHelper.heightSurface
} else {
-it.getPlaneImage().height * 2f
}
it.setLocation(x, y)
}
}
obtion的作用是从集合中找到空闲状态的对象,如果没有则判断集合总数是否大于我们设定的最大值,没有大于的话就创建对象,然后利用随机数对这个空闲的对象进行参数设置。
motionPlane的方法比较简单,就是根据飞机的方向对y轴变量进行修改,直到飞机飞出屏幕后设为空闲状态。但这个方法需要做一个碰撞检测,检测是以当前的飞机的矩形是否和玩家飞机矩形相交为标准的,如果相交的话就调用PlayerPlane的相关方法(爆炸,扣分,重置,再重新出场)。检测矩形相关的检测方法如下:
object MathUtils {
/**
* 判断两个矩形是否相交
*/
fun cross(r1: Rect, r2: Rect): Boolean {
val zx = abs(r1.left + r1.right - r2.left - r2.right)
val x = abs(r1.left - r1.right) + abs(r2.left - r2.right)
val zy = abs(r1.top + r1.bottom - r2.top - r2.bottom)
val y = abs(r1.top - r1.bottom) + abs(r2.top - r2.bottom)
return zx <= x && zy <= y
}
}
private fun collision(ep: EnemyPlane) {
// 获取敌机矩形
val eRect = ep.getRect()
// 获取玩家矩形
val pRect = PlayerPlane.getInst().getRect()
// 检测是否和玩家飞机碰撞
if (MathUtils.cross(eRect, pRect)) {
// 玩家被击中
PlayerPlane.hit()
// 敌机置为空闲
ep.flagUsed = false
// 敌机爆炸
BombManager.getInst().obtain(
eRect.centerX().toFloat(),
eRect.centerY().toFloat(),
ep.getPlaneImage().width / 2.toFloat()
)
// 通过消息总线发给主界面更新计分控件
LiveEventBus.get(AppHelper.COLLI_EVENT, Int::class.java).post(1)
}
}
在线程的循环内还有一个sendBullet方法,这个方法用于判断敌机是否需要发射子弹,和子弹在屏幕上的起始位置;
private fun sendBullet() {
enemyList.filter { it.flagUsed && it.type in (6 until 10) }
.forEach {
if ((System.currentTimeMillis() - lastMillis) > 1000) {
if (it.y > 10) {
BulletManager.sendBossBullet(
it.x.toInt(), it.y.toInt(),
it.getPlaneImage().width,
it.getPlaneImage().height
)
}
lastMillis = System.currentTimeMillis()
}
}
}
程序飞机类的几个要点就讲到这里,还有一些方法是需要和子弹类相关的,不过内容比较简单,我就直接把EnemyPlaneManager的源代码贴出来,大家慢慢看,并不复杂。
/**
* 敌机资源管理
*/
class EnemyPlaneManager private constructor() {
private val enemyList = mutableListOf<EnemyPlane>()
private lateinit var executor: ExecutorService
private fun init() {
executor = Executors.newSingleThreadExecutor()
executor.submit {
while (AppHelper.isRunning) {
if (AppHelper.isPause) continue
buildEnemyPlane() // 生成敌机
motionPlane() // 对敌机列表遍历,作运动处理
sendBullet() // 让指定的飞机发射子弹
Thread.sleep(Holder.DELAY) // 这个延时能决定飞机的移动的快慢
}
}
}
private object Holder {
const val ENEMY_MAX = 12 // 最大敌机数量
const val DELAY = 2L
val instance = EnemyPlaneManager()
}
companion object {
fun getInst() = Holder.instance
fun init() = getInst().apply { this.init() }
fun buildEnemyPlane() {
getInst().obtain()
}
}
fun release() {
executor.shutdown()
}
/**
* 初始化飞机(工厂化生产)
*/
fun obtain() {
var plane = enemyList.find { !it.flagUsed }
if (plane == null && enemyList.size < Holder.ENEMY_MAX) {
plane = EnemyPlane()
enemyList += plane
}
// 随机生成敌机类型
var enemyType = Random().nextInt(11)
// 控制大号机的数
val count = enemyList.count { it.type in (6..10) && it.flagUsed }
if (count >= 5) enemyType -= 5
plane?.let {
it.flagUsed = true
when (enemyType) {
0 -> it.buildPlane("ep0.png", 1.0f, 1)
1 -> it.buildPlane("ep1.png", 1.1f, 1)
2 -> it.buildPlane("ep2.png", 1.5f, 1, isTrack = true)
3 -> it.buildPlane("ep3.png", 1.2f, 2, isTrack = true)
4 -> it.buildPlane("ep4.png", 0.8f, 2)
5 -> it.buildPlane("ep5.png", 0.8f, 1, 3)
6 -> it.buildPlane("ep6.png", 0.3f, 5, 20, "up")
7 -> it.buildPlane("ep7.png", 0.4f, 4, 10)
8 -> it.buildPlane("ep8.png", 0.2f, 5, 12, "up")
9 -> it.buildPlane("ep9.png", 0.35f, 8, 20)
10 -> it.buildPlane("ep10.png", 0.37f, 9, 20)
}
it.type = enemyType
// 随机设置敌机的起始位置
val x = Random().nextInt(AppHelper.widthSurface - it.getPlaneImage().width).toFloat()
val y = if (enemyType in arrayOf(6, 8)) {
it.getPlaneImage().height * 2f + AppHelper.heightSurface
} else {
-it.getPlaneImage().height * 2f
}
it.setLocation(x, y)
}
}
private var lastMillis = 0L
private fun sendBullet() {
enemyList.filter { it.flagUsed && it.type in (6 until 10) }
.forEach {
if ((System.currentTimeMillis() - lastMillis) > 1000) {
if (it.y > 10) {
BulletManager.sendBossBullet(
it.x.toInt(), it.y.toInt(),
it.getPlaneImage().width,
it.getPlaneImage().height
)
}
lastMillis = System.currentTimeMillis()
}
}
}
/**
* 飞行的运动循环
*/
private fun motionPlane() {
enemyList.filter { it.flagUsed }.forEach {
if (it.isTrack) {
val offset = it.getPlaneSpeed() / 2
if ((PlayerPlane.getInst().x - it.x) < 0)
it.offsetLeft(offset)
else
it.offsetRight(offset)
}
collision(it) // 碰撞检测
if (it.direction == "down")
if (!it.moveBottom()) it.flagUsed = false
if (it.direction == "up")
if (!it.moveTop()) it.flagUsed = false
}
}
/**
* 碰撞检测
*/
private fun collision(ep: EnemyPlane) {
// 获取敌机矩形
val eRect = ep.getRect()
// 获取玩家矩形
val pRect = PlayerPlane.getInst().getRect()
// 检测是否和玩家飞机碰撞
if (MathUtils.cross(eRect, pRect)) {
// 玩家被击中
PlayerPlane.hit()
// 敌机置为空闲
ep.flagUsed = false
// 敌机爆炸
BombManager.getInst().obtain(
eRect.centerX().toFloat(),
eRect.centerY().toFloat(),
ep.getPlaneImage().width / 2.toFloat()
)
// 通过消息总线发给主界面更新计分控件
LiveEventBus.get(AppHelper.COLLI_EVENT, Int::class.java).post(1)
}
}
fun draw(canvas: Canvas?) {
if (canvas == null) return
enemyList.filter { it.flagUsed }.forEach {
canvas.drawBitmap(it.getPlaneImage(), it.x, it.y, null)
}
}
/**
* 判断子弹是否击中飞机
* @return 返回值表示飞机是否中弹
*/
fun isHit(bulletPoint: Rect, bulletLevel: Int): Point? {
enemyList.filter { it.flagUsed }.forEach {
// 获取飞机的矩形
val px = it.x.toInt()
val py = it.y.toInt()
val width = it.getPlaneImage().width
val height = it.getPlaneImage().height
val right = width + px
val bottom = height + py
// 计算矩形框内缩1/5的比例
val scaleW = width / 5
val scaleH = height / 5
val planeRect = Rect(px + scaleW, py + scaleH, right - scaleW, bottom - scaleH)
if (MathUtils.cross(planeRect, bulletPoint)) {
it.hp -= bulletLevel // 根据子弹等级减血
// 敌机血降为0,标记对象为false,可被再次复用
if (it.hp <= 0) {
it.flagUsed = false
// 根据分数给子弹升级
when (PlayerPlane.hitCount++) {
in (0..10) -> BulletManager.level = 1
in (10..20) -> BulletManager.level = 2
else -> {
BulletManager.level = 3
// 击落一定数量的飞机就奖励一次爆雷
if ((PlayerPlane.hitCount % 100) == 0) {
LiveEventBus.get(AppHelper.BOMB_ADD_EVENT, Int::class.java).post(1)
}
}
}
LiveEventBus.get(AppHelper.FIRE_EVENT, Int::class.java)
.post(PlayerPlane.hitCount)
// 更新界面上的分数
LiveEventBus.get(AppHelper.SCORE_EVENT, Int::class.java).post(it.score)
// 子弹击中,返回爆炸中心坐标
return Point(px + (width / 2), py + (height / 2))
}
// 子弹击中,需要回收子弹资源
return Point(-1, -1)
}
}
// 子弹未击中
return null
}
/**
* 全屏炸弹
*/
fun fullScreenBomb() {
// 清飞机
enemyList.filter { it.flagUsed }.forEach {
BombManager.getInst().obtain(
it.x, it.y,
(it.getPlaneImage().width / 2).toFloat()
)
// 爆雷击落的飞机只算1分
LiveEventBus.get(AppHelper.SCORE_EVENT, Int::class.java).post(1)
it.flagUsed = false
}
// 清子弹
BulletManager.getInst().clearBullet()
}
}
github完整源码:https://github.com/greentea107/PlaneGame
传送门 - Android开发:不用游戏引擎也能做游戏
传送门 - Android游戏教程:从SurfaceView开始
传送门 - Android游戏教程:背景卷轴
传送门 - Android游戏教程:玩家飞机
点击链接加入QQ群聊:https://jq.qq.com/?_wv=1027&k=5z4fzdT
或关注微信公众号:口袋里的安卓
网友评论