游戏开发中的长地图和摄像机
在一些横版游戏中,需要搭建超过一个屏幕的长地图,然后控制角色在整个地图中进行移动。本文主要讨论通过 tiled map 实现长地图场景,然后通过控制摄像机的方式访问整个地图。
瓦片地图
什么是瓦片地图
某些游戏会存在超过屏幕大小的背景地图,如果通过切换背景图的方式来切换背景,需要提前准备很多背景图,这些图片体积较大,会造成资源浪费。
2d 游戏是由一张张图片构成,图片上总是有相同部分,把这些相同的,可以重复利用的部分看成盖房子的瓦片。这样只需要利用重复拼接,就能创造出各类图片。
瓦片地图是由一张一张的正方形小图片拼接成的地图,把不同的瓦片拼接在一起,就可以组成完整的地图,例如炸弹人,QQ 堂都是非常典型的瓦片游戏。
tiled是一款比较流行的制作瓦片地图的编辑工具,下面介绍 tiled 的一些基础概念
- 图块资源,导入一张图片,可以选择图片上的部分图片作为瓦片图块
- tsx 文件,引用图块资源的 xml 文件,注意这里不是 react 的 tsx 文件,而是一个 xml 描述文件
- tmx 文件,引用 tsx 的地图描述文件,其本质是一个三维数组,第一维是图层,第二三维就是描述地图的二维数组,每个元素使用贴图 id 填空
- 层
- 图层,图层主要用于隔离不同层级的贴图,方便管理和层叠限制;在同一个图层中,一个方块只能放置一张贴图
- 对象层,主要用于需要进行移动、变换形态、显示或隐藏等逻辑的元素,可以通过 API 得到对应对象
其出入是图块资源文件,输出是 tsx 和 tmx 文件,最后将这三个文件放在游戏项目下,就可以通过 tmx 在游戏中渲染出对应的地图了
创作瓦片地图
下面收集了一些介绍一些 tiled 教学视频,需要多加练习
- bilibili | 如何使用 Tiledmap 绘制地图
- TiledMap简单使用
- 如何使用cocos2d制作基于tile地图的游戏教程:第一部分,这个系列教程虽然有点老了,但是值得一读
- 官方文档
- Tiled Map Editor Tutorial Series
在瓦片地图中甚至可以制作一些简单的帧动画,比如地图上面飘曳的火把、闪烁的灯光等等,减少在游戏引擎中的开发工作。
在制作时需要注意当前层,以及及时保存等习惯,否则可能会将图片放置在错误的图层上面,最后通过 api 获取到的图块异常,肉眼却看不来是什么问题。
也可以通过使用查看tsx
和tmx
等 xml 文件的内容,来排查最后产出的地图文件是否符合预期,
在 cocos 中使用瓦片地图
参考
- 文档 | 瓦片图资源(TiledMap)
- 文档 | TiledMap 组件参考
- Cocos Creator 配合 Tiled 地图的使用
- 如何使用 Tiledmap 绘制地图?从零开发 ARPG 地下城系列 Cocos Creator 教程
下面是访问瓦片地图中某个层某个瓦片单元格的demo代码
export default class NewClass extends cc.Component {
@property(cc.TiledMap)
map: cc.TiledMap = null;
onLoad() {
this.initMap();
}
initMap() {
// 获取瓦片宽度
let tiledSize = this.map.getTileSize();
// 通过layerName获取地图中的层,layerName是在tiled中制作地图时声明的
let layer = this.map.getLayer("ground");
// 获取瓦片地图的尺寸,单位是个
let layerSize = layer.getLayerSize();
for (let i = 0; i < layerSize.width; ++i) {
for (let j = 0; j < layerSize.height; ++j) {
// 依次获得对应layer的每一个瓦片单元格
let tiled = layer.getTiledTileAt(i, j, true);
console.log(tiled.x, tiled.y) // x === i, y ===j, 以左下角为原点
// 每个瓦片都有全局唯一id,被称作gid,空瓦片的gid为0
if (tiled.gid !== 0) {
tiled.node.group = "ground";
// 获取到tiled.node之后,就可以执行各种操作了,如添加物理组件、碰撞逻辑等
// 比如常用到的地面碰撞检测
}
}
}
}
}
需要注意的是tiled的坐标问题
对于单个瓦片而言,在瓦片地图存在好几个坐标
- 瓦片坐标,也就是上面遍历
layer.getTiledTileAt
传入的i、j,在直角地图中左下角为原点 - 节点坐标,相对地图layer锚点作为原点的本地坐标系
为了更直观的确定瓦片的坐标,我常用的做法是将tilemap节点的锚点设置为0,0,这样就跟getTiledTileAt
一致了,这样也可以很方便地获取某个瓦片的本地坐标和世界坐标
比如我们要将某个节点放在0,0坐标的瓦片对应的位置上
// 当瓦片分辨率比价低的时候,可能会通过对map节点进行scale放大来增加分辨率
const scale = this.map.node.scale
const tileSize = this.map.getTileSize();
const layer = this.map.getLayer("ground");
const tiled = layer.getTiledTileAt(0, 0, true);
// 世界坐标转换
const pos = tiled.node.parent.convertToWorldSpaceAR(tiled.node.position)
const localPos = this.hero.node.parent.convertToNodeSpaceAR(pos)
// 锚点偏移量
localPos.x += tileSize.width * scale / 2
localPos.y += tileSize.height * scale / 2
this.hero.node.position = localPos
cocos creator 摄像机
参考
基础知识
什么是摄像机?说的直白些,摄像机就是游戏中我们能看到的游戏画面,模型、特效、动画等都是通过摄像机展现给我们的
- 摄像机的显示范围是以摄像机为中心,以屏幕大小为范围, 绘制物体,然后成像到屏幕上
- 可以通过 Camera 组件的
culling Mask
来设置当前摄像机需要拍摄的分组, 如果勾选上相关分组,说明当前摄像机会拍摄对应分组的这些物体, 不在物体类型中的物体不会被摄像机绘制 - 如果存在多个摄像机,每个摄像机画面先后都会被绘制到屏幕上,最终屏幕显示的是多个摄像机叠加后的结果,可以通过 Camera 组件的
depth
来决定绘制顺序,depth 小的先绘制,大的后绘制。
手动渲染摄像机内容
除了 main camera 之外,还可以手动添加额外的 camera 节点,用于渲染指定分组的节点。
摄像机有一个比较特殊的属性targetTexture
,如果设置了 targetTexture,那么摄像机渲染的内容不会输出到屏幕上,而是会渲染到 targetTexture 上。
因此,如果想把摄像机的渲染内容手动绘制到屏幕上,其大致实现为
// 构建一个空的texture
const texture = new cc.RenderTexture();
texture.initWithSize(this.sp_camera.node.width, this.sp_camera.node.height);
// 使用texture构建一个spriteFrame,方便某个Sprite节点使用
const spriteFrame = new cc.SpriteFrame();
spriteFrame.setTexture(texture);
// 用来承接spriteFrame并展示的节点
this.sp_node.spriteFrame = spriteFrame;
// 设置相机的targetTexture,这样相机渲染的内容会绘制到texture,通过spriteFrame在sp_node上展示
this.camera.targetTexture = texture;
移动摄像机
添加了 camera 节点之后,也可以通过移动节点的方法来移动摄像机
update(){
// todo 计算x
this.camera.node.x = x
}
一些可以运用的场景
- 在游戏开始时平移浏览全部场景中的敌人分布,如战棋游戏等
- 一些横版游戏的背景移动,如飞机大战、跑酷等
摄像机跟随角色
参考
在很大的地图场景中,移动角色的同时还需要移动背景等元素,更常见的做法是让摄像机跟随玩家操作的角色,使玩家看起来像在地图中移动一样
具体步骤为,
- 将背景与角色放在同一个分组中,将相机的 cullingMask 设置为为该分组
- 如果元素较少,直接使用 Main Camera 相机和 default 分组即可
- 如果有其他不同渲染逻辑的节点(比如操作按钮是固定在屏幕上面不会动的),可以使用单独的分组和摄像机来管理
- 在角色移动的时候,同时更新对应相机的 node.position 即可
- 在期间会遇见一些问题,比如角色的坐标可能会超出屏幕,因此这里需要用到摄像机运镜技术。
下面是一个简单的例子
如下图构建节点树
大概的场景界面为
然后编写脚本组件
@ccclass
export default class NewClass extends cc.Component {
@property(cc.Node)
player: cc.Node = null;
@property(cc.Camera)
mainCamera: cc.Camera = null;
@property(cc.Camera)
uiCamera: cc.Camera = null;
dir: 1 | -1;
start() {
this.dir = 1;
}
// 点击前进按钮
moveFoward() {
this.dir = 1;
}
// 点击后退按钮
moveBack() {
this.dir = -1;
}
updateCameraPosition() {
const pos = this.player.position;
const screenW = this.node.width;
if (this.dir === 1) {
pos.x = Math.max(Math.min(screenW * 0.5, pos.x), screenW * -0.5);
} else {
pos.x = Math.min(Math.max(screenW * -0.5, pos.x), screenW * 0.5);
}
this.mainCamera.node.position = pos;
}
update(dt) {
this.player.x += dt * this.dir * 300;
this.updateCameraPosition();
}
}
这里运用了一个简单的运镜,当角色移动到屏幕边缘时,就不会更新摄像头位置了,这样可以避免旁边出现黑边。
关于摄像机运镜,在查阅资料的时候发现这篇文章Scroll Back:2D 横版游戏摄像机运镜原理与实践,写的真的是太好了
小结
本文整理了tiledmap的一些基础知识,了解如何使用tiledmap搭建游戏地图。然后介绍通过移动摄像机的方式浏览长地图。
最近一直在折腾相关的功能点,代码写的比较草率,后面继续完善。
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。