安卓小游戏:俄罗斯方块

news2024/11/19 0:47:07

安卓小游戏:俄罗斯方块

前言

最近用安卓自定义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延缓向下的移动,留出时间来左右移动或者旋转,更加人性话点。

结语

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/348471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机械革命z2黑苹果改造计划第三番-macOS键盘快捷键Win键盘适配

macOS键盘快捷键&Win键盘适配 键盘区别 首先下图是苹果妙控键盘无指纹版&#xff0c;官网售价699&#xff0c;穷学生的我是真的买不起 然后下图是我正在使用的机械键盘ikbc w200 87键版本 可以看出两者在键位排列上的区别主要在于 win/command 键&#xff0c;在macOS中大…

12 个华丽的 UI 组件,为您提供设计灵感✨

现代 Web 开发已转向基于组件的架构&#xff0c;从而实现更快的开发、更多的控制和更低的维护成本。在本文中&#xff0c;我精心挑选了一些我最喜欢的 UI 组件作为您的设计灵感。我尝试在我们的开发工作流程中包含不同类型的一些最常用的组件&#xff0c;包括卡片、文本、按钮、…

本人使用的idea插件

文章目录&#x1f68f; 本人使用的idea插件&#x1f6ac; pojo to Json&#x1f6ac; GsonFormatPlus&#x1f6ac; EasyYapi&#x1f6ac; Chinese (Simplified) Language Pack / 中文语言包&#x1f6ac; MyBatis Log Free&#x1f6ac; MyBatisPlusX&#x1f6ac; Statistic…

软件测试如何获得高薪?

软件测试如何获得高薪&#xff1f; 目录&#xff1a;导读 测试基础理论/测试设计能力 业务知识 行业技术知识 数据库 掌握编程语言 搞定自动化测试 质量流程管理 下面谈谈不同level的测试工程师应具备的基本能力 第一个&#xff1a;我们称之为测试员/测试工程师 第二…

Linux C/C++ 调试的那些“歪门邪道”

无数次被问道&#xff1a;你在终端下怎么调试更高效&#xff1f;或者怎么在 Vim 里调试&#xff1f;好吧&#xff0c;今天统一回答下&#xff0c;我从来不在 vim 里调试&#xff0c;因为它还不成熟。那除了命令行 GDB 裸奔以外&#xff0c;终端下还有没有更高效的方法&#xff…

【正点原子FPGA连载】第十二章PS端RTC中断实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十二章PS端RTC…

免费链接投票作品投票通道线上投票活动制作网络投票制作

“文明健康、绿色环保”网络评选投票_免费链接投票_作品投票通道_扫码投票怎样进行现在来说&#xff0c;公司、企业、学校更多的想借助短视频推广自己。通过微信投票小程序&#xff0c;网友们就可以通过手机拍视频上传视频参加活动&#xff0c;而短视频微信投票评选活动既可以给…

node.js+vue大学生校园论坛系统vscode mysql

技术难点 &#xff08;1&#xff09;没有待测试程序文本、控制流程图及有关要求、规范等文件。 &#xff08;2&#xff09;测试用例及测试例程的分析、理解和设计。 &#xff08;3&#xff09;没有开发组织的配合&#xff0c;没有软件测试团队之间的讨论。 &#xff08;4&#…

九龙证券|巴菲特最新操作曝光!刚建仓就大幅减持,台积电盘后暴跌5%

当地时间2月14日&#xff0c;美股三大指数收盘涨跌纷歧。道指跌0.46%&#xff0c;标普500指数跌0.03%&#xff0c;纳指涨0.57%。 大型科技股多数上涨&#xff0c;特斯拉涨7.51%&#xff0c;领涨标普500指数成份股。热门中概股走弱&#xff0c;纳斯达克中国金龙指数跌0.55%&…

【零基础入门前端系列】—CSS介绍(九)

