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

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


  • 首页

  • 归档

  • 搜索

【Flutter&Flame游戏 - 拾肆】碰撞检测 之

发表于 2022-06-08

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


前言

这是一套 张风捷特烈 出品的 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 源码分析 - 视口与相机

第一季完结,谢谢支持 ~


0. 前情回顾

之前的 矩形域 和 中心点 的包含关系,这样校验有很大的误差。上一篇介绍了 CollisionCallbacks 的使用,本篇就来基于 CollisionCallbacks 对之前的代码进行优化。首先让 TolyGame 混入 HasCollisionDetection 支持配置检测:

1
dart复制代码lass TolyGame extends FlameGame with HasCollisionDetection,

接下来让三类主要角色: HeroComponent 、Monster 、Bullet 混入 HasCollisionDetection 。确定碰撞区域,并处理碰撞逻辑。代码详见 【14/01】


1.子弹的处理逻辑

之前把子弹分为了静止 Bullet 的和运动 AnimBullet 的 ,这里把 Bullet 去掉了。一张图片也可以看做是一帧动画,只要不循环播放,先在 AnimBullet 中混入 CollisionCallbacks ,支持监听碰撞回调:

1
dart复制代码class AnimBullet extends SpriteAnimationComponent with CollisionCallbacks {

然后在 onLoad 中添加 Hitbox 碰撞区域,这里用矩形区域:

1
2
3
4
5
dart复制代码void addHitbox(){
ShapeHitbox hitbox = RectangleHitbox();
hitbox.debugMode = true;
add(hitbox);
}

然后覆写 onCollision 监听碰撞,这里的处理逻辑是:当子弹类型是 hero 射的,碰撞物体是 Monster ,则说名主角的子弹命中的怪兽,消失即可,反正亦然。

1
2
3
4
5
6
7
8
9
10
dart复制代码@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (type == BulletType.hero && other is Monster) {
removeFromParent();
}
if (type == BulletType.monster && other is HeroComponent) {
removeFromParent();
}
}

2. HeroComponent 的处理逻辑

先看 HeroComponent 中的处理,先混入 CollisionCallbacks ,支持监听碰撞回调:

1
dart复制代码class HeroComponent extends PositionComponent with HasGameRef, CollisionCallbacks

然后在 onLoad 中添加 Hitbox 碰撞区域,这里用矩形区域:

1
2
3
4
5
dart复制代码void addHitbox(){
ShapeHitbox hitbox = RectangleHitbox();
hitbox.debugMode = true;
add(hitbox);
}

然后覆写 onCollision 监听碰撞,这里的处理逻辑是当碰撞物体类型是 AnimBullet ,且类型为 BulletType.monster 时,就表示 HeroComponent 已经中弹了,通过 loss 方法添加伤害。

1
2
3
4
5
6
7
dart复制代码@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is AnimBullet&&other.type==BulletType.monster) {
loss(other.attr);
}
}

3. Monster 的处理逻辑

同样,先混入 CollisionCallbacks ,支持监听碰撞回调:

1
dart复制代码class Monster extends SpriteAnimationComponent with CollisionCallbacks,

然后在 onLoad 中添加 Hitbox 碰撞区域,这里用矩形区域:

1
2
3
4
5
dart复制代码void addHitbox(){
ShapeHitbox hitbox = RectangleHitbox();
hitbox.debugMode = true;
add(hitbox);
}

然后覆写 onCollision 监听碰撞,这里的处理逻辑是当碰撞物体类型是 AnimBullet ,且类型为 BulletType.hero 时,就表示 Monster 已经中弹了,通过 loss 方法添加伤害。

1
2
3
4
5
6
7
dart复制代码@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollision(intersectionPoints, other);
if (other is AnimBullet&&other.type==BulletType.hero) {
loss(other.attr);
}
}

其实 Monster 和 HeroComponent 整体结构是类似的。都具有发射子弹、减少生命值、基础属性等特点。但也有很多不同点,比如主角是用户的主动操作,怪物是系统的固定行为。以后可以考虑进一步进行抽象,求同存异来,优化逻辑。

最后把之前在 TolyGame#update 中校验的一堆逻辑删掉即可。从这里可以看出,CollisionCallbacks 的优势是: 可以给让构件监听到自身的碰撞事件,从而主动处理碰撞逻辑;而之前那样,就像是一个 监工 ,在不断看着有没有人碰撞,构件本身是被动的。


4. 区域调整

由于图片的关系,可能碰撞区域会比较大,如果我们只想取区域中的某一块,而非整体区域,该怎么做呢?


默认情况下,RectangleHitbox 会占据整个构件区域,但其构造方法中可以指定一些尺寸和位置属性。


下面我们来结合上一篇中的碰撞试针,来看一下如何对碰撞区域进行调整,代码详见 【14/02】。下图中将矩形区域的宽变成角色尺寸的 0.5 倍,高变为 0.8 倍。可以看出默认情况下,矩形区域的左上角和构件的左上角对齐:

1
2
3
4
5
6
7
8
dart复制代码void addHitbox(){
Vector2 boxSize = Vector2(size.x*0.5, size.y*0.8);
ShapeHitbox hitbox = RectangleHitbox(
size: boxSize,
);
hitbox.debugMode = true;
add(hitbox);
}

当将RectangleHitbox 的 anchor 指定为 Anchor.center 时,可以看出此时矩形的中心和构件的左上角对齐。

1
2
3
4
5
6
7
8
9
dart复制代码void addHitbox(){
Vector2 boxSize = Vector2(size.x*0.5, size.y*0.8);
ShapeHitbox hitbox = RectangleHitbox(
size: boxSize,
anchor: Anchor.center,
);
hitbox.debugMode = true;
add(hitbox);
}

然后只有通过 position 进行偏移 size/2 即可和构件的中心点对齐,可以看出此时矩形区域还需要向下微调。

1
2
3
4
5
6
7
8
9
10
dart复制代码void addHitbox(){
Vector2 boxSize = Vector2(size.x*0.5, size.y*0.8);
ShapeHitbox hitbox = RectangleHitbox(
size: boxSize,
anchor: Anchor.center,
position: size/2
);
hitbox.debugMode = true;
add(hitbox);
}

主要,偏移的微调最好用构件的尺寸分率计算得到,不要直接写死多少数值,不然在构建进行放大时,肯定会出问题。如下的操作,就可以得到一个比较满意的碰撞区域:

1
2
3
4
5
6
7
8
9
10
11
dart复制代码void addHitbox(){
Vector2 boxSize = Vector2(size.x*0.5, size.y*0.8);
double offsetFixY = -size.y*0.11;
ShapeHitbox hitbox = RectangleHitbox(
size: boxSize,
anchor: Anchor.center,
position: size/2-Vector2(0,offsetFixY)
);
hitbox.debugMode = true;
add(hitbox);
}

其他的构建,也可以用类似的方式来优化碰撞区域。本文介绍了一下如通过 CollisionCallbacks 来优化之前代码中的碰撞检测逻辑。一般的休闲游戏的重头戏就是对碰撞的检测和逻辑处理,可以说这点还是非常实用的。那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

【Flutter&Flame游戏 - 拾叁】碰撞检测 C

发表于 2022-06-07

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


前言

这是一套 张风捷特烈 出品的 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. 碰撞检测场景搭建

前面我们 矩形域 和 中心点 的包含关系校验是否碰撞,这样会有很大的误差。Flame 中提供了 CollisionCallbacks ,对碰撞检测进行了封装,支持两个形状间的配置检测。本文我们就来认识一下该如何使用这个 mixin 。

为了更好地说明碰撞,这里先用图形进行简单示意,本文将在这个场景的基础上进行测试。如下所示,定义一个 Cilrcle 构件和 Line 构件,其中 Line 的位置可以随着触点的拖动而更新:代码详见 【13/01】

