开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

如何为前端项目一键自动添加eslint和prettier的支

发表于 2022-07-01

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

本文属于源码共读第35期 | 为 vite 项目自动添加 eslint 和 prettier点击了解本期详情一起参与

前言

我之前好多次都是一步一步的安装eslint和prettier及相关依赖,一个配置文件一个配置文件的粘贴复制,并修改其中的相关配置。而且可能会在每个项目中都要去处理,如果项目工程规划化以后,eslint和prettier确实是项目少不了的配置。不知道你有没有像我一样操作过呢?

那么有没有一种更简单的方式去处理呢?答案是我终于遇到了。通过若川大佬的源码共读活动发现了,真的是太棒了。

本文以vite脚手架创建的项目为基础进行研究的,如果是其他脚手架创建的项目,那么就要自己去修改处理,但是原理是一样的。

那么接下来,我就要来一探究竟,先看看如何使用,然后查阅一下它的源码,看看它到底是如何实现的呢?

1、vite创建项目

  • 创建项目
1
javascript复制代码yarn create vite

一顿操作以后项目就创建完毕了

image.png

  • 2、安装依赖
1
javascript复制代码yarn
  • 3、运行项目
1
javascript复制代码yarn dev
  • 4、运行初始化eslint和prettier命令
1
javascript复制代码yarn create vite-pretty-lint

先来看没有执行命令前的文件目录

image.png

再来看执行完命令后的文件目录

image.png

可以发现文件目录中增加了eslint和prettier的相关配置,package.json中增加了相关的依赖、以及vite.config.xx文件也增加了相关配置,具体的文件变更可以查看github.com/lxchuan12/v…

一个命令干了这么多事情,真的太优秀了。接下来我们就去看看这如此优秀的源代码吧

2、整个过程的示意图

通过大致的查看源代码,简单总结出来的代码执行过程示意图,仅供参考

vite-pretty-lint (1).png

3、源码调试过程

3.1、找到调试代码的位置

通过package.json中的bin节点可以发现,yarn create vite-pretty-lint最终执行的便是lib/main.js中的代码

1
2
3
javascript复制代码  "bin": {
"create-vite-pretty-lint": "lib/main.js"
},

3.2、 开始调试的命令

因为我们现在只是要执行lib/main.js这个入口文件,通过package.json的scripts 也没有发现执行命令,所以现在我们可以直接通过node来运行代码

1
javascript复制代码node lib/main.js

调试成功的结果如下图所示

企业微信截图_16564645675849.png

3.3、 查看头部引入的模块

  • chalk终端多色彩输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript复制代码npm i chalk

import chalk from 'chalk'

const log = console.log
// 字体背景颜色设置
log(chalk.bgGreen('chalk打印设置') )

// 字体颜色设置
log(chalk.blue('Hello') + ' World' + chalk.red('!'))

// 自定义颜色
const custom = chalk.hex('#F03A17')
const bgCustom = chalk.bgHex('#FFFFFF')
log(custom('customer'))
log(bgCustom('bgCustom'))

执行效果如下图所示

image.png

  • gradient 文字颜色渐变
1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 安装
npm i gradient-string
// 引入
import gradient from 'gradient-string'

// 使用
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));
console.log(gradient('cyan', 'pink')('你好啊赛利亚欢迎来到编码世界'));

执行效果如下图所示

image.png

  • child_process node.js中的子进程。

在node.js中,只有一个线程执行所有的操作,如果某个操作需要大量消耗CPU资源的话,后续的操作就需要等待。后来node.js就提供了一个child_process模块,通过它可以开启多个子进程,在多个子进程之间可以共享内存空间,可以通过子进程的互相通信来实现信息的交换。

1
2
3
4
5
6
7
8
9
10
javascript复制代码import { exec } from 'child_process';

exec('ls',(error, stdout,stderr)=> {
if(error) {
console.log(error)
return;
}
console.log('stdout: ' + stdout)
console.log('执行其他操作')
})

执行效果如下图所示

image.png

  • fs
    fs用来操作文件的模块
1
2
3
4
5
6
javascript复制代码import fs from 'fs'

// 同步的读取方法,用来读取指定文件中的内容
fs.readFileSync() 
// 同步的写入方法,用来向指定文件中写内容
fs.writeFileSync()
  • path路径分类
1
2
3
4
javascript复制代码import path from 'path';

// 拼接路径
console.log(path.join('src', 'task.js')); // src/task.js
  • nanospinner命令行中的加载动画
1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// 安装
npm i nanospinner

// 引入模块
import { createSpinner } from 'nanospinner';

const spinner = createSpinner('Run test').start()

setTimeout(() => {
spinner.success()
}, 1000)

执行效果如下图所示(Run test在加载的一个效果)

3.gif

  • enquirer (utils.js文件)

交互式询问CLI 简单说就是交互式询问用户输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码npm i enquirer 

import enquirer from 'enquirer'

let tempArray = ['major(1.0.0)','minor(0.1.0)', 'patch(0.0.4)', "customer" ]
const { release } = await enquirer.prompt({
type: 'select',
name: 'release',
message: 'Select release type',
choices: tempArray
})

if(release === 'customer') {
console.log(release, 'customer')
} else {
const targetVersion = release.match(/\((.*)\)/)[1]
console.log(targetVersion, 'targetVersion')
}

执行效果如下图所示:先出来一个下拉选择,选择完后根据if判断进行输出

4.gif

3.4、 调试具体代码

3.4.1、 main.js中的入口
1
2
3
4
5
6
7
javascript复制代码async function run() {
// 所有的逻辑代码
}

run().catch((e) => {
console.error(e);
});

通过run函数封装异步方法,这样最外面调用run函数时可以通过异步方法的catch捕获错误异常。

看一个小例子

1
2
3
4
5
6
7
8
javascript复制代码const runTest = async () => {
console.log('Running test')
throw new Error('run test报错了')
}

runTest().catch(err => {
console.log('Error: ' + err)
})

执行后打印顺序如下

1
2
javascript复制代码Running test
Error: Error: run test报错了

可以发现catch中截获了异常

接下来开始进入run函数了

3.4.2、 打印色彩字体
1
2
3
4
5
6
javascript复制代码// 这个看上面的引入模块解析即可
console.log(
chalk.bold(
gradient.morning('\n🚀 Welcome to Eslint & Prettier Setup for Vite!\n')
)
);
3.4.3、 交互式命令行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
javascript复制代码export function getOptions() {
const OPTIONS = [];
fs.readdirSync(path.join(__dirname, 'templates')).forEach((template) => {
const { name } = path.parse(path.join(__dirname, 'templates', template));

OPTIONS.push(name);
});
return OPTIONS;
}

export function askForProjectType() {
return enquirer.prompt([
{
type: 'select',
name: 'projectType',
message: 'What type of project do you have?',
choices: getOptions(),
},
{
type: 'select',
name: 'packageManager',
message: 'What package manager do you use?',
choices: ['npm', 'yarn'],
},
]);
}

try {
const answers = await askForProjectType();
projectType = answers.projectType;
packageManager = answers.packageManager;
} catch (error) {
console.log(chalk.blue('\n👋 Goodbye!'));
return;
}

getOptions 函数根据fs.readdirSync读取项目工程template文件夹下的所有文件,并通过path.parse转换对象,来获取文件名称name。

askForProjectType函数通过enquirer.prompt返回两个交互式命令行,供用户进行选择
projectType选择项目类型: 【react-ts】 【react】【vue-ts】 【vue】
packageManager选择项目包管理方式:【npm】 【yarn】

3.4.4、根据交互命令行返回结果进行匹配模板

假如我们上面选择的是[vue-ts]

1
2
3
js复制代码const { packages, eslintOverrides } = await import(
`./templates/${projectType}.js`
);

/template/vue-ts.js模板中的代码(其中代码较多但一看就明白我就不贴了),就是export导出了两个固定的模板变量数组,packages则相当于要引入的npm模块列表,eslintOverrides这算是.eslintrc.json初始化模板。

3.4.5、拼接变量数组
1
2
3
javascript复制代码const packageList = [...commonPackages, ...packages];
const eslintConfigOverrides = [...eslintConfig.overrides, ...eslintOverrides];
const eslint = { ...eslintConfig, overrides: eslintConfigOverrides };

commonPackages是shared.js中预定义的公共的npm 模块
eslint则是通过公共npm模块中的eslintConfig和上面选择的template/xxxx.js中的进行拼接组成。

3.4.6、 生成安装依赖包的命令
1
2
3
4
javascript复制代码const commandMap = {
npm: `npm install --save-dev ${packageList.join(' ')}`,
yarn: `yarn add --dev ${packageList.join(' ')}`,
};

将packageList数组通过join转换为字符串,通过命令将所有拼接npm模块一起安装

image.png

3.4.7、 读取项目的vite配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码  const projectDirectory = process.cwd();

const viteJs = path.join(projectDirectory, 'vite.config.js');
const viteTs = path.join(projectDirectory, 'vite.config.ts');
const viteMap = {
vue: viteJs,
react: viteJs,
'vue-ts': viteTs,
'react-ts': viteTs,
};

