安卓小游戏:俄罗斯方块

2023-11-02

安卓小游戏:俄罗斯方块

前言

最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏,还是比较简单的,这几天又把俄罗斯方块还原了一下,写了一天,又摸鱼调试了两天,逻辑不是很难,但是要理清、处理对还是有点东西的。

需求

这里的需求玩过的都知道,简单说就是控制四种砖块将底部填满,砖块可以进行旋转,当砖块超过顶部就游戏结束了。核心思想如下:

  • 1,用一个二维数组保存地图信息,显示固定的砖块
  • 2,每次只出现一个砖块,可以左右移动,手指向上移动进行旋转,手指向下移动快速坠落
  • 3,砖块和地图信息有交互,地图信息限制砖块移动和旋转,到达底部或者底部被阻挡会触发固定
  • 4,固定之后,根据砖块更新地图信息,并进行下一轮砖块

效果图

这里网上找的GIF转换工具,只能生成30秒的内容,不过游戏的内容已经显示得差不多了。

tetris

代码

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.abs
import kotlin.collections.indices as indices1

/**
 * 俄罗斯方块view
 */
class TetrisGameView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {
    companion object {
        // 游戏更新间隔,一秒5次
        const val GAME_FLUSH_TIME = 200L
        // 砖块移动间隔,一秒2.5次
        const val TETRIS_MOVE_TIME = 400L
        // 快速模式把更新时间等分
        const val FAST_MOD_TIMES = 10

        // 四个方向
        const val DIR_NULL = -1
        const val DIR_UP = 0
        const val DIR_RIGHT = 1
        const val DIR_DOWN = 2
        const val DIR_LEFT = 3

        // 四种砖块对应的配置,是一个2 * 8的数组, 这里用二进制来保存
        // 顶点左上角,二行四列,默认朝右,方向变换咦左上角旋转
        private const val CONFIG_TYPE_L = 0b1110_1000
        private const val CONFIG_TYPE_T = 0b1110_0100
        private const val CONFIG_TYPE_I = 0b1111_0000
        private const val CONFIG_TYPE_O = 0b0110_0110

        // 砖块类型数组,用于随机生成
        val sTypeArray = intArrayOf(CONFIG_TYPE_L, CONFIG_TYPE_T, CONFIG_TYPE_I, CONFIG_TYPE_O)
    }

    // 屏幕划分数量及等分长度
    private val mRowNumb: Int
    private var mRowDelta: Int = 0
    private val mColNumb: Int
    private var mColDelta: Int = 0

    // 节点掩图
    private val mTetrisMask: Bitmap?

    // 游戏地图是个二维数组
    private val mGameMap: Array<IntArray>

    // 当前操作的方块
    private val mTetris = Tetris(0, 0, 0, 0)

    // 不要在onDraw中创建对象, 在onDraw中临时计算绘制位置
    private val mPositions = ArrayList<MutableTriple<Int, Int, Boolean>>(8).apply {
        for (i in 0..7) add(MutableTriple(0, 0, false))
    }

    // 游戏控制器
    private val mGameController = GameController(this)

    // 画笔
    private val mPaint = Paint().apply {
        color = Color.LTGRAY
        strokeWidth = 1f
        style = Paint.Style.STROKE
        flags = Paint.ANTI_ALIAS_FLAG
    }

    // 上一个触摸点X、Y的坐标
    private var mLastX = 0f
    private var mLastY = 0f

    init {
        // 读取配置
        val typedArray =
            context.obtainStyledAttributes(attrs, R.styleable.TetrisGameView)

        // 横竖划分
        mRowNumb = typedArray.getInteger(R.styleable.TetrisGameView_rowNumber, 30)
        mColNumb = typedArray.getInteger(R.styleable.TetrisGameView_colNumber, 20)

        // 根据行数和列数生成地图
        mGameMap = Array(mRowNumb){ IntArray(mColNumb)}

        // 节点掩图
        val drawable = typedArray.getDrawable(R.styleable.TetrisGameView_tetrisMask)
        mTetrisMask = if (drawable != null) drawableToBitmap(drawable) else null

        typedArray.recycle()
    }

