Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生

前言

小时候的我们,都有一个小小的游戏梦

什么 植物大战僵尸、红警、CF、LOL、王者荣耀 ...

这些游戏无不手拿把掐

半夜躲在被窝里偷偷玩电脑、玩手机,那更是 基操勿六

即使现在长大了,没时间玩了

💭 那份 热爱,从来没消失过 ...

本系列文章,我们就来看看如何使用 Flutter 开发一款 2D像素风 游戏。

本着开源精神,所有 资源素材源代码 , 将在 github 上公开。


Flame 开发引擎

Flame 是一个构建于 Flutter 之上的 轻量级 2D 游戏引擎

让我们能够使用熟悉的 Flutter + Dart 技术栈快速制作跨平台游戏。

不需要我们深入研究底层渲染、动画系统、触摸事件管理或碰撞检测,就能轻松上手开发一款游戏。

特点

功能模块 说明
组件化系统(FCS) 游戏角色、背景、UI 全部组件化,结构清晰、扩展灵活。
游戏循环(Game Loop) 内置稳定帧循环,自动处理 update()render()
动画管理简单 Sprite / SpriteSheet / SpriteAnimation 使用便捷。
碰撞检测 & 物理支持 自带碰撞系统,支持 Shape、Hitbox、物理模拟等。
输入处理统一 轻松处理点击、拖拽、多点触控、键盘、手柄等输入。
生态完善 拥有大量扩展插件,如 flame_audio、flame_behaviors、flame_rive、flame_forge2d 等。

拓展

在今年 9 月 3 日 举办的 FlutterNFriends 大会上,Flame 还展示了关于 3D 游戏的支持 flame_3d

Flame 3D 是 Flame 团队推出的基于 Flutter + Impeller 的轻量级 3D 游戏引擎扩展。它延续了 Flame 的组件化架构与简单 API,让开发者能用熟悉的 Dart 写出包含 3D 模型、光照、摄像机、交互等效果的场景。适用于移动端轻量 3D 游戏、展示类应用及 UI+3D 混合项目,学习门槛极低。


素材推荐

相信总会有一部分兄弟有一颗做游戏的心,但是苦于没有素材。

自己这三脚猫功夫,也不可能自己绘制。

在这里给大家推荐 两种方式

1. itch.io

itch.io 是一个开放的独立游戏发布平台,同时也是开发者获取游戏素材的重要来源。平台上有大量由创作者上传的 像素素材、UI、地图瓦片、音效、背景音乐、角色动画、特效包 等资源,支持免费、付费或"随心付" 下载。开发者可以快速获取美术与音频资产,用于原型制作或正式项目。

🗺️ 网站地址itch.io

2. Holopix AI

Holopix AI 是一款专为游戏设计而生的 AI 美术平台,帮助开发者显著提升生产力。它通过文生图、草稿细化、多风格生成等能力,快速产出 2D 原画、角色、场景和 UI 素材;并支持 2D→3D 转换、局部精修、高清放大 等专业能力,让美术风格保持一致。平台还涵盖图生视频与营销素材生成,让 AI 无缝融入游戏创作与发行流程,非常适合独立开发者和小团队快速构建游戏原型与正式资源。

🗺️ 网站地址holopix.cn


精灵图

2D游戏 的本质,其实可以归结为两点:

  • 在一个平面世界里渲染图像
  • 让图像根据规则动起来

玩家眼中看到的角色、怪物、背景、UI,其实都是一张张图像按顺序绘制出来的

无论是走路、跳跃、攻击,还是爆炸特效,本质都是:不断更新图片 → 渲染到屏幕 → 形成视觉动画

在早期图形系统中,这些可移动的小图像被称为 精灵(Sprite) ,是 2D 游戏世界中角色与物体的基础单位

随着游戏规模增大、资源增多,为了让这些 Sprite 加载更快、渲染更高效、管理更统一,开发者进一步提出了:

👉 精灵图(Sprite Sheet)

它将角色的所有动作帧、特效图块等合并到一张大图中,通过 切图 + 帧序播放 实现动画。

Flame 里,大多数角色动画、特效乃至 UI 图形,都是基于精灵图构建的。

Sprite2D 游戏 显示的 基础单位 ,而 精灵图 则是让这些 Sprite 在现代游戏中更高效运作的资源组织方式。

