~~~接上篇,游戏的主体框架完成了,接下来我们对游戏中存在的两个主要实体进行分析,一个是雷点类BombObject(节点对象),一个是节点对象对应的图片对象BombImgObject,根据第一篇的介绍,这两个类的对象配合棋盘类实现游戏过程,这里重新解释下这两个类的用处:
一、雷点类(BombObject):一个承上启下的对象类别。
1. 此类的对象集合(棋盘类中的ObjList数组)组成了游戏棋盘上的每一个节点,节点的类型有空节点(数值为0)、雷点(数值为-1)、非雷非空节点(数值为N,依据节点周围的雷点个数而定)。
2. 每一个对象需要标记其在棋盘中的坐标位置(x、y),同时能够获取其周围8个方位上的节点坐标(没有即为null),这8个方位节点的属性标记为North、NorthEast、East、SouthEast、South、SouthWest、West、NorthWest。
3. 节点对象包含一个与其一一对应的图片对象(ImgObj),展现给玩家的棋盘最终以图片对象列表的形式,图片对象根据当前节点数值属性(DisplayNum)的值显示不同的图片。
类的定义如下:
1 //棋盘节点对象类 2 function BombObject(x, y, num, xNum, yNum) { 3 this.X = x; //节点的x坐标 4 this.Y = y; //节点的y坐标 5 this.ImgObj = new BombImgObject(num, x, y); //节点对应的图片对象 6 this.IsBomb = (num === -1) ? true : false; //节点是否是雷点 7 this.DisplayNum = (this.IsBomb) ? -1 : parseInt(num); //节点对应的数值 8 //北方节点对象 9 this.North = (function() { 10 if ((y - 1) < 0) { 11 return null; 12 } 13 else { 14 return { X: x, Y: y - 1 }; 15 } 16 } ()); 17 //东北方向节点对象 18 this.NorthEast = (function() { 19 if ((y - 1) < 0 || (x + 1) >= xNum) { 20 return null; 21 } 22 else { 23 return { X: x + 1, Y: y - 1 }; 24 } 25 } ()); 26 //东方的节点对象 27 this.East = (function() { 28 if ((x + 1) >= xNum) { 29 return null; 30 } 31 else { 32 return { X: x + 1, Y: y }; 33 } 34 } ()); 35 //东南方的节点对象 36 this.EastSouth = (function() { 37 if ((y + 1) >= yNum || (x + 1) >= xNum) { 38 return null; 39 } 40 else { 41 return { X: x + 1, Y: y + 1 }; 42 } 43 } ()); 44 //南方节点对象 45 this.South = (function() { 46 if ((y + 1) >= yNum) { 47 return null; 48 } 49 else { 50 return { X: x, Y: y + 1 }; 51 } 52 } ()); 53 //西南方节点对象 54 this.SouthWest = (function() { 55 if ((y + 1) >= yNum || (x - 1) < 0) { 56 return null; 57 } 58 else { 59 return { X: x - 1, Y: y + 1 }; 60 } 61 } ()); 62 //西方节点对象 63 this.West = (function() { 64 if ((x - 1) < 0) { 65 return null; 66 } 67 else { 68 return { X: x - 1, Y: y }; 69 } 70 } ()); 71 //西北方节点对象 72 this.WestNorth = (function() { 73 if ((x - 1) < 0 || (y - 1) < 0) { 74 return null; 75 } 76 else { 77 return { X: x - 1, Y: y - 1 }; 78 } 79 } ()); 80 } 81 //判断两个节点对象是否相等 82 BombObject.prototype.Equals = function(that) { 83 if (!(that instanceof BombObject) || that.constructor !== BombObject || that == null) { 84 throw new Error("the add obj is not allowed."); 85 return false; 86 } 87 else if (this.X == that.X && this.Y == that.Y) { 88 return true; 89 } 90 else { 91 return false; 92 } 93 }; 94 //判断当前节点对象的坐标值是否一致 95 BombObject.prototype.EqualsEx = function(x, y) { 96 if (this.X == x && this.Y == y) { 97 return true; 98 } 99 else { 100 return false; 101 } 102 };
可以看到,节点对象类包含了一个图片对象,同时也定义了两个对象比较函数,可由节点类对象直接调用。下面来介绍这个图片对象类:
二、图片对象类(BombImgObject):定义与节点对象一一对应的图片对象。
1. 此类的对象是节点对象的表现层,节点的有些属性和操作均通过此对象实现,每个节点对象是根据节点数值属性定义的。
2. 图片类对象是展现给用户的表现层,游戏的功能及效果都是通过此层实现的,故需要在图片对象上定义鼠标事件(左键、右键、按下、弹起、左右键一起按、移入、移出等)。
3. 同时在响应鼠标事件的同时,需要触发有些功能监听事件,比如空节点点击事件、数值判断左右键同时按下事件等。
类的定义包括一个Img元素(展现给玩家用)标签对象,对应的棋盘坐标x、y,是否被标记标志flag(左键、右键的响应视为已经标记),类的定义如下:
1 //图片对象集合共用的鼠标类型值,0为无鼠标事件,1为左键,2为右键,3为左右键,4为滚轮 2 BombImgObject.MouseType = 0; 3 //定义各节点的图片对象类(传递图片在棋盘中的数值:0~8、-1,图片在棋盘中的x、y坐标) 4 function BombImgObject(altValue, xPos, yPos) { 5 //保存this指针,防止函数嵌套时指代不明确 6 var img = this; 7 8 img.ImgObj = null; //图片实体对象 9 img.x = xPos; //图片在棋盘中的x坐标 10 img.y = yPos; //图片在棋盘中的y坐标 11 if (document.createElement) { 12 img.ImgObj = document.createElement("img"); 13 } 14 if (img.ImgObj) { 15 img.ImgObj.lang = altValue; //保存当前图片对象对应的节点数值,0~8、-1 16 img.ImgObj.src = "img/small/normal.bmp"; //赋值默认图片路径 17 img.ImgObj.flag = false; //标记图片处理标示,如果图片处理过(标记、显示等)则不再响应其他事件 18 img.ImgObj.id = "img" + img.x + "-" + img.y; //根据图片对应棋盘中的坐标定义图片对象的id属性 19 20 //定义图片对象的鼠标按下事件 21 img.ImgObj.onmousedown = function() { 22 //如果没有触发雷点 23 if (BombObjectList.fireFlag !== 2) { 24 //判断是何种鼠标事件 25 switch (window.event.button) { 26 case 1: 27 //左键 28 if (this.src.indexOf("normal") >= 0) { 29 this.src = "img/small/kong.bmp"; 30 } 31 //标记当前的鼠标操作类型 32 BombImgObject.MouseType = 1; 33 //图片鼠标按下事件,标记游戏开始 34 BombObjectList.fireFlag = 1; 35 break; 36 case 2: 37 //右键 38 if (this.src.indexOf("normal") >= 0) { 39 //标记图片已处理(被标记) 40 this.flag = true; 41 this.src = "img/small/flag.bmp"; 42 BombObjectList.MarkedNum++; 43 } 44 else if (this.src.indexOf("flag") >= 0) { 45 //取消图片已处理标记(还原默认) 46 this.flag = false; 47 this.src = "img/small/normal.bmp"; 48 BombObjectList.MarkedNum--; 49 } 50 //标记当前的鼠标操作类型 51 BombImgObject.MouseType = 2; 52 break; 53 case 3: 54 //左右键一起 55 BombImgObject.MouseType = 3; 56 //以该点为中心,在其8个方向上进行节点判断,如果已经都展开,则什么也不做,如果有未展开的节点,而这些节点里面有雷点,则不展开,反之将递归展开所有 57 BombObjectList.DC_X = img.x; 58 BombObjectList.DC_Y = img.y; 59 break; 60 case 4: 61 //滑轮 62 BombImgObject.MouseType = 4; 63 break; 64 } 65 } 66 } 67 //定义图片对象的右键处理程序,空返回 68 img.ImgObj.oncontextmenu = function() { 69 if (BombObjectList.fireFlag !== 2) { 70 return false; 71 } 72 } 73 //定义图片对象的鼠标提起事件处理程序 74 img.ImgObj.onmouseup = function() { 75 //如果没有触发雷点 76 if (BombObjectList.fireFlag !== 2) { 77 if (this.src.indexOf("flag") >= 0 || this.src.indexOf("normal") >= 0) { 78 //如果图片被标记处理或者未处理过,则直接返回 79 return; 80 } 81 else { 82 //如果不是右键事件,则进入处理 83 if (BombImgObject.MouseType !== 2) { 84 if (BombImgObject.MouseType !== 1) { 85 //双击时,不进行接下来的操作 86 BombImgObject.MouseType = 0; 87 return; 88 } 89 //标记图片对象已被处理过 90 this.flag = true; 91 //根据图片数值显示对应的图片信息 92 var caseValue = parseInt(this.lang); 93 switch (caseValue) { 94 case -1: //雷点 95 { 96 this.src = "img/small/fire.bmp"; 97 //触雷,更行触发标记,以便定时器捕获 98 BombObjectList.fireFlag = 2; 99 break; 100 } 101 case 1: //该节点旁边有一个雷点,标记数值1 102 { 103 this.src = "img/small/1.bmp"; 104 break; 105 } 106 case 2: //该节点旁边有两个雷点,标记数值2 107 { 108 this.src = "img/small/2.bmp"; 109 break; 110 } 111 case 3: //该节点旁边有三个雷点,标记数值3 112 { 113 this.src = "img/small/3.bmp"; 114 break; 115 } 116 case 4: //该节点旁边有四个雷点,标记数值4 117 { 118 this.src = "img/small/4.bmp"; 119 break; 120 } 121 case 5: //该节点旁边有五个雷点,标记数值5 122 { 123 this.src = "img/small/5.bmp"; 124 break; 125 } 126 case 6: //该节点旁边有六个雷点,标记数值6 127 { 128 this.src = "img/small/6.bmp"; 129 break; 130 } 131 case 7: //该节点旁边有七个雷点,标记数值7 132 { 133 this.src = "img/small/7.bmp"; 134 break; 135 } 136 case 8: //该节点旁边有八个雷点,标记数值8 137 { 138 this.src = "img/small/8.bmp"; 139 break; 140 } 141 case 0: 142 { 143 //空节点,需要遍历相邻的所有空节点,目前采用递归方法 144 this.src = "img/small/kong.bmp"; 145 //定义当前空节点的坐标未知,以便定时器捕获对该位置相连的所有节点进行递归处理,显示递归中发现的所有空节点 146 BombObjectList.fire_X = img.x; 147 BombObjectList.fire_Y = img.y; 148 break; 149 } 150 } 151 } 152 } 153 } 154 //鼠标提起时,清空鼠标操作状态。 155 BombImgObject.MouseType = 0; 156 } 157 //禁止图片对象的鼠标拖动事件 158 img.ImgObj.ondrag = function() { 159 return false; 160 } 161 //定义图片对象的鼠标移入事件 162 img.ImgObj.onmouseout = function() { 163 if (BombObjectList.fireFlag!==2 && !this.flag && BombObjectList.IsMouseDown && this.src.indexOf("kong") >= 0 && BombImgObject.MouseType != 2) { 164 this.src = "img/small/normal.bmp"; 165 } 166 } 167 img.ImgObj.onmouseover = function() { 168 if (BombObjectList.fireFlag !== 2 && !this.flag && BombObjectList.IsMouseDown && this.src.indexOf("normal") >= 0 && BombImgObject.MouseType != 2) { 169 this.src = "img/small/kong.bmp"; 170 } 171 } 172 } 173 //根据x、y坐标显示该图片对象的图像 174 img.ShowNumImg = function(tag) { 175 if (!img.ImgObj.flag) { 176 if (arguments.length === 0) { 177 if (parseInt(img.ImgObj.lang) == 0) { 178 //为空时 179 document.getElementById("img" + img.x + "-" + img.y).src = "img/small/kong.bmp"; 180 } 181 else { 182 //数值时 183 document.getElementById("img" + img.x + "-" + img.y).src = "img/small/" + img.ImgObj.lang + ".bmp"; 184 } 185 //标记该图像已经处理过,不再响应后续的所有操作。 186 img.ImgObj.flag = true; 187 } 188 else { 189 //双击时 190 if (tag === 1) { 191 //按下 192 document.getElementById("img" + img.x + "-" + img.y).src = "img/small/kong.bmp"; 193 } 194 else { 195 //弹起 196 document.getElementById("img" + img.x + "-" + img.y).src = "img/small/normal.bmp"; 197 } 198 } 199 } 200 } 201 202 //返回节点的图像对象 203 return img; 204 } 205 //设置图片对象的数值 206 BombImgObject.prototype.SetImgNum = function(num) { 207 if (this !== null) { 208 this.ImgObj.lang = num; 209 } 210 }
由于游戏需要,从空间复杂度上考虑,需要定义两个成员函数,一个是图片节点的动态展开函数,标记为ShowNumImg;另一个是动态改变图片对象对应的数值的函数,标记为SetImgNum。
~~代码中灰色底纹部分是为了响应棋盘类监听事件所写的,在javascript中,类与类之间的相互作用没有VC++中的消息机制,这里在一个类中采用定时器监听(读)变量、另一个类中根据触发点设置变量的方法(写)的方法,技术有限,欢迎大家指导学习!!
三、游戏控制类
至此,游戏功能部分全部完成,不过就展现给玩家的界面来说,还比较单调,仅仅一个期盼而已,为了完善游戏的辅助功能,增添游戏的趣味性,还需要完成以下一系列工作:
1. 需要一个计时器,每秒钟刷新一次,统计当前游戏用时,然后反馈给玩家。
2. 需要一个计数器,显示当前游戏剩余雷点水,一旦用户标记出一个雷点,计数器减1,玩家误标记而计数器为零且游戏未结束时,需要出现负数提示。
3. 游戏可以重新开始,模仿windows下的扫雷,设置一个重新开始按钮,游戏重新开始需要清除当前过程的所有临时变量,以便为接下来新的游戏过程预留空间。
4. 剩余雷点数不多时,需要自动识别剩下的节点是否全是雷点,如果是,则游戏自动成功结束,以提高游戏趣味性。
鉴于上述分析,我们需要定义一个游戏控制类,标记为PlayBombGame,对游戏的整个流程和状态进行控制和监听,对客户机环境进行兼容性判断,为整个游戏检测提供一个正常的运行环境,类的定义如下:
1 //玩家操作接口类的定义 2 function PlayBombGame(TimerId, BombNumId, ContentId, TryAgainId, width, height, BombNum) { 3 //预留退路 4 if (ContentId === "" || BombNumId === "" || TryAgainId === "" || TimerId === "") return false; 5 if (!document.getElementById) return false; 6 if (!document.createDocumentFragment) return false; 7 if (!document.createElement) return false; 8 if (!document.getElementById(ContentId)) return false; 9 if (!document.getElementById(BombNumId)) return false; 10 if (!document.getElementById(TryAgainId)) return false; 11 if (!document.getElementById(TimerId)) return false; 12 13 //保存当前对象,以防函数嵌套时指代不清 14 var PBG = this; 15 16 PBG.GameInfo = new BombObjectList("ContentSection", width, height, BombNum); //游戏操作对象 17 PBG.TimerID = TimerId; //计时器元素id 18 PBG.BombNumId = BombNumId; //雷点计数器元素id 19 PBG.TryAgainId = TryAgainId; //重置按钮id 20 PBG.CurSecond = 0; //当前用户用时(s) 21 PBG.CurBombNum = BombNum; //当前剩余雷点个数 22 PBG.GameState = -1; //当前的游戏状态,-1为结束(未开始),1为进行中 23 24 var timer = null; 25 var ListenTimer = null; 26 //开始初始化游戏 27 PBG.play = function() { 28 if (PBG.GameInfo != null || PBG.GameInfo != undefined) { 29 PBG.GameInfo.Initial().Display(); 30 } 31 } 32 //重新初始化游戏 33 PBG.playAgain = function() { 34 if (PBG.GameInfo != null || PBG.GameInfo != undefined) { 35 PBG.GameInfo.TryAgain(); 36 BombObjectList.fireFlag = 0; 37 BombObjectList.MarkedNum = 0; 38 //关闭计时器 39 PBG.CurSecond = 0; 40 PBG.CurBombNum = BombNum; 41 PBG.GameState = -1; 42 //重新开始监测 43 clearInterval(ListenTimer); 44 clearInterval(timer); 45 timer = null; 46 ListenTimer = null; 47 PBG.TimerControl(); 48 } 49 } 50 //游戏结束时的处理 51 PBG.GameOver = function(tag) { 52 //标记游戏状态结束 53 PBG.GameState = -1; 54 //结束时处理 55 if (arguments.length !== 0) { 56 //成功 57 PBG.GameInfo.GameOver(tag); 58 document.getElementById(PBG.TryAgainId).src = "img/face/over.bmp"; 59 } 60 else { 61 //失败 62 PBG.GameInfo.GameOver(); 63 document.getElementById(PBG.TryAgainId).src = "img/face/fail.bmp"; 64 } 65 //关闭定时器 66 clearInterval(ListenTimer); 67 clearInterval(timer); 68 timer = null; 69 ListenTimer = null; 70 } 71 //监测游戏状态 72 PBG.TimerControl = function() { 73 if (ListenTimer === null || ListenTimer === undefined) { 74 ListenTimer = setInterval(function() { 75 if (BombObjectList.fireFlag === 2) { 76 PBG.GameOver(); 77 } 78 else if (BombObjectList.fireFlag === 1) { 79 //开启计时器 80 if (timer == null) { 81 //标记游戏开始 82 PBG.GameState = 1; 83 timer = setInterval(function() { 84 PBG.CurSecond++; 85 document.getElementById(PBG.TimerID).innerHTML = GetCount(PBG.CurSecond); 86 if (PBG.CurSecond === 999) { 87 //最长时间999秒,超出即结束 88 PBG.GameOver(); 89 } 90 }, 1000); 91 //开启对空节点的监听 92 PBG.GameInfo.ListenKong(); 93 } 94 } 95 else { 96 //未开始状态下 97 if (PBG.GameState === -1) { 98 //如果在进行中点击了重新开始 99 document.getElementById(PBG.TimerID).innerHTML = GetCount(PBG.CurSecond); 100 clearInterval(timer); 101 timer = null; 102 } 103 } 104 //监听剩余雷点 105 if (BombObjectList.fireFlag !== 2) { 106 //监听玩家标记出的个数来展现当前剩余的雷的个数 107 PBG.CurBombNum = BombNum - BombObjectList.MarkedNum; 108 document.getElementById(PBG.BombNumId).innerHTML = GetCount(PBG.CurBombNum); 109 if ($("#" + ContentId + " > IMG").length > 0) { 110 //如果剩余雷点数为0,且棋盘上剩余未标记节点个数为0,则游戏结束,全部标记正确,游戏成功 111 if (PBG.CurBombNum === 0 && $("#" + ContentId + " > img[flag='false']").length === 0) { 112 PBG.GameOver(1); 113 BombObjectList.fireFlag = 2; 114 } 115 //剩余未标记的都是雷点,则游戏结束,程序自动标记所有的雷点,游戏成功 116 if (PBG.CurBombNum > 0 && $("#" + ContentId + " > img[flag='false']").not("lang='-1'").length === 0) { 117 PBG.GameOver(2); 118 BombObjectList.fireFlag = 2; 119 } 120 } 121 } 122 }, 50); 123 } 124 } 125 //启动检测 126 PBG.TimerControl(); 127 //根据数值获取图片数值展现对象 128 function GetCount(num) { 129 var numArr = num.toString().split(""); 130 for (var i = 0; i < numArr.length; i++) { 131 numArr[i] = (numArr[i] == "-") ? "line" : numArr[i]; 132 } 133 if (numArr.length === 1) { 134 return "<img src="img/num/0.bmp" alt="bomb" /><img src="img/num/0.bmp" alt="bomb" /><img src="img/num/" + numArr[0] + ".bmp" alt="bomb" />"; 135 } 136 else if (numArr.length === 2) { 137 return "<img src="img/num/0.bmp" alt="bomb" /><img src="img/num/" + numArr[0] + ".bmp" alt="bomb" /><img src="img/num/" + numArr[1] + ".bmp" alt="bomb" />"; 138 } 139 else { 140 return "<img src="img/num/" + numArr[0] + ".bmp" alt="bomb" /><img src="img/num/" + numArr[1] + ".bmp" alt="bomb" /><img src="img/num/" + numArr[2] + ".bmp" alt="bomb" />"; 141 } 142 } 143 //对文档的全局事件进行定义 144 AddEvent(document, "mousedown", function(e) { 145 BombObjectList.IsMouseDown = true; 146 //冒泡捕获图片对象的按下事件 147 var event = window.event || e; 148 var targetE = (event.srcElement) ? event.srcElement : event.target; 149 if (BombObjectList.fireFlag !== 2 && targetE.getAttribute("flag") !== null && BombImgObject.MouseType===1) { 150 document.getElementById(PBG.TryAgainId).src = "img/face/down.bmp"; 151 } 152 }); 153 AddEvent(document, "mouseup", function(e) { 154 BombObjectList.IsMouseDown = false; 155 //冒泡捕获图片对象的按下事件 156 var event = window.event || e; 157 var targetE = (event.srcElement) ? event.srcElement : event.target; 158 if (BombObjectList.fireFlag !== 2 && targetE.getAttribute("flag") != null) { 159 document.getElementById(PBG.TryAgainId).src = "img/face/up.bmp"; 160 } 161 }); 162 AddEvent(document, "contextmenu", function() { return false; }); 163 AddEvent(document.getElementById(PBG.TryAgainId), "click", function() { PBG.playAgain(); }); 164 AddEvent(document.getElementById(PBG.TryAgainId), "mousedown", function() { this.src = "img/face/0.bmp"; }); 165 AddEvent(document.getElementById(PBG.TryAgainId), "mouseup", function() { this.src = "img/face/up.bmp"; }); 166 167 function AddEvent(target, eventType, callback) { 168 if (target.addEventListener) { 169 target.addEventListener(eventType, callback, false); 170 } 171 else { 172 target.attachEvent("on" + eventType, function(event) { return callback.call(target, event); }); 173 } 174 }; 175 }
完成上面类的定义,整个游戏就完成了,展现给玩家的是篇章一所示的效果图,嗯,界面比较粗糙哈~~
题外:
经常来博客园看技术大牛的分享,不知不觉就3年过去了,就在月初,突然发现自己博客园的年龄都3年临三个月了,说来惭愧,自己的博客园却空空如野,什么都没有,作为一个程序猿,热爱编程的同时突然很强烈想着在这个职业上留下点什么,说做就做,于是就有了上述四个篇章,游戏虽小,但也要五脏俱全,玩和做真的不一样。
想是一回事,做是一回事,写出来又是一回事,个人觉得这三步的难度是递进关系的,哈哈,个人鄙见。此章节的完成,突然发现兴趣是最好的推动力,工作之余,做自己想做的事,也对之前一段时间以来所学习的内容进行一个小结,美事一桩,第一次写自己的博客,这只是个开端,接下来我将自己学习所得、所想、所做一一与大家分享,因为分享,所以快乐~~
~~Little Dream,在与大家互相交流中成长~~