如下 TolyGame 中有 Line 和 Circle 两个角色,混入 PanDetector 。覆写 onPanUpdate 方法,在触点更新时,增加 line 的位移。 另外目前 Circle 和 Line 两个构件都只是覆写 render 方法,进行绘制而已,没什么要点,代码就不贴了,详见源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dart复制代码class TolyGame extends FlameGame with PanDetector{
late Line line;
late Circle circle;

@override
Future<void> onLoad()async{
circle = Circle(radius: 40, position: size/2);
line = Line(lineWidth: 80,position: size/2-Vector2(140,0));
add(circle);
add(line);
}

@override
void onPanUpdate(DragUpdateInfo info) {
line.position+=info.delta.global;
}
}

2. 如何让构件支持碰撞检测

如下所示,直线代表 矩形域 ,圆形代表 圆形域 ,当两个区域发成重叠时,则发生碰撞。代码详见【13/02】

首先说明一点:顶层的 TolyGame 需要混入 HasCollisionDetection 才可以支持碰撞检测。

1
dart复制代码class TolyGame extends FlameGame with HasCollisionDetection,PanDetector{

另外,想让某个 Compoment 支持碰撞检测,需要三个步骤:

    1. 混入 CollisionCallbacks :
1
dart复制代码class Circle extends PositionComponent with CollisionCallbacks{
    1. 添加检测区域 Hitbox : 这个区域就是代表着当前构建件的命中区域。
1
2
3
4
5
6
7
dart复制代码late ShapeHitbox hitbox;

@override
Future<void> onLoad() async {
hitbox = CircleHitbox();
add(hitbox);
}
  • 3.覆写碰撞检测回调方法:CollisionCallbacks 中有三个碰撞回调的方法,可以监听到碰撞开始、碰撞结束、发生碰撞三个事件。

如下,在 Circle 构件中,覆写 onCollisionStart 方法,当开始发送碰撞时,将圆的颜色置为 blue;碰撞结束时,将圆的颜色置为 white 。

1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
_paint.color = Colors.blue;
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
_paint.color = Colors.white;
}

3.碰撞形状

其实这样看来,碰撞检测最重要的还是碰撞区域的确定。在 Flame 的 collisions/hitboxes 中只提供了一些常用的形状,比如 圆形 、多边形 、矩形 、屏幕 。感觉还是挺有局限性的,如果能加上 Path 自定义形状就好了。

其中我们在之前已经见识过了 RectangleHitbox ,提供将它的 debugMode 置为 true ,就可以看到边界信息。其实本质上就是为该构件确定一个碰撞检测的区域:


下面我们通过一个案例来测试一下 多边形 和 屏幕边界 的碰撞检测:【13/03】

如下定义一个菱形的 PolygonHitbox ,主要就是根据顶点,确定多边形。这样当一个 CollisionCallbacks 的角色和其碰撞,就可以接收到 onCollisionXXX 的回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码---->[Polygon]----
late ShapeHitbox hitbox;

@override
Future<void> onLoad() async {
hitbox = PolygonHitbox([
Vector2(size.x, size.y/2),
Vector2(size.x/2, size.y),
Vector2(0, size.y/2),
Vector2(size.x/2, 0),
]);
add(hitbox);
}

最后看一下 ScreenHitbox ,它本质上非常简单,就是在最外层添加一个 RectangleHitbox 而已。


总得来说,通过 CollisionCallbacks 实现碰撞的检测,本质就是定义碰撞物的区域。然后根据碰撞的回调处理逻辑而已,其实 CollisionCallbacks 也是比较有局限性的。但相比于之前的 矩形域 和 中心点 的包含关系,可以说上升了一个维度,支持了 域 和 域 之间的碰撞。

本文介绍了一下 Flame 中通过 CollisionCallbacks 实现碰撞检测的方式,下一篇我们将基于这种方式来对前面的案例进行优化。那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

2小时入门 Jetpack Compose(上)

发表于 2022-06-06

往期文章:

《00. 文章合集目录》

《10. 揭秘 Compose 原理》

《11. Google 强推的 Baseline Profiles 国内能用吗?我找 Google 工程师求证了!》

你好,我是朱涛。这是「沉思录」的第四篇文章。

最近工作有点忙的,趁着端午节3天小长假,我来写个 Compose 快速入门的教程吧!我们的目标是:「2小时入门 Compose」!

怎么学?

学习 Compose 最快的方式是什么?当然是写代码呀!可是,Google 官方给出的开源项目太复杂了,初学者看了就头疼。我举个例子,Google 官方最简单的教学 App:Jetsnack,都有将近5000行代码,这怎能不让人望而却步?

因此,我们需要找一个更简单的项目,代码量还要再小一点。我个人比较喜欢 Google 官方的架构案例:TodoApp,它的功能足够简单,也比较贴近我们的实际生活。

不过,可惜的是,这个官方仓库并没有完整的 Compose 实现,GitHub上面类似的实现又太复杂了。怎么办呢?那我就仿着它来写一个吧。Logo 资源什么的,我也给扒过来了。

开始吧!

To Do App

整个项目的代码我已经写完了,它的结构非常简单,一共就只有三个页面:

  • 第一个页面:开屏的 Splash 页面,也就是文章开头我放的动图,这里我们用 Compose 的动画 API 就能轻松实现。
  • 第二个页面:首页,也就是代处理任务的列表。
  • 第三个页面:任务详情。

整个工程的代码量,我统计了一下,只有1400多行。

对于一个功能完整的 App,这样的代码量已经算上很小的了,想想我们工作中的一个 Presenter 代码量都不止1400行代码吧?

到这里,你会不会觉得,这个代码量实在太小了呢?这App会不会是个 Hello World 级别的呢?让我来带你看看它的功能。

工程介绍

总的来说,核心的 UI 只有这么几个文件:

开屏页面

其中,最简单的,就是 Splash 页面,它的作用只有一个:展示 Logo,接着等待一小会,进入首页。在这里,我们会实现一个动画。

这个动画分为两个部分:Logo 的「透明度」动画,还有文字的「透明度」+「位移」动画。

首页

接着,我们来看看首页的功能,它会展示当前所有的任务和状态。

这里,我写了一个进场动效,这个在 Compose 当中实现起来真没什么难度。

总的来说,就是:Index 越大的 Item,它的初始位移越大,进场的时候,再把所有 Item 挪回原处即可。

拖拽删除

然后,我还实现了一下拖拽删除功能。

拖拽删除本身没什么难度,不过,在拖拽的过程中,做一些其他的事情,还是比较有意思的。这里有两个细节:

  • 第一,手指拖动到一定范围的时候,弹出一个 Toast,告诉用户,可以松手删除了。如果你看过我的博客《揭秘 Compose 原理》的话,你一定能体会到其中的难处:如何避免 Toast 反复弹出。
  • 第二,手指拖动到一定范围后,垃圾桶图标还做了一个:倾倒的动画。

以上这两个细节,我也会在后面的博客详细介绍。

任务详情页面

详情页,这个页面反而没什么特殊东西,就是两个输入框,分别是:任务标题、任务详情,还有一个 CheckBox,代表任务是否已经完成。

这里的输入框的动画效果,是 TextField 自带的,也不需要我们自己实现,虽然有点丑,但也能用了。

Splash 代码

OK,工程介绍完了,我们就可以上代码了!这篇文章,我们先从最简单的 Splash 页面开始吧!

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
kotlin复制代码@Composable
fun Splash(offsetState: Dp, alphaState: Float) {
// 1
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.splashBackground),
contentAlignment = Alignment.Center
) {
// 2
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
// 3
Image(
modifier = Modifier
.size(LOGO_HEIGHT)
.alpha(alpha = alphaState),
painter = painterResource(id = getLogo()),
contentDescription = stringResource(id = R.string.to_do_logo)
)
// 4
Text(
modifier = Modifier
.offset(y = offsetState)
.alpha(alpha = alphaState),
text = stringResource(id = R.string.app_name),
color = MaterialTheme.colors.splashText,
style = MaterialTheme.typography.h5,
fontWeight = FontWeight.Bold,
maxLines = 1,
)
}
}
}