优点✨ 说明 效果
减少加载次数 多帧动画只加载 1 张图,而不是多张图片 加载速度更快、进入场景不再卡顿
降低内存与 GPU 纹理切换 所有帧共享同一个纹理对象(GPU 优化) 更省内存、减少 GPU draw calls、游戏更稳定
动画播放更流畅 多帧在同一张图内,无需频繁切换纹理 动画更丝滑、掉帧概率更低
减少 I/O(磁盘读取) 一次加载大图比多次加载小图快得多 场景切换、人物加载速度显著提升
资源管理更简单 所有帧统一管理,不易出错 文件结构清晰、开发效率高
Flame 原生优化支持 Flame ImageCache、SpriteSheet、SpriteAnimation 等都为精灵图优化 代码更短、更快、更稳定

MyHero

一. 本章目标

二. 项目构建

1. 引入依赖

yaml 复制代码
# 最新版本以官网为准
flame: ^1.34.0         # Flame 主引擎:提供组件系统、渲染、动画、输入事件、碰撞检测等 2D 游戏核心功能
flame_audio: ^2.11.12  # 音频扩展库:简化 BGM、音效的加载和播放(底层基于 audioplayers)
flame_tiled: ^1.17.0   # 支持 Tiled 地图(*.tmx)解析,用于关卡设计、地形图层、碰撞层等

本章仅会用到 flame ,其他依赖会在后续文章中一一使用。

2. 引入资源

在项目,根目录下创建 assets/images/ 文件夹,专门存储图片资源文件。

然后在 pubspec.yaml 中配置对应的文件夹。


这里的图片,是我在 itch.io ,找到的一个免费的资源。

像素小人 塞提尔

包含了 待机、奔跑、攻击、死亡、游泳 ... 多个动画。


3. 初始化模板

(1)项目目录说明
less 复制代码
lib/
├── main.dart          // 游戏入口
├── game/
│   ├── my_game.dart   // 游戏核心逻辑
│   ├── component/     // 游戏中各类组件(角色、敌人、道具等)
│   └── state/         // 游戏对象的状态逻辑(角色状态、敌人 AI 等)
  • main.dart :

    启动 Flutter 应用,创建并挂载 GameWidget,作为游戏渲染入口。

  • my_game.dart :

    游戏主类,管理游戏世界(World)、相机、输入处理、更新循环、组件添加与调度。

  • component/ :

    存放游戏中的各类可复用组件,比如:

    • 玩家角色 PlayerComponent
    • 敌人 EnemyComponent
    • 子弹、道具、障碍物等

    负责显示、动画、碰撞体积等具体表现。

  • state/ :

    放置组件的 行为状态 逻辑,比如:

    • 角色状态(Idle、Run、Attack、Dead)
    • 敌人状态机(Patrol、Chase、Attack、Die)
    • 道具的生命周期状态

    把行为从组件中分离出来,使逻辑更清晰。


(2)main.dart - 游戏入口
dart 复制代码
import 'package:flutter/material.dart';
import 'game/my_game.dart';
import 'package:flame/game.dart';

void main() {
  runApp(
    GameWidget(
      game: MyGame(),
    ),
  );
}
  1. GameWidget
    • MyGame 游戏实例嵌入 Flutter Widget 树
    • 自动处理:
      • 每帧刷新 (update + render)
      • 输入事件(触摸、手势、键盘)
      • 屏幕尺寸变化 (onGameResize)
  2. runApp
    • 启动 Flutter 应用
    • 显示游戏画面到屏幕

(3)my_game.dart - 游戏核心类
dart 复制代码
import 'package:flame/game.dart';
import 'package:flutter/material.dart';

class MyGame extends FlameGame {

  @override
  Future<void> onLoad() async {
    // 加载游戏资源
    super.onLoad();
  }

  @override
  void update(double dt) {
    // 游戏逻辑,每帧更新
    super.update(dt);
  }

  @override
  void render(Canvas canvas) {
    // 渲染逻辑
    super.render(canvas);
  }
}
  1. FlameGame

    • Flame 的基础游戏类
    • 自动管理组件 (children)
    • 自动处理渲染循环
  2. 生命周期方法

    • onLoad()

      • 游戏启动时异步加载资源(图片、音效、精灵表等)
      • 在这里可以 add() 组件到游戏中
    • update(double dt)

      • 每帧调用一次,处理游戏逻辑
      • dt:上一帧耗时(秒),用于实现帧率无关移动
    • render(Canvas canvas)

      • 每帧绘制游戏画面
      • FlameGame 会自动渲染已添加的组件

(4)补充说明
① 组件管理(Components)

