FlutterFlame游戏实践#15 | 生命游戏 - 演绎启动

news2024/11/15 15:35:02

theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter\&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]\ 第二季:从休闲游戏实践,进阶 Flutter\&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


接下来 Flutter&Flame 系列将迎来最重要的一个系列:《生命游戏》。我们将一起演绎这场计算机与数学逻辑间的绝妙故事。本篇将完成如下的功能:给定一个生命游戏的初始状态,可以进行每帧的迭代:

01.gif


一、生命游戏规则解读

我们将单元格称之为生存空间,每个空间中可以容纳一个细胞,界面中的白块表示空间中有细胞存活。对于每一块空间来说,《生命游戏》 规定了三条生存法则:

image.png

[1]. 空间周围细胞数 = 3,该空间产生细胞,或维持生存。 [2]. 空间周围细胞数 = 2,该空间维持不变。 [3]. 其余情况下,该空间细胞无法生存。


1. 上帝视角下的生命游戏

生命游戏的规则应该说非常简单,核心是计算出空间四周存活细胞个数,这里姑且称为 拥挤度 吧。下面来开启上帝视角,结合图片具体介绍一下:

  • 左上角的格子(红框)有三个邻格,三个邻格没有存活的细胞。拥挤度 = 0
  • 中间的红框中格子有8个邻格,其中两块有细胞。拥挤度 = 2

image.png

上面是上帝视角下的生命游戏,我为每个空间标记了它的 拥挤度 ,其中数字的颜色也根据状态进行了区分,以便于可视化理解生命游戏规则:

  • 灰色数字空间,下一帧不会存活细胞;
  • 绿色数据空间,会诞生细胞或继续生存;
  • 红色数字空间,细胞将会死亡;

2. 细胞的诞生和维持

细胞在空间中的 诞生维持生存,都会在下一帧存活:比如

  • 下面的第 6 行第 5 列的空间,四周有 3 个细胞;并且当前没有细胞,下一帧将会诞生新细胞。
  • 下面的第 5 行第 4 列的空间,四周有 2 个细胞;细胞在下一帧将会维持生存。

image.png


3. 细胞的死亡

下面是第二帧的情况: 其中有细胞存在且 拥挤度>3 的空间,其中数字通过红色示意。这些细胞将会在下一帧中被杀死:

image.png

下面是第三帧的情况: 在第二帧中绿色数字的区域,在第三帧会有细胞存活。以此类推:

image.png

这样,我们不仅可以展示出生命游戏的细胞存活情况,也可以通过上帝视角的空间拥挤度,掌握下一帧的细胞情况。通过可视化的手段,更容易理解规则。

2.gif

接下来,就一起通过 Flutter&Flame 实现生命游戏演示功能。在真正写代码之前,我们应该详细地分析当前需求。包括三个方面,本节将逐一分析:

  • 视图层: 界面的布局如何实现,界面元素有哪些交互逻辑。
  • 数据层: 应用在完成功能需求的过程中,需要依赖哪些数据进行展示。
  • 逻辑层: 功能需求完成的过程中,数据的操作逻辑。

二、视图层实现

当前视图结构如下,上方是标题栏,左侧是工具栏,中间是游戏面板。其中标题和侧栏的工具使用 Flutter 内置组件,而且侧栏使用了 TolyUI 组件库;游戏区通过 Flame 框架进行构建游戏界面。

image.png

也就是说 Flutter 的组件布局体系和 Flame 的构件布局体系是可以兼容的。在构建界面的过程中可以各取所长。侧栏的按钮第一版中包括:运行、下一帧、前一帧、重置、清空五个按钮。通过事件触发来通知游戏区的数据发生变化。


1. 视图层:空间管理器 SpaceManager

游戏区和上一篇实现的扫雷类似,都是在宫格中盛放内容。这里称每个宫格是生存的空间 Space,游戏世界 LifeWord 中通过 SpaceManager 维护 Space 空间集合。

image.png

空间管理器负责绘制网格与添加空间构件,完成核心的展示任务。如下所示,在构造方法中传入网格的行列数,已经每个单元格的边长 side 。然后复写 render 回调方法,通过 Canvas 基于网格行列数和格子边长绘制网格:

```dart class SpaceManager extends PositionComponent { final double side; final int row; final int column;

SpaceManager({this.side = 20, this.row = 9, this.column = 9});

@override void render(Canvas canvas) { priority = 2; drawGrid(canvas, side, row, column); }

void drawGrid(Canvas canvas, double boxSize, int row, int column) { Paint girdPaint = Paint() ..style = PaintingStyle.stroke ..color = const Color(0xff505050); Path path = Path(); Path lightPath = Path(); double width = row * boxSize; double height = column * boxSize; for (int i = 0; i <= column; i++) { path.moveTo(0, boxSize * i); path.relativeLineTo(width, 0); } for (int i = 0; i <= row; i++) { path.moveTo(boxSize * i, 0); path.relativeLineTo(0, height); } canvas.drawPath(path, girdPaint); } } ```


