Three.js系列: 寫一個第一/三人稱視角小遊戲

程式設計師秋風發表於2022-03-02

大家好,我是秋風,在上一篇 中說到了 Three.js 系列的目標以及寶可夢遊戲,那麼今天就來通過 Three.js 來談談關於遊戲中的視角跟隨問題。相信我的讀者都或多或少玩一些遊戲,例如王者榮耀、絕地求生、寶可夢、塞爾達、原神之類的遊戲。那麼你知道他們分別是什麼視角的遊戲麼?你知道第一人稱視角和第三人稱視角的差異麼?通過程式碼我們怎麼能實現這樣的效果呢?如果你對以上問題好奇,並且不能完全回答。那麼請跟隨著我一起往下看吧。

視角講解

首先我們先來看看第一人稱視角、第三人稱視角的概念。其實對於我們而言 第一人稱 和 第三人稱,是非常熟悉的,第一人稱就是以自己的口吻講述一件事,例如自傳都是以這種形式抒寫,第三人稱則是以旁觀者,例如很多小說,都是以他(xxx)來展開式將的,觀眾則是以上帝視角看著這整個故事。對應的第一人稱視角、第三人稱視角也是相同的概念,只不過是視覺上面。那麼他們各自有上面區別呢?第一人稱視角的有點是可以給玩家帶來最大限度的沉浸感,從第一人稱視角“我”去觀察場景和畫面,可以讓玩家更加細緻地感受到其中的細節,最常見的就是類似絕地求生、極品飛車之類的。

而第一人稱視角也有他的侷限性。玩家的視野受限,無法看到更廣闊的的視野。另一個就是第一人稱視角會給玩家帶來“3D眩暈感”。當反應速度更不上鏡頭速度的時候會造成眩暈感。那麼第三人稱視角呢?他的優勢就是自由,視野開闊,人物移動和視角是分開的,一個用來操作人物前進方向,另一個則是用來操控視野方向。

它的劣勢就是無法很好的聚焦區域性,容易錯過細節。但是總的來說,目前大多數遊戲都提供了兩種視角的切換來滿足不同的情形。例如絕對求生中平時走路用第三人稱視角跟隨移動,開槍的時候一般用第一人稱視角。好了,到目前為主我們已經知道了第一人稱視角、第三人稱視角各自概念、區別。那麼我們接下來以第三人稱視角為例,展開分析我們該如何實現這樣的一個效果呢?(第三人稱的編寫好後,稍加修改就可以變成第一人稱,因此以更加複雜的第三人稱為例)把大象放入冰箱需要幾步?三步!開啟冰箱,把大象放進冰箱,關上冰箱。顯然如果真的要把大象放進冰箱是很難的事情,但是從巨集觀角度來看,就是三個步驟。因此我們也將實現第三人稱視角這個功能分成三步:

步驟拆分

以下的步驟拆分不會包含任何程式碼,請放心使用:1.人物如何運動我們都知道在物理真實的世界中,我們運動起來是靠我們雙腿,邁開就動起來了。那這個過程從更巨集觀的角度來看是怎麼樣的呢?其實如果從地球外,從一個更遠的角度來看,我們做運動更像是一個個平移變化。相同地,我們在計算機中來表示運動也就是運用了平移變化。平移變化詳細大家以前都比較熟悉,如果現在不熟悉了呢,也沒有什麼關係,先看下面的座標軸。(小方塊的邊長是1)

小方塊從A1位置移動到位置A2就是平移變化,如果用數學表示式來表示的話就是

上面是什麼意思呢?就是說我們讓小方塊中所有的小點的 x 值都加2,而 y 的值不變。我們隨意取一些值來驗證一下。例如A1位置小方塊,左下角是 (0,0), 通過以上變化,就變成了 (2, 0),我們來A2中看小方塊新的位置就是 (2, 0);再用右上角的 (1,1) 代入,發現就變成了(3,1),和我們真實移動到的位置也是一樣的。所以上面的式子沒有什麼問題。但是後來呢,大家覺得像上面那樣的式子用來表示稍微有點不夠通用。至於這裡為什麼說不夠通用,在後面的系列文章中會詳細講解,因為還涉及到了其他變化,例如旋轉、縮放,他們都可以用一個矩陣來進行描述,因此如果平移也能夠用矩陣的方式來表達,那麼整個問題就變得簡單了,也就是說:運動變化 = 矩陣變化我們來看看把最開始的式子變成矩陣是什麼樣子的:

可以簡單講解一下右邊這個矩陣是怎麼來的

左上角的這個部分稱為單位矩陣,後面的 2 0 則就是我們需要的平移變化,至於為什麼從2維變成了3維,則是因為引入了一個齊次矩陣的概念。同樣的原理,類比到 3維,我們就需要用到4維矩陣。所以說,我們通過一系列的例子,最終想要得到的一個結論就是,所有的運動都是矩陣變化

2.鏡頭朝向人物我們都知道,在現實世界中我們眼睛看出去的視野是有限的,在電腦中也是一樣的。假設在電腦中我們的視野是  3 * 3 的方格,我們還是以之前座標軸舉例子,黃色區域是我們的視野可見區域:

現在我們讓小塊往右移動3個單位,再網上移動1個單位。

這個時候我們會發現,我們的視野內已經看不到這個小塊了。試想一下,我們正在玩一個射擊遊戲,敵人在眼前移動,我們為了找到它會在怎麼辦?沒錯,我們會旋轉我們的腦袋,從而使得敵人暴露在我們的視野內。就像這樣:

這下就把敵人鎖定住了,能夠始終讓人物出現在我們的視野內並且保持相對靜止。3.鏡頭與人物同距光有鏡頭朝向人物還不夠,我們還得讓我們的鏡頭和人物同距。為什麼這麼說呢,首先還是我們座標軸的例子,但是這次我們將擴充一個z軸:然後我們看看正常下的平面截圖

截圖:

現在我們將我們的小塊往-Z 移動1個單位:

截圖:

這個時候我們發現這個小方塊變小了,並且隨著小方塊往 -z方向移動的越多,我們看到的小塊會越來越小。這個時候我們明明沒有改變我們的視角,但是還是無法很好的跟蹤小塊。因此我們需要移動為我們視角的位置,當我們看不清一個遠處的路標的時候,我們會怎麼辦?沒錯,湊近點!

截圖:

完美!現在我們通過三個方向的講解,將如果實現一個第三人稱視角的功能從理論上面實現了!

搞程式碼

接下來我們只需要按照我們的以上的理論,來實現程式碼就好了,程式碼無法就是我們用另一種語言的實現方式,知道了原理都是非常簡單的。1.初始化畫布場景

<canvas class="webgl"></canvas>  
...  
<script>  
// 建立場景  
const scene = new THREE.Scene()  
// 加入相機  
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height);  
camera.position.y = 6;  
camera.position.z = 18;  
const controls = new OrbitControls(camera, canvas)  
controls.enableDamping = true; // 設定阻尼,需要在 update 呼叫  
scene.add(camera);  
// 渲染  
const renderer = new THREE.WebGLRenderer({  
    canvas  
})  
renderer.setSize(sizes.width, sizes.height)  
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))  
renderer.render(scene, camera);  
</script>  

場景、相機、渲染器是一些比較固定的東西,這一節不主要進行講解,可以理解為我們專案初始化的時候一些必備的語句。這個時候我們開啟頁面,是黑乎乎的一片,為了美觀,我給整個場景加上一個地板。

// 設定地板  
const geometry = new THREE.PlaneGeometry(1000, 1000, 1, 1);  
// 地板貼圖  
const floorTexture = new THREE.ImageUtils.loadTexture( '12.jpeg' );  
floorTexture.wrapS = floorTexture.wrapT = THREE.RepeatWrapping;   
floorTexture.repeat.set( 10, 10 );  
// 地板材質  
const floorMaterial = new THREE.MeshBasicMaterial({   
    map: floorTexture,   
    side: THREE.DoubleSide   
});  
  
const floor = new THREE.Mesh(geometry, floorMaterial);  
// 設定地板位置  
floor.position.y = -1.5;  
floor.rotation.x = - Math.PI / 2;  
  
