65.9K
CodeProject 正在变化。 阅读更多。
Home

海军战舰游戏 - II

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.82/5 (5投票s)

2005年7月27日

5分钟阅读

viewsIcon

49658

downloadIcon

976

一款客户端-服务器端的海军战棋游戏。

Server image with the X

引言

一款使用套接字和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。此语句保证了在您完成所有必要操作后该对象会被释放。请注意,有一个名为 DesenhoDraw 对象。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。
© . All rights reserved.