const viteFile = viteMap[projectType];
const viteConfig = viteEslint(fs.readFileSync(viteFile, 'utf8'));
const installCommand = commandMap[packageManager];

if (!installCommand) {
console.log(chalk.red('\n✖ Sorry, we only support npm and yarn!'));
return;
}

根据选择的项目类型,来拼接vite.config的路径,并读取项目中的vite.config配置文件

上面用到了一个函数viteEslint,这个具体的实现可以去看shared.js中,主要就是读取文件内容后,传入的参数code,就是vite.config.ts中的所有字符

1656558114378.png
通过babel的parseSync转换为ast。ast对象如下图所示

1656558646620.png

对ast数据进行了一系列的处理后,再通过babel的transformFromAstSync将ast转换为代码字符串。

对于babel处理这一块我也不太了解,有时间我得去加一下餐,具体的可以参考 juejin.cn/post/684490…

3.4.8 执行命令、执行完将eslint和prettier配置重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
javascript复制代码const spinner = createSpinner('Installing packages...').start();
exec(`${commandMap[packageManager]}`, { cwd: projectDirectory }, (error) => {
if (error) {
spinner.error({
text: chalk.bold.red('Failed to install packages!'),
mark: '✖',
});
console.error(error);
return;
}

const eslintFile = path.join(projectDirectory, '.eslintrc.json');
const prettierFile = path.join(projectDirectory, '.prettierrc.json');
const eslintIgnoreFile = path.join(projectDirectory, '.eslintignore');

fs.writeFileSync(eslintFile, JSON.stringify(eslint, null, 2));
fs.writeFileSync(prettierFile, JSON.stringify(prettierConfig, null, 2));
fs.writeFileSync(eslintIgnoreFile, eslintIgnore.join('\n'));
fs.writeFileSync(viteFile, viteConfig);

spinner.success({ text: chalk.bold.green('All done! 🎉'), mark: '✔' });
console.log(
chalk.bold.cyan('\n🔥 Reload your editor to activate the settings!')
);
});

首先通过createSpinner来创建一个命令行中的加载,然后通过child_process中的exec来执行[3.4.6]中生成的命令,去安装依赖并进行等待。

如果命令执行成功,则通过fs.writeFileSync将生成的数据写入到三个文件当中.eslintrc.json、.prettierrc.json、.eslintignore、vite.config.xx。

4、npm init、npx

印象里面大家可能对它的记忆可能都停留在,npm init之后是快速的初始化package.json,并通过交互式的命令行让我们输入需要的字段值,当然如果想直接使用默认值,也可以使用npm init -y。

create-app-react创建项目命令,官网链接可以直接查看 create-react-app.dev/docs/gettin…

1
2
3
4
5
6
7
js复制代码//官网的三种命令
npx create-react-app my-app
npm init react-app my-app
yarn create react-app my-app

//我又发现npm create也是可以的
npm create react-app my-app

上述这些命令最终效果都是可以执行创建项目的

同样的vite创建项目的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码//官网的命令
npm create vite@latest
yarn create vite
pnpm create vite

// 指定具体模板的
// npm 6.x
npm create vite@latest my-vue-app --template vue
//npm 7+, extra double-dash is needed:
npm create vite@latest my-vue-app -- --template vue

yarn create vite my-vue-app --template vue

pnpm create vite my-vue-app --template vue

可以发现vite官网没有使用npx命令,不过我在我自己电脑上尝试了另外几个命令确实也是可以的

1
2
javascript复制代码npx create-vite my-app
npm init vite my-app

image.png

通过上面的对比可以一个小问题,yarn create去官网查了是存在这个指令的,官网地址可看 classic.yarnpkg.com/en/docs/cli…

而对于npm create这个命令在npm官网是看不到的,但是在一篇博客中发现了更新日志

image.png

意思就是说npm create xxx和npm init xxx 以及yarn create xxx效果是一致的。那么我们来本文的命令行

1
2
3
4
5
6
7
8
javascript复制代码// 我们是通过npm安装的,并且包名里是包含create的
npm i create-vite-pretty-lint

// 那么以下几种方式都可以使用的
npm init vite-pretty-lint
npm create vite-pretty-lint
yarn create vite-pretty-lint
npx create-vite-pretty-lint

再来看一下npx

假如我们只在项目中安装了vite,那么node_modules中.bin文件夹下是会存在vite指令的

image.png

如果我们想在该项目下执行该命令第一种方式便是

image.png

第二种方式就是直接在package.json的scripts属性下

image.png

关于npx的详细说明可以看一下阮一峰大佬的精彩分享 www.ruanyifeng.com/blog/2019/0…

5、总结

  • npm init xxx的妙用,以及对npx的了解,感觉对package.json的每一个属性,可以专门去学习一下
  • 对于自动添加eslint和prettier配置的原理分析
  • .eslintrc.json、.eslintignore、.prettierrc.json算是直接新增文件,处理相对简单一些
  • 最重要的学习点:对vite.config文件在原有基础上的修改,这里就涉及到了AST抽象语法树

6、加餐 V8下的AST抽象语法树

有兴趣的话可以看看我前几天刚刚总结的关于V8引擎是如何运行JavaScript代码的,其中就涉及到关于AST的部分juejin.cn/post/710941…。

接下来有时间我会简单的把AST详细的学习一下,查了很多资料发现AST还是非常重要的,无论是babel、webpack、vite、vue、react、typescript等都使用到了AST。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Flutter 小技巧之 MediaQuery 和 buil

发表于 2022-06-28

今天这篇文章的目的是补全大家对于 MediaQuery 和对应 rebuild 机制的基础认知,相信本篇内容对你优化性能和调试 bug 会很有帮助。

Flutter 里大家应该都离不开 MediaQuery ,比如通过 MediaQuery.of(context).size 获取屏幕大小 ,或者通过 MediaQuery.of(context).padding.top 获取状态栏高度,那随便使用 MediaQuery.of(context) 会有什么问题吗?

首先我们需要简单解释一下,通过 MediaQuery.of 获取到的 MediaQueryData 里有几个很类似的参数:

  • viewInsets : 被系统用户界面完全遮挡的部分大小,简单来说就是键盘高度
  • padding : 简单来说就是状态栏和底部安全区域,但是 bottom 会因为键盘弹出变成 0
  • viewPadding :和 padding 一样,但是 bottom 部分不会发生改变

举个例子,在 iOS 上,如下图所示,在弹出键盘和未弹出键盘的情况下,可以看到 MediaQueryData 里一些参数的变化:

  • viewInsets 在没有弹出键盘时是 0,弹出键盘之后 bottom 变成 336
  • padding 在弹出键盘的前后区别, bottom 从 34 变成了 0
  • viewPadding 在键盘弹出前后数据没有发生变化

image-20220624115935998

可以看到 MediaQueryData 里的数据是会根据键盘状态发生变化,又因为 MediaQuery 是一个 InheritedWidget ,所以我们可以通过 MediaQuery.of(context) 获取到顶层共享的 MediaQueryData 。

那么问题来了,InheritedWidget 的更新逻辑,是通过登记的 context 来绑定的,也就是 MediaQuery.of(context) 本身就是一个绑定行为,然后 MediaQueryData 又和键盘状态有关系,所以:键盘的弹出可能会导致使用 MediaQuery.of(context) 的地方触发 rebuild,举个例子:

如下代码所示,我们在 MyHomePage 里使用了 MediaQuery.of(context).size 并打印输出,然后跳转到 EditPage 页面,弹出键盘 ,这时候会发生什么情况?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
dart复制代码
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("######### MyHomePage ${MediaQuery.of(context).size}");
return Scaffold(
body: Container(
alignment: Alignment.center,
child: InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: new Text(
"Click",
style: TextStyle(fontSize: 50),
),
),
),
);
}
}

class EditPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text("ControllerDemoPage"),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
new Container(
margin: EdgeInsets.all(10),
child: new Center(
child: new TextField(),
),
),
new Spacer(),
],
),
);
}
}

如下图 log 所示 , 可以看到在键盘弹起来的过程,因为 bottom 发生改变,所以 MediaQueryData 发生了改变,从而导致上一级的 MyHomePage 虽然不可见,但是在键盘弹起的过程里也被不断 build 。

image-20220624121917686

试想一下,如果你在每个页面开始的位置都是用了 MediaQuery.of(context) ,然后打开了 5 个页面,这时候你在第 5 个页面弹出键盘时,也触发了前面 4 个页面 rebuild,自然而然可能就会出现卡顿。

那么如果我不在 MyHomePage 的 build 方法直接使用 MediaQuery.of(context) ,那在 EditPage 里弹出键盘是不是就不会导致上一级的 MyHomePage 触发 build ?

答案是肯定的,没有了 MediaQuery.of(context).size 之后, MyHomePage 就不会因为 EditPage 里的键盘弹出而导致 rebuild。

所以小技巧一:要慎重在 Scaffold 之外使用 MediaQuery.of(context) ,可能你现在会觉得奇怪什么是 Scaffold 之外,没事后面继续解释。

