前言
在 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),不定時推送文章:

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/460855.html
標籤:JavaScript
上一篇:JavaScript の querySelector 使用說明
下一篇:Node.js基礎入門第十天
