五邑隱俠,本名關健昌,10年游戲生涯,現隱居五邑。本系列文章以TypeScript為介紹語言。
這一篇介紹瓦片地圖,在開發模擬經營類游戲、SLG類游戲、RPG游戲,都會使用到瓦片地圖。瓦片地圖地面是通過一個個地磚拼起來的,又分為45度角和90度角兩種。45度角俗稱2.5D,每個格子都是菱形,而90度角每個格子都是正方形。
瓦片地圖一般包括以下圖層(不一定同時存在,例如一般RPG游戲沒有背景和自由裝飾層):
1.背景層(大圖拼接的背景)
2.地形層(瓦片格子拼接的地形)
3.建築層(按瓦片格子擺放的建築、地面物品、角色)
4.自由裝飾層(雲朵、煙霧)
除此以外,還需要提供隱藏的層用於編輯數據,控制游戲邏輯,例如阻擋、擺放區域等,稱為數據層。其中需要瓦片布局的有3種圖層:地形層、建築層、數據層。
先來說說背景層BgLayer,背景層在美術設計里是張完整的大圖,為了避免連續內存太大導致加載失敗,把大圖打碎成等大小的小圖拼接。一般出於性能考慮,小圖長寬選擇為1024、512、256、128。
設置背景需要提供背景的小圖數組,列數,每張小圖的寬高
public setPieces(fileArr: Array<string>, colCount: number, pieceW: number, pieceH: number, loadAtOnce: boolean = true): void {}
如果是在H5平台,要考慮資源的逐步加載。所以在H5平台該方法最好只是做占位,提供方法進行資源加載
public loadPiece(file: string) {}
除了加載外,由於玩家在移動地圖時只看到背景的一部分,不在視窗內的應該裁剪掉,所以還要提供方法對背景進行裁剪
public setViewPort(x: number, y: number, w: number, h: number): void {}
完全不在視窗的小圖,通過removeFromParent從渲染列表移除。
接下來說說基於瓦片布局的圖層。基於瓦片的圖片我們會擺放一些物品,例如地形層的地形塊、建築層的建築、地面物品、角色。所以在介紹瓦片布局的圖層前,先介紹下地圖里的物品。一般包括:
1.地形塊
2.建築、地面物品
3.角色
4.編輯數據
先設計一個基類MapItem,定義物品的基本屬性:格子位置、寬高占格子數、是否可穿越、可否編輯
export class MapItem extends cc.Component { protected mGridX: number = 0; protected mGridY: number = 0; protected mRow: number = 1; protected mCol: number = 1; protected mCanPass: boolean = true; private mEditable: boolean = true; private mIsLock: boolean = false; }
地形塊主要是顯示一張一格大小的圖片,一般會先把這些地形圖片合成一張大圖,按照格子大小等分。設計一個TileSet類定義這張合圖的結構
export class TileSet { private mId: number = -1; private resPath: string = null; private tex: cc.Texture2D = null; private gridW: number = 0; private gridH: number = 0; private row: number = 0; private col: number = 0; }
每個地形塊Tile引用TileSet里的資源
export class Tile extends MapItem { private mSpr: cc.Sprite = null; private mId: number = 0; private mTileSetId: number = 0; private mIdx: number = 0; }
當然,如果希望地面有幀動畫,也可以讓Tile保存一個幀系列,按順序切換mSpr的spriteFrame屬性。
建築要考慮建築、地面物品的方向和操作
export class MapBuilding extends MapItem { protected mId: number = 0; protected mType: number = 0; protected mResType: number = 0; // 0 image, 1 spine protected mResPath: string = null; protected mDir: number = 0; protected mReverse: boolean = false; }
為了區分擺放和旋轉點,我會給MapBuilding添加兩個子節點,因為在45角地圖擺放建築時,一般以右下角格子做擺放參考點,但是旋轉是以左上角格子為參考。
需要注意,在H5平台,建築的圖片資源應該是動態加載的,在地圖移動時逐漸加載顯示。所以要提供接口對資源進行加載。
public loadRes(): void {}
還有點擊范圍,圖片是矩形的,點擊的時候可能是點到了圖片的空白區域,這時后玩家看到的是點擊了后面的建築,如果沒有處理透明區域剔除,實際判斷卻是點擊了前面的建築。對於圖片資源的建築在H5平台,可以通過使用該圖片相同位置,截取點擊區的一個像素,模擬畫在屏幕該地方,如果getImageData獲取的data[3]不是0,說明點擊到了該建築,否則是點擊了空白區。
角色都是spine動畫,是可以運動的物體,需要路徑、速度相關的屬性
export class MapActor extends cc.Component { protected mActorId: number = 0; protected mResPath: string = null; protected mRoadGrids: Array<cc.Vec2> = []; // 路徑 protected mCurRoadIdx: number = 0; protected mSpeed: number = 0; // 設置的行走速度, 跟x方向速度絕對值一樣 protected mSpeedX: number = 0; // 實時行走X速度 protected mSpeedY: number = 0; // 實時行走Y速度 protected mTestActor: boolean = false; protected mIsKeepRoad: boolean = false; protected mIsPauseWalk: boolean = false; }
編輯數據是不可見的,只有基本的位置信息和相應數據
export class MapDataItem extends MapItem { private data: any = null; }
瓦片布局的圖層有一個基類,負責物品MapItem的添加、刪除、查詢等操作
/** * 基於地磚的圖層基類 * 定義基於地磚圖層基本數據和接口 */ export abstract class TileBaseLayer extends cc.Component implements GestureListener { protected mRow: number = 0; protected mCol: number = 0; protected mGridW: number = 0; protected mGridH: number = 0; protected mMapItems: Array<MapItem> = []; protected mOriX: number = 0; protected mOriY: number = 0; }
對於90度角的地圖,沒有太多復雜的地方需要處理。點擊點到格子的映射是簡單除以寬、高。遮擋是從左往右,從上到下設定zorder。
對於45度角的地圖,點擊點到格子的映射需要用到解析幾何
每個點擊點,按照格子寬高比的斜率經過該點作直線,到經過點(oriX,oriY)平行於x軸的直線都有交點,而且交點的距離等於格子的寬度。所以,交點相對於點(oriX,oriY)的距離(x - oriX)可以判斷該點屬於哪行或哪列。對於行,交點可以通過(oriY - touchY)* (gridW / gridH) - (touchX - oriX)。對於列,交點可以通過(touchX - oriX) + (oriY - touchY)* (gridW / gridH)。(平行、三角形中同樣大小的角的對邊比鄰邊比例一樣)
角色起始在格子中心點,行走的時候,按照格子的寬高比,把速度分解為x、y兩個方向,這樣角色就能沿着格子走下去。每次走到格子中心時才做站立、繼續行走的處理。
遮擋方面,45度角依然是從上到下遮擋
要注意的是,建築占多格,而設置遮擋時只能設置zorder,所以采用的是中心位置方式,把建築中心格子作為zorder的參照。角色在地圖中行走的時候,按上圖的規則修改角色的zorder,建築的zorder一直不變。
自由裝飾層沒有擺放限制,可以放置雲朵、霧等動畫,增加層次感。
瓦片地圖先說到這里,下一篇我們將介紹A*尋路算法。