纯Javascript写的连连看小游戏,不使用任何游戏引擎

最近写了一个连连看小游戏。JavaScript语言,不使用任何游戏引擎,不依赖任何外部程序,无需安装node.js,不需要http服务,只需双击index.html文件即可运行。当时这样写的初衷是想熟悉纯Javascript编程语言,简单,方便,无需安装一系列的外部环境。在手机上只要用个类似记事本的应用打开js文件,即可编辑。打开index.html文件即可查看运行效果。非常方便。有时想测试一个小的算法设计时,我就用js来实现,方便快速的查看效果。在公交,地铁上,可以随时随地的编写运行。

想要实现个游戏,基本的游戏框架还是要简单搭一搭的。这样的话,整体的游戏逻辑会显得更清晰,有利于后续的调试和功能增强。整个项目我已经放在了github上。 (GitHub - mygithubdream/link_game: Javascript linking game (lianliankan))

整个游戏的基础框架都写在game.js文件中,里面定义了三个对象:Sprite, Scene, Game. 在这里我用JS的对象来模拟面向对象编程语言的“类”的概念。虽然JS在ES6(ECMAScript 2015)中引入了类的概念,但ES6中的类实际上是JavaScript原型链的一种语法糖,也只是包了一层,因此我没有使用这种方式,而是使用了最传统的Javascript语法来实现,如下:

var Sprite={
	createNew:function(gameTmp, imgMrk){
		var _this={};
		_this.game=gameTmp;
		var imgTmp=gameTmp.getImg(imgMrk);
		_this.img=imgTmp;
		_this.wCur=imgTmp.width;
		_this.hCur=imgTmp.height;
		_this.xCur=0;
		_this.yCur=0;
		_this.xClpCur=0;
		_this.yClpCur=0;
		_this.wClpCur=imgTmp.width;
		_this.hClpCur=imgTmp.height;
		_this.alpha=1;
		_this.setPos=function (xTmp, yTmp){
			_this.xCur=xTmp;
			_this.yCur=yTmp;
		};
	
		_this.tick=function(){
			_this.game.ctx.globalAlpha=_this.alpha;
			_this.game.ctx.drawImage(_this.img, _this.xClpCur, _this.yClpCur, _this.wClpCur, _this.hClpCur, _this.xCur, _this.yCur,_this.wCur, _this.hCur);
			//console.log("sprite tick");
		};

		_this.onClck=function(){
			//console.log("sprite is clicked.")
		}

		return _this;
	}
};

通过Sprite.createNew()的调用来生成一个类实例。

继承的实现也简单,例如:我们继承Sprite, 生成子类SprBox:

var SprBox={
	createNew:function(gmeTmp, imgMrk){
		var gme=gmeTmp;
		var _this=Sprite.createNew(gme, imgMrk);
		var tickPar=_this.tick;
		_this.mrk=0;
		_this.rInd=0;
		_this.cInd=0;
		_this.shown=true;
		_this.turn=[[-1,-1,-1],[-1,-1,-1]];

		_this.tick=function(){
			var ctx=gme.ctx;
			tickPar();
			var isSelected=gme.sprLstClcked.indexOf(_this);
			if(_this.shown==false){
				_this.alpha=0.0;
			}else if(isSelected==-1){
				_this.alpha=1.0;
			}else{
				//when it is selected
				ctx.fillStyle="#FF0000";
				ctx.globalAlpha=0.5;
				ctx.fillRect(_this.xCur,_this.yCur,_this.wCur,_this.hCur);
				ctx.fillStyle="#000000";
				ctx.globalAlpha=1;
			}
		};
		_this.onClck=function(){
			var ctx=gme.ctx;
			if(gme.sprLstClcked.length<2){
				if(_this.shown){
					gme.sprLstClcked.push(_this);
				}else{
					gme.sprLstClcked.splice(0, gme.sprLstClcked.length);
				}
			}
			console.log(_this.mrk+"  is clicked.");
		};
		return _this;
	}
}

 通过下列代码,我们得到了父类Sprite的一个实例。

var _this=Sprite.createNew(gme, imgMrk);

那么对于子类SprBox特有的一些属性,我们写成这样的形式即可,_this.mrk,_this.rInd, _this.cInd。

简单的解释一下这三个类:Sprite, Scene, Game。

Game: 表示一个游戏实例。整个游戏的画面渲染,使用的是JS中的Canvas功能。通过对Canvas的定时刷新来形成游戏中的动画效果。

Scene:表示游戏中的一个场景,游戏中可以很多个场景,场景之间可以来回切换。

Sprite: 几乎所有的游戏引擎中的概念,精灵。它表示的就是游戏中的一个个可移动或不可移动的物体。

gamemy.js文件是包含了连连看的主要逻辑。其中有四个子类:

SprBck: 背景Sprite

SprPthLne: 连线Sprite

SprBox: 连连看中的每个方格Sprite

GameMy: 连连看方格的消除和生成的主要逻辑都在这个子类里。

这里咱主要说说方格消除的算法。其它的程序逻辑大家可以参考源代码。

gamemy.js 125行:当检测到两个方格被点击时,执行judgeDiff方法来检测两个格是否图案相同且连通(两格之间的连线不能超过两次转折)

