likes
comments
collection
share

Flutter&Flame游戏实践#01 | Trex-角色登场

作者站长头像
站长
· 阅读数 9

Flutter&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结] 第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。


Trex 小游戏介绍

Chrome 在断网时,会有一个小恐龙跳跃躲避障碍物的小游戏,也可以在 chrome://dino/ 地址访问。这个游戏 麻雀虽小五脏俱全 ,是体验游戏开发很好的切入点。

Flutter&Flame游戏实践#01 | Trex-角色登场

它包含以下几个要点:

  • 角色呈现
  • 跳跃移动
  • 碰撞检测
  • 分数记录

这个小游戏将作为 Flutter&Flame 第二季的先锋。通过对恐龙跳跃小游戏的逐步实现,来初步体验 Flame 开发一个小游戏的基本工作流程。下面开始进入游戏开发的世界吧~


一、地面、云朵和障碍物的呈现

本小结你将收获的技能点: 本节源码见 [lib/trex/01]

[1]. 资源加载 : 运行 Flame 的项目代码,加载图片资源。 [2]. 角色的呈现: 如何通过精灵图将角色展示到场景中。 [3]. 角色的定位:如何控制角色在场景中的位置。

在 Flame 中,场景中的一切都是 Component 对象的组合,为了区分Flutter 中的 Widget (组件),文中一律称之为 构件 (游戏的构成零件) 。比如下图中的小恐龙、云朵、地面、分数、障碍物,都是一个个被加入到游戏主类中的构件:

Flutter&Flame游戏实践#01 | Trex-角色登场

本小节我们将读取图片资源,展示地面、云朵和障碍物三个静态的角色,了解一下 Component 的基本使用。


1.游戏主类和资源图片加载

Flame 中通过 GameWidget 组件呈现,其中传入一个 FlameGame 的派生类作为游戏的入口。这里游戏中的所有资源通过 精灵图 的方式集合在一起,如下所示:

Flutter&Flame游戏实践#01 | Trex-角色登场

TrexGame 可以在 onLoad 回调 中异步加载资源;游戏的图片资源可以通过 Flame.images.load 方法加载。复写 backgroundColor 方法,可以修改游戏的背景色(默认是黑色)

---->[lib/trex/01/main.dart]----
main() => runApp(GameWidget(game: TrexGame()));

---->[lib/trex/01/trex_game.dart]----
class TrexGame extends FlameGame {
  late final Image spriteImage;

  @override
  Future<void> onLoad() async{
    spriteImage = await Flame.images.load( 'trex/trex.png' );
  }
  
  @override
  Color backgroundColor() {
    return  const Color(0xffffffff);
  }
}

2. 静态角色的呈现: 云朵

拿云朵来说,它在游戏中的也是以 Component 的身份呈现在场景中的。SpriteComponent 可以展示一个精灵资源,对于 精灵图 来说,我们可以通过顶点坐标 srcPosition 和尺寸 srcSize 来确定某一个精灵,如下示意:

Flutter&Flame游戏实践#01 | Trex-角色登场

下面定义 CloudComponent 继承自 SpriteComponent,在 onLoad 回调中根据图片资源对象创建 Sprite ,并为 sprite 赋值即可:

tips: with HasGameReference<TrexGame> 后,类中可以通过 game 得到 TrexGame 对象.

---->[lib/trex/01/heroes/cloud_component.dart]----
class CloudComponent extends SpriteComponent with HasGameReference<TrexGame>{
  @override
  Future<void> onLoad() async {
    sprite = Sprite(
      game.spriteImage,
      srcPosition: Vector2(166.0, 2.0),
      srcSize: Vector2(92.0, 28.0),
    );
  }
}

有人可能会问,我怎么能知道坐标和尺寸的确切数值?

  • 精灵图制作时,工具会给出坐标相关的配置信息(如下图),可以解析 json 文件得到精灵尺寸和位置。
  • 如果你是拿别人的精灵图,且没有配置信息,可以自己用 PhotoShop 量一下。

Flutter&Flame游戏实践#01 | Trex-角色登场


云朵的构件已经准备完毕,接下来把它 "挂在" 屏幕上。TrexGame#onLoad 方法中通过 add 方法添加 Component 进行展示。构件默认会定位在场景的 左上角

Flutter&Flame游戏实践#01 | Trex-角色登场