    private fun drawableToBitmap(drawable: Drawable): Bitmap? {
        val w = drawable.intrinsicWidth
        val h = drawable.intrinsicHeight
        val config = Bitmap.Config.ARGB_8888
        val 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)
        mRowDelta = h / mRowNumb
        mColDelta = w / mColNumb
        // 开始游戏
        load()
    }

    // 加载
    private fun load() {
        mGameController.removeMessages(0)
        mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)
    }

    // 重新加载
    private fun reload() {
        mGameController.removeMessages(0)
        // 清空界面
        for (array in mGameMap) {
            array.fill(0)
        }
        mGameController.isNewTurn = true
        mGameController.isGameOver = false
        mGameController.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)
        // 绘制网格,mColDelta * mColNumb.toFloat() != width
        for (i in 0..mRowNumb) {
            canvas.drawLine(0f, mRowDelta * i.toFloat(),
                mColDelta * mColNumb.toFloat(), mRowDelta * i.toFloat(), mPaint)
        }
        for (i in 0..mColNumb) {
            canvas.drawLine(mColDelta * i.toFloat(), 0f,
                mColDelta * i.toFloat(), mRowDelta * mRowNumb.toFloat(), mPaint)
        }

        // 绘制地图元素, (i, j)表示第i行,第j列
        for (i in mGameMap.indices1) {
            val array = mGameMap[i]
            for (j in array.indices1) {
                if (mGameMap[i][j] > 0) {
                    canvas.drawBitmap(mTetrisMask!!,
                        j * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,
                        i * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,
                        mPaint)
                }
            }
        }

        // 绘制当前砖块,仅绘制,碰撞、旋转由GameController控制
        for (pos in mPositions) {
            if (pos.third) {
                canvas.drawBitmap(mTetrisMask!!,
                    pos.second * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,
                    pos.first * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,
                    mPaint)
            }
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when(event.action) {
            MotionEvent.ACTION_DOWN -> {
                mLastX = event.x
                mLastY = event.y
            }
            MotionEvent.ACTION_MOVE -> {}
            MotionEvent.ACTION_UP -> {
                val lenX = event.x - mLastX
                val lenY = event.y - mLastY

                // 只更改方向,逻辑由GameController处理,方向更改成功与否需要确认
                if (abs(lenX) > abs(lenY)) {
                    // 左右移动
                    // val delta = (lenX / mColDelta).toInt()
                    // mGameController.colDelta = delta
                    mGameController.colDelta = if (lenX > 0) 1 else -1
                }else {
                    if (lenY >= 0) {
                        // 往下滑动加快
                        mTetris.fastMode = true
                    }else {
                        // 往上移动切换形态
                        mGameController.newDirection = mTetris.dir + 1
                        if (mGameController.newDirection > 3) {
                            mGameController.newDirection = 0
                        }
                    }
                }
            }
        }
        return true
    }

    private fun gameOver() {
        AlertDialog.Builder(context)
            .setTitle("继续游戏")
            .setMessage("请点击确认继续游戏")
            .setPositiveButton("确认") { _, _ -> reload() }
            .setNegativeButton("取消", null)
            .create()
            .show()
    }

    // kotlin自动编译为Java静态类,控件引用使用弱引用
    class GameController(view: TetrisGameView): Handler(Looper.getMainLooper()){
        // 控件引用
        private val mRef: WeakReference<TetrisGameView> = WeakReference(view)
        // 防止大量生成对象
        private val mTempPositions =
            ArrayList<MutableTriple<Int, Int, Boolean>>(8).apply {
            for (i in 0..7) add(MutableTriple(0, 0, false))
        }
        // 新砖块
        internal var isNewTurn = true
        // 左右移动
        internal var colDelta = 0
        // 更改的新方向
        internal var newDirection = DIR_NULL
        // 游戏结束标志
        internal var isGameOver = false
        // 砖块移动控制变量,让左右移动能比向下移动快一步
        private var mMoveCounter = 0

        override fun handleMessage(msg: Message) {
            mRef.get()?.let { gameView ->
                // 新一轮砖块
                startNewTurn(gameView)

                // 移动前先校验旋转和左右移动
                val movable = preMoveCheck(gameView)

                if (movable) {
                    // 移动砖块
                    moveTetris(gameView)
                }else {
                    // 固定砖块
                    settleTetris(gameView)
                    // 检查消除底层
                    checkRemove(gameView)
                }

                // 循环发送消息,刷新页面
                gameView.invalidate()
                if (!isGameOver) {
                    if (gameView.mTetris.fastMode) {
                        gameView.mGameController.sendEmptyMessageDelayed(0,
                            GAME_FLUSH_TIME / FAST_MOD_TIMES)
                    }else {
                        gameView.mGameController.sendEmptyMessageDelayed(0, GAME_FLUSH_TIME)

                    }
                }else {
                    gameView.gameOver()
                }
            }
        }

        private fun startNewTurn(gameView: TetrisGameView) {
            if (isNewTurn) {
                // 保留旋转空余
                val col = (3 + Math.random() * (gameView.mColNumb - 6)).toInt()
                val type = sTypeArray[(Math.random() * 4).toInt()]
                gameView.mTetris.dir = (Math.random() * 4).toInt()
                // 因为旋转所以要保证在界面内
                gameView.mTetris.posRow = 0 + when(gameView.mTetris.dir) {
                    DIR_LEFT -> 1
                    DIR_UP -> 2
                    else -> 0
                }
                gameView.mTetris.posCol = col
                gameView.mTetris.config = type
                gameView.mTetris.fastMode = false

                isNewTurn = false
            }
        }

        private fun preMoveCheck(gameView: TetrisGameView): Boolean {
            // 一个一个校验,罗嗦了但是结构清晰
            val tetris = gameView.mTetris

            // 方向预测
            if (newDirection != DIR_NULL) {
                getPositions(mTempPositions, tetris, tetris.posRow, tetris.posCol, newDirection)
                val flag = checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,
                    gameView.mGameMap)
                if (flag) {
                    tetris.dir = newDirection
                }
                newDirection = DIR_NULL
            }

            // 左右预测
            if (colDelta != 0) {
                getPositions(mTempPositions, tetris, tetris.posRow, tetris.posCol + colDelta, tetris.dir)
                val flag = checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,
                    gameView.mGameMap)
                if (flag) {
                    tetris.posCol += colDelta
                }
                colDelta = 0
            }

            // 向下移动预测
            getPositions(mTempPositions, tetris, tetris.posRow + 1, tetris.posCol, tetris.dir)
            return checkOutAndOverlap(mTempPositions, gameView.mRowNumb, gameView.mColNumb,
                gameView.mGameMap)
        }

        // 根据条件获得positions,直接定义不同方向的dir_type会更好吗?其实也要确定锚点,一样的
        private fun getPositions(positions: ArrayList<MutableTriple<Int, Int, Boolean>>,
            tetris: Tetris, posRow: Int, posCol: Int, dir: Int) {
            for (i in 0..1) for (j in 0..3) {
                val index = i * 4 + j
                // 按位取得配置
                val mask = 1 shl (7 - index)
                val flag = tetris.config and mask == mask
                val triple = positions[index]
                // 将不同方向对应的位置转换到config的顺序,并保存该位置是否绘制的flag
                triple.third = flag
                var optimizedDir = dir
                // 对方块和条形类型特别优化
                if (tetris.config == CONFIG_TYPE_O) optimizedDir = DIR_RIGHT
                if (tetris.config == CONFIG_TYPE_I && dir >= DIR_DOWN) {
                    optimizedDir = dir - 2
                }
                when(optimizedDir) {
                    // 以o为锚点旋转,再优化,左边为旋转后,右边为优化后,目的:减小影响范围,限制在矩形内
                    // 一开始以右向左上角旋转,范围是7*7,可通过取值的变换,变换为5*5或者4*4的矩阵
                    // - x x - -
                    // - x x x -      - x x -
                    // x x o x x      x o o x
                    // x x x x x      x o o x
                    // - - x x -  =>  - x x -

                    // 右向,基础型
                    // o x x x      x o x x
                    // x x x x  ->  x x x x
                    DIR_RIGHT -> {
                        triple.first = posRow + i
                        triple.second = posCol + j - 1
                    }
                    // 下向
                    // x o      x x
                    // x x      x o
                    // x x      x x
                    // x x  ->  x x
                    DIR_DOWN -> {
                        triple.first = posRow + j - 1
                        triple.second = posCol - i
                    }
                    // 左向
                    // x x x x      x x x x
                    // x x x o  ->  x x o x
                    DIR_LEFT -> {
                        triple.first = posRow - i
                        triple.second = posCol - j + 1
                    }
                    // 上向
                    // x x      x x
                    // x x      x x
                    // x x      o x
                    // o x  ->  x x
                    DIR_UP -> {
                        triple.first = posRow - j + 1
                        triple.second = posCol + i
                    }
                    else -> {}
                }
            }
        }

        // 检测出界和重叠,下边和左右
        private fun checkOutAndOverlap(positions: ArrayList<MutableTriple<Int, Int, Boolean>>,
            rowNumb: Int, colNumb: Int, gameMap: Array<IntArray>): Boolean {
            var flag = true
            for (pos in positions) {
                // 只对有值的位置进行验证
                if(!pos.third) continue

                // 出下界
                if (pos.first >= rowNumb) {
                    flag = false
                    break
                }

                // 左右出界
                if (pos.second >= colNumb || pos.second < 0) {
                    flag = false
                    break
                }

                // 旋转后有冲突,暂且忽略上边之外的情况
                if (pos.first < 0) continue
                if (pos.third && gameMap[pos.first][pos.second] > 0) {
                    flag = false
                    break
                }
            }

            return flag
        }

        private fun moveTetris(gameView: TetrisGameView) {
            val tetris = gameView.mTetris
            // 对向下移动控制,左右移动、旋转不限制
            mMoveCounter++
            if (mMoveCounter == (TETRIS_MOVE_TIME / GAME_FLUSH_TIME).toInt()) {
                tetris.posRow += 1
                mMoveCounter = 0
            }
            getPositions(gameView.mPositions, tetris, tetris.posRow, tetris.posCol, tetris.dir)
        }

        private fun settleTetris(gameView: TetrisGameView) {
            // 固定砖块的位置,moveTetris已经将位置放到了gameView.mPositions中
            for (pos in gameView.mPositions) {
                if (pos.third) {
                    // 注意这里位置超出屏幕上方就是游戏结束
                    if (pos.first < 0) {
                        isGameOver
                    }else {
                        gameView.mGameMap[pos.first][pos.second] = 1
                    }
                }
            }

            isNewTurn = true
        }

        private fun checkRemove(gameView: TetrisGameView) {
            // 应该从顶层到底层检查,这样消除后的移动逻辑才没错,就是复杂了点
            val gameMap = gameView.mGameMap
            for (i in gameMap.indices1) {
                val array = gameMap[i]
                var isFull = true
                for (peer in array) {
                    if (peer <= 0) {
                        isFull = false
                    }
                }

                // 消除,数组移位就行了
                if (isFull) {
                    for (j in (i - 1) downTo 0) {
                        // 把上面一层的数据填到当前层即可,最后会填到空层
                        val cur = gameMap[j + 1]
                        val another = gameMap[j]
                        for (k in cur.indices1) {
                            cur[k] = another[k]
                        }
                    }
                    // 最顶上填空
                    gameMap[0].fill(0)
                }
            }
        }
    }

    /**
     * 供外部回收资源
     */
    fun recycle()  {
        mTetrisMask?.recycle()
        mGameController.removeMessages(0)
    }

    // 砖块,以左上角为旋转中心旋转
    data class Tetris(
        var posRow: Int,
        var posCol: Int,
        var dir: Int,
        var config: Int,
        var fastMode: Boolean = false)

    data class MutableTriple<T, V, R>(var first: T, var second: V, var third: R)
}

