本系列文章对应游戏代码已开源 Sinuous game。javascript
每一个游戏都会包含场景和角色。要实现一个游戏角色,就要清楚角色在场景中的位置,以及它的运动规律,并能经过数学表达式表现出来。java
canvas 2d的场景坐标系采用平面笛卡尔坐标系统,左上角为原点(0,0),向右为x轴正方向,向下为y轴正方向,坐标系统的1个单位至关于屏幕的1个像素。这对咱们进行角色定位相当重要。git
游戏中的敌人为无数的红色粒子,往同一个方向作匀速运动,每一个粒子具备不一样的大小。github
入口处经过一个循环来建立Enemy粒子,随机生成粒子的位置x, y。并保证每一个粒子都位于上图坐标系所在象限中。因为 map.width <= x <= 2 * map.width,因此粒子最开始是看不到的。canvas
//index.js function createEnemy(numEnemy) { enemys = []; for (let i = 0; i < numEnemy; i++) { const x = Math.random() * map.width + map.width; const y = Math.random() * map.height; enemys.push(new Enemy({x, y})); } }
接下来只要在update中给粒子一个位移偏量speed,粒子就会作匀速运动。speed越大,速度越快。segmentfault
update() { this.x -= this.speed; //speed为位移偏量 this.y += this.speed; }
因为红色粒子看起来是无穷无尽的,而咱们只是建立了有限个粒子,因此须要在粒子离开视界的时候重置粒子的位置。视界以外的位置开始运动,并保证该位置的随机性。数组
//Enemy.js update() { this.x -= this.speed; //speed为位移偏量 this.y += this.speed; //粒子从左边离开视界 if (this.x < -10) { this.x = map.width + 10 + Math.random() * 30; } //粒子从底部离开视界 if (this.y > map.height + 10) { this.y = -10 + Math.random() * -30; } }
能够用一张图来直观地表示Enemy粒子的运动过程dom
玩家粒子则由鼠标控制,在上一节中咱们已经简单介绍了游戏中的鼠标交互。this
而在手机上的实现还略有差异。手机上的作法是监听手指的位移量并让Player粒子作偏移。而不是每次touch都重置粒子的位置,这样体验就会好不少。spa
//Player.js if (isMobile) { self.moveTo(self.x, self.y); window.addEventListener('touchstart', e => { e.preventDefault(); self.touchStartX = e.touches[0].pageX; self.touchStartY = e.touches[0].pageY; }); //手机上用位移计算位置 window.addEventListener('touchmove', e => { e.preventDefault(); let moveX = e.touches[0].pageX - self.touchStartX; let moveY = e.touches[0].pageY - self.touchStartY; self.moveTo(self.x + moveX, self.y + moveY); self.touchStartX = e.touches[0].pageX; self.touchStartY = e.touches[0].pageY; }); } else { let left = (document.getElementById("game").clientWidth - document.getElementById("world").clientWidth)/2; window.addEventListener('mousemove', (e = window.event) => { self.moveTo(e.clientX - left - 10, e.clientY - 30); }); }
Player 粒子值得一讲的就是它飘逸的尾巴。在通过反复尝试了屡次后才实现这个效果。
首先想到要让尾巴长度固定,那么在每次render的时候,都在尾部渲染固定数量的粒子。那粒子的位置怎么判断呢?
在每次render的时候,咱们往数组添加一个粒子,记录此时的Player坐标,当数组达到必定长度时,删除尾部粒子,添加新粒子。这样尾巴就记录了Player一个短期内的各个时间点位置。看起来就像是"跟随"在Player粒子后面了。
//Player.js render() { self.recordTail(); } recordTail() { let self = this; //保持尾巴粒子个数不变 if (self.tail.length > self.tailLen) { self.tail.splice(0, self.tail.length - self.tailLen); } self.tail.push({ x: self.x, y: self.y }); }
这样只是记录了一些尾巴上点的位置,咱们须要把各个点连起来。这里须要用到lineTo方法。
具体代码实现:
//Player.js renderTail() { let self = this; let tails = self.tail, prevPot, nextPot; map.ctx.beginPath(); map.ctx.lineWidth = 2; map.ctx.strokeStyle = self.color; for(let i = 0; i < tails.length - 1; i++) { prevPot = tails[i]; nextPot = tails[i + 1]; if (i === 0) { map.ctx.moveTo(prevPot.x, prevPot.y); } else { map.ctx.lineTo(nextPot.x, nextPot.y); } //保持尾巴最小长度,并有波浪效果 prevPot.x -= 1.5; prevPot.y += 1.5; } map.ctx.stroke(); self.renderLife(); }
若是只是链接各点,那只能画出Player划过的轨迹,咱们还要给尾巴加上惯性效果,注意到上面有这两行代码
prevPot.x -= 1.5; prevPot.y += 1.5;
每一次render中,让尾巴中的每一个点x-1.5, y-1.5。实际上就是让粒子沿着左下方的方向运动,这跟Enemy粒子的方向是一致的。实现了尾巴惯性摆动的效果。
接下来就是添加尾巴上的生命点,这个就比较简单,只需在尾巴上间隔的某些点,画出圆形就能够了
//Player.js //渲染生命值节点 renderLife() { let self = this; for(let j = 1; j <= self.livesPoint.length; j++) { let tailIndex = j * 5; let life = self.livesPoint[j - 1]; life.render(self.tail[tailIndex]); } } //Life.js render(pos) { let self = this; //粒子撞击后不渲染 if (!this.dead) { map.ctx.beginPath(); map.ctx.fillStyle = self.color; map.ctx.arc(pos.x, pos.y, 3, 0, 2 * Math.PI, false); map.ctx.fill(); } }
Skill粒子实际上能够看作是Enemy中的一种特殊粒子,具备和Enemy同样的运动规律。代码中的Skill也是继承自Enemy的(这有点奇怪..)
Skill粒子具备不一样的属性和颜色,实现起来也很简单。
//Skill.js const COLORS = { shield: '#007766', gravity: '#225599', time: '#665599', minimize: '#acac00', life: '#009955' }; const TEXTS = { shield: '盾', gravity: '力', time: '慢', minimize: '小', life: '命' }; render() { var self = this; map.ctx.beginPath(); self.color = COLORS[self.type]; map.ctx.fillStyle = self.color; map.ctx.arc(self.x, self.y, self.radius, 0, Math.PI*2, false); map.ctx.fill(); }
到此游戏中的角色都介绍完了,下一节要讲的是 《从零开始开发一款H5小游戏(四) 撞击吧粒子-炫酷技能的实现》。