---->[lib/trex/01/trex_game.dart]----
@override
Future<void> onLoad() async {
  spriteImage = await Flame.images.load('trex/trex.png');
  add(CloudComponent());
}

3. 构件的定位: 地面和障碍物

如下所示,我们先把地面放在场景中。同样定义一个 GroundComponent 的构件,在 onLoad 时设置地面对于的精灵图。默认会在左上角,SpriteComponent 派生类中,可以通过 x,y 决定构件的位置:

Flutter&Flame游戏实践#01 | Trex-角色登场

onGameResize 回调会 在窗口尺寸变化时 或者构件加载完后 触发,其中的 size 是窗口尺寸。这里想让地面在中间偏下一点,只要将 y 赋值即可:

class GroundComponent extends SpriteComponent with HasGameReference<TrexGame>{

  final double groundHeight = 24;

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    y = size.y / 2 + groundHeight/2;
  }
  
  @override
  Future<void> onLoad() async {
    sprite = Sprite(
      game.spriteImage,
      srcPosition: Vector2(2, 104.0),
      srcSize: Vector2(2400, groundHeight),
    );
  }
}

接下来把第一个障碍物放到场景的中间,同理创建一个 ObstacleComponent 构件表示障碍物。在 onLoad 回调 中创建 Sprite ; 在 onGameResize 回调 中设置偏移量:

Flutter&Flame游戏实践#01 | Trex-角色登场

class ObstacleComponent extends SpriteComponent
    with HasGameReference<TrexGame> {

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    y = size.y / 2 - 55.0 + 21;
    x = size.x / 2 - width / 2;
  }

  @override
  Future<void> onLoad() async {
    sprite = Sprite(
      game.spriteImage,
      srcPosition: Vector2(446.0, 2.0),
      srcSize: Vector2(34.0, 70.0),
    );
  }
}

通过云朵、地面、障碍物三个图片精灵的展示,大家应该对如何呈现一个图片资源有了清晰地认知。

小思考: 如何在场景中添加多个障碍物和云朵? (稍后介绍)


二、小恐龙的呈现与状态变化

本小结你将收获的技能点: 本节源码见 [lib/trex/02]

[1]. 多状态精灵 : 一个构建中如何拥有多种状态,并支持切换。 [2]. 键盘和手势 : 通过点击事件和键盘回调事件,切换小恐龙的展示状态。

Flutter&Flame游戏实践#01 | Trex-角色登场


1. 多状态精灵图片的处理

场地已经在界面上了,那么接下来让小恐龙登场吧! 在游戏中,小恐龙有 不同状态, 使用需要展示不同的图片资源,这里将它的状态通过 PlayerState 表示:

---->[lib/trex/02/heroes/player.dart]----
enum PlayerState {
  waiting, // 等待
  running, // 奔跑
  jumping, // 跳跃
  down, // 趴下
  crashed, // 死亡
}

像这种不同状态有不同图片,而且某些状态需要有 序列帧动画 的角色。可以通过 SpriteAnimationGroupComponent 构件进行展示,它支持一个 泛型 T 表示状态。创建 Player 类型如下:

---->[lib/trex/02/heroes/player.dart]----
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<TrexGame>{
    // TODO
}

上面的 SpriteComponent 通过 sprite 对象展示静态的精灵图片,这里 SpriteAnimationGroupComponent 有一个映射 animations 对象:

以状态 T 为键,以 SpriteAnimation 为值。我们需要完成对 animations 映射赋值的工作。

Flutter&Flame游戏实践#01 | Trex-角色登场

其中 SpriteAnimation 就是序列帧图片,用来展示角色,每一帧图片在对应精灵图的一个矩形区域。下面简单封装 loadAnimation 方法来加载:

  • 传入角色的尺寸 size 和位置列表 frames,来确定矩形区域。
  • stepTime 表示帧动画的间隔时间秒数。
---->[lib/trex/02/heroes/player.dart]----
SpriteAnimation loadAnimation({
  required Vector2 size,
  required List<Vector2> frames,
  double stepTime = double.infinity,
}) {
  return SpriteAnimation.spriteList(
    frames.map((vector) => Sprite(
            game.spriteImage,
            srcSize: size,
            srcPosition: vector,
          )).toList(),
    stepTime: stepTime,
  );
}

