Flutter中文网

Flutter中文网

使用Flutter Flame快速轻松地创建2D游戏

41
2024-07-16

Flutter 可以用一套代码为 Android、iOS、桌面和网页等平台开发应用程序。作为跨平台 UI 工具包,Flutter 团队致力于让所有类型的开发者都能快速构建和发布应用程序。例如,游戏开发者现在可以轻松构建美观的游戏应用,无需担心性能、加载时间和应用大小等问题。

本教程将为您介绍Flutter Flame游戏引擎。您将学习如何设置和构建Flutter Flame游戏、加载精灵以及添加动画。

本教程假定您已经掌握了Dart和Flutter的基本知识。

Flame 引擎

Flame是一个运行在Flutter上的2D游戏开发框架。Flame引擎可以轻松实现游戏循环以及动画、碰撞检测、反弹检测和视差滚动等其他必要功能。

Flame是模块化的,它提供了可以独立使用的包,用于扩展其功能,例如:

Flutter Flame设置

要开始使用 Flame,您需要安装该包。在您的 pubspec.yaml 文件中,按照以下方式添加依赖项:

    dependencies:
      flame: ^1.1.1

为了运行游戏,您需要使用 GameWidget 组件。在 main.dart 文件中添加下面的代码片段可以渲染一个Flame游戏,但目前游戏界面是一片空白。

    void main() {
      final game = FlameGame();
      runApp(
        GameWidget(
          game: game,
        ),
      );
    }

现在你可以开始为游戏添加图形了。

加载精灵

要生成静态图像,您需要使用 SpriteComponent 类。将需要的游戏图像添加到 assets/images 文件夹中,并更新 pubspec.yaml 文件以加载资产。所需的图片可以在这里找到。

你需要在  lib  文件夹中创建并更新以下三个文件:

  • dino_player.dart  将加载并定位我们的玩家角色:

    import 'package:flame/components.dart';
    
    class DinoPlayer extends SpriteComponent with HasGameRef {
      DinoPlayer() : super(size: Vector2.all(100.0));
    
      @override
      Future<void> onLoad() async {
        super.onLoad();
        sprite = await gameRef.loadSprite('idle.png');
        position = gameRef.size / 2;
      }
    }

  • dino_world.dart它将加载我们的游戏背景:

    import 'package:flame/components.dart';
    
    class DinoWorld extends SpriteComponent with HasGameRef {
      @override
      Future<void> onLoad() async {
        super.onLoad();
        sprite = await gameRef.loadSprite('background.png');
        size = sprite!.originalSize;
      }
    }
  • dino_game.dart,它将管理我们所有的游戏组件。它添加我们的游戏玩家和背景并确定他们的位置。

    import 'dart:ui';
    
    import 'package:flame/game.dart';
    import 'dino_player.dart';
    import 'dino_world.dart';
    
    class DinoGame extends FlameGame{
      DinoPlayer _dinoPlayer = DinoPlayer();
    DinoWorld _dinoWorld = DinoWorld();
      @override
      Future<void> onLoad() async {
        super.onLoad();
        await add(_dinoWorld);
        await add(_dinoPlayer);
        _dinoPlayer.position = _dinoWorld.size / 1.5;
        camera.followComponent(_dinoPlayer,
            worldBounds: Rect.fromLTRB(0, 0, _dinoWorld.size.x, _dinoWorld.size.y));
      }
    }

camera.followComponent 将游戏视窗设置为跟随玩家移动。这个函数是必需的,因为我们将在游戏中为玩家添加移动效果。

更新你的  main.dart  文件加载  DinoGame  ,就像下面展示的那样:

    import 'package:flame/game.dart';
    import 'package:flutter/material.dart';
    import 'dino_game.dart';
    
    void main() {
      final game = DinoGame();
      runApp(
        GameWidget(game: game),
      );
    }

运行你的应用程序应该会显示你的玩家和背景。

游戏背景和玩家

移动精灵

要移动玩家,您需要检测并响应您所选择的方向。在这节教程中,您将使用游戏的箭头键为玩家添加移动功能。