整个 Splash 的 UI 元素其实很简单,我标记了4个注释,我们一起看看:

  • 注释1,它是一个撑满屏幕的 Box,它相当于 Android 当中的 FrameLayout。
  • 注释2,Column,它相当于 Android 当中的 LinearLayout,并且是纵向布局。
  • 注释3,Image,它其实就是开屏当中的 LOGO。
  • 注释4,Text,它其实就是开屏当中的文字:To Do。

OK,UI 元素写出来了,动画怎么搞?

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
kotlin复制代码@Composable
fun Splash(
gotoHomeScreen: () -> Unit
) {
// 1
var start by remember { mutableStateOf(false) }
// 2
val offset by animateDpAsState(
targetValue = if (start) 0.dp else 100.dp,
animationSpec = tween(
durationMillis = 1000
)
)
val alpha by animateFloatAsState(
targetValue = if (start) 1f else 0f,
animationSpec = tween(
durationMillis = 2000
)
)

// 3
LaunchedEffect(key1 = Unit) {
start = true
delay(SPLASH_DELAY)
gotoHomeScreen()
}

Splash(offsetState = offset, alphaState = alpha)
}

其实,动画也很简单,我们通过注释来看:

  • 注释1,start 标记动画的状态。
  • 注释2,使用 animateDpAsState、animateFloatAsState 生成对应的 alpha、offset,并且,将它们传入 Splash 页面当中去使用。你可以回过头看看 Splash 页面是如何使用这两个参数的。
  • 注释3,LaunchedEffect,这里传入参数 Unit,代表它只会执行一次。它的作用,就是启动一个协程,并且在协程当中改变 start 的状态,接着延迟一小会,就可以进入首页了。

最终的效果就是这样的:

结束语

OK,恭喜你,你已经完成了一个 Compose 页面!

考虑到篇幅限制,首页、详情页,我们留到后面的博客再讲具体代码实现吧。

关于源代码,后续我会在我的公众号“朱涛的自习室”放出来,敬请关注。

好了,不多说,马上周一了,我要给公司搬砖去了。

点赞+评论+转发,请安排上吧!

感谢你的阅读,我们下次再见?[逃]。

本文转载自: 掘金

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

【Flutter&Flame游戏 - 拾贰】探索构件 角

发表于 2022-06-06

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


前言

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

  • 【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. 管理怪兽

前面把 Monster 全写在 TolyGame 中,看起来很杂乱。一个场景中可能存在多个怪兽,比如下面六个小怪和一个 Boss 。我们可以通过自定义一个 MonsterManager 构件对这些怪兽进行管理:代码详见【12/01】


在 MonsterManager 中通过入参将小怪和 Boss 的序列帧传入,这样可以中使用时自定义序列帧:

