1、绘制背景
main.js脚本已经引入tinyHeart.html中了。假设mian.js里面有很多函数。
我们获得了两个画笔,两个画布。下面来分一下,哪些内容放在canvas1上面,哪些在canvas2上面。
后面的那个canvas2(z-index:0在后面):背景,海葵ane,果实
前面那个canvas1(z-index:1在前面):大小鱼,浮动的dust,浮游生物,成绩,吃果实白色圈圈特效,喂小鱼的特效。
游戏如何动起来?就是需要一个一帧一帧刷新。比如小鱼走动,就是位移不断刷新。定义函数gameloop()
使用了一个函数requestAnIMFrame(gameloop),相对于setInterval和setTimeout更科学。当前绘制完成以后,根据机器性能来确定,间隔多长时间会绘制下一帧。而setInterval和setTimeout是指定时间。万一指定内容很大,不能绘制怎么办?
但是会导致一个问题,fps,每秒多少帧,会有一个动态的时间间隔。
不同的浏览器需要配饰,这里有个commonFunction.js文件,已经打包好,直接拷贝,然后用。先在tinyHeart.html文件中引用。
测试一下,可以循环打印了。
因为循环执行的时间间隔不一定,所以需要知道两帧之间时间间隔。
测试一下:帧与帧之间的时间间隔是不稳定的。
如果小人在走,给一个不稳定的时间差,就可以非匀速运动了。运动比较平稳。
新建一个background.js文件,然后其中写函数:
function drawBackground() { ctx2.drawImage(bgPic,0,0,canWidth,canHeight) }
在main.js中定义一个var bgPic变量。初始化的时候,指定src属性 。定义变量var canWidth和 canHeight,获取canvas的宽和高。初始化的时候canWidth = can1.width; canHeight = can1.height;
把 drawBackground();放在 gameloop()中。background.js加载到html文件中。
2、回顾几个API
先回顾几个api:
beginPath() 方法开始一条路径,或重置当前的路径。
提示:请使用这些方法来创建路径:moveTo()、lineTo()、
arcTo() 以及 arc()。
提示:请使用 stroke() 方法在画布上绘制确切的路径。
beginPath()起始一条路径,或重置当前路径
closePath()创建从当前点回到起始点的路径
strokeStyle设置或返回用于笔触的颜色、渐变或模式
stroke()绘制已定义的路径
lineCap设置或返回线条的结束端点样式
lineWidth设置或返回当前的线条宽度
3、绘制海葵
第一阶段,绘制静态海葵,第二阶段再做动画。就是一个直线。描绘一个路径。如何画?定义一个类,类里设置一个数组,数组每个成员都是一个海葵。数组包括海葵属性,位置,长度等。
新建一个文件ane.js,写一个海葵的类:aneObj
在main.js中初始化函数init中,新建海葵对象,并初始化一下。ane.js引入html中。一个坑:js脚本引入的时候,注意顺序关系,否者可能会出现某函数未定义的情况。
4、果实绘制(静态果实)
黄色果实和蓝色果实
使用的API:drawImage()
关键在于果实是在不断长大的,成熟后开始飘离海葵。飘离速度也是各有不同。
设置一个池子,池子里面有足够数量的果实。每次只需要挑选出需要出生的果实。让它出生,长大往上飘。飘出屏幕就死了。果实有两种主要状态:活跃、不活跃。怎么判断是否活跃?屏幕有15个果实,整个果实池,设置足够大,比如30个,只要有果实需要,池子里面一定有闲置的果实。池子中果实数量有足够多,估算一下。30个果实有两种状态,一种是闲置的,还有一种状态,被指定一种任务,要开始在海葵上生长了,长大之后往上飘,飘出屏幕,告诉它任务完成,回去排队。排队概念,就是把状态set到某个闲置的状态。有任务的海葵又有两种状态,一种是海葵上正在长大,一种是飘的过程。
下面开始写任务:
新建一个文件:fruit.js ,定义一个类。设立数组,来控制很多数量的海葵。
在main.js文件中,定义var fruit变量,在main.js函数的init函数中,fruit = new fruitObj() ; fruit.init() ,再循环gameloop中绘制果实fruit.draw() ,在html文件中引入fruit.js文件。
图片资源绘制的果实有两种状态,一种蓝色,一种黄色。要把图片资源加载进去。
规划如下:认为池子中所有的果实,都是在执行任务的,包括长大,成熟,飘出去,飘出屏幕。这些果实的状态都是活着的,所以上面12行初始化的时候,应该为true。
fruitObj.prototype.draw() 里面先一个for循环,画果实。画果实的步骤,首先找到一个定位,找到一个海葵find an ane ,然后长大grow,最后开始飘fly up。
首先写一下出生函数fruitObj.prototype.bron,随机找一个海葵,总共有50个海葵,记录一下海葵的位置,因为果实是长在海葵上面的。需要知道海葵的坐标。在海葵的数量(50)中随机找一个,并且aneID要是整数值,使用一下floor。x坐标为ane.x[aneID] ,y坐标: canHeight-ane.len[aneID];这两个属性对应坐标值,这个对应的果实的x,y值,在fruitObj中记录一下,添加一个this.x和this.y。初始化的时候,fruitObj.prototype.init函数里初始化个0就行了。出生的时候,fruitObj.prototype.bron给一个正确的值、
再画的时候,就可以用到坐标值了。fruitObj.prototype.draw时候,先不用管果实大小,born是出生时候画的,所以放在函数fruitObj.prototype.init中。初始化的时候让所有果实都出生。效果如下。
有两个问题,首先位置有偏差,因为绘制的时候drawImage()时候从0 0 点开始绘制,所以要减去fruit宽度和高度的一半。
另外一个问题,不是30个果实,因为会有重复的,所以要判断一下,海葵上是不是长了果实。如果长了果实,就找下一个海葵。(这个不做深入了)
5、果实绘制(果实上浮)
果实出生时候是不断变大的。所以要需要控制一下。就是绘制图片的时候,一开始绘制很小的图片,然后慢慢加大。在fruitObj中添加一个属性,this.l 代表果实的长度。出生的时候,this.l[i]为0. fruitObj.prototype.draw中,l会随着时间不断增长。也即是this.l[i] += 0.01*deltaTime。这个deltaTime是每两帧之间的时间间隔。然后画果实的位置,需要变成this.x[i] - this.l[i] *0.5 ,this.y[i] - this.l[i] *0.5 。这时候果实会不断变大,不会停止,所以增加一个判断。如果果实大于14,就停止长大。然后开始往上飘。往上飘的本质就是y坐标在不断变小。
但是有个问题,就是速度比较单一。我们给它一个不同的成长和往上飘的速度。
在fruitObj中添加一个属性this.spd,成长和往上飘的速度。初始化的时候这里给它一个随机的速度:19行
然后画图的时候:
每个果实有个不同的速度。33行成长,37行往上飘。
飘出去之后,做一个判断。飘出之后果实的任务就完成了,所以要变成待命状态。做一个检测,如果y坐标小于10,alive状态就要变成false。只有alive状态为true时候才做成长和往上飘的事情。所以要进行一下判断:
6、果实绘制(果实数量控制)
并不是所有果实成熟后飘上去就完了。而是控制屏幕中果实数量在一定数量范围。
添加游戏规则:保持屏幕上有15个果实。
所以一开始初始化的时候,不应该就让所有的果实出生,添加一个判断,当前屏幕上有点多少个果实是活着。在fruit.js中,添加一个监视功能。判断当前屏幕上有多少个果实是活着的。活着的状态,要做一个数据统计。如果活着的果实少于15个,要发出一个果实,也就是让果实出生。但是有一个细节,哪个果实是闲置,那个是活着的,要判断一下。这里做一个循环,当前果实alive状态是false,就让它出生。
66行添加 sendFruit() 函数执行一下。
同时把fruitMonitor放在主循环里面,每一帧都要判断。
另外:黄色果实和蓝色果实两种。一是他们资源不一样,二是功能不一样。大鱼吃一个黄色果实得10分,蓝色果实可以让吃到的黄色果实数量加倍。
下面做蓝色果实:
区别果实类型,fruitObj增加一个属性,fruitType,果实类型。在初始化的时候,先让它为空。出生的时候,再给一个具体的类型。
做一个随机,让果实是蓝色或者黄色。在绘制图片的时候,根据果实类型,绘制相应的图片。在外面定义一下:
根据果实类型,设置pic的资源:
画图的时候,不在使用this.orange了,而是使用pic如下更改
35行下标掉了,补上,如下:
7、大鱼绘制
画大鱼的身子,尾巴,眼睛
使用的API:
translate() ,
rotate(), 这两个canvas的api
Math.atan2(y,x) 反正切,y在前,JavaScript的api
同时返回是一个 -π 到π
新建一个mom.js,在html中引用。在mom.js中,定义类 momObj,初始化函数init和画图函数draw
main.js中大鱼的对象定义一下。var mom;
main.js 中init时候 给mom赋值,同时初始化。
大鱼吃到的果实,身体会变化,尾巴也会变化。这里先不做动画。
大鱼的属性,包括眼睛,身体,尾巴。
初始化的时候,加载一下资源。如下:
下面开始绘制,把大鱼绘制在中间,而且是第一个canvas上面。先画上去各个部分,然后调整位置,把draw放到主循环中。如下:
大鱼已经画出来了,但是并不像图片的样子,比较粗啊。原图的线条比较细。为什么?
canvas1是一个透明的,只有需要绘制的东西,才不是透明,其他没有绘制的地方,都是透明的。覆盖在第二个canvas上面。需要把前面一帧clear一下,把他清空掉,然后绘制新的。循环里面添加这个功能:64行
在干净的画布上绘制。然后结果:
正常了。大鱼的坐标并不是很对。一是为了调节三张图片之间的距离,同时有利于大鱼旋转这个动作。大鱼是一直跟着鼠标的,为了旋转方便,使用前面介绍的translate方法。给它指定一个相对的原点值。因为这个仅适用大鱼,所以加一个save和restore,来确定使用范围。让这些属性仅使用大鱼:
20行,translate之后,this.x ,this.y就是原点,所以下面坐标都需要改一下:
三张图片的中心位置都固定在一个点了,现在把尾巴往后移动一点;
8、大鱼随着鼠标移动
获取鼠标的位置,首先main.js定义一个鼠标位置变量,var mx,var my
。在初始化的时候,初始化成canvas的中间,也就是mx = canWidth*0.5,my = canHeight*0.5。要捕捉到鼠标的动作,首先canvas添加检测鼠标的事件。把事件放到canvas1上,如图,鼠标移动的时候,就可以监听了:
下面定义一下onMouseMove(e)
打印一下,可以看出,鼠标移动的时候,就获取到了鼠标的坐标。offsetX,offsetY发生事件的地点在事件源元素的坐标系统中的 x 坐标和 y 坐标。
如何让鱼妈妈位置坐标跟随鼠标呢?
在momObj.prototype.draw中做这些事情:
首先做一个lerp ,x,y 什么是ler?就是让一个值趋向目标值。
在commonFunction.js中:
aim是目标值,cur是当前值,ratio是一个系数。然后delta是两点之间距离。比如这次位置是aim+10,delta是10 ,ratio是0.9 ,那么下一次位置就是aim + 9 。
这个时候,大鱼的位置,就会一直跟着鼠标动。大鱼中心位置就是鼠标位置。但是大鱼没有旋转,不是头向着鼠标。在极坐标中,大鱼的方向是π或者-π,看旋转方向,顺时针为π。鼠标和大鱼之间会有一个坐标差。坐标差形成一个夹角。就是鼠标相对于大鱼的角度。大鱼的角度要追随坐标差的角度。添加一个属性,然后初始化:
大鱼的角度知道,然后计算大鱼和鼠标之间的角度:
让大鱼的角度不断趋近与鼠标角度lerp angle:
画图的时候,调用lerpAngle函数。获得这个角度后,让鱼妈妈旋转起来。旋转画布,且只作用于大鱼。并且在translate后面做这个命令。为什么?因为只有先转将相对移动原点,然后才旋转。调整相应的旋转速度,和追寻鼠标速度就可以了。
9、大鱼和果实的碰撞检测
原理:检测大鱼和果实的距离
每一帧的时候判断一下,大鱼和这个果实之间的距离是不是足够近。如果是,就是把果实吃掉了。
新建一个collision.js文件。文件里只有碰撞检测功能。把js保存到html中。
有很多果实,我们要一个循环进行判断。首先判断是不是alive状态,然后判断果实跟大鱼之间的距离,如果足够近,就吃掉啦。
使用一下commonFunction函数中的距离平方函数:
这个就是距离平方封装好的函数。
下面进行判断大鱼和果实距离:
第12行,如果距离小于900,就吃掉果实啦。“吃掉果实”是在fruit.js中添加一个方法。功能让当前果实死亡状态,也就是让alive状态改变。
然后使用这个方法就好了:
10、优化
切换界面之后,果实一直在长大。这是个bug啊。所以优化一下。
因为绘制果实的时候,跟deltaTime成正比的。deltaTime是两帧之间的时间间隔。但是切换到其他页面时候,这个页面的帧就停止了。所以切换过来,两帧的时间间隔就特别大。
给它设置一个最大上限就好了,如上图。
Date.now()返回的是1970年到现在的毫秒数。
11、小鱼绘制
新建一个baby.js,定义一个小鱼的类 babyObj;
在主函数中定义一下小鱼:
并且在main.js中的init()函数中,
下面开始在baby.js中绘制小鱼
1、像大鱼一样,想把图片资源绘制上去。2、让小鱼动起来。3、让小鱼跟随鱼妈妈走。
在babyObj中定义小鱼图片变量。初始化的时候加载。如下进行加载
下面开始绘制小鱼:(小鱼的位置修改了一下)
把draw放到主循环gameloop里面:
小鱼已经画上去了。
开始转移原点坐标:
通过save和restore保存状态。这时候小雨的坐标就成了0 0 点。
调整眼睛身体尾巴的位置:注意先画的在下面,后画的在上面。
下面让小鱼动起来,就是让小鱼的当前坐标对大鱼一个lerp,让它趋向大鱼坐标。
小鱼就跟着大鱼动起来了。下面让小鱼跟着大鱼旋转。就是一个角度的lerp的过程。
先定义angle属性:
然后旋转:
总结:
首先是