2. 映射关系的初始化和呈现

在 Player 构件的 onLoad 回到中通过 _initAnimations 方法来初始化映射关系:

---->[lib/trex/02/heroes/player.dart]----
@override
Future<void> onLoad() async {
  _initAnimations();
}

将不同的 PlayerState 状态,对应为不同的 SpriteAnimation 资源。其中 frames 表示图片的序列帧起点坐标,有多个就表示当前状态具有动画效果:

---->[lib/trex/01/heroes/player.dart]----
void _initAnimations(){
  animations = {
    PlayerState.running: loadAnimation(
      size: Vector2(88.0, 90.0),
      frames: [Vector2(1514.0, 4.0), Vector2(1602.0, 4.0)],
      stepTime: 0.2,
    ),
    PlayerState.waiting: loadAnimation(
      size: Vector2(88.0, 90.0),
      frames: [Vector2(76.0, 6.0)],
    ),
    PlayerState.jumping: loadAnimation(
      size: Vector2(88.0, 90.0),
      frames: [Vector2(1338.0, 4.0)],
    ),
    PlayerState.crashed: loadAnimation(
      size: Vector2(88.0, 90.0),
      frames: [Vector2(1778.0, 4.0)],
    ),
    PlayerState.down: loadAnimation(
      size: Vector2(114.0, 90.0),
      frames: [Vector2(1866, 6.0), Vector2(1984, 6.0)],
      stepTime: 0.2,
    ),
  };
  current = PlayerState.waiting;
}

然后在 TrexGame 中创建 Player 对象,在 onLoad 方法中通过 add 添加构建,此时角色精灵就可以展示出来了。

Flutter&Flame游戏实践#01 | Trex-角色登场

SpriteAnimationGroupComponent 中同样可以通过 x 和 y 数值设置构件的位置。在 onGameResize 回调中根据窗口尺寸进行设置:

Flutter&Flame游戏实践#01 | Trex-角色登场

---->[lib/trex/02/heroes/player.dart]----
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<TrexGame> {
  double get centerY => (game.size.y / 2) - height / 2;

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    y = centerY;
    x = 60;
  }
  /// 略同...

3.键盘所示与 Player 状态切换

SpriteAnimationGroupComponent 中的 current 表示当前的状态,更新该值就可以展示对应状态的图片资源。如下所示,在 toggleState 方法中轮换状态值:

---->[lib/trex/02/heroes/player.dart]----
void toggleState() {
  int nextIndex = (current?.index ?? 0) + 1;
  nextIndex = nextIndex % PlayerState.values.length;
  current = PlayerState.values[nextIndex];
}

然后只要在合适的时机触发 Player#toggleState 方法即可切换小恐龙的状态。通过混入:

  • KeyboardEvents 监听键盘事件。
  • TapCallbacks 监听点击手势事件。

下面代码中,监听到按键 a 以及 onTapDown 事件时,触发 player.toggleState():

Flutter&Flame游戏实践#01 | Trex-角色登场

---->[lib/trex/02/heroes/player.dart]----
import 'package:flutter/widgets.dart' hide Image;
    
class TrexGame extends FlameGame with KeyboardEvents, TapCallbacks{

  /// 略同...

  @override
  KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed ) {
    if (keysPressed.contains(LogicalKeyboardKey.keyA)) {
      player.toggleState();
    }
    return KeyEventResult.handled;
  }

  @override
  void onTapDown(TapDownEvent event) {
    player.toggleState();
  }
}

SpriteAnimationGroupComponent 可以通过 debugMode=true 展示调试信息,包括矩形的边界和位置信息。如下所示,切换是否展示信息也就是切换 debugMode 的真假:

Flutter&Flame游戏实践#01 | Trex-角色登场

这里在 Player 中添加一个 toggleDebugMode 方法,切换 debugMode 值,并且在键盘 D :

Flutter&Flame游戏实践#01 | Trex-角色登场

---->[lib/trex/02/heroes/player.dart]----
void toggleDebugMode() {
  debugMode = !debugMode;
}

三、文字的展示

界面呈现中处理图片之外,最重要的就是文字。Flame 中通过 TextComponent 展示文字,本节就来介绍一下文字的展现方式。

[1]. 使用文字 :通过文本展示小恐龙的状态信息以及提示信息。 [2]. 精灵字体 :通过 SpriteFont 展示分数的图片像素文字。


