梦回童年-使用Compose搞一个贪吃蛇游戏

简介: 梦回童年-使用Compose搞一个贪吃蛇游戏

灵感来源


前段时间看到了大佬fundroid使用compose编写俄罗斯方块的文章,深受启发,当时便决定也要把诺基亚的贪吃蛇搬到android上来,因此才有了这篇文章

本篇文章界面和思路参考于 fundroid的文章

fundroid俄罗斯方块传送:blog.csdn.net/vitaviva/ar…


最终效果


提前发一下效果,让读者有个心理预期,逻辑真不复杂,务必耐心看完

image.png


思路


我们的界面分为两部分,上半部分是游戏的动态显示区域,下半部分是操作区域


显示区域

显示区域也可以细分为两部分,

  • 边框+黑线边框
  • 屏幕动态显示区域

所以我们把这两部分分成两个组合@Composable BaseScreen@Composable GameScreen

BaseScreen用于绘制边框和黑线边框,这部分只需要绘制一次即可,所以我们需要抽离出来

GameScreen用于绘制动态区域,主要是绘制基础的浅色砖块矩阵,和蛇身深色砖块、游离蛇身深色砖块


操作区域

操作区域也有两部分,

  • 开始重置按钮、恢复暂停按钮,这两个按钮用于操作游戏状态
  • 方向键按钮,用于操纵蛇头方向

操作区域组合名称为@Composable OperateBody

其中操作状态按钮使用Row包裹两个@Composable GameButton来实现

方向键按钮使用一个Box来包裹四个@Composable DirectionButtom来实现


逻辑

如何数据通信

使用StateFlow.collectAsState方法来获取Compose状态,从而在状态更改的时候刷新界面


如何刷新游戏状态

使用LauncherEffect内部定义一个死循环来维护时钟,保证我们可以在自定义的时间间隔内更新游戏状态。


蛇身状态

我们使用一个数组来维护所有蛇身的砖块,然后就是蛇移动、蛇吃游离蛇身、蛇撞墙几个逻辑


蛇移动

蛇单纯移动的话,我们可以通过构建一个新数据的方式来实现,新数组的第一个元素就是蛇身下一个要移动到的砖块,然后将老的蛇身数组addAll到新的蛇身数组中,最后移除新蛇身数组的最后一个蛇身。刷新StateFlow状态


蛇吃游离蛇身

如果蛇下次要移动到的砖块位置刚好是游离蛇身的位置的话,那么我们直接将游离蛇身插入到数组的第一个元素位置,并且不移除蛇身数组最后位置的砖块


蛇撞墙

整个屏幕被我们栅格化为Width_Matrix * Height_Matrix数组,如果蛇头的位置超出栅格数组那么蛇撞墙死,游戏结束。


代码分析


StateFlow声明和使用

声明StateFlow,这部分不需要引入任何依赖

private val _flow = MutableStateFlow(SnakeState(action = Action.GameTick))
val stateFlow = _flow.asStateFlow()
复制代码


使用StateFlow获取状态

val model = viewModel<GameViewModel>()
 val state = model.stateFlow.collectAsState().value
复制代码


这里的SnakeState就是我们定义的状态实体,代码如下:

data class SnakeState(
    var action: Action,
    val direction: Direction = Direction.Bottom,
    val snakeBody: MutableList<SnakePart> = mutableListOf(SnakePart(10, 10)),
    val freeSnakePart: SnakePart = SnakePart(15, 15),
    var Width_Matrix: Int = 0,
    var Height_Matrix: Int = 0,
    var isRunning: Boolean = true
)
复制代码


游戏状态时钟代码

使用LauncherEffect维持死循环,300ms分发一次时钟

val model = viewModel<GameViewModel>()
    LaunchedEffect(key1 = Unit) {
        while (isActive) {
            delay(300)
            model.dispatch()
        }
    }
复制代码


蛇移动、吃游离蛇身、撞墙代码

