【Flutter&Flame 游戏 - 贰】操纵杆与角色移动

5,228 阅读6分钟

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


前言

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

第一季完结,谢谢支持 ~


1. Flame 官方案例

github 仓库中 flame/examples 中是官方的案例,对于入门而言是很有参考意义的。 但它也不是非常惊艳,作为一个游戏引擎的官方案例来说,还是太过简陋。

其中介绍了很多基本模块,每个模块中的案例都能很方便地找到对应的源码,这一点还是很值得肯定的。


本文我们将基于如下的 Joystick 案例,介绍一下操纵杆的使用,以及角色的移动。移动是最基础的游戏交互,还是先介绍为好。


本文的效果如下,通过左下角的操纵杆,来移动角色:本文源码于 【lib/02】


2. 操纵杆的使用

操纵杆的原理非常简单,如下以大圆中心为原点建立坐标系,正方向分别向 。通过小圆心的坐标就可以确定偏移量以及旋转角度。这里主要使用偏移量来修改角色的 position 位置。


同样,操纵杆本身也是 Component 构建。如下,在 TolyGameonLoad中构造 JoystickComponent 对象,通过 add 方法加入到游戏中。主要这里的 TolyGame 需要混入 HasDraggables ,才能支持操纵杆拖拽。

---->[02/game.dart]----
class TolyGame extends FlameGame with HasDraggables {
  
  late final JoystickComponent joystick;

  @override
  Future<void> onLoad() async {
    final knobPaint = BasicPalette.blue.withAlpha(200).paint();
    final backgroundPaint = BasicPalette.blue.withAlpha(100).paint();
    joystick = JoystickComponent(
      knob: CircleComponent(radius: 25, paint: knobPaint),
      background: CircleComponent(radius: 60, paint: backgroundPaint),
      margin: const EdgeInsets.only(left: 40, bottom: 40),
    );
    add(joystick);
  }

现在操纵杆已经加入到了 游戏场景 之中,接下来把角色加入进来。方式也很简单,创建 HeroComponent 对象,再添加到场景中即可。代码如下:

---->[02/game.dart]----
late final HeroComponent player;
	
---->[onLoad 方法]----
player = HeroComponent();
add(player);

这就说明,游戏中的各种角色,都是 Component 构件,添加到游戏场景之中,后添加的在上层。游戏的核心是维护各个对象数据间的关系。


3. 角色的移动

在上一篇中,我们介绍了 PositionComponent 一族的构件中有 position 属性,来定位角色位置。也就是说,只要根据操纵杆的偏移量,对 position 属性进行修改即可。另外说一下,在一个 Component 中添加 RectangleHitbox ,就可以有如上的紫色信息框,便于查看角色所占区域即位置。

class HeroComponent extends SpriteAnimationComponent with HasGameRef {
 
  HeroComponent() : super(size: Vector2(50,37), anchor: Anchor.center);
  @override
  Future<void> onLoad() async {
    List<Sprite> sprites = [];
    for(int i=0;i<=8;i++){
      sprites.add(await gameRef.loadSprite('adventurer/adventurer-bow-0$i.png'));
    }
    animation = SpriteAnimation.spriteList(sprites, stepTime: 0.15);
    position = gameRef.size / 2; 
    add(RectangleHitbox()..debugMode = true);
  }
  
  double speed = 200.0; // Pixels/ 秒
  
  void move(Vector2 ds){
    position.add(ds);
  }
}

这里定义一个 move 方法,接受 Vector2 位移量,类中定义了一个 speed ,用于控制移动速度,值越大就表示每秒运动的位移越长。


4. 世界的刷新

我们日常生活中有钟表计时,可以明确时间的概念,现实中时间是不断进行的,永不停息。在游戏开发中也是类似,默认情况下世界处于不断刷新渲染之中,每次的刷新渲染成为一帧。如果每秒渲染 60 次,那就说明游戏每秒可达 60 帧,也就是常说的 60fps 。不过游戏中的时间是可以暂停的。

另外,在 Component 类中定义了 update 方法,可以覆写它来监听每次刷新的事件。前面我们知道 FlameGame 本身也是 Component ,所以在子类 TolyGame 中可以覆写 update 来监听帧的更新。通过打印日志可以看出来,会不断触发,其中 dt 回调表示两帧之间的时间差。而且每帧之间约等于 0.01666 秒 ,也就是 16.6 ms ,即每秒可刷新 60 次。

---->[02/game.dart/TolyGame]----
@override
void update(double dt) {
  super.update(dt);
  print(dt);
}


使用,只要在 update 回调中,执行 playermove 方法即可修改角色位置。其中 joystick.relativeDelta 是偏移量和外圆半径的比值,也就是指移动的百分比。根据物理学公式,可以计算出偏移位移

ds = v * t

其中速度是一个二维的向量,是速度值和 joystick.relativeDelta 向量结合获得的。从而达到操纵杆百分比越大,速度越快的效果。

@override
void update(double dt) {
  super.update(dt);
  if (!joystick.delta.isZero()) {
    Vector2 ds = joystick.relativeDelta * player.speed * dt;
    player.move(ds);
  }
}

另外可以通过 joystick.delta.screenAngle() 获取操纵杆的旋转角度,也就是可以对角色进行旋转操作,如下所示:

PositionComponent 中除了 Vector2 类型的 position 进行定位;还有double 类型的 angle 用于控制旋转角度;以及 Vector2 类型的 scale 控制缩放。

---->[HeroComponent#rotateTo]----
void rotateTo(double deg){
  angle = deg;
}

joystick 偏移了非零时,获取角度为 player 设置旋转角度即可。另外,如果操纵杆偏移量为 0 ,恢复原位。

---->[TolyGame#update]----
// 角色旋转
if (!joystick.delta.isZero()) {
  player.rotateTo(joystick.delta.screenAngle());
}else{
  player.rotateTo(0);
}

5. 小结

本文主要简单认识了一下 JoystickComponent 操纵杆构件,并基于此实现了对角色的移动和旋转操作。也简单认识了一下世界的刷新的触发,这里简单瞄一下源码,其实刷新的触发和 Flutter 原生的 Animation 动画刷新是类似的,都是基于 Ticker 来触发。

Flame 引擎中的 GameLoop 就相当于一个没有停止时间,不断运行的动画。看过《动画小册》的应该对这些比较清楚,这里不过多引申,后面有机会再掰扯掰扯源码。动画和游戏有种类似的本质,都是连续变化的帧。只不过游戏有大量的交互和对象间关系的处理,逻辑非常复杂而已。那本文就到这里,明天见 ~


\