【零基础入门前端系列】—CSS介绍&#xff08;九&#xff09; 一、为什么需要CSS&#xff1f; 使用Css的目的就是让网页具有美观一致的页面, 另外一个最重要的原因是内容与格式 分离,在没有CSS之前&#xff0c;我们想要修改HTML元素的样式需要为每个HTML元素单独定义 样式属性…

用户认证-cookie和session

无状态&短链接 短链接的概念是指&#xff1a;将原本冗长的URL做一次“包装”&#xff0c;变成一个简洁可读的URL。 什么是短链接-> https://www.cnblogs.com/54chensongxia/p/11673522.html HTTP是一种无状态的协议 短链接&#xff1a;一次请求和一次响应之后&#…

女生可以参加IT培训吗?

2023年了&#xff0c;就不要把性别当作选择专业的前提条件了。虽然这句话说过很多次了&#xff0c;作为IT行业来说&#xff0c;是非常欢迎女生的加入&#xff1b;尤其是整天都是面对一大堆男攻城狮&#xff0c;工作氛围一点都不活跃&#xff0c;反而显得压抑和杂乱&#xff0c;…

在Windows上安装Scala

文章目录Windows上安装Scala&#xff08;一&#xff09;到Scala官网下载Scala&#xff08;二&#xff09;安装Scala安装向导&#xff08;三&#xff09;配置Scala环境变量&#xff08;四&#xff09;测试Scala是否安装成功1、查看Scala版本2、启动Scala&#xff0c;执行语句Win…

什么是装运单IFTMIN?

符合EDIFACT国际报文标准的IFTMIN主要用于传输电子运输订单&#xff0c;这些装运单作为EDI数据交换的一部分&#xff0c;由客户或托运人发送给物流服务提供商。通过EDI传输的运输信息可以被用来计划当前所需的运输能力&#xff0c;并且物流服务提供商也可以据此提前将包装材料准…

【正点原子FPGA连载】第十一章PL SYSMON测量输入模拟电压 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Vitis开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第十一章PL SYSM…

使用注意力机制的seq2seq

注意力机制在NLP中的应用&#xff0c;是早期工作之一 1.为什么使用注意力机制 ①在机器翻译的时候&#xff0c;每个生成的词可能相关于源句子不同的词 ②语言翻译的时候&#xff0c;中英文存在倒装句&#xff0c;几个相同意思的句子中的词的位置可能近似的对应。翻译句子某部分…

Lp正则化

一、L1 和 L2范数&#xff08;norm&#xff09;A norm is a mathematical thing that is applied to a vector. The norm of a vector maps vector values to values in [0,∞). In machine learning, norms are useful because they are used to express distances: this vect…

DataWhale-统计学习方法打卡Task01

学习教材《统计学习方法&#xff08;第二版&#xff09;》李航 统计学习方法&#xff08;第2版&#xff09; by...李航 (z-lib.org).pdf https://www.aliyundrive.com/s/maJZ6M9hrTe 点击链接保存&#xff0c;或者复制本段内容&#xff0c;打开「阿里云盘」APP &#xff0c;无…

论文笔记:NeuLF: Efficient Novel View Synthesis with Neural 4D Light Fight

中文标题&#xff1a;基于神经4D光场的高效新视角合成 创新点 在我们的方法中&#xff0c;一个3D场景被表示为一个光场&#xff0c;即一组射线&#xff0c;每条射线在到达图像平面时都有相应的颜色。为了实现高效的新视图渲染&#xff0c;我们采用了光场的双平面参数化&#…

JAVA实现心跳检测【长连接】

文章目录1、心跳机制简介2、心跳机制实现方式3、客户端4 、服务端5、代码实现5.1 KeepAlive.java5.2 MyClient.java5.3 MyServer5.4 测试结果1、心跳机制简介 在分布式系统中&#xff0c;分布在不同主机上的节点需要检测其他节点的状态&#xff0c;如服务器节点需要检测从节点…