微信小游戏——贪吃蛇

博客简介

本篇博客介绍的是微信小游戏贪吃蛇的案例,详细的开发过程,并且提供代码压缩包下载。

微信小游戏贪吃蛇

案例简介

作者学习微信小程序时偶然想到贪吃蛇游戏,画布绘制了一番,突发了做贪吃蛇的想法。由于还没有正式开始学习小游戏,并且只是想要简单的做出这个程序来,就没有去用游戏引擎,深究小游戏中的细节。游戏基于canvas以及响应事件,效果如下。(由于微信审核小城序名称的限制,将其命名为贪吃毛毛虫)

在这里插入图片描述

布局

整个小游戏界面的布局很简单,包括小蛇,食物,以及纯色背景,插入的文字得分组成。

在这里插入图片描述

构建对象

按照布局中的元素,我们构建蛇头对象,蛇身对象,食物三个对象,并且添加相应的位置,颜色,大小属性:

//蛇头对象
var snakeHead = {
  x: 100,
  y: 100,
  r: 15,
  deg: 0,
  snakeDirection: "right",
  color: '#CD5C5C',
  drawHead: function () {
    //画出嘴脸this.
    ctx.save();
    ctx.translate(this.x, this.y);
    ctx.rotate(this.deg);//旋转
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.arc(0, 0, this.r, Math.PI / 7, -Math.PI / 7, false);
    ctx.fillStyle = this.color;
    ctx.fill();
    ctx.beginPath();
    ctx.arc(0, -this.r / 2, 2, Math.PI * 2, 0, true);
    ctx.fillStyle = 'black';
    ctx.fill();
    ctx.restore();
  }
}
//蛇身数组  
var snakeBody = [];
//食物数组
var foods = [];
//创建食物对象
//food构造函数
function Food() {
  this.x = parseInt(Math.random() * windowWidth);
  this.y = parseInt(Math.random() * windowHeight);
  this.r = parseInt(Math.random() * 10 + 10);
  this.color = "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")";
  //重新随机位置和颜色
  this.reset = function () {
    this.x = parseInt(Math.random() * windowWidth);
    this.y = parseInt(Math.random() * windowHeight);
    this.r = parseInt(Math.random() * 10 + 10);
    this.color = "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")";
  }
}
//创建食物对象数组
function makeFoods() {
  for (var i = 0; i < 15; i++) {
    //push20个食物对象
    foods.push(new Food());
  }
}

事件监听

在正式开始绘制之前,我们要确定snake的移动方向,这就需要我们根据玩家手指滑动的方向来确定当前是上滑动还是下滑动还是左滑动或是右滑动?我们可以这样来设置监听:

  • onTouchStart获取当前触摸点x,y坐标
  • onTouchMove获取当前触摸点x坐标
  • onTouchEnd发生时将方向改为相应的上下左右
wx.onTouchEnd(function (res) {
  snakeHead.snakeDirection = direction;
})

wx.onTouchMove(function (res) {
  moveX = res.changedTouches[0].clientX // 重新判断当前触摸点x坐标
  moveY = res.changedTouches[0].clientY // 重新判断当前触摸点y坐标
  dx = moveX - startX;
  dy = moveY - startY;
  if (Math.abs(dx) > Math.abs(dy)) {
    if (dx > 0) direction = "right";
    else if (dx < 0) direction = "left";
  }
  else if (Math.abs(dx) < Math.abs(dy)) {
    if (dy > 0) direction = "buttom";
    else if (dy < 0) direction = "top";
  }
})

wx.onTouchStart(function (res) {
  startX = res.changedTouches[0].clientX // 重新判断当前触摸点x坐标
  startY = res.changedTouches[0].clientY // 重新判断当前触摸点y坐标
})

蛇头对象的移动

整个snake对象想要动起来我们就必须给snake定义一个animation函数,一帧一帧的移动位置。

  • 蛇头对象在每一帧开始时根据方向来移动一个距离