1
2
3
4
5
6
7
dart复制代码class MonsterManager extends PositionComponent with HasGameRef {
final SpriteSheet bossSpriteSheet;
final SpriteSheet stoneSpriteSheet;
MonsterManager({
required this.bossSpriteSheet,
required this.stoneSpriteSheet,
}):super(anchor: Anchor.center);

在 onLoad 方法中,通过 createBoss 方法创建并添加 Boss 构件,然后通过 createStoneMonster 方法在左右分别遍历三个小怪。创建和添加 Monster 的方式和前面是一样的,这里就不赘述了,详见源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dart复制代码@override
Future<void> onLoad() async {
createBoss();
int lineCount =3;
double step = gameRef.size.y/lineCount ;
for(int i=1;i<=lineCount;i++){
final double pY = i*step- 30;
final double pX = gameRef.size.x - 200;
createStoneMonster(Vector2(pX, pY));
}
for(int i=1;i<=lineCount;i++){
final double pY = i*step- 30;
final double pX = 150;
createStoneMonster(Vector2(pX, pY));
}
}

2. 怪兽发射子弹

现在怪物站在那傻乎乎的被打很不公平,下面看看如何让怪兽发射子弹。这里用来两个序列帧动画作为子弹的资源,如下图所示:代码详见【12/02】

其实本质上,怪物发射子弹和主角发射子弹本质上是一样的。不同点在于,主角子弹发送是用户控制的,怪物一般是定时发射子弹。另外,要区分一下子弹的类型,是怪物发射的,还是主角发射的。之前角色的弓箭是静态图片,这里可以定义一个 AnimBullet 来支持序列帧的子弹:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dart复制代码enum BulletType{
hero,
monster
}

class AnimBullet extends SpriteAnimationComponent {
double speed = 200;
final double maxRange;
final BulletType type;
final bool isLeft;

AnimBullet({
required SpriteAnimation animation,
required this.maxRange,
required this.type,
required this.speed,
this.isLeft = true,
}) : super(animation : animation);

// 略同...

然后看一下如何通过 Timer 来定时不断发射子弹,这里的 Timer 是 Flame 中封装的,不是 Flutter 自带的。如下 Monster 的各个生命周期中对 Timer 进行相关处理:onLoad 方法中初始化 _timer 对象,隔 3 s 钟触发一次 addBullet 方法添加子弹。onMount 方法中开启 _timer ,onRemove 中停止 _timer 。

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
dart复制代码---->[Monster]----
late Timer _timer;

@override
Future<void> onLoad() async {
_timer = Timer(3, onTick: addBullet, repeat: true);
}

@override
void onMount() {
super.onMount();
_timer.start();
}

@override
void update(double dt) {
super.update(dt);
_timer.update(dt);
}

@override
void onRemove() {
super.onRemove();
_timer.stop();
}

如下是 addBullet 方法,和之前主角发射子弹的逻辑基本一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dart复制代码// 添加子弹
void addBullet() {
AnimBullet bullet = AnimBullet(
type: BulletType.monster,
animation: bulletSprite,
maxRange: attr.attackRange,
speed: attr.attackSpeed,
isLeft: isLeft,
);
bullet.size = bulletSize;
bullet.anchor = Anchor.center;
bullet.priority = 1;
priority = 2;
bullet.position = position - Vector2(0, size.y/2);
gameRef.add(bullet);
}

3.怪兽发射的命中

如下所示,在怪物发射的子弹命中主角时,主角也会受到伤害。生命值降低,并且显示伤害数值:代码详见【12/03】

目前仍是校验 矩形域 和 中心点 的包含关系来判定是否命中。如下,在 update 中通过 _checkAttackHero 校验是否命中,命中时 player 触发 loss 方法掉血。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dart复制代码---->[TolyGame#update]----
@override
void update(double dt) {
super.update(dt);
_checkAttackMonster();
_checkAttackHero();
}

void _checkAttackHero() {
final Iterable<AnimBullet> bullets = children
.whereType<AnimBullet>()
.where((AnimBullet e) => e.type == BulletType.monster);
for (AnimBullet bullet in bullets) {
if (bullet.shouldRemove) {
continue;
}
if (player.containsPoint(bullet.absoluteCenter) ||
bullet.containsPoint(player.absoluteCenter)) {
bullet.removeFromParent();
player.loss(bullet.attr);
break;
}
}
}

到这里,我们已经完成了 主角 和 怪物 间的基本交互,也基本上对 Component 有了较深的理解。接下来将进一步探讨 碰撞检测 相关的知识,毕竟现在靠的是 矩形域 和 中心点 的包含关系,并不是非常准确。那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

【Flutter&Flame游戏 - 拾壹】探索构件 C

发表于 2022-06-05

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


前言

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

  • 【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.关于 Compoent 树

如下图场景,每个显示的物体都是 Component ,它们形成一个树形结构。代码见 【11/01】

各种角色通过 add 方法添加到树中,此时的树型结构如下:

现在有个问题:因为血条和血量是被加入到 Adventurer 构件中的,所以 Adventurer 的变换行为也会引起血条的变换。如下角色沿 Y 轴镜像,可以看到血条及文字也发生了镜像,这并不是我们所期望的。

那如何解决呢?思路很简单,既然 Adventurer 有单独镜像的需求,那就不能是血条的父级。将两者从父子关系变为兄弟关系即可,这里将血条封装为 LifeComponent 构建,和 Adventurer 一起存在于 HeroComponent 中:


2.角色移动中的镜像反转

现在想实现如下效果:如果触点在角色左侧,角色会镜像反转到左侧,反之,镜像反转到右侧。这样的目的是为了角色可以选择攻击的方向,比如面向左侧攻击左侧怪物:代码见 【11/02】


因为这里只有左右反转,在 HeroComponent 中定义一个 isLeft 的 bool 值用于记录状态。如果需要支持其他方向,比如上、下、左上、右下等,可以通过枚举来维护。

1
2
3
4
dart复制代码---->[HeroComponent]----
bool isLeft = true;
late Adventurer adventurer;
late LifeComponent lifeComponent;

在点击屏幕时,触发 toTarget 方法,在开始可以通过 _checkFlip 方法来对 isLeft 属性进行维护,已经在需要反转是通过 flip 反转角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dart复制代码---->[HeroComponent#toTarget]----
void toTarget(Vector2 target) {
_checkFlip(target);
// 略同...
}

void _checkFlip(Vector2 target){
if (target.x < position.x) {
if (isLeft) {
flip();
isLeft = false;
}
}
if (target.x > position.x) {
if (!isLeft) {
flip();
isLeft = true;
}
}
}

用于只想要让主角反转,所以在 flip 中,执行 adventurer.flip 即可。这样就不会影响血条的显示:

1
2
3
4
5
6
dart复制代码void flip({
bool x = false,
bool y = true,
}) {
adventurer.flip(x: x, y: y);
}

1
2
3
4
5
6
7
dart复制代码---->[HeroComponent#flip]----
void flip({
bool x = false,
bool y = true,
}) {
adventurer.flip(x: x, y: y);
}

另外关于反转,还需要注意子弹的发射方向。因为前面的子弹总是向右侧发射的,如果面朝左侧,应该向左运动,如下所示:

处理起来也比较简单,根据 isLeft 确实向左还是向右发射即可,如下tag1

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码---->[Bullet]----
@override
void update(double dt) {
super.update(dt);
Vector2 ds = Vector2(isLeft ? 1 : -1, 0) * speed * dt; // tag1
_length += ds.length;
position.add(ds);
if (_length > maxRange) {
_length = 0;
removeFromParent();
}
}

3. 关于属性的维护

前面为了方便演示,对于角色的属性,写的比较零散,比如速度、攻击力等。在这里既然可以封装了 HeroComponent 来维护主角类。就可以定义一个 HeroAttr 类来维护主角的属性,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dart复制代码class HeroAttr {
double life; // 生命值
double speed; // 速度
double attackSpeed; // 攻击速度
double attackRange; // 射程
double attack; // 攻击力
double crit; // 暴击率
double critDamage; // 暴击伤害

HeroAttr({
required this.life,
required this.speed,
required this.attackSpeed,
required this.attackRange,
required this.attack,
required this.crit,
required this.critDamage,
});
}

这样在构建 HeroComponent 时,传入 HeroAttr 对象来确定该对象的属性信息。

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码---->[TolyGame#onLoad]----
final HeroAttr heroAttr = HeroAttr(
life: 3000,
speed: 100,
attackSpeed: 200,
attackRange: 200,
attack: 50,
crit: 0.75,
critDamage: 1.5,
);
player = HeroComponent(attr: heroAttr);
add(player);

这样在怪物损失生命值,可以根据 HeroAttr 的属性进行计算:

1
2
3
4
5
6
7
8
9
10
11
dart复制代码---->[Liveable]----
void loss(HeroAttr attr) {
double point = attr.attack;
double crit = attr.crit;
double critDamage = attr.critDamage;
bool isCrit = _random.nextDouble() < crit;
if (isCrit) {
point = point * critDamage;
}
_damageText.addDamage(-point.toInt(), isCrit: isCrit);
}

添加子弹时,可以根据 HeroAttr 的属性信息确定攻速和射程:


本篇,我们继续拓展了角色的功能,知道父级构件的变换会影响子级组件,所以在使用构件时需要注意构件间的关系。另外通过 HeroAttr 封装了角色信息,这样通过 HeroComponent 就可以添加多个主角节点,就可以双人模式打怪。

到这里,可以看到 TolyGame 中非常乱,下一章我来介绍一下,如何对多个角色和怪物进行管理,包括怪物的生成、发射子弹、命中主角等。那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

【Flutter&Flame游戏 - 拾】探索构件 Co

发表于 2022-06-04

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


前言

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

  • 【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. Component 生命周期回调一览

所谓生命周期,就是一个对象从生到死的过程。在上一篇中介绍过 Component 的生命周期状态 (LifecycleState) 有如下六种。可能很多人分不清什么是生命周期,什么是生命周期回调。

生命周期,本质上是一种 状态 ,也就是说它是一种数据;而生命周期回调是一个函数,或说方法,一般来说该函数会在状态切换时触发,从而让外界可以感知到对象的状态变化,以此实现某些特定的逻辑。 Component 中的生命周期回调方法如下:

一般来说,常用的是如下六个回调,先简单认识一下:

  • onGameResize : 顶层画布尺寸变化时
  • onLoad:资源加载时
  • onMount:添加到父节点时
  • onRemove:从父节点移除时
  • update:跟随 Ticker 不断触发
  • render:新帧渲染时触发

2. onGameResize 和 onLoad

如下可以看出,在生命周期状态从 uninitialized 切换到 loading 时,会触发一次 onGameResize;紧接着触发 onLoad 异步方法。在 483 行所示,异步任完成后,生命周期状态将置为 loaded。


如下通过断点查看一下自定义的 Ball 组件 onLoad 方法触发时,方法栈的情况。可见在 Flutter 程序的开始,BuildOwner#buildScope 构建组件时, _GameWidgetState 会触发 loaderFuture 的方法。在父构件执行 add 方法,会先触发该子构建的 onLoad 方法来加载资源。可就是说,通过这个回调,可以给构件准备资源的机会。


3. onMount 和 onRemove

这两个是一对反义词,onMount 方法在生命周期状态变为 mounted 之前触发。让使用者知道该构件节点添加到构件树的确切时机。


当某个组件被父节点踢出群聊时,会触发onRemove 方法,之后紧接着将生命周期状态置为 removed 。让使用者知道该构件节点添加到构件树的确切时机。


4. update 和 render

前面我们对这两个方法已经有所了解,这两者都是一个持续不断的回调,一般每隔 16.66ms 触发一次,也就是一秒钟触发 60 次 。update 方法本质上由 Ticker 触发,这点可以通过断点调试进行应证,如下所示:


而 render 方法本质上是在帧绘制期间被触发的,也就是 RendererBinding.drawFrame 方法。这个看过 《Flutter 渲染机制 - 聚沙成塔》 的朋友对这些应该比较熟悉,没看过也没有关系。 Ticker 触发新帧的申请,回调 update 方法,在新帧来临是触发 drawFrame 方法,回调 render 方法,所以这两者的先后关系是很明确的。


如下是着六个回调方法顺序的简单示意,其中 update 和 render 方法是在 Ticker 循环中不断触发的,当 Ticker 停止时,这两个方法也会停止回调。另外当该组件被移除之后,也不会继续回调update 和 render 。


5. 运动圆

下面通过一个小案例来梳理一下 Component 的生命周期回调。如下,小圆不停运动,在碰到桌面后反弹,代码详见 【10/01】


在 onLoad 方法中,可以对画笔、位置、速度、加速度等属性进行初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
dart复制代码final Paint _paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1;

Vector2 v = Vector2.zero(); // 速度 px/s
Vector2 a = Vector2.zero(); // 加速度 px/s^2

@override
Future<void> onLoad() async {
_paint.color = color;
position = gameRef.size / 2;
v = Vector2(80, 50);
}

在 render 方法中进行绘制圆:

1
2
3
4
5
6
dart复制代码@override
void render(Canvas canvas) {
super.render(canvas);
canvas.translate(size.x / 2, size.y / 2);
canvas.drawCircle(Offset.zero, size.x / 2, _paint);
}


在 update 中,根据运动学格式,在 dt 的时间内,更新速度和位移的值,小球即可运动。另外小球的碰壁反弹可以通过位置校验来处理 ,Flame 中有对于碰撞的简单封装,但这里还是自己手动校验,体会一下简单的配置检测。

速度的合成.png

碰撞分析png

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
dart复制代码@override
void update(double dt) {
super.update(dt);
v += a * dt;
position += v * dt;
Vector2 winSize = gameRef.size;
//限定下边界
if (position.y > winSize.y - size.y/2) {
position.y = winSize.y - size.y/2;
v.y = -v.y;
}
//限定上边界
if (position.y < size.y/2) {
position.y = size.y/2;
v.y = -v.y;
}
//限定左边界
if (position.x < size.x/2) {
position.x = size.x/2;
v.x = -v.x;
}
//限定右边界
if (position.x > winSize.x - size.x/2) {
position.x = winSize.x - size.x/2;
v.x = -v.x;
}
}

下面可以继续拓展,比如在点击屏幕时添加通过 Ball ,双击屏幕时移除 Ball 列表的第一个。效果如下,代码详见 【10/02】

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
dart复制代码class TolyGame extends FlameGame with TapDetector,DoubleTapDetector{

int _counter = 0;

@override
Future<void> onLoad() async {
addABall();
}

void addABall(){
Ball ball = Ball(tag: 'tag$_counter');
add(ball);
_counter++;
}

@override
void onTap() {
addABall();
}

@override
void onDoubleTap() {
List<Ball> balls = children.whereType<Ball>().toList();
if(balls.isNotEmpty){
balls.first.removeFromParent();
}
}
}

这样在移除时 Ball 自身可以通过 onRemove 监听到事件:


到这里,我们就对 Flame 中 Component 的生命周期回调有了较深的理解。这个知识点是非常重要的,希望大家可以好好消化吸收。那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

【Flutter&Flame游戏 - 玖】探索构件 Co

发表于 2022-06-03

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


前言

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

  • 【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. Component 的树形结构

通过前面八篇的尝鲜,或说预热,我们可以感知到无论是主角、怪兽、文字、子弹、触点都是 Component 。它是游戏的基本构建模块,可以表示任何需要被渲染、更新的内容。


下面是 Component 类的部分结构,可以看出 Component 是一个普通类。其本身会持有父级构件,以及子级构件集合。也就是说 Component 本身是一个树形结构的节点类,认识到这一点至关重要。


正是由于树形结构的特点,Component 类有添加和 移除 构件的能力。如下所示,可以通过 add 方法添加子级构件,也可以通过 addToParent 方法,将自身添加到父级构件中。


前面介绍过子弹、怪物消失,使用的是 removeFromParent 方法。如下源码中可以看出,是调用父级构件 _parent 的移除方法,把当前构件对象从父级节点上移除:

1
2
3
4
5
dart复制代码---->[Component#removeFromParent]----
/// Remove the component from its parent in the next tick.
void removeFromParent() {
_parent?.remove(this);
}

2. Component 生命周期状态

Component 中有一个 _state 属性,其类型为 LifecycleState 枚举,用于表示构件的状态:

其中有如下 6 种状态,初始状态是 uninitialized ,表示未初始化,也就是构件实例化时的默认状态。前面知道构件中有个 onLoad 的异步方法用于加载资源,在执行异步方法的前一刻就是 loading 状态。该状态会持续到异步方法执行完毕,变成 loaded 状态。

1
2
3
4
5
6
7
8
dart复制代码enum LifecycleState {
uninitialized,
loading,
loaded,
mounted,
removing,
removed,
}

Component 是树形结构的节点,当某个 Component 添加到父节点上后,就会变成 mounted 状态。相关代码如下所示:


另外当父级执行 remove 方法时,入参的子构件非 removing 状态时,会被加入到 lifecycle._removals 列表中,等待下帧触发时移除。此时该子构件的状态为 removing 。当构件被从父节点上移除后,其状态为 removed ,就变成了孤魂野鬼,等待被 GC 回收。

如下图是六种状态的转换示意图,其实还是比较清晰的。了解这六种状态,在下篇介绍 Component 生命周期方法时,就会更好理解。


另外 Component 中关于生命周期状态有三个 get 方法,这里介绍一下:

  • isLoaded:非 uninitialized 且非 loading 状态,表示异步加载任务是否已经完成。
  • isMounted: mounted 或 removing 状态,表示构件依然在树上。
  • shouldRemove: removing 状态,表示构件已被收集到移除列表中,将在下一帧中被移除。
1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码---->[Component]----
bool get isLoaded {
return (_state != LifecycleState.uninitialized) &&
(_state != LifecycleState.loading);
}

bool get isMounted {
return (_state == LifecycleState.mounted) ||
(_state == LifecycleState.removing);
}

bool get shouldRemove => _state == LifecycleState.removing;

3. Component 的衍生类

在 Flame 的 components 包中的文件,是对 Component 的衍生。其中一些 mixin ,比如 Draggable 、Hoverable 、Tappable 等都是依赖于 Component ,情理上来说也算是 Component 的衍生产物。


Component 大致可分为三大类,支持定位和变换的 PositionComponent 、附加效果的 Effect 、以及直接继承自 Component 的少数构建。

其中群体最庞大的是 PositionComponent ,这一族引入了 尺寸 、锚点、位置 、旋转 、缩放 等属性,决定了该族构件将非常实用:

我们之前用的 SpriteComponent 、TextComponent 、SpriteAnimationComponent 等都是 PositionComponent 一族的。另外,自定义的子弹、主角、怪兽,也都是 PositionComponent。


另外,Effect 一族定义在 effects 包中,我们在前面用到的 MoveEffect 就是这类的构件。在之后的学习中我们再深入认识其他的效果,或者自定义 Effects 。


4. 自定义 Component

前面的案例中我们也尝试过自定义 Component ,比如 Adventurer 、Monster 、TouchIndicator 等。其实自定义 Component 和 Flutter 中自定义 Widget 的功效类似,都是为了把一些通用的构成逻辑进行封装,以便复用和管理。

比如通过下面的 Monster 类,可以生成多个怪兽对象:可以定义不同的帧序列和生命值,它们对于玩家来说就是两个不同的怪兽。对于编程者而言它们都是通过 Monster 构建类实例化的对象,本质没有什么区别。代码详见 【09/01】

2022年06月02日15-17-12.gif

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
dart复制代码class Monster extends SpriteAnimationComponent with Liveable {
final double life;
Monster({
required SpriteAnimation animation,
required Vector2 size,
required Vector2 position,
this.life = 4000,
}) : super(
animation: animation,
size: size,
position: position,
anchor: Anchor.center,
);

@override
void onDied() {
removeFromParent();
}

@override
Future<void> onLoad() async {
initPaint(lifePoint: life);
}

void move(Vector2 ds) {
position.add(ds);
}
}

另外,通过自定义构件类,可以覆写 Component 的相关回调方法,监听相关状态,处理逻辑。这里先对 Component 认识到这里,下一章我们将信息探讨一下 Component 的生命周期回调。那时你就会对 Component 有一个更深的认知,那么本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

【Flutter&Flame游戏 - 捌】装弹完毕 角色

发表于 2022-06-02

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


前言

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

  • 【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. 本文目标

今天来看一下角色如何发射子弹,这里把 子弹 作为 发射物 的统称。少数人不要杠,明明是弓箭,非说是子弹。关于子弹,有些注意点,首先它是基于某个角色进行产出的;其次,它会被频繁创建和销毁。它被销毁的时机包括:命中物体时,移出屏幕,或者超出射程,又或者固定在诞生几秒后自动移除等。

这里使用射程来对子弹进行移除,对水平发射而言,射程就是子弹在水平方向上的偏移距离,如下图蓝框所示区域:


2. 主动触发帧动画

前面我们的弓手是不断循环的帧动画,现在来先看一下如何主动触发:比如下面案例中,按下键盘的 J 键就执行一次动画,代码详见 【08/01】


在 Adventurer 的 onLoad 方法中,指定 playing 为 false 可以在开始不会执行帧动画。将 loop 置为 false ,帧就不会重复执行;通过 animation 的 onComplete 回调方法,可以监听到帧动画结束的时机。这里当结束时,触发 _onLastFrame ,置为第一帧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码---->[08/01/Adventurer$onLoad]----
playing = false;
animation = SpriteAnimation.spriteList(
sprites,
stepTime: 0.15,
loop: false,
);
animation!.onComplete = _onLastFrame;

---->[08/01/Adventurer$_onLastFrame]----
void _onLastFrame() {
animation!.currentIndex = 0;
animation!.update(0);
}

那如何让执行帧动画呢,很简单:将 playing 置为 true ,然后触发 animation 的 reset 方法即可。如下通过 shoot 方法完成,只要在监听 J 按键,触发 shoot 即方法可。

1
2
3
4
dart复制代码void shoot() {
playing = true;
animation!.reset();
}

3. 子弹的发射

如下,定义 Bullet 构建来表述子弹角色,在构造时指定图片 sprite 和最大射程 maxRange 。子弹在诞生之后,就会一直处于运动状态,可以覆写 update 方法,根据时间和速度计算偏移量。如下 tag1 处所示:当偏移总量大于 maxRange 时,进行移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dart复制代码class Bullet extends SpriteComponent {
double _speed = 200;
final double maxRange;

Bullet({required Sprite sprite, required this.maxRange})
: super(sprite: sprite);

double _length = 0;

@override
void update(double dt) {
super.update(dt);
Vector2 ds = Vector2(1, 0) * _speed * dt;
_length += ds.length;
position.add(ds);
if (_length > maxRange) { // tag1
_length = 0;
removeFromParent();
}
}
}

接下来只要在 Adventurer 动画序列完成后,也就是 _onLastFrame 回调方法中添加子弹即可。这里有两个知识点,其一 priority 可以确定构件的优先级,默认情况下,后被添加的的显示在上层。这里要让子弹在角色下方,把角色优先级高于子弹即可。

第二点是:这里使用 gameRef 添加子弹,而添加入 Adventurer 自身中。因为如果添加到 Adventurer ,其作为子构件,会伴随 Adventurer 移动,这并不符合尝试。比如你扔个石头,离手后它不会随着你的移动而移动。代码详见:【08/02】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dart复制代码---->[08/02/Adventurer]----
late Sprite bulletSprite;

---->[onload]----
bulletSprite = await gameRef.loadSprite('adventurer/weapon_arrow.png');

void _onLastFrame() async{
animation!.currentIndex = 0;
animation!.update(0);

// 添加子弹
Bullet bullet = Bullet(sprite: bulletSprite,maxRange: 200);
bullet.size = Vector2(32, 32);
bullet.anchor = Anchor.center;
bullet.priority = 1;
priority = 2;
bullet.position = position-Vector2(0,-3);
gameRef.add(bullet);
}

4. 命中处理 - 极简版

如下图所示,接下来把前几篇的知识串联一下:综合角色移动、子弹发射、怪兽受伤害,做个小场景。其中弓箭和怪物的碰撞检测,使用最精简的方式:矩形区域。代码详见:【08/03】

这种校验的思路是:在每帧触发 update 时,校验怪物的矩形区域是否包含某点。比如说,当弓箭的中心在怪物的矩形域中,就表示命中。代码处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dart复制代码@override
void update(double dt){
super.update(dt);
final Iterable<Bullet> bullets = children.whereType<Bullet>();
for(Bullet bullet in bullets){
if(bullet.shouldRemove){
continue;
}
if(monster.containsPoint(bullet.absoluteCenter)){ // tag1
bullet.removeFromParent();
monster.loss(50);
break;
}
}
}

其中上面tag1 处的 absoluteCenter 代表构件中心的绝对坐标,如下以该点为圆心画了一个小圆示意:


另外,大家可以基于此自己尝试实现怪兽不断发射子弹,攻击主角的功能。经历了这八篇的研究,完成了一个小的交互,也借此简单认识了一下 Flame 框架的使用。到现在算是个尝鲜,还有一些比较重要的基础概念还没涉及:比如 Component 的生命周期、各种 Effect 效果、相机操作、高级的碰撞检测等。在后续会逐步介绍,那本文就到这里,明天见 ~

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

\

本文转载自: 掘金

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

Android 强推的 Baseline Profiles

发表于 2022-06-01

往期文章:

《00. 文章合集目录》

《09. 图解协程原理》

《10. 揭秘 Compose 原理》

你好,我是朱涛。这是「沉思录」的第三篇文章。

今天我们来扒一下 Baseline Profiles 的底层原理。

Baseline.gif

正文

今年 Google I/O 大会上,Android 官方强推了一把 Baseline Profile,不仅在 Android、Jetpack 的主题演讲里有提到了它,就连 Jetpack Compose、Android Studio 相关的主题里也有它的身影。

第一眼,我就被它给惊艳到了!动辄 30%、40% 的启动优化成绩,还是一个通用的解决方案,真的很牛逼了!而且 App 越复杂,提升明显!说实话,刚开始我甚至有点不太相信。

国内能用吗?

在官方介绍 Baseline Profile 的时候,放了一张这样的图,貌似 Google Play Service 在中间扮演着重要的角色。

Google Play??我心里顿时就凉了半截。完了!这么牛逼的东西,国内不能用吗? 吓得我赶紧找来了文档,仔细看了一遍 Baseline Profile 的用法以及原理,这才放下心来:

国内能用 Baseline Profiles,只是 Cloud Profiles 不可用而已。

为了保险起见,我也在 Twitter 上找了 Google 工程师,对方也证实了我的想法。

那就没毛病了!学起来!

底层原理

其实吧,Baseline Profile 并不是一个新的东西。而且它也不是一个 Jetpack Library,它只是存在于 Android 系统的一个文件。

这里,我们要从 Android 系统的发展说起。

  • 对于 Android 5.0、6.0 系统来说,我们的代码会在安装期间进行全量 AOT 编译。虽然 AOT 的性能更高,但它会带来额外的问题:应用安装时间大大增加、磁盘占用更加大。
  • 对于 Android 7.0+ 系统来说,Android 支持 JIT、AOT 并存的混合编译模式。在这些高版本的系统当中,ART 虚拟机会在运行时统计到应用的热点代码,存放在/data/misc/profiles/cur/0/包名/primary.prof这个路径下。ART 虚拟机会针对这些热点代码进行 AOT 编译,这种方式要比全量 AOT 编译灵活很多。

看到这里,你是不是已经猜到了 Baseline Profile 的底层原理了呢?

不难发现,对吧?由于 ART 虚拟机需要执行一段时间以后,才能统计出热点代码,而且由于每个用户的使用场景、时长不一样,最终统计出来的热点代码也不一定是最优的。

Google 的思路其实也很简单:让开发者把热点代码提前统计好,然后跟着代码一起打到 APK 当中,然后将对应的规则存到/data/misc/profiles/cur/0/这个目录下即可。总的来说,就是分成两步:1. 统计热点代码的规则;2. 将规则存到特定目录下。

统计热点代码

Baseline Profile 其实就是一个文件,它里面会记录我们应用的热点代码,最终被放在 APK 的 assets/dexopt/baseline.prof 目录下。有了它,ART 虚拟机就可以进行相应的 AOT 编译了。

虽然,我们也可以往 Baseline Profile 当中手动添加对应的方法,但 Google 更加推荐我们使用 Jetpack 当中的 Macrobenchmark。它是 Android 里的一个性能优化库,借助这个库,我们可以:生成Baseline Profile文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码@ExperimentalBaselineProfilesApi
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule()

@Test
fun startup() =
baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") {
pressHome()
// This block defines the app's critical user journey. Here we are interested in
// optimizing for app startup. But you can also navigate and scroll
// through your most important UI.
startActivityAndWait()
}
}

唯一需要注意的,就是我们需要在 root 过后的 AOSP 9.0+ 的系统上才能采集到热点代码的信息。最终,Macrobenchmark 会把统计到的热点代码信息放到文件里。

1
bash复制代码/storage/emulated/0/Android/media/package.name.SampleStartupBenchmark_startup-baseline-prof.txt

我们拿到这个统计的文件,将其重命名为baseline-prof.txt,放到工程里去即可。

写入 baseline.prof

经过前面的分析,我们知道,baseline.prof 需要写入到系统特定的目录下,才能够引导 AOT 编译。这一点又是如何做到的呢?

这时候,我们需要用到另一个 Jetpack Library:ProfileInstaller。从它的名字,我们就能看出,它的功能就是:将 APK 当中的 baseline.prof 写入到系统目录下。

它的用法也很简单:

1
groovy复制代码implementation "androidx.profileinstaller:profileinstaller:1.2.0-beta02"

引入依赖,这没什么好说的,常规操作。然后就是初始化设置。

1
2
3
4
5
6
7
8
9
xml复制代码
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data android:name="androidx.profileinstaller.ProfileInstallerInitializer"
tools:node="remove" />
</provider>

可以看到,它是通过集成 androidx.startup 库,实现的初始化,用的是 Content Provider 的思路,也是常规操作了。我们来分析一下源代码吧!

总的来说,ProfileInstaller 的代码结构很简单:

通过前面 XML 的分析,我们知道,ProfileInstallerInitializer 肯定是功能的入口,我们来看它的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class ProfileInstallerInitializer
implements Initializer<ProfileInstallerInitializer.Result> {
private static final int DELAY_MS = 5_000;

@NonNull
@Override
public Result create(@NonNull Context context) {
if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
// 小于 7.0 的系统没必要执行
return new Result();
}
// 延迟 5 秒,写入 profile 文件
delayAfterFirstFrame(context.getApplicationContext());
return new Result();
}
}
}

接着,我们来看看 Delay 是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RequiresApi(16)
void delayAfterFirstFrame(@NonNull Context appContext) {
// 从第一帧开始算,延迟 5 秒
Choreographer16Impl.postFrameCallback(() -> installAfterDelay(appContext));
}

void installAfterDelay(@NonNull Context appContext) {
Handler handler;
if (Build.VERSION.SDK_INT >= 28) {
handler = Handler28Impl.createAsync(Looper.getMainLooper());
} else {
handler = new Handler(Looper.getMainLooper());
}
Random random = new Random();
int extra = random.nextInt(Math.max(DELAY_MS / 5, 1));
// Handler 实现 delay
handler.postDelayed(() -> writeInBackground(appContext), DELAY_MS + extra);
}

可以看到,为了避免 Profile 的写入影响到 App 的正常执行,这里延迟了 5 秒左右。最终,会执行writeInBackground(),进行真正的写入操作。

1
2
3
4
5
6
7
8
9
10
java复制代码private static void writeInBackground(@NonNull Context context) {
Executor executor = new ThreadPoolExecutor(
/* corePoolSize = */0,
/* maximumPoolSize = */1,
/* keepAliveTime = */0,
/* unit = */TimeUnit.MILLISECONDS,
/* workQueue = */new LinkedBlockingQueue<>()
);
executor.execute(() -> ProfileInstaller.writeProfile(context));
}

这里,程序会创建一个线程数量为 1 的线程池,然后将执行流程交给 ProfileInstaller,进行 Profile 文件的写入。

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
java复制代码static void writeProfile(
@NonNull Context context,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics,
boolean forceWriteProfile
) {
Context appContext = context.getApplicationContext();
String packageName = appContext.getPackageName();
ApplicationInfo appInfo = appContext.getApplicationInfo();
AssetManager assetManager = appContext.getAssets();
String apkName = new File(appInfo.sourceDir).getName();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
return;
}
File filesDir = context.getFilesDir();
// 判断是否要写入
if (forceWriteProfile
|| !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
transcodeAndWrite(assetManager, packageName, packageInfo, filesDir, apkName, executor,
diagnostics);
}
}

writeProfile()的主要逻辑就是判断当前是否要强制写入 Profile 文件(正常情况是不强制的),以及之前是否已经写入过了。之后,程序会执行transcodeAndWrite()方法,也就是转码并写入。

终于到关键逻辑了!我们来看看它的逻辑。

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
java复制代码private static void transcodeAndWrite(
@NonNull AssetManager assets,
@NonNull String packageName,
@NonNull PackageInfo packageInfo,
@NonNull File filesDir,
@NonNull String apkName,
@NonNull Executor executor,
@NonNull DiagnosticsCallback diagnostics
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
return;
}
File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);

DeviceProfileWriter deviceProfileWriter = new DeviceProfileWriter(assets, executor,
diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);

// 是否具备写入权限
if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
return; /* nothing else to do here */
}

