梦回童年-使用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亿美元,完成对高德地图的全资收购,所以高德地图目前是属于阿里巴巴集团下的公司了。
3201 0
|
11月前
|
存储 JSON 运维
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
探索基础设施即代码(IaC):Terraform 与 CloudFormation 的应用
329 1
Excel如何使用VBA操作引用其它工作簿中的单元格
Excel引用其它工作簿中的单元格的值及使用VBA操作
|
存储 计算机视觉
OpenCV(三十九):积分图像
OpenCV(三十九):积分图像
196 0
|
存储 关系型数据库 MySQL
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
上一篇文章我们介绍了【MySQL从入门到精通】【高级篇】(七)设计一个索引&InnoDB中的索引方案,该文介绍了如何设计一个索引,以及InnoDB中的索引如何形成。
462 0
【MySQL从入门到精通】【高级篇】(八)聚簇索引&非聚簇索引&联合索引
|
虚拟化 数据安全/隐私保护 网络安全
|
关系型数据库 MySQL Java
MySQL的数据库访问接口
mysql-connectors Table of Contents 1. 数据库访问接口概述 2. MySQL 的数据库访问接口 1 数据库访问接口概述 大部分流行的关系数据库产品采用的都是 C/S 架构。因此,应用程序要访问数据库的数据就必须和各种数据库客户端(也称为数据库访问接口)打交道。可以这么认为,每一种流行的编程语言都需要一个对应的数据
8747 0
|
网络协议 安全 测试技术
常见内网穿透工具,收好了!(二)
常见内网穿透工具,收好了!
常见内网穿透工具,收好了!(二)
支付宝预授权接口报错系统异常自查方案
说明: 本帖主要和大家交流一下调用alipay.fund.auth.order.app.freeze(线上资金授权冻结接口)报错自查的方案 参考以下排查方案: 一、调用方法 alipay.fund.auth.order.app.freeze接口响应参数输出的调用方法必须是sdkExecute()。
2597 4
PSCC2019下载AdobePhotoshopCC2019破解版永久安装免费教程
PSCC2019下载AdobePhotoshopCC2019破解版永久安装免费教程 2018年10月15日Adobe官方宣布AdobePhotoshopCC2019正式升级,从版本号V19升级到V20.功能更新的也是比较多的,具体更新了什么这里不做阐述,大家最关心的应该是怎么进行永久注册PhotoshopCC2019,怎么永久的安装PhotoshopCC2019,怎么永久的使用PhotoshopCC2019,怎么永久破解,PhotoshopCC2019那么接下来呢,小编就和大家详细讲解一下。
24852 0