function animation() {
  //开始绘制
 //判定方向
  switch (snakeHead.snakeDirection) {
    case 'top': snakeHead.y -= 1.8 * snakeHead.r; snakeHead.deg = -Math.PI / 2; break;
    case 'buttom': snakeHead.y += 1.8 * snakeHead.r; snakeHead.deg = Math.PI / 2; break;
    case 'left': snakeHead.x -= 1.8 * snakeHead.r; snakeHead.deg = Math.PI; break;
    case 'right': snakeHead.x += 1.8 * snakeHead.r; snakeHead.deg = 0; break;
  }
  ctx.save();
  ctx.clearRect(0, 0, windowWidth, windowHeight);
  ctx.fillStyle ="#8A2BE2";
  ctx.fillRect(0, 0, windowWidth, windowHeight);
  //绘制蛇头
  snakeHead.drawHead();
}

蛇身的移动

蛇身对象的移动我们可以这样考虑:

  • 蛇身是一个对象数组
  • 整个蛇第i节下一帧的位置就是他的上一个节点(i-1)的位置
  • 添加新的一节在蛇头的位置
  • 移除最后一节蛇身

这样一来蛇头向前移动,就像蛇头的位置添加一个新的蛇身节点,移除最后一节

function animation() {
  //添加新的身体
  snakeBody.push({
    x: snakeHead.x,
    y: snakeHead.y,
    r: snakeHead.r,
    color: "#708090"
  });

  //判定方向
  switch (snakeHead.snakeDirection) {
    case 'top': snakeHead.y -= 1.8 * snakeHead.r; snakeHead.deg = -Math.PI / 2; break;
    case 'buttom': snakeHead.y += 1.8 * snakeHead.r; snakeHead.deg = Math.PI / 2; break;
    case 'left': snakeHead.x -= 1.8 * snakeHead.r; snakeHead.deg = Math.PI; break;
    case 'right': snakeHead.x += 1.8 * snakeHead.r; snakeHead.deg = 0; break;
  }

  //绘制身体
  //没有碰撞食物则移除最后一节身体(下标为0,cover掉)
  //否则不做移除,长度增加,改isCollision为false表示未碰撞
  //绘制身体数组
  for(var i=0;i<snakeBody.length;i++)
  {
    draw(snakeBody[i]);
  }
  //绘制蛇头
  snakeHead.drawHead();
  //绘制得分
  ctx.save();
}

食物刷新

食物的刷新很简单,食物对象是一个数组,我们在每一帧发生时将食物根据当前位置绘制在画布上:

  • 初始化时生成15个食物对象,随机位置,大小和颜色
  • 在每一帧发生时根据食物当前位置绘制在画布上
//创建食物对象
//food构造函数
function Food() {
  this.x = parseInt(Math.random() * windowWidth);
  this.y = parseInt(Math.random() * windowHeight);
  this.r = parseInt(Math.random() * 10 + 10);
  this.color = "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")";
  //重新随机位置和颜色
  this.reset = function () {
    this.x = parseInt(Math.random() * windowWidth);
    this.y = parseInt(Math.random() * windowHeight);
    this.r = parseInt(Math.random() * 10 + 10);
    this.color = "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")";
  }
}
//创建食物对象数组
function makeFoods() {
  for (var i = 0; i < 15; i++) {
    //push20个食物对象
    foods.push(new Food());
  }
}

