【图片消消乐】单机游戏-微信小程序项目开发入门

这是一个微信小程序项目,是类似开心消消乐的休闲小游戏,老少皆宜,游戏互动里面的图片是用的任何图片素材,根据自己的需求更换图片即可。想要做游戏不知道怎么做,建议从这个小程序入手,花时间研究学习,很快就拥有属于自己的小程序。

准备

  • 会使用微信开发工具
  • 有游戏图片素材
  • 有游戏背景音效

打开微信开发工具,选择一个小程序项目,点+号新建,依次选择

创建小程序

  • 不使用云服务
  • JavaScript - 基础模板

修改项目名,点确定,就可以开始做了

页面布局

首先,开始做项目的时候,设计稿或者效果图是准备好的,这样让我们知道下一步该怎么做

效果图

来看一看效果图,要做的游戏页面是像下面这样的
在这里插入图片描述

布局

有目标,加油干!

这里做页面是用基础模板template来做的,有vue基础的就很容易完成,

在页面文件pages/game/game.wxml中的添加布局,大致如下

<view class="container">
  <view class="row">
    <view class="expand">
      <view class="row">
        <image src="{{firstImg}}" class="icon" mode="scaleToFill" />
        <view>
          <text>×{{scope}}</text>
        </view>
      </view>
    </view>
    <view class="" style="width: 28vw;">
      <view class="row">
        <image src="/static/five_oclock_3d.png" class="icon" mode="scaleToFill"/>
        <view>
          <text>{{timerNum}}s</text>
        </view>
      </view>
    </view>
  </view>
  <view class="expand">
    <view class="canvas-box">
      <canvas class="canvas" id="canv" type="2d" bindtouchstart="onTouchStart" bindtouchend="onTouchEnd"></canvas>
    </view>
  </view>
  <progress percent="{{progressPercent}}" activeColor="#38f" backgroundColor="#ccc" />
</view>

还有样式文件,这里不展开讲,自己知道怎么改样式,做成效果图一样的就好了
效果图中的弹出对话框,这是小程序系统自带的,做的时候忽略掉

以上页面的布局中,有两个部分

  • 一个画布canvas,显示游戏画面
  • 一个显示游戏状态信息,还有进度条

游戏逻辑

接下来,就写写游戏逻辑了,整理一下游戏思路,比如做一份流程图,把思路现出来

流程图

一个流程图出来了,如下所示,这下思路变清晰了吧

Created with Raphaël 2.3.0 游戏开始 初始化 倒计时开始 等待用户选两个图片 是否消除? 进行消除动画,累计数量 倒计时结束? 计算得分 退出游戏? 游戏结束 yes no yes no yes no

大纲

看一下大纲,就能知道大概的游戏逻辑吧,

游戏的相关配置如下,是可以调节的

const MatchCount = 3;//达到3个可消除
const AnimationTime = 10;//动画延迟10ms
const isShowClearLine = false;//圈出欲消除的图片
const maxTimeNum = 300;//倒计时最大值
const ColNum = 7;//列数,数字越大装得图片越多

逻辑代码,都写在一个页面文件pages/game/game.js中,方便阅读,代码如下

Page({
	/**
	 * 页面的初始数据
	 */
	data: {
	  firstImg:"",//第一个图片
	  progressPercent:100,//进度条进度
	  timerNum:maxTimeNum,//计数
	  scope:0,//消除数量
	  imgList:[],//存放游戏图片
	},
	/**
	 * 生命周期函数--监听页面加载
	 */ 
	onLoad(options){
		//...这里处理初始化
	},
	/**
	 * 生命周期函数--监听页面卸载
	 */
	onUnload() {
		//关闭定时器
		if(this.data.timer){
		  clearInterval(this.data.timer);
		}
		//销毁游戏背景音
		if(this.data.audioPlay && this.data.audioPlay.destroy){
		  this.data.audioPlay.destroy(); 
		}
	},
	onTouchStart(e){
		//...用户触摸时,会触发这个事件方法
		//就在这里处理用户选图片的逻辑
	},
	onTouchEnd(){
		//...用户不触摸时,会触发这个事件
	},
	showGameoverModal(){
		//...游戏结束,计算得分,用对话框提示
	},
	initCanvasData(canvasData){
		//...初始化画布数据,用于绘制游戏图
	},
	clearGrids(callback){
		//...处理消除逻辑,包括消除动画,处理完成后回调callback
	},
	//...剩下的方法省略
})

在方法onLoad(options)这里,写初始化逻辑,代码如下

