最近写了一个连连看小游戏。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];
}
这里我使用的算法效率不算太高,肯定有更高效,更简明的算法。以后我会考虑加以改进。代码里如果有错误或者有更好的改进空间,欢迎大家给予批评指正。谢谢!