那到这里有人可能就要说了:我们通过 MediaQuery.of(context) 获取到的 MediaQueryData ,不就是对应在 MaterialApp 里的 MediaQuery 吗?那它发生改变,不应该都会触发下面的 child 都 rebuild 吗?

这其实和页面路由有关系,也就是我们常说的 PageRoute 的实现。

如下图所示,因为嵌套结构的原因,事实上弹出键盘确实会导致 MaterialApp 下的 child 都触发 rebuild ,因为设计上 MediaQuery 就是在 Navigator 上面,所以弹出键盘自然也就触发 Navigator 的 rebuild。

image-20220624141749056

那正常情况下 Navigator 都触发 rebuild 了,为什么页面不会都被 rebuild 呢?

这就和路由对象的基类 ModalRoute 有关系,因为在它的内部会通过一个 _modalScopeCache 参数把 Widget 缓存起来,正如注释所说:

缓存区域不随帧变化,以便得到最小化的构建。

举个例子,如下代码所示:

  • 首先定义了一个 TextGlobal ,在 build 方法里输出 "######## TextGlobal"
  • 然后在 MyHomePage 里定义一个全局的 TextGlobal globalText = TextGlobal();
  • 接着在 MyHomePage 里添加 3 个 globalText
  • 最后点击 FloatingActionButton 触发 setState(() {});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
dart复制代码class TextGlobal extends StatelessWidget {
const TextGlobal({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
print("######## TextGlobal");
return Container(
child: new Text(
"测试",
style: new TextStyle(fontSize: 40, color: Colors.redAccent),
textAlign: TextAlign.center,
),
);
}
}
class MyHomePage extends StatefulWidget {
final String? title;
MyHomePage({Key? key, this.title}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
TextGlobal globalText = TextGlobal();
@override
Widget build(BuildContext context) {
print("######## MyHomePage");
return Scaffold(
appBar: AppBar(),
body: new Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
globalText,
globalText,
globalText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {});
},
),
);
}
}

那么有趣的来了,如下图 log 所示,"######## TextGlobal" 除了在一开始构建时有输出之外,剩下 setState(() {}); 的时候都没有在触发,也就是没有 rebuild ,这其实就是上面 ModalRoute 的类似行为:弹出键盘导致了 MediaQuery 触发 Navigator 执行 rebuild,但是 rebuild 到了 ModalRoute 就不往下影响。

其实这个行为也体现在了 Scaffold 里,如果你去看 Scaffold 的源码,你就会发现 Scaffold 里大量使用了 MediaQuery.of(context) 。

比如上面的代码,如果你给 MyHomePage 的 Scaffold 配置一个 3333 的 ValueKey ,那么在 EditPage 弹出键盘时,其实 MyHomePage 的 Scaffold 是会触发 rebuild ,但是因为其使用的是 widget.body ,所以并不会导致 body 内对象重构。

如果是 MyHomePage 如果 rebuild ,就会对 build 方法里所有的配置的 new 对象进行 rebuild;但是如果只是 MyHomePage 里的 Scaffold 内部触发了 rebuild ,是不会导致 MyHomePage 里的 body 参数对应的 child 执行 rebuild 。

是不是太抽象?举个简单的例子,如下代码所示:

  • 我们定义了一个 LikeScaffold 控件,在控件内通过 widget.body 传递对象
  • 在 LikeScaffold 内部我们使用了 MediaQuery.of(context).viewInsets.bottom ,模仿 Scaffold 里使用 MediaQuery
  • 在 MyHomePage 里使用 LikeScaffold ,并给 LikeScaffold 的 body 配置一个 Builder ,输出 "############ HomePage Builder Text " 用于观察
  • 跳到 EditPage 页面打开键盘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
dart复制代码class LikeScaffold extends StatefulWidget {
final Widget body;

const LikeScaffold({Key? key, required this.body}) : super(key: key);

@override
State<LikeScaffold> createState() => _LikeScaffoldState();
}

class _LikeScaffoldState extends State<LikeScaffold> {
@override
Widget build(BuildContext context) {
print("####### LikeScaffold build ${MediaQuery.of(context).viewInsets.bottom}");
return Material(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [widget.body],
),
);
}
}
····
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
return new LikeScaffold(
body: Builder(
builder: (_) {
print("############ HomePage Builder Text ");
return InkWell(
onTap: () {
Navigator.of(context).push(CupertinoPageRoute(builder: (context) {
return EditPage();
}));
},
child: Text(
"FFFFFFF",
style: TextStyle(fontSize: 50),
),
);
},
),
);
}
}

可以看到,最开始 "####### LikeScaffold build 0.0 和 ############ HomePage Builder Text 都正常执行,然后在键盘弹出之后,"####### LikeScaffold build 跟随键盘动画不断输出 bottom 的 大小,但是 "############ HomePage Builder Text ") 没有输出,因为它是 widget.body 实例。

所以通过这个最小例子,可以看到虽然 Scaffold 里大量使用 MediaQuery.of(context) ,但是影响范围是约束在 Scaffold 内部。

接着我们继续看修改这个例子,如果在 LikeScaffold 上嵌套多一个 Scaffold ,那输出结果会是怎么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
var routeLists = routers.keys.toList();
///多加了个 Scaffold
return Scaffold(
body: new LikeScaffold(
body: Builder(
·····
),
),
);
}

答案是 LikeScaffold 内的 "####### LikeScaffold build 也不会因为键盘的弹起而输出,也就是: LikeScaffold 虽然使用了 MediaQuery.of(context) ,但是它不再因为键盘的弹起而导致 rebuild 。

因为此时 LikeScaffold 是 Scaffold 的 child ,所以在 LikeScaffold 内通过 MediaQuery.of(context) 指向的,其实是 Scaffold 内部经过处理的 MediaQueryData。

image-20220624150712453

在 Scaffold 内部有很多类似的处理,例如 body 里会根据是否有 Appbar 和 BottomNavigationBar 来决定是否移除该区域内的 paddingTop 和 paddingBottom 。

所以,看到这里有没有想到什么?为什么时不时通过 MediaQuery.of(context) 获取的 padding ,有的 top 为 0 ,有的不为 0 ,原因就在于你获取的 context 来自哪里。

举个例子,如下代码所示, ScaffoldChildPage 作为 Scaffold 的 child ,我们分别在 MyHomePage和 ScaffoldChildPage 里打印 MediaQuery.of(context).padding :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
dart复制代码class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("MyHomePage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Scaffold(
appBar: AppBar(
title: new Text(""),
),
extendBody: true,
body: Column(
children: [
new Spacer(),
ScaffoldChildPage(),
new Spacer(),
],
),
);
}
}
class ScaffoldChildPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("ScaffoldChildPage MediaQuery padding: ${MediaQuery.of(context).padding}");
return Container();
}
}

如下图所示,可以看到,因为此时 MyHomePage 有 Appbar ,所以 ScaffoldChildPage 里获取到 paddingTop 是 0 ,因为此时 ScaffoldChildPage 获取到的 MediaQueryData 已经被 MyHomePage 里的 Scaffold 改写了。

image-20220624151522429

如果此时你给 MyHomePage 增加了 BottomNavigationBar ,可以看到 ScaffoldChildPage 的 bottom 会从原本的 34 变成 90 。

image-20220624152008795

到这里可以看到 MediaQuery.of 里的 context 对象很重要:

  • 如果页面 MediaQuery.of 用的是 Scaffold 外的 context ,获取到的是顶层的 MediaQueryData ,那么弹出键盘时就会导致页面 rebuild
  • MediaQuery.of 用的是 Scaffold 内的 context ,那么获取到的是 Scaffold 对于区域内的 MediaQueryData ,比如前面介绍过的 body ,同时获取到的 MediaQueryData 也会因为 Scaffold 的配置不同而发生改变

所以,如下动图所示,其实部分人会在 push 对应路由地方,通过嵌套 MediaQuery 来做一些拦截处理,比如设置文本不可缩放,但是其实这样会导致键盘在弹出和收起时,触发各个页面不停 rebuild ,比如在 Page 2 弹出键盘的过程,Page 1 也在不停 rebuild。

1111333

所以,如果需要做一些全局拦截,推荐通过 useInheritedMediaQuery 这种方式来做全局处理。

1
2
3
4
5
6
dart复制代码return MediaQuery(
data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
child: MaterialApp(
useInheritedMediaQuery: true,
),
);

所以最后做个总结,本篇主要理清了:

  • MediaQueryData 里 viewInsets \ padding \ viewPadding 的区别
  • MediaQuery 和键盘状态的关系
  • MediaQuery.of 使用不同 context 对性能的影响
  • 通过 Scaffold 内的 context 获取到的 MediaQueryData 受到 Scaffold 的影响

那么,如果看完本篇你还有什么疑惑,欢迎留言评论交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰玖】pinball

发表于 2022-06-27

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 30 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】和 【pinball】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. 认识视口与相机

相机是我们日常生活中非常常见的概念,在 Flame 中,相机的概念如何理解呢?现实生活中,当你使用相机拍出一张照片,其囊括的区域是有限的,这个区域也就是视口 Viewport。下面是 Flame 中对Camera 类的定义,其继承自 Projector ,且持有 Viewport 对象。