对应style配置

res -> values -> tetris_game_view_style.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name ="TetrisGameView">
        <attr name="rowNumber" format="integer"/>
        <attr name="colNumber" format="integer"/>
        <attr name="tetrisMask" format="reference"/>
    </declare-styleable>
</resources>

砖块掩图

res -> drawable -> ic_tetris.xml

<vector android:height="24dp" android:tint="#6F6A6A"
    android:viewportHeight="24" android:viewportWidth="24"
    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
    <path android:fillColor="@android:color/white" android:pathData="M18,4L6,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,6c0,-1.1 -0.9,-2 -2,-2zM18,18L6,18L6,6h12v12z"/>
</vector>

layout布局

    <com.silencefly96.module_views.game.TetrisGameView
        android:id="@+id/gamaView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/black"
        app:rowNumber="30"
        app:colNumber="20"
        app:tetrisMask="@drawable/ic_tetris"
        />

主要问题

我这里的代码主要以逻辑模块为主,可能会冗余,但是力求逻辑清晰,下面简单讲讲吧。

资源加载、定时刷新逻辑

老生常谈的问题了,有兴趣的可以看看我前面三个游戏的资源加载、定时刷新逻辑,这里并没有新事物可言,也比较简单。

砖块的类型

这里用八位二进制数来表示砖块,一共四种砖块,前面四位表示第一行,后面四位表示第二行,还是很好理解的。

