
前言
小时候的我们,都有一个小小的游戏梦
什么 植物大战僵尸、红警、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 图形,都是基于精灵图构建的。
Sprite是 2D 游戏 显示的 基础单位 ,而精灵图则是让这些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(),
),
);
}
- GameWidget :
- 将
MyGame游戏实例嵌入 Flutter Widget 树 - 自动处理:
- 每帧刷新 (
update+render) - 输入事件(触摸、手势、键盘)
- 屏幕尺寸变化 (
onGameResize)
- 每帧刷新 (
- 将
- 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);
}
}
-
FlameGame:
- Flame 的基础游戏类
- 自动管理组件 (
children) - 自动处理渲染循环
-
生命周期方法:
-
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. 初次定位与屏幕适配
-
初次进入游戏时居中:
dartposition = game.size / 2; -
屏幕尺寸变化时重新居中:
dartvoid onGameResize(Vector2 size) { position = size / 2; }
场景变化(横屏、竖屏、窗口大小变化)后,主角仍保持在屏幕中心。
四. 随心所动
人物渲染出来后,最核心的交互能力就是 移动 。
一个完善、自然、流畅的移动系统通常包含下面 三个关键步骤:
- 输入控制:捕获玩家操作(摇杆/键盘/手势)
- 状态机驱动动画:根据移动状态切换对应动画
- 方向控制:角色自动朝向移动方向(翻转)
下面将一步步完善这三个部分。
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 中加载地图,并完善碰撞逻辑。
-
实现相机跟随玩家移动。
-
完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。
-
实现怪物生成、自动攻击与玩家碰撞逻辑。
-
支持局域网多玩家联机功能。