//根据id获取到画布的节点node和大小size
wx.createSelectorQuery().select('#canv').fields({ node:true, size:true },res=>{
	const { width, height, node:canvas } = res;
	const canvasData = {
	  canvas,//画布元素(节点)
	  context: canvas.getContext('2d'),//获取画布的操作对象(上下文)
      columns: ColNum,//每行的个数,设置在6~12之间最佳
	};
	   //这是一些图片名字
	const ImageList = [
	  //...更多图片资源
	  "deer","chicken","chipmunk","cow_face","crab",
	];
	//加入微任务列表
	let taskList = ImageList.map(function(filename){
		//加载图片是异步操作的
	  return new Promise(function(resolve,reject){
	    let image = canvas.createImage();
	    //...省略更多
		//加载项目下static文件夹里面的图片资源
	    image.src=`/static/${filename}_3d.png`;
	  });
	});
	//执行微任务
	Promise.all(taskList).then(imgs=>{
		//执行到这里,微任务执行结束,就显示第一个图片
		if(!this.data.firstImg) {
          this.setData({
            firstImg:'../../'+imgs[0].src
          })
        }
        this.data.imgList = imgs;
        this.initCanvasData(canvasData);
        this.initAudioPlay();//初始化游戏音效
		//...省略更多,
		//开始执行消除处理动画的方法
		  this.clearGrids(()=>{
		    //...省略更多
		    //消除完了,开始倒计时
		    this.data.timer = setInterval(()=>{
		      let num = this.data.timerNum-1;
		      if (num<0){
		      	//倒计时结束了
		        clearInterval(this.data.timer);
		        this.data.timer = null;
		        //游戏结束,弹出对话框提示
		        this.showGameoverModal();
		        return;
		      }
		      // 更新倒计时和进度条
		      this.setData({
		        timerNum:num,
		        progressPercent:Math.trunc(num*100/maxTimeNum)
		      })
		    },1000);
		  });
	}).catch(function(err){
		//如果执行有问题,会将错误输出到控制台
	  console.error(err)
	});
	//...
}).exec();

代码中有调用的其它方法,它们不是重要的,这里不展开讲,
如果你对这个Promise感到陌生,它是处理异步(微)任务的,建议你熟悉 Promise,相信这对你很有帮助

初始化数据

在这个方法initCanvasData(canvasData)里去实现初始化,参数canvasData表示画布数据,很简单,代码如下

let padding = 1;//初始内边距
const bgColor = '#000';//背景色
const lineColor = '#ff3';//选中边框色
//获取初始化数据对象
const { canvas } = this.data.canvasData;
const { width, height } = canvas;
padding += Math.trunc((width-padding*2)%columns/2);
//根据画布宽高,算出单元格大小
const gridSize = Math.trunc((width-padding*2)/columns);
//算出有多少行
const rows = Math.trunc((height-padding*2)/gridSize);
//网格列表
const gridList = [];
for(let row=0; row<rows; row++){
  for(let col=0; col<columns; col++){
    let grid = {
      //...
    };
    //获取随机的图片(索引)
    grid.font = this.getRandomFont();
    //加入列表
    gridList.push(grid);
  }
}
//所有初始化数据放到canvasData中
Object.assign(canvasData, { bgColor, lineColor, gridList, gridSize, padding, columns, rows });
this.data.canvasData = canvasData;

其中方法getRandomFont() 是获取随机的图片,返回图片列表中的索引即可,代码如下,只写一行足矣

return Math.trunc(Math.random()*this.data.imgList.length);

重复写代码是低效率的,要这样做,将重复的代码块放进一个方法中,后面有很多地方会调用到

处理用户选图片

这应该不难吧,在这个方法onTouchStart(e)中去处理,参数e是用户触摸画布时由系统处理传入的,代码如下

//触摸时,获取第一个坐标点对象
const t = e.touches[0];
const { selectedGrid, isAnimating } = this.data;
//如果在进行动画,就直接返回,不继续执行
if(isAnimating) return;
const { context, canvas, gridList, gridSize, bgImg } = this.data.canvasData;
//获取触摸到的图片索引
let gIndex = this.getTouchGridIndex(t);
//若触摸的不是图片,就直接返回
if(gIndex<0) return;
//开始清理画布
this.clearCanvas(true);
context.drawImage(bgImg,0,0,canvas.width,canvas.height);
//选中图片,画出选中小效果
let grid = gridList[gIndex];
//获取图片的坐标
let coord = this.getGridCoordinate(grid);
context.rect(coord.left,coord.top,gridSize,gridSize);
context.stroke();
//将选中的图片存到起
let newSelectedGrid = {
  //...
};
if (selectedGrid) {
  this.data.selectedGrid = null;
  //选到了两个图片,这里开始处理切换动画
  this.startToggleAnimation(selectedGrid,newSelectedGrid);
}else{
  this.data.selectedGrid = newSelectedGrid;
}

其中,两个方法getTouchGridIndex(touch)getGridCoordinate(grid)是很容易实现的,这里不展开讲,