其中 Projector 是对投影的抽象,Flame 只是个二维的游戏引擎,所以投影的概念也很简单。就是对一个平面空间点位,进行操作,产出与之对应的点位而已。如下的 projectVector 方法的作用是:传入一个 Vector2 ,进行变换后,产出一个 Vector2 。

简单来说,相机的作用是:在视口内对原本空间坐标信息进行变换,完成对应的功能需求。注意,这里的 Camera 类和硬件设备的相机没有半毛钱关系。


2. 简单使用 Camera

FlameGame 中持有 CameraWrapper 对象,该对象内部持有 Camera 对象。如下箭头所示,FlameGame 中,可以通过 get camera 来访问相机。另外 FlameGame 的尺寸也是由相机决定的。


我们知道, 默认情况在 FlameGame 会填充整个窗口,而且背景是黑色的。


当窗口尺寸发生变化时,由于角色的坐标、尺寸等数据和逻辑像素是 1:1 的对应关系,也就是说坐标点没有进行过任何变换。所以角色的显示情况不会有任何变化:代码见 【29/01】


下面通过使用 FixedResolutionViewport 视口,实现固定视口尺寸的需求。此时游戏视口尺寸和 窗口尺寸 就不是一个概念了。无论应用窗口有多大,对游戏而言视口尺寸是恒定的。如下白色背景构件添加到游戏场景中,布满视口,视口会根据大小来适应窗口 ,不在视口区域内的部分会显示底色。【29/02】

比如上图中默认相机的视口尺寸是 900*600 ,并不是指白色区域的是 900*600 逻辑像素。另外,可以看到角色的尺寸没有改动,但在这个视口尺寸下,就会显得较小。为相机指定视口直接用 camera.viewport 指定即可。

1
2
3
4
5
6
7
8
dart复制代码---->[TolyGame]----
@override
Future<void> onLoad() async {
final Vector2 fixSize = Vector2(900, 600,);
camera.viewport = FixedResolutionViewport(fixSize);
add(Background());
add(HeroMan()..position=size/2);
}

比如下面将视口指定为 90*60 ,相对而言角色的尺寸就会变大。所以,从这里可以体会一下相机视口对于坐标系变换的特性。

此时改变窗口尺寸,通过打印日志可以发现, FlameGame 中的尺寸始终保持不变。这就是 FixedResolutionViewport 的作用,它可以保证在任何窗口尺寸下,游戏视口尺寸的恒定。也就是说,让游戏的可见部分在所有设备上都是相同的。


2. 相机的变换操作

相机的变换是针对于整个视口进行的,也就是说,可视区域内的角色呈现都会受到相机变换的影响。比如在现实生活中,当你移动相机,或者拉进、远离相机和目标的位置,都会影响最终的成像情况。

通过如下案例来说明一下相机变换操作对显示的影响:小人在中间,背景中左右各有 18 个原点。可以注意到,当圆点在视口之外,是无法显示的。就像相机拍照时,只能显示出其成像的区域。代码详见 【29/03】


相机缩放是比较简单的,对 camera.zoom 值进行改变即可:

1
2
3
4
5
6
7
dart复制代码if (event.logicalKey == LogicalKeyboardKey.keyZ && isKeyDown) {
camera.zoom += 0.1;
}

if (event.logicalKey == LogicalKeyboardKey.keyX && isKeyDown) {
camera.zoom -= 0.1;
}

如下,通过减小 zoom 值,可以达到缩小的目的;就相当于照相机远离目标,从而成像区域可以包含更多内容,但视口中的内容也会相对变小。


同理,增加 zoom 值,可以达到放大的目的;就相当于照相机靠近目标,从而成像区域包含内容减少,但视口中的内容也会相对变大。简单来说,就是近大远小。


我们也可以对相机进行移动,从而改变成像区域的内容。Camera 中提供了 moveTo 和 snapTo 两个移动方法,分别表示动画移动到某点和立刻移动到某点。并且可以通过 camera.speed 设置移动的速度。

1
2
3
dart复制代码if (event.logicalKey == LogicalKeyboardKey.arrowUp && isKeyDown) {
camera.moveTo(Vector2(0, size.y/2-37/2));
}


3.相机的伴随移动

相机伴随角色移动很好理解,比如现实生活中拍电影,摄像机需要跟随演员同步运动,这样才能保证演员在移动时常驻在视图中。官方的案例对这个知识点的说明比较好,这里就对它介绍一下。

场景中主要有 3 种构件:主角 、背景场地 和岩石方块 。场地是圆形和正方向构成的,颜色随机,其中圆形是正方向的内接圆。岩石随机出现在场地中,主角是一个动画帧。


如下所示,在角色移动过程中,始终保持在中心位置,但感官上它确实在运动。通过相机和角色的伴随移动,就可以始终让角色成为焦点,角色在移动的过程中,视口内容因相机的移动而扩展,这是符合我们常识的。代码详见 【29/04】

代码实现起来非常简单,只要调用 camera.followComponent 方法,指定需要跟随的构件即可。这样当构件的位置发生改变,相机也会随之变化。

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码@override
Future<void> onLoad() async {
final Vector2 fixSize = Vector2(500, 500,);
camera.viewport = FixedResolutionViewport(fixSize);
add(Ground());
add(ember = MovableEmber());
camera.speed = 1;
camera.followComponent(ember, worldBounds: Ground.bounds);
for (var i = 0; i < 30; i++) {
add(Rock(Vector2(Ground.genCoord(), Ground.genCoord())));
}
}

该案例,当角色和岩石碰撞时,可以看出角色在视口区域的 中上方 ,而且会动画平滑过渡;离开岩石后,又会在视口中间。在 MovableEmber 中可以看到碰撞逻辑,执行的是相机的 setRelativeOffset 方法。可以看出,相机的使用还是比较简单的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dart复制代码@override
void onCollision(Set<Vector2> points, PositionComponent other) {
super.onCollision(points, other);
if (other is Rock) {
gameRef.camera.setRelativeOffset(Anchor.topCenter);
}
}

@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (other is Rock) {
gameRef.camera.setRelativeOffset(Anchor.center);
}
}

4. pinball 中相机的处理

pinball 中相机的行为被封装为 CharacterSelectionBehavior 构建,用于处理相机的行为。

如下所示,在点击 Play 时,场景会进行放大和移动。同样,游戏结束时也会有个类似的放大,移动到排行榜的位置。


对于不同的状态,操纵摄像机进行不同的处理,这里通过 _foci 映射来维护状态 GameStatus 和相机参数信息 _FocusData:

1
dart复制代码final Map<GameStatus, _FocusData> _foci = {};


CameraFocusingBehavior 监听着 GameState 的变化,所以可以在游戏状态变化时进行对应的处理。和新方法是 onNewState 回调中执行的 _zoomTo 方法:


这里 pinball 项目中封装了 CameraZoom 特效对动画缩放进行了封装,本质就是不断改变 zoom 值产生动画效果而已。其实 flame 本身应该提供对相机的动画缩放,已经动画结束的回调监听。


到这里,关于相机和视口就简单地介绍完毕。这个系列中,整个 Flame 的各个方面基本上都涵盖了,并且结合 Flutter 官方开源的 pinball 项目进行源码分析,或多或少对大家研究 Flutter 休闲游戏开发有所帮助。那第一季的 Flutter&Flame 游戏开发入门教程就到这里。

另外关于地图、flame_forge2d 等知识以后再说吧,是否开启第二季,会根据本系列的关注度、热度、或是 Flame 的发展综合考虑是否继续研究。目前看来,本系列的文章并没有太多人看,所以没有太大的动力去研究,我也不想投入太多的精力在游戏开发中。所以如果本系列对你有所帮助,还望多多点赞支持,后会有期 ~

  • @张风捷特烈 2022.06.27 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【若川视野 x 源码共读】第35期 为 vite 项目自

发表于 2022-06-26

源码共读前言

为了能帮助到更多对源码感兴趣、想学会看源码、提升自己写作和前端技术能力的同学。 帮助读者夯实基础,查漏补缺,开阔眼界,拓宽视野,知其然知其所以然。

我倾力组织了每周一起学200行左右的源码共读活动。我写有《学习源码整体架构系列》20余篇,走过路过的小伙伴可以点击关注下这个目前是掘金关注数最多的专栏。

欢迎点此扫码加我微信 ruochuan02 交流,参与 源码共读 活动,每周大家一起学习200行左右的源码,共同进步。可以持续关注我@若川。

从易到难推荐学习顺序

活动介绍和顺序具体看这里从易到难推荐学习顺序

提交笔记

提交笔记方式
简言之:看任务,看辅助文章、看源码,交流讨论,在掘金写笔记,写好后提交到本文评论区。

为了给大家谋福利,另外给大家的文章带来更多阅读量,便于搜索,从2022年3月27日起,笔记可以直接发布在掘金,以《标题自取》标题不限,可以取个好标题,容易被掘金推荐。

笔记文章开头加两句话:

  • 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
  • 这是源码共读的第xx期,链接:xxx。

