App Store 曾經將 io 遊戲列為「值得關注的四個趨勢」之一。io 類遊戲在國內又被稱為休閒競技類遊戲、大作戰遊戲、大亂鬥遊戲等,以操作規則簡單、多人在線對抗、死後即刻復活等為特點,曾經風靡一時的《球球大作戰》、《貪吃蛇大作戰》都屬於這一類型,廣受玩家喜愛。
Cocos 引擎官方於上周正式推出一款 io 類玩法遊戲源碼《奔跑吧小仙女》。本項目源碼包裡包含了完整策劃文檔、項目源碼、美術源文件,且官方支持免費提供微信小遊戲上線源碼授權服務。
遊戲中角色會自動前進,玩家需要滑動屏幕控制角色移動的方向,通過收集木板來跨域水域達到抄近路的目的。遊戲一共5人參與,最快到達終點的玩家即為勝利。
《奔跑吧小仙女》在不使用物理的基礎上進行開發。同時,我們也配置了完整的教程,包括如何啟動場景、攝像機跟隨、水面和天空球的設置、玩家與 AI 的控制,以及如何加載數據,相關配置文件等。除此之外我們還有地圖編輯器,供玩家擴展使用。
今天主要從以下四個方面對本項目進行關鍵技術點解析:
遊戲場景與地圖生成
玩家控制與 AI
地圖編輯器
性能優化
《奔跑吧小仙女》設定在一個水上樂園,為這美麗的環境,我們給場景中添加了天空球和水面效果。
我們只需要簡單的圖片和簡單的模型就能實現好看的效果!
天空球由以下組成:
球模型
一張貼圖
一個旋轉動畫
一個 builtin-unlit 無光照材質
平面模型(四方形)
噪聲貼圖(用於顯示水紋)
深度貼圖(控制水的顏色,中心到邊緣的變化)
一小段 shader 代碼
頂點偏移動畫,上下起伏:
#if USE_WAVE
vec3 offset;
vec3 tangent;
vec3 bitangent;
gerstnerWaves(worldPos.xyz, waveVisuals.xyz, waveDirections, offset, v_normal, tangent, bitangent);
worldPos.xyz += offset;
#if USE_NORMAL_MAP
v_tangent = tangent;
v_bitangent = bitangent;
#endif
#endif
水基礎顏色:
// color
vec4 waterColor = shallowColor;
計算水紋:
// caustic
vec4 finalCausticColor = vec4(0.);
#if USE_CAUSTIC
float causticDepth = causticParams2.x;
vec3 causticColor = causticParams2.yzw;
finalCausticColor.rgb = caustic() * causticColor;
#endif
計算深度顏色:
#if USE_DEPTH
float waterDepth = texture(surfaceWaterDepth, v_uv).r;
float depth = clamp(1. - waterDepth / depthMaxDistance, 0., 1.);
vec4 depthColor = mix(depthGradientShallow, depthGradientDeep, depth);
waterColor = alphaBlend(depthColor, waterColor);
#endif
最終顏色:
// final
vec4 finalColor = waterColor + finalFoamColor + finalCausticColor;
地圖在設計時採用了地圖塊的方式。先預設幾個地圖塊,接著根據配置表中的地圖塊的位置縮放等信息,生成完整地圖。
// 根據配置表信息布置地圖塊
ndItem.position = gameUtils.setStringToVec3(itemData.position);
ndItem.scale = gameUtils.setStringToVec3(itemData.scale);
ndItem.eulerAngles = new Vec3(0, Number(itemData.eulY), 0);
小仙女的跑動怎麼能少了攝影師?為了能更好的拍攝小仙女的運動,這裡設計了跟隨相機。
只需小小的 lookAt 加上平緩的插值預算,相機跟隨如你所願!
/**
* 移動攝像機位置/角度 每幀運行
* @param lerpPosNum 坐標修改的lerp參數
* @param lerpEulNum 角度修改的lerp參數
*/
private followTarget(lerpPosNum: number, lerpEulNum: number ) {
// 目標節點的位置
const targetPos = this.ndTarget.getPosition();
// 目標節點在y軸的旋轉
const eulerY = this.ndTarget.eulerAngles.y;
const _quat = Quat.fromEuler(new Quat(), 0, eulerY, 0);
// 相對目標節點的位置y旋轉矩陣,用於偏移向量轉到該坐標系
const _mat4 = Mat4.fromRT(new Mat4(), _quat, targetPos);
// 相機位置偏移向量
v3_pos.set(this.offsetPos);
// 求出在目標節點坐標系的偏移向量
v3_pos.transformMat4(_mat4);
// 求一個插值
v3_selfPos.lerp(v3_pos, lerpPosNum);
// 設置相機的位置
this.node.position = v3_selfPos;
// 求出lookAt的目標點坐標
v3_pos.set(targetPos.add(this.offsetLookAtPos));
// 求一個插值
v3_look.lerp(v3_pos, lerpEulNum);
// 設置相機 lookAt
this.node.lookAt(v3_look);
}
小仙女目前是在一個水平面奔跑的,所以小仙女的移動位置可以根據速度和角色朝向去控制。
// 下落移動
this._nowSpeedY += gameConstants.ROLE_GRAVITY_JUMP * dt;
pos.y += this._nowSpeedY * dt;
// 前進移動
let speed = dt * this._speed;
const eulYAngle = eul.y * macro.RAD;
const addX = speed * Math.sin(eulYAngle);
const addZ = speed * Math.cos(eulYAngle);
pos = pos.subtract3f(addX, 0, addZ); //角色前進方向為當前朝向的反向
this.node.setPosition(pos);
因為有些路塊的形狀比較特殊,我們為小仙女和路塊添加了碰撞觸發器。可以通過設置分組與掩碼來控制觸發器的觸發。
https://docs.cocos.com/creator/3.0/manual/zh/physics/physics-group-mask.html
//只要以下條件為真就會進行檢測
//(GroupA & MaskB) && (GroupB & MaskA)
//碰撞分組/掩碼
COLLIDER_GROUP_LIST: {
DEFAULT: 1 << 0,
PLAYER: 1 << 1,
FLOOR: 1 << 2,
AI: 1 << 3,
},
// 設置地板的 分組,掩碼
let colliderList = ndItem.getComponents(Collider)!;
for (let j = 0; j < colliderList.length; j++) {
colliderList[j].setGroup(gameConstants.COLLIDER_GROUP_LIST.FLOOR);
colliderList[j].setMask(gameUtils.getAiAndPlayerGroup());
}
// 設置角色分組,掩碼
const rbAi = this.node.addComponent(RigidBody);
rbAi.setGroup(gameConstants.COLLIDER_GROUP_LIST.AI);
rbAi.setMask(gameConstants.COLLIDER_GROUP_LIST.FLOOR);
小仙女是否到達終點和腳下的路面的信息都是根據觸發器的事件去記錄。
// 觸發器事件
collider.on('onTriggerEnter', this._triggerEnter, this);
collider.on('onTriggerExit', this._triggerExit, this);
_triggerEnter(event: ITriggerEvent) {
if (ndOther.name === gameConstants.CSV_MAP_ITEM_NAME.FINISH_LINE) {
// 到達終點
return;
}
//角色與地面接觸,加入列表
this._onFloorList.push(ndOther);
}
_triggerExit(event: ITriggerEvent) {
if (!event.otherCollider) return;
let ndOther = event.otherCollider.node;
let findIndex = this._onFloorList.indexOf(ndOther);
if (findIndex !== -1) {
//角色離開地面,移除列表
this._onFloorList.splice(findIndex, 1);
}
}
當然,小仙女的主要邏輯採用的是狀態機的模式去控制和設計。
set roleState(state: number) {
this._roleState = state;
// 播放對應的動作
this._aniRole.play(gameConstants.ROLE_STATE_NAME[this._roleState]);
}
AI 小仙女大部分邏輯與玩家控制的小仙女的邏輯相通,與之不同的是,AI是讀取配置,生成一條路徑。
移動時,根據速度計算兩個路徑點間的插值,算出最終位置。
//通過貝塞爾路徑點xz軸移動
this._bezierNowId += dt * this._speed;
let bezierNowId = Math.floor(this._bezierNowId);
if (bezierNowId >= this._bezierList.length - 1) {
this._isOver = true;
// 到達終點
return;
}
if (bezierNowId !== this._bezierlastId) {
// 處理朝向
this._bezierlastId = bezierNowId;
const sub = this._bezierList[bezierNowId].clone().subtract(this._bezierList[bezierNowId + 1]);
this._nextEul.set(0, Math.atan2(sub.x, sub.y) * macro.DEG, 0)
}
// 插值
const subIndex = this._bezierNowId - bezierNowId;
this.node.setRotationFromEuler(this._nextEul);
const nextPos = this._bezierList[bezierNowId].clone().lerp(this._bezierList[bezierNowId + 1], subIndex)
this.node.setPosition(nextPos.x, pos.y, nextPos.y);
this._checkSpeed(dt);
上面提到了地圖配置,AI 路徑配置,這些並不是憑空去配置的。
為此在 Cocos Creator 編輯器中,專門添加了一個 map.scene 場景,為策劃提供可視化的配置。
策劃只需要在指定節點編輯地圖塊(或 AI 位置),點擊導出按鈕即可。
程序根據節點的信息,生成對應的配置數據。
//當前項目文件路徑
const projectPath = window.cce.project as string;
projectPath.replace("\\", " / ");
const filePath = `${projectPath}/` + MAP_PATH;
// 一鍵引入文件操作
const fs = require('fs');
//關卡數據處理
let data = MAP_DATA_FIRST + '';
for (let i = 0; i < this.node.children.length; i++) {
let ndItem = this.node.children[i];
//坐標/大小/旋轉均以最多兩位小數存儲
const pos = this._getNumberToFixed2(ndItem.getPosition());
const scale = this._getNumberToFixed2(ndItem.getScale());
const eulY = ndItem.eulerAngles.y;//this._getNumberToFixed2(ndItem.eulerAngles.clone());
//生成sting型數據 數據之間以,隔開 在最後加上換行\n
let itemData = `${i + 1},${ndName},${pos},${scale},${eulY}` + '\n';
data += itemData;
}
// 寫文件
fs.writeFile(filePath + MAP_PREFIX + this.mapNameSave + '.csv', data, (err: Error) => {
//...
});
// 讀文件
const path = `${projectPath}/` + MAP_PATH + MAP_PREFIX + this.mapNameLoad + '.csv';
fs.readFile(path, 'utf-8', (err: Error, data: any) => {
//...
})
對於還不需要使用的碰撞體,並且會與多個分組發生碰撞,產生計算的模型。可先暫時關閉模型上的碰撞體,根據距離判斷模型是否需要開啟碰撞體。亦或是,節省不必要的碰撞體,使用距離計算,適用於場景中的道具類型物品。
當前項目中:
分以下幾步處理:
1、計算當前z對應的磚塊所在區間(例:當前將所有磚塊根據z軸的距離1進行劃分);public static checkNowBrickIndex(posZ: number) {
//對當前坐標z值進行對gameConstants.BRICK_CAN_GET_INTERVAL取餘並四捨五入取整
return Math.abs(Math.floor(posZ / gameConstants.BRICK_CAN_GET_INTERVAL));
}
let nowRow = gameUtils.checkNowBrickIndex(pos.z);
if (!GameManager.canGetBrickList[nowRow]) {
//判斷是否不存在 不存在則需要聲明為數組
GameManager.canGetBrickList[nowRow] = [];
}
GameManager.canGetBrickList[nowRow].push(ndNowBrick);
const pos = this.node.getPosition();
let index = gameUtils.checkNowBrickIndex(pos.z)
let nowBrickList = GameManager.canGetBrickList[index];
if (!nowBrickList) return;
for (let i = nowBrickList.length - 1; i > -1; i--) {
//進一步判斷當前磚塊與主角的距離是否拾取
}
update(){
const num = 3; //間隔num幀執行一次方法
//director.getTotalFrames() 獲取 director 啟動以來遊戲運行的總幀數
if (director.getTotalFrames() % num === 0) {
//執行相應操作
}
}
https://store.cocos.com/app/detail/3126
https://store.cocos.com/document/zh/
《Creator 3.x <奔跑吧小仙女> 3D源碼分析與實戰》
https://bycwedu.vipwan.cn/course/56/Creator-3-x-ben-pao-ba-mei-shao-nv-3D-yuan-ma-fen-xi-yu-shi-zhan
感謝社區大神「博毅創為」Blake 老師的激情爆肝!視頻教程共9課時,全免費,現已全部上線。
《奔跑吧小仙女》目前正在 Cocos Store 及 Cocos 官方微店同步熱賣中,點擊【閱讀原文】即可跳轉查看詳情。
另外,8月20日中午12:00,我們將為評論區留言點讚前3名送出《奔跑吧小仙女》全套源碼~