web版扫雷小游戏(四)

~~~接上篇,游戏的主体框架完成了,接下来我们对游戏中存在的两个主要实体进行分析,一个是雷点类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,在与大家互相交流中成长~~

原文地址:https://www.cnblogs.com/freshfish/p/3389156.html