2. 视图层:网格 Space

每个方格空间由一个 Space 构件表示,它构造时需要坐标边长是否存活拥挤度 四个数据。render 回调可以控制绘制,在存活时绘制白块即可。在 onLoad 回调中通过 TextComponent 展示当前空间的拥挤度。这样在 SpaceManager 中添加若干个 Space 就可以展示在网格中:

image.png

image.png

```dart class Space extends PositionComponent { final (int,int) p; final double side; final bool alive; final int value;

Space( this.p, { this.side = 20, this.alive = true, this.value = 0, }) : super(size: Vector2(side, side));

@override FutureOr onLoad() { TextStyle style = TextStyle(fontSize: 16, fontFamily: 'BlackOpsOne', package: 'life_game', color: color); TextComponent text = TextComponent( text: '$value', textRenderer: TextPaint(style: style), position: Vector2(p.$1 * side, p.$2 * side), anchor: Anchor.center, ); add(text); text.position = text.position + size / 2; return super.onLoad(); }

@override void render(Canvas canvas) { if (!alive) return; Paint paint = Paint()..color = Colors.white; canvas.drawRect(Rect.fromLTWH(p.$1 * side, p.$2 * side, width, height), paint); }

Color? get color { if (alive && (value < 2 || value > 3)) { return Colors.red; } if (!alive && (value <= 2 || value > 3)) { return Colors.grey; } return Colors.green; } } ```


3. 操作工具栏

工具栏使用 Flutter 的 Widget 进行构建,目前由五个按钮。这里定义 ToolAction 枚举记录对应的行为和图标。

image.png

```dart enum ToolAction { play(TolyIcon.iconplay), next(TolyIcon.iconnext), prev(TolyIcon.iconprev), reset(TolyIcon.iconreset), clear(TolyIcon.icon_clear), ;

final IconData icon;

const ToolAction(this.icon); } ```

然后定义 ActionToolbar 组件构建侧栏按钮的展示,这里用到了 TolyUI 中的 TolyAction 组件,展示一个图标按钮;通过 onAction 回调将事件传递给使用者,做具体的行为逻辑处理。

```dart class ActionToolbar extends StatelessWidget { final ValueChanged onAction;

const ActionToolbar({super.key, required this.onAction});

@override Widget build(BuildContext context) { ActionStyle style = const ActionStyle( backgroundColor: Colors.black, padding: EdgeInsets.all(2), borderRadius: BorderRadius.all(Radius.circular(4)), ); return Container( padding: const EdgeInsets.symmetric(vertical: 8), width: 30, alignment: Alignment.topCenter, decoration: const BoxDecoration( border: Border(top: BorderSide(color: Colors.black, width: 0.5)), ), child: Wrap( spacing: 6, direction: Axis.vertical, children: ToolAction.values.map((e) { Color? color = e == ToolAction.play ? Colors.green : null; return TolyAction( style: style, child: Icon(e.icon, size: 18, color: color), onTap: () => onAction(e), ); }).toList()), ); } } ```


4. Widget 与 Component

Widget 是 Flutter 的界面构建体系,Component 是 Flame 的界面构建体系。两者唯一的关联是 GameWidget,这是一个 Flutter 的 Widget ,且具有展示 Component 的能力。这里通过 Row 将 ActionToolbar 和 GameWidget 横向排列即可。在 onAction 回调中,可以操作 LifeGame 的方法,控制游戏行为:

```dart class LifeGameView extends StatefulWidget { const LifeGameView({super.key});

@override State createState() => _LifeGameViewState(); }

class _LifeGameViewState extends State { final LifeGame game = LifeGame();

@override Widget build(BuildContext context) { return Row( children: [ ActionToolbar(onAction: _onAction,), Expanded(child: GameWidget(game: game)), ], ); }

void _onAction(ToolAction value) { switch(value){ case ToolAction.next: game.nextFrame(); break; case ToolAction.play: break; case ToolAction.prev: break; case ToolAction.reset: game.reset(); case ToolAction.clear: game.clear(); } } } ```


三、 数据层与维护逻辑

视图层已经准备完毕,接下来需要准备和维护数据。我们可以把网格坐标 XY 和生死状态 bool 建立一个映射关系,称为地图数据。每一步迭代称为一帧 Frame ,它的数据会发生变化,然后基于数据去更新界面展示。


