海军战舰游戏 - II






2.82/5 (5投票s)
2005年7月27日
5分钟阅读

49658

976
一款客户端-服务器端的海军战棋游戏。
引言
一款使用套接字和IO实现的客户端-服务器端海军战棋游戏应用。
开始阅读之前
本文是两篇系列文章的第二部分。 第一部分 介绍了如何绘制海军战棋的棋盘。第二部分介绍了客户端-服务器连接以及如何将坐标绘制到正确的位置。为了更好地理解本文,您应该对套接字连接(用于第二部分)、System.Drawing
命名空间以及 IO(也用于第二部分)有所了解。如果您对此一无所知,我推荐您阅读 Deitel 的书籍“C# How to Program”。但如果您没钱,MSDN 中的文档将是一个很好的帮助。最好的方法是同时拥有这本书并浏览 MSDN。
我们又回来了
我们又回来了,这次将解释客户端-服务器连接以及如何绘制棋盘上的“X”。
服务器
在客户端-服务器连接中,我们必须遵循一些步骤。以下是服务器的步骤:
- 创建监听器对象;
- 启动对象;
- 将其与套接字关联以进行数据传输;
- 发送和接收数据;
- 最后,在所有事情完成后关闭连接;
在客户端-服务器应用程序中,启动服务器或客户端的方法必须放在线程中。您必须这样做,因为服务器会一直监听直到建立连接,而当客户端应用程序启动时,服务器会捕获客户端发起的连接请求,然后神奇的事情就发生了。
好的,现在我在这里放上我的类的构造函数。
//The constructor
public Server(Graphics g)
{
G = g;
IPAddress IPLocalhost = IPAddress.Parse("127.0.0.1");
//first commandment: create the listener
ServerListener = new TcpListener(IPLocalhost, 65000);
//second commandment: start it!
ServerListener.Start();
}
您注意到在我的构造函数中传递了一个名为“g
”的 Graphics
参数,这是为什么呢?解释在 Running()
方法中的 “Desenho
” 对象里。
public void Running()
{
try
{
//third commandment: associate the Listener
//with the socket to receive data
while(true)
{
Connexion = ServerListener.AcceptSocket();
//fourth commandment: make transactions
if(Connexion.Connected)
{
DataStream = new NetworkStream(Connexion);
Reader = new BinaryReader(DataStream);
Writer = new BinaryWriter(DataStream);
m_bConnect = true;
do
{
sData = Reader.ReadString();
using(Draw Desenho = new Draw())
{
Desenho.GetCoordinates(sData+
Form1.stCoordinates, G, true);
}
}while(DataStream != null &&
sData != "Close connection");
}
}
//fifth commandment: close it!
Reader.Close();
Writer.Close();
DataStream.Close();
Connexion.Close();
Application.Exit();
}
catch(SocketException SocketEx)
{
MessageBox.Show(SocketEx.Message);
}
这个方法有一个 try
/catch
块,其中有一个套接字,它接收了我的 TcpListener
对象调用 AcceptSocket()
方法返回的一个套接字对象。它通过访问 Connected
属性来验证连接是否已建立,并创建数据流以及读取器和写入器。那么 Graphics g
参数是怎么回事呢?在 do while
循环中,有一条语句: using
。此语句保证了在您完成所有必要操作后该对象会被释放。请注意,有一个名为 Desenho
的 Draw
对象。Desenho
对象的 GetCoordinates
方法被调用,并且其中一个参数是 Graphics
对象。
注意:创建 Graphics
类型的变量并不是一个好主意。正如 Jesse Liberty 所说:“在需要时使用 Graphics
对象,并在完成后释放它。”我只在我的 GetCoordinates
方法中使用这个对象,当 using
语句结束时它会被自动释放。
客户端
现在,让我们看看客户端的步骤。
- 创建对象;
- 获取用于发送和接收数据的流;
- 发送和接收数据;
- 关闭连接;
客户端的步骤与服务器的步骤差别不大。客户端通过发送请求来建立与服务器的通信。客户端的请求就像一个连接器,而套接字服务器就像一个番茄(tomade)。这很简单。现在,我们来分析构造函数和 RunningClient()
方法。
看看这段代码。
//The constructor
public Client(Graphics G)
{
Gr = G;
}
public void RunningClient()
{
cClient = new TcpClient();
try
{
cClient.Connect("localhost", 65000);
DataStream = cClient.GetStream();
Reader = new BinaryReader(DataStream);
Writer = new BinaryWriter(DataStream);
do
{
sData = Reader.ReadString();
using(Draw Desenho = new Draw())
{
Desenho.GetCoordinates(sData +
Form1.stCoordinates, Gr, true);
}
} while(DataStream != null && sData != "Close connection");
Reader.Close();
Writer.Close();
DataStream.Close();
cClient.Close();
Application.Exit();
}
catch(SocketException ex)
{
MessageBox.Show(ex.Message);
}
}
您很可能已经注意到,这个方法和 Server
类中的 Running
方法非常相似。您是对的。但有一些细微差别。看看这段代码。
public void RunningClient()
{
cClient = new TcpClient();
try
{
cClient.Connect("localhost", 65000);
DataStream = cClient.GetStream();
//others parts of code supressed
}
请注意,客户端构造函数接收一个 Graphics
参数,与服务器构造函数一样。另外,请注意 TcpClient
对象没有 Start
方法。作为客户端,它通过 Connect()
向服务器发送连接请求,而在服务器端有一个 Start()
方法会监听是否有客户端想要建立连接(放在一个 while
循环中)。连接建立后,客户端调用 GetStream()
方法获取用于传输数据的 NetworkStream
。此信息被赋给 DataStream
对象。因此,传输就会发生。
GetCoordinates 方法
此方法在我们的海军战棋游戏中非常重要。它在服务器和客户端中都使用。它接收三个参数:一个 Graphics
参数,一个包含坐标的字符串,以及一个布尔值。Graphics
参数仅用于传递给一个名为 DrawX()
的 private
方法。布尔变量用于判断是玩家输入坐标来填充自己的棋盘,还是输入一个坐标来攻击敌人。字符串变量包含当前用户棋盘的坐标,或者当前用户选择攻击敌人的坐标。以下是该方法。
public void GetCoordinates(string sCoordinates,
Graphics G, bool bInserted)
{
int[] iLocations = new int[sCoordinates.Length];
int iLength = sCoordinates.Length;
string sReceive;
string sCompare = sCoordinates.Substring(0,2);
m_bWhatX = bInserted;
for(int i = 0; i < iLength ; i++)
{
sReceive = sCoordinates.Substring(i,1);
switch (sReceive)
{
/*populating the array with the coordinates
that will be used to draw*/
case "A":
iLocations[i] = 90;
break;
case "a":
iLocations[i] = 90;
break;
case "B":
iLocations[i] = 140;
break;
case "b":
iLocations[i] = 140;
break;
case "C":
iLocations[i] = 190;
break;
case "c":
iLocations[i] = 190;
break;
case "D":
iLocations[i] = 240;
break;
case "d":
iLocations[i] = 240;
break;
case "E":
iLocations[i] = 290;
break;
case "e":
iLocations[i] = 290;
break;
case "1":
iLocations[i] = 40;
break;
case "2":
iLocations[i] = 90;
break;
case "3":
iLocations[i] = 140;
break;
case "4":
iLocations[i] = 190;
break;
case "5":
iLocations[i] = 240;
break;
}
}
for(int i = 0 ; i <= 8 ; i+=2)
{
//to draw the data inserted in TxtLocation
if(!bInserted)
{
/*just a inversion, cause horizontal
*line in battlefield is of numbers
* and vertical, is composed by letters
*/
DrawX(iLocations[i+1], iLocations[i], G);
}
//to compare and draw the data sended
//by client with the server coordinates and draw
else
{
//with this, the string will be
//the length of 10 when the length is 12
sCoordinates = sCoordinates.Remove(0,2);
while(true)
{
if(sCompare == sCoordinates.Substring(i,2))
{
DrawX(iLocations[i+1], iLocations[i], G);
MessageBox.Show("Acertou");
break;
}
else
{
DrawX(iLocations[i+1], iLocations[i], G);
break;
}
}
break;
}
}
}
不要被这个方法的大小吓到,它很简单。该方法获取字符串中的每个字符,并将其放入一个 switch
语句中,以填充整数数组 iLocations
,该数组用于保存所有将由 DrawX()
方法使用的坐标。iLocation
数组中保存的数字用于 DrawX()
方法在由字符串 sCoordinates
指定的某个方块的中心绘制“X”。然后,布尔变量 bInserted
用于指示 DrawX()
方法是绘制单个坐标(即敌人发送的坐标),还是绘制当前用户选择放置在其棋盘上的坐标。
DrawX 方法
最后,我们将讨论 DrawX()
方法。它非常、非常简单。您自己看看。
private void DrawX(int iX, int iY, Graphics G)
{
Pen pColor;
if(!m_bWhatX)
{
//in the case of server the code is this:
// pColor = new Pen(Color.Blue, 5);
pColor = new Pen(Color.Red, 5);
}
else
{
//in the case of server the code is this:
// pColor = new Pen(Color.Red, 5);
pColor = new Pen(Color.Blue, 5);
}
Point pA = new Point(iX, iY);
Point pB = new Point(iX+30, iY+30);
Point pA1 = new Point(iX+30, iY);
Point pB1 = new Point(iX, iY+30);
G.DrawLine(pColor,pA,pB);
G.DrawLine(pColor,pA1,pB1);
}
请记住,此方法使用布尔变量 m_bWhatX
来绘制发送的用于填充棋盘的客户端坐标,或绘制敌人发送的坐标。该变量接收 bInserted
参数的值。我将服务器的 Pen
对象注释掉了,只是为了让您知道每个应用程序绘制什么“X”。您明白为什么这个方法如此简单了吧?
再次感谢
伙计,我希望您喜欢这篇文章以及它的第一部分。我非常喜欢写这篇文章(这是我第一次)。如果您喜欢或不喜欢,请发电子邮件给我 邮件,包含您的评论、疑问、更正或新想法。我想告诉您一件事:如果您想成为一名优秀的程序员,您需要做一件事:努力学习,大量学习。这是我个人经验之谈。
文章历史
- 2005-07-16 - 本系列文章第二部分的开始。
- 2005-07-26 - 我停了一段时间,但最终,第二部分完成了。对两部分都进行了修订,并将完整包发送给了 The Code Project。