//获取屏幕宽高并且初始化食物对象,蛇身对象
function window() {
  wx.getSystemInfo({
    success(res) {
      windowWidth = res.windowWidth;
      windowHeight = res.windowHeight;
      canvas.width = windowWidth;
      canvas.height = windowHeight;
    }
  });

  makeFoods();//初始化20个食物
  //绘制食物
  for (var i = 0; i < foods.length; i++) {
    draw(foods[i]);
  }

  snakeBody.push({
    x: snakeHead.x,
    y: snakeHead.y,
    r: snakeHead.r,
    color: "#708090"
  });
}
function animation() {
  //添加新的身体
  snakeBody.push({
    x: snakeHead.x,
    y: snakeHead.y,
    r: snakeHead.r,
    color: "#708090"
  });

  //判定方向
  switch (snakeHead.snakeDirection) {
    case 'top': snakeHead.y -= 1.8 * snakeHead.r; snakeHead.deg = -Math.PI / 2; break;
    case 'buttom': snakeHead.y += 1.8 * snakeHead.r; snakeHead.deg = Math.PI / 2; break;
    case 'left': snakeHead.x -= 1.8 * snakeHead.r; snakeHead.deg = Math.PI; break;
    case 'right': snakeHead.x += 1.8 * snakeHead.r; snakeHead.deg = 0; break;
  }
  //开始绘制
  //绘制食物
  ctx.save();
  ctx.clearRect(0, 0, windowWidth, windowHeight);
  ctx.fillStyle ="#8A2BE2";
  ctx.fillRect(0, 0, windowWidth, windowHeight);
  for (var i = 0; i < foods.length; i++) {
    draw(foods[i]);
  }
  //绘制身体
  //绘制身体数组
  for(var i=0;i<snakeBody.length;i++)
  {
    draw(snakeBody[i]);
  }
  //绘制蛇头
  snakeHead.drawHead();
}

碰撞检验

在蛇和食物可以顺利的刷新移动后,我们要给蛇设置碰撞检验,包括蛇和墙壁的检验,蛇和食物的检验

(1)检验触壁

检验小蛇和墙壁的触碰很简单,只要在animation函数中判断当前蛇头的位置是否在屏幕范围内,如果不在范围内说明触壁,游戏结束

//判定是否触壁
function JudgeStared(){
    if(snakeHead.x<0||snakeHead.x>windowWidth||snakeHead.y<0||
    snakeHead.y>windowHeight) started=false;
}

(2)检验碰撞食物

检验碰撞食物很简单,遍历整个食物数组,判断蛇头圆心和食物圆心的位置关系,如果距离大于半径和说明碰撞。需要注意的是碰撞后的连锁反应,蛇身增长,食物要随机刷新到另外一个位置,要点如下:

  • 判断碰撞:(R+r)^2 <= (x1-x2)^2 + (y1-y2)^2
  • 蛇身增长:如果碰撞,那么我们不移除蛇尾
  • 碰撞随机刷新到另外一个位置
  • 分数point++
  • 设置变量iscollison来记录当前的碰撞状态
//动画函数
function animation() {
  //添加新的身体
  snakeBody.push({
    x: snakeHead.x,
    y: snakeHead.y,
    r: snakeHead.r,
    color: "#708090"
  });

  //边界
  if (snakeHead.x > windowWidth) snakeHead.snakeDirection = "left";
  if (snakeHead.x <= 0) snakeHead.snakeDirection = "right";
  if (snakeHead.y <= 0) snakeHead.snakeDirection = "buttom";
  if (snakeHead.y > windowHeight) snakeHead.snakeDirection = "top";

  //判定方向
  switch (snakeHead.snakeDirection) {
    case 'top': snakeHead.y -= 1.8 * snakeHead.r; snakeHead.deg = -Math.PI / 2; break;
    case 'buttom': snakeHead.y += 1.8 * snakeHead.r; snakeHead.deg = Math.PI / 2; break;
    case 'left': snakeHead.x -= 1.8 * snakeHead.r; snakeHead.deg = Math.PI; break;
    case 'right': snakeHead.x += 1.8 * snakeHead.r; snakeHead.deg = 0; break;
  }
  //判定是否触壁
  JudgeStared();
  //检验碰撞,移动食物
  for (var i = 0; i < foods.length; i++) {
    isCollision = collision(snakeHead, foods[i]);
    //碰撞则重新绘制
    //发生碰撞
    if (isCollision) {
      foods[i].reset();
      break;
    }
  }
  //开始绘制
  //绘制食物
  ctx.save();
  ctx.clearRect(0, 0, windowWidth, windowHeight);
  ctx.fillStyle ="#8A2BE2";
  ctx.fillRect(0, 0, windowWidth, windowHeight);
  for (var i = 0; i < foods.length; i++) {
    draw(foods[i]);
  }

  //绘制身体
  //没有碰撞食物则移除最后一节身体(下标为0,cover掉)
  //否则不做移除,长度增加,改isCollision为false表示未碰撞
  if (!isCollision) {
    snakeBody.shift();
  }
  else {
    isCollision=false;
  }
  //绘制身体数组
  for(var i=0;i<snakeBody.length;i++)
  {
    draw(snakeBody[i]);
  }
  //绘制蛇头
  snakeHead.drawHead();
}

