iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 25
4
Modern Web

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

用 Three.js 來當個創世神 (24):專案實作#13 - 子彈射擊效果

今天終於要來實作本專案的靈魂了!沒錯!就是射擊效果,一款射擊遊戲不能發射子彈完全是沒有靈魂啊,而除了完成射擊效果外,還要來把昨天沒做完的苦力怕剛體化及比例調整一下,另外也心血來潮做了疊磚的效果。

24_front

Photo by rawpixel on Unsplash


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

用 Three.js 來當個創世神 (23):專案實作#12 - 導入 Cannon.js 物理效果


今日目標

24_demo

請參考完整原始碼成果展示,原本想讓它與玩家本體一比一,但突然覺得擊倒巨大苦力怕很有趣,那就 ... 讓它更大吧,讀者可以試試看能不能利用子彈將它擊倒。

筆者試玩了之後有發現一個秘技,這邊就先不點破了。

今天的實作有以下幾個目標:

  • 左鍵射擊效果
  • 右鍵疊磚功能
  • 苦力怕剛體化

那就讓我們開始吧!

 

專案實作

1. 射擊效果與疊磚

其實這兩者是同一件事,因為一個是按左鍵發射球體,另一個則是從右鍵發射正方體,程式碼部分先從 index.js 中的 click 事件開始看:

// shooting related settings
const ballShape = new CANNON.Sphere(0.2)
const ballGeometry = new THREE.SphereGeometry(ballShape.radius, 32, 32)
let shootDirection = new THREE.Vector3()
const shootVelo = 15
let raycaster = new THREE.Raycaster() // create once
let mouse = new THREE.Vector2() // create once
// shooting event
window.addEventListener('click', function(e) {
  if (controls.enabled == true) {
    let ammoShape
    let ammoGeometry
    let ammoMass
    let ammoColor
    switch (e.which) {
      case 1: // 左鍵射擊
        ammoShape = ballShape
        ammoGeometry = ballGeometry
        ammoMass = 20
        ammoColor = 0x93882f
        break
      case 3: // 右鍵疊磚
        ammoShape = boxShape
        ammoGeometry = boxGeometry
        ammoMass = 50
        ammoColor = 0x0f0201
      default:
        break
    }

    // 取得目前玩家位置
    let x = playerBody.position.x
    let y = playerBody.position.y
    let z = playerBody.position.z

    // 子彈剛體與網格
    const ammoBody = new CANNON.Body({ mass: ammoMass })
    ammoBody.addShape(ammoShape)
    const ammoMaterial = new THREE.MeshStandardMaterial({ color: ammoColor })
    const ammoMesh = new THREE.Mesh(ammoGeometry, ammoMaterial)
    world.addBody(ammoBody)
    scene.add(ammoMesh)
    ammoMesh.castShadow = true
    ammoMesh.receiveShadow = true
    ammos.push(ammoBody)
    ammoMeshes.push(ammoMesh)
    getShootDir(e, shootDirection) // 取得射擊方向
    ammoBody.velocity.set(
      shootDirection.x * shootVelo,
      shootDirection.y * shootVelo,
      shootDirection.z * shootVelo
    )
    // Move the ball outside the player sphere
    x += shootDirection.x * (sphereShape.radius * 1.02 + ballShape.radius)
    y += shootDirection.y * (sphereShape.radius * 1.02 + ballShape.radius)
    z += shootDirection.z * (sphereShape.radius * 1.02 + ballShape.radius)
    ammoBody.position.set(x, y, z)
    ammoMesh.position.set(x, y, z)
  }
})

這邊先根據點擊左鍵或右鍵決定發射出的物體是子彈或磚塊,再建立發射物的剛體及網格模型,比較值得注意的是這邊有個 getShootDir(e, shootDirection),就是用來得知道發射物是朝什麼方向前進:

function getShootDir(event, targetVec) {
  // 取得滑鼠在網頁上 (x, y) 位置
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

  // 透過 raycaster 取得目前玩家朝向方向
  raycaster.setFromCamera(mouse, camera)

  // 取得 raycaster 方向並決定發射方向
  targetVec.copy(raycaster.ray.direction)
}

這邊官方範例中使用較早以前 Three.js 版本用的 THREE.Projector() 來取得發射方向,後來找了一些資料得到可以透過 raycaster 解決,當滑鼠及相機方向具備後,就可以用 raycaster.ray.direction 來取得子彈方向。

最後最重要的一步,就是要記得在 render() 中更新網格的位置與剛體同步了:

function render() {
  requestAnimationFrame(render)
  ...
  for (let i = 0; i < ammos.length; i++) {
    ammoMeshes[i].position.copy(ammos[i].position)
    ammoMeshes[i].quaternion.copy(ammos[i].quaternion)
  }
  ...
  renderer.render(scene, camera)
}

另外程式碼中建立剛體及網格模型可參考前面「第 20 篇:Cannon.js 基本練習」

2. 苦力怕剛體化

由於苦力怕分別有頭、身體、四隻腳,共六個箱體,做法都差不多,這邊只示範頭的部分,首先看到 creeperModule.js 的程式碼:

const headShape = new CANNON.Box(
  new CANNON.Vec3(2 * sizeScale, 2 * sizeScale, 2 * sizeScale)
)
this.headBody = new CANNON.Body({
  mass: 5 * massScale,
  position: new CANNON.Vec3(0, 12 * sizeScale, 0)
})

