游戏地图功能主要分为三块:
地图最基础的做法,是出一张图,然后拖入到场景中;
但这种做法只适合有一张主城地图的界面游戏,若有多张地图,在场景中设置的地图,可能不是游戏中所需的,所以需要动态加载地图。
loadMap(mapId) {
this.mapId = mapId;
let node = new cc.Node()
node.setAnchorPoint(0, 0)
node.parent = this.mapRoot;
this.spMap = node.addComponent(cc.Sprite)
this.stm = new Date().getTime()
let url = "map/" + this.mapId
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
this.spMap.spriteFrame = spf;
console.log("loading cost time:", new Date().getTime() - this.stm, "ms")
})
}
一般地图都会很大,有十几屏,甚至几十屏,为了后续的优化,先将地图切成地图块,然后分开加载,地图纹理的切分工具,参考之前的博文。
loadMap(mapId) {
this.mapId = mapId;
this.loadConfig()
}
loadConfig() {
this.stm = new Date().getTime()
let url = "map/" + this.mapId + "/" + this.mapId
cc.resources.load(url, cc.JsonAsset, (error: Error, jsonAsset: cc.JsonAsset) => {
if (error) {
console.error(error.message || error)
}
this.config = jsonAsset.json
let node = new cc.Node()
node.setAnchorPoint(0, 0)
node.parent = this.mapRoot;
let size = this.config.size
node.width = size[0]
node.height = size[0]
this.ndMap = node;
this.loadTexure()
})
}
loadTexure() {
let cnt = this.config.cnt
let pos
for (let x = 0; x < cnt[0]; x++) {
for (let y = 0; y < cnt[1]; y++) {
pos = [x, y]
this.loadingQueue.push(pos)
}
}
this.loadTile()
}
loadTile() {
if (this.loadingQueue.length === 0) {
this.onLoadFinish()
return
}
let pos = this.loadingQueue.pop()
let tileName = "tile_" + pos[0] + "_" + pos[1]
let url = "map/" + this.mapId + "/" + tileName
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
let node = new cc.Node()
node.setAnchorPoint(0, 0)
node.parent = this.ndMap
node.name = tileName
let tile = this.config.tile
node.x = pos[0] * tile[0]
node.y = pos[1] * tile[1]
let sp = node.addComponent(cc.Sprite)
sp.spriteFrame = spf;
this.loadTile()
})
}
onLoadFinish() {
console.log("loading time:", new Date().getTime() - this.stm, "ms")
}
地图分块之后,需要一个配置文件,保存原始地图的尺寸、地图块尺寸、地图块的总数量等。
单纯将地图分块 IO 耗时会更长,占用的内存也一样,实际上是个负优化。在分块的基础上,根据玩家当前位置,判断哪些地图块可见,只把可见的地图块加载进内存,这样能减少内存的占用。
可视区域如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ncH0e2Ku-1598854597756)(./visible_rect.png)]
ndMapRoot 的大小是通过 widget 设置的四个边距都为0,刚好是屏幕的大小,即 ndMapRoot 的宽高,就是宽高。ndMap 的父节点是 ndMapRoot,通过 ndMap 的坐标 (x, y),可以得到可视区域的坐标为 (-x, -y),所以可见的矩形区域即为 (-x, -y, ndMapRoot.width, ndMapRoot.height)
// 获取可视区域矩形
// mapRoot 的大小通过 widget 设置边距都为 0,所以 mapRoot 刚好就是屏幕的大小
getVisibleRect() {
return new cc.Rect(-this.ndMap.x, -this.ndMap.y, this.mapRoot.width, this.mapRoot.height)
}
// 判断坐标位置的图块是否可见
isTileVisible(x, y) {
let tile = this.config.tile
let tileW = tile[0]
let tileH = tile[1];
let tileBound = new cc.Rect(x * tileW, y * tileH, tileW, tileH);
let visibleBound = this.getVisibleRect();
return tileBound.intersects(visibleBound);
}
loadTexure() {
let cnt = this.config.cnt
let pos
for (let x = 0; x < cnt[0]; x++) {
for (let y = 0; y < cnt[1]; y++) {
if (this.isTileVisible(x, y)) {
pos = [x, y]
this.loadingQueue.push(pos)
}
}
}
this.loadTile()
}
地图分块加载之后,因为加载是异步加载,在移动的过程中,会看到还没有加载出来的地块位置是黑色的。为了解决这个问题,可以首先加载一个小地图纹理,放大作为背景。因为小地图的资源小,加载比较快,但是放大之后会变模糊。在移动的过程中,会呈现出有模糊变清晰的过程。
loadMiniMap() {
let url = "map/" + this.mapId + "/" + this.mapId
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
let miniNode = new cc.Node()
let sprite = miniNode.addComponent(cc.Sprite)
sprite.spriteFrame = spf
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM
miniNode.width = this.ndMap.width
miniNode.height = this.ndMap.height
miniNode.zIndex = 0
miniNode.setAnchorPoint(0, 0)
miniNode.setPosition(0, 0)
miniNode.parent = this.ndMap
this.loadTexure()
})
}
阻挡分为两块:
地图的阻挡信息,是通过把整个地图划分成网格,在网格上标记不同的数值,表示当前网格的地图信息。参考之前的博文
寻路使用的是 A* 寻路,网上有很多教程,之前的博文也有用到。
小地图功能:
在场景中,加载添加一个精灵,作为小地图。在加载地图背景时,已经把小地图加载进游戏,赋值给小地图即可。
可视区域在加载地图块时已经有函数可以获得,只要将获得的可是矩形区域,按比例映射到小地图,即可获得在小地图上的矩形区域,然后通过 Graphics 组件,将矩形区域绘制出来即可,Graphics文档,Graphics 组件和 Sprite 组件,都是渲染组件不能同时出现在一个节点,因此需要再另外创建一个节点。
一般游戏都可以点击小地图进行寻路,可以监听小地图节点的触摸事件,获取到交互节点位置,通过缩放比例,映射的地图的位置,即寻路的目标位置。
loadMiniMap() {
let url = "map/" + this.mapId + "/" + this.mapId
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
// 小地图
this.spMiniMap.spriteFrame = spf
let nd = this.spMiniMap.node
let scale = this.mapRoot.width * 0.2 / this.ndMap.width
nd.width = scale * this.ndMap.width
nd.height = scale * this.ndMap.height
nd.getComponent(cc.Widget).updateAlignment()
// 地图背景节点
let miniNode = new cc.Node()
let sprite = miniNode.addComponent(cc.Sprite)
sprite.spriteFrame = spf
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM
miniNode.width = this.ndMap.width
miniNode.height = this.ndMap.height
miniNode.zIndex = 0
miniNode.setAnchorPoint(0, 0)
miniNode.setPosition(0, 0)
miniNode.parent = this.ndMap
this.updateMiniVisible()
this.loadTexure()
})
}
updateMiniVisible() {
let scale = this.spMiniMap.node.width / this.ndMap.width
if (this.graphic === null) {
let nd = new cc.Node()
nd.setAnchorPoint(0, 0)
nd.setPosition(0, 0)
nd.width = this.spMiniMap.node.width
nd.height = this.spMiniMap.node.height
nd.parent = this.spMiniMap.node
this.graphic = nd.addComponent(cc.Graphics)
this.graphic.strokeColor = cc.Color.RED
nd.on(cc.Node.EventType.TOUCH_START, (event)=>{
let ori = nd.convertToNodeSpaceAR(event.getLocation());
let pos = cc.v2(Math.round(ori.x / scale), Math.round(ori.y / scale))
console.log(pos.x, pos.y) // 输出获取到的坐标
});
}
let rect: cc.Rect = this.getVisibleRect()
this.graphic.rect(
Math.floor(rect.x * scale),
Math.floor(rect.y * scale),
Math.floor(rect.width * scale),
Math.floor(rect.height * scale),
)
this.graphic.stroke();
}
镜头跟随也是有两种方法可以实现移动地图和移动相机两种方案,很多传统的2D游戏,没有镜头的概念,只能通过移动地图的方式实现。当角色移动时,实际上是角色和地图的相对位置发生变化,相对位置发生改变可以修改角色的位置,也可以反向修改地图的位置。角色始终要停留在屏幕的中心位置,角色的位置不能改变,只能反向修改地图的位置。
median(min, value, max) {
return Math.min(Math.max(min, value), max);
}
onActorMove(x, y){
let viewSize = this.mapRoot
this.ndMap.x = this.median(viewSize.width - this.ndMap.width, viewSize.width / 2 - x, 0)
this.ndMap.y = this.median(viewSize.height - this.ndMap.height, viewSize.height / 2 - y, 0)
this.updateMiniVisible()
this.loadTexure()
}
注意不要让地图移除屏幕出现黑边
地图相关同能是一个单独的模块,可以提取成一个单独的组件,方便在其他场景中使用。
const { ccclass, property } = cc._decorator;
@ccclass
export default class MapComp extends cc.Component {
stm: number = 0
mapId: string = ""
config: object = null
ndMap: cc.Node = null
loadingQueue = []
graphic: cc.Graphics = null
spMiniMap: cc.Sprite = null
loadFinishCb: Function = null
loadMap(mapId, cb) {
this.mapId = mapId
this.loadFinishCb = cb
this.createMiniMap()
this.loadConfig()
}
createMiniMap() {
let miniNode = new cc.Node()
miniNode.zIndex = 2
miniNode.setAnchorPoint(0, 0)
miniNode.setPosition(0, 0)
miniNode.parent = this.node.parent
let widget = miniNode.addComponent(cc.Widget)
widget.isAlignTop = true
widget.top = 0
widget.isAlignRight = true
widget.right = 0
this.spMiniMap = miniNode.addComponent(cc.Sprite)
}
loadConfig() {
this.stm = new Date().getTime()
let url = "map/" + this.mapId + "/" + this.mapId
cc.resources.load(url, cc.JsonAsset, (error: Error, jsonAsset: cc.JsonAsset) => {
if (error) {
console.error(error.message || error)
}
this.config = jsonAsset.json
let size = this.config.size
this.node.width = size[0]
this.node.height = size[0]
this.loadMiniMap()
})
}
loadMiniMap() {
let url = "map/" + this.mapId + "/" + this.mapId
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
// 小地图
this.spMiniMap.spriteFrame = spf
let nd = this.spMiniMap.node
let scale = this.node.parent.width * 0.2 / this.node.width
nd.width = scale * this.node.width
nd.height = scale * this.node.height
nd.getComponent(cc.Widget).updateAlignment()
// 地图背景节点
let miniNode = new cc.Node()
let sprite = miniNode.addComponent(cc.Sprite)
sprite.spriteFrame = spf
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM
miniNode.width = this.node.width
miniNode.height = this.node.height
miniNode.zIndex = 0
miniNode.setAnchorPoint(0, 0)
miniNode.setPosition(0, 0)
miniNode.parent = this.node
this.updateMiniVisible()
this.loadTexure()
})
}
updateMiniVisible() {
let scale = this.spMiniMap.node.width / this.node.width
if (this.graphic === null) {
let nd = new cc.Node()
nd.setAnchorPoint(0, 0)
nd.setPosition(0, 0)
nd.width = this.spMiniMap.node.width
nd.height = this.spMiniMap.node.height
nd.parent = this.spMiniMap.node
this.graphic = nd.addComponent(cc.Graphics)
this.graphic.strokeColor = cc.Color.RED
nd.on(cc.Node.EventType.TOUCH_START, (event)=>{
let ori = nd.convertToNodeSpaceAR(event.getLocation());
let pos = cc.v2(Math.round(ori.x / scale), Math.round(ori.y / scale))
this.onActorMove(pos.x, pos.y)
});
}
let rect: cc.Rect = this.getVisibleRect()
this.graphic.clear()
this.graphic.rect(
Math.floor(rect.x * scale),
Math.floor(rect.y * scale),
Math.floor(rect.width * scale),
Math.floor(rect.height * scale),
)
this.graphic.stroke();
}
// 获取可视区域矩形
// mapRoot 的大小通过 widget 设置边距都为 0,所以 mapRoot 刚好就是屏幕的大小
getVisibleRect() {
return new cc.Rect(-this.node.x, -this.node.y, this.node.parent.width, this.node.parent.height)
}
// 判断坐标位置的图块是否可见
isTileVisible(x, y) {
let tile = this.config.tile
let tileW = tile[0]
let tileH = tile[1];
let tileBound = new cc.Rect(x * tileW, y * tileH, tileW, tileH);
let visibleBound = this.getVisibleRect();
return tileBound.intersects(visibleBound);
}
loadTexure() {
let cnt = this.config.cnt
let pos
for (let x = 0; x < cnt[0]; x++) {
for (let y = 0; y < cnt[1]; y++) {
if (this.isTileVisible(x, y)) {
pos = [x, y]
this.loadingQueue.push(pos)
}
}
}
this.loadTile()
}
loadTile() {
if (this.loadingQueue.length === 0) {
this.onLoadFinish()
return
}
let pos = this.loadingQueue.pop()
let tileName = "tile_" + pos[0] + "_" + pos[1]
let url = "map/" + this.mapId + "/" + tileName
cc.resources.load(url, cc.SpriteFrame, (error: Error, spf: cc.SpriteFrame) => {
if (error) {
console.error(error.message || error)
}
let node = new cc.Node()
node.setAnchorPoint(0, 0)
node.parent = this.node
node.name = tileName
node.zIndex = 1
let tile = this.config.tile
node.x = pos[0] * tile[0]
node.y = pos[1] * tile[1]
let sp = node.addComponent(cc.Sprite)
sp.spriteFrame = spf
this.loadTile()
})
}
onLoadFinish() {
console.log("loading time:", new Date().getTime() - this.stm, "ms")
if (this.loadFinishCb) {
this.loadFinishCb()
}
}
median(min, value, max) {
return Math.min(Math.max(min, value), max);
}
onActorMove(x, y){
let viewSize = cc.winSize;
this.node.x = this.median(viewSize.width - this.node.width, viewSize.width / 2 - x, 0)
this.node.y = this.median(viewSize.height - this.node.height, viewSize.height / 2 - y, 0)
this.updateMiniVisible()
this.loadTexure()
}
}
最后,我是寒风,欢迎加入Q群(830756115)讨论。