fun dispatch() {
        val state = stateFlow.value
        if (state.action!=Action.GameTick){
            return
        }
        if (!state.isRunning) {//暂停状态不更新数据
            return
        }
        val firstPart = state.snakeBody.first()
        val newList = mutableListOf<SnakePart>()
        if (state.direction == Direction.Left || state.direction == Direction.Right) {
            val x = firstPart.x + state.direction.increase
            if (x == state.freeSnakePart.x && firstPart.y == state.freeSnakePart.y) {//吃游离蛇身
                newList.add(state.freeSnakePart)
                newList.addAll(state.snakeBody)
                emit(state.copy(snakeBody = newList, freeSnakePart = newFreeSnakePart()))
                return
            }
            if (x > state.Width_Matrix || x < 0) {//撞墙
                emit(
                    state.copy(
                        action = Action.GameOver,
                    )
                )
                return
            } else {
                newList.add(SnakePart(x, firstPart.y))
                newList.addAll(state.snakeBody)
                newList.removeLast()//删除蛇身的最后一节
            }
        } else {
            val y = firstPart.y + state.direction.increase
            if (y == state.freeSnakePart.y && firstPart.x == state.freeSnakePart.x) {//吃游离蛇身
                newList.add(state.freeSnakePart)
                newList.addAll(state.snakeBody)
                emit(state.copy(snakeBody = newList, freeSnakePart = newFreeSnakePart()))
                return
            }
            if (y > state.Height_Matrix || y < 0) {//撞墙
                emit(
                    state.copy(
                        action = Action.GameOver,
                    )
                )
                return
            } else {
                newList.add(SnakePart(firstPart.x, y))
                newList.addAll(state.snakeBody)
                newList.removeLast()//删除蛇身的最后一节
            }
        }
        emit(state.copy(snakeBody = newList))
    }
    fun emit(state: SnakeState) {
        _flow.value = state
    }
复制代码


未实现部分

  • 蛇撞自身蛇身逻辑
  • 蛇撞墙后游戏结束的动画

这部分有兴趣的可以fork代码后自己实现一下,相信会有所收获。

代码:github.com/ananananzhu…



相关文章
高德地图目前是哪个集团下的公司?
其实在2014年02月,阿里巴巴就斥资11亿美元,完成对高德地图的全资收购,所以高德地图目前是属于阿里巴巴集团下的公司了。
3753 0
|
存储 JSON 运维
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
379 1
Excel如何使用VBA操作引用其它工作簿中的单元格
Excel引用其它工作簿中的单元格的值及使用VBA操作
|
存储 计算机视觉
OpenCV(三十九):积分图像
OpenCV(三十九):积分图像
243 0
Markdown 拓展 - 对数学公式的支持
MathJax 和 LaTeX 数学公式 支持 MathJax 是一款运行在浏览器中的开源数学符号渲染引擎,使用MathJax可以方便的在浏览器中显示数学公式,不需要使用图片。目前,MathJax可以解析 Latex、MathML 和 ASCIIMathML 的标记语言。MathJax 项目于 2009 年开始,发起人有 American Mathematical Society, Design Science等,还有众多的支持者,个人感觉MathJax会成为今后数学符号渲染引擎中的主流,也许现在已经是了。本文接下来会讲述 MathJax 的基础用法,但不涉及MathJax 的安装及配置。
566 0
Markdown 拓展 - 对数学公式的支持
|
存储 关系型数据库 MySQL
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
上一篇文章我们介绍了【MySQL从入门到精通】【高级篇】(七)设计一个索引&InnoDB中的索引方案,该文介绍了如何设计一个索引,以及InnoDB中的索引如何形成。
492 0
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
|
存储 弹性计算 运维
Flink on K8s 在京东的持续优化实践
京东资深技术专家付海涛在 FFA 2021 的分享
Flink on K8s 在京东的持续优化实践
|
Oracle 关系型数据库 MySQL
Navicat Premium 15 永久破解激活工具及安装教程(亲测可用)
Navicat Premium 15 永久破解激活工具及安装教程(亲测可用)
1985 0
|
机器学习/深度学习 算法 数据可视化
Deep Graph Infomax:互信息最大化的无监督图对比学习
Deep Graph Infomax:互信息最大化的无监督图对比学习
1089 0
Deep Graph Infomax:互信息最大化的无监督图对比学习
|
弹性计算 Ubuntu 安全
阿里云服务器可选操作系统汇总(公共镜像类型)
本文介绍了阿里云服务器公共镜像类型可选系统汇总,可供新手用户了解当前选择公共镜像类镜像时有哪些可选的操作系统。
1630 0
阿里云服务器可选操作系统汇总(公共镜像类型)