美文网首页我爱编程
canvas实现世界杯最佳接球手HTML5小游戏

canvas实现世界杯最佳接球手HTML5小游戏

作者: 立正小歪牙 | 来源:发表于2018-07-14 00:24 被阅读2次

最近全民热点事件就是世界杯了嘛,身为伪球迷为了蹭一波热度,于是在之前射击小游戏(戳这: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......总之一脸懵。(°ー°〃)

因为本人才疏学浅,有些想法可能比较表面,有不足之处还请指出。

相关文章

网友评论

    本文标题:canvas实现世界杯最佳接球手HTML5小游戏

    本文链接:https://www.haomeiwen.com/subject/mgyqpftx.html