梦回童年-使用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…



相关文章
|
弹性计算 缓存 分布式计算
阿里云服务器新人特惠活动改版,共享型和独享型云服务器1折起
8月3日,阿里云对旗下的云服务器新人特惠活动完成了全新升级,升级之后的活动保留了限时特惠板块,新增了六代共享型和六代独享型板块,新增板块折扣为1折起,通过活动上云省心更省钱,活动内云服务器性能卓越、弹性可伸缩、可靠稳定、安全可信。
2010 0
阿里云服务器新人特惠活动改版,共享型和独享型云服务器1折起
|
存储 JSON 运维
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
355 1
Excel如何使用VBA操作引用其它工作簿中的单元格
Excel引用其它工作簿中的单元格的值及使用VBA操作
|
存储 计算机视觉
OpenCV(三十九):积分图像
OpenCV(三十九):积分图像
216 0
Markdown 拓展 - 对数学公式的支持
MathJax 和 LaTeX 数学公式 支持 MathJax 是一款运行在浏览器中的开源数学符号渲染引擎,使用MathJax可以方便的在浏览器中显示数学公式,不需要使用图片。目前,MathJax可以解析 Latex、MathML 和 ASCIIMathML 的标记语言。MathJax 项目于 2009 年开始,发起人有 American Mathematical Society, Design Science等,还有众多的支持者,个人感觉MathJax会成为今后数学符号渲染引擎中的主流,也许现在已经是了。本文接下来会讲述 MathJax 的基础用法,但不涉及MathJax 的安装及配置。
550 0
Markdown 拓展 - 对数学公式的支持
|
存储 关系型数据库 MySQL
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
上一篇文章我们介绍了【MySQL从入门到精通】【高级篇】(七)设计一个索引&InnoDB中的索引方案,该文介绍了如何设计一个索引,以及InnoDB中的索引如何形成。
485 0
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
|
弹性计算 编解码 前端开发
阿里云ecs.c6.xlarge服务器4核8G计算型c6性能评测
阿里云服务器ECS计算型c6实例4核8G配置ecs.c6.xlarge,CPU采用Intel Xeon(Cascade Lake) Platinum 8269CY,2.5 GHz主频,睿频3.2 GHz,计算性能稳定,I/O优化实例,具有超高网络收发包PPS能力
1241 0
阿里云ecs.c6.xlarge服务器4核8G计算型c6性能评测
pr2023百度网盘下载Premiere2023最新版
Premiere Pro 2023中文版当然也不是全无创新,比如现在用户们可以在时间轴上批量编辑字幕,这对于经常与文字打交道的用户可以说是非常不错的提升,比如各种汉化组,要知道各种动漫、影视剧的字幕量不是一般的多,而且在时间轴上编辑可以更好的字幕与语音进行精准配对。
2673 0
|
Oracle 关系型数据库 MySQL
Navicat Premium 15 永久破解激活工具及安装教程(亲测可用)
Navicat Premium 15 永久破解激活工具及安装教程(亲测可用)
1867 0