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



相关文章
|
测试技术
软件测试中的QPS和TPS解析:以秒杀系统为例
软件测试中的QPS和TPS解析:以秒杀系统为例
1226 0
软件测试中的QPS和TPS解析:以秒杀系统为例
|
编解码 算法 数据挖掘
【数据挖掘】聚类趋势估计、簇数确定、质量测定等评估方法详解(图文解释 超详细)
【数据挖掘】聚类趋势估计、簇数确定、质量测定等评估方法详解(图文解释 超详细)
611 0
|
10月前
|
机器学习/深度学习 人工智能 自然语言处理
通用人工智能的标准是什么,与大模型有何区别?发展到什么程度了?
本文深入解析2025年迅猛发展的通用人工智能(AGI),梳理其核心概念、关键技术与现实应用,对比当前主流大模型的差异,并探讨普通人如何在日常生活与工作中体验和应用这一颠覆性技术,展望AGI带来的社会变革与伦理挑战。
2727 5
|
数据可视化 Linux iOS开发
(2024年)Typora-最新版安装(1.8.10)
Typora是一款高效、简洁且功能强大的Markdown编辑器,支持Windows、MacOS及Linux系统,并实现可视化编辑,同一窗口实时呈现编辑效果。相较于传统Markdown编辑器,Typora的界面更为直观易用,极大地提升了编辑效率。用户可在官网(https://typoraio.cn/)下载安装包进行安装。
1234 5
|
运维 监控 BI
卓越架构之FinOps最佳实践
本文探讨了云成本管理的趋势和FinOps的最佳实践。随着云计算的普及,传统的IT管理模式已无法适应按需使用和按量付费的新模式,导致企业面临资源浪费和成本失控的风险。FinOps作为一种管理理念,强调运维、财务和技术团队的合作,通过数据驱动和业务价值驱动的方式优化云成本。文章介绍了FinOps的核心挑战、最佳实践及技术工具的应用,帮助企业有效管理和优化云成本,实现降本增效。
|
数据可视化
团队协作方法:世界咖啡法实操指南
世界咖啡法(World Café) 是一种让团队通过轻松对话激发创意、共享智慧的协作工具。
884 9
团队协作方法:世界咖啡法实操指南
|
人工智能 搜索推荐 前端开发
seo如何优化
木头左,物联网工程师,分享AI工具。本文探讨SEO优化,包括理解基本概念,关键词研究,内容、外部链接和技术优化。关键词研究注重长尾词和竞争度;内容优化要求高质量、结构清晰、定期更新;外部链接要来自高权重源,自然且多样;技术优化涉及URL结构、网站速度、移动友好性和安全性等。记得点赞、收藏和关注哦!
seo如何优化
|
网络协议 Windows
电脑ip在哪里查看?windows系统查看ip地址的8种方法
在Windows系统中,有多种方法可以查看电脑的IP地址。
9739 2
|
数据采集 存储 中间件
Scrapy,作为一款强大的Python网络爬虫框架,凭借其高效、灵活、易扩展的特性,深受开发者的喜爱
【6月更文挑战第10天】Scrapy是Python的高效爬虫框架,以其异步处理、多线程及中间件机制提升爬取效率。它提供丰富组件和API,支持灵活的数据抓取、清洗、存储,可扩展到各种数据库。通过自定义组件,Scrapy能适应动态网页和应对反爬策略,同时与数据分析库集成进行复杂分析。但需注意遵守法律法规和道德规范,以合法合规的方式进行爬虫开发。随着技术发展,Scrapy在数据收集领域将持续发挥关键作用。
425 4
|
消息中间件 Kafka
Kafka对于消息顺序性的最佳实践
Kafka对于消息顺序性的最佳实践