Android游戏教程:玩家飞机

2020-09-17  本文已影响0人  超级绿茶
飞机大战Demo

熟悉Android开发的应该知道,玩家对于飞机的各种移动和开火等操作使用的是Android的事件分发机制来的。在开始着手编写一个可以在屏幕上到处移动的飞机之前我们需要先构建一个飞机的父类,并以此父类派生出玩家飞机和程序控制的敌机。因为我们需要先创建一个名为Plane.kt的父类

open class Plane {
    var x = 0f
    var y = 0f
    protected var speed = 1f // 飞机移动的速度
    var hp = 1 // 飞机血量
    private var isEnableOut = false // 是否允许飞出边界
    private lateinit var bmpPlane: Bitmap

    /**
     * 根据文件名加载飞机的位图,加载后的位图会放在map集合中缓存起来,以便下回取出
     */
    open fun setPlaneImage(img: String) {
        this.bmpPlane = BitmapCache.loadBitmap(img)
    }

    fun getPlaneImage(): Bitmap {
        return bmpPlane
    }

    fun setLocation(x: Float, y: Float) {
        this.x = x
        this.y = y
    }

    fun setEnableOut(isEnableOut: Boolean) {
        this.isEnableOut = isEnableOut
    }

    fun moveLeft() {
        this.x -= this.speed
        if (this.x <= 0) this.x = 0f
    }

    fun moveRight() {
        this.x += this.speed
        if (this.x >= (AppHelper.bound.right - bmpPlane.width)) {
            this.x = (AppHelper.bound.right - bmpPlane.width).toFloat()
        }
    }

    fun moveTop(): Boolean {
        this.y -= this.speed
        val topSide =
            if (isEnableOut) AppHelper.bound.top - bmpPlane.height else AppHelper.bound.top
        if (this.y <= topSide) {
            this.y = topSide.toFloat()
            return false
        }
        return true
    }

    fun moveBottom(): Boolean {
        this.y += this.speed
        val bottomSide =
            if (isEnableOut) AppHelper.bound.bottom else AppHelper.bound.bottom - bmpPlane.height
        if (this.y > bottomSide) {
            this.y = bottomSide.toFloat()
            return false
        }
        return true
    }

    open fun draw(canvas: Canvas?) {
        if (canvas == null) return
        canvas.drawBitmap(bmpPlane, x, y, null)
    }
}

在Koltin中要让一个类可以被继承需要在class之前加上open关键字。在这个父类中我们实现了一些基本的功能,能够加载图片,能够上下左右的移动,能够在canvas上根据坐标绘制飞机的位图。

在Plane类中有一个isEnableOut变量,这个变量主要是控制当飞机的坐标到达屏幕边缘时是否继续让飞机离开屏幕;这个功能主要是为程序控制的飞机使用的,正常情况下玩家控制的飞机到达屏幕边缘时坐标就不再变动,使飞机停在屏幕边缘,但程序控制的飞机则需要能继续飞出屏幕,而不是很突兀的边缘消失。

另外Plane类的setPlaneImage方法是用于从本地加载飞机位图的方法。由于考虑到之后的程序飞机会有很多位图加载的操作,所以特地写了一个位图加载的工具类;可以将加载过的位图保存到LruCache里缓存起来,以便于下次的使用,省去每次从文件加载的开销;

object BitmapCache {
    private const val SCALE = 1080 / 720 // 缩放比例
    private val mapCache = LruCache<String, Bitmap>(32)

    fun loadBitmap(imgUrl: String): Bitmap {
        // 如果位图没有缓存过的话,加载位图并缩放大小到合适的比例
        return if (mapCache[imgUrl] == null) {
            // 加载位图
            val bmp = BitmapFactory.decodeStream(MyApp.context.assets.open(imgUrl))
            // 对位图按比例缩放
            val bmpTemp = Bitmap.createBitmap(
                bmp.width * SCALE, bmp.height * SCALE,
                Bitmap.Config.ARGB_8888
            )
            val canvas = Canvas(bmpTemp)
            canvas.drawBitmap(
                bmp,
                Rect(0, 0, bmp.width, bmp.height),
                Rect(0, 0, bmpTemp.width, bmpTemp.height), null
            )
            // 缓存位图
            mapCache.put(imgUrl, bmpTemp)
            bmpTemp
        } else
            mapCache[imgUrl]!!
    }
}

有了飞机的父类后就可以派生玩家飞机PlayerPlane,PlayerPlane类除了继承了Plane类的方法外还需要实现对玩家操作的响应,持续按下某个链后飞机不断的移动,但这种移动是有时间间隔,如果没有间隔的话就会出现飞机瞬移的情况。同样的,当玩家持续的按下了开火链后,我们除了持续的发射子弹外还需要对子弹发射的时间间隔做处理,不然就会看到一条密集的直线。所以梳理一下PlayerPlane除了继承来的方法外还需要实现如下功能:

