前言
在 3D 遊戲中,都會有一個主人公。我們可以通過點選遊戲中的其他位置,使遊戲主人公向點選處移動。
那當我們想要實現一個“點選地面,人物移動到點選處”的功能,需要什麼前置條件,並且具體怎麼實現呢?本文帶大家一步步實現人物行走移動,同時進行狀態改變的功能。
一、骨骼動畫
骨骼動畫(Skeleton animation 又稱骨架動畫,是一種計算機動畫技術,它將三維模型分為兩部分:用於繪製模型的蒙皮(Skin),以及用於控制動作的骨架。
一般在 3D 遊戲中的主人公,它的跑步、走路、站立的動作,都是模型檔案的自帶骨骼動畫。
骨骼動畫權重
改變骨骼動畫的權重,可以使得動畫間的過渡更為自然。比如體測時,當你到達終點後,會逐漸減慢速度,跑步動作的幅度越來越小,然後變成走路,最後停止。
讓我們看看一個倆動作權重漸變的例子:
這個例子中,從休閒變到走路,休閒動畫的權重從1到0遞減,同時走路動畫的權重從0到1遞增。可以的點選? 這個網站中 > Crossfading > from idle to walk 體驗一下。
在本次 3D 沙盒遊戲中,人物狀態改變,主要是滑鼠點選地面後,人物從休閒狀態轉為跑步狀態,當人物到達目的地後,又變為休閒狀態。我們先來看看這些狀態改變是如何實現的。
首先,我們需要設計師提供一個擁有骨骼動畫的模型,它有兩個骨骼動畫,一個為休閒(idle)狀態,一個為跑步(run)狀態。
1.1 思路
1.2 動畫初始化
先讓我們將骨骼動畫、動畫名稱、權重放到一個物件中儲存起來,
idleAnimConfig = {
name: string;
anim: AnimationGroup;
weight: number;
}
那麼如何判斷是否正在行走呢?就需要一個當前動畫的 flag,初始化時將 idle 設為當前動畫
currentAnimConfig = idleAnimConfig
1.3 動畫權重改變
如圖,我們在人物狀態改變時,需要將當前狀態的動畫權重遞增,另一狀態的動畫權重遞減(注意,權重值需要限制在[0, 1])。讓我們看下虛擬碼,假設 deltaWeight
為正數
changeAnimWeight() {
// 當前動畫 -> 遞增
if (currentAnimConfig) {
setAnimationWeight(currentAnimConfig, deltaWeight)
}
// 其他動畫 -> 遞減,如站立動作切換到走路
if (currentAnimConfig !== idleAnimConfig) {
setAnimationWeight(idleAnimConfig, -deltaWeight)
}
// 其他動畫 -> 遞減,如走路動作切換到站立
if (currentAnimConfig !== runAnimConfig) {
setAnimationWeight(runAnimConfig, -deltaWeight)
}
}
然後在 render 的時候,進行狀態切換
onRender() {
if (準備到達目的地) {
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}
1.4 缺少動畫
如果 animationGroup 裡只有一個 run 動畫怎麼辦呢?
答案還是一樣的,只要將 idle 動畫的骨骼動畫設為 null
即可,像這樣:
idleAnimConfig = {
name: string;
anim: null;
weight: number;
}
這麼做即使後來更換了具有兩個動畫的人物模型,也能複用。
動畫狀態切換實現效果
二、行走移動
當我們平常寫動畫時,會用到 rAF 並遞迴呼叫渲染函式,實現一個逐幀渲染動畫。當人物行走在平地上時,也可以利用逐幀移動,來實現一個位移的動畫。例如 Babylon 已經封裝好了 render 的事件 API,只要我們將渲染動畫繫結 render 事件,就可以使用了。
讓我們看看具體思路:
2.1 移動
由上面的思路可以看出,我們移動的時候需要用到幾個變數:
- 距終點的距離(distance)
- 移動的方向(direction)
那麼就需要在點選的時候,獲取到這些變數。distance 可以利用矩陣對應座標相加減計算,direction 就是目標位置減初始位置的法向量
directToPath() {
// 將人物的位置設為初始位置
initVec = this.player.position
// 計算初始位置與終點的距離
distance = Distance(targetVec, initVec)
// 將終點位置與初始位置相減
targetVec = targetVec.subtract(initVec)
// 使用法向量計算出與終點的朝向
direction = Normalize(targetVec)
player.lookAt(targetVec)
}
onClick() {
// ...
directToPath()
}
在 render 的時候進行位移
onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移動 SPEED 距離
player.translate(direction, SPEED, Space.WORLD)
}
}
位移實現效果
2.2 結合動畫
當我們的移動結合模型的骨骼動畫
讓我們看看虛擬碼:
onRender() {
if (distance > READY_ARRIVE) {
distance -= SPEED
// 人物朝 direction 方向移動 SPEED 距離
player.translate(direction, SPEED, Space.WORLD)
setCurAnimation(runAnimConfig)
} else {
setCurAnimation(idleAnimConfig)
}
changeAnimWeight()
}
位移及狀態變化實現效果
三、人物避障
3.1 思路
人物行走避障,實際上就是從起點到終點,在這之中新增了中間點。如圖
所以我們只要記錄下當前起點到終點這個路徑陣列,每次都朝陣列的第N個點行走,就能做到轉向。下面我們來根據思路及虛擬碼進行步驟細化。
(1) 記錄路徑和初始化當前的路徑索引
path = getPath(targetVec)
prePathIdx = 0
(2) 當到達當前中間點時,切換到下一個中間點。當走到最後一個,則停止
onRender() {
if (distance > READY_ARRIVE) {
// ...移動及動畫權重切換...
} else {
switchPath()
// ...
}
// ...
}
switchPath() {
prePathIdx += 1
directToPath()
}
directToPath() {
const curPath = path[prePathIdx]
if (!curPath) return
// ...人物移動及轉向...
}
3.2 接入實際避障演算法
由 3.1 得知,人物的行走移動要接入避障演算法,需要利用到該演算法提供的路徑規劃陣列。實際應用中,我們只需要把,虛擬碼裡的getPath()
方法,換成演算法計算道路的方法即可。
3.2.1 RecastJSPlugin
下面我們使用 Babylon 自帶的 Recast 外掛 ,來具體說明一下如何接入避障演算法。
方法 1
在 recast 中,可以通過 computePath
獲取路徑:
const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
const path = this.navigationPlugin.computePath(
this._crowd.getAgentPosition(0),
closestPoint
)
然後利用 3.1 的思路,通過路徑索引切換進行移動。
方法 2
recast 首先會建立一個導航網格,然後通過新增 agent
讓它們約束在這個導航網格中,而這些 agent
的集合,稱為 crowd
。
並且 recast 自帶了移動的 API —— agentGoto
,此時可以不需要再去計算距離和方向,並且也不需要手動切換移動路徑,讓我們看看具體是怎麼做的。
(1) 初始化外掛,並設定 Web Worker 來獲取網格資料以優化效能
initNav() {
navigationPlugin = new RecastJSPlugin()
// 設定Web Worker,在裡面獲取網格資料
navigationPlugin.setWorkerURL(WORKER_URL)
// 建立導航mesh
navigationPlugin.createNavMesh([
ground,
...obstacleList, // 障礙物列表mesh
], NAV_MESH_CONFIG, (navMeshData) => {
navigationPlugin.buildFromNavmeshData(navMeshData)
}
this.navigationPlugin = navigationPlugin
}
(2) 初始化 crowd(crowd:約束在導航網格中 agent
的集合)
initCrowd() {
this.crowd = this.navigationPlugin.createCrowd(1, MAX_AGENT_RADIUS, this.scene)
const transform = new TransformNode('playerTrans')
this.crowd.addAgent(this.player.position, AGENTS_CONFIG, transform)
}
(3) 點選時利用 agentGoto
API進行移動,pickedPoint
為點選點的三維座標,由於 crowd 裡只有一個物件,所以索引是 0
const closestPoint = this.navigationPlugin.getClosestPoint(pickedPoint)
this.crowd.agentGoto(0, closestPoint)
(4) 判斷是否停止,未停止則改變人物朝向
那麼如何改變人物的朝向呢,我們需要下一個中間點的位置,讓人物看向它即可。
所以回到之前初始化的地方,建立一個 navigator
。
initCrowd() {
// ...
const navigator = MeshBuilder.CreateBox('navBall', {
size: 0.1,
height: 0.1,
}, this.scene)
navigator.isVisible = false
this.navigator = navigator
// ...
}
在 render 的時候,人物是否停止,可以通過當前 agent
的移動速度來進行判斷。而改變方向,則是通過將 navigator
移動到下一個 path
的中間點,讓人物看向它。
onRender () {
// 第一個agent物件的移動速度
const velocity = this.crowd.getAgentVelocity(0)
// 移動人物到agent的位置
this.player.position = this.crowd.getAgentPosition(0)
// 將navigator的位置移到下一個點
this.crowd.getAgentNextTargetPathToRef(0, this.navigator.position)
if (velocity.length() > 0) {
this.player.lookAt(this.navigator.position)
// ...
} else {
// ...
}
// ...
}
4. 避障實現效果
讓我們看看最後的效果
5. 遇到的問題
整個開發過程中,其實也不是非常順利,總結了一些遇到的問題,可以給大家參考一下。
(1) 年獸移動時,有時會“無法剎車”,導致在終點時反覆來回停不下來;
這是因為在這一幀裡,由於年獸的加速度較小,無法使得短時間內將速度降為0。所以只能“走過頭”再“走回來”直到速度降為0之後,停止在終點。
此時,只需要 hack 一下,將 agent 的 maxAcceleration 設為極大,讓其有種勻速行走並立馬停下的感覺。
export const AGENTS_CONFIG: IAgentParameters = {
maxAcceleration: 1000
// ...
}
(2) 障礙物的動態新增與移除
如果障礙物在該場景初始化後,位置發生了改變,此時再去銷燬建立一次 navMesh 是很消耗效能的。
於是我們通過查詢文件,看到還有動態新增障礙物的 API。再立馬調了下文件中的Playground,發現是可以用的。但是當我們把障礙物放大了之後,穿模了? 看看這裡。
於是在 Babylon 的論壇上提了這個問題,20分鐘後就得到了 reply,這個速度?。
原來是需要調整 NavMeshParameters
的 ch
/ cs
/ tileSize
引數,對專案做適配。
那如果想要自己實現避障,建立更快的navMesh,我們應該怎麼做呢?可以看看這篇文章:3D 沙盒遊戲之避障踩坑和實現之旅
總結
這篇文章,我們從骨骼動畫的介紹及使用、模型的移動及狀態改變、路徑規劃的適配三個方面,講解了3D沙盒遊戲中實現人物行走移動並進行狀態改變的思路及步驟,希望新人閱讀結束之後,能更快上手這個功能。
當然,本篇文章介紹的實現方式還仍有不足之處,比如移動可以加上加速度,讓動作與移動速度匹配得更自然等。
如果還有什麼合適的建議,也歡迎大家積極留言交流。
參考資料
- 骨骼動畫 - 維基百科,自由的百科全書
- Grouping Animations | Babylon.js Documentation
- Advanced Animation Methods | Babylon.js Documentation
- Vector3 | Babylon.js Documentation
- Crowd Navigation System | Babylon.js Documentation
- Web Workers API
Make crowd agent move at constant speed - Questions - Babylon.js
歡迎關注凹凸實驗室部落格:aotu.io
或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章。