另一个方法 startToggleAnimation(selectedGrid,nextSelectedGrid) ,是处理切换动画效果的,实现起来有难度,这里大致讲一下,代码如下

//...
//判断选的两个图片是否可以切换
const isToggle = this.calcIsClearUp(selectedGrid,nextSelectedGrid);
let hasEq;
//...省略判断逻辑
//如果可以消除
if(hasEq){
  //...
  //设置动画进行中状态
  this.data.isAnimating=true;
  const { canvas, context, gridSize, gridList, bgImg } = this.data.canvasData;
  const { grids, direction } = hasEq;
  //...
  //动画结束方法
  const stopAnimation = () => {
    if(isToggle){
      //...
    }
    //...
    this.redrawBg(()=>{
	    //处理消除逻辑的方法
	    this.clearGrids(res=>{
	      const { count } = res;
	      if(count>0){
	      	//更新消除结果
	        this.setData({
	          scope:this.data.scope+count,
	          timerNum:maxTimeNum,
	          progressPercent:100
	        })
	      }
	    });
    });
  }
  function startAnimation(){
    this.clearCanvas(true);
    context.drawImage(bgImg,0,0,canvas.width,canvas.height);
    //...省略了开始动画逻辑
    setTimeout(startAnimation,AnimationTime*2);
  }
  //重新绘制背景的方法
  this.redrawBg((img)=>{
	 //开始动画
     startAnimation();
  });
}

代码中还用到了好几个方法,这里就不展开讲,
代码中用到了好几个箭头符号,据说这个是叫语法糖,

考虑到有的萌新小同学看不懂,这里讲一下语法糖
箭头符号表示什么意思,看如下代码,应该能明白吧

//箭头函数
const fun1 = (args) => {
	console.log('hello')
};

//编译器把箭头函数还原
function fun1(args) {
	console.log('hello')
}

也许有同学疑问:没看出什么用途。
在一个对象实例 中,写方法(函数 )里有写了this的,就用箭头函数吧。

为方便阅读思路清晰,避免搞混,代码中用到的this,都是统一指向当前页面的实例对象

处理消除逻辑

在一个方法clearGrids(callback,count=0)里实现,这是最难的实现部分,代码如下

const { gridList, gridSize, columns, rows, padding } = this.data.canvasData;
//先扫描可以清除的网格,记录下来
let clearGridsAtCols = [];
for(let x=0; x<columns; x++){
  //...
  for(let y=rows-1; y>=0; y--){
    //...
  }
  //...
}
//如果有清除的网络
if(clearGridsAtCols.length>0) {
	//这里添加需要移动的数据
  clearGridsAtCols.forEach(item=>{
    //...
    for(let y=rows-1; y>=0; y--){
      //...
    }
  });
  const offset = 4;
  //开始动画
  const startAnimating=()=>{
    let isDownMove=false;
    //处理移动的数据,更新移动位置
    clearGridsAtCols.forEach(x=>{
      //...
    });
    //如果没有可以移动的,就结束动画
    if(!isDownMove){
    	//...  
	    this.redrawBg(()=>{
			//...  处理完,回调返回消除数量
	      if(typeof callback == 'function') callback({ count });
	    });
      return;
    }
    this.redrawBg();
    //还能向下移动的,再调用startAnimating方法
    setTimeout(startAnimating,AnimationTime);
  }
	//重新绘制一遍
  this.redrawBg();
  if(!this.data.isAnimating){
    this.data.isAnimating=true;
    this.data.audioPlay.play();//播放游戏音效
    // console.log('start autio')
  }
  startAnimating();
  return true;
}
//没有可消除的,直接返回状态
this.data.isAnimating=false;
return false;

关于项目

相关代码就讲到这了,有了上面的思路,应该能自己做出来吧,接下来是运行测试

运行效果

做好后,运行效果图如下,换个水果之类的图片,看着感觉还可以

在这里插入图片描述
为什么不用表情图片了?

有人体验后反馈,说玩久了眼睛看着会不舒服,

原因吧,是显示的图片又多又小,还有图片之间颜色相似的也多,找起来容易引起视觉疲劳,

使用表情图片大多数都是黄脸,所以颜色相似的很多,这是作为图形设计师的基本常识把,
知道了这点不足,就换了图片,看上面的感觉还好,

运行小程序项目,游戏交互效果,动图如下
请添加图片描述

既然能换图片,那就改名项目为图片消消乐好了

项目源码

  • 用到的图片素材,是参考 fluent-emoji 这里,有很多可以选几个当作游戏素材,
  • 用到的游戏背景音,也是从网上找来的,很容易找到,
  • 想看项目源代码,请前往 下载点这里消消乐项目源码
  • 3
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TA远方

谢谢!收到你的爱╮(╯▽╰)╭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值