boolean success = deviceProfileWriter.read()
.transcodeIfNeeded()
.write();

if (success) {
noteProfileWrittenFor(packageInfo, filesDir);
}
}

public boolean deviceAllowsProfileInstallerAotWrites() {
if (mDesiredVersion == null) {
result(ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, Build.VERSION.SDK_INT);
return false;
}

if (!mCurProfile.canWrite()) {
// 某些厂商可能不允许写入 Profile 文件
result(ProfileInstaller.RESULT_NOT_WRITABLE, null);
return false;
}

mDeviceSupportsAotProfile = true;
return true;
}

从上面的注释,我们可以看到,transcodeAndWrite()主要还是在判断当前设备是否支持写入 Profile 文件,如果支持才会继续。

至此,我们整个 Baseline Profile 的技术方案就分析完了!

注意事项

在研究 Baseline Profiles 的过程中,我也发现了一些小细节,可能需要大家额外留意。

  • 第一,由于 Android 手机有许多的厂商,每个厂商会对系统进行一些定制化,也许某些厂商会封死 Profile 文件的写入权限。即使这个方案无需 Google Play,但国内支持写入 Profile 的手机具体占多大的比例,我目前还没有数据,欢迎大家在使用了 Baseline Profile 以后来向我反馈。
  • 第二,如何衡量 Baseline Profile 带来的性能提升?这一点, Macrobenchmark 也提供了相关的能力,具体可以看这个官方文档的链接。
  • 第三,Debug 编译的 App,是不会进行 AOT 编译的,因此它的性能会比 release 低不少。
  • 第四,baseline-prof.txt放的位置很关键,它必须跟AndroidManifest.xml是同级目录下。
  • 第五,Baseline Profile 必须使用 AGP 7.1.0-alpha05 及以上的版本,7.3.0-beta01及以上对 App Bundle、多 Dex 应用的支持会更好。
  • 第六,baseline-prof.txt 文件大小不得超过 1.5M,且,其中定义的规则不能太宽泛,否则可能反而降低性能。