首先,创建一个名为helpers的文件夹,并将以下文件放入其中,并按照以下方式更新它们:

  • directions.dart 枚举包含以下方向:

    enum Direction { up, down, left, right, none }
  • navigation_keys.dart 包含了导航键的UI和逻辑。

    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    import 'directions.dart';
    
    class NavigationKeys extends StatefulWidget {
      final ValueChanged<Direction>? onDirectionChanged;
    
      const NavigationKeys({Key? key, required this.onDirectionChanged})
          : super(key: key);
    
      @override
      State<NavigationKeys> createState() => _NavigationKeysState();
    }
    
    class _NavigationKeysState extends State<NavigationKeys> {
      Direction direction = Direction.none;
    
      @override
      Widget build(BuildContext context) {
        return SizedBox(
          height: 200,
          width: 120,
          child: Column(
            children: [
              ArrowKey(
                icons: Icons.keyboard_arrow_up,
                onTapDown: (det) {
                  updateDirection(Direction.up);
                },
                onTapUp: (dets) {
                  updateDirection(Direction.none);
                },
                onLongPressDown: () {
                  updateDirection(Direction.up);
                },
                onLongPressEnd: (dets) {
                  updateDirection(Direction.none);
                },
              ),
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  ArrowKey(
                    icons: Icons.keyboard_arrow_left,
                    onTapDown: (det) {
                      updateDirection(Direction.left);
                    },
                    onTapUp: (dets) {
                      updateDirection(Direction.none);
                    },
                    onLongPressDown: () {
                      updateDirection(Direction.left);
                    },
                    onLongPressEnd: (dets) {
                      updateDirection(Direction.none);
                    },
                  ),
                  ArrowKey(
                    icons: Icons.keyboard_arrow_right,
                    onTapDown: (det) {
                      updateDirection(Direction.right);
                    },
                    onTapUp: (dets) {
                      updateDirection(Direction.none);
                    },
                    onLongPressDown: () {
                      updateDirection(Direction.right);
                    },
                    onLongPressEnd: (dets) {
                      updateDirection(Direction.none);
                    },
                  ),
                ],
              ),
              ArrowKey(
                icons: Icons.keyboard_arrow_down,
                onTapDown: (det) {
                  updateDirection(Direction.down);
                },
                onTapUp: (dets) {
                  updateDirection(Direction.none);
                },
                onLongPressDown: () {
                  updateDirection(Direction.down);
                },
                onLongPressEnd: (dets) {
                  updateDirection(Direction.none);
                },
              ),
            ],
          ),
        );
      }
    
      void updateDirection(Direction newDirection) {
        direction = newDirection;
        widget.onDirectionChanged!(direction);
      }
    }
    
    class ArrowKey extends StatelessWidget {
      const ArrowKey({
        Key? key,
        required this.icons,
        required this.onTapDown,
        required this.onTapUp,
        required this.onLongPressDown,
        required this.onLongPressEnd,
      }) : super(key: key);
      final IconData icons;
      final Function(TapDownDetails) onTapDown;
      final Function(TapUpDetails) onTapUp;
      final Function() onLongPressDown;
      final Function(LongPressEndDetails) onLongPressEnd;
    
      @override
      Widget build(BuildContext context) {
        return GestureDetector(
          onTapDown: onTapDown,
          onTapUp: onTapUp,
          onLongPress: onLongPressDown,
          onLongPressEnd: onLongPressEnd,
          child: Container(
            margin: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: const Color(0x88ffffff),
              borderRadius: BorderRadius.circular(60),
            ),
            child: Icon(
              icons,
              size: 42,
            ),
          ),
        );
      }
    }

然后,更新  main.dart  文件,以显示您的游戏和按键,如下所示:

    void main() {
      final game = DinoGame();
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          home: Scaffold(
            body: Stack(
              children: [
                GameWidget(
                  game: game,
                ),
                Align(
                  alignment: Alignment.bottomRight,
                  child: NavigationKeys(onDirectionChanged: game.onArrowKeyChanged,),
                ),
              ],
            ),
          ),
        ),
      );
    }

 dino_game.dart  文件中添加以下代码以实现玩家的移动功能:

    onArrowKeyChanged(Direction direction){
      _dinoPlayer.direction = direction;
    }

