
Flutter 勇闯2D像素游戏之路(一):一个 Hero 的诞生
Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图
前言
在上篇文章中,我们基本完成了一个像素风游戏角色的搭建与移动控制。
本篇文章,我们就来看看游戏地图是怎么绘制的。
而绘制 2d地图 我们就不得不借助于 Tiled。
什么是 Tiled ?

Tiled 是一个免费的 2D 地图编辑器(Map Editor),主要用于制作:
- RPG 地图
- 闯关地图
- 迷宫
- 关卡碰撞层
- 动态对象(宝箱、门、怪物刷新点)
- 动画瓦片(如会动的水面、火焰)
在 Flame 中,Tiled 是最常见、最成熟的地图制作方式。
Tiled 的使用教程
1. Tiled 的安装

点击进入 Tiled Map Editor 安装界面,大家选择合适自己的版本安装即可。


安装完成后,按上述步骤即可设置中文。
有些同学,语言中 系统默认 可能就是中文,那么上述步骤就无需设置了。
2. 新建地图参数

点击新建地图之后,就出现了地图弹窗,可以将其分为三个部分:
- 最上面的 地图 部分:一般保持默认参数即可。
- 右边的 块大小 部分:块的宽高一般和你地图素材大小相关。
- 左边的 地图大小 部分:就是以 块 为基本单位,指定地图的大小。
3. 图块集


图块集(Tilemap )在 Tiled 中是用于构建和管理地图的基本元素之一,主要由一组瓦片(Tiles)构成。每个瓦片代表地图中的一个小区域或单元,通常是一个固定大小的图像,可以用来表示不同的地图元素,如地面、墙壁、水体、装饰物等。图块集将这些瓦片组织在一起,通常按纹理图像的方式排列,方便在地图编辑器中选择和使用。通过图块集,我们能够快速构建地图,选择不同的瓦片来创建多样化的地图元素和场景。
(1). 图块集类型
基于整张图块集图片
-
定义:这种图块集类型使用一张大图作为瓦片集的来源,图块集的每个瓦片都来自这张图片的不同部分。
-
特点:
- 纹理图像:所有的瓦片都来自于一张单独的图片,通常是一个大图,包含多个小的瓦片。
- 简单和高效:这种方式比较常见,适用于瓦片数量较少的地图。它可以通过设定瓦片的大小、间隙和边距等参数来从大图中提取不同的瓦片。
- 性能优化:加载时只需要加载一张图像,减少了加载多个图像的开销。
-
应用场景:适用于较简单的地图设计,其中瓦片种类不多,且纹理图像较为统一。
多图片集合
-
定义:这种图块集类型使用多个图片文件来作为瓦片集,每个图片文件代表不同的瓦片组。
-
特点:
- 多个图像文件:每个图块集中的瓦片可以来自不同的图片文件,这样可以根据需要拆分图块集,避免一张图片过大。
- 灵活性:适用于需要更大、更多种类瓦片的地图,能够根据不同的需求拆分多个图像文件进行管理。
- 复杂性:由于使用多个图像文件,可能会增加地图管理的复杂性,但也能使瓦片集的管理更加灵活。
-
应用场景:适用于复杂的地图设计,其中瓦片种类和图块集较多,且每个图像文件大小较大。
(2). 是否嵌入地图
嵌入地图
-
定义 :图块集可以选择是否嵌入到地图文件中,这意味着图块集的图像数据会直接包含在地图文件(如
.tmx)内。 -
特点:
- 便于分发:嵌入地图的图块集会使地图文件包含所有必要的数据,使得地图文件在移动或共享时更加独立,减少了依赖外部资源的需要。
- 增加地图文件大小 :嵌入图块集会增加
.tmx文件的大小,因为图像数据会被编码并存储在地图文件中。 - 适用场景:适合小型项目或当你希望把所有地图数据捆绑在一起时。
-
应用场景:适用于需要将所有地图和图块集数据打包在一个文件中的场景,便于管理和传输。
不嵌入地图
-
定义 :如果不选择将图块集嵌入地图,图块集将作为外部资源存在,即地图文件将引用外部的图块集文件(通常是
.tsx文件)。 -
特点:
- 减少地图文件大小:图像数据存储在外部图块集文件中,地图文件只需要保存图块集的引用,地图文件更小。
- 资源共享:外部图块集可以被多个地图共享,方便资源复用和管理。
- 便于修改:修改外部图块集文件时,所有引用该图块集的地图都会自动更新。
-
应用场景:适用于大型项目或需要多个地图共享相同图块集的场景,便于维护和更新。
(3). 图像参数
透明度
- 定义:透明度设置控制图块集的整体透明程度。
- 功能:通过调整透明度,你可以使图块集在地图上的显示效果更为柔和或透明,常用于创建具有层次感的地图效果。
- 应用:适用于背景图层或需要部分透明效果的地图元素。
块宽度
- 定义:块宽度是每个瓦片的水平像素尺寸。
- 功能:设置图块的宽度决定了每个瓦片的水平尺寸。不同的瓦片大小会影响地图的整体布局和渲染效果。
- 应用:你可以根据实际需要调整瓦片宽度,以适应游戏或地图的设计需求。瓦片的宽度通常与图像的实际尺寸一致,或者是设定的纹理切割方式。
块高度
- 定义:块高度是每个瓦片的垂直像素尺寸。
- 功能:类似于块宽度,块高度设置了每个瓦片的垂直尺寸。调整瓦片的高度对于地图的布局和显示效果同样重要。
- 应用:可以根据需求设定瓦片的高度,通常瓦片的高度和宽度是相同的,但也可以是矩形瓦片。
边距
- 定义:边距是瓦片图像与图像边缘之间的空隙,通常用于设置瓦片之间的间隔。
- 功能:边距设置了每个瓦片与纹理图像边缘之间的距离。它有助于确保图像中的瓦片之间不会出现视觉重叠,尤其是在瓦片集图像的边缘。
- 应用:当图块集的图像有多个瓦片时,设置边距可以避免瓦片之间的像素混叠,提升地图的渲染效果。
间距
- 定义:间距是图块集中的每个瓦片之间的空隙,即瓦片之间的空白区域。
- 功能:间距控制瓦片之间的间隔距离,通常用于分隔不同的瓦片,以确保它们不会相互重叠或对齐不当。
- 应用:适用于确保瓦片集中的瓦片之间有明确的分隔,尤其是在每个瓦片的纹理图像之间有额外的空白时。
4. 图层