scene.add(floor);  
`

![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa8cea3705594d10a7e01e7225283a78~tplv-k3u1fbpfcp-zoom-1.image)

這個時候畫面還不錯\~2.人物運動根據理論,我們需要加入一個人物,這裡為了方便,也還是加入一個小方塊為主:
// 小滑塊  
const boxgeometry = new THREE.BoxGeometry(1, 1, 1);  
const boxMaterials = [];  
for (let i = 0; i < 6; i++) {  
    const boxMaterial = new THREE.MeshBasicMaterial({  
        color: Math.random() * 0xffffff,  
    });  
    boxMaterials.push(boxMaterial);  
}  
// 小塊  
const box = new THREE.Mesh(boxgeometry, boxMaterials);  
box.position.y = 1;  
box.position.z = 8;  
  
scene.add(box);  
  

為了好看,我給小塊加了六面不同的顏色。

雖然看起來還是有點簡陋,但是俗話說高階的食材往往只需要最樸素的烹飪方式。小塊雖小,但是五臟俱全。現在我們渲染出了小塊後,要做的事情就是繫結快捷鍵。

對應的程式碼:

// 控制程式碼  
const keyboard = new THREEx.KeyboardState();  
const clock = new THREE.Clock();  
const tick = () => {  
    const delta = clock.getDelta();  
    const moveDistance = 5 * delta;  
    const rotateAngle = Math.PI / 2 * delta;  
      
    if (keyboard.pressed("down"))  
        box.translateZ(moveDistance);  
    if (keyboard.pressed("up"))  
        box.translateZ(-moveDistance);  
    if (keyboard.pressed("left"))  
        box.translateX(-moveDistance);  
    if (keyboard.pressed("right"))  
        box.translateX(moveDistance);  
  
    if (keyboard.pressed("w"))  
        box.rotateOnAxis( new THREE.Vector3(1,0,0), rotateAngle);  
    if (keyboard.pressed("s"))  
        box.rotateOnAxis( new THREE.Vector3(1,0,0), -rotateAngle);  
    if (keyboard.pressed("a"))  
        box.rotateOnAxis( new THREE.Vector3(0,1,0), rotateAngle);  
    if (keyboard.pressed("d"))  
        box.rotateOnAxis( new THREE.Vector3(0,1,0), -rotateAngle);  
          
    renderer.render(scene, camera)  
    window.requestAnimationFrame(tick)  
}  
tick();  

這裡解釋一下 translateZ、translateX,這倆函式就是字面意思,往 z 軸 和 x 軸移動,如果想要往前,就往 -z 軸移動,如果是往 左就是往 -x 軸移動。clock.getDelta () 是什麼意思呢?簡單說.getDelta ()方法的功能就是獲得前後兩次執行該方法的時間間隔。例如我們想要在1秒內往前移動5個單位,但是直接移動肯定比較生硬,因此我們想加入動畫。我們知道為了實現流暢的動畫,一般通過瀏覽器的APIrequestAnimationFrame實現,瀏覽器會控制渲染頻率,一般效能理想的情況下,每秒s渲染60次左右,在實際的專案中,如果需要渲染的場景比較複雜,一般都會低於60,也就是渲染的兩幀時間間隔大於16.67ms。因此為了移動這5個單位,我們將每一幀該移動的距離,拆分到了這 60次渲染中。最後來說說 rotateOnAxios,這個主要就是用來控制 小盒子的旋轉。

.rotateOnWorldAxis ( axis : Vector3, angle : Float ) : this axis -- 一個在世界空間中的標準化向量。
angle -- 角度,以弧度來表示。

3.相機與人物同步回顧理論部分,我們最後一個步驟就是想要讓相機(人眼)和物體保持相對靜止的,也就是距離不變。

const tick = () => {  
  ...  
  const relativeCameraOffset = new THREE.Vector3(0, 5, 10);  
    
  const cameraOffset = relativeCameraOffset.applyMatrix4( box.matrixWorld );  
    
  camera.position.x = cameraOffset.x;  
  camera.position.y = cameraOffset.y;  
  camera.position.z = cameraOffset.z;  
  // 始終讓相機看向物體  
  controls.target = box.position;  
  ...  
}  

這裡有個比較核心的點就是 relativeCameraOffset.applyMatrix4( box.matrixWorld ); 其實這個我們在理論部分說過了,因為我們的物體移動的底層原理就是做矩陣變化,那麼想要讓相機(人眼)和物體的距離不變,我們只需要讓相機(人眼)和物體做相同的變化。而在 Three.js 中物體所有的自身變化都記錄在 .matrix 裡面,只要外部的場景不發生變化,那麼.matrixWorld 就等於  .matrix 。而applyMatrix4 的意思就是相乘的意思。

效果演示

這樣我就最終實現了整個功能!我們下期見!

原始碼地址:https://github.com/hua1995116...

結語

❤️關注+點贊+收藏+評論+轉發❤️,原創不易,鼓勵筆者創作更好的文章

關注公眾號秋風的筆記,一個專注於前端面試、工程化、開源的前端公眾號

相關文章