碰撞函数collision:

//碰撞函数,返回bolean值
function collision(obj1, obj2) {
  var r1 = obj1.r, r2 = obj2.r;
  var dis = (obj1.x - obj2.x) * (obj1.x - obj2.x) + (obj1.y - obj2.y) * (obj1.y - obj2.y)
  if (dis < (r1 + r2) * (r1 + r2)) 
  {
    audioEat.stop();
    audioEat.play();
    point++;
    return true;
  }
  else return false;
}

绘制得分

得分绘制十分简单,插入ctx.fillText(‘当前得分:’ + point, 135, 220)即可

  //绘制得分
  ctx.save();
  ctx.fillStyle ='#B0C4DE';
  ctx.font = "normal 20px 幼圆";
  ctx.fillText('当前得分:' + point, 135, 220);
  ctx.restore();
  if(!started) end();

开始界面和结束界面

现在我们的游戏大体上已经准备好了,但是直接进入游戏未免有些枯燥,我们需要一个开始界面和结束界面,当点击这个界面的时候游戏开始:

  • 设置两个函数end和start分别表示结束界面和开始界面
  • 绘制这个开始或者结束界面
  • 给界面设置监听
  • 当点击屏幕时游戏开始,调用定时器,当碰撞到墙壁时清除定时器
  • 由于开始之后 wx.onTouchStart(function (res))仍然在执行,我们不能一直让监听执行调用定时器,所以我们用started记录当前游戏开始或结束的状态,仅仅在started=false时,可以调用定时器,在started=true时能够清除定时器
