全部章節vue
這一章主要實現粒子效果、死亡斷定以及跌落,因爲時間關係而且threejs不是我主攻的方向(仍是要將時間用在正兒八經的事情上),其它的功能實現會在結尾大體分析一下。react
我在調試時內存蹭蹭往上竄,大量對象實例被引用,發現單純的dispose
效果甚微,得作的完全一點,像這樣:github
export const destroyMesh = mesh => {
if (mesh.geometry) {
mesh.geometry.dispose()
mesh.geometry = null
}
if (mesh.material) {
mesh.material.dispose()
mesh.material = null
}
mesh.parent.remove(mesh)
mesh.parent = null
mesh = null
}
複製代碼
如今就不怕內存暴漲了。同時,我爲每一個類實現了destroy
方法來避免內存問題,web
上一章中,咱們列出了小人的幾個相關注意點:spring
接下來實現起跳和落地時的特效。segmentfault
繼續打開微信跳一跳,擼一把先......api
那麼如今粒子類應該具有基本的2個方法: 粒子流
和粒子噴泉
(我隨便起的不專業😄),可是呢,要有這2個特效,咱們首先得生成粒子。那麼緩存
粒子流
效果,終點很明確,咱們能夠將小人的腳下中心點做爲終點(須要算上粒子的高度),而對於粒子噴泉
,沒法肯定終點在哪,只能大體肯定一個向上的方向,因此也能夠認爲噴出粒子的終點是隨機的,可是統一貫外側方向,同時限制噴出粒子的最大行程就好了而後根據上述分析畫了一張大體的垂直截面圖:安全
圖一旦畫出來,咱們又能想到新的問題
粒子流
效果,只要不鬆開手指是一直運行的,那麼這裏須要時時刻刻生成新的粒子嗎?粒子噴泉
效果,須要生成新的粒子嗎?而後我想到一個比較相似的場景來解決這個問題:就像一個假山公園,假山上有流水、水池中有噴泉,假山的水是一直在流,是什麼讓它一直流?確定是有個水泵在起做用,水泵將流下來的水一直往上抽,同時假山上水也一直往下流,造成一個循環,而噴泉也是利用水池中的水,水最終也落到水池中,不考慮水蒸發,水量是固定的。
不如將這個場景映射到咱們的問題中,咱們首先在小人腳下準備好定量的粒子(水池中定量的水),再準備一個粒子泵(水泵),粒子泵不斷的將小人腳下(水池)的粒子往上抽(設置隨機位置,假山),同時讓被抽上來的粒子繼續前往終點(水往下流),而後粒子噴泉
直接複用腳下的粒子(水池中的水),利用粒子噴泉粒子泵將水向上噴,噴完後將它們放進水池(重置到腳下),造成一個循環。同時,這裏的粒子系統,應該是跟着小人走的,因此,咱們能夠將粒子系統做爲小人的一部分(添加到一個組中)。
ok,已經有了思路,如今能大體寫出Particle
類的結構
class Particle {
constructor ({
world,
quantity = 20, // 粒子數量
triggerObject, // 觸發對象
}) {
this.world = world
this.quantity = quantity
this.triggerObject = triggerObject
this.particleSystem = null
}
// 生產定量的粒子
createParticle () {}
// 將粒子放到腳下
resetParticle () {}
// 粒子流粒子泵
runParticleFlowPump () {}
// 粒子流
runParticleFlow () {}
// 粒子噴泉粒子泵
runParticleFountainPump () {}
// 粒子噴泉
runParticleFountain () {}
}
複製代碼
首先生成定量的粒子,這裏threejs的粒子我研究了半天,仍是不得要領,有幸在網上找到了一個demo,而後我直接參考了它。而後根據觀摩微信跳一跳的粒子效果,粒子的顏色應該只有2種,白色和綠色,因此這裏設置一半的粒子爲白色,一半爲綠色。new THREE.TextureLoader().load('xxx.png')
這種方式出問題毫無徵兆,應該使用new THREE.TextureLoader().load(require('./dot.png'), callback)
這種形似,或者套一個Promise。
// 生成粒子
createParticle () {
const { quantity, triggerObject } = this
// 一半白色、一半綠色
const white = new THREE.Color( 0xffffff )
const green = new THREE.Color( 0x58D68D )
const colors = Array.from({ length: quantity }).map((_, i) => i % 2 ? white : green)
const particleSystem = this.particleSystem = new THREE.Group()
new THREE.TextureLoader().load(require('./dot.png'), dot => {
const baseGeometry = new THREE.Geometry()
baseGeometry.vertices.push(new THREE.Vector3())
const baseMaterial = new THREE.PointsMaterial({
size: 0,
map: dot,
// depthTest: false, // 開啓後能夠透視...
transparent: true
})
colors.forEach(color => {
const geometry = baseGeometry.clone()
const material = baseMaterial.clone()
material.setValues({ color })
const particle = new THREE.Points(geometry, material)
particleSystem.add(particle)
})
this.resetParticle()
triggerObject.add(particleSystem)
})
}
複製代碼
而後將粒子放到小人腳下,須要注意的是,若是這裏直接將粒子放到腳下,小人空翻時能被看到,因此須要藏起來。這裏約定一個粒子的最大大小值initalY
// 將粒子放到小人腳下
resetParticle () {
const { particleSystem, initalY } = this
particleSystem.children.forEach(particle => {
particle.position.y = initalY
particle.position.x = 0
particle.position.z = 0
})
}
複製代碼
如今,咱們已經將定量的粒子生成並放入初始位置了,接下來實現粒子泵,粒子泵的做用就是將腳下的粒子隨機放到小人的上方周圍(將水往上抽),那麼這裏的隨機值就須要考慮一個範圍,而且不能將粒子隨機在小人的身體中,這裏從分析的第一張圖就能夠看出來。那如今咱們以小人的身高胖瘦爲準,約定粒子的隨機位置爲小人上半身周圍,同時以小人的寬度爲準限制水平方向的範圍。同理,約定噴泉的粒子隨機位置爲小人的下半身周圍,粒子大小爲粒子流
的一半(觀測比粒子流的小),最大噴射距離(行程)爲小人身高的一半。
constructor ({
world,
quantity = 20, // 數量
triggerObject // 觸發對象
}) {
this.world = world
this.quantity = quantity
this.triggerObject = triggerObject
this.particleSystem = null
const { x, y } = getPropSize(triggerObject)
this.triggerObjectWidth = x
// 限制粒子水平方向的範圍
this.flowRangeX = [-x * 2, x * 2]
// 粒子流,垂直方向的範圍,約定從小人的上半身出現,算上粒子最大大小
const flowSizeRange = this.flowSizeRange = [x / 6, x / 3]
this.flowRangeY = [y / 2, y - flowSizeRange[1]]
// 粒子初始的y值應該是粒子大小的最大值
this.initalY = flowSizeRange[1]
// 粒子噴泉,垂直方向的範圍,約定從小人的下半身出現,算上粒子最大大小
const fountainSizeRange = this.fountainSizeRange = this.flowSizeRange.map(s => s / 2)
this.fountainRangeY = [fountainSizeRange[1], y / 2]
this.fountainRangeDistance = [y / 4, y / 2]
// 限制粒子水平方向的範圍
this.fountainRangeX = [-x / 3, x / 3]
}
複製代碼
既然約定好了安全值,如今就來實現粒子流粒子泵邏輯
// 粒子流粒子泵
runParticleFlowPump () {
const { particleSystem, quantity, initalY } = this
// 粒子泵只關心腳下的粒子(水池)
const particles = particleSystem.children.filter(child => child.position.y === initalY)
// 腳下的粒子量不夠,抽不上來
if (particles.length < quantity / 3) {
return
}
const {
triggerObjectWidth,
flowRangeX, flowRangeY, flowSizeRange
} = this
// 好比隨機 x 值爲0,這個值在小人的身體範圍內,累加一個1/2身體寬度,這樣作可能有部分區域隨機不到,不過影響不大
const halfWidth = triggerObjectWidth / 2
particles.forEach(particle => {
const { position, material } = particle
const randomX = rangeNumberInclusive(...flowRangeX)
const randomZ = rangeNumberInclusive(...flowRangeX)
// 小人的身體內,不能成爲起點,須要根據正反將身體的寬度加上
const excludeX = randomX < 0 ? -halfWidth : halfWidth
const excludeZ = randomZ < 0 ? -halfWidth : halfWidth
position.x = excludeX + randomX
position.z = excludeZ + randomZ
position.y = rangeNumberInclusive(...flowRangeY)
material.setValues({ size: rangeNumberInclusive(...flowSizeRange) })
})
}
複製代碼
如今粒子流的泵已經準備好了,咱們進一步實現粒子流的效果,打開微信跳一跳,擼幾把......
應該能發現粒子除了是直線運動,也是勻速的(就算不是勻速,也將它處理成勻速吧),也能夠先不關心速度,這裏還需考慮些東西,那就是粒子流是一直運行的(只有不鬆開手指),而後到達腳下的粒子也是在不斷的被重置位置並開始向腳下移動,因此這裏咱們沒有辦法使用Tweenjs來控制動畫,由於不曉得粒子流會運行多久,那麼這裏惟一能參考的就只有時間了,咱們能夠根據時間流失(時間差)的多少來肯定粒子應該走多遠,而後約定一個粒子的固定速度,那麼配合requestAnimationFrame這個api
// 約定一個固定速度,每毫秒走多遠
const speed = triggerObjectWidth * 3 / 1000
const prevTime = 0
const animate = () => {
if (prevTime) {
const diffTime = Date.now() - prevTime
// 粒子的行程
const trip = diffTime * speed
}
prevTime = Date.now()
requestAnimationFrame(animate)
}
複製代碼
如今咱們能算出粒子的行程,那麼算出粒子下一次的座標也就簡單了,根據當前的視角畫一張圖來理解:
在每個幀時,根據上一次的座標和終點算出上一次粒子離小人腳下的距離,同時根據時間差和速度能算出粒子本次應該走多遠,而後用類似三角形的特性,咱們就能算出z'、x'、y'
,也就是粒子的新位置。同時,粒子流還須要有一箇中止的方法,用來在鬆開手指時終止
// 粒子流
runParticleFlow () {
if (this.runingParticleFlow) {
return
}
this.runingParticleFlow = true
const { world, triggerObjectWidth, particleSystem, initalY } = this
let prevTime = 0
// 約定速度,每毫秒走多遠
const speed = triggerObjectWidth * 3 / 1000
const animate = () => {
const id = requestAnimationFrame(animate)
if (this.runingParticleFlow) {
// 抽粒子
this.runParticleFlowPump()
if (prevTime) {
const actives = particleSystem.children.filter(child => child.position.y !== initalY)
const diffTime = Date.now() - prevTime
// 粒子的行程
const trip = diffTime * speed
actives.forEach(particle => {
const { position } = particle
const { x, y, z } = position
if (y < initalY) {
// 只要粒子的y值超過安全值,就認爲它已經到達終點
position.y = initalY
position.x = 0
position.z = 0
} else {
const distance = Math.sqrt(Math.pow(x, 2) + Math.pow(z, 2) + Math.pow(y - initalY, 2))
const ratio = (distance - trip) / distance
position.x = ratio * x
position.z = ratio * z
position.y = ratio * y
}
})
world.stage.render()
}
prevTime = Date.now()
} else {
cancelAnimationFrame(id)
}
}
animate()
}
// 中止粒子流
stopRunParticleFlow () {
this.runingParticleFlow = false
this.resetParticle()
}
複製代碼
如今,不出意外,粒子流效果已經實現了,在小人蓄力階段去觸發它,而後鬆開手指時中止它。接下來咱們實現粒子噴泉相關邏輯。首先,粒子噴泉粒子泵也是直接使用小人腳下的粒子,根據個人觀摩,噴泉的粒子數量要稍微少一些
// 粒子噴泉
runParticleFountain () {
if (this.runingParticleFountain) {
return
}
this.runingParticleFountain = true
const { particleSystem, quantity, initalY } = this
// 粒子泵只關心腳下的粒子(水池)
const particles = particleSystem.children.filter(child => child.position.y === initalY).slice(0, quantity)
if (!particles.length) {
return
}
const {
triggerObjectWidth,
fountainRangeX, fountainSizeRange, fountainRangeY
} = this
const halfWidth = triggerObjectWidth / 2
particles.forEach(particle => {
const { position, material } = particle
const randomX = rangeNumberInclusive(...fountainRangeX)
const randomZ = rangeNumberInclusive(...fountainRangeX)
// 小人的身體內,不能成爲起點,須要根據正反將身體的寬度加上
const excludeX = randomX < 0 ? -halfWidth : halfWidth
const excludeZ = randomZ < 0 ? -halfWidth : halfWidth
position.x = excludeX + randomX
position.z = excludeZ + randomZ
position.y = rangeNumberInclusive(...fountainRangeY)
material.setValues({ size: rangeNumberInclusive(...fountainSizeRange) })
})
// 噴射粒子
this.runParticleFountainPump(particles, 1000)
}
複製代碼
如今,實現粒子噴泉粒子泵,它的邏輯和粒子流粒子泵的邏輯相似,座標計算方法都是同樣的,不一樣的地方是因爲粒子噴泉的粒子各有各的終點,須要將終點記錄起來(能夠用userData屬性),並且粒子噴泉不須要終止方法,只須要注意一下,若是當前粒子噴泉尚未結束時觸發了粒子流,則當即中止粒子噴泉,讓粒子流看起來有一個連貫的效果。而後粒子噴泉應該是在落地時觸發
// 粒子噴泉粒子泵
runParticleFountainPump (particles, duration) {
const { fountainRangeDistance, triggerObjectWidth, initalY, world } = this
// 隨機設置粒子的終點
particles.forEach(particle => {
const { position: { x, y, z } } = particle
const userData = particle.userData
userData.ty = y + rangeNumberInclusive(...fountainRangeDistance)
// x軸和z軸 向外側噴出
const diffX = rangeNumberInclusive(0, triggerObjectWidth / 3)
userData.tx = (x < 0 ? -diffX : diffX) + x
const diffZ = rangeNumberInclusive(0, triggerObjectWidth / 3)
userData.tz = (z < 0 ? -diffZ : diffZ) + z
})
let prevTime = 0
const startTime = Date.now()
const speed = triggerObjectWidth * 3 / 800
const animate = () => {
const id = requestAnimationFrame(animate)
// 已經在腳下的粒子不用處理
const actives = particles.filter(particle => particle.position.y !== initalY)
if (actives.length && !this.runingParticleFlow && Date.now() - startTime < duration) {
if (prevTime) {
const diffTime = Date.now() - prevTime
// 粒子的行程
const trip = diffTime * speed
actives.forEach(particle => {
const {
position,
position: { x, y, z },
userData: { tx, ty, tz }
} = particle
if (y >= ty) {
// 已經到達終點的粒子,從新放到腳下去
position.x = 0
position.y = initalY
position.z = 0
} else {
const diffX = tx - x
const diffY = ty - y
const diffZ = tz - z
const distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2) + Math.pow(diffZ, 2))
const ratio = trip / distance
position.y += ratio * diffY
position.x += ratio * diffX
position.z += ratio * diffZ
}
})
world.stage.render()
}
prevTime = Date.now()
} else {
this.runingParticleFountain = false
cancelAnimationFrame(id)
}
}
animate()
}
複製代碼
如今,粒子效果終於完成了。整個功能其實還有不少待考慮的地方,這裏主要只是針對小人實現,若是後續須要作的更通用一點,能夠優化一下。
我按照這個官方例子,嘗試了好久,就是看不到一丟丟殘影🤮,估計是哪一個不太明顯的地方用法不對,殘影以後有時間再實現,若是朋友們有此經驗,能夠在下方留言提示一下,感激涕零。
前面已經實現大部分遊戲邏輯,此時的遊戲中,小人能隨意跳躍,而且無論從什麼位置起跳,下一次它老是躍向下一個盒子,同時在小人跳躍以前咱們就已經算出落地點,因此,這裏的死亡斷定只須要判斷落地點是否在盒子上就ok了,那麼直接使用threejs相關的api爲Prop
類實現一個containsPoint
方法
// 檢測點是否在盒子內
containsPoint (x, y, z) {
const { body } = this
// 更新包圍盒
body.geometry.computeBoundingBox()
// 更新盒子世界矩陣
body.updateMatrixWorld()
// 點的世界座標,y等於盒子高度,這裏須要-1
const worldPosition = new THREE.Vector3(x, y - 1, z)
const localPosition = worldPosition.applyMatrix4(new THREE.Matrix4().getInverse(body.matrixWorld))
return body.geometry.boundingBox.containsPoint(localPosition)
}
複製代碼
如今小人的jump
方法中能夠肯定落地後的狀態
if (nextProp.containsPoint(jumpDownX, propHeight, jumpDownZ)) {
// 躍向當前盒子
// 生成新盒子、移動場景......
} else if (!currentProp.containsPoint(jumpDownX, propHeight, jumpDownZ)) {
// gameOver
}
複製代碼
可是......這個方法只對立方體有效,若是是圓柱體就無法用了,因此這裏不能直接使用包圍盒來檢測。既然不能用包圍盒,那就本身算唄,因爲死亡是統一在一個高度斷定的,因此能夠簡化爲計算一個點是否在平面內,即落地點是否在盒子的頂部平面,也就是說,只須要知道當前盒子是立方體仍是圓柱體,而後分別處理一下就能算出來點是否在盒子上了。因爲我沒有找到判斷當前盒子類型的方法,而且BufferGeometry
通過clone
以後也沒法使用instanceof
來判斷是不是BoxBufferGeometry
或者CylinderBufferGeometry
,因此,我在通用的立方體中使用了userData
屬性
// 立方體
export const baseBoxBufferGeometry = new THREE.BoxBufferGeometry(1, 1, 1, 10, 4, 10)
baseBoxBufferGeometry.userData.type = 'box'
// 圓柱體
export const baseCylinderBufferGeometry = new THREE.CylinderBufferGeometry(1, 1, 1, 30, 5)
baseCylinderBufferGeometry.userData.type = 'Cylinder'
複製代碼
如今,將containsPoint
方法改造一下
containsPoint (x, z) {
const { body } = this
const { type } = body.geometry.userData
const { x: sx, z: sz } = this.getSize()
const { x: px, z: pz } = this.getPosition()
if (type === 'box') {
const halfSx = sx / 2
const halfSz = sz / 2
const minX = px - halfSx
const maxX = px + halfSx
const minZ = pz - halfSz
const maxZ = pz + halfSz
return x >= minX && x <= maxX && z >= minZ && z <= maxZ
} else {
const radius = sx / 2
// 小人腳下中心點離圓心的距離
const distance = Math.sqrt(Math.pow(px - x, 2) + Math.pow(pz - z, 2))
return distance <= radius
}
}
複製代碼
若是須要實現跌落效果,咱們須要將gameOver
分支再細分一下,打開微信跳一跳擼一擼......
對於狀況1,處理起來至關簡單,而對於狀況2,我們還得再琢磨琢磨
currentProp
的遠邊緣nextProp
的近邊緣nextProp
的遠邊緣nextProp
的兩側邊緣,這取決於盒子的大小和距離如今整理一下
currentProp
的遠邊緣nextProp
的近邊緣nextProp
的遠邊緣nextProp
的兩側邊緣小人完處於半空中,直接讓小人垂直下落就行
小人落在盒子邊緣,這種狀況的跌落須要將動做分解爲3個,一個是旋轉,一個是向下位移,而後是想外位移。對於這個過程,我拿着我那包快抽完的軟白沙煙盒在電腦桌上開啓了個人小實驗,思考了良久以後,我決定將效果實現的比較貼近天然一點,可是在實現過程當中,碰到了比較麻煩的東西(數學太弱了),以後仔細體會了微信跳一跳的處理方式,發現他們其實也並無想將這些細節作得盡善盡美,畢竟這只是整個遊戲中的一個不起眼的小插曲。因此我也就退一步用簡單的方式實現,或者熟悉物理引擎的朋友們也能夠考慮物理引擎。用一張圖來描述一下個人簡單思路。
首先,肯定一下支撐點(圖中紅點),而後讓小人沿着支撐點旋轉90度,接着將小人着地。在跌落以前,得先讓小人以統一的姿式站好(否則算起來太麻煩),也就是說,假設此遊戲中小人的正前方是Z軸
方向,不作處理時,若跌落的方向不是Z軸
就須要計算出3個方向的角度和位移值,反之若是將小人旋轉到當前的跌落方向,咱們就能統一以小人的本地座標系來實現動效。那如今約定小人的正前方是Z軸
,經過調整Y軸
角度後,統一調整小人X軸
值向下旋轉,調整Z軸
值讓小人向下跌落,調整Y軸
值讓小人在跌落過程當中向外側偏移
Y軸
旋轉的角度,讓小人面朝跌落方向如今將以前的containsPoint
方法改造一下:
/** * 計算跌落數據 * @param {Number} width 小人的寬度 * @param {Number} x 小人腳下中心點的X值 * @param {Number} z 小人腳下中心點的Z值 * @return { * contains, // 小人中心點是否在盒子上 * isEdge, // 是否在邊緣 * translateZ, // 將小人旋轉部分移動 -translateZ,將網格移動translateZ * degY, // 調整小人方向,而後使用小人的本地座標進行平移和旋轉 * } */
computePointInfos (width, x, z) {
const { body } = this
if (!body) {
return {}
}
const { type } = body.geometry.userData
const { x: sx, z: sz } = this.getSize()
const { x: px, z: pz } = this.getPosition()
const halfWidth = width / 2
// 立方體和圓柱體的計算邏輯略有差異
if (type === 'box') {
const halfSx = sx / 2
const halfSz = sz / 2
const minX = px - halfSx
const maxX = px + halfSx
const minZ = pz - halfSz
const maxZ = pz + halfSz
const contains = x >= minX && x <= maxX && z >= minZ && z <= maxZ
if (contains) {
return { contains }
}
const translateZ1 = Math.abs(z - pz) - halfSz
const translateZ2 = Math.abs(x - px) - halfSx
// 半空中
if (translateZ1 >= halfWidth || translateZ2 >= halfWidth) {
return { contains }
}
// 計算是否在盒子的邊緣
let isEdge = false
let degY = 0
let translateZ = 0
// 四個方向上都有可能
if (x < maxX && x > minX) {
if (z > maxZ && z < maxZ + halfWidth) {
degY = 0
} else if (z < minZ && z > minZ - halfWidth) {
degY = 180
}
isEdge = true
translateZ = translateZ1
} else if (z < maxZ && z > minZ) {
if (x > maxX && x < maxX + halfWidth) {
degY = 90
} else if (x < minX && x > minX - halfWidth) {
degY = 270
}
isEdge = true
translateZ = translateZ2
}
return {
contains,
translateZ,
isEdge,
degY
}
} else {
const radius = sx / 2
// 小人腳下中心點離圓心的距離
const distance = Math.sqrt(Math.pow(px - x, 2) + Math.pow(pz - z, 2))
const contains = distance <= radius
if (contains) {
return { contains }
}
// 半空中
if (distance >= radius + halfWidth) {
return { contains }
}
// 在圓柱體的邊緣
const isEdge = true
const translateZ = distance - radius
let degY = Math.atan(Math.abs(x - px) / Math.abs(z - pz)) * 180 / Math.PI
if (x === px) {
degY = z > pz ? 0 : 180
} else if (z === pz) {
degY = x > px ? 90 : 270
} else if (x > px && z > pz) {
} else if (x > px && z < pz) {
degY = 180 - degY
} else if (z < pz) {
degY = 180 + degY
} else {
degY = 360 - degY
}
return {
contains,
translateZ,
isEdge,
degY
}
}
}
複製代碼
而後,就能根據這個方法實現跌落的效果了,首先改造一下小人的jump
方法,增長一個落地後的回調,在回調中判斷是否死亡,若是沒有死亡,則執行緩存效果並生成新的道具繼續遊戲,反之,根據計算出的結果讓小人跌落。
// 跳躍
jump () {
const {
stage, body,
currentProp, nextProp,
world: { propHeight }
} = this
const duration = 400
const start = body.position
const target = nextProp.getPosition()
const { x: startX, y: startY, z: startZ } = start
// 開始遊戲時,小人從第一個盒子正上方入場作彈球下落
if (!currentProp && startX === target.x && startZ === target.z) {
// ...
} else {
if (!currentProp) {
return
}
const { bodyScaleSegment, headSegment, G, world, width } = this
const { v0, theta } = this.computePowerStorageValue()
const { rangeR, rangeH } = computeObligueThrowValue(v0, theta * (Math.PI / 180), G)
const { jumpDownX, jumpDownZ } = computePositionByRangeR(rangeR, start, target)
// 水平勻速
// ...
// y軸上升段、降低段
const rangeHeight = Math.max(world.width / 3, rangeH) + propHeight
const yUp = animate(
{
from: { y: startY },
to: { y: rangeHeight },
duration: duration * .65,
easing: TWEEN.Easing.Cubic.Out,
autoStart: false
},
({ y }) => {
body.position.setY(y)
}
)
const yDown = animate(
{
from: { y: rangeHeight },
to: { y: propHeight },
duration: duration * .35,
easing: TWEEN.Easing.Cubic.In,
autoStart: false
},
({ y }) => {
body.position.setY(y)
},
() => yDownCallBack()
)
yUp.chain(yDown).start()
// 空翻
this.flip(duration)
// 從起跳開始就回彈
currentProp.springbackTransition(500)
// 落地後的回調
const yDownCallBack = () => {
const currentInfos = currentProp.computePointInfos(width, jumpDownX, jumpDownZ)
const nextInfos = nextProp.computePointInfos(width, jumpDownX, jumpDownZ)
// 沒有落在任何一個盒子上方
if (!currentInfos.contains && !nextInfos.contains) {
// gameOver 遊戲結束,跌落
console.log('GameOver')
this.fall(currentInfos, nextInfos)
} else {
bufferUp.onComplete(() => {
if (nextInfos.contains) {
// 落在下一個盒子才更新場景
// 落地後,生成下一個方塊 -> 移動鏡頭 -> 更新關心的盒子 -> 結束
world.createProp()
world.moveCamera()
this.currentProp = nextProp
this.nextProp = nextProp.getNext()
}
// 粒子噴泉
this.particle.runParticleFountain()
// 跳躍結束了
this.jumping = false
}).start()
}
}
// 落地緩衝段
const bufferUp = animate(
{
from: { s: .8 },
to: { s: 1 },
duration: 100,
autoStart: false
},
({ s }) => {
bodyScaleSegment.scale.setY(s)
}
)
}
}
複製代碼
接下來根據前面的分析實現跌落方法fall
。
// 跌落
fall (currentInfos, nextInfos) {
const {
stage, body,
world: { propHeight }
} = this
let degY, translateZ
if (currentInfos.isEdge && nextInfos.isEdge) {
// 同時在2個盒子邊緣
return
} else if (currentInfos.isEdge) {
// 當前盒子邊緣
degY = currentInfos.degY
translateZ = currentInfos.translateZ
} else if (nextInfos.isEdge) {
// 目標盒子邊緣
degY = nextInfos.degY
translateZ = nextInfos.translateZ
} else {
// 空中掉落
return animate(
{
from: { y: propHeight },
to: { y: 0 },
duration: 400,
easing: TWEEN.Easing.Bounce.Out
},
({ y }) => {
body.position.setY(y)
stage.render()
}
)
}
// 將粒子銷燬掉
this.particle.destroy()
const {
bodyRotateSegment, bodyScaleSegment,
headSegment, bodyTranslateY,
width, height
} = this
const halfWidth = width / 2
// 將旋轉原點放在腳下,同時讓小人面向跌落方向
headSegment.translateY(bodyTranslateY)
bodyScaleSegment.translateY(bodyTranslateY)
bodyRotateSegment.translateY(-bodyTranslateY)
bodyRotateSegment.rotateY(degY * (Math.PI / 180))
// 將旋轉原點移動到支撐點
headSegment.translateZ(translateZ)
bodyScaleSegment.translateZ(translateZ)
bodyRotateSegment.translateZ(-translateZ)
let incrementZ = 0
let incrementDeg = 0
let incrementY = 0
// 第一段 先沿着支點旋轉
const rotate = animate(
{
from: {
degY: 0
},
to: {
degY: 90
},
duration: 500,
autoStart: false,
easing: TWEEN.Easing.Quintic.In
},
({ z, degY }) => {
bodyRotateSegment.rotateX((degY - incrementDeg) * (Math.PI / 180))
incrementDeg = degY
stage.render()
}
)
// 第二段 跌落,沿z軸下落,沿y軸向外側偏移
const targZ = propHeight - halfWidth - translateZ
const fall = animate(
{
from: {
y: 0,
z: 0
},
to: {
y: halfWidth - translateZ,
z: targZ,
},
duration: 300,
autoStart: false,
easing: TWEEN.Easing.Bounce.Out
},
({ z, y }) => {
headSegment.translateZ(z - incrementZ)
bodyScaleSegment.translateZ(z - incrementZ)
bodyRotateSegment.translateY(y - incrementY)
incrementZ = z
incrementY = y
stage.render()
}
)
rotate.chain(fall).start()
}
複製代碼
如今跌落基本已經實現,但此時的跌落時是能夠穿過盒子的,這也是比較麻煩的一點,因爲算力有限,這裏僅作一個簡單的碰撞效果
那麼首先得實現一個檢測物體碰撞的方法,找來找去仍是得用到射線,而後在網上找到了這個粒子。要用這個方式,首先須要注意一下物體的頂點數量,若是太多的話,那性能就無法看,因此
Z
值大於0的頂點(差很少取一半)。😄實際上是能夠算出盒子某一側的頂點的,而且也能夠算出小人的路徑通過的那部分頂點,若是這樣作了,那就是幾十倍的優化,由於在動畫requestAnimationFrame
過程當中,大量計算很容易形成卡頓。X軸
或者Y軸
(世界座標系),還需須要知道小人墜落的方向(基於方向的先後),好比像圖中同樣倒向紅色盒子,須要過濾掉紅色盒子距離小人遠端的頂點,若倒向的是綠色盒子,則須要過濾掉綠色盒子離小人遠端的頂點。下面,根據上面的分析,將射線檢測方法改造一下
/** * 獲取靜止盒子的碰撞檢測器 * @param {Mesh} prop 檢測的盒子 * @param {String} direction 物體過來的方向(世界座標系) * @param {Boolean} isForward 基於方向的先後 */
export const getHitValidator = (prop, direction, isForward) => {
const origin = prop.position.clone()
const vertices = prop.geometry.attributes.position
const length = vertices.count
// 盒子是靜止的,先將頂點到中心點的向量準備好,避免重複計算
const directionVectors = Array.from({ length })
.map((_, i) => new THREE.Vector3().fromBufferAttribute(vertices, i))
.filter(vector3 => {
// 過濾掉一部分盒子離小人遠端的頂點
if (direction === 'z' && isForward) {
// 從當前盒子倒向目標盒子
return vector3.z < 0
} else if (direction === 'z') {
// 從目標盒子倒向當前盒子
return vector3.z > 0
} else if (direction === 'x' && isForward) {
return vector3.x < 0
} else if (direction === 'x') {
return vector3.x > 0
}
})
.map(localVertex => {
const globaVertex = localVertex.applyMatrix4(prop.matrix)
// 先將向量準備好
return globaVertex.sub(prop.position)
})
return littleMan => {
for (let i = 0, directionVector; directionVector = directionVectors[i]; i++) {
const raycaster = new THREE.Raycaster(origin, directionVector.clone().normalize())
const collisionResults = raycaster.intersectObject(littleMan, true)
// 發生了碰撞
if(collisionResults.length > 0 && collisionResults[0].distance < directionVector.length() + 1.2 ){
return true
}
}
return false
}
}
複製代碼
接下來,將fall
方法完善一下,增長碰撞檢測
// 跌落
fall (currentInfos, nextInfos) {
const {
stage, body, currentProp, nextProp,
world: { propHeight }
} = this
// 跳躍方向
const direction = currentProp.nextDirection
let degY, translateZ,
validateProp, // 須要檢測的盒子
isForward // 相對方向的前、後
if (currentInfos.isEdge && nextInfos.isEdge) {
// 同時在2個盒子邊緣
return
} else if (currentInfos.isEdge) {
// 當前盒子邊緣
degY = currentInfos.degY
translateZ = currentInfos.translateZ
validateProp = nextProp
isForward = true
} else if (nextInfos.isEdge) {
// 目標盒子邊緣
degY = nextInfos.degY
translateZ = nextInfos.translateZ
// 目標盒子邊緣多是在盒子前方或盒子後方
if (direction === 'z') {
isForward = degY < 90 && degY > 270
} else {
isForward = degY < 180
}
validateProp = isForward ? null : currentProp
} else {
// 空中掉落
return animate(
{
from: { y: propHeight },
to: { y: 0 },
duration: 400,
easing: TWEEN.Easing.Bounce.Out
},
({ y }) => {
body.position.setY(y)
stage.render()
}
)
}
// 將粒子銷燬掉
this.particle.destroy()
const {
bodyRotateSegment, bodyScaleSegment,
headSegment, bodyTranslateY,
width, height
} = this
const halfWidth = width / 2
// 將旋轉原點放在腳下,同時讓小人面向跌落方向
headSegment.translateY(bodyTranslateY)
bodyScaleSegment.translateY(bodyTranslateY)
bodyRotateSegment.translateY(-bodyTranslateY)
bodyRotateSegment.rotateY(degY * (Math.PI / 180))
// 將旋轉原點移動到支撐點
headSegment.translateZ(translateZ)
bodyScaleSegment.translateZ(translateZ)
bodyRotateSegment.translateZ(-translateZ)
let incrementZ = 0
let incrementDeg = 0
let incrementY = 0
let hitValidator = validateProp && getHitValidator(validateProp.body, direction, isForward)
// 第一段 先沿着支點旋轉
const rotate = animate(
{
from: {
degY: 0
},
to: {
degY: 90
},
duration: 500,
autoStart: false,
easing: TWEEN.Easing.Quintic.In
},
({ degY }) => {
if (hitValidator && hitValidator(body.children[0])) {
rotate.stop()
hitValidator = null
} else {
bodyRotateSegment.rotateX((degY - incrementDeg) * (Math.PI / 180))
incrementDeg = degY
stage.render()
}
}
)
// 第二段 跌落,沿z軸下落,沿y軸向外側偏移
const targZ = propHeight - halfWidth - translateZ
const fall = animate(
{
from: {
y: 0,
z: 0
},
to: {
y: halfWidth - translateZ,
z: targZ,
},
duration: 300,
autoStart: false,
easing: TWEEN.Easing.Bounce.Out
},
({ z, y }) => {
if (hitValidator && hitValidator(body.children[0])) {
fall.stop()
// 稍微處理一下,頭撞到盒子的狀況
const radian = Math.atan((targZ - z) / height)
if (isForward && direction === 'z') {
bodyRotateSegment.translateY(-height)
body.position.z += height
body.rotateX(-radian)
} else if (direction === 'z') {
bodyRotateSegment.translateY(-height)
body.position.z -= height
body.rotateX(radian)
} else if (isForward && direction === 'x') {
bodyRotateSegment.translateY(-height)
body.position.x += height
body.rotateZ(radian)
} else if (direction === 'x') {
bodyRotateSegment.translateY(-height)
body.position.x -= height
body.rotateZ(-radian)
}
stage.render()
hitValidator = null
} else {
headSegment.translateZ(z - incrementZ)
bodyScaleSegment.translateZ(z - incrementZ)
bodyRotateSegment.translateY(y - incrementY)
incrementZ = z
incrementY = y
stage.render()
}
}
)
rotate.chain(fall).start()
}
複製代碼
到這裏,跌落和碰撞就差很少實現完成了,還有很大的瑕疵,因此,若是朋友你看到這裏以爲不太友好的話,暫時很抱歉。若後續我須要更多的涉及到threejs,我再來優化它🙏。
這個效果和粒子效果相似,建立後將它們添加到小人的組合中,須要的時候亮出來就行。
這個中心點在全局只須要建立一個,而後在須要時顯示它,波紋可能就是中心點擴散的效果。
在盒子上停留加分這種功能估計須要支持外部自定義,提供給外部加分的api,可是因爲外部不知道停留多久,因此還得經過一種方式告訴外部小人在盒子上的整個生命週期過程,既然這樣,那就乾脆支持一下外部定義盒子的生命週期(相似vue、react的方式),可能包括盒子建立、小人跳上盒子時、小人蓄力時、小人起跳離開時等等......而後遊戲內部在不一樣時期調用對應的鉤子。
大體能想到的就這些了,但願對你有幫助。