前言
这是一套 张风捷特烈 出品的 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 游戏 - 贰贰】菜单、字体和浮层
未完待续 ~
1. 他乡遇故知 - Canvas
小册 《Flutter 绘制指南 - 妙笔生花》可以说是专门为 Canvas
绘制而生的。其实游戏的本质就是不断刷新中的绘制,在 Flame
引擎中,也暴露了渲染方法,给使用者自定义绘制的机会。这就说明我们在之前累积的绘制技巧,也可以在 Flame
中得以应用。
如上图说示,render
方法定义在 Component
类中,所以说任何构件
都可以覆写它,来获取 Canvas
构建。另外可以看到 Component
本身有一些生命周期方法,我们之前已经见识过了 onLoad
、update
、onGameResize
方法。关于 Component
的生命周期,现在先不着急,后面会专门写一篇来说。
2.简单画一笔
如下在 Monster
类中覆写 render
方法,通过 Canvas
对象绘制一个白圈 。可以看出这里画布的原点在构件的左上角:代码见 【05/01】
---->[component/Monster]----
@override
void render(Canvas canvas){
super.render(canvas);
canvas.drawCircle(Offset.zero, 10, Paint()..color=Color(0xffffffff));
}
其中注意一点:super.render
代表父类执行渲染方法。未覆写render
方法时, Monster
的绘制会默认触发父级的。如果你覆写了 render
方法,则会走 Monster
中的,如果不调用 super.render
,那么父级的绘制将不会生效,也就是说怪兽是没有被渲染的,这是面向对象的最基本知识。
另外绘制也是 后者居上
,也就是说出现重叠时,后绘制的图案会盖住先绘制的图案,如下所示:
3. 绘制血条
既然怪兽已经出现了,血条自然不能少。如下,在 Monster
类中简单画个白框红血的条:代码见 【05/02】
下面是绘制的简单逻辑,其中主要逻辑的是计算外框和血条的两个 Rect
矩形对象。外框的白条矩形通过中心点加宽高来确定的,因为这里希望血条居中,且可以可以通过比率 widthRadio
控制长度。在白条矩形确定之后,左下角的点就能确定,此时通过两点确定矩形会比较方便。如何选用最简单的方式来确定图形信息,是绘制的一个小细节。
Paint _outlinePaint = Paint();
Paint _fillPaint = Paint();
@override
void render(Canvas canvas) {
super.render(canvas);
Color lifeColor = Colors.red; // 血条颜色
Color outlineColor = Colors.white; // 外框颜色
final double offsetY = 10; // 血条距构件顶部偏移量
final double widthRadio = 0.8; // 血条/构件宽度
final double lifeBarHeight = 4; // 血条高度
final double lifeProgress = 0.8; // 当前血条百分百
Rect rect = Rect.fromCenter(
center: Offset(size.x / 2, lifeBarHeight / 2 - offsetY),
width: size.x * widthRadio,
height: lifeBarHeight);
Rect lifeRect = Rect.fromPoints(
rect.topLeft Offset(rect.width * (1 - lifeProgress), 0),
rect.bottomRight);
_outlinePaint
..color = outlineColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
_fillPaint.color = lifeColor;
canvas.drawRect(lifeRect, _fillPaint);
canvas.drawRect(rect, _outlinePaint);
}
这里为了方便演示,只画了最简洁的血条,大家也可以发挥自己的绘画天赋,在网上找一些好看的血条画画看。
4. 代码复用的好帮手 -mixin
我们刚才只在 Monster
类中覆写的 render
,绘制血条。那主角 Adventurer
也需要要血条,笨方法是把 Monster
中的绘制拷一份到 Adventurer
中。如果一个游戏中有非常多需要需要血条的构件,这样做显然是不可行的。相同的绘制逻辑分散在各个类中,不利于维护和拓展。
反过来我们可以想一下,为什么每个构件都可以很简单地使用手势事件,答案是 mixin
。通过混入的方式可以拓展一个类的功能,所以混入一个 Liveable
来让构件显示血条,自然也不是什么难事。如下所示,是【05/03】 的案例效果 :
如下,Liveable
依赖于 PositionComponent
类,因为绘制时需要构件的尺寸。其中 initPaint
方法中,用于初始化一些配置参数用于自定义,比如血条颜色、外框颜色、生命上限等。这里只是简单演示,满足最基本的需求,你也可以提供一些其他的配置参数,或者定义一个配置信息类,简化传参流程。
在这里只要覆写 render
方法,执行刚才写的绘制逻辑即可。
---->[05/03/mixins]----
mixin Liveable on PositionComponent {
final Paint _outlinePaint = Paint();
final Paint _fillPaint = Paint();
late double lifePoint; // 生命上限
late double _currentLife; // 当前生命值
void initPaint({
required double lifePoint,
Color lifeColor = Colors.red,
Color outlineColor = Colors.white,
}) {
_outlinePaint
..color = outlineColor
..style = PaintingStyle.stroke
..strokeWidth = 1;
_fillPaint.color = lifeColor;
this.lifePoint = lifePoint;
_currentLife = lifePoint;
}
// 当前血条百分百
double get _progress => _currentLife / lifePoint;
@override
void render(Canvas canvas) {
super.render(canvas);
final double offsetY = 10; // 血条距构件顶部偏移量
final double widthRadio = 1.5; // 血条/构件宽度
final double lifeBarHeight = 4; // 血条高度
Rect rect = Rect.fromCenter(
center: Offset(size.x / 2, lifeBarHeight / 2 - offsetY),
width: size.x / 2 * widthRadio,
height: lifeBarHeight);
Rect lifeRect = Rect.fromPoints(
rect.topLeft Offset(rect.width * (1 - _progress), 0),
rect.bottomRight);
canvas.drawRect(lifeRect, _fillPaint);
canvas.drawRect(rect, _outlinePaint);
}
}
使用起来就非常简单,只要是 PositionComponent
一族的构件,都可以混入刚才自定义的 Liveable
,然后只要在 onLoad
方法中通过 initPaint
方法初始化数据即可。
这样就可以在 Adventurer
的头上加一个血条,生命值上限是 1000
。
通过这里可以看出 mixin
对于独立逻辑的封装,还是非常有优势的。混入类中可以拓展 属性
和 方法
,只要 A
混入了 B
,就说明 A
可以视为 B
的子类,即可访问 B
中的所有非私有 属性
和 方法
。
对于 mixin
的理解,是 Dart
中非常重要,也是很多新手所忽略的。在 Flutter
框架的源码中 mixin
有着非常多的使用场景。在 《Flutter 渲染机制 - 聚沙成塔》的第十二章,结合源码中的实际使用对 mixin
有详细的介绍。网上很多文章简单写两个 demo
,是很难真正理解 mixin
的价值的。
5. 血条的减少
有了血条不让它减少有点可惜了,如下案例中,通过点击事件让怪物的血量减少:代码见 【05/04】
血量是在 Liveable
类中定义的,所以也在此维护血量值。如下在 Liveable
中定义 loss
方法,对生命值进行减少。并在生命值小于 0
时,触发 onDied
方法。混入类可以覆写这个方法来监听角色的死亡。
---->[05/04/Liveable]
void loss(double point) {
_currentLife -= point;
if (_currentLife <= 0) {
_currentLife = 0;
onDied();
}
}
void onDied() {
}
然后在 TolyGame
中使用 TapDetector
监听点击事件,每次点击让 monster
减少 50
点生命值。游戏中让射手发出弓箭,检测命中后,让 monster
生命值减少,或通过体力药水或辅助角色恢复生命值。
class TolyGame extends FlameGame with KeyboardEvents,TapDetector {
@override
void onTap() {
monster.loss(50);
}
下面来看一下,如何在 monster
生命值为 0
时进行移除。上面在 Liveable
定义了 onDied
回调,只要在被混入类中覆写 onDied
方法即可监听到生命值为小于等于零的事件。然后执行 removeFromParent
方法即可:
class Monster extends SpriteAnimationComponent with Liveable {
@override
void onDied() {
removeFromParent();
}
本文通过了自定义绘制角色的血条,认识了 Component#render
回调方法,在其中可以获取 Canvas
对象,进行自定义绘制操作。另外也借此认识了一下 mixin
对于独立逻辑封装的妙处。那本文就到这里,明天见 ~
@张风捷特烈 2022.05.30 未允禁转
我的 公众号: 编程之王
我的 掘金主页
: 张风捷特烈我的 B站主页
: 张风捷特烈我的 github 主页
: toly1994328