考虑到主界面和其它的对象(程序飞机类以及子弹类)都需要获取玩家飞机的数据,我们把PlayerPlane设计成单例模式,之后程序飞机和子弹类也都采用这个模式。

class PlayerPlane private constructor() : Plane() {
    private object Holder {
        val instance = PlayerPlane()
    }

    companion object {
        fun getInst() = Holder.instance
    }
}

然后在PlayerPlane中设置两个线程:一个用于管理飞机的移动,另一个用于管理发射子弹的间隔。并且通过成员变量来控制移动的方向和是否开火。我们将这步封装成方法中。

    private lateinit var executors: ExecutorService
    private var isLeft = false
    private var isRight = false
    private var isTop = false
    private var isBottom = false
    private var isAttack = false
    private var isEntrance = false // 是否处于入场阶段
    private var startMillis = 0L
    ...
    private fun init() {
        setPlaneImage("mine.png") // 加载玩家飞机的位图
        executors = Executors.newFixedThreadPool(2)
        // 飞行运行协程
        executors.submit {
            while (AppHelper.isRunning) {
                if (AppHelper.isPause) continue
                if (isEntrance) { // 玩家飞机是否处于入场阶段
                    entrance()
                } else {
                    if (isRight) moveRight()
                    if (isLeft) moveLeft()
                    if (isTop) moveTop()
                    if (isBottom) moveBottom()
                }
                // 延时-运动间隔,没有这个间隔的话会出现瞬移
                Thread.sleep(DELAY_MOTION)
            }
        }
        // 子弹协程:主要是控制子弹的发射间隔不至于变成一条密集的线
        executors.submit {
            while (AppHelper.isRunning) {
                if (AppHelper.isPause) continue
                if (isAttack && !isEntrance)
                    BulletManager.sendPlayerBullet(
                        x.toInt(), y.toInt(),
                        getPlaneImage().width
                    )
                // 延时-子弹射击间隔
                Thread.sleep(DELAY_BULLET)
            }
        }
    }

代码里的isEntrance用于标记玩家飞机是否处于入场阶段。入场阶段指的是玩家飞机从屏幕底部飞至屏幕三分之一处的这段时间,很多游戏都有这种入场动画。这段时间内玩家是不能控制飞机的,直到入场完毕为止。入场方法代码如下:

    private fun entrance() {
        if (isEntrance) {
            val beginY = AppHelper.heightSurface - (AppHelper.heightSurface / 3)
            val endY = AppHelper.heightSurface
            if (y in (beginY..endY)) {
                moveTop()
            } else {
                isEntrance = false
            }
        }
    }

另外我们还给玩家飞机设置了一个保护时间,在这个时间内玩家飞机是无敌,为了在画面中体现这段时间,我们在飞机的外围画了一个类似气泡的圈。

    override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        if (isSafe()) { // 飞机处于无敌时间内就
            drawSafeOval(canvas)
        }
    }

    private val paintOval = Paint()
    /**
     * 在飞机的外围画圆,表示飞机处于保护中
     */
    private fun drawSafeOval(canvas: Canvas?) {
        val cx = getCenter().x.toFloat()
        val cy = getCenter().y.toFloat()
        val radius = getSize().right / 2
        paintOval.let {
            val gradient = RadialGradient(
                cx, cy, radius * 1.5f + 0.1f,
                intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.WHITE), null,
                Shader.TileMode.CLAMP
            )
            it.alpha = 100
            it.setShader(gradient)
        }
        canvas?.drawCircle(cx, cy, radius * 1.5f, paintOval)
    }

最后看一下PlayerPlane.kt文件


/**
 * 玩家飞机
 */
class PlayerPlane private constructor() : Plane() {
    private lateinit var executors: ExecutorService
    private var isLeft = false
    private var isRight = false
    private var isTop = false
    private var isBottom = false
    private var isAttack = false
    private var isEntrance = false
    private var startMillis = 0L

    private object Holder {
        val instance = PlayerPlane()
        const val SAFE_TIME = 5000L //无敌时间
    }

    companion object {
        private const val DELAY_MOTION = 2L
        private const val DELAY_BULLET = 100L
        var hitCount = 0 // 累加连接击落数,用于升级子弹
        fun getInst() = Holder.instance
        fun init(): PlayerPlane {
            getInst().let {
                it.init()
                it.reset()
            }
            return getInst()
        }

        /**
         * 玩家被击落
         */
        fun hit() {
            if (getInst().isSafe()) return
            // 玩家飞机爆炸
            BombManager.getInst().obtain(
                getInst().getCenter().x.toFloat(),
                getInst().getCenter().x.toFloat(),
                getInst().getSize().right / 2.toFloat()
            )
            getInst().reset()
        }
    }