笔记文章最后,建议写上总结、收获、感受等。

  • 开头第一句作用是:方便每月统计评优,掘金赞助送小礼物。顺便帮忙宣传推广,让更多人参与进来,一起学习。
  • 开头第二句作用是:加上是多少期,当前任务说明的链接,方便读者知道这是什么活动。

笔记写完后,到当前期活动的文章评论区留言自己的文章和笔记特点。方便大家查阅学习交流讨论。

往期所有笔记存放在语雀讨论区。

任务发布时间

6月27日 - 7月10日。可以按照自己节奏学习,提交笔记即可(不一定要严格按照我规定的时间)。往期共读也可以及时复习,笔记未完成可以继续完成。

语雀本期任务说明链接

语雀有树形菜单,更方便查看,所以也放下语雀的这一期链接

学习任务

  • 为 vite 项目自动添加 eslint 和 prettier
  • github仓库地址 github.com/tzsk/vite-p…
  • github1s: github1s.com/tzsk/vite-p…
  • 建议克隆源码 git clone https://github.com/tzsk/vite-pretty-lint.git
  • 可以用 npm init vite 或者 yarn create vite 生成一个 vite 的项目
  • 在这个vite项目的基础上,再执行 npm init vite-pretty-lint 或者 yarn create vite-pretty-lint
  • 如果对 npm init 不了解的,可以参考我之前的 create-vue 文章 npm init,源码共读中的第9期。
  • 这样就可以看到添加 eslint 和 prettier 的效果了。添加后的变更效果可以参考我的 vite 项目 commit 记录。github.com/lxchuan12/v…
  • 学会此思路后,可以批量给多个项目添加一些配置或者修改文件等。
  • 最后大家没填问卷的,有空抽几分钟来填下源码共读活动问卷~你们的反馈至关重要wj.qq.com/s2/9304505/…

参考文章

  • 看文章,看源码,交流讨论,写笔记发布在掘金/语雀。再在这篇文章下评论放上提交笔记的链接。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰捌】pinball

发表于 2022-06-24

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 29 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】和 【pinball】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. 游戏主界面的构成

通过前面四篇,我们分析了 pinball 项目的整体结构,以及资源加载、主菜单、角色选择和玩法介绍等面板。最后剩下最重要的一块,就是游戏的主界面,主要包括六个部分:背景 、发射器 、轨道、小球、碰撞得分物 以及底部 摆动挡板 ,其中最复杂的是各种碰撞体角色。


上面提及的游戏主界面构件,基本上都定义在 pinball_components 中。这也是 pinball 项目中文件最多,结构最复杂的包。

对游戏主界面的构成分析,也就是看这些构件以什么类型,如何分布在游戏场景中。下面我们就来一一介绍场景中的几个部分。


2.背景构件

主界面最吸引人的是表面有一定的 透视感 和 深度感 ,这样就形成了一个视觉上的立体空间。其实看到资源图片就会知道,这只是图片本身的效果,并非真正的 3D 模型。


如下是背景图片资源,可以看出,图片本身就是带有透视效果的。另外背景图片的边缘是比较随意的,因为它只是一个背景,在其周围会有相关的覆盖物。


对资源进行定位,很容易知道背景图的使用场景,如下是资源管理工具会生成 boardBackground 方法应用获取背景图片:

接下来继续追踪,可以看到在 board_background_sprite_component 文件中使用到了背景资源。在分析代码时,根据细微的线索去追踪有价值信息,是一件非常有趣的事。我们面对复杂的源码体系,寻找合适的切入点非常重要,没有必要一开始就硬啃核心代码处理逻辑。从周边入手,一层层剥离外围,找出线索,逐步深入,是一个很好的研究方式。


如下,就定位到了背景所对应的构件 BoardBackgroundSpriteComponent 。其中在 onLoad 方法中通过背景图片资源,为 sprite 成员赋值,并且尺寸是原图的十分之一。


紧接着,可以继续追踪 BoardBackgroundSpriteComponent 的使用场景。如下,它在 PinballGame 的 onLoad 方法中被加入到场景中:


2. 地板构件和小球构件

在背景的底层,可以看到有个 ArcadeBackground 组件,它对应的就是最底层的地板。我们上一篇说过,这个地板会随着选择的角色而变化,这里就来看一下其中的原因。

另外有一个小细节,在移动端是没有地板的,整个视口都是游戏面板。毕竟移动端是竖屏的,没有空间显示更多内容。


如下所示,主要显示的内容由 ArcadeBackgroundSpriteComponent 觉得,他会监听 ArcadeBackgroundState 的变化。在新状态产出时,会更新 sprite 成员对应的图像。

可以看出,这里的地板的图片资源是定义在 CharacterTheme 中的,每个实现类都有相关的资源图片。比如下面是 android 对应的资源,这样就不难理解为什么角色的变化,会让地板图片产生变化。


接下来,最后一个问题,选择角色主题的逻辑是由 CharacterThemeCubit 完成的,产出的是 CharacterThemeState。而这里源码是监听的是 ArcadeBackgroundState 状态,对应的是 ArcadeBackgroundCubit ,这显然是两个不同的 Bloc 。那这两者是如何产生关联的呢?也就是说选择角色时,CharacterThemeState 的变化如何通知到 ArcadeBackgroundCubit 。

以 ArcadeBackgroundCubit#onCharacterSelected 事件为线索,不难定位到:在 CharacterSelectionBehavior 中,会监听 CharacterThemeState 的变化,来触发 ArcadeBackgroundCubit 的事件,产出新的 ArcadeBackgroundState ,从而促使地板进行切换图片资,这个逻辑搭建可以梳理一下。


另外,从上面代码可以发现,小球 Ball 构件,也会受到角色主题的影响。仔细观察也不难发现,不同的角色主题,对应的小球颜色是不同的:

dash android sparky

通过查看 BallSpriteComponent 中逻辑的,可以知道小球构件会监听 BallState 的状态,来更新图片资源,而该资源就是定义在主题类中的。

比如 dash 主题对应的是小蓝球:

当用户选择某个角色主题,在 CharacterSelectionBehavior 中触发 onNewState 。如下红框在会寻找 Ball 对应的 BallCubit 来触发 onCharacterSelected 事件,产出新状态。从而更新小球的图片资源,这和地板的资源变化是一个道理。


3. 边界构件: Boundaries

在背景的上面是 Boundaries 构件:

从构件的定义中,很容易看出三个部分分别对应 boundary 文件夹中的三个图片。可以发现 pinall 项目无论是对 Flutter 中的组件,还是 Flame 中的构件,抽离分层的处理还是很细致和到位的。


通过图片资源可以看出,Boundaries 就是对背景之上的边界进行处理。比如左图的外框,中间是镂空的透明色,将背景图叠在其下,就可以遮住上面背景四周的部位。这样在视觉上就能营造出一种立体效果。


4.发射台构件:Launcher

Launcher 构件主要包括三个部分:发射杆、发射台 、轨道 。如下是三个部分的示意:

从资源中可以看出,发射台 、轨道 、挡板 这些都是独立的资源。通过在主界面上拼接,进行组合,呈现的整体视觉效果。

如下 Launcher 构件在有四个子构件, LaunchRamp 是轨道、Plunger 是发射杆、RocketSpriteComponent 是发射台。另外 Flapper 构件用于处理边界,限制小球在外壁之内,试了一下,如果把下面的 Flapper 注释掉,小球就会飞出边界之外。 对于可以运行的代码,当我们不提明白一处的作用,可以将其屏蔽掉,看一下前后的差异,从而认识某个模块的作用。


5. 碰撞体

另外还有很多小的障碍物,用于碰撞后得分,这里就不一一细看了。在处理上都比较类似,可以根据资源中的图片来定位到相关的构件。


比如右侧中间的小恐龙,在游戏中会一直动,这对应的资源是 dino/animatronic 下的序列帧:


可以根据资源名称,追踪到 DashAnimatronic 构件,它是一个 SpriteAnimationComponent 。其中序列帧的资源使用的就是上面的图片:


底部可以摆动的两个挡板资源图片在 flipper 文件夹下,对应的构件是 BottomGroup ,包含左右两个_BottomGroupSide 构件。

整个游戏主界面的结构就介绍到这里,另外关于小球碰撞的相关处理,pinball 中使用的是 flame_forge2d ,我目前还没有研究,就先不分析了。下一篇,我们来看一下 Flame 中视口和相机的概念,并结合 pinball 中对相机的使用来实际体会相机的作用。那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.24 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰柒】pinball

发表于 2022-06-23

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 28 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】和 【pinball】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. 选择角色介绍

上一篇介绍了主界面布局结构的源码实现,本篇继续来看。在点击 Play 按钮之后,背景的游戏界面会有一个移动和缩放的特效,让游戏主题面板填充屏幕。并且会展示角色选择的面板,效果如下:

*注* :为了减少 gif 的大小,生成时采用 6fps ,所以实际效果要你下图流畅很多。


在选择对应的角色图标时,背景会进行对应的变化。很明显,在两个不同界面中的数据需要共享,很自然就会想到使用 状态管理 。