1.文字信息的展示

虽然现在呈现了小恐龙的状态变化,但是看起来并不是很清晰,如果界面上可以展示一些提示文字,就可以清晰地自动当前案例的作用。比如当前操作的按键作用以及小恐龙的状态信息:

Flutter&Flame游戏实践#01 | Trex-角色登场


flame 中一切的表现都是 Component , 为了方便展示维护提示信息,可以像 Player 那样将其视为一个角色加入游戏场景中。 如下所示,定义 HelpText 继承自 PositionComponent ,让其拥有定位能力;在 onLoad 回调中加入两个 TextComponent 分别展示状态和提示文字。并提供 changeState 方法更新状态文字的内容:

class HelpText extends PositionComponent with HasGameReference<TrexGame>  {
  TextStyle stateStyle = const TextStyle(fontSize: 12, color: Colors.blue);
  TextStyle infoStyle = const TextStyle(fontSize: 12, color: Colors.grey);

  final String _info = '提示信息:\n'
 '键盘a/点击: 切换恐龙状态\n'
 '键盘 d: 切换展示边框信息' ;

  String initState = '' ;

  HelpText(this.initState);

  double get centerY {
    return (game.size.y / 2) - height / 2;
  }

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    y = centerY;
    x = 60;
  }

  late TextComponent _stateText;

  void changeState(String state) {
    _stateText.text = state;
  }

  @override
  Future<void> onLoad() async {
    _stateText = TextComponent(
      text: initState,
      position: Vector2(0,68),
      textRenderer: TextPaint(style: stateStyle),
    );
    add( _stateText);
    add(TextComponent(
      position: position.translated(0, 68+20),
      text: _info,
      textRenderer: TextPaint(style: infoStyle),
    ));
  }
}

然后在 TrexGame 中将 HelpText 像 Player 那样加入到场景中。当点击和按键事件时,通过 changeState 方法修改状态文字即可:

Flutter&Flame游戏实践#01 | Trex-角色登场

class TrexGame extends FlameGame with KeyboardEvents, TapCallbacks {
  /// 略同...
  late  final HelpText helpText;

  @override
  Future<void> onLoad() async {
    spriteImage = await Flame.images.load( 'trex/trex.png' );
    add(player);
    String initState = player.current.toString();
    helpText = HelpText(initState);
    add(helpText);
  }

2. 精灵字体 SpriteFont

Flame 中提供了 SpriteFont 方便展示精灵图中的字体。在项目精灵图中,有数字和字母相关的图片作为分数。通过精灵字体,就可以将对应的字符串 映射为 精灵图片列表 展示:

Flutter&Flame游戏实践#01 | Trex-角色登场

比如下面的 1024 HI 2048 字符串,就可以访问到对应的精灵图片,展示文字:

Flutter&Flame游戏实践#01 | Trex-角色登场

同样,这里也定义一个 ScoreComponent 负责维护分数角色的展示, 在 onLoad 回调中创建并添加 TextComponent 。通过 SpriteFont 来建立字符集合图片区域的映射 Glyph 对象。这样对应的字符在渲染时就可以找到对应区域的图片精灵,完成展示:

class ScoreComponent extends PositionComponent with HasGameReference<TrexGame> {
  
  late TextComponent _score;
  
  @override
  Future<void> onLoad() async {
    const chars = '0123456789HI ';
    final renderer = SpriteFontRenderer.fromFont(
      SpriteFont(
        source: game.spriteImage,
        size: 23,
        ascent: 23,
        glyphs: [
          for (var i = 0; i < chars.length; i++)
            Glyph(chars[i], left: 954.0 + 20 * i, top: 0, width: 20),
        ],
      ),
      letterSpacing: 2,
    );
    _score = TextComponent( textRenderer: renderer);
    _score.text = '1024  HI 2048';
    add(_score);
  }
  
  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);
    x = size.x - _score.width -20;
    y = 20;
  }
}

到这里已经人物登场啦,第一集的内容就介绍完毕了。下面整理了一下本集的知识。大家可以自己根据每一项思考一下具体内容:

Flutter&Flame游戏实践#01 | Trex-角色登场

下一章将继续推进,学习如何让画面动起来。

转载自:https://juejin.cn/post/7341668771911876635
评论
请登录