    private fun init() {
        setPlaneImage("mine.png") // 加载玩家飞机的位图
        executors = Executors.newFixedThreadPool(2)
        // 飞行运行协程
        executors.submit {
            while (AppHelper.isRunning) {
                if (AppHelper.isPause) continue
                if (isEntrance) { // 玩家飞机是否处于入场阶段
                    entrance()
                } else {
                    if (isRight) moveRight()
                    if (isLeft) moveLeft()
                    if (isTop) moveTop()
                    if (isBottom) moveBottom()
                }
                // 延时-运动间隔,没有这个间隔的话会出现瞬移
                Thread.sleep(DELAY_MOTION)
            }
        }
        // 子弹协程:主要是控制子弹的发射间隔不至于变成一条密集的线
        executors.submit {
            while (AppHelper.isRunning) {
                if (AppHelper.isPause) continue
                if (isAttack && !isEntrance)
                    BulletManager.sendPlayerBullet(
                        x.toInt(), y.toInt(),
                        getPlaneImage().width
                    )
                // 延时-子弹射击间隔
                Thread.sleep(DELAY_BULLET)
            }
        }
    }

    /**
     * 重置玩家
     */
    fun reset() {
        startMillis = System.currentTimeMillis()
        // 连接击落数和火力,爆雷重置
        hitCount = 0
        BulletManager.level = 1
        LiveEventBus.get(AppHelper.FIRE_EVENT, Int::class.java).post(hitCount)
        LiveEventBus.get(AppHelper.BOMB_RESET_EVENT, Int::class.java).post(3)
        // 玩家飞机的初始位置
        val x = (AppHelper.widthSurface / 2).toFloat()
        val y = AppHelper.heightSurface.toFloat()
        getInst().setLocation(x, y)
        isEntrance = true
    }

    /**
     * 玩家入场,此方法会在线程中循环调用。
     * 玩家飞机由屏幕底部飞机底部三分之一处为止
     * 入场时不能操作,不能开火
     */
    private fun entrance() {
        if (isEntrance) {
            val beginY = AppHelper.heightSurface - (AppHelper.heightSurface / 3)
            val endY = AppHelper.heightSurface
            if (y in (beginY..endY)) {
                moveTop()
            } else {
                isEntrance = false
            }
        }
    }

    /**
     * 判断是否处于保持时间内(无敌)
     */
    fun isSafe() = (System.currentTimeMillis() - startMillis) < Holder.SAFE_TIME

    fun actionRight() {
        isRight = true
    }

    fun actionLeft() {
        isLeft = true
    }

    fun actionTop() {
        isTop = true
    }

    fun actionBottom() {
        isBottom = true
    }

    fun releaseAction() {
        isRight = false
        isLeft = false
        isTop = false
        isBottom = false
    }

    fun release() {
        executors.shutdown()
    }

    fun attack(isAttack: Boolean) {
        this.isAttack = isAttack
    }

    fun getCenter(): Point {
        val cx = getPlaneImage().width / 2 + x
        val cy = getPlaneImage().height / 2 + y
        return Point(cx.toInt(), cy.toInt())
    }

    fun getSize() = Rect(0, 0, getPlaneImage().width, getPlaneImage().height)

    /**
     * 获取玩家位于屏幕上的矩形
     */
    fun getRect(): Rect {
        val pr = getPlaneImage().width + x.toInt()
        val pb = getPlaneImage().height + y.toInt()
        val scaleWidth = getPlaneImage().width / 5
        val scaleHeight = getPlaneImage().height / 5
        return Rect(
            x.toInt() + scaleWidth,
            y.toInt() + scaleHeight,
            pr - scaleWidth,
            pb - scaleHeight
        )
    }

    override fun draw(canvas: Canvas?) {
        super.draw(canvas)
        if (isSafe()) { // 飞机处于无敌时间内就
            drawSafeOval(canvas)
        }
    }

    private val paintOval = Paint()
    /**
     * 在飞机的外围画圆,表示飞机处于保护中
     */
    private fun drawSafeOval(canvas: Canvas?) {
        val cx = getCenter().x.toFloat()
        val cy = getCenter().y.toFloat()
        val radius = getSize().right / 2
        paintOval.let {
            val gradient = RadialGradient(
                cx, cy, radius * 1.5f + 0.1f,
                intArrayOf(Color.TRANSPARENT, Color.TRANSPARENT, Color.WHITE), null,
                Shader.TileMode.CLAMP
            )
            it.alpha = 100
            it.setShader(gradient)
        }
        canvas?.drawCircle(cx, cy, radius * 1.5f, paintOval)
    }
}

github完整源码:https://github.com/greentea107/PlaneGame

传送门 - Android开发:不用游戏引擎也能做游戏
传送门 - Android游戏教程:从SurfaceView开始
传送门 - Android游戏教程:背景卷轴

点击链接加入QQ群聊:https://jq.qq.com/?_wv=1027&k=5z4fzdT
或关注微信公众号:口袋里的安卓

上一篇下一篇

猜你喜欢

热点阅读