开发步骤:
搭建项目–创建玩家–添加重力–玩家动作设计–设计sprite图片–碰撞块–碰撞检验–sprite动画–HitBox实现(键盘和摇杆)–sprite过度–进入大门的动作逻辑–改变关卡–下一步
我打算用kingland作为整个项目的名字,因为整个游戏是关于一个小人在房间里探索,那他就是这个游戏里的国王
然后我们要新建index文件,加上canvas标签,接下来要考虑怎么实现人物动画了,
我们在script里面进行一些测试
首先要获取到canvas对象,我选择用document.getElmentById来获取,给他命名为canvas
接下来我们要开始画画了,那么我们需要用到canvas.getContext(‘2d’)来表示我们要开始在2维上面进行画画,我们把这个赋值给ctx,
然后我们要考虑我们的游戏背景是多大,大部分的显示都是按照16:9的比例来设置宽高的,所以我们的背景也这么大吧,然后这个游戏是一个像素小游戏,而像素小游戏,基本上都是64*64,所以,我们的宽高就可以这样来表示,64*16,64*9。
然后我们要在canvas画一个大的长方形来作为我们的背景,这里我们要用到,fillRect和fillStyle,第一个是来画一个长方形的方法,他需要我们指定这个长方形从哪里开始画,以及大小,第二个是填充颜色,接收颜色参数,那么背景我们就从原点开始,大小就是我们的canvas.width和height了,颜色,我们先用白色代替。
接下来就是创建玩家了
创建之前,我们先想一想,玩家会做什么,以及需要用什么方法来实现,首先玩家,我们可以用一个矩形代替,然后玩家会移动,移动到边缘也会碰壁,玩家会跳跃,那么我们可以像上面实现背景那样,实现一个小矩形代替玩家,然后还需要调用widow的requestAnimationFrame方法来实现动画效果,我们可以先实现一个小测试
基于上面的想法,我们可以创建一个Player类,里面要包含玩家的位置和大小,然后我们还要实现一个动画方法,这个方法用于实现玩家的简单移动,这里需要用到清除画布的方法,因为canvas的绘画,如果你不清除,他会一直存在,所以我们要清除,然后还需要加上之前已经画的背景,这个确实很麻烦,但是我们先这样实现着,我的做法是,让玩家一直向下,直到碰到地板停下来,那么动画停止的条件就是玩家的位置,那么我们要实现这个效果,需要在我们的玩家类里面加上一个draw方法,用来在画布上画我们的玩家,然后我们要实现物理碰撞,那就加一个update方法,实现玩家碰壁就停止,但是如何知道玩家会碰壁呢,我们现在只有玩家位置和大小,那么我们就利用玩家的位置和大小来实现玩家碰壁的检测,玩家的底部位置可以表示为玩家的y坐标+玩家高度,
代码看起来是这样的
那么接下来,我们开始处理重力
涉及到物理知识了,有点小生疏,大家都知道重力的原因,我们在下降的时候会有重力加速度,
x = v平均t,v1=v0+at
那么我们可以模拟一下这个过程,那么我们需要给我们的玩家定义一个速度,这个速度分为x方向的和y方向的,velocity,然后我们给我们的y加上速度,再进行判断,如果到地面了,速度要变成0,同时我们判断地面的条件也需要加上我们的速度来判断,这个条件就变成了,bottom+yelocity小于canvas的height,这里我不知道为什么
接下来需要完善玩家的移动,也就是按键操作玩家运动
接下来,我们要在js里面添加EventListener,w作为跳跃键,要实现跳跃,很简单,给y的速度设置为-10,然后我们要保证一次只能限制一次跳跃,就需要加上判断,只有在velocity.y为0才能执行跳跃,然后是左右移动,如果和跳跃一样实现,会出现左右移动不够丝滑,我们新建keys变量,存储左右键,有一个pressed属性,默认为false,然后给animate中间加判断,如果d是true设置velocity.x为正,否则a为true,velocity.x为负,最后为了实现玩家x移动,要给玩家设置一下x位置为初始x加velocity.x
代码是这样的
接下来是要设置游戏贴图,雪碧图
新建一个Spritejs文件,新建类,构造函数,我们需要图片资源,
class Sprite {
constructor({position, imageSrc, frameRate = 1,
animations, frameBuffer = 2, loop = true,
autoplay = true
}) {
this.position = position
this.image = new Image()
this.image.onload = () => {
this.loaded = true
this.width = this.image.width / this.frameRate
this.height = this.image.height
}
this.image.src = imageSrc
this.loaded = false
this.frameRate = frameRate
this.currentFrame = 0
this.elapsedFrames = 0
this.frameBuffer = frameBuffer
this.animations = animations
this.loop = loop
this.autoplay = autoplay
this.currentAnimation
if(this.animations) {
for(let key in this.animations) {
const image = new Image()
image.src = this.animations[key].imageSrc
this.animations[key].image = image
}
}
}
draw() {
if(!this.loaded) return
const cropbox = {
position: {
x: this.width * this.currentFrame,
y: 0
},
width: this.width,
height: this.height
}
c.drawImage(
this.image, cropbox.position.x, cropbox.position.y,
cropbox.width, cropbox.height, this.position.x,
this.position.y, this.width, this.height
)
this.updateFrames()
}
play() {
this.autoplay = true
}
updateFrames() {
if(this.autoplay === false) return
this.elapsedFrames += 1
if(this.elapsedFrames % this.frameBuffer === 0) {
if(this.currentFrame < this.frameRate -1) {
this.currentFrame += 1
}
else if(this.loop) {
this.currentFrame = 0
}
}
if(this.currentAnimation?.onComplete) {
if(this.currentFrame === this.frameRate - 1 && !this.currentAnimation.isActive) {
this.currentAnimation.onComplete()
this.currentAnimation.isActive = true
}
}
}
}
然后我们需要处理碰撞物体,如果玩家碰到了墙壁,那么他就被堵住了,所以,我们要有一个地图,然后需要设置碰撞,tiled这个能帮助我们自动生成地图上的碰撞代码,也就是把地图转化成代码
我们把生成的地图数据,放到data下面的关卡文件里面,collisionsjs,
const collisionsLevel1 = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0,
0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
0, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
const collisionsLevel2 = [
292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
292, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
292, 0, 0, 0, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
292, 292, 292, 292, 0, 0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 292, 0, 0, 292, 0, 0, 292, 292, 292, 292, 292, 292, 0,
0, 292, 292, 292, 0, 0, 292, 292, 292, 292, 0, 0, 0, 0, 292, 0,
0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 292, 0,
0, 292, 0, 0, 0, 0, 0, 0, 0, 0, 292, 292, 292, 292, 292, 0,
0, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 0, 0, 0, 0]
const collisionsLevel3 = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 0,
0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0,
0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0,
0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 250, 250, 250, 0,
0, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 0,
0, 250, 250, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 250, 0, 0,
0, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 250, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
我们拿到这些地图数据,然后就需要处理,按照每行16列来处理数据,我们定义一个rows存储行,
Array.prototype.parse2D = function() {
const rows = []
for (let i = 0; i < this.length; i += 16) {
rows.push(this.slice(i, i + 16))
}
return rows
}
然后我们能看到数据中哪些250,这些就代表墙壁,我们需要处理一下他们
class CollisionBlock {
constructor({position}) {
this.position = position
this.width = 64
this.height = 64
}
draw() {
c.fillStyle = 'rgba(255, 0, 0, 0.3)'
c.fillRect(this.position.x, this.position.y, this.width, this.height)
}
}
新建墙壁类,接收位置,设置大小和颜色,然后利用canvas填充进去,
然后我们要转化墙壁
Array.prototype.createObjectsFrom2D = function () {
const objects = []
this.forEach((row, y) => {
row.forEach((symbol, x) => {
if (symbol === 292 || symbol === 250) {
// push a new collision into collisionblocks array
objects.push(
new CollisionBlock({
position: {
x: x * 64,
y: y * 64,
},
}))
}
})
})
return objects
}
遍历行对象,获取x和y的值,然后新建墙体对象