gamemy.js 270行:调用judgeRoute来判断两格之间是否存在一条有效的通路。这是一个递归函数。算法的总体思想是:从起始方格spr1到目标方格spr2是否存在一个两折内的通路,取决于从spr1到spr2周边的方格sprPre是否存在一个两折内的通路。进而形成了一个递归判断。

这里需要注意,当判断从spr1到spr2周边的方格sprPre是否存在通路时,需要考虑两种情况:

1. sprPre是横向连接到的spr2。这种情况下,如果spr1最终是竖向连接到的sprPre,那么这个所需折数一定等于spr1到spr2所需的横向折数减1。

2. sprPre是竖向连接到的spr2。这种情况下,如果spr1最终是横向连接到的sprPre,那么这个所需折数一定等于spr1到spr2所需的竖向折数减1。

这也是连连看消格算法的关键部分。以下是judgeRoute的详细代码:

/**
		 * 
		 * @param {*} spr1 起始方格实例
		 * @param {*} spr2 结束方格实例
		 * @param {[]} needed 带有两个元素的数组,元素1:表示到达第二个方格的路线最终是横着到达的所需的最多折数。元素2:表示到达第二个方格的路线最终是竖着到达的所需的最多折数。初始值是[2,2]
		 * @param {[]} vstRecur 表示一次路线递归探测的过程中,已经探测过的方格,不能在递归中出现第二次探测
		 * @param {[]} route 记录有效通路的节点
		 * @returns 两元素数组,元素1:值0表示横向,值1表示竖向。元素2:表示经过几折到达
		 */
		var judgeRoute=function(spr1,spr2,needed,vstRecur,route){
			if(spr1.rInd==spr2.rInd && Math.abs(spr1.cInd-spr2.cInd)==1 && needed[0]>=0){
				return [0,0];
			}
			if(spr1.cInd==spr2.cInd && Math.abs(spr1.rInd-spr2.rInd)==1 && needed[1]>=0){
				return [1,0];
			}
			var offset=[[0,-1],[0,1],[-1,0],[1,0]];//围绕结束方格的上下左右四个格的偏移,前两个元素横向偏移,后两个元素竖向偏移
			for(let i=0;i=2){
					continue;//同上,但是关于竖向偏移
				}
				var p2Pre={rInd:spr2.rInd+offset[i][0],cInd:spr2.cInd+offset[i][1]};
				if(p2Pre.rInd<0 || p2Pre.rInd>=_this.sprBoxM.length || p2Pre.cInd<0 || p2Pre.cInd>=_this.sprBoxM.length){
					continue;
				}
				var sprPre=_this.sprBoxM[p2Pre.rInd][p2Pre.cInd];//到达spr2的前一个格子这里记做sprPre
				if(sprPre.shown){
					continue;
				}
				var sprPreExistInd=vstRecur.indexOf(sprPre);
				if(sprPreExistInd>=0){
					//console.log("vstRecur exist index:  "+sprPre.rInd+" "+sprPre.cInd+" "+sprPreExistInd);
					continue;
				}
				//上面三种情况是一些边界条件的探测,不符合的情况跳过
				var needR=i<2?needed[0]:needed[1]-1;//if sprPre是横向偏移的格子,那到达sprPre方格的路线最终是横着到达的所需的最多折数,等于spr2的needed[0]。
				var needC=i<2?needed[0]-1:needed[1];//if sprPre是横向偏移的格子,那到达sprPre方格的路线最终是竖着到达的所需的最多折数,等于spr2的needed[0]-1。
				var exstR=0;
				var exstC=0;
				//为了免去重复计算,这里存储了以前计算过的值。如下:
				if(needR>=0){
					exstR=sprPre.turn[0][needR];//先尝试sprPre的turn数组里的值。
				}
				if(exstR==1){
					return [0,needR];
				}
				if(needC>=0){
					exstC=sprPre.turn[1][needC];
				}
				if(exstC==1){
					return [1,needC];
				}
				//exstR=0 or -1, exstC=0 or -1
				if(exstR==0){
					needR=-1;//disable judge row
				}
				if(exstC==0){
					needC=-1;
				}
				vstRecur.push(sprPre);
				var ret=judgeRoute(spr1,sprPre,[needR,needC],vstRecur,route);
				vstRecur.pop();
				if(ret[0]>=0){
				    //find a path
					route.push(sprPre);
					sprPre.turn[ret[0]][ret[1]]=1;
					var tmpRet=[];
					if(i<2){//row's grid
						if(ret[0]==0){//from row
							tmpRet=[0,ret[1]];
						}else{
							tmpRet=[0,ret[1]+1];
						}
					}else{//col's grid
						if(ret[0]==0){//from row
							tmpRet=[1,ret[1]+1];
						}else{
							tmpRet=[1,ret[1]];
						}
					}
					return tmpRet;
				}else{
					sprPre.turn[0][needR]=0;
					sprPre.turn[1][needC]=0;
				}
				
			}
			return [-1,-1]; 
		}

这里我使用的算法效率不算太高,肯定有更高效,更简明的算法。以后我会考虑加以改进。代码里如果有错误或者有更好的改进空间,欢迎大家给予批评指正。谢谢!

你可能感兴趣的:(javascript)