在 Flame 中,游戏中的任何元素(人物、背景、道具、碰撞体等)都应该封装成 Component

常用组件类型:

  • PositionComponent:具有位置、大小、角度
  • SpriteComponent:显示一张静态图片
  • SpriteAnimationComponent:播放动画序列
  • TextComponent:渲染文本
  • 自定义组件:继承 Component

👉 添加组件

onLoad() 里添加:

dart 复制代码
@override
Future<void> onLoad() async {
  add(HeroComponent());
  add(EnemyComponent());
  add(BackgroundComponent());
}

Flame 会负责:

  • 管理生命周期
  • 自动调用组件的 update()render()
  • 统一渲染顺序(可用 priority 调整层级)
② 输入事件(Interaction)

游戏需要交互时,只需要让 Game 类混入能力模块

常见模块:

输入类型 混入 (with xxx) 组件需继承
点击 HasTappables Tappable
拖拽 HasDraggables Draggable
键盘 KeyboardEvents ---
手势(长按/双击) HasGestureDetectors ---

👉 例如:点击让角色跳跃

dart 复制代码
class MyGame extends FlameGame with HasTappables {}

class HeroComponent extends SpriteAnimationComponent with Tappable {
  @override
  bool onTapDown(TapDownInfo info) {
    // 点击触发跳跃
    y -= 50;
    return true;
  }
}
③ 屏幕适配(onGameResize)

当设备旋转、窗口变化时 Flame 会自动触发:

dart 复制代码
@override
void onGameResize(Vector2 size) {
  super.onGameResize(size);
  print('新的屏幕大小 $size');
}

你可以用它来:

  • 居中角色
  • 调整 UI 布局
  • 计算碰撞区域

👉 例如:让主角始终在屏幕中心

dart 复制代码
@override
void onGameResize(Vector2 gameSize) {
  super.onGameResize(gameSize);
  position = gameSize / 2;
}

三. 英雄登场

在完成前置条件之后,我们可以正式创建游戏中的第一个角色组件 HeroComponent
HeroComponent 代表玩家角色,它继承自 Flame 提供的 SpriteAnimationComponent,用于播放角色动画。

1. HeroComponent 代码

dart 复制代码
import 'package:myhero/game/my_game.dart';
import 'package:flame/sprite.dart';
import 'package:flame/components.dart';

class HeroComponent extends SpriteAnimationComponent with HasGameReference<MyGame> {

  HeroComponent()
      : super(
          size: Vector2(32, 32),
          anchor: Anchor.center,
        );

  @override
  Future<void> onLoad() async {
    // 加载整张精灵图
    final image = await game.images.load('SPRITE_SHEET.png');

    // 按 32×32 分割精灵表
    final sheet = SpriteSheet(
      image: image,
      srcSize: Vector2(32, 32),
    );

    // 创建动画(第 0 行,帧 0~5)
    animation = sheet.createAnimation(
      row: 0,
      stepTime: 0.15,
      from: 0,
      to: 5,
      loop: true,
    );

    // 实际渲染大小放大为 100×100
    size = Vector2(100, 100);

    // 初始位置:屏幕中心
    position = game.size / 2;
  }

  @override
  void onGameResize(Vector2 size) {
    super.onGameResize(size);

    // 屏幕变化时保持英雄居中
    position = size / 2;
  }
}

2. 代码详解

① 继承 SpriteAnimationComponent
dart 复制代码
class HeroComponent extends SpriteAnimationComponent

选择这个组件是因为:

  • 可自动播放逐帧动画
  • 支持 animation = ... 动态切换动画(后续可做 idle/run/attack)
  • 已包含位置、大小、角度等属性

② super 构造参数:size 与 anchor
dart 复制代码
HeroComponent() : super(size: Vector2(32, 32), anchor: Anchor.center);
  • size: Vector2(32, 32):逻辑上的精灵尺寸
  • anchor: Anchor.center:组件位置以中心点为基准,便于旋转和居中显示

③ onLoad:加载资源与初始化动画

1. 加载整张精灵图

dart 复制代码
final image = await game.images.load('SPRITE_SHEET.png');

Flame 会自动缓存图片,同一个资源不会重复加载。

2. 创建 SpriteSheet

dart 复制代码
final sheet = SpriteSheet(image: image, srcSize: Vector2(32, 32));

表示图片被切割成 32×32 的小格,是精灵图中角色动画每帧大小。

3. 创建动画