private const val CONFIG_TYPE_L = 0b1110_1000
private const val CONFIG_TYPE_T = 0b1110_0100
private const val CONFIG_TYPE_I = 0b1111_0000
private const val CONFIG_TYPE_O = 0b0110_0110
// 四种类型
// o o o x   o o o x     o o o o     x o o x
// x x o x   x o x x     x x x x     x o o x

这里还有一个砖块的旋转问题,我这里并没有额外的定义一种砖块对应的四种状态,我这里用四个方向来表示四个状态,后面取值的时候变换下就行了:

const val DIR_NULL = -1
const val DIR_UP = 0
const val DIR_RIGHT = 1
const val DIR_DOWN = 2
const val DIR_LEFT = 3

坐标形式及转换

不同于二维的垂直坐标系,这里用的坐标是row和col的数,即第几行第几列的哪个位置。比如,在30*20的地图里,第一个方块是第0行第0列,最后一个方块是第29行第19列。

可以简单的认为row就是Y坐标,col就是横坐标:

for (i in mGameMap.indices1) {
    val array = mGameMap[i]
    for (j in array.indices1) {
        x = j * mColDelta
        y = i * mRowDelta
    }
}

界面元素绘制

页面上要绘制的东西就三种,网格、已经固定的砖块、可以移动的砖块(仅一个)。