在 Tiled 中,图层(Layers)是地图的基本组成部分,用于存储不同的地图元素。每个图层可以包含不同类型的信息,例如瓦片(Tile)、对象(Object)、图形(Graphics)等。
以下是 Tiled 中常见的三种图层类型的解析:
(1). Tile Layer(图块层)
-
定义:最常用的图层类型,包含瓦片的网格布局。每个瓦片代表地图上的一小块区域,通常用于构建地形、背景等。
-
特点:
- 可以使用 Tileset 来定义瓦片集,Tileset 是一组图像,每个瓦片对应一个图像部分。
- 图层中的每个格子都对应一个瓦片,瓦片是基于 Tileset 的索引来标识的。
- 可以设置图层的透明度、可见性等属性。
-
常见应用:地面、墙壁、道路等。
(2). Object Layer(对象层)
-
定义:包含一个或多个对象,每个对象可以是任意形状的区域(矩形、圆形、多边形等),每个对象通常可以带有附加的属性(例如,ID、名称、标签等)。
-
特点:
- 每个对象的坐标、大小、旋转角度和其他属性可以自由设置。
- 对象可以不受格子限制,适合存放动态元素,比如敌人、道具、触发区等。
- 可以添加额外的属性,如图片、链接等。
-
常见应用:敌人、NPC、触发器、道具等。
(3). Image Layer(图像图层)
-
定义:此图层使用一个单一的图像(背景或装饰)作为图层内容。
-
特点:
- 图像直接覆盖在地图上,通常不参与瓦片的拼接,而是作为一种背景或装饰。
- 可以控制图像的位置和大小。
-
常见应用:背景图像、装饰性图像等。
图层的其他设置:
- 层顺序:在 Tiled 中,图层的渲染顺序是从下到上的。即,下面的图层会先绘制,上面的图层会覆盖下面的图层。
- 图层属性:每个图层可以设置不同的属性,如可见性、透明度、锁定状态等。
MyHero
一. 本章目标

二. 绘制地图
本项目绘制地图所采用参数,是符合于项目内素材的。
如用不同素材,酌情调整参数。
1. 新建地图

2. 导入瓦片集

选择项目中该素材地址:myhero/lib/assets/image/xxx.png 即可。

3. 绘制地面
首先新建 ground图层。

-
绘制普通地板:
- 在右下角的素材面板中,选择 普通地板 类型的素材。
- 然后点击上方的 喷漆工具。(这个工具允许你快速地在地图上绘制普通地板)
- 在地图区域中点击需要放置地板的地方,这样你就完成了基础地板的绘制。
-
添加特别的想法:
- 先在右下角框选需要使用的素材。
- 点击上方的 印章工具。(印章工具允许你将选择的素材或特殊效果快速应用到地图中)
- 然后,在地图中点击需要放置特殊设计的地方,即可添加。
4. 绘制水池
首先新建 water图层。

