最近全民热点事件就是世界杯了嘛,身为伪球迷为了蹭一波热度,于是在之前射击小游戏(戳这:canvas实现飞机打怪兽射击小游戏)基础上改了一个适配手机的接球手小游戏。
这个游戏其实比射击小游戏简化了,减少了场景、子弹类,主要区别的地方是把键盘事件改为了手指触摸事件,增加音效。
按照惯例,先甩 demo 地址:https://littleyljy.github.io/demo/soccergame/
游戏规则及场景
游戏规则简单粗暴,手指拖动守门员完成接球操作,然后根据接球数返回接球结果。场景也只有三个:
- 开始游戏(.game-intro)
- 游戏中(#canvas)
- 游戏通关(.game-all-success)
面向对象
定义父类、子类的方法在射击小游戏(还是戳这:canvas实现飞机打怪兽射击小游戏)中已经阐述过了,这里就不详细讲了。这回把飞机对象当成守门员,怪兽对象当成足球(因为懒癌患者,类名没改代码直接复用了)。
怪兽部分可以照搬,飞机控制子弹的方法(如 hasHit() 、shoot()、drawBullets())都不需要,有区别的地方是:
- 原来飞机只是左右移动,现在守门员需要全屏移动,即守门员 (x, y) 坐标值都会改变,direction() 改为 setPosition(x, y)。
- 还增加了一个判断守门员是否与足球接触的 hasCrash() 碰撞检测方法。
//方法:飞机方向
Plane.prototype.setPosition = function (x, y) {
this.x = x;
this.y = y;
return this;//方便链式调用
};
//方法:碰撞检测
Plane.prototype.hasCrash = function(target){
var crash = false;
if(!(target.x + target.size < this.x) &&
!(this.x + this.width < target.x) &&
!(target.y + target.size < this.y) &&
!(this.y + this.height < target.y)){
crash = true;
}
return crash;
};
关于矩形碰撞检测的原理如下:
矩形2 和 矩形1 之间没有发生碰撞共有四种可能的情况:
- 矩形2的右侧 离 矩形1的左侧有一段距离
- 矩形2的左侧 离 矩形1的右侧有一段距离
- 矩形2的底部 离 矩形1的顶部有一段距离
- 矩形2的顶部 离 矩形1的底部有一段距离
当符合上面其中一种情况,则两个矩形没有发生碰撞。当上面四种情况都不满足的时候,则代表两个矩形碰撞了。
我们把守门员当成一个矩形,通过判断四边是否有间距来确定是否发生了碰撞,碰撞返回 true。
游戏逻辑
因为大部分逻辑与射击小游戏差不多,主要说下触摸事件、生成足球、返回的结果和游戏音效。
1、触摸事件
- touchstart:当在屏幕上按下手指时触发。
- touchmove:当在屏幕上移动手指时触发。
- touchend:当在屏幕上抬起手指时触发
- touchcancel:当一些更高级别的事件发生的时候(如电话接入或者弹出信息)会取消当前的touch操作,即触发 touchcancel。一般会在 touchcancel 时暂停游戏、存档等操作。(此段出处:移动端web开发---Touch事件详解)
实现思路是(此处请脑补飞机就是守门员):我们需要记录手指刚触摸屏幕的坐标(startTouchX),手指移动到某点时候的坐标(newTouchX),然后相减就能得到手指滑动距离(newTouchX - startTouchX),滑动距离加飞机初始坐标(startPlaneX)就能得到飞机的新坐标(newPlaneX)。
我们把新坐标传入飞机的 setPosition(x, y) 方法,就能修改飞机的 (x, y) 属性了。当手指离开屏幕再次触摸时(即再次触发 touchstart ),就把飞机上一次的新坐标作为飞机初始坐标。
(关于 touches、targetTouches、changedTouches 三个触摸点列表可以戳这里了解:js中触摸相关变量touches,targetTouches和changedTouches的区别)
bindTouchEvent: function () {
var self = this;
//飞机位置
var newPlaneX = this.newPlaneX;
var newPlaneY = this.newPlaneY;
//手指初始位置坐标
var startTouchX;
var startTouchY;
//飞机初始位置
var startPlaneX;
var startPlaneY;
//首次触屏
canvas.addEventListener('touchstart', function (e) {
var plane = self.plane;
//记录首次触摸位置
startTouchX = e.touches[0].pageX;
startTouchY = e.touches[0].pageY;
//consol.log('touchstart', startTouchX, startTouchY);
//记录飞机初始位置
startPlaneX = plane.x;
startPlaneY = plane.y;
});
//滑动触屏
canvas.addEventListener('touchmove', function (e) {
var newTouchX = e.touches[0].pageX;
var newTouchY = e.touches[0].pageY;
console.log('newTouch', newTouchX, newTouchY);
//飞机新坐标=飞机起始坐标+手指滑动距离
newPlaneX = startPlaneX + newTouchX - startTouchX;
newPlaneY = startPlaneY + newTouchY - startTouchY;
console.log('touchmove', newPlaneX, newPlaneY);
if (newPlaneX < self.planeMinX) {
newPlaneX = self.planeMinX;
}
if (newPlaneX > self.planeMaxX) {
newPlaneX = self.planeMaxX;
}
if (newPlaneY < self.planeMinY) {
newPlaneY = self.planeMinY;
}
if (newPlaneY > self.planeMaxY) {
newPlaneY = self.planeMaxY;
}
self.plane.setPosition(newPlaneX, newPlaneY);
//禁止默认事件,防止滚动屏幕
e.preventDefault();
});
},
2、生成足球
为了制造随机出球的效果,下面这个公式不可少:
获取 [min, max] 范围的计算公式:Math.random() * (max - min + 1) + min
可以从三个参数控制:
- (x, y) 坐标
- 速度
- 开始方向
足球横坐标在屏幕宽度范围内随机生成,纵坐标从[距离屏幕上边界一个屏高,0]范围内随机生成,这样下落的高度是随机的。
另外球速有慢有快,也是使用随机函数,取值范围可以自定义。
//生成敌人
createEnemy: function (enemyType) {
var opts = this.opts;
var level = opts.level;
var enemies = this.enemies;
var numPerLine = opts.numPerLine;
var padding = opts.canvasPadding;
var gap = opts.enemyGap;
var size = opts.enemySize;
var speed = opts.enemySpeed;
//每升级一关敌人多numPerLine个
for (var i = 0; i < level * numPerLine; i++) {
var initOpt = {
x: parseInt(Math.random() * (canvasWidth - size + 1) + padding, 10),
y: -parseInt(Math.random() * (canvasHeight - size + 1) + padding, 10),
size: size,
speed: Math.round(Math.random() * 10 + 3, 10),
status: enemyType,
enemyDirection: randomDirection(),
enemyIcon: opts.enemyIcon,
enemyBoomIcon: opts.enemyBoomIcon
};
enemies.push(new Enemy(initOpt));
}
return enemies;
},
足球一开始移动的方向随机左右,使用 randomDirection() 函数来实现,通过 Math.round(Math.random()) 随机生成 0 或 1 其中一个数,然后定义 0 代表左,1代表右,然后返回方向。
//随机生成方向
function randomDirection() {
var direction = Math.round(Math.random());//随机赋值0或1
direction = direction === 0 ? 'left' : 'right';
return direction;
}
3、返回的结果
定义一个函数 famousMan(score) 不同档次得分时返回不同的背景图和文字,只要在 end() 中调用即可。
end: function (status) {
var self = this;
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
this.setStatus(status);
totalScoreText.innerText = this.score;
resultText.innerText = famousMan(self.score).goalkeeper;
resultDescText.innerText = famousMan(self.score).resultdesc;
effect.pause();
return;
},
//返回结果
function famousMan(score) {
var goalkeeper = '';
var resultdesc = '';
var total = (CONFIG.numPerLine + CONFIG.totalLevel * CONFIG.numPerLine) * CONFIG.totalLevel / 2;
if (0 <= score && score < total / 3) {
goalkeeper = '中国队派来的卧底';
resultdesc = 'emmmm...啥也不想说了';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-1.png)';
} else if (total / 3 <= score && score < total / 2) {
goalkeeper = '冰岛门将哈尔多松';
resultdesc = '不好好接球就要回去当导演了';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-2.png)';
} else if (total / 2 <= score && score < total * 2 / 3) {
goalkeeper = '俄罗斯门将阿金费耶夫';
resultdesc = '草原雕不发威当我是小鸡咕咕';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-6.png)';
} else if (total * 2 / 3 <= score && score < total * 4 / 5) {
goalkeeper = '墨西哥门将奥乔亚';
resultdesc = '北美吴镇宇零封卫冕冠军出局';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-5.png)';
}else if (total * 4 / 5 <= score && score < total *9 / 10) {
goalkeeper = '英格兰门将皮克福德';
resultdesc = '小个子保送三喵军团进四强';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-3.png)';
} else {
goalkeeper = '哇塞,获得金手套!';
resultdesc = '你太牛了金手套非你莫属';
gameAllSuccess.style.backgroundImage = 'url(./img/bg-end-4.png)';
}
console.log('goalkeeper', goalkeeper);
return {
goalkeeper: goalkeeper,
resultdesc: resultdesc
}
}
4、游戏音效
手机游戏为了体验效果,音效一般都是必不可少的。
首先在 HTML 中定义了两个 <audio> 标签,一个放背景音乐,一开始自动播放加循环(关于有些浏览器不允许页面一加载就自动播放的问题,我在canvas实现HTML5“正义联盟要造反”小动画这篇文章最后有提到一些解决方案),另一个是守门员接到球时触发的接球音效。
<audio id="bg-music" src="./audio/soccer.mp3" autoplay loop></audio>
<audio id="effect-music" src="./audio/biu.mp3"></audio>
在每次更新足球状态的时候,我之前尝试直接使用 effect.play(),但实际效果是当前音效播放完毕后才重新播放,这样会造成在很短时间内接中了好几个球,但是只播放了一次音效的问题。目前我的解决方案是 effect.cloneNode().play() ,每次接到一个球就复制并返回调用它的节点的副本。缺点是当生成的足球非常多的时候,会占用大量内存。
//更新敌人状态
updateEnemeis: function () {
var opts = this.opts;
var plane = this.plane;
var enemies = this.enemies;
var i = enemies.length;
//循环更新敌人
while (i--) {
var enemy = enemies[i];
if (enemy.x < this.enemyMinX || enemy.x >= this.enemyMaxX) {
enemy.enemyDirection = enemy.enemyDirection === 'right' ? 'left' : 'right';
}
enemy.down();
enemy.direction(enemy.enemyDirection);
switch (enemy.status) {
case 'normal':
if (plane.hasCrash(enemy)) {
enemy.booming();
effect.cloneNode().play();//会造成资源变大!
}
if (enemy.y >= canvasHeight) {
enemies.splice(i, 1);//移出屏幕底部时从数组中删除
}
break;
case 'booming':
enemy.booming();
break;
case 'boomed':
enemies.splice(i, 1);
this.score += 1;
break;
default:
break;
}
}
},
另外 iOS 下微信浏览器中,只能听到背景音乐,听不到音效,而在手机QQ自带的浏览器中正常。
一个题外话
说个题外话,当时把这个 demo 链接发到微信朋友圈,第二天中午,微信居然提示该链接包含恶意欺诈内容,如下:
很纳闷,游戏既没有分享提示,也没有获取用户信息,咋就恶意欺诈了呢?然后看了下微信规则(《微信外部链接内容管理规范》),发现微信现在对外链的管理超级严格,里面提到一条感觉勉强沾边的:
H5游戏、测试类内容
以游戏、测试等方式,吸引用户参与互动的,具体形式包括但不限于比手速、好友问答、性格测试,测试签、网页小游戏等;
若内容中包含以上情况,一经发现,立即停止链接内容在朋友圈继续传播、停止对相关域名或IP地址进行的访问;对于情节恶劣的情况,永久封禁帐号、域名、IP地址。
难道是因为这个 H5 是网页小游戏,吸引用户参与互动,接球形式又类似拼手速?
然后很奇怪的是,当天下午又莫名其妙解封了。(@_@;)
emmm......总之一脸懵。(°ー°〃)
因为本人才疏学浅,有些想法可能比较表面,有不足之处还请指出。