iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 23
2
Modern Web

用 Three.js 來當個創世神系列 第 23

用 Three.js 來當個創世神 (22):專案實作#11 - 使用 PointerLockControls 實現射擊遊戲視角 Part.2

前情提要,昨天為了達成射擊遊戲中利用鼠標瞄準的條件,已經完成了 PointerLockControls 的初始化,今天會完成畫面更新中關於碰撞、重力、及速度的邏輯解析,解決專案中射擊遊戲視角控制的部分。

22_front

Photo by Michael Sum on Unsplash


這是本系列第 22 篇,如果還沒看過第 21 篇可以點以下連結前往:

用 Three.js 來當個創世神 (21):專案實作#10 - 使用 PointerLockControls 實現射擊遊戲視角 Part.1


今日目標

今天要繼續解析昨天的第二部分,畫面更新中關於碰撞、重力、及速度的邏輯解析。

21_demo

請參考完整原始碼成果展示

程式解析

關於今天鼠標控制器的畫面更新相關程式內容在 pointerLockControlsRender() 中, PointerLockControls 以下簡稱鼠標控制器。

1. 初始化 Raycaster 做為跳躍的碰撞偵測

raycaster = new THREE.Raycaster(
  new THREE.Vector3(), // 射線起點向量
  new THREE.Vector3(0, -1, 0), // 射線投射方向向量
  0, // 投射近點
  10 // 投射遠點
)

22_raycaster

Photo by mooonx on segmentfault

先簡單介紹一下 raycaster 是什麼。

由於網頁是 2D 的畫面,想要在 3D 世界中取得鼠標位置會利用 raycaster(射線),原理就是從鼠標處朝某個方向發射(投影)一條射線,若射線與物體相交,代表鼠標選到了它,實例可以參考官方 raycaster 範例,或之前筆者在決定要做射擊遊戲前,原本想嘗試做密室逃脫遊戲,而用 raycaster 練習做的一個開門的範例

更多資訊請參考文章:Three.js 通过THREE.Raycaster给模型绑定点击事件

回到正題,這邊會使用 raycaster 是因為要做碰撞偵測,跟上面一樣的概念,利用 raycaster 朝某方向一定距離內射出射線,判斷是否有碰撞到任何物體,也就是說這裡宣告一個方向向 -y 軸的射線,後面在更新畫面的部分會檢測在玩家實體(controls.getObject())下距離 10 的地方是否有物體碰撞,來判斷跳躍後在哪個物體上或需要向下墜落。

2. 更新鼠標控制器畫面

(1) 基本設定

let prevTime = Date.now() // 初始時間
let velocity = new THREE.Vector3() // 移動速度向量
let direction = new THREE.Vector3() // 移動方向向量

function pointerLockControlsRender() {
  if (controls.isLocked === true) {
	 // 計算時間差
    const time = Date.now()
    const delta = (time - prevTime) / 1000 // 大約為 0.016
	 ...
    prevTime = time
  }
}

首先宣告幾個初始向量,時間差的部分每一次進來的 delta 值大約是 0.016,就是每 16 ms 更新一次的意思,而鼠標控制器提供一個旗標值 isLocked 來判斷使用者是否已解鎖畫面,預設為 false 為鎖定的狀態。

(2) 跳躍

// Raycaster 複製控制器的位置
raycaster.ray.origin.copy(controls.getObject().position)
    
// 使用 Raycaster 判斷腳下是否與場景中物體相交
const intersections = raycaster.intersectObjects(scene.children, true)
const onObject = intersections.length > 0

// 判斷是否停在地面上
if (onObject === true) {
  velocity.y = Math.max(0, velocity.y)
  canJump = true
}

controls.getObject().translateY(velocity.y * delta)

// 控制器下墜超過 -2000 則重置位置
if (controls.getObject().position.y < -2000) {
  velocity.y = 0
  controls.getObject().position.set(10, 100, 60)
  canJump = true
}

首先先理解跳躍的部分,這裡用一個向下的 raycaster 來偵測是否與 scene.children 相交,而由於場景中目前會在控制器腳下的只有地板(其實嚴格說還有苦力怕),可以理解為目前的 raycaster 是用來偵測控制器是否在地板上。

當控制器在地板上時,會利用 velocity.y = Math.max(0, velocity.y) 來將 y 軸速度歸零而靜止不動,並設定旗標值 canJump 為可以跳躍的狀態;而當使用者按空白鍵跳起時,前面監聽事件會被觸發:

