Flutter 勇闯2D像素游戏之路(二):绘制加载游戏地图

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 游戏的世界中。

  1. 参数

    • tileSize: 表示每个瓦片的原始宽度和高度。
    • mapScale: 表示每个瓦片在项目中的缩放比例。
    • realTileSize:表示真实应用在项目中的瓦片大小。
  2. 加载

    • Vector2.all(tileSize) 是一个向量,用于设置地图中每个瓦片的大小。
    • TiledComponent.load() 方法是 Flame 引擎中的 Tiled 地图加载器,它会将 Tiled 地图文件(.tmx 文件)解析为 Flame 可使用的 TiledComponent 对象。
    • await 用于等待地图加载完成。
  3. 添加

    • Flame 的 World 是一个容器,用来管理和更新游戏中的所有组件(如精灵、物体、背景等)。
    • world.add() 将加载的 Tiled 地图组件 tiled 添加到游戏的世界(World)中。

四. 相机跟随

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);
  }

这短短两行代码就实现了 相机跟随

  1. camera.setBounds()

    • 通过 setBounds 方法来限制摄像机的移动范围。

    • Rectangle.fromLTRB(0, 0, tiled.size.x, tiled.size.y) 创建了一个矩形区域,该矩形的左上角是 (0, 0),右下角是 tiled.size.xtiled.size.y,也就是地图的宽高。

    • 如此摄像机只能在该矩形区域内移动,确保视角不会超出地图的范围。

  2. 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. 碰撞回调

每当英雄与墙体发生碰撞时,我们可以通过 onCollisionStartonCollisionEnd 方法来处理相关逻辑。

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像素地图
  • 碰撞区检测:角色与墙体产生碰撞,并实现平滑侧移。

展望

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

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

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

🚪 github 源码
💻 个人门户网站


之前尝试的Demo预览

相关推荐
程序员老刘1 小时前
千万别再纠结Flutter状态管理,90%项目根本不需要选
flutter·客户端
renxhui1 小时前
Flutter 常用组件全属性说明(持续更新中)
flutter
m0_看见流浪猫请投喂2 小时前
Flutter鸿蒙化现有三方插件兼容适配鸿蒙平台
flutter·华为·harmonyos·flutterplugin·flutter鸿蒙化
top_designer2 小时前
PS 样式参考:3D 白模直接出原画?概念美术的“光影魔术手”
游戏·3d·prompt·aigc·技术美术·建模·游戏美术
雨季6663 小时前
Flutter 智慧物流仓储服务平台:跨端协同打造高效流转生态
flutter
勇气要爆发3 小时前
【第五阶段—高级特性和框架】第十一章:Flutter屏幕适配开发技巧—变形秘籍
flutter
吃好喝好玩好睡好3 小时前
Flutter与Electron在OpenHarmony生态的融合实践:构建下一代跨平台应用
javascript·flutter·electron
ujainu4 小时前
Flutter:在平台博弈中构建跨端开发新生态
flutter
子春一5 小时前
Flutter 测试体系全栈指南:从单元测试到 E2E,打造零缺陷交付流水线
flutter·单元测试·log4j