1. Frame 的定义

Frame 中定义了 spacesspaceValueMap 两个映射关系,前者是地图数据,后者是为了便于开上帝视角而维护的当前帧拥挤度关系。 本文我们只看一个固定案例的生命游戏演化,自己绘制的功能将在写篇实现。在 reset 方法中重置地图数据,给出当前存活的细胞。也就是下面的初始场景:

image.png

```dart typedef XY = (int, int);

class Frame { XY size;

/// 地图数据 Map spaces = {}; /// 拥挤度关系 Map spaceValueMap = {};

int spaceValue(XY key) => spaceValueMap[key] ?? 0;

Frame(this.size) { reset(); }

void reset() { spaces = {(3, 4): true, (4, 4): true, (5, 4): true, (4, 3): true}; _calcSpaceValue(); } ```


2. 空间的演化

生命游戏中最最重要的一个方法是如何计算出四周邻格的存活细胞总数。其实这个和扫雷中如何计算单元格周围的地雷个数是一个算法。我们在 《Flutter&Flame游戏实践#14 | 扫雷 - 逻辑实现》 一文中已经领略过了,这里再说明一下

image.png

如下所示 _calculate 函数计算 (x,y) 坐标四周的空间存活细胞数。只需要遍历 [x-1,x+1] ~ [y-1,y+1] 区间的九个格点即可。另外和扫雷不同的时,当前格点不计入计算。下面看起来是一个二重 for 循环,但该函数只会触发 9 次循环体,是一个 O(1) 复杂度的函数,而非 O(n^2)。

int _calculate(int x, int y) { int count = 0; for (int i = y - 1; i <= y + 1; i++) { for (int j = x - 1; j <= x + 1; j++) { if ((x, y) == (j, i)) continue; if (spaces[(j, i)] == true) count++; } } return count; }

下面 _calcSpaceValue 会遍历行列数,通过 _calculate 计算每个格点的拥挤度。该函数的复杂度本应和行列数分别正相关,为 O(n^2)。其实毕竟屏幕的尺寸是有限的,对于行列数非常大的地图,可以使用窗口的思想,仅仅解析当前视口中的网格,所以解析的行列数总是有限可数的,可以达到 O(1) 复杂度。

dart void _calcSpaceValue() { for (int y = 0; y < size.$1; y++) { for (int x = 0; x < size.$2; x++) { int count = _calculate(x, y); spaceValueMap[(x, y)] = count; } } }


现在万事俱备,可以通过 evolve 基于当前的拥挤度映射,来更新 space 中的细胞的生死情况。根据生命游戏规则:

  • tag1: 当前空间有存活时,周围是 2和3 拥挤度表示细胞存活.
  • tag2: 当前空间无存活时,拥挤度位 3 诞生新细胞。

_evolveAt 方法处理指定坐标空间的演化,其中很好地用代码诠释了这两条规则:

```dart void evolve() { spaceValueMap.forEach(_evolveAt); _calcSpaceValue(); }

void _evolveAt(XY key, int value){ bool live = spaces[key] == true; if (live) { bool keepAlive = (value == 2 || value == 3); if(!keepAlive) spaces.remove(key); } else { if (value == 3) spaces[key] = true; } } ```

这就是生命游戏的核心数据以及演化逻辑,代码总量不过 60 行,也足以见得生命游戏规则的简单。


3. 用 Frame 渲染游戏世界

现在万事俱备,只需要将 Frame 中的数据,决定SpaceManager 中的 Space 列表即可。我在 SpaceManager 中添加了一个 setFrame 的方法,根据 Frame 对象更新管理器在的 Space 内容:

```dart class SpaceManager extends PositionComponent { final double side; final int row; final int column;

SpaceManager({this.side = 20, this.row = 9, this.column = 9});

void setFrame(Frame frame) { removeWhere((e) => true); Map data = frame.spaces; for (int y = 0; y < row; y++) { for (int x = 0; x < column; x++) { bool alive = data[(x, y)] == true; int value = frame.spaceValue((x, y)); add(Space((x, y), alive: alive, value: value)); } } } ```

游戏世界中目前只有一个 SpaceManager,在 onLoad 回调中设置帧数据,并添加到世界中。

```dart class LifeWord extends World with HasGameRef {

final SpaceManager spaceManager = SpaceManager();

@override FutureOr onLoad() { spaceManager.setFrame(game.frame); add(spaceManager); return super.onLoad(); } } ```

