写在后面
SegmentFault 思否的吉祥物是一只举世无双、特立独行、酷爱自在的 (>^ω^<) 独角猫,也是社区的首席摸鱼官。于是本次作品是以思否猫为原型搭配原生 js 实现了一个相似跑酷的小游戏
游戏链接
https://simplerobort.github.i…
技术栈形成
(javascript 做游戏可用的引擎:1、Babylon.js;2、Three.js;3、Turbulenz;4、Famo.us;5、PlayCanvas.js;6、Goo Engine;7、CooperLicht;8、Voxel 等。)
由上可见 js 可能应用的游戏引擎还挺多,最近为了进军元宇宙在学 threeJs,为了更好锤炼对于游戏的了解,因而没有应用任何游戏引擎 (有轮子我偏不必,我造!)
采纳的技术栈为:vue+vuex+vueRouter+ 原生 js(人物的碰撞,跳跃,自由落体等)
实现过程与思考
设计思路
效果图如下(障碍物很潦草哈哈)
- 将人物作为一个模块,将挪动、死亡、跳跃相干代码逻辑搁置在人物模块,次要是感觉这样更加正当,是因为有了人物才会有这个性能,所以这些性能应该和人物强绑定
- 将障碍物,触碰会死亡的障碍物作为模块复用
- store 治理公共状态:以后已过关卡、障碍物信息、人物是否正在挪动相干信息
- router 治理页面:欢送页面、每一个关卡、死亡页面、胜利页面
- 游戏外围模块:人物挪动跳跃、物理碰撞
人物实现
人物款式
人物的静止、跳跃、死亡应用 gif,通过双向绑定 src,依据逻辑替换就实现了工作状态的变动
<template>
<div class="human" :style="{left:Hleft,bottom:Hbottom}" ref="human">
<img :src="humanPic" alt="">
</div>
</template>
左右挪动
从人物款式的代码截图能够看到,人物的挪动其实只是将人物的 left 与 bottom 双向绑定到组件状态中,在之后便能够实现通过疾速批改 left 与 bottom 来实现人物的挪动
左右挪动的逻辑:
在人物组件初始化时会调用 moveOpen 并且 moveOpen 只做了三件事
- 监听键盘按下
- 监听键盘弹起
- 调用 moveClock
moveOpen: function () {document.onkeydown = () => {...}
document.onkeyup = () => {...}
this.moveClock()},
上面具体聊聊这三件事内都做了什么
1. 监听键盘按下
假如咱们当初按住了 D 键挪动,其实做了这两件事
- 如果曾经是死亡状态就进行逻辑(因为死亡有动画,避免死亡动画内还能挪动)
- goRight 函数内将人物的向右挪动状态改为 true
document.onkeydown = () => {if (this.isDead) return
const key = window.event.keyCode
switch (key) {
case 65:
// A
this.goLeft(0)
break
case 68:
// D
this.goRight(0)
break
case 87:
case 32:
// W
this.jump(8)
break
}
}
goLeft: function (type) {if (type === 0) {this.changeStatus('isLeft', true) // changeStatus 就是一个批改 store 的封装
} else if (type === 1) {this.changeStatus('isLeft', false)
}
},
goRight: function (type) {if (type === 0) {this.changeStatus('isRight', true)
} else if (type === 1) {this.changeStatus('isRight', false)
}
},
2. 监听键盘弹起
弹起只做了一件事:将人物的对应的挪动状态改为 false
document.onkeyup = () => {
const key = window.event.keyCode
switch (key) {
case 65:
// A
this.goLeft(1)
break
case 68:
// D
this.goRight(1)
break
}
}
3. 调用 moveClock
moveclock 其实就是开启了一个定时器,每次执行 4 件事
- 副作用拦挡:在未挪动状态或者死亡状态不执行,走 return
- 游戏界面限度:当君子在右边或者最左边不容许持续走出
- 障碍物:判断是否碰到障碍物来决定是否死亡或者胜利或者进行挪动
- 下坠:判断是否合乎下坠条件
- 挪动:最初执行挪动
moveClock: function () {setInterval(() => {const { human} = this.$refs
const {cantgo, freeFall} = this.$store.getters
const {stayIndex, humanInfo} = this.$store.state
const Hleft = parseFloat(this.Hleft)
const Hbottom = parseFloat(this.Hbottom)
// 因为计时器始终在走,不在左右挪动状态或者在死亡状态时不执行逻辑
if (this.isDead || (!humanInfo.isLeft && !humanInfo.isRight)) return
// 当君子在最右边或者最左边时不容许在挪动
const condition = humanInfo.isLeft
? human.offsetLeft <= 0
: humanInfo.isRight && (human.offsetLeft + 15) >= 1280
if (condition) {
this.Hleft = humanInfo.isLeft ? '0px' : '1265px'
return
}
// 判断是否碰到了障碍物来决定是走路还是死亡还是胜利还是进行
const left = humanInfo.isLeft ? Hleft - 5 : Hleft + 15
if (cantgo(left, Hbottom, 'stopArea')) return
if (cantgo(left, Hbottom, 'deadArea')) return this.gotDead()
if (cantgo(left, Hbottom, 'winArea')) return this.win()
// stayindex 不是 - 1 就代表以后踩的是障碍物而不是高空
// freefall 判断有没有走出障碍物,走出就下坠
if (stayIndex !== -1 && freeFall(Hleft)) this.jump(0)
// 最初执行挪动
const forWard = humanInfo.isLeft ? -2.5 : 2.5
this.Hleft = Hleft + forWard + 'px'
}, 10)
}
人物跳跃
人物跳跃为了仿真逐步变慢直到降落落地的过程采纳了递归递加
人物跳跃次要做了三件事
- 拦挡副作用:当曾经在跳跃状态或者死亡状态不容许再次跳跃
- 批改跳跃状态:批改跳跃状态为真,将人物 gif 换成跳跃的 gif
- 跳跃计算:执行一个递归函数 jp
jp 函数外部次要做了 3 件事
- 判断是否碰到禁止挪动障碍物,触碰后如果是回升状态就将速度修改为 0 并且开始降落行为,如果是降落状态就进行跳跃行为
- 判断是否碰到死亡障碍物或者是胜利障碍物来决定是否死亡与进入胜利画面
- 判断这次位移后是否碰到高空,碰到就进行跳跃,若没有碰到就减速度并且递归 jp 函数直到碰到障碍物或者高空
jump: function (speed) {
// 如果死亡或者正在跳跃回绝发动跳跃行为
if (this.isDead || this.$store.state.humanInfo.isJump) return
// 将跳跃状态改为真
this.changeStatus('isJump', true)
this.humanPic = require('@/assets/human/jump1.0.1.gif')
const jp = () => {const { cantJump} = this.$store.getters
const jpheight = parseFloat(this.Hbottom)
const Hleft = parseFloat(this.Hleft)
// 判断回升或者着落时是否碰到障碍物触发特定行为
if (cantJump(speed > 0, jpheight, Hleft, 'stopArea')) {
// 以后处于降落就进行
if (speed <= 0) return this.jumpStop()
// 以后处于回升就间接将速度改为 0 变成降落
speed = 0
}
if (cantJump(speed > 0, jpheight, Hleft, 'deadArea')) return this.gotDead()
if (cantJump(speed > 0, jpheight, Hleft, 'winArea')) return this.win()
// 判断是否碰地来抉择持续回升降落还是进行跳跃
const isTouchLand = jpheight + speed <= 187.61
this.Hbottom = isTouchLand ? '187.61px' : jpheight + speed + 'px'
if (isTouchLand) {this.$store.commit('changestayIndex', -1)
return this.jumpStop()}
// 没有碰到任何意外状况,减速度持续递归
speed -= 0.2
setTimeout(jp, 10)
}
jp()},
障碍物实现
障碍物初始化
目前共有三种类型的障碍物:死亡障碍物、进行障碍物、胜利障碍物
并且是以无状态组件的形式实现,目标是为了只须要关注障碍物的地位而不是我要用一次就要写一次逻辑
能够看到障碍物生成时只是把本人的宽高定位信息传给了 store,store 会把信息保留在数组里
<template>
<div id="rock" class="onTheLand" :style="{left:this.rleft,width:this.rwidth,height:this.rheight,bottom:this.rbottom}"></div>
</template>
<script>
export default {
name: 'rock',
props: ['rleft', 'rwidth', 'rheight', 'rbottom'],
created () {this.init()
},
methods: {init: function () {
let obj = {rl: parseFloat(this.rleft.replace('px', '')),
rw: parseFloat(this.rwidth.replace('px', '')),
rh: parseFloat(this.rheight.replace('px', '')),
rb: parseFloat(this.rbottom.replace('px', ''))
}
this.$store.commit('addStopArea', obj)
}
}
}
</script>
<style scoped>
#rock {
border: 2px solid black;
/*background-color: black;*/
}
</style>
障碍物碰撞原理
人物在每次挪动时会调用 store 提供的 api 来判断是否碰到某一类型的障碍物,所以只须要实现障碍物碰撞的计算判断即可
人物挪动时依据障碍物裸露的三个 api 来实现碰撞计算:cantgo、cantJump、freeFall
cantgo 是人物左右挪动时判断是否碰到障碍物的 api,传入三个参数:人物的 left、人物的 bottom、须要判断的障碍物类型
进行障碍物判断的传的 left 其实是人物假如挪动当前的 left,判断的形式是循环判断挪动后的 left 是否在障碍物外部,如果是的就代表碰到了
cantgo: function (state) {return function (left, feet, area) {if (!state.humanInfo.isRight && !state.humanInfo.isLeft) return false
return state[area].some(
item => (
left >= item.rl &&
left < (item.rl + item.rw) &&
feet >= item.rb &&
feet < (item.rb + item.rh)
)
)
}
},
cantJump 是人物跳跃时是否碰到障碍物的 api,与 cantgo 同理,只是新增一个是否在回升阶段,因为起飞阶段如果碰到了障碍物进行,会保留阻碍的 index 到 stayindex,这个值在 freeFall 会用到
cantJump: function (state) {return function (isUp, feet, left, area) {if (isUp) {return state[area].some(
item => ((feet + 60) >= item.rb &&
(feet + 60) < (item.rb + item.rh) &&
(left + 15) >= item.rl &&
left <= (item.rl + item.rw)
)
)
} else {return state[area].some((item, index) => {
if (feet >= item.rb &&
feet <= (item.rb + item.rh + 6) &&
(left + 15) >= item.rl &&
left <= (item.rl + item.rw)
) {
state.stayIndex = index
return true
}
}
)
}
}
},
freeFall 是在障碍物上挪动时判断是否走出障碍物的 api
通过 stayIndex 判断在哪个障碍物上,在判断 left 是否小于障碍物左侧或者大于右侧
freeFall: function (state) {return function (left) {if (state.isJump) return false
const isRightDown = state.humanInfo.isRight &&
left > (state.stopArea[state.stayIndex].rl + state.stopArea[state.stayIndex].rw)
const isLeftDown = state.humanInfo.isLeft &&
(left + 15) <= state.stopArea[state.stayIndex].rl
return isLeftDown || isRightDown
}
}
完结
前端一昧的开发必然干燥,要可能从干燥的生存中找到你工作的乐趣
本文参加了 1024 程序员节,欢送正在浏览的你也退出。