dart 复制代码
 animation = sheet.createAnimation(
      row: 0,
      stepTime: 0.15,
      from: 0,
      to: 5,
      loop: true,
    );
  • row:使用第 0 行 的动画帧
  • stepTime:每帧播放时间 0.15 秒
  • from:从 帧 0 播到帧 5
  • to:播放至该行 第5帧
  • loop: 是否循环播放

4. 设置渲染尺寸

dart 复制代码
    size = Vector2(100, 100);

虽然原图是 32×32,但渲染时可以放大到 100×100 更清晰。


5. 初次定位与屏幕适配

  • 初次进入游戏时居中:

    dart 复制代码
    position = game.size / 2;
  • 屏幕尺寸变化时重新居中:

    dart 复制代码
    void onGameResize(Vector2 size) {
          position = size / 2;
        }

场景变化(横屏、竖屏、窗口大小变化)后,主角仍保持在屏幕中心。

四. 随心所动

人物渲染出来后,最核心的交互能力就是 移动

一个完善、自然、流畅的移动系统通常包含下面 三个关键步骤

  1. 输入控制:捕获玩家操作(摇杆/键盘/手势)
  2. 状态机驱动动画:根据移动状态切换对应动画
  3. 方向控制:角色自动朝向移动方向(翻转)

下面将一步步完善这三个部分。


1. 创建摇杆,实现基础移动

移动的第一步是捕获玩家输入。

Flame 提供了现成的 JoystickComponent,通过它可以轻松实现虚拟摇杆控制。

① 创建摇杆组件

dart 复制代码
    // 创建摇杆
    joystick = JoystickComponent(
      knob: CircleComponent(radius: 30, paint: Paint()..color = Colors.white70),
      background: CircleComponent(
        radius: 80,
        paint: Paint()..color = Colors.black87,
      ),
      margin: const EdgeInsets.only(left: 50, bottom: 50),
    );

② 根据摇杆移动人物

dart 复制代码
 // 每秒移动速度
  double speed = 160;
  
  @override
    void update(double dt) {
      super.update(dt);

      final joy = game.joystick;

      if (joy.direction != JoystickDirection.idle) {
        // 获取单位方向向量,例如 (0.7, -0.3)
        Vector2 dir = joy.relativeDelta;
        position += dir * speed * dt;
      }
    }