网格绘制的时候要注意下面这个问题,即对width等分之后取Int型有偏差。

mColDelta * mColNumb.toFloat() != width

地图的绘制就根据mGameMap存的值绘制就行了,有值就绘制,无值空着。但是要注意下drawBitmap取的是bitmap的左边和上边,但是地图小方块的宽高和bitmap的宽高不一定一致,即:

mColDelta != mTetrisMask.width;
mRowDelta != mTetrisMask.height

所以,这里要进行一下处理,将bitmap摆放到地图小方块的中间去:

canvas.drawBitmap(mTetrisMask!!,
    j * mColDelta.toFloat() + mColDelta / 2 - mTetrisMask.width / 2,
    i * mRowDelta.toFloat() + mRowDelta / 2 - mTetrisMask.height / 2,
    mPaint)

至于可以移动的砖块,上面用了八位二进制数来表示类型,这里也用size为8的mPositions来保存受移动砖块所影响的八个坐标的信息,onDraw中只要考虑绘制这八个坐标的信息,至于逻辑会在GameController中处理。

方块的控制

在核心思想里面,已经设计了四种控制形式,即左右移动,向上变换,向下加速,只要在onTouchEvent中识别这四个方向,设置好控制变量,剩下的也交给GameController去处理。

GameController

将和游戏逻辑无关的绘制、交互分发出去后,GameController的职责就很清楚了,大致就是一下几个:

  1. 生成新砖块
  2. 检查交互逻辑
  3. 移动
  4. 固定
  5. 消除

新砖块生成

这里用了一个控制变量来控制是否新生成砖块(isNewTurn),当砖块固定后就会触发isNewTurn为true,进行新一轮。

新砖块从上面生成,左右随机,类型及方向随机,这里并没有创建新的对象,因为砖块就一个,更改mTetris的属性就行。

检查交互逻辑

这里的交互就是上面的几个控制,旋转及移动不能出界,如果可能出界就不应该旋转或者移动。这里专门写了一个getPositions函数来获得对应位置、方向被移动砖块影响的坐标列表,传入预测后的位置及方向,得到坐标列表,对这些坐标再进行校验,看看是否出界或者重叠,再回来确定旋转或移动操作是否能进行,能进行才对可移动砖块属性做修改,进入到下一步的移动。

这里专门写了getPositions和checkOutAndOverlap来获取被影响坐标和校验出界或者重叠,checkOutAndOverlap比较简单,下面重点讲下getPositions,这个是这个游戏里面的核心。

游戏核心:getPositions

说白了整个游戏就一个难点,如何确定移动砖块的位置,两种砖块四种状态,八种情况。上面讲到了,砖块的类型是通过8bit来表示的,形式如下:

// 四种类型
// o o o x   o o o x     o o o o     x o o x
// x x o x   x o x x     x x x x     x o o x

上面代表着八个点,计算的时候在方块的坐标(锚点)处将八个点映射到地图上(下面o为锚点):

// o x x x
// x x x x

上面这种情况是对应左向状态的情况,剩下的四种状态是通过旋转来得到的,这里以o为旋转点,可以得到四种情况:

//              x o                 x x
//              x x                 x x
// o x x x      x x     x x x x     x x
// x x x x      x x     x x x o     o x

而实际情况下,我们并不想旋转影响太大的范围,这里就要改一下锚点的位置:

//              x x                 x x
//              x o                 x x
// x o x x      x x     x x x x     o x
// x x x x      x x     x x o x     x x

由同一个锚点展开四种情况的影响位置,得到下面范围,在一个5*5的范围内(或者更进一步到4*4):