一个有趣的故事

这个故事具体的来源是谁,我忘了,反正是某个 Google 工程师说的。关于,Baseline Profile 是如何诞生的。

其实,它跟 Jetpack Compose 还有一些渊源。Compose 由于它的底层原理,它的核心代码是会频率调用的,因此对性能要求非常高。

在 Google 内部研发 Jetpack Compose 的过程中,他们发现:Compose 应用在初次安装、启动的阶段,会非常的卡!等到应用使用一段时间后,Compose 应用的体验才会慢慢好起来。

这是为什么呢?

你肯定能猜到,对吧?没错!因为 ART 默认情况下,并没有把 Compose 的核心代码进行 AOT 编译,而是 JIT 执行。这就要命了,像 Compose 底层的 Snapshot 系统、Slot Table,都是热点代码,短时间内会被频繁调用,JIT 根本无法满足 Compose 的性能要求。

怎么办呢?当然是 Baseline Profile 啦!其实,这套方案,早在 2021 年就被率先引入 Jetpack Compose 当中。今年 2022 年的 Google I/O 大会上,才被官方拿出来大力推广。

感谢 Android 团队,让我们开发者拥有了一个新的角度,来优化应用的性能。

OK,感谢你的阅读,咱们下周……额……我也不知道啥时候能写出下一篇,总之,下次再见!