這邊要先說一下,由於昨天苦力怕變太大隻,在建立苦力怕物件時,改寫成可以動態傳入尺寸 sizeScale 跟體重 massScale 兩個參數,可以用來調整比例。

而箱型剛體(CANNON.Box)一直沒仔細解釋,其實筆者一開始也一直好奇為什麼要用網格幾何體的一半長來宣告,看官網文件只寫請傳入「halfExtents」也沒什麼解釋,猜測 CANNON.Box 應該是以重心為標準去看待 shape 的長寬高。

然後在 index.js 中的 createCreeper() 初始化中做 world.addBody(creeperObj.headBody) 把剛體加到世界中,並且在動畫更新部分也要記得同步更新網格的位置:

creeperObj.head.position.copy(creeperObj.headBody.position)
creeperObj.head.quaternion.copy(creeperObj.headBody.quaternion)

3. 苦力怕連結處約束效果

而上面的剛體化效果對六塊物體依序做完,會發現苦力怕是有剛體化了,不過卻四分五裂散落一地:

24_split

有沒有想到昨天提到的一個概念?沒錯,「約束」在這邊就派上用場了,一樣舉頭部當範例,我們得將頭與身體的連接處,姑且稱它為脖子(雖然苦力怕沒有),來做一個頭與身體的約束效果:

creeperModule.js

// Neck joint
this.neckJoint = new CANNON.LockConstraint(this.headBody, this.bodyBody)

index.js

function createCreeper() {
  ...
  world.addConstraint(creeperObj.neckJoint)
  ...
}

Cannon.js 中的約束有很多種,筆者測試到後來發現 LockConstraint 是可以順利完成我們要的效果的,加上去之後就會發現苦力怕成功地連結在一起剛體化了,可以透過子彈將它擊倒啦!

 

開發後記 - 遇到的問題

1. 取得 raycaster 射線方向

射擊部分主要的難點其實在於如何取得射擊的方向上,一開始只知道試著寫 raycater.direction 一直是 undefined,查了半天才知道是要寫成 raycater.ray.direction

2. 苦力怕剛體化之約束問題

其實今天比較晚發文是因為這塊一直卡關,因為苦力怕有六塊,一直沒辦法組在一起,還有一些靈異現象像是頭會噴飛之類的,一度還覺得要不要乾脆做個長方體就好,但身為工程師就是不能放棄,持續的 try and error 才是王道,不然前面做了很久的苦力怕就不能用了!

而關於四分五裂的問題一開始也沒想法,後來想到可以參考範例,就找到了這個

24_q_2

trace 了一下 code,發現原來是需要加上「約束」啊!就依樣畫葫蘆試著加上了:

this.neckJoint = new CANNON.ConeTwistConstraint(
  this.headBody,
  this.bodyBody,
  {
    pivotA: new CANNON.Vec3(0, 6, 0),
    pivotB: new CANNON.Vec3(0, 4, 0),
    axisA: CANNON.Vec3.UNIT_Y,
    axisB: CANNON.Vec3.UNIT_Y,
    angle: Math.PI / 4,
    twistAngle: Math.PI / 8
  }
)

24_q_3

你誰!?

後來就到官網文件查「Constraint」,改成另一個比較像的 LockConstraint 才成功。

3. 苦力怕剛體化之定位問題

最後剩下的一個 bug 是剛體跟網格的位置對不起來,就是網格模型生在(10, 0, 10),但實際上剛體位置似乎不在那而在大約(0, 0, 0)的位置,原本一直嘗試是不是 CANNON.Box 宣告形狀的定義搞錯了?還是 CANNON.Body 的 position 要自己平移?在各種試都無解的狀況下,還試著要加上 Cannon.js 的 debug tool,但卻一直噴 error(什麼巫術),後來追到各種 position 的問題才發現是在 index.jscreateCreeper() 初始化時設了個 creeperObj.creeper.position.set(10, 0, 10),難怪一直有位置不對應的問題,才終於完成今天的所有進度。

 

開發後記 - 待處理問題

1. 子彈或磚塊數量太多 FPS 狂降

可能解法像是「限制子彈數量」或「子彈過一段時間後需做 dispose」等等。

2. 摩擦力與彈性調整

目前整個物理世界內的物體似乎都設成一樣的,但應該個別做調整,不然疊磚時很滑都疊不起來。

3. 子彈會從點擊開始的位置發射

使用者體驗不太好,或許要在網頁上做個按鈕引導使用者點擊中心開始遊戲。

4. 瀏覽器支援

由於筆者是使用 Google Chrome 在 Macbook 上開發測試,所以使用桌面版的 Chrome 能擁有最佳體驗;剛才稍微試了一下 Safari 無法點右鍵,而 Firefox 還算正常,但左鍵連點會反白。
 

今日小結

今天終於完成了射擊的效果,開始漸漸比較有遊戲性了,也順手加上了類似遊戲中的原始疊磚塊的玩法,不過既然是可以射擊又可以蓋地基,難道可以做成一款 Fortnite 了嗎?

剩下幾天要來處理子彈射到苦力怕爆炸、遊戲流程、整合各項功能、背景及射擊爆炸音效、場景及細節優化,雖然時間內可能不會全部都做到,但至少整個基本遊戲流程希望可以順利完成,我們明天見。

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


上一篇
用 Three.js 來當個創世神 (23):專案實作#12 - 導入 Cannon.js 物理效果
下一篇
用 Three.js 來當個創世神 (25):專案實作#14 - 遊戲內容設計
系列文
用 Three.js 來當個創世神31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言