// - x x - -
// - x x x -      - x x -
// x x o x x      x o x x
// x x x x x      x x x x
// - - x x -  =>  - x x -

理解清楚原理,就很好写代码了,这里还有两个问题要注意下。第一个是掩码的取值,要从前往后取:

val mask = 1 shl (7 - index)

另一个就是最好对长条和方块特别优化下:

// 对方块和条形类型特别优化
if (tetris.config == CONFIG_TYPE_O) optimizedDir = DIR_RIGHT
if (tetris.config == CONFIG_TYPE_I && dir >= DIR_DOWN) {
    optimizedDir = dir - 2
}

移动砖块

在校验里面已经对向下移动进行了校验,如果能向下移动,只需要调用getPositions把得到的坐标存入gameView.mPositions里面就行了,在onDraw里面会对砖块进行绘制。

固定砖块

如果preMoveCheck里面得到不能再向下移动了,那就应该对砖块进行固定,并开启新一轮砖块。固定的时候只要把砖块影响位置赋值到地图二维数组里就行了。

检查消除

每次固定好砖块,都应该确认下是否需要消除。这里因为涉及到移动地图二维数组,所以应该先从顶层检查,遍历一下。消除的时候把上面的所有数组向下移动,最顶层增加空的array就行了。

快速模式和间隔向下

这里在GameController引入了变量来实现了快速模式和间隔向下,快速模式就是降低handler的发送延时,间隔向下就是通过控制变量让moveTetris延缓向下的移动,留出时间来左右移动或者旋转,更加人性话点。

结语

这里写得有点多了,写一个游戏还是挺有意思的,朋友说这东西没有技术性,我还是觉得只有你做过,你才知道你有没有学到东西,不去做,永远停留在纸面上。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

安卓小游戏:俄罗斯方块 的相关文章

  • 通过数据绑定将字符串传递到包含的布局不起作用

    我正在尝试使用 Android 数据绑定功能将一个简单的字符串从我的主布局传递到布局 它编译得很好 但传递给包含的值实际上并未传递 即 它没有出现在我的布局中
  • 单击 RecyclerView 内的 ImageView 时更改图像资源

    每当我单击它时 单击图像 而不是项目 我都会尝试更改回收器视图内特定位置的设备图像资源 我尝试将 setOnClickListener 放入 onBindViewHolder 方法中 但只有最后一个项目受到影响 这是我的回收者视图 http
  • Android KeyBoard.Key 禁用图标 预览特殊键?

    我通过实现 KeyboardView OnKeyboardActionListener 接口来自定义自己的软键盘 按下按键时 将显示预览弹出窗口 我的问题是如何禁用 SHIFT 和 DELETE 等特殊键的预览弹出窗口 我尝试将 andro
  • 定期运行任务(每天一次/每周一次)

    我想定期 每周 每天一次 运行一些任务 即获取我的网站新闻页面 即使我的应用程序已关闭 是否可以 是的 您需要查看报警管理器 http developer android com reference android app AlarmMan
  • 显示警报或收到通知时的视图

    我正在关注this http tokudu com 2010 how to implement push notifications for android 显示的教程通知 on an 安卓设备 当我在设备上运行该应用程序时 状态栏上会出现
  • Android Studio - 错误:未捕获翻译错误:com.android.dx.cf.code.SimException:本地 0001:无效

    我刚刚使用 Android Studio 设置了一台新计算机 并从 bitbucket 导入了我的项目 问题是我现在在尝试构建项目时遇到此错误 信息 Gradle 任务 app clean app generateDebugSources
  • Google Firebase - 如何删除崩溃报告?

    我最终失明了吗 还是没有明显的方法可以通过 Google Firebase Web 控制台删除 Firebase 崩溃报告 我的 Android 应用程序已成功记录报告 但现在出现大量 开发崩溃 占用了我在控制台中的大部分视图 这使得找到实
  • 访问角落里的存储

    我能找到的与文件存储有关的最接近文档的是这个帖子 http nookdeveloper zendesk com entries 20257971 updated what are the size constraints on my app
  • Android中不同线程的数据库访问

    我有一个在 AsyncTasks 中从互联网下载数据的服务 它解析数据并将其存储在数据库中 该服务持续运行 当服务写入数据库时 活动会尝试从数据库中读取更改 我有一个数据库助手 有多种写入和读取方法 这会导致问题吗 可能尝试从两个不同的线程
  • SQLite支持android的数据类型有哪些

    谁能告诉我 SQLITE 中支持 ANDROID 的数据类型列表 我想确认 TIME 和 DATE 数据类型 这里有一个list http www sqlite org datatype3 htmlSQLite 的数据类型 支持时间和日期间
  • AnalyticsService 未在应用程序清单中注册 - 错误

    我正在尝试使用 sdk 中提供的以下文档向 Android 应用程序实施谷歌分析服务 https developers google com analytics devguides collection android v4 https d
  • 如何从android获取应用程序安装时间

    我尝试了一些方法 但没有成功 请帮助我 PackageManager pm context getPackageManager ApplicationInfo appInfo pm getApplicationInfo app packag
  • Android模拟器分配内存失败8

    当我尝试从 Eclipse 运行 WXGA800 模拟器时 出现如下错误 Failed to allocate memory 8 This application has requested the Runtime to terminate
  • 加快 ImageView 中的缩放功能

    我目前正在处理非常大的图像 7 10mb 由于多种原因无法调整大小或压缩 现在 我们的想法是在自定义 ImageView 中显示它们 使用户能够进行双击缩放 捏合缩放等 我使用这个库来完成这项工作 https github com Mike
  • jar 中的 apklib 有什么优点?

    我正在关注这个问题 https stackoverflow com questions 6059502 whats the difference between apklib and jar files但它并没有完全回答我的问题 jar 中
  • 使用嵌套的 hashmap 参数发送 volley 请求

    我正在使用 android volley 框架向我的服务器发送 jsonobject 请求 get 请求工作正常 现在我想发送一个带有请求参数的 post 请求 该请求参数是嵌套的 hashmap 我重写 getparams 方法 但它期望
  • 在 Android 中上传文件出现内存不足错误

    我的上传代码如下 String end r n String twoHyphens String boundary try URL url new URL ActionUrl HttpURLConnection con HttpURLCon
  • Android - iphone 风格 tabhost [关闭]

    就目前情况而言 这个问题不太适合我们的问答形式 我们希望答案得到事实 参考资料或专业知识的支持 但这个问题可能会引发辩论 争论 民意调查或扩展讨论 如果您觉得这个问题可以改进并可能重新开放 访问帮助中心 help reopen questi
  • 如何在Android中解析xml类型的HTTPResponse

    我有一个 Android 应用程序 我使用 POST 方法来获取响应 这是我的代码 HttpResponse httpResponse httpclient execute httppost HttpEntity resEntity htt
  • 具有矢量可绘制的 ImageView 的 Resources$NotFoundException

    我遇到了崩溃 Resources NotFoundException用于在活动创建时绘制的矢量 21 日前崩溃 安卓工作室2 1 支持库24 0 0 Gradle插件2 1 0 目标SDK 23 最小SDK 15 buildTools版本

