最经济 网站建设,江门网站开发多少钱,网络舆情监测中心,网站优化 西安安卓小游戏#xff1a;俄罗斯方块
前言
最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏#xff0c;还是比较简单的#xff0c;这几天又把俄罗斯方块还原了一下#xff0c;写了一天#xff0c;又摸鱼调试了两天#xff0c;逻辑不是很难#xff0c;但是…安卓小游戏俄罗斯方块
前言
最近用安卓自定义view写了下飞机大战、贪吃蛇、小板弹球三个游戏还是比较简单的这几天又把俄罗斯方块还原了一下写了一天又摸鱼调试了两天逻辑不是很难但是要理清、处理对还是有点东西的。
需求
这里的需求玩过的都知道简单说就是控制四种砖块将底部填满砖块可以进行旋转当砖块超过顶部就游戏结束了。核心思想如下
1用一个二维数组保存地图信息显示固定的砖块2每次只出现一个砖块可以左右移动手指向上移动进行旋转手指向下移动快速坠落3砖块和地图信息有交互地图信息限制砖块移动和旋转到达底部或者底部被阻挡会触发固定4固定之后根据砖块更新地图信息并进行下一轮砖块
效果图
这里网上找的GIF转换工具只能生成30秒的内容不过游戏的内容已经显示得差不多了。 代码
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 -1const val DIR_UP 0const val DIR_RIGHT 1const val DIR_DOWN 2const val DIR_LEFT 3// 四种砖块对应的配置是一个2 * 8的数组, 这里用二进制来保存// 顶点左上角二行四列默认朝右方向变换咦左上角旋转private const val CONFIG_TYPE_L 0b1110_1000private const val CONFIG_TYPE_T 0b1110_0100private const val CONFIG_TYPE_I 0b1111_0000private 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: Intprivate var mRowDelta: Int 0private val mColNumb: Intprivate var mColDelta: Int 0// 节点掩图private val mTetrisMask: Bitmap?// 游戏地图是个二维数组private val mGameMap: ArrayIntArray// 当前操作的方块private val mTetris Tetris(0, 0, 0, 0)// 不要在onDraw中创建对象, 在onDraw中临时计算绘制位置private val mPositions ArrayListMutableTripleInt, 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.LTGRAYstrokeWidth 1fstyle Paint.Style.STROKEflags Paint.ANTI_ALIAS_FLAG}// 上一个触摸点X、Y的坐标private var mLastX 0fprivate var mLastY 0finit {// 读取配置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 nulltypedArray.recycle()}private fun drawableToBitmap(drawable: Drawable): Bitmap? {val w drawable.intrinsicWidthval h drawable.intrinsicHeightval config Bitmap.Config.ARGB_8888val 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 / mRowNumbmColDelta 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 truemGameController.isGameOver falsemGameController.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() ! widthfor (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.xmLastY event.y}MotionEvent.ACTION_MOVE - {}MotionEvent.ACTION_UP - {val lenX event.x - mLastXval lenY event.y - mLastY// 只更改方向逻辑由GameController处理方向更改成功与否需要确认if (abs(lenX) abs(lenY)) {// 左右移动// val delta (lenX / mColDelta).toInt()// mGameController.colDelta deltamGameController.colDelta if (lenX 0) 1 else -1}else {if (lenY 0) {// 往下滑动加快mTetris.fastMode true}else {// 往上移动切换形态mGameController.newDirection mTetris.dir 1if (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: WeakReferenceTetrisGameView WeakReference(view)// 防止大量生成对象private val mTempPositions ArrayListMutableTripleInt, 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 0override 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 - 1DIR_UP - 2else - 0}gameView.mTetris.posCol colgameView.mTetris.config typegameView.mTetris.fastMode falseisNewTurn 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: ArrayListMutableTripleInt, 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 maskval triple positions[index]// 将不同方向对应的位置转换到config的顺序并保存该位置是否绘制的flagtriple.third flagvar optimizedDir dir// 对方块和条形类型特别优化if (tetris.config CONFIG_TYPE_O) optimizedDir DIR_RIGHTif (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 xDIR_RIGHT - {triple.first posRow itriple.second posCol j - 1}// 下向// x o x x// x x x o// x x x x// x x - x xDIR_DOWN - {triple.first posRow j - 1triple.second posCol - i}// 左向// x x x x x x x x// x x x o - x x o xDIR_LEFT - {triple.first posRow - itriple.second posCol - j 1}// 上向// x x x x// x x x x// x x o x// o x - x xDIR_UP - {triple.first posRow - j 1triple.second posCol i}else - {}}}}// 检测出界和重叠下边和左右private fun checkOutAndOverlap(positions: ArrayListMutableTripleInt, Int, Boolean,rowNumb: Int, colNumb: Int, gameMap: ArrayIntArray): Boolean {var flag truefor (pos in positions) {// 只对有值的位置进行验证if(!pos.third) continue// 出下界if (pos.first rowNumb) {flag falsebreak}// 左右出界if (pos.second colNumb || pos.second 0) {flag falsebreak}// 旋转后有冲突暂且忽略上边之外的情况if (pos.first 0) continueif (pos.third gameMap[pos.first][pos.second] 0) {flag falsebreak}}return flag}private fun moveTetris(gameView: TetrisGameView) {val tetris gameView.mTetris// 对向下移动控制左右移动、旋转不限制mMoveCounterif (mMoveCounter (TETRIS_MOVE_TIME / GAME_FLUSH_TIME).toInt()) {tetris.posRow 1mMoveCounter 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.mGameMapfor (i in gameMap.indices1) {val array gameMap[i]var isFull truefor (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 MutableTripleT, V, R(var first: T, var second: V, var third: R)
}对应style配置 res - values - tetris_game_view_style.xml ?xml version1.0 encodingutf-8?
resourcesdeclare-styleable name TetrisGameViewattr namerowNumber formatinteger/attr namecolNumber formatinteger/attr nametetrisMask formatreference//declare-styleable
/resources砖块掩图 res - drawable - ic_tetris.xml vector android:height24dp android:tint#6F6A6Aandroid:viewportHeight24 android:viewportWidth24android:width24dp xmlns:androidhttp://schemas.android.com/apk/res/androidpath android:fillColorandroid:color/white android:pathDataM18,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/
/vectorlayout布局 com.silencefly96.module_views.game.TetrisGameViewandroid:idid/gamaViewandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:backgroundcolor/blackapp:rowNumber30app:colNumber20app:tetrisMaskdrawable/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 * mColDeltay 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的职责就很清楚了大致就是一下几个
生成新砖块检查交互逻辑移动固定消除
新砖块生成
这里用了一个控制变量来控制是否新生成砖块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延缓向下的移动留出时间来左右移动或者旋转更加人性话点。
结语
这里写得有点多了写一个游戏还是挺有意思的朋友说这东西没有技术性我还是觉得只有你做过你才知道你有没有学到东西不去做永远停留在纸面上。