游戏的主类是 LifeGame,其中维护 Frame 对象,在 onLoad 中通过将相机居中;另外,我们知道 Flame 的游戏循环是一致会触发渲染的,但是生命游戏查看下一帧功能,并不需要持续渲染。可以通过在 update 回调中通过 paused = true 来暂停游戏世界的时间。这样在按钮的操作是只需要开启一下,就可以触发一帧的游戏动画。

```dart class LifeGame extends FlameGame { LifeGame() : super(world: LifeWord());

Frame frame = Frame((9,9));

@override FutureOr onLoad() { camera.viewfinder.anchor = Anchor.center; return super.onLoad(); }

void clear() { paused = false; frame.clear(); world.spaceManager.setFrame(frame); }

void nextFrame() { paused = false; frame.evolve(); world.spaceManager.setFrame(frame); }

@override void update(double dt) { super.update(dt); paused = true; }

void reset() { paused = false; frame.reset(); world.spaceManager.setFrame(frame); } } ```


到这里,我们就完成了生命游戏的下一帧演化功能,这也是生命游戏最最基础的展示能力。通过可视化空间拥挤度,也可以让大家更直观地了解生命游戏的规则。后期将带来生命游戏更复杂的操作功能,比如自定义绘制地图数据、自动运行、空间行列数自动扩张等。敬请期待 ~

2.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1914680.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

78000A 信号分析软件

思仪(Ceyear) 78000A 信号分析软件 78000A 信号分析软件是一款能够在电脑上运行的应用软件&#xff0c;预留了开放式的 SCPI 控制指令&#xff0c;可以远程控制信号/频谱分析仪采集数据&#xff0c;也可以回放仿真数据或者采集的历史数据文件&#xff0c;执行通用频谱测量、矢…

Windows 下安装 Memcached

Memcached 安装包下载 官网上并未提供 Memcached 的 Windows 平台安装包。 我们可以使用以下链接来下载&#xff0c;你需要根据自己的系统平台及需要的版本号点击对应的链接下载即可&#xff1a; 32位系统 1.2.5版本&#xff1a;http://static.jyshare.com/download/memcache…

(Windows环境)FFMPEG编译,包含编译x264以及x265

本文使用 MSYS2 来编译 ffmpeg 一、安装MSYS2 MSYS2 是 Windows 下的一组编译套件&#xff0c;它可以在 Windows 系统中模拟 Linux 下的编译环境&#xff0c;如使用 shell 运行命令、使用 pacman 安装软件包、使用 gcc (MinGW) 编译代码等。 MSYS2 的安装也非常省心&#x…

视频共享交换平台LntonCVS视频监控平台智慧加油站安全管理方案

加油站作为危化品行业的一部分&#xff0c;日常的加油和卸油作业安全至关重要。目前国内加油站的管理主要依赖于人为管控、监控摄像头和人工巡检&#xff0c;这些方法存在效率低下和反应滞后的问题。为了有效应对安全风险&#xff0c;急需引入人工智能、物联网和大数据技术&…

Android14系统应用统一裁剪方案

Android14系统应用统一裁剪方案 背景 当前移除集成到系统里的应用,一般都是根据应用名,到各个mk文件里逐个在PRODUCT_PACKAGES中删除;这种方法,耗时而且不易管理集成到系统里的应用;需要有一个统一管理删除不需要应用的方案。 方案 参考PRODUCT_PACKAGES变量,添加PRO…

NVIDIA良心给显卡免费升级,只为挨更多的骂

起猛了&#xff0c;还真的以为 NVIDIA 良心发现了。 众所周知&#xff0c;英伟达对于咱们普通游戏玩家向来不屑一顾。只因为游戏业务在 NVIDIA 收入中占比较少。 在最新的 40 系显卡 RTX 4070 Ti Super 显卡中&#xff0c;NVIDIA悄悄给它来了一次核心「升级」&#xff0c;将原…

开发问卷微信小程序:从零到部署

开发问卷小程序&#xff1a;从零到部署 引言 微信小程序作为一种轻量级的应用形式&#xff0c;以其便捷性和广泛的用户基础&#xff0c;成为开发者的新宠。本博客将详细介绍如何使用HBuilderX编辑器和uView UI框架&#xff0c;开发一款问卷小程序。 环境准备 HBuilderX下载与…

快速拿下GIS开发的正确方式→定制属于自己的gis学习路线图

随着技术融合&#xff0c;人工智能、大数据、物联网与GIS的结合应用&#xff0c;以及移动GIS和云GIS解决方案的兴起&#xff0c;GIS行业的市场规模和应用深度持续增长&#xff0c;为智慧城市、环境保护、自然资源规划、智能交通、商业分析与研究、应急管理等多个领域带来革新。…