通过查看资源图片的位置,不难发现,这里四种角色主题是在 pinball_theme 中提供的。也就是说,pinball 项目中,对这个小模块进行了分包处理。可以思考一下:这个模块是相对独立的,而且有一定的拓展需求,比如增加其他的角色,可以在该包在进行处理。而不是全部塞在主项目中,这样有利于对项目结构的划分,也能让读者更容易理解。

不过仔细看一下这个包,会发现其中只是定义主题的数据,被抽象为 CharacterTheme,四种主题便是 CharacterTheme 的实现类。


比如下面的 AndroidTheme 继承自 CharacterTheme ,实现相关资源的 get 方法,其他的主题也是类似。也就是说,如果需要增加一个主题角色,可以自定义 CharacterTheme 的实现类。


2. 角色选择的业务逻辑

从上面可以看出,pinball_theme 包中,只是定义主题的数据类型,并未涉及业务逻辑 与 视图变化 。跟随 CharacterTheme 这条显示,很容易可以定位到它的使用场景。如下,在主项目的 select_character 文件夹下,盛放着角色选择的相关文件。cubit 是业务逻辑的处理,view 是视图的呈现。

可以看出 CharacterThemeCubit 非常简单,维护着 CharacterThemeState 状态数据,只有一个 characterSelected 事件,根据入参选择的角色,产出新的状态数据。其中 CharacterThemeState 状态数据也非常简单,只维护了一个 CharacterTheme 成员和四个 get 方法方便获取激活的主题。


默认情况是 DashTheme ,想要知道角色主题是何时切换的,也非常简单。因为使用了Bloc ,业务逻辑封装了,使用统一的事件接口触发。这里只需看一下何时触发 characterSelected 方法即可。这就是业务分层所带来的的好处之一。


3.角色选择的视图变化

如下,在 _Character 组件点击时,是唯一触发 characterSelected 的场景。这里的 _Character 组件就是右边可点击的圆形角色头像,其中需要传入 CharacterTheme 对象和 isSelected 是否被选中。从 build 逻辑中可以看出:选中时,透明度是 1 ,否则是 0.4 ,这和实际操作是吻合的。


角色选择的界面主体内容是中间的 Row ,包含 _CharacterPreview 和 _CharacterGrid 组件,且平分水平方向空间。


_CharacterPreview 组件是角色的预览,它会随着右侧角色的选择而发生变化。所以这里通过 BlocBuilder 让组件跟随 CharacterThemeState 的变化而重新构建。这里对 SelectedCharacter 组件进行抽离,因为其内部需要进行动画,封装之后独立性较好,表意也更明确。


这里通过 SelectedCharacter 组件进行展示某个角色,主要就是一个 Column 的上下结构。下方不断运动的序列帧通过 SpriteAnimationWidget 进行展示。这样选择角色的界面和处理逻辑就介绍地差不多了,至于背景的贴图如何变化的,在后面分析主场景再进行介绍。

其实如果不创建 _CharacterPreview ,直接在 SelectedCharacter 中使用 BlocBuilder ,或直接在 定义一个方法 返回 _CharacterPreview 中组件,在功能上是没有什么区别的。好处是少了一个类,坏处是看起来代码比较杂糅,表意性不是太好。这也没有什么定式,大家根据自己的喜好,斟酌选择即可。

程序的最终目的是实现需求,像先迈左脚,还是右脚;用袋子装鸡蛋,还是用篮子装鸡蛋;喝可乐开还喝绿茶;这并不会影响最终的目的。结合场景和个人的喜好即可,并没有必要强制必须如何如何。没必要画个圈,或让别人给你画个圈,把自己的行动范围定死,这点思考和选择的能力还是要有的。


4. 玩法介绍面板

在选择完角色之后,会弹出 How to Play 的面板,介绍玩法。可以看出这个对话框的整体结构和上面角色选择是一致的,这个对话框是源码中的 PinballDialog 组件。


玩法介绍地面板,是的 HowToPlayDialog 组件呈现的,他是一个 StatefulWidget 。因为其中有一个自动消失的需求,如红框所示,通过 closeTimer 开启一个 3 s 的延迟任务,来让对话框消失。


对话框界面的构建逻辑如下,显示的主体是 PinballDialog 对话框,对话框的内容会根据 是否是移动端 进行适配。原因很简单,移动端通过点击屏幕,桌面端通过按键触发事件 ,玩法是有区别的。


代码中对界面的分层处理是很值得借鉴的,而不是把所有的构建逻辑写在一块。抽离组件可以让整体结构更加清晰,比如下面的红框中,代码的组件和界面的呈现,两者的对应关系非常清晰。源码中的处理方式,特别是官方提供的源码,是非常值得学习和借鉴的。能将这些思想消化吸收,应用到实际开发中,是有益的。


最后,来看一下 HowToPlayDialog 是如何显示出来的,也就是触发的时机。查看一些 HowToPlayDialog 组件的使用情况,很容易可以定位到 start_game_listener.dart 中。

在 _onHowToPlay 私有方法中,进行展示 HowToPlayDialog 对话框。所以关键就是该方法的触发时机:


在 StartGameListener 中,会监听 StartGameState 状态的变化,如果是 howToPlay 状态,则会触发 _onHowToPlay 方法,显示玩法对话框。从这里可以看出 Bloc 处理可以根据状态来构建组件,也可以监听状态的变化,进行逻辑处理。


在 StartGameBloc 中,CharacterSelected 事件会将状态值变为 howToPlay 。另外在选中角色后,会触发 CharacterSelected 事件,这就是HowToPlayDialog 对话框显示的整体逻辑。


本文介绍了 pinball 游戏的角色选择和玩法介绍 两个模块。从中可以看出 bloc 在状态数据共享,以及状态变化监听中的价值。下一篇,我们将进入最重要的游戏主界面,那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.23 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰陆】pinball

发表于 2022-06-22

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 27 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】 和 【pinball】,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1.主界面视图

上一篇介绍了 pinball 中的资源加载逻辑以及 Loading 界面的布局结构。当加载完成之后,就会进入到如下的主菜单界面,有个大大的 Play 按钮。

可以看到此时两个吉祥物是在运动的,而且背景中也会显示排行榜的信息。这说明目前就已经进入了游戏的世界,通过源码可以看得更加清楚。如下所示:当资源加载完毕时,会显示 PinballGameLoadedView 组件:


通过源码可以看出 PinballGameLoadedView 主要有 3 个部分,通过 Stack 进行叠放。其中最下层是 GameWidget<PinballGame> ,也就是 Flame 的游戏场景:


2. 分数面板: _PositionedGameHud

下面来看另外两个组件, _PositionedGameHud 是分数面板 对于的组件。如下是_PositionedGameHud 的源码,可以看出只有在游戏处于 isPlaying 并且 !isGameOver 的状态下,才会显示分数面板。


另外可以 注意到,这里使用了两个 Bloc 中的数据: StartGameBloc 和 GameBloc 来获取当前游戏状态。其中 StartGameState 中只维护了 StartGameStatus 状态数据。

如下 StartGameStatus 表示游戏开始前的状态,包括四种:initial 表示初始状态;selectCharacter 表示进入选择角色面板的状态;howToPlay 表示弹出 如何玩 面板时的状态。play 表示游戏开始的状态。

1
2
3
4
5
6
dart复制代码enum StartGameStatus {
initial, /// Initial status.
selectCharacter,/// Selection characters status.
howToPlay, /// How to play status.
play, /// Play status.
}

GameBloc 中维护的状态数据比较多,比如当前的总分数 totalScore、倍率 multiplier 、小球所有奖励的历史信息 bonusHistory 等。

目前 _PositionedGameHud 中使用的是 GameStatus 状态:默认状态是 waiting 状态;游戏开始是 playing 状态;游戏结束是 gameOver 状态:

1
2
3
4
5
dart复制代码enum GameStatus {
waiting,
playing,
gameOver,
}

也就是说,左上角的分数面板通过两个 Bloc 中的状态值,保证只在游戏进行中才会显示:


3. info 图标:_PositionedInfoIcon

最后一个是 _PositionedInfoIcon 组件,如下代码在可以看出,它只是一个显示在左上角的 IconButton ,点击时执行 showMoreInformationDialog 方法。另外通过 BlocBuilder 中的构建逻辑可以看出,只有当游戏状态是 isGameOver ,才会显示。


界面显示如下,在游戏结束后,点击左上角按钮,会弹信息框对该项目进行介绍:

提示框对应的组件,可以详见源码在的 MoreInformationDialog ,这和游戏本身关系不大,就不赘述了。到这里,我们就对 pinball 界面结构有了整体的认知。


4. 游戏中的浮层

在 《【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层》中介绍过浮层在 Flame 游戏场景中的使用。这里刚好可以通过实际的场景来加深理解。如下 GameWidget 中有三个浮层:

其中 PlayButtonOverlay 就是开始菜单中的 Play 按钮。在点击时,通过 StartGameBloc 触发 PlayTapped 事件来通知游戏开始。