case 32: // space
  if (canJump === true) velocity.y += 350 // 跳躍高度
  canJump = false
  break

此時 y 軸速度為向上 350,並將旗標值重置,並根據 velocity.y -= 9.8 * 100.0 * delta 這邊設定的一個向下模擬重力的速度將使用者拉回地面。

最後筆者有加了一個重生的機制,就是當走出地板後,是可以實際下墜一段距離後會重生在初始位置的,如果不想有墜落效果的話,也可以將下墜值設為 0 方便調整。

(3) 移動

// 設定每次更新時速度
velocity.x -= velocity.x * 10.0 * delta
velocity.z -= velocity.z * 10.0 * delta

// 判斷按鍵朝什麼方向移動,並設定對應方向速度變化
direction.z = Number(moveForward) - Number(moveBackward)
direction.x = Number(moveLeft) - Number(moveRight)
    
// 向量正規化(長度為 1),確保每個方向保持一定移動量
direction.normalize()
    
// 根據移動方向變更速度值
if (moveForward || moveBackward) velocity.z -= direction.z * 400.0 * delta
if (moveLeft || moveRight) velocity.x -= direction.x * 400.0 * delta

// 根據速度值移動控制器位置
controls.getObject().translateX(velocity.x * delta)
controls.getObject().translateZ(velocity.z * delta)

由於前後與左右邏輯一樣,以下簡單化只討論 x 軸變化。

移動的部分筆者個人覺得稍難理解,首先為什麼要設定 velocity.x -= velocity.x * 10.0 * delta 這個邏輯?為什麼當有移動事件時要做 velocity.x -= direction.x * 400.0 * delta

這裡如果你也很難理解,可以試著將 velocity.xdirection.x 一起印出來看就會清楚一些。當使用者從移動到靜止時,direction.x 會是 0,而 velocity.x -= velocity.x * 10.0 * delta 會使 velocity.x 漸趨近於 0;而當使用者持續移動時,會觀察到 (velocity.x, direction.x) 呈現一個 (40, -1)(-40, 1) 兩種情況,就是利用這樣的關係,讓使用者在移動時可以維持在最高為 40 的速度。

而在原始碼中,還設定了 direction.normalize(),這是將方向向量正規化為單位向量的效果,官方解釋是要「確保每個方向保持一定移動量」,不過仔細看其實 Number(moveForward) - Number(moveBackward) 也只有(1, 0, -1)三種可能而已,實驗後應是可以不用這行的。

 

其他問題

1. 程式碼移動問題

關於程式碼的移動部分,拋出一個令筆者百思不得其解的地方,就是如果你將 (velocity.z, direction.z) 印出來,當你一直按著前進方向時會是 (-40, 1),但不管你用鼠標轉向哪個方向,這個數組都不會變,那為甚麼當你面朝 z 軸方向持續前進時,照理說 controls.getObject().translateZ(-40 * delta) 會將控制器的方向一直移至 -z 軸的方向,也就是會一直後退,但實際上不會有這種詭異的情況呢?translateZ(-40) 應該是指朝 -z 軸方向平移 40 沒錯吧,還是筆者理解有誤?再麻煩想明白的讀者留言幫忙解個惑。

2. 裝置問題

後來發現使用這個鼠標鎖定控制器的話,在移動裝置上是不支援的,如果要比照手機射擊遊戲的話,大概就需要設定一些控制面板來操作移動、旋轉視角、瞄準、射擊等等行為,想必是需要另外下功夫的,就暫時先做桌面版的了。

 

今日小結

今天完成了將 PointerLockControls 鼠標鎖定視角控制器在畫面更新上的邏輯分析,可以發現在沒有引入物理引擎的狀況下,要靠自己實作碰撞、重力及速度位移是比較複雜的,筆者光理解就花掉不少時間了,之後將會開始引入 Cannon.js,將原本的做法取代掉,並賦予場景中物體具有剛體特性,使它們具有物理效果,我們明天見!

最後再附上今天的完整原始碼成果展示

 

參考資料


上一篇
用 Three.js 來當個創世神 (21):專案實作#10 - 使用 PointerLockControls 實現射擊遊戲視角 Part.1
下一篇
用 Three.js 來當個創世神 (23):專案實作#12 - 導入 Cannon.js 物理效果
系列文
用 Three.js 來當個創世神31

尚未有邦友留言

立即登入留言