本文转载自: 掘金

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

【Flutter&Flame 游戏 - 柒】人随指动 动

发表于 2022-06-01

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


前言

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

  • 【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. 本文目标

之前的主角是通过键盘来控制移动,但移动设备中一般不通过键盘操作,而是点击操作。比如下面的人物在点击时,会动画移动到点击的位置,这就涉及到构件的动画移动。

另外还有个小细节,就是点击时触点的动画,其特点是在点击后显示,一会便自动消失。本文的目标就是实现这两个小功能。


2. 点触动画

这里点击时一闪的小星星,本质上是一个序列动画,如下是序列图片:

我们在 【第一篇】 就介绍了通过 SpriteAnimationComponent 构件对序列帧进行动画播放,但当时并没有细说。 类型维护的 SpriteAnimation 对象是由 Sprite 列表构成的列表,本质上就是在 update 方法中,根据时长来不断更新显示的帧索引而已。

这里把触点指示器封装成一个构件 TouchIndicator ,由外界提供 position 确定位置。在 onLoad 回调中,加载序列帧图片形成 SpriteAnimation 。注意一点,默认情况下序列帧动画是在不断运行的,可以指定 loop: false 设置播放一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dart复制代码class TouchIndicator extends SpriteAnimationComponent {
TouchIndicator({required Vector2 position})
: super(
size: Vector2(30, 30),
anchor: Anchor.center,
position: position,
);

@override
Future<void> onLoad() async {
List<Sprite> sprites = [];
for(int i=1;i<=10;i++){
sprites.add(await Sprite.load('touch/star_${'$i'.padLeft(2,'0')}.png'));
}
removeOnFinish = true;
animation = SpriteAnimation.spriteList(sprites, stepTime: 1/15,loop: false);
}
}