其中 ReplayButtonOverlay 结束游戏中的 Replay 按钮。在点击时,通过 GameBloc 触发 GameStarted 来重置游戏状态;通过 StartGameBloc 触发 PlayTapped 事件来通知游戏开始。


最后是 MobileControls,可以看出只有在移动端,才会添加这个浮层。原因也很简单,因为移动端一般不会外接键盘,所以通过 MobileControls 来模拟按键,触发事件。


另外,从中我们能学到一个非常实用的小知识:在移动端通过 Game 对象可以发送按下键盘的事件。这样在移动端,可以通过按扭的事件,来发送键盘事件,这样在游戏中只需要考虑键盘事件即可。如下的 MobileDpad 组价是一个透明层,左右两侧的点击分别对应按键的左右:


本文我们分析了 pinball 主界面的布局,以及相关组件的显示逻辑,并且认识了 StartGameBloc 和 GameBloc 两个维护游戏状态的Bloc 。下一篇我们将继续分析 pinall 的源码,看一下如何选择角色、如何弹出 how to play 的信息面板。那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.22 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰伍】pinball

发表于 2022-06-21

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 26 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. 加载界面在哪里

pinball 在游戏开始时,会显示资源加载的界面,是一个加载的进度条,如下所示。那问题来了,如何定位这个界面在源码中的位置。这样才能有机会分析资源加载的代码:


从一些外在表征去定位源码,是一个非常有用手段,比如在资源文件中可以看出 loading_game 的文件夹,其中包含着 io_pinball.png 的图片。这就说明只要找到什么地方使用了 io_pinball.png,就可以发现相关视图处理的代码逻辑


全局搜索一下,就不难发现,该图片名称在 lib/gen/assets.gen.dart 中被使用:

点进去可以看到该文件是通过工具自动生成的资源管理代码,ioPinball 代表这个图片资源:


然后 顺藤摸瓜 ,就可以找到图片资源使用的场景,这就像根据线索来探查真相。现在知道一开始加载的界面的代码在 lib/assets_manager/views/assets_loading_page.dart 中。这样我们就能通过源码来分析一下界面实现的逻辑,包括界面如何布局,进度如何变化等。


2. 加载界面 - 背景与图片

加载中的布局主要右四个部分组成,分别是 背景 、图片 、Loading 文字以及 进度条 :

在上面可以看出,图片本身背景是透明的,所以背景中的横线条纹在源码中一定有其出处:从界面组件 AssetsLoadingPage 的实现中可以看出,背景是通过 CrtBackground 装饰进行绘制的。其实这里的 Container 没有使用其他属性完全可以换成 DecoratedBox 组件,更加轻便:

这里的选择是自定义子类集成自 BoxDecoration,感觉并没有太大的必要性。直接使用 BoxDecoration 指定 gradient 参数就行了,不过也无伤大雅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码class CrtBackground extends BoxDecoration {
/// {@macro crt_background}
const CrtBackground()
: super(
gradient: const LinearGradient(
begin: Alignment(1, 0.015),
stops: [0.0, 0.5, 0.5, 1],
colors: [
PinballColors.darkBlue,
PinballColors.darkBlue,
PinballColors.crtBackground,
PinballColors.crtBackground,
],
tileMode: TileMode.repeated,
),
);
}

这里的图片组件是通过 ioPinball 对象调用 image() 方法获取的,其实这就是自动生成的代码给的一个形式语法糖。

1
2
3
4
dart复制代码Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Assets.images.loadingGame.ioPinball.image(),
),

ioPinball 类型为 AssetGenImage ,是一个 ImageProvider ,也就是 Image 组件在需要传入的 image 参数类型。该类中的 image 方法,提供了构造 Image 所需的可选属性,返回 Image 组件。并将自身作为构造 Image 组件的 image 入参:

也就是说,下面的两种写法是等价的,只不过上面在的写法不需要嵌入在 Image 组件下而已。本质上没有什么区别,只是简化书写形式而已:

1
2
3
dart复制代码Assets.images.loadingGame.ioPinball.image()
等价于
Image(image:Assets.images.loadingGame.ioPinball)

3. 加载界面 - 加载中文字与指示器

如下所示: Loading 文字三个点会依次出现,是个循环动画。另外加载进度通过下面的指示器来显示,整个加载中界面的 业务逻辑 只有一个: 加载进度值的计算。


Loading... 的循环动画是通过 AnimatedEllipsisText 组件实现的,这个组件感觉挺实用。如果以后有需要,可以直接拷贝过去用,这就是 Flutter 组件化的好处。

简单瞄一眼源码,这里 ... 不断运动的动画,是通过 Timer.periodic 周期触发定时器实现的,每 500 ms 触发一次更新。由于这里是单独抽离的 AnimatedEllipsisText ,所以 setState 也只是局部的组件更新,不会影响触发外界组件的重新构建。


最后,是加载页最核心的业务逻辑,该项目是通过 flutter_bloc 来进行状态管理的。这里使用 AssetsManagerCubit 来维护加载资源的逻辑,其中状态数据是 AssetsManagerState ,该状态量可以获取加载的进度。这里通过 BlocBuilder 来监听状态的变化来构建组件。

从代码中可以看出,这个像素风格的进度条,通过 PinballLoadingIndicator 组件进行显示。构造中传入进度值,红色的区域就会占据相应的百分比。


从 PinballLoadingIndicator 组件的源码实现中可以看出,这个像素风格的进度条是通过六个 _InnerIndicator 组件进行显示的。仔细数一下上图,就会发现整体是由六个细条,从上到下排列的而构成的。


4. 资源加载的业务逻辑

上面我们知道,资源加载的核心逻辑以及过程中的进度状态数据,是由 AssetsManagerCubit 进行维护的。如下,在 lib/assets_manager 文件夹中管理着资源加载的 bloc 业务逻辑和 views 视图:

下面我们就进入 AssetsManagerCubit ,来看一下资源是如何加载的,以及进度状态的产出。AssetsManagerCubit 构造时需要传入如下两个对象,其中只有一个 load 异步方法,本身还是比较简单的。


这个 load 方法,会在 AssetsManagerCubit 构造时被立刻触发:


load 方法在有一个小细节,一开始延迟了一秒钟才开始真正加载,这是因为加载是个昂贵的操作,先给出 1s 的时间,让 UI 先展示出来,然后再真正进行加载资源。这里加载资源的异步任务通过 loadables 列表进行维护:


异步操加载资源的任务,被定义在个个模块中。比如 _game.preLoadAssets() 方法,会返回所有构件图片资源加载的异步方法,其他几个也是类似。当你看到源码的这么多资源加载的异步方法,就会明白为什么这个 load 会是昂贵的。


然后通过 _triggerLoad 局部函数对象,分三波依次触发这些异步任务。每次异步任务完成时,都会产出新的状态,让已加载的资源数加一。


这样状态数据中的进度值 progress 就会变化,整个加载的小体系就得以运转,从业务逻辑到视图更新展示,可以体会一下,bloc 在其中的角色,品味一下状态管理的价值。

到这里,pinball 首次进入时资源加载,以及进度的显示流程就介绍完毕了。那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.21 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰肆】pinball

发表于 2022-06-20

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 25 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. pinball 整体介绍

Flutter 在 I/O 2022 中开源的 pinball 游戏,无疑让大家眼前一亮,看到 Flutter 在休闲游戏中也可以发挥价值。但其中使用了 Firebase 进行用户认证,并记录成绩进行排名,这对于 I/O 2022 中的小游戏,是非常必要的。

但是,由于网络原因,克隆项目后并没有跑起来。另外 pinball 目前好像只是支持 web ,这对于学习而言并不是很友好。所我打算对 pinball 这个项目的源码进行解读,从中抽离出一些模块进行介绍,,保证代码的可运行。在其中肯定也能见识到官方大佬对于整体逻辑和结构的处理技巧,相信一定有所收获。


pinball 中的场景并不是非常复杂,在游戏开始时,是资源加载的界面,会有一个加载的进度条:


主页面如下,主要是背景和 play 的按钮,相当于一个简单的菜单,可以看出整体上是一个 伪 3D 的风格,营造一种空间感。


在点击 play 之后,会有一个选择菜单,选择地板的图片纹理。在选择相关的 icon 时,地板就会进行相应的变化。


选中之后,会出现一个 How to Play 的面板,介绍玩法,及按键的操作。这个界面将在几秒后消失:


当游戏结束时,会有一个挺好看的移动转场,用户输入名字后,显示 Replay 的菜单:

总的来看,pinball 整体玩法并不是非常复杂,很符合休闲小游戏的定位。但从代码的角度来看,还是有不少内容的,特别是主游戏场景中,有很多构件,以及弹球的碰撞、发射等。


2. 应用分包处理

pinball 作为官方的开源项目,在项目结构上的处理还是很值得借鉴的。如下所示,可以看出项目对不同模块进行了分包处理。这就可以对项目进行分层管理,本质来说,这就相当于自己在本地创建了一个三方库进行依赖。


在主项目的 pubspec.yaml 中,可以在 dependencies 依赖节点引入这些包。使用 path 属性,指定相对路径即可,如下图所示:


3. 简看各个包的作用