重复上一步骤操作,选择素材完成绘制。
5. 绘制墙体
首先新建 wall图层。

重复上一步骤操作,选择素材完成绘制。
6. 绘制杂物
首先新建 other图层。

重复上一步骤操作,选择素材完成绘制。
7. 添加碰撞区
首先新建 Collisions对象图层。

- 点击上方矩形框工具。
- 在地图中框选出墙体。
- 再给每个碰撞区添加属性:
type:collision。
三. 加载地图

1. 配置

yaml
flutter:
assets:
- assets/images/
- assets/tiles/
首先在 项目文件 和 配置文件
pubspec.yaml中正确设置。
2. 代码
dart
// 地图缩放比例
static const double mapScale = 2.0;
// 地图瓦片大小
static const double tileSize = 8.0;
// 加载地图
final realTileSize = mapScale * tileSize;
final tiled = await TiledComponent.load('地牢.tmx', Vector2.all(realTileSize));
world.add(tiled);
这段代码的目的是加载 Tiled 地图并将其添加到 Flame 游戏的世界中。
-
参数
tileSize: 表示每个瓦片的原始宽度和高度。mapScale: 表示每个瓦片在项目中的缩放比例。realTileSize:表示真实应用在项目中的瓦片大小。
-
加载
Vector2.all(tileSize)是一个向量,用于设置地图中每个瓦片的大小。TiledComponent.load()方法是 Flame 引擎中的 Tiled 地图加载器,它会将 Tiled 地图文件(.tmx文件)解析为 Flame 可使用的TiledComponent对象。await用于等待地图加载完成。
-
添加
- Flame 的
World是一个容器,用来管理和更新游戏中的所有组件(如精灵、物体、背景等)。 world.add()将加载的 Tiled 地图组件tiled添加到游戏的世界(World)中。
- Flame 的
四. 相机跟随

1. 必要性
相机跟随 的作用是让玩家角色始终保持在屏幕可视范围内,使地图可以比屏幕更大,同时带来更强的空间感与沉浸感。随着角色移动,画面随之平滑移动,让世界显得更真实、更连贯。
如果没有相机跟随,角色一旦走出屏幕(就像上一步仅仅 加载地图 一样),游戏就无法正常体验,因此它是大多数地图类游戏的 核心机制 之一。
2. 代码
dart
@override
Future<void> onLoad() async {
// 加载游戏资源
super.onLoad();
......
// ---- Camera ----
camera.setBounds(Rectangle.fromLTRB(0, 0, tiled.size.x, tiled.size.y));
camera.follow(hero);
}
这短短两行代码就实现了 相机跟随:
-
camera.setBounds()-
通过
setBounds方法来限制摄像机的移动范围。 -
Rectangle.fromLTRB(0, 0, tiled.size.x, tiled.size.y)创建了一个矩形区域,该矩形的左上角是(0, 0),右下角是tiled.size.x和tiled.size.y,也就是地图的宽高。 -
如此摄像机只能在该矩形区域内移动,确保视角不会超出地图的范围。
-
-
camera.follow(hero);- 这行代码将摄像机与
hero绑定,使得摄像机会一直跟随英雄(HeroComponent)的位置。 - 每当英雄移动时,摄像机会自动调整视角,保持英雄始终在屏幕上的某个位置。
- 这行代码将摄像机与
五. 碰撞区检测

