上一节我们讨论的游戏大厅的实现,这一节我们来看一下客户端游戏棋盘的处理
关于棋盘的呈现采用了GDI的DrawImage方法,先准备一张400*400的棋盘图片和两个40*40的棋子图片(分别为黑棋和白棋),我们的思路是通过和客户端服务器的数据交互得到游戏大厅某桌的棋子信息,然后客户端直观的呈现该信息。
棋盘同样可以看作一个对象,只不过这个对象我们需要从Form来继承,因为我们需要用到PictureBox控件来呈现棋盘和棋子
//此处选择从Form继承,是考虑需要在Form上利用GDI画图已达到走棋的效果 public partial class FormPlaying : Form { private int tableIndex; private int side; //这里和服务器端的GameTable一样也定义了grid(8,8),目的是在客户端保存棋子的状态 //每次通讯时只由服务器端通知更改的棋子的位置和颜色,客户端也作相应的修改 //当然这里也可以不声明grid(8,8),不过那样每次通讯都需要传递所有的棋子位置状态,效率不高 private int[,] grid = new int[8, 8]; //工具类 private Service service; //FormPlaying和FormRoom类似都需要被负责接受数据的线程操作,需要调用Object.Invoke方法,详见第二节 delegate void LabelDelegate(Label label, string str); delegate void ButtonDelegate(Button button, bool flag); LabelDelegate labelDelegate; ButtonDelegate buttonDelegate; //保存棋子位图信息 private Bitmap blackBitmap; private Bitmap whiteBitmap; //FormPlaying将在玩家落座后呈现,在FormRoom中被初始化 //TableIndex 桌数 //Side 黑方或者白方 //sw 客户端发送的数据流 public FormPlaying(int TableIndex,int Side,StreamWriter sw) { InitializeComponent(); //构造工具类,负责打印调试信息及发送数据流到服务器端 service = new Service(listBox1, sw); this.tableIndex = TableIndex; this.side = Side; blackBitmap = new Bitmap("black.gif"); whiteBitmap = new Bitmap("white.gif"); } } |
这里和服务器端的GameTable一样也定义了grid(8,8),目的是在客户端保存棋子的状态,每次通讯时只由服务器端通知更改的棋子的位置和颜色,客户端也作相应的修改。当然也可以不声明grid(8,8),不过那样每次通讯都需要传递所有的棋子位置状态,效率不高
上一节提到当玩家选中Checkbox表明坐下时,触发CheckedChange事件,这一节对该事件进行修改,初始化棋盘FormPlaying并打开
void checkBox_CheckedChanged(object sender, EventArgs e) { CheckBox checkbox = (CheckBox)sender; if (checkbox.Checked) { int i = int.Parse(checkbox.Name.Substring(5, 4)); int j = int.Parse(checkbox.Name.Substring(9, 4)); side = j; service.SendToServer(string.Format("SitDown,{0},{1}", i, j)); //初始化棋盘 formPlaying = new FormPlaying(i, j, sw); formPlaying.Show(); } } |
调用GDI画棋盘及棋子信息(初始化及任一方落子都调用该事件)
private void pictureBox1_Paint(object sender, PaintEventArgs e) { Graphics g = e.Graphics; for (int i = 0; i <= grid.GetUpperBound(0); i++) { for (int j = 0; j <= grid.GetUpperBound(1); j++) { if (grid[i, j] != DotColor.None) { if (grid[i, j] == DotColor.Black) { //i代表行j代表列 y轴应为列 x轴应为行 g.DrawImage(blackBitmap, (i + 1) * 40,(j + 1) * 40); } else { g.DrawImage(whiteBitmap, (i + 1) * 40, (j + 1) * 40); } } } } } |
当玩家落子的时候实际上是触发了PictureBox的MouseDown事件,程序判断是否可在该处下子,如果可以则调用GDI的Graphics.DrawImage将棋子“画”到棋盘上。
private void private void pictureBox1_MouseDown(object sender, MouseEventArgs e) { //i代表行j代表列 y轴应为列 x轴应为行 //棋子的图片大小为40*40,所以这里根据坐标信息得到棋子在对应的grid数组中的位置 int x = e.X / 40; int y = e.Y / 40; //黑白棋逻辑 //保证鼠标点击位置在棋盘内部 if (!(x < 1 || x > 8 || y < 1 || y > 8)) { if (grid[x - 1,y - 1] == DotColor.None) { //int color = grid[x - 1, y - 1]; //格式 SetDot,参数1,参数2,参数3,参数4 //功能:由服务器端判断此处是否允许落子, // 不允许落子的情况 // 1 在不合法的位置落子 // 2 轮到黑方落子,但白方在点击棋盘,则对白方不允许落子 //如果允许同时计算落子后双方棋子的变化情况, //参数1 桌号 //参数2 棋盘行 //参数3 棋盘列 //参数4 黑方或白方 service.SendToServer(string.Format("SetDot,{0},{1},{2},{3}", tableIndex, x - 1,y-1, side)); } } } |
此处的实现效率上有些问题,把判断能否落子的任务交给服务器端,实际上我们已经在客户端利用grid数组保存了棋子的状态,完全可以放到客户端去判断,放到服务器端会造成很多无用的通讯数据(写文章的时候代码已经完成,不打算改了)
列一下服务器端处理SetDot命令的逻辑
1 如果没有轮到该方落子,则丢弃此次命令
2 将落子的位置向八个方向进行比较(上、下、左、右、左上、左下、右上、右下)
考虑落子的同一列的上下两个方向的比较情况
(1)单列中小于目标行的情况,目标行最小为第1行(下标为0)
假设目标行为第5行(下标为4)则比较行k1依次为3,2,1,0
如果目标行为第1行(下标为0)则不需要比较小于目标行的情况
(2)单列中大于目标行的情况,目标行最大为第8行(下标为7)
假设目标行为第5行(下标为4)则比较行k2依次为5,6,7
如果目标行为第8行(下标为7)则不需要比较大于目标行的情况
3 准备八个List<string>,将八个方向需要反转的棋子分别存入代表各自方向的List<string>中,以向上方向为例
(1)从最靠近目标行的行数开始,逐个检验同列中棋子颜色,遇到相同颜色或者无子即退出,连续不同存入;如果检测位置无子,则晴空该方向List<string>,即无法改变该方向棋子颜色
(2)如果有同颜色的子,则退出,List<string>中的值即为该方向需要更改的值
(3)如果有不同颜色的子且不为第1行,则记录,否则清空列表。假设在第1列第1-4行为黑子,白方在第1列第5行打算落下白子,从向上的方向比较,当检测到第1行时,该方向列表中已有3个子,但第一行为黑子,无法反转1-4行黑子,所以清空已有列表。
4 如果八个方向List<string>均为空,则表明此处无法下子,丢弃此命令
5 通知客户端修改 落子的信息及需要反转的棋子的所有信息
此处代码较多,不列了,请看源码
本节完成了棋盘的呈现及简单的落子算法,但未考虑游戏中一方无法落子而由另一方继续落子的情况,下一节将对代码进行简单的重构以添加该功能,最终完成一个完整的网络黑白棋对弈。
看过网上很多的类似系列教程(博客园包包版网络大厅的+桥牌系统),写的很深入,感觉比较复杂,初学者不宜上手。我是在学习WCF的时候,发现自己对底层的传输原理都没有搞明白,于是又回头学习网络传输的一些知识,自己写了一个简单的网络游戏黑白棋,因此也想把学习的一个过程记录下来和初学者们一块交流。我的只是小儿科,还请网友多多包涵,高手们也不要吝惜你们的砖头。
第一次写系列教程,心里没底,本来自己水平就一般,却要完成这个命题作文,难煞我了。好歹程序写的差不多了(不过还没有最终完成),这里先把完成的部分分章介绍一下,程序我在慢慢补。
本系列源代码TCP网络传输参考《C#网络应用高级编程》人民邮电出版社,马骏编,黑白棋游戏部分为本人(http://www.basilwang.net)编写。
写这个游戏只是为了我个人理解基于TCP网络游戏编程的基本思路,算法部分写的比较乱,没有优化,不过我都做了注释,方便大家阅读。
先说一下黑白棋,又叫反棋(Reversi)、奥赛罗棋(Othello),苹果棋,翻转棋。黑白棋在西方和日本很流行。但是这个游戏在中国目前还不够推广,下棋的水平还不高。黑白棋规则很简单,只要肯花点脑筋,新手也能玩得很好。因为棋盘小,下一局棋所花的时间也不多。对于黑白棋,有一种说法是:只需要几分钟学会它,却需要一生的时间去精通它(a minute to learn, a lifetime to master)。
早些年文曲星里面带的游戏就喜欢它了,我这个人脑子不开窍,很多游戏连电脑AI都打不过,好容易整了个能打败AI的游戏,还不往死里玩,嘿嘿。不过我写的这个黑白棋只是供网络对战使用,不涉及AI算法部分(我还不知道怎么做呢),斗胆发到网上,权作抛砖引玉,废话少说,进入正题。
系列的介绍打算以我学习TCP网络编程的过程为顺序,每一篇教程都能够完成功能,附上的源代码能够独立运行,我会把代码中碰到的相关知识做相应的介绍,使初学者能有直观的认识。
游戏完成的部分
1 网络大厅,可自定义桌数,人数 (完全参照《C#网络应用高级编程》,马老师应该不介意吧)
2 黑白棋游戏客户端
2.1 吃子
2.2 奇偶数统计
2.3 轮流下子
2.4 终局胜负提示
未完成部分
1 黑白棋游戏客户端
1.1 下子时间限制
1.2 甲方无子可下时,程序判定由乙方下子,甲方丢掉一次机会
1.3 判定任一方无子可下的程序(思路:需要计算盘中空子的列表,然后调用已完成的吃子程序,看能否下子,但并不真正下子)
源程序写的很简单,界面比较简陋,只是为了帮助大家更好的理解基于TCP的网络游戏传输的基本原理,还请大家多包涵。

