持续创作,加速成长!这是我参与「掘金日新计划 · 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 游戏 - 贰贰】菜单、字体和浮层
未完待续 ~
1. 本文目标
今天来看一下角色如何发射子弹,这里把 子弹
作为 发射物
的统称。少数人不要杠,明明是弓箭,非说是子弹。关于子弹,有些注意点,首先它是基于某个角色进行产出的;其次,它会被频繁创建和销毁。它被销毁的时机包括:命中物体时,移出屏幕,或者超出射程,又或者固定在诞生几秒后自动移除等。
https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4b601aaef5cb4287abd7c73b2b6d8778~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?
这里使用射程来对子弹进行移除,对水平发射而言,射程就是子弹在水平方向上的偏移距离,如下图蓝框所示区域:
2. 主动触发帧动画
前面我们的弓手是不断循环的帧动画,现在来先看一下如何主动触发:比如下面案例中,按下键盘的 J
键就执行一次动画,代码详见 【08/01】
https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2a438c942fc34707908c777305bdf0a1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?
在 Adventurer
的 onLoad
方法中,指定 playing
为 false
可以在开始不会执行帧动画。将 loop
置为 false
,帧就不会重复执行;通过 animation
的 onComplete
回调方法,可以监听到帧动画结束的时机。这里当结束时,触发 _onLastFrame
,置为第一帧:
---->[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
即方法可。
void shoot() {
playing = true;
animation!.reset();
}
复制代码
3. 子弹的发射
如下,定义 Bullet
构建来表述子弹角色,在构造时指定图片 sprite
和最大射程 maxRange
。子弹在诞生之后,就会一直处于运动状态,可以覆写 update
方法,根据时间和速度计算偏移量。如下 tag1
处所示:当偏移总量大于 maxRange
时,进行移除。
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】
https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/faf10574769142b89a4e6b74ef4593ed~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?
代码语言:javascript复制---->[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】
https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9486ed49a154b6ea914be1347f0c2a0~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp?
这种校验的思路是:在每帧触发 update
时,校验怪物的矩形区域是否包含某点。比如说,当弓箭的中心在怪物的矩形域中,就表示命中。代码处理如下:
@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