在游戏开发中,碰撞检测是一个常见而又重要的环节,它确保了游戏对象在交互过程中能够正确地反应,本文主要涉及到 墙体碰撞与英雄角色 的交互。
1. 创建墙体组件 (WallComponent)
首先,我们创建了一个 WallComponent,它代表地图中的墙体,并为其添加了一个 RectangleHitbox 来检测碰撞:
dart
class WallComponent extends PositionComponent with CollisionCallbacks {
WallComponent({
required Vector2 position,
required Vector2 size,
}) : super(position: position, size: size);
@override
Future<void> onLoad() async {
await super.onLoad();
add(RectangleHitbox()); // 为墙体添加碰撞检测
}
}
WallComponent类继承自PositionComponent,它需要指定位置(position)和大小(size)。- 通过
add(RectangleHitbox())为墙体添加一个矩形碰撞体,确保我们可以检测到与其它对象的碰撞。
2. 地图碰撞区生成
在加载地图时,我们读取地图的 ObjectLayer(就是我们在Tiled 绘制的 Collisions 对象图层),并将其对象转换为墙体组件,添加到一个 walls 列表中,以便后续使用。
dart
final List<WallComponent> walls = [];
final objGroup = tiled.tileMap.getLayer<ObjectGroup>('Collisions');
if (objGroup != null) {
for (final obj in objGroup.objects) {
final x = mapScale * obj.x;
final y = mapScale * obj.y;
final w = mapScale * obj.width;
final h = mapScale * obj.height;
// 将墙体添加为 WallComponent 组件
final wall = WallComponent(
position: Vector2(x, y),
size: Vector2(w, h),
);
wall.debugMode = true;
walls.add(wall);
await world.add(wall); // 将墙体添加到世界中
}
}
3. 英雄角色与墙体的碰撞检测
接下来,我们就要在 HeroComponent 类中实现了英雄角色与墙体的碰撞检测。
我们之前通过 JoystickComponent 获取用户输入来控制角色的移动,现在我们要检查每一帧是否发生碰撞。
dart
final Set<WallComponent> _nearbyWalls = {}; // 存储与英雄接触的墙体
late RectangleHitbox _hitbox; // 英雄角色的碰撞体
@override
void update(double dt) {
super.update(dt);
final joy = game.joystick;
if (joy.direction == JoystickDirection.idle) {
_setState(HeroState.idle);
return;
}
_setState(HeroState.run);
final movement = joy.relativeDelta * speed * dt;
final originalPosition = position.clone();
// 尝试移动角色
position += movement;
// 检测碰撞
if (_wouldCollideWithWalls()) {
position.setFrom(originalPosition); // 如果发生碰撞,回到原位置
// 通过滑动轴检查X、Y轴方向
position.x += movement.x;
if (_wouldCollideWithWalls()) {
position.x = originalPosition.x;
}
position.y += movement.y;
if (_wouldCollideWithWalls()) {
position.y = originalPosition.y;
}
}
}
bool _wouldCollideWithWalls() {
final heroRect = _hitbox.toAbsoluteRect();
// 检查英雄与墙体的碰撞
for (final wall in game.walls) {
final wallHitboxes = wall.children.query<RectangleHitbox>();
if (wallHitboxes.isEmpty) continue;
final wallRect = wallHitboxes.first.toAbsoluteRect();
if (heroRect.overlaps(wallRect)) return true; // 如果有重叠,表示发生碰撞
}
return false;
}
- 碰撞判断 :通过
_wouldCollideWithWalls()方法判断是否发生碰撞。 - 平滑移动:如果发生碰撞,则将角色位置恢复到原位置,并在 X、Y 轴方向上分别尝试滑动,直到没有发生碰撞为止。
4. 碰撞回调
每当英雄与墙体发生碰撞时,我们可以通过 onCollisionStart 和 onCollisionEnd 方法来处理相关逻辑。
dart
@override
void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is WallComponent) {
_nearbyWalls.add(other);
}
}
@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (other is WallComponent) {
_nearbyWalls.remove(other);
}
}
- 发生碰撞(
onCollisionStart) :将墙体添加到nearbyWalls集合中。 - 碰撞结束(
onCollisionEnd) :将nearbyWalls集合中墙体移除。
💡 小知识 :
组件.debugMode = true
让组件显示调试可视化内容,方便检查碰撞框和位置。开启后,Flame 会在屏幕上绘制:
- 组件的边界框
- 碰撞区(Hitbox)
- 组件中心点与大小轮廓
常用于地图碰撞、角色位置、物体范围等的调试。
六. 总结与展望
总结
本章主要介绍了 Flutter&Flame 开发 2D像素游戏 关于 地图 的基础实践。
通过上述步骤,我们完成了加载地图、在地图中跟随人物 移动相机 和 与地图元素交互 。
截至目前为止,游戏主要包括了以下内容:
- 角色与动画 :使用精灵图 (
SpriteSheet) 创建角色,支持 idle/run 等动画状态切换。 - 玩家交互:通过摇杆控制角色移动,并根据方向翻转动画。
- 地图 :通过
Tiled绘制并在 Flame 中加载的 2d像素地图。 - 碰撞区检测:角色与墙体产生碰撞,并实现平滑侧移。
展望
-
完成攻击与技能系统,包括动画切换、攻击范围和远程弹道。
-
实现怪物生成、自动攻击与玩家碰撞逻辑。
-
支持局域网多玩家联机功能。

之前尝试的Demo预览