随机推荐

  • EasyExcel 对已有文件追加sheet页

    背景 用户上传了一个excel 在不动已有数据的情况下 进行追加sheet页和数据操作 默认情况下 write操作会把其他sheet页和数据清空掉 复制一份上传的excel进行重命名 FileUtils copyFile sourceFil
  • 大华web插件

    因为项目需要 需要把大华视频监控移至到网页端 在网上找了很多资料 最终找到下面链接的插件包 本人自己调试了一下 可以正常运行 而且包中附带有详细的二次开发资料 很方便 测试浏览器使用360安全浏览器 并且在兼容模式和极速模式下均可正常使用
  • 安卓计算器

    本项目采用Java语言 xml布局 Android studio开发 使用API34 项目一共两个页面 简单计算器和专业计算器 当然也不专业 该项目主要借鉴了小米计算器的键盘布局 简约风 其实是我太菜辣 特别提醒其中的百分号是取模运算 并不
  • 刷脸支付帮商户降低人力成本引流圈客

    被支付宝 微信量大巨头推上风口浪尖的黑科技刷脸支付 不但具备超高的投资潜力 同时也给用户带来了便捷的支付体验 给商家带来了诸多好处 包括提升收银效率 降低人力成本 引流圈客等 随着刷脸支付使用人数的增长 很多商户都开始尝试使用刷脸支付设备
  • 元宇宙的六大核心技术

    1 区块链技术 哈希算法及时间戳技术 数据传播及验证机制 共识机制 分布式存储 智能合约 分布式账本 2 交互技术 VR虚拟现实技术 AR增强现实技术 MR混合现实技术 全息影像技术 脑机交互技术 传感技术 体感 环境等 3 电子游戏技术
  • STC8学习-ADC2

    1 ADC第15通道内部电压的使用 首先是电压值的查询 这里需要通过指针获取内部电压值 unsigned int VDDA BGV 定义指针变量 BGV int idata 0xef 获取内部电压值 放在主函数前面执行一次就可以了 2 例程
  • APP是什么? 为什么叫APP APP全称是什么

    应用程序 Application的缩写 APP 应用程序 外语缩写 App 外语全称 Application APP狭义指智能手机的第三方应用程序 广义指所有客户端软件 现多指移动应用程序 app是什么意思 一 最常用的解决APP是什么意思
  • FLASHBACK TABLE ora-01031 权限不足

    FLASHBACK TABLE emp TO timestamp to timestamp 2013 04 08 16 10 59 yyyy mm dd hh24 mi ss 执行上边语句 一直在报权限不足 D flashback tabl
  • qt 等待线程结束_QT5线程关闭

    QT5线程关闭 QThread析构函数的说明 请注意 删除一个QThread对象不会停止它管理的线程的执行 删除正在运行的QThread 即isFinished 返回false 将导致程序崩溃 在删除QThread之前等待finished
  • QT5.12编译MQTT 5.13图文详细版

    看到很多小伙伴都在用mqtt 在此记录下 1 下载最新的MQTT源码 https github com emqx qmqtt git 2 使用Qt qcreator打开 qtmqtt qtmqtt pro工程文件 或者使用步骤9终极杀招只需
  • 零基础 SQL 数据库小白,从入门到精通的学习路线与书单

    一 学SQL的苦恼 我观察了 865 个 SQL 入门者 发现大家在学习 SQL 的时候 最大的问题不是 SQL 语法 而是对 SQL 原理的不熟悉 很多初学者 往往被 SQL 能做什么 为什么会有 SQL 这门语言给迷惑到了 他们用学英语
  • java 纯面向对象_Java是纯粹的面向对象语言吗?

    码农公社 210 net cn 210 1024 10月24日一个重要的节日 码农 程序员 节 纯面向对象语言支持或具有将程序内的所有内容视为对象的功能 不支持原始数据类型 如 int char float bool 等 什么是纯面向对象语
  • vue3中使用vue3-seamless-scroll(最新版本滚动插件)

    vue3中使用vue3 seamless scroll 最新版本滚动插件 Vue3 0 无缝滚动组件 支持Vite2 0 支持服务端打包 官方git地址 npm安装 npm install vue3 seamless scroll save
  • BERT,Transformer的模型架构与详解

    BERT Transformer的模型架构与详解 1 1 认识BERT 学习目标 了解什么是BERT 掌握BERT的架构 掌握BERT的预训练任务 什么是BERT BERT是2018年10月由Google AI研究院提出的一种预训练模型 B
  • 强制卸载symantec

    在网上找了一堆的文章 都没有解决我的问题 最后Google了一把 还是利用symantec自己出的工具 CleanWipe zip 把它卸载了 正所谓解铃还需系铃人啊 太无耻了 分割线 要下载一个工具CleanWipe zip来卸载 然后打
  • GDAL源码剖析(二)之编译说明

    转载自 http blog csdn net liminlu0314 article details 6937194 一 简单的编译 1 使用VisualStudio IDE编译 首先进入GDAL的源代码目录 可以看到有几个sln为后缀的文
  • vue可以用ajax,Vue 中使用Ajax请求

    Vue 项目中常用的 2 个 ajax 库 一 vue resource vue 插件 非官方库 vue1 x 使用广泛 vue resource 的使用 下载 npm install vue resource save 编码 引入模块 注
  • 『学Vue2+Vue3』声明式导航

    day06 一 声明式导航 导航链接 1 需求 实现导航高亮效果 如果使用a标签进行跳转的话 需要给当前跳转的导航加样式 同时要移除上一个a标签的样式 太麻烦 2 解决方案 vue router 提供了一个全局组件 router link
  • ES6使用递归实现深拷贝

    使用递归实现深拷贝 let deepCopy function obj 判断拷贝的要进行深拷贝的是数组还是对象 是数组的话进行数组拷贝 对象的话进行对象拷贝 let newObj obj instanceof Array if obj ty
  • 安卓小游戏:俄罗斯方块

    安卓小游戏 俄罗斯方块 前言 最近用安卓自定义view写了下飞机大战 贪吃蛇 小板弹球三个游戏 还是比较简单的 这几天又把俄罗斯方块还原了一下 写了一天 又摸鱼调试了两天 逻辑不是很难 但是要理清 处理对还是有点东西的 需求 这里的需求玩过