既然把每个模块进行分包处理,那么每个包的作用就相对比较明确。这里想要了解一个包的作用,可以从三个角度入手:外貌 、资源、源码 。外貌就是表面上显示的信息,比如包的名称、包的描述简介,比如 authentication_repository 包,从名称上就可以看个大概,是用于处理用户认证信息的仓储。

包中的 资源 :表示这个包中有哪些文件,从中可以对其复杂程度做一个初步的认知,比如这里 authentication_repository 就一个主要文件,可见其本身并不复杂。对于不太复杂的包,可以稍微瞄一眼其中的源码实现,不一定要全懂,但至少混个眼熟。

这里认证逻辑是非常简单的,通过 FirebaseAuth 出发 signInAnonymously 方法而已。


geometry 包也比较简单,用于提供一些方法作为几何计算的辅助:


leaderboard_repository 用于提供排行榜数据,而且数据是存储在 Firebase 云端存储的。


pinball_audio 包的作用是管理游戏中的音频资源数据,可以看出 pinball 对包的划分还是非常细致的。


pinball_components 是一个非常复杂的包,其中维护着游戏中相关的各种 Component 。前面我们也知道,对于 Flame 游戏而言,最重要的就是各种维护 Component 角色之间的关系。所以这个包必定是以后研究的重点。


pinball_flame 是专门针对于该项目的一些偏僻问题的解决方案,也就是根据实际需求对 Flame 的拓展。其中 Flame 在 1.2.0 在新加入的 parent_is_a.dart 应该就是吸取了这里的经验。框架与实际应用相辅相成,互相促进发展,是个很好的现象。


pinball_theme 也比较简单,从 assets 文件夹在就可以看出,这个包的作用的对选择地板砖的主题进行设置。


pinball_ui 用于处理字体、颜色、对话框、主题等显示样式的工作:


platform_helper 中目前只有一个校验平台是否是 Android 或 iOS 的方法,没有什么其他内容。没有要太在意:


share_repository 包主要用于分享分数,维护社交平台分享文字的数据,比如 twitter 和 facebook 。


这样,对于 pinball 的整体结构我们就有了一个基本的认知。其中并不太感兴趣的是关于 Firebase 的内容,和它相关的有 authentication_repository 和 leaderboard_repository 。下一篇就来从源码的角度去分析一下其中的具体实现。相信从中可以学到很多知识,那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.20 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Flutter&Flame 游戏 - 贰叁】 资源管理与国

发表于 2022-06-19

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 24 天,点击查看活动详情


前言

这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:

  • 【Flutter&Flame 游戏 - 壹】开启新世界的大门
  • 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
  • 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
  • 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
  • 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
  • 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
  • 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
  • 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
  • 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
  • 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
  • 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
  • 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
  • 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
  • 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
  • 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
  • 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
  • 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
  • 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
  • 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
  • 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
  • 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
  • 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
  • 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
  • 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
  • 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
  • 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
  • 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
  • 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
  • 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


1. 为什么要管理资源

本文介绍一下 Flutter 项目中的资源管理和国际化的一些知识。注意,这是 Flutter 本身的知识点,不只限于 Flame 游戏开发,Flame 是在 Flutter 框架基础上的游戏引擎,可以使用 Flutter 本身的一切知识。对于一个游戏来说,图片、音频、字体等资源非常多,如果不进行管理就会非常杂乱。本文代码见【23】


比如加载图片时,每次都要去拷贝地址,而且名字写死在项目里,也不容易维护和统一加载。使用我们希望有一种手段来统一管理这些资源,最好能自动生成一些代码,帮助我们调度资源。


在探究 Flutter 官方开源的 pinball 游戏时,发现其中使用的 flutter_gen 挺不错的。可以根据资源信息,自动生成类来管理资源,比如下面红框中的代码是工具自动生成的。


2. flutter_gen 工具的安装

首先 flutter_gen 并非是一个三方插件,而是一个命令行工具。可以通过下面的命令来下载:

1
shell复制代码dart pub global activate flutter_gen

此时会出现如下的信息,可以看到一个文件夹,并且说期望把这个文件夹添加到系统的环境变量中:

我们在如下文件夹中就可以看到 fluttergen 的批处理文件:

把文件夹添加到系统环境变量中,是为了在任何地方都可以使用命令:


当输入 fluttergen --version ,有版本信息输出时,表示工具可以使用:

1
2
shell复制代码----[~  fluttergen --version
----~] FlutterGen v4.3.0

3. flutter_gen 工具的使用

首先在 pubspec.yaml 的最外层节点下,配置资源代码生成的位置。另外还可以配置其他的信息,详见 flutter_gen 官方文档:

1
2
3
yaml复制代码---->[pubspec.yaml]----
flutter_gen:
output: lib/base_course/23/gen # 代码生成的路径位置

在项目根目录使用如下指令即可,在对于的文件夹下就会生成资源管理的相关代码:

1
shell复制代码fluttergen -c pubspec.yaml


另外,注意一个小细节,资源名不能使用纯数字,或 dart 中的关键字、首字母是有意义的特殊符号,比如 :、{ 、! 等。因为资源管理本质上就是生成和文件名相同的 get 方法,进行访问,也就是说文件名必须要符合方法名的规范。虽然工具端可以进行名称的优化,但最好资源名还是自身规范一些比较好。


flutter_gen 只是提供了一个资源文件管理的方式,避免在代码中写死资源路径,不然当资源名称变化时,代码中未及时更改,就会产生潜在的隐患。本身并不影响核心的使用方式:

1
2
3
4
5
dart复制代码---->[之前]----
const String src = 'adventurer/animatronic.png';

---->[现在]----
String src = Assets.images.adventurer.animatronic.keyName;

另外注意一点,默认情况下 Flame会对资源添加前缀,图片是 assets/images/,而通过 flutter_gen 生成的代码获取的路径是全路径,所以需要清除前缀:

可以在 XXXGame 的构造方法体重清除前缀,如下所示:

1
2
3
dart复制代码TolyGame() {
images.prefix = '';
}

4. 国际化

国际化是指提供多个版本的语言文字支持,以便满足不同国家、地区的使用。这里使用的也是官方在 pinball 项目的国际化方式,也是官方推荐的方式:《Internationalizing Flutter apps》。首先在 pubspec.yaml 中 添加依赖:

1
2
3
4
5
6
yaml复制代码---->[pubspec.yaml]----
dependencies:
#...
flutter_localizations:
sdk: flutter
intl: ^0.17.0

然后在 pubspec.yaml 的 flutter 节点下,添加 generate: true ,来表示需要自动生成代码。注意,虽然都是自动生成代码,但这里和 fluttergen 是没有关系的。在 flutter 节点下,应该是官方的行为:

1
2
3
4
yaml复制代码---->[pubspec.yaml]----
flutter:
generate: true
#...

然后需要在根目录创建一个 l10n.yaml 的配置文件,给出文字资源的路径,以及生成代码文件的名称:

1
2
3
4
5
yaml复制代码---->[l10n.yaml]----
arb-dir: lib/base_course/23/i10n/arb
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false

然后在指定的文件夹下提供应用中的相关文字资源:

在 pubspec.yaml 中点击 pub get 后,或在项目根目录执行 :

1
shell复制代码flutter pub get

在 .dart_tool 中会生成相关的代码。注意这里的 flutter_gen 文件夹和上面的 flutter_gen 工具是两个不同的东西,只是名字一样。flutter_gen 工具不是官方出品的,而且只是对资源进行管理。

然后为 MaterialApp 组件指定本地化代理:

1
2
3
4
5
6
7
dart复制代码import 'package:flutter_gen/gen_l10n/app_localizations.dart';

MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: AppLocalizations.supportedLocales[0] // 指定语言
)

使用时,通过 AppLocalizations.of 调用相关资源名即可:

1
2
dart复制代码String play = AppLocalizations.of(context).play;
String options = AppLocalizations.of(context).options;


这样,当 local 被改为第一个,也就是中文时,对应的文字资源就会是中文,如下图所示:

1
2
3
4
5
dart复制代码MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
locale: AppLocalizations.supportedLocales[1] // 指定语言
)

你可以使用状态管理来切换全局的 local 参数,来达到在应用中切换语言的目的。


另外,在 pinball 在对资源的使用进行了一个小优化,对 BuildContext 拓展了一个方法,便于调用,本质上没有任何区别。

1
2
3
4
5
6
7
8
dart复制代码import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

export 'package:flutter_gen/gen_l10n/app_localizations.dart';

extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}

在使用上如下所示,确实简单一些:

1
2
dart复制代码String play = context.l10n.play;
String options = context.l10n.options;

本文介绍了 Flutter 对资源文件的管理,以及国际化的相关知识。到这里,我们就对 Flame 引擎整体上有了基本的认知。之后,我们将进入下一阶段,敬请期待。那本文就到这里,明天见 ~

  • @张风捷特烈 2022.06.19 未允禁转
  • 我的 公众号: 编程之王
  • 我的 掘金主页 : 张风捷特烈
  • 我的 B站主页 : 张风捷特烈
  • 我的 github 主页 : toly1994328

\

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…919293…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%