另外 SpriteAnimationComponent 内部维护了 removeOnFinish 成员,用于表示是否在序列帧播放完毕时移除当前构件,这里设置为 true 即可实现如下效果:代码见 【07/01】

TolyGame 中,只需要监听 onPanDown 事件,添加 TouchIndicator 构件即可。

1
2
3
4
5
6
dart复制代码class TolyGame extends FlameGame with TapDetector, PanDetector {
@override
void onPanDown(DragDownInfo info) {
add(TouchIndicator(position: info.eventPosition.global));
}
}

3. 移动点触

点击显示 TouchIndicator 完成了,其实移动也就非常简单,覆写 onPanUpdate 方法,在其中添加 TouchIndicator 即可。但这里有个问题,onPanUpdate 更新的频率是非常快的,直接添加就会是如下效果,密密麻麻的一坨。这显然并非我们期望的:

1
2
3
4
dart复制代码@override
void onPanUpdate(DragUpdateInfo info) {
add(TouchIndicator(position: info.eventPosition.global));
}

这个问题的解决方式其实很简单:只要在更新期间,校验一下偏移量是否大于某个值,再添加即可。效果如下,当间隔的位移长度大于 10 时,才添加 TouchIndicator :

1
2
3
4
5
6
7
8
9
10
dart复制代码double ds = 0;

@override
void onPanUpdate(DragUpdateInfo info) {
ds += info.delta.global.length;
if (ds > 10) {
add(TouchIndicator(position: info.eventPosition.global));
ds = 0;
}
}

4.构建的的移动效果: MoveEffect

需要让角色动画移动到某个点,可以添加 MoveEffect 构件。如下,在 Adventurer 类中定义toTarget 方法,使用 MoveEffect.to 创建效果对象并进行添加。代码详见:【07/02】

其中 EffectController 可以指定很多动画的参数,比如重复次数、动画曲线、动画时长等。后面会就各种 Effect 进行专文介绍,这里暂时不深入,简单使用以下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dart复制代码---->[07/02/Adventurer]----
final double _speed = 100;

void toTarget(Vector2 target) {
double timeMs = (target-position).length/_speed;
add(
MoveEffect.to(
target,
EffectController(
duration: timeMs,
),
),
);
}

然后在 TolyGame 中,点击屏幕时,执行 player.toTarget 方法。角色就会从当前位置,动画移动到指定的 target 位置。

1
2
3
4
5
6
dart复制代码@override
void onPanDown(DragDownInfo info) {
Vector2 target = info.eventPosition.global;
add(TouchIndicator(position: target));
player.toTarget(target);
}

4. Effect 效果的移除

上面的处理会出现一个问题,如下图所示:当前一次移动动画没有结束前,点一下其他位置,由于两个动画效果同时作用在构建上,所以无法正常完成移动到某点的任务。


解决的思路是:当点击时,应该要移除之前的 MoveEffect ,避免对接下来的移动效果产生影响。每个 Component 中都有 children 成员,表示子构件列表;通过 whereType 可以获取指定类型的子构件列表;然后使用 removeAll 将其移除即可。这样点哪里,角色就是动画运动到哪里。

1
2
3
4
5
6
7
8
9
10
11
12
dart复制代码void toTarget(Vector2 target) {
removeAll(children.whereType<MoveEffect>());
double timeMs = (target-position).length/_speed;
add(
MoveEffect.to(
target,
EffectController(
duration: timeMs,
),
),
);
}

本文主要介绍了如何让帧动画在播放完后自动移除,以此实现点触时和移动时的闪光动画。接着介绍了使用 MoveEffect 构件完成动画移动的效果。这两者结合起来,就完成了对角色通过触点来控制移动的需求。那本文就到这里,明天见 ~

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

本文转载自: 掘金

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

1…939495…956

开发者博客

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