function end()
{
  clearInterval(timer);//清除定时器
  audioDie.play();//死亡语音
  audioBackground.stop();//背景音乐
  ctx.save();
  ctx.drawImage(imageStart, 0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.fillStyle = "#8A2BE2";
  ctx.fillRect(windowWidth * 5 / 7, 0, windowWidth * 2 / 7, windowHeight * 1 / 10);
  ctx.arc(canvas.width / 2, canvas.height / 2, 120, 0, Math.PI * 2, false);
  ctx.fillStyle = "#6495ED";
  ctx.fill();
  ctx.fillStyle = '#FF7F50';
  ctx.font = "normal 20px 幼圆";
  ctx.fillText('.游戏结束.', canvas.width / 2 - 50, canvas.height / 2 - 25);
  ctx.fillText('您的得分是'+point+"分", canvas.width / 2 - 60, canvas.height / 2);
  ctx.fillText('->点击屏幕重新开始<-', canvas.width / 2 - 100, canvas.height / 2 + 25);
  ctx.restore();
  //重置参数
  point=0;
  snakeHead.x=100;
  snakeHead.y=100;
  snakeHead.snakeDirection="right";
  direction='right';
  started=false;
  snakeBody.length=0;
  wx.onTouchStart(function (res) {
    if (!started) {
      start();
    }
  })
}
function start()
{
  imageStart.onload = function () {
    ctx.clearRect(0,0,windowWidth,windowHeight);
    ctx.drawImage(imageStart, 0,0,canvas.width, canvas.height);
    ctx.beginPath();
    ctx.fillStyle ="#8A2BE2";
    ctx.fillRect(windowWidth * 5 / 7, 0, windowWidth * 2 / 7, windowHeight * 1 / 10);
    ctx.arc(canvas.width/2,canvas.height/2,120,0,Math.PI*2,false);
    ctx.fillStyle ="#6495ED";
    ctx.fill();
    ctx.fillStyle = '#FF7F50';
    ctx.font = "normal 20px 幼圆";
    ctx.fillText('<贪吃小虫>', canvas.width / 2-50, canvas.height / 2-30);
    ctx.fillText('->点击屏幕开始<-', canvas.width / 2 - 75, canvas.height / 2+20);
    audioBackground.loop = true;
    audioBackground.play();
    //调用定时器
      wx.onTouchStart(function (res) {
        if (!started) {
          audioStart.play();
          started = true;
          timer = setInterval(animation, 500);
          ctx.clearRect(0, 0, windowWidth, windowHeight);
        }
      })
}

在这里插入图片描述
在这里插入图片描述

前台后台的切换

游戏界面有了,但是当切到后台时怎么办?从后台调到前台有如何处理?我们可以这样设置

  • 设置stopped记录当前的路由的来源
  • 当stopped=true表示程序来自后台
  • stopped=false表示直接打开程序
  • 在start函数中添加stopped状态,如果stopped=true直接调用定时器
function start()
{
  imageStart.onload = function () {
    ctx.clearRect(0,0,windowWidth,windowHeight);
    ctx.drawImage(imageStart, 0,0,canvas.width, canvas.height);
    ctx.beginPath();
    ctx.fillStyle ="#8A2BE2";
    ctx.fillRect(windowWidth * 5 / 7, 0, windowWidth * 2 / 7, windowHeight * 1 / 10);
    ctx.arc(canvas.width/2,canvas.height/2,120,0,Math.PI*2,false);
    ctx.fillStyle ="#6495ED";
    ctx.fill();
    ctx.fillStyle = '#FF7F50';
    ctx.font = "normal 20px 幼圆";
    ctx.fillText('<贪吃小虫>', canvas.width / 2-50, canvas.height / 2-30);
    ctx.fillText('->点击屏幕开始<-', canvas.width / 2 - 75, canvas.height / 2+20);
    audioBackground.loop = true;
    audioBackground.play();
    //调用定时器
    if(stopped)//来源后台
    {
      audioStart.play();
      started = true;
      stopped=false;
      timer = setInterval(animation, 500);
      ctx.clearRect(0, 0, windowWidth, windowHeight);
    }
    else
    {
      wx.onTouchStart(function (res) {
        if (!started) {
          audioStart.play();
          started = true;
          timer = setInterval(animation, 500);
          ctx.clearRect(0, 0, windowWidth, windowHeight);
        }
      })
    }
  }
}

wx.onShow(function () {
    start();
})

wx.onHide(function(){
  clearInterval(timer);//清除定时器,暂停
  started=false;//停止运行
  stopped=true;//切后台
})
wx.showShareMenu({
  withShareTicket: true
})

添加音频

此小游戏有4个音频,音频的设置往往能有不错的效果,要点如下

  • 背景音频audioBackground:在小游戏运行在前台的调用
  • 获取食物音频audioEat:在碰撞函数collision中调用
  • 开始音频audioStart:在游戏开始时调用
  • 结束音频audioDie:在游戏结束时调用
var audioBackground = wx.createInnerAudioContext();//背景音乐
audioBackground.src ="audio/background.mp3";
var audioEat = wx.createInnerAudioContext();//吃的声音
audioEat.src = "audio/eat.mp3";
var audioDie = wx.createInnerAudioContext();//死亡声音
audioDie.src = "audio/end.mp3";
var audioStart = wx.createInnerAudioContext();//死亡声音
audioStart.src = "audio/start.mp3";

展示

最后整个小游戏就完成了,展示如下
在这里插入图片描述

微信小游戏贪吃蛇

  • 9
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值