【ajax+node.js+webpack+git】学习笔记---ajax01

一、学习资料 1.学习课程 黑马程序员前端AJAX入门到实战全套教程&#xff0c;包含学前端框架必会的&#xff08;ajaxnode.jswebpackgit&#xff09;&#xff0c;一套全覆盖_哔哩哔哩_bilibili 2.学习框架 二、AJAX 1.定义 使用XMLHttpRequest对象与服务器通信。具有异步特性…

react 组件通信 —— 父子传值 【 函数式/类式 】

1、函数式组件通信 父子间通信 —— 父传子 父组件 export default function father() {return (<div style{{width:400px,height:200px,background:pink,marginLeft:500px}}>我是父组件<hr /><Son name{"韩小刀"}/></div>) } 子组件 ex…

【CUDA】 Trust基本特性介绍及性能分析

Trust简介 Thrust 是一个实现了众多基本并行算法的 C 模板库,类似于 C 的标准模板库(standard template library, STL)。该库自动包含在 CUDA 工具箱中。这是一个模板库,仅仅由一些头文件组成。在使用该库的某个功能时,包含需要的头文件即可。该库中的所有类型与函数都在命名空…

【C++】C++11(三)

我们在C11&#xff08;2&#xff09;中已经很好的解释了右值引用&#xff0c;这次来看看右值引用剩余的一些话题&#xff1a;可变参数包与emplace_back。 目录 可变参数模板&#xff1a;可变参数的sizeof&#xff1a;可变参数的展开&#xff1a;递归函数方式展开参数包&#x…

前端JS特效第27波:jQuery商品放大镜预览代码

jQuery商品放大镜预览代码&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下&#xff1a; <!doctype html> <html lang"zh"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatible" content&quo…

【数智化案例展】厦门市信息中心——爱数助力厦门政务云构建两地三中心多级数据灾备体系...

‍ 爱数案例 本项目案例由爱数投递并参与数据猿与上海大数据联盟联合推出的《2024中国数智化转型升级创新服务企业》榜单/奖项”评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 厦门市信息中心是厦门市电子政务专门机构&#xff0c;加挂厦门市电子政务中心、厦门市大数…

拆分盘究竟是什么?一篇文章带你了解!

拆分盘是一种特殊的理财产品或投资模式&#xff0c;它通常被描述为“只涨不跌”的投资方式&#xff0c;多指股票&#xff0c;但实质上与传统股市中的股票有本质区别。以下是对拆分盘的详细解析&#xff1a; 一、拆分盘的定义 拆分盘可以理解为一种只涨不跌的理财股票。其特点在…

1996-2023年各省农村居民人均消费支出数据(无缺失)

1996-2023年各省农村居民人均消费支出数据&#xff08;无缺失&#xff09; 1、时间&#xff1a;1996-2023年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;农村居民人均消费支出 4、范围&#xff1a;31省 5、缺失情况&#xff1a;无缺失 6、指标解释&…

HTTP中常见的状态码有哪些?

常用的包括以下几个&#xff1a; 200&#xff1a;表示客户端请求成功 201&#xff1a;请求成功,服务器创建了新资源。 204&#xff1a;无内容&#xff0c;服务器成功处理请求&#xff0c;但未返回任何内容。 206: 表示“部分内容”,当客户端请求一个资源的一部分时&#xff0c;…

如何利用AI自动生成绘画?5款AI绘画的六大神器!

以下是五款专业级别的AI绘画工具&#xff0c;它们能够帮助用户迅速生成高质量的AI艺术作品&#xff1a; 1.AI先行者&#xff1a; 这是一款流行的 AI 绘画平台&#xff0c;它利用深度学习技术将你的照片或图像转换成艺术风格的绘画作品。你可以在线使用上上传图片并选择喜欢的艺…

微信定时推送LeetCode每日一题,再也不怕没人喊你刷题了

前段时间发过一篇关于微信机器人开发的文章&#xff0c;讲述了如何快速开发一个微信机器人&#xff0c;本篇文章就来实现一个最近开发的一个功能案例&#xff0c;在这个案例中会遇到了各种问题&#xff0c;可以帮助大家减少自己去踩坑的时间。通过此案例也可以帮助你去扩想一些…

OpenCV中使用Canny算法在图像中查找边缘

操作系统&#xff1a;ubuntu22.04OpenCV版本&#xff1a;OpenCV4.9IDE:Visual Studio Code编程语言&#xff1a;C11 算法描述 Canny算法是一种广泛应用于计算机视觉和图像处理领域中的边缘检测算法。它由John F. Canny在1986年提出&#xff0c;旨在寻找给定噪声条件下的最佳边…