最后,更新 dino_player.dart 文件,通过包含以下代码片段来更新玩家的位置:

    Direction direction = Direction.none;
    
    @override
    void update(double dt) {
      super.update(dt);
      updatePosition(dt);
    }
    
    updatePosition(double dt) {
      switch (direction) {
        case Direction.up:
          position.y --;
          break;
        case Direction.down:
          position.y ++;
          break;
        case Direction.left:
          position.x --;
          break;
        case Direction.right:
          position.x ++;
          break;
        case Direction.none:
          break;
      }
    }

运行你的程序,按下任意一个箭头方向应该会更新玩家的位置。

Sprite animations 精灵动画

现在,你的玩家按预期移动,但现在还没有动画效果,看起来很不自然。为了让你的玩家产生动画,你需要使用精灵表sprite sheet。

精灵表是一组排列成行和列的精灵图像的集合。与单独的精灵图相比,加载速度更快。Flame引擎可以只加载和渲染精灵表的一部分。下面是一张恐龙玩家的精灵表。

恐龙玩家的精灵表

精灵表包含了不同的玩家帧,可以被动画化以展示诸如向右或向左行走等动作。精灵表被添加到 assets/images 文件夹中。

在 dino_player.dart 文件中,按照以下步骤来使玩家动起来:

  1. 用 SpriteAnimationComponent 替代 SpriteComponent 。

  2. 初始化你的动画和动画速度。在这节教程中,我们将专注于左右行走的动画。

    late final SpriteAnimation _walkingRightAnimation;
    late final SpriteAnimation _walkingLeftAnimation;
    late final SpriteAnimation _idleAnimation;
    
    final double _animationSpeed = .15;
  1. 从精灵表中加载精灵。精灵的加载取决于它们在表上的位置。你可以通过指定每个精灵的宽度和列数,或者根据其行和列的位置来加载精灵。

    Future<void> _loadAnimations() async {
      final spriteSheet = SpriteSheet.fromColumnsAndRows(
          image: await gameRef.images.load('spritesheet.png'),
          columns: 30,
          rows: 1);
    
      _idleAnimation = spriteSheet.createAnimation(
          row: 0, stepTime: _animationSpeed, from: 0, to: 9);
    
      _walkingRightAnimation = spriteSheet.createAnimation(
          row: 0, stepTime: _animationSpeed, from: 10, to: 19);
    
      _walkingLeftAnimation = spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, from: 20, to: 29);
    }

spriteSheet.createAnimation函数根据rowfromto属性选择了动画精灵序列,并让它们动起来。

  1. 更新玩家以加载选定的动画.

首先, 重写 onLoad 方法加载 _idleAnimation.

    @override
    Future<void> onLoad() async {
      super.onLoad();
      await _loadAnimations().then((_) => {animation = _idleAnimation});
    }

然后更新 updatePosition 函数,根据玩家所面对的方向加载不同的动画。本教程提供了静止状态、向右移动和向左移动的精灵。

    updatePosition(double dt) {
      switch (direction) {
        case Direction.up:
          position.y --;
          break;
        case Direction.down:
          position.y ++;
          break;
        case Direction.left:
          animation = _walkingLeftAnimation;
          position.x --;
          break;
        case Direction.right:
          animation = _walkingRightAnimation;
          position.x ++;
          break;
        case Direction.none:
          animation = _idleAnimation;
          break;
      }
    }

运行你的应用程序并向左或向右移动,会更新玩家的移动状态,现在看起来更加真实了。

恭喜,你刚刚用Flame制作了你的第一个简单的游戏!

你可以使用flame_tiled 包在应用程序中加载带有碰撞层的自定义地图或贴图,以改进游戏。要设计地图和贴图,你需要知道如何使用Tiled创建它们。

你还可以使用flame_audio包在游戏中添加音频。

总结

Flame是一个基于Flutter的轻量级游戏引擎,可帮助开发人员快速创建2D游戏。

在本教程中,您学习了如何安装和使用 Flame。我们还介绍了如何使用 Flutter Flame 引擎来加载精灵、添加精灵移动和动画。您还了解了可以增强游戏的多种独立包。

希望您喜欢本教程!