在移动更新逻辑中,我们主要关心两个变量:

  • direction(摇杆方向)

    方向 角度范围(度) 简要说明
    idle --- 没有输入(delta.isZero()
    up 0° ~ 22.5°、337.5° ~ 360° 上方(包含左右两端的小范围)
    upRight 22.5° ~ 67.5° 右上
    right 67.5° ~ 112.5°
    downRight 112.5° ~ 157.5° 右下
    down 157.5° ~ 202.5°
    downLeft 202.5° ~ 247.5° 左下
    left 247.5° ~ 292.5°
    upLeft 292.5° ~ 337.5° 左上
  • relativeDelta(标准化后的方向向量)

    它由摇杆当前的偏移量自动 归一化 得到,表示纯粹的方向(长度始终为 1)。

💡 position += dir * speed * dt 是如何计算的?

按当前方向 dir,乘以移动速度 speed,乘以本帧的时间 dt,更新角色位置。

公式可以拆成:

bash 复制代码
位移 = 方向向量 dir × 速度 speed × 时间 dt
新位置 = 旧位置 + 位移

举例:

  • dir = (1, 0)(向右)
  • speed = 200 像素/秒
  • dt = 0.016(60FPS)

计算:

scss 复制代码
位移 = (1, 0) * 200 * 0.016
    = (3.2, 0)
position = position + (3.2, 0)

👉 角色这一帧 向右移动了 3.2 像素。

虽然这里已经完成了角色的位置移动 ,但此时角色的动画仍然停留在 待机(Idle)状态

原因是:

  • 目前只渲染了单个待机动画 ------ 未建立可切换的动画状态集合。
  • 移动逻辑和动画逻辑是独立的 ------ 位置改变不会自动切换到 跑步/行走 动画。

2. 根据状态切换动画

移动不仅要位移,还要 行为表现

要使角色自然地 走起来 / 停下来,我们必须根据当前状态决定渲染哪个动画。

① 定义角色状态

dart 复制代码
enum HeroState {
 idle,
 run,
 swim,
 attack,
 hurt,
 die,
}

② 状态改变时更新对应的动画

dart 复制代码
HeroState state = HeroState.idle;

void _setState(HeroState newState) {
  if (state == newState) return; // 避免重复切换

  state = newState;
  animation = animations[state]!;
}

③ 加载动画帧,生成动画集合

dart 复制代码
late Map<HeroState, SpriteAnimation> animations;

Future<void> _loadAnimations() async {
  final image = await game.images.load('SPRITE_SHEET.png');
  final sheet = SpriteSheet(image: image, srcSize: Vector2(32, 32));

  animations = {
    HeroState.idle: sheet.createAnimation(
      row: 0,
      stepTime: 0.15,
      from: 0,
      to: 6,
      loop: true,
    ),
    HeroState.run: sheet.createAnimation(
      row: 1,
      stepTime: 0.10,
      from: 0,
      to: 8,
      loop: true,
    ),
  };
}

④ 在 update() 中根据输入切换状态

dart 复制代码
@override
void update(double dt) {
  super.update(dt);

  final joy = game.joystick;

  if (joy.direction == JoystickDirection.idle) {
    _setState(HeroState.idle);
  } else {
    _setState(HeroState.run);
    position += joy.relativeDelta * speed * dt;
  }
}

至此,角色的动作已经可以随着摇杆移动自动切换动画,看起来行走更加自然。

但是当 摇杆方向角色当前朝向 时,角色看起来就像 倒退行走,这显然不符合认知习惯。

3. 分析摇杆方向,实现左右翻转

一个横版 RPG/ARPG 中,角色通常使用 同一套竖直方向帧 ,需要通过 水平翻转 来表现 朝左/朝右

Flame 提供了翻转方法:flipHorizontally()

① 具体翻转方法

dart 复制代码
bool facingRight = true;

void _faceRight() {
  if (!facingRight) {
    flipHorizontally();
    facingRight = true;
  }
}

void _faceLeft() {
  if (facingRight) {
    flipHorizontally();
    facingRight = false;
  }
}

② 根据摇杆方向翻转角色

dart 复制代码
if (joy.relativeDelta.x > 0) {
  _faceRight();
} else if (joy.relativeDelta.x < 0) {
  _faceLeft();
}

通过判断摇杆 relativeDelta.x 的正负值,就可以确定角色应该面向的方向:

  • relativeDelta.x > 0 时,摇杆向右,角色应面向右方
  • relativeDelta.x < 0 时,摇杆向左,角色应面向左方
  • relativeDelta.x == 0 时,角色水平朝向保持不变

这种方法非常简单高效,无需计算角度,在角色移动时自动翻转,避免了 倒退行走

五. 总结与展望

总结

本章主要介绍了 Flutter&Flame 开发 2D像素游戏 关于 主角人物 的基础实践。

通过上述步骤,我们完成了一个像素风游戏角色的搭建与移动控制,主要包括了以下内容:

  • 角色与动画 :使用精灵图 (SpriteSheet) 创建角色,支持 idle/run 等动画状态切换。
  • 玩家交互:通过摇杆控制角色移动,并根据方向翻转动画。

展望

之前尝试的Demo预览

  • Tiled 中制作专属地图,包含不同层级和碰撞区域。

  • 在 Flutter 中加载地图,并完善碰撞逻辑。

  • 实现相机跟随玩家移动。

  • 完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。

  • 实现怪物生成、自动攻击与玩家碰撞逻辑。

  • 支持局域网多玩家联机功能。

🚪 github 源码
💻 个人门户网站

相关推荐
wanhengidc1 小时前
云手机 多端互通 科技
运维·服务器·科技·游戏·智能手机
kirk_wang1 小时前
Flutter插件在鸿蒙端的开发与部署:跨生态桥梁的架构与实现
flutter·移动开发·跨平台·arkts·鸿蒙
小猪努力学前端2 小时前
基于PixiJS的小游戏广告开发
前端·webgl·游戏开发
勇气要爆发4 小时前
【第五阶段—高级特性和框架】复杂动画案例分析初体验
flutter
勤劳打代码5 小时前
追本溯源 —— SetState 刷新做了什么
flutter·面试·性能优化
星空露珠7 小时前
lua获取随机颜色rgb转换hex
数据结构·数据库·算法·游戏·lua
松☆7 小时前
OpenHarmony 后台任务与 Flutter 生命周期协调:构建稳定可靠的混合应用
flutter
松☆7 小时前
Flutter 与 OpenHarmony 深度集成:自定义 MethodChannel 插件开发全指南
flutter·wpf