海军战斗游戏-III(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/navy-battle-game-iii-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 25 分钟阅读 - 12506 个词海军战斗游戏-III(译文)
原文地址:https://www.codeproject.com/Articles/11287/Navy-Battle-Game-III
原文作者:Perry Marchant
译文由本站 robot-v1.0 翻译
前言
An article on creating a multi-player game using the TGSDK.
有关使用TGS’DK创建多人游戏的文章.
介绍(Introduction)
本文演示了如何使用提供给第三方开发人员的多人在线游戏开发SDK(TGSDK)来实现Navy Battle游戏.本文的灵感来自Alex Cutovoi,他写了最初的两部分(This article demonstrates how to use the multi-player online game development SDK (TGSDK), provided to third party developers, to implement the Navy Battle game. This article is inspired by Alex Cutovoi who wrote the original two part) 海军战役(Navy Battle) 文章.(articles.)
背景(Background)
在(At) 托盘游戏(TrayGames) 我们提供了API和易于使用的测试工具,使您可以在本地计算机上开发和测试多人游戏,而无需昂贵的多计算机测试系统.当您的游戏准备就绪时,您可以将其发送给我们,我们只需将其插入我们的托管服务器网络即可.(we provide the APIs and a simple-to-use test harness that allow you do develop and test your multi-player game on your local computer without the need for an expensive multiple computer test system. When your game is ready to go you send it to us and we simply plug it into our network of hosting servers.) 当我看到编写了我非常喜欢的<海军战斗>游戏的C#实现时,我立即认为这是移植到TGSDK的完美游戏.使用该SDK,我们可以重用原始文章中的所有游戏代码,我们只需要删除套接字和IO代码,并将其替换为一些TGSDK代码即可.但是,为了添加功能并修复某些错误,必须重写许多原始游戏代码以及TGSDK所需的更改.令人高兴的是,API使客户端-服务器系统完全无缝且对我们不可见.我们根本不需要对TCP/IP或匹配的播放器做任何事情,因此我们可以专注于制作出色的游戏,因此让我们深入研究吧!(When I saw a C# implementation of the Navy Battle game was written, a game I like a lot, I immediately thought it would be a perfect game to port to the TGSDK. This SDK would allow us to reuse all of the game code from the original article, we only need to remove the socket and IO code and replace it with some TGSDK code. However in order to add functionality and fix some bugs a lot of the original game code had to be rewritten along with the changes required for the TGSDK. The nice part is that the APIs make the client-server system totally seamless and invisible to us. We don’t have to do anything at all with TCP/IP or matching players, so we can focus on making an awesome game, so let’s dig in!)
使用代码(Using the code)
可下载的演示文件和可下载的源归档文件均包含"(Both the downloadable demo and downloadable source archive files contain a “)游戏示范(Game Demo)“文件夹.此文件夹包含您为TGSDK生成游戏所需的TrayGames库.此文件夹还包含尝试进行Navy Battle游戏所需的所有内容.TrayGames游戏服务器实际上只是开发人员构建的一个或多个DLL,用于与他们的游戏一起进行.主DLL(如果有多个)称为”(” folder. This folder has the TrayGames libraries you need to build a game for the TGSDK. This folder also contains everything you need to try out the Navy Battle game. A TrayGames game server is actually just one or more DLL’s built by a developer to go with their game. The main DLL (if there is more than one) is known as the “)游戏守护进程(Game Daemon)".要使用它,您需要运行一个名为”(". To use it you need to run a tool called the “)守护进程线束(Daemon Harness)"((” ()TGDaemonHarness.exe(TGDaemonHarness.exe)),您可以在"(), which you’ll find in the “)游戏示范(Game Demo)服务器在其生产环境中的TrayGames服务器上运行时,它将运行大量支持服务,以提供游戏匹配和与游戏客户端的连接.但是,这种设置对于开发人员而言并不实用.通常只有一台计算机可以开发.因此,游戏守护程序可以在守护程序线束中以"测试线束"模式运行一种特殊模式.要以这种模式运行游戏守护程序,您需要一个游戏守护程序初始化文件.该文件的位置必须与Daemon Harness位于同一文件夹中.对于本文而言,“海军战斗"游戏的”(” folder. When the server is running in its production environment on a TrayGames server, it has plenty of support services running with it to provide for the game matching and connections to the game clients. This set up is not practical for developers however, who often have only one computer to develop on. For this reason there is a special mode that the game daemon can run in called “Test Harness” mode within the Daemon Harness. To run a game daemon in this mode you need a game daemon initialization file. The location of this file needs to be in the same folder as the Daemon Harness. For this article the initialization file has been provided for the Navy Battle game in the “)游戏示范(Game Demo)“文件夹.在此文件中,您将找到以下行:(” folder. In this file you will find the line:)
NumPlayers = 2
该条目的目的是在启动游戏之前告诉服务器要等待多少个客户端.如果该文件不存在且放置在正确的位置,则服务器会认为该文件处于实时生产环境中,并查找开发人员计算机上不存在的后端支持系统.在这种情况下,不能保证服务器的行为(尽管您很可能会收到异常).现在,您可以运行守护程序线束了.从”(This entry serves the purpose of telling the server how many clients to wait for, before launching the game. If this file is not present and in the right place, the server thinks it is in a live-production environment, and looks for back end support systems that are not present on a developer’s machine. No guarantee is made as to the server’s behavior in this situation (though you are likely to get an Exception). Now you are ready to run the Daemon Harness. Run it from the “)游戏示范(Game Demo)文件夹中,单击”(" folder, click the “)加载游戏守护进程(Load Game Daemon)“按钮,然后导航到”(” button and navigate to the “)NavyBattleDaemon.dll(NavyBattleDaemon.dll)".选择文件并将其打开.守护程序线束应报告游戏守护程序在"测试束线"模式下以及当前时间启动.现在我们需要将几个游戏连接到该守护程序.(”. Select the file and open it. The Daemon Harness should report that the game daemon was started in Test Harness mode, and the current time. Now we need to get a couple of games connected to this daemon.)
我们需要运行两个游戏客户端,因为这是一个两人游戏.运行海军作战游戏应用程序的一个实例,然后再运行另一个.在运行游戏的第二个实例时,游戏守护程序应意识到它有足够的玩家并继续启动游戏.请注意,此设置只能在开发模式下在一台计算机上使用.您可以在开发环境中运行一个客户端以帮助调试.调试游戏守护程序并不容易,但是您可以使用(We need to run two game clients, because it’s a two player game. Run one instance of the Navy Battle game application, and then another. Upon running the second instance of the game the game daemon should realize it has enough players and proceed to start the game. Note that this setup works only across one machine in development mode. You can run one client inside the development environment to aid in debugging. It is not easy to debug a game daemon, but you can use) DiagnosticMsg
事件以获取有关正在发生的事情的反馈.如果您下载源代码,则可以找到"(events to get feedback on what’s going on. If you download the source, you can find a “)海军战斗游戏(NavyBattleGame.sln)“下的解决方案文件(” solution file under the “)游戏样本(Game Sample)“文件夹,该文件夹将构建本文中提到的所有项目.(” folder that will build all of the projects mentioned in this article.)
客户端(The client)
我们有一堂课(We have a single class) NavyBattleForm
其中包含绘制战场,游戏逻辑和TGSDK调用的代码.由于我将专注于TGSDK,因此让我们看一下促进游戏交流所需的步骤:(that contains the code for drawing the battlefield, game logic, and TGSDK calls. Since I will be focusing on the TGSDK, let’s take a look at the steps needed to facilitate communication for a game:)
- 添加对(Add a reference to the)*TGGameLib(TGGameLib)*库并导入(library and import the)
TG.Game
命名空间.(namespace.) - 声明并初始化一个(Declare and initialize a)
TGGameClass
班级成员,并称其为(class member, and call its)Start
方法.(method.) - 为游戏服务器将引发的事件建立处理程序.这些事件处理程序将负责接收和处理与游戏和系统相关的消息.(Establish handlers for events that the game server will raise. These event handlers will be responsible for receiving and processing the game and system related messages.)
- 设置计时器(或使用其他机制)以检查传入消息,这将引发必须处理的事件.(Set a timer (or use some other mechanism) to check for incoming messages, which will raise events that must be handled.)
- 使用(Use the)
TGGameClass.SendToServer
将游戏消息发送给其他玩家的方法.(method to send game messages to the other player.) 现在让我们详细了解这些步骤,我尝试将代码整理为(Let’s take a look at these steps in detail now, I’ve tried to organize the code in)#region
为了清楚起见,在可下载的源代码中添加了块.可下载的源归档文件包含您需要在游戏项目中引用的库.首先是导入语句:(blocks in the downloadable source code for clarity. The downloadable source archive contains the libraries that you will need to reference in your game project. First there is the import statement:)
using TG.Game;
然后,我们必须添加一些必要的成员(Then there are some necessary members we have to add to the) Form
类.这些是与使用TGSDK直接相关的数据成员.您将在本节中看到如何实际使用它们.(class. These are the data members that are directly related to using the TGSDK. You’ll see how these are actually used throughout this section.)
private TG.Game.TGGameClass m_TgGame =
new TG.Game.TGGameClass();
private System.Threading.AutoResetEvent m_NewData =
new System.Threading.AutoResetEvent(false);
private System.Windows.Forms.Timer m_Timer =
new System.Windows.Forms.Timer();
private int m_WhoStarts = 0;
private int m_WhosTurn = -1;
private Int32 m_LocalPlayerIndex;
// stores both player names
private string[] m_PlayerNames = new string[2];
public static string m_GameName = "NavyBattle";
为事件建立事件处理程序(To establish the event handlers for the) TGGameClass
您将处理的对象(object you will handle the) Load
Windows窗体上的事件,并将以下代码添加到您的处理程序方法中:(event on a Windows Form and add the following code to your handler method:)
// Wire the appropriate handlers to their
// incoming TrayGames message events.
// this first handler is responsible for receiving
// and processing all 'normal' game messages
m_TgGame.GameEvent +=
new TGGameClass.TGGameEventHandler(GameEventHandler);
// This handler is for all incomming administration events.
// Administration events are differentiated by an
// AdminEventType, and include Connected, Disconnected,
// Minimize, and Shutdown.
m_TgGame.AdminEvent +=
new TGGameClass.TGAdminEventHandler(AdminEventHandler);
// This handler is used to signal our main GUI
// thread (this one) that new data has arrived. Be
// aware that the handler for this event is not running
// in the Main thread, but rather in a
// TrayGames Communications thread.
m_TgGame.DataEvent += new EventHandler(DataEventHandler);
// Now set a timer going to check for incoming data
m_Timer.Interval = 200;
m_Timer.Tick += new EventHandler(TimerTickEventHandler);
m_Timer.Start();
// The handlers are set up so the TGGameClass
// object is now properly initialized.
// We can call TgGame.Start which results
// in a connection to the server.
m_TgGame.Start(8000, true); // 8K memQ
设置处理程序后,(Once the handlers are set up the) TGGameClass
对象已正确初始化,我们可以调用(object is properly initialized, and we can call) TGGameClass.Start
建立与服务器的连接.在加载Windows窗体时建立连接将表明我们”(to make a connection to the server. Establishing the connection upon loading the Windows Form will indicate that we are “)准备(Ready)",我们将发送一个”(”, we will send an “)初始放置(InitialPlacement)“消息,表示我们所有的飞船都已放置在板上,因此可以开始播放.一旦服务器知道客户端已成功启动,它就可以发送(” message later to indicate that all of our ships have been placed on the board so the play can start. Once the server knows that the client has started up successfully, it can send the) AdminEventType.Connected
消息(稍后会详细介绍)给两个游戏客户端,这样您就可以看到您正在与之对抗的人的姓名,也许是您放置碎片时可能要发送的任何聊天消息(例如,告诉对手赶快!).尽管此版本的Navy Battle不支持聊天,但TGSDK使其添加起来相当容易,因此将来的版本可能会添加.(message (more on this later) out to both game clients so that you can see the name of the person you’re playing against, maybe for any chat messages that you might want to send while placing the pieces (for example telling your opponent to hurry up!). Although this version of Navy Battle doesn’t support chat, the TGSDK makes it fairly easy to add in so maybe a future version will.)
现在让我们看一下事件处理程序.我想指出的第一件事是,游戏客户端要处理的大多数消息都是由游戏服务器决定的.因此,这里描述的消息是特定于我对Navy Battle游戏的实现的,只有少数例外. TGSDK在传递的消息以及这些消息的内容方面是完全灵活的.它不在乎您要为游戏传递什么,它仅提供消息传递机制.(Now let’s look at the event handlers. The first thing I want to point out is that most of the messages that the game client is going to handle are dictated by your game server. So the messages described here are specific to my implementation of the Navy Battle game, with a few exceptions. The TGSDK is completely flexible in terms of the messages that are passed and what the contents of those messages are. It doesn’t care about what you want to pass for your game, it only provides the message passing mechanism.)
的(The) GameEventHandler
方法将接收游戏消息.消息信息始终包含在(method will receive game messages. Message information is always contained in a) System.Collections.Hashtable
类.此游戏必须处理的两条消息是(class. The two messages that must be processed for this game are the) Ready
和(and) Update
消息.的(messages. The) Ready
一旦所需的播放器数量(在这种情况下为2)上线,TGSDK总是将消息发送给所有播放器.此消息将使我们能够获取我们的玩家编号和姓名,轮到的年龄以及其他玩家数据(如果需要).的(message is always sent by the TGSDK to all players once the required number of players, in this case 2, come online. This message will allow us to get our player number and name, whose turn it is, and other player data if needed. The) Update
message是我们为此游戏创建的自定义消息.(message is a custom message we have created for this game.)
private void GameEventHandler(object sender, GameEventArgs e)
{
Hashtable data = e.Value;
// The data received is encapsulated in a
// Hashtable, it is important to fully
// appreciate the power of the hashtable
// to use TrayGames effectively.
if (data.Contains("MsgType"))
{
string MsgType = (string)data["MsgType"];
switch (MsgType)
{
case "Ready":
m_LocalPlayerIndex = (int)data["YourPlayerNum"];
for (int i = 0; i <= 1; i++)
m_PlayerNames[i] = (string)data[i];
m_WhosTurn = m_WhoStarts;
break;
// . . .
我们可以期待(We can expect an) Update
我们的对手每次行动时都会传达的信息.此消息将包含玩家,他们已采取的行动,接下来的轮到谁,并告诉我们是否有人赢得了比赛.这使我们能够在海军战役期间强制玩家回合,并且由于获胜者的逻辑在游戏服务器中作弊很难.由于游戏是回合制,因此使用TGSDK的游戏中通常会出现这样的消息.但是,此消息不是强制性的.您可以传递游戏中喜欢的任何消息,其中包含您想要的任何内容.的(message every time our opponent makes a move. This message will contain the player, what move they have made, whose turn it is next and let us know if someone has won the game. This allows us to enforce player turns during the Navy Battle, and because the winner logic is in the game server cheating is difficult. A message like this one is typical of a game using the TGSDK since the games are turn based. This message is not mandatory however; you can pass whatever messages you like in your game, containing whatever you wish. The) Hashtable
必须按照游戏服务器对每种自定义消息的期望方式进行准备,例如:(must be prepared in the way that your game server will expect for every custom messages like this one:)
// . . .
case "Update":
int Turn = (int)data["Turn"];
int Winner = (int)data["Winner"];
m_WhosTurn = Turn;
if (data.Contains("AttackCoordinates"))
{
// Update the appropriate board
// with the attack info
int coordinates =
(int)data["AttackCoordinates"]; //0..24
int x = coordinates % BoardXSize;
int y = coordinates / BoardYSize;
bool hit = (bool)data["Hit"];
int[,] board;
if (Turn == m_LocalPlayerIndex)
// If it's my turn now, then the
// last shot fired was at *me*.
board = m_YourBoard;
else
// If it's not my turn then I fired
// the last shot.
board = m_EnemyBoard;
board[x,y] = (hit)?3:2;
// Refresh the affected square so it updates
if (Turn == m_LocalPlayerIndex)
panel1.Invalidate(new Rectangle(x*40+1,
y*40+1, 39, 39));
else
panel2.Invalidate(new Rectangle(x*40+1,
y*40+1, 39, 39));
}
if (Winner == 0 || Winner == 1)
{
// Somebody won
if (Winner == m_LocalPlayerIndex)
ClientLabel.Text =
"You sunk your enemy's navy! You win!";
else
ClientLabel.Text =
m_PlayerNames[Winner] + " has won the game.";
}
else
{
// Nobody has won yet so keep playing
if (m_WhosTurn == m_LocalPlayerIndex)
ClientLabel.Text = "It's your move, " +
"click on an Enemy Grid location...";
else
ClientLabel.Text = "It's " +
m_PlayerNames[m_WhosTurn] + "'s move...";
}
break;
}
}
}
要发送消息,您只需准备一个(To send a message you simply need to prepare a) Hashtable
包含消息内容.您可以在(with the message contents in it. You can send anything you like in the) Hashtable
,只要它是可序列化的(实现(, as long as it is serialiazable (implements) ISerializable
),或者是原始类型,例如(), or it’s a primitive type, such as) string
和(and) int
.这样,如果您有要触发到服务器的类,则可以构建它来实施(. In this way, if you have a class you want to fire through to the server, you can build it to implement) ISerializable
并将其添加到(and add it to the) Hashtable
消息,它将在服务器端显示为重构的类.就像您想象的那样,序列化时从可序列化对象到其他对象的引用需要特殊处理,并且服务器必须知道发送给它的所有类型.(message, and it will appear on the server side as a reconstituted class. As you might imagine, references from a serializable object to other objects need special handling when serializing, and the server must know about all the types you send to it.)
我们需要让游戏服务器知道我们已经将所有飞船放置在板上,因此我们创建了一个(We need to let the game server know that we have placed all of our ships on our board, so we created an) InitialPlacement
信息.服务器从两个游戏客户端都收到此消息后,就可以开始游戏了.(message. Once the server has received this message from both the game clients the play can begin.)
// If five ships have been placed, we exit Placement Mode...
if (PlacementCount == ShipsCount)
{
m_IsInitialPlacement = false;
panel2.Visible = true;
ClientLabel.Text = "Waiting for your opponent...";
// Send message to server to say we're ready
Hashtable msg = new Hashtable();
msg.Add("MsgType", "InitialPlacement");
msg.Add("Player", m_LocalPlayerIndex);
msg.Add("Placement", m_YourBoard);
m_TgGame.SendToServer(msg);
}
我们使用(We use a) MouseUp
事件处理程序,用于使用发送事件坐标(event handler for sending the attack coordinates using the) TGGameClass.SendToServer
方法.首先,我们确保它不是重复射击,然后将射击记录在敌方板上,最后发送一个(method. First we make sure that it’s not a repeat shot, then we register the shot on our enemy board, and finally send a) Move
包含镜头坐标的消息到服务器.这是特定于我们游戏的另一条自定义消息:(message containing the shot coordinates to the server. This is another custom message specific to our game:)
if ((m_EnemyBoard[UpGridX, UpGridY] & 2) == 0 &&
m_WhosTurn == m_LocalPlayerIndex)
{
m_EnemyBoard[UpGridX, UpGridY] |= 2;
Hashtable msg = new Hashtable();
msg.Add("MsgType", "Move");
msg.Add("Player", m_LocalPlayerIndex);
msg.Add("AttackCoordinates", UpGridY * 5 + UpGridX);
m_TgGame.SendToServer(msg);
m_WhosTurn = ((m_WhosTurn + 1) % 2);
ClientLabel.Text = "It's " +
m_PlayerNames[m_WhosTurn] + "'s move...";
}
快速浏览一下TGSDK提供的消息传递机制.游戏客户端处理(That’s a quick look at the message passing mechanism the TGSDK provides. The game client handles the) GameEvent
事件以接收来自服务器的消息,并调用(event to receive a message from the server, and it calls the) SendToServer
将消息发送到服务器的方法,该消息会将其路由到其他游戏客户端.这些消息大部分是特定于您的游戏逻辑的.就这么简单.在本文后面有关服务器的部分中,我们将在游戏守护进程中看到所有这些消息的补充.让我们继续查看服务器引发的事件.(method to send a message to the server which will route it to the other game client(s). These messages, for the most part, are specific to your game logic. It’s that simple. We’ll see the complement to all of these messages in the game daemon in the section about the server later in this article. Let’s continue looking at the events that the server raises.)
除了(Besides the) GameEvent
如果还有(event there is also the) AdminEvent
事件.这些事件由TGSDK引发,以指示不同的管理状态.这些是您应遵守的来自TrayGames服务器或ClientManager的指令.的(event. These events are raised by the TGSDK to indicate different administrative states. These are directives from the TrayGames server or ClientManager which you should comply with. The) AdminEventType.Connected
事件类型很重要,因为它为我们提供了有关玩家的详细信息,包括他们的姓名,排名(如果为此游戏而实现),他们玩游戏的次数等.首先,让我们看一下(event type is important because it provides us with detailed information about the player including their name, ranking (if implemented for this game), the number of times they have played the game, etc. First let’s look at the) Hashtable
包含我们服务器代码准备的所有这些信息,请注意内部(that contains all of this information as prepared by our server code, note the inner) Hashtable
:(:)
Dim PlayerData As New Hashtable ' Data for all players
For Each Player As GamePerson In Group.Players.Values
' Info on a particular player
Dim PlayerInfo As Hashtable = New Hashtable
PlayerInfo.Add("Nick", Player.Nick)
PlayerInfo.Add("Rank", Player.Rank)
PlayerInfo.Add("TimesPlayed", Player.TimesPlayed)
PlayerInfo.Add("HasBoughtGameLevel", _
CType(Player.BoughtGameLevel, Integer))
PlayerData.Add(player.PlayerNumber, PlayerInfo)
Next
Msg.Add("PlayerData", PlayerData)
For Each player As GamePerson In Group.Players.Values
Msg("PlayerNum") = player.PlayerNumber
If IsTestHarnessMode Then
TestMsgToPlayer(Group.Id, player.Id, Msg)
Else
SendMsgToPlayer(Group.Id, player.id, Msg)
End If
Next
现在这是我们的(Now here is our) AdminEvent
事件处理程序代码访问服务器提供的所有信息:(event handler code accessing all of that information the server provides:)
public void AdminEventHandler(object sender, AdminEventArgs e)
{
switch (e.Type)
{
case AdminEventType.Connected:
// Extract the PlayerData from the EventArgs object
int LocalPlayerIndex = (int)e.Data["PlayerNum"];
Hashtable playerData = (Hashtable)e.Data["PlayerData"];
foreach (DictionaryEntry de in playerData)
{
int PlayerNumber = (int)de.Key;
Hashtable PlayerInfo = (Hashtable)de.Value;
string PlayerName = (string)PlayerInfo["Nick"];
int PlayerRank = (int)PlayerInfo["Rank"];
int TimesPlayed = (int)PlayerInfo["TimesPlayed"];
int HasBoughtGameLevel =
(int)PlayerInfo["HasBoughtGameLevel"];
}
// We now have a connection to the
// game server, we may receive
// messages at any time, and are
// free to send messages also.
break;
case AdminEventType.Disconnected:
// We may no longer send messages, nor
// will we receive any, until we reconnect
break;
case AdminEventType.Shutdown:
// The client manager has called
// for the shutdown of all games.
OnClose(true);
break;
}
}
最后,我们来看一下(Lastly, we look at the) DataEvent
事件处理程序.此事件的处理程序不在主线程中运行,而是在TrayGames通信线程中运行.建议您仅使用此事件来通知您的主线程处理新消息,因为此处理程序是从非GUI线程调用的.所以我们在这里所做的只是设置(event handler. The handler for this event is not running in the main thread, but rather in a TrayGames communications thread. It is advised that you simply use this event to signal your main thread to process the new messages because this handler is being called from a non-GUI thread. So all we do here is just set the) m_NewData
事件通知,因此我们的主线程意识到需要处理一些事情.(event to signaled, so our main thread realizes that there’s something to process.)
public void DataEventHandler(object sender, EventArgs e)
{
m_NewData.Set();
}
的事件处理程序(The event handler for the) Timer
我们在我们的创造(that we have created in our) Load
Windows窗体的事件处理程序等待(event handler for the Windows Form waits for) m_NewData
被告知.发出信号时(to be signaled. When it is signaled the) TGGameClass.RetrieveMessages
方法被调用,导致(method is called, resulting in) AdminEvent
和(and) GameEvent
从该线程引发的事件,这应该防止任何与线程相关的冲突.(events getting raised from this thread, which should prevent any thread related conflicts.)
private void TimerTickEventHandler(object sender, EventArgs e)
{
if (m_NewData.WaitOne(0, false))
{
// We have new data!
m_TgGame.RetrieveMessages();
}
}
这就是针对海军战斗游戏客户端的TGSDK特定代码.其余代码是绘图和游戏逻辑.(That’s all about the TGSDK specific code that there is for the Navy Battle game client. The rest of the code is drawing and game logic.)
服务器(The server)
在TrayGames世界中,游戏客户端的对应对象是游戏守护程序.这是您实现的服务器端代码,用于协调所有玩家的回合,跟踪得分等.游戏守护程序等待所有玩家连接并发送其启动消息.它处理(The counterpart of the game client in the TrayGames world is the game daemon. This is the server side code that you implement that coordinates the turns of all players, keeps track of scores, etc. The game daemon waits for all players to get connected and send their startup message. It handles the) NewGroupEvent
事件,通常会创建一个游戏实例,并将一些游戏初始化数据发送给所有玩家.游戏守护进程还处理(event, typically creating a game instance, and sending some game initialization data out to all players. The game daemon also handles the) PlayerMsgEvent
事件以接收来自游戏客户端的消息,该消息通常传递到该游戏客户端所属的游戏实例.最后,它处理(event to receive a message from a game client, this message is usually passed on to the game instance, to which that game client belongs. Lastly it handles the) PostErrorEvent
错误处理事件.(event for error handling.)
- 添加对(Add a reference to the)*TGDaemonLib(TGDaemonLib)*库并导入(library and import the)
TG.Daemon
命名空间.(namespace.) - 创建一个(Create a)
GameDaemon
实现(object that implements the)IGameDaemon
接口.(interface.) - 建立方法来处理系统将引发的关键事件.(Establish methods to handle key events that the system will raise.)
- 建立处理事件的方法(Establish methods to handle events that the)
Game
对象会上升.(object will raise.) - 创建一个(Create a)
Game
具有您游戏特定代码的对象.(object that has your game specific code.) 我们将研究创建(We’ll look at what’s involved in creating the)GameDaemon
全班第一它封装了游戏实例管理器和游戏状态以及管理和进度游戏的调用和逻辑.的(class first. It encapsulates the game instance manager and game states as well as the calls and logic to manage and progress the game. The)GameDaemon
对象实现(object implements the)IGameDaemon
在此定义的接口:(interface which is defined here:)
Event GameStartedEvent(ByVal sender As Object, _
ByVal e As GameDaemonEventArgs)
Event GameEndedEvent(ByVal sender As Object, _
ByVal e As GameDaemonEventArgs)
Event PlayerJoinedEvent(ByVal sender As Object, _
ByVal e As GameDaemonEventArgs)
Event PlayerLeftEvent(ByVal sender As Object, _
ByVal e As GameDaemonEventArgs)
Event MsgIntoDaemonEvent(ByVal sender As Object, _
ByVal e As GameDaemonMsgEventArgs)
Event MsgOutOfDaemonEvent(ByVal sender As Object, _
ByVal e As GameDaemonMsgEventArgs)
Event DiagnosticMsgEvent(ByVal sender As Object, _
ByVal e As GameDaemonMsgEventArgs)
ReadOnly Property Id() As Guid
ReadOnly Property Games() As Hashtable
ReadOnly Property Channel() As String
Property AllowNewGames() As Boolean
Sub Initialize()
Sub Close()
Sub EndGame(ByVal groupId As Guid)
Sub EndAllGames()
首先,我们的进口声明(First there is the import statement in our) GameDaemon
类:(class:)
using TG.Daemon;
然后,我们必须添加一些必要的成员(Then there are some necessary members we have to add to the) GameDaemon
类.这些是与使用TGSDK直接相关的数据成员.您将在本节中看到如何实际使用它们.(class. These are the data members that are directly related to using the TGSDK. You’ll see how these are actually used throughout this section.)
首先,我们必须声明我们的类将需要的一些数据成员:(First we have to declare some data members that our class will need:)
private TGDaemonClass _myTgD = new TGDaemonClass();
private Hashtable _games = new Hashtable(); // of clsGames
private bool _allowNewGames;
private Guid _id = Guid.NewGuid();
实施属性(Implementing the properties of the) IGameDaemon
界面很简单.我们只是返回上面描述的一些类数据成员:(interface is straightforward. We’re just returning some of our class data member described above:)
public Guid Id
{
get { return(_id); }
}
public string Channel
{
// Must be unique in the TrayGames system
get { return("NavyBattle"); }
}
public Hashtable Games
{
get { return(_games); }
}
public bool AllowNewGames
{
get { return(_allowNewGames); }
set { _allowNewGames = value; }
}
接下来,我们需要建立事件处理程序并调用(Next, we need to establish the event handlers and call the) Startup
方法.这是在(method. This is done in the) Initialize
方法如下图所示:(method shown below:)
void TG.Daemon.IGameDaemon.Initialize()
{
_myTgD.NewGroupEvent += new
TGDaemonClass.NewGroupEventHandler(GameGroupAdd);
_myTgD.PlayerMsgEvent += new
TGDaemonClass.PlayerMsgEventHandler(MsgReceive);
Game.PostErrorEvent += new
Game.PostErrorEventHandler(PostErrorHandler);
_allowNewGames = true;
if (_myTgD.Startup(Channel))
// . . .
}
我们用来完成此界面的其他方法涉及结束游戏,其中包括向小组广播消息,清理(The other methods that we implement to complete this interface deal with ending a game and which includes broadcasting a message to the group, cleaning up the) Hashtable
游戏并关闭守护进程:(of games and closing down the daemon:)
public void Close()
{
EndAllGames();
_myTgD.Shutdown();
// If you don't call dispose, then the Listener
// thread in the DaemonLib is never killed.
_myTgD.Dispose();
}
在里面(In the) Initialize
我们分配的方法(method we assigned the) NewGroupEvent
事件到(event to the) GameGroupAdd
方法.当系统引发此事件时(method. When this event is raised by the system) GameGroupAdd
采取以下步骤:(take the following steps:)
- 得到(Get the)
GameGroup
使用我们的对象(object using our)GroupId
系统分配给我们小组的唯一ID.该ID作为事件参数发送.(a unique ID for our group assigned by the system. This ID is sent as an event argument.) - 创建一个新的实例(Create a new instance of the)
Game
目的.稍后我们将查看该对象的详细信息.(object. We will be looking at the details of this object later.) - 设置(Setup the)
Game
对象并将其添加到我们的游戏桌中(object and add it to our table of games, which is the)_games
数据成员.安装程序包括将事件处理程序添加到由(data member. Setup includes adding event handlers to the delegates defined by the)Game
目的.(object.) - 向所有玩家发送"就绪"消息.(Send out a “Ready” message to all players.)
- 开始第一个游戏.(Start the first game.)
下面显示(The following shows the)
GameGroupAdd
方法.请注意,一些诊断消息和外围内容已被省略,以保持该方法的可读性:(method. Note that some of the diagnostic messages and peripheral stuff has been left out to keep the method more readable:)
public void GameGroupAdd(object sender, NewGroupEventArgs e)
{
Guid GroupId = e.GroupId;
GameGroup gameGroup =
(GameGroup)_myTgD.GameGroups[GroupId];
// Add a new group of players to the server
Game ng = new Game(GroupId, gameGroup);
//_myTgD.Msg2Player
ng.MsgToPlayerEvent += new
Game.MsgToPlayerEventHandler(MsgToPlayerHandler);
//_myTgD.Msg2Group
ng.MsgToGroupEvent += new
Game.MsgToGroupEventHandler(MsgToGroupHandler);
ng.GameOverEvent += new
Game.GameOverEventHandler(SignalGameOver);
ng.PlayerLeftGameEvent += new
Game.PlayerLeftGameEventHandler(PlayerLeftHandler);
_games.Add(GroupId, ng);
// If you are ever sending messages to clients
// that ARE NOT RESULTING FROM AN INCOMING MESSAGE,
// then it is important that you hold incomming
// messages while you do the processing.
_myTgD.HoldMyMessages(gameGroup.Id, true);
// Get the ball rolling with the
// first message to each human player
Hashtable msg = new Hashtable();
// First message from the server.
// Your Player number is tp.PlayerNum
msg.Add("MsgType", "Ready");
msg.Add("YourPlayerNum", -1);
foreach(GamePerson tp in gameGroup.Players.Values)
{
msg.Add(tp.PlayerNumber, tp.Nick);
// All players start as connected to the game.
tp.PlayerStatus = GamePerson.UserStatus.Connected;
}
// Send out 'Ready' messages to non AI players
foreach(GamePerson tp in gameGroup.Players.Values)
{
if (!tp.AI)
{
msg["YourPlayerNum"] = tp.PlayerNumber;
_myTgD.MsgToPlayer(gameGroup.Id,
tp.PlayerNumber, msg);
}
}
// Start the first game
ng.StartFirstGame();
// Don't forget to stop holding messages
_myTgD.HoldMyMessages(gameGroup.Id, false);
_myTgD.GameStartedForGroup(gameGroup.Id);
GameStartedEvent(this,
new GameDaemonEventArgs(gameGroup.Id));
foreach(GamePerson player in
gameGroup.Players.Values)
{
if (!player.AI)
PlayerJoinedEvent(this,
new GameDaemonEventArgs(GroupId,
(Guid)gameGroup.PlayerNumberToGuid[player.PlayerNumber],
player.PlayerNumber, player.Nick));
}
e.Success = true;
}
请注意,(Note that the call to) HoldMyMessages
仅出于线程安全性考虑.如果您的计时器正在发送一条消息,并且(在另一个线程上)有一条消息传入并正在处理中,那么如果您的游戏守护进程不是线程安全的,则可能会出现问题.什么(is just for thread safety. If your timer is sending a message and (on a different thread) a message comes in and is being processed, there is the possibility for problems if your game daemon is not thread safe. What) HoldMyMessages
例如,在使用计时器线程执行操作时,它可以使您禁止消息线程执行操作.(does for you is allow you to prohibit the message thread from doing stuff while you’re doing things with the timer thread for example.)
在里面(In the) Initialize
方法,我们还分配了(method we also assigned the) PlayerMsgEvent
事件到(event to the) MsgReceive
方法.当从游戏客户端收到消息时引发此事件.下面显示了消息的正常处理.通常,我们只想将消息路由到所涉及的游戏,这就是我们在此示例中所做的.但是,在某些情况下,我们需要进行其他处理.对于游戏中仅封装一个玩家的持久性世界/掉落游戏,但守护进程可能正在运行这些玩家/游戏所在的整个世界,则消息的所有处理都很可能在此处进行:(method. This event is raised when a message is received from a game client. The following shows the normal handling of a message. Typically we just want to route the message to the game involved which is what we are doing in this example. However there are some cases where we want to do additional processing. For persistent world/drop in games where the game encapsulates only one player, but the daemon might be running the whole world where these Player/Games are taking place in, then all the processing of the message would most likely take place here:)
public void MsgReceive(object sender, PlayerMsgEventArgs e)
{
Guid groupId = e.GroupId;
int playerNumber = e.PlayerNumber;
Hashtable msg = e.Msg;
if (_games.Contains(groupId))
{
MsgIntoDaemonEvent(this, new GameDaemonMsgEventArgs(groupId,
playerNumber, msg));
Game MyGame = (Game)_games[groupId];
MyGame.MsgReceive(playerNumber, msg);
}
}
现在我们可以接收消息了,我们还需要具有将消息发送给播放器的功能,以下方法可以做到这一点:(Now that we can receive messages we also need the capability to send messages to the players, the following methods do just that:)
private void MsgToPlayerHandler(Guid groupId,
int playerNumber, Hashtable msg)
{
MsgOutOfDaemonEvent(this, new GameDaemonMsgEventArgs(groupId,
playerNumber, msg));
_myTgD.MsgToPlayer(groupId, playerNumber, msg);
}
private void MsgToGroupHandler(Guid groupId, Hashtable msg)
{
MsgOutOfDaemonEvent(this,
new GameDaemonMsgEventArgs(groupId, -1, msg));
_myTgD.MsgToGroup(groupId, msg);
}
这就是设置(That’s all there is to set up the) GameDaemon
.现在,让我们继续执行(. Now let’s move on to the implementation details of the) Game
类.首先,我们需要与(class. First we need the same import statement that the) GameDaemon
具有:(has:)
using TG.Daemon;
然后,我们必须声明该类将使用的一些数据成员,以及守护程序类可以附加到的事件(带有委托):(Then we have to declare some data members that this class will use, and events (with delegates) that the daemon class can attach to:)
public delegate void MsgToGroupEventHandler(Guid id,
Hashtable msg);
public delegate void MsgToPlayerEventHandler(Guid id,
int PlayerNumber, Hashtable msg);
// Send game-over message to all players
public delegate void GameOverEventHandler(Guid id);
public delegate void PlayerLeftGameEventHandler(Guid id,
Guid playerGuid);
public delegate void PostErrorEventHandler(string text);
public event MsgToGroupEventHandler MsgToGroupEvent;
public event MsgToPlayerEventHandler MsgToPlayerEvent;
public event GameOverEventHandler GameOverEvent;
public event PlayerLeftGameEventHandler PlayerLeftGameEvent;
public static event PostErrorEventHandler PostErrorEvent;
private const int NumPlayers = 2;
private Guid m_id;
// Consisting of 2 players min
private TG.Daemon.GameGroup m_group;
private int m_turn;
private int[][,] m_Placement = new int[2][,];
接下来是构造函数代码,用于处理新游戏开始的代码以及用于结束游戏的代码.请记住(Next there is the constructor code, code to handle the start of a new game, and code to end a game. Remember that) StartFirstGame
是从(is called from the) GameDaemon.GameGroupAdd
方法,而(method, while) EndGame
是从(is called from the) GameDaemon.SignalGameOver
方法.对于这个特定的游戏,我们在游戏开始时并没有做太多事情,当游戏结束时,我们可以清理游戏中使用的任何对象,并告诉每个玩家对方已经离开了游戏(这是正确的):(method. For this particular game we don’t do much when a game is starting, when a game ends we can clean up any objects the game uses, and tell each player that the other has left the game (which is true enough):)
public Game(Guid groupId, TG.Daemon.GameGroup group)
{
m_id = groupId;
m_group = group;
}
public void StartFirstGame()
{
StartNewGame();
}
public void StartNewGame()
{
// TODO: Add code here to start up game here
}
public void EndGame()
{
Hashtable msg = new Hashtable();
msg.Add("MsgType", "UserLeavingGame");
msg.Add("Player", 0);
MsgToPlayerEvent(m_id, 1, msg);
msg["Player"] = 1;
MsgToPlayerEvent(m_id, 0, msg);
}
我们要做的最后一件事是处理来自游戏客户端的实际消息.此代码将始终特定于您的游戏.如果您还记得,(The last thing we have to do is handle the actual messages coming in from our game clients. This code will always be specific to your game. If you recall, the) GameDaemon
对象添加其(object adds its) MsgReceive
方法(method to the) PlayerMsgEventHandler
代表.该方法将找出消息所属的特定游戏实例,然后调用(delegate. That method will figure out which particular game instance the message belongs to and call the) Game
对象的(object’s) MsgReceive
方法.对于海军战役,我们处理三种消息类型,(method. For Navy Battle we handle three message types,) InitialPlacement
,(,) Move
和(, and) UserLeavingGame
.(.)
的(The) InitialPlacement
该消息是该游戏所特有的,因为我们必须等待两个玩家放置自己的飞船后才能开始.当我们从两个玩家那里收到此消息时,我们会发送一个(message is unique to this game because we have to wait for both players to place their ships before we can start. When we receive this message from both players we send out an) Update
响应消息,由我们的游戏客户端处理.我们还保存了两个玩家的船只的位置:(message in response, which is handled by our game client. We also save the positions of both players' ships:)
public void MsgReceive(int playerNum, Hashtable msg)
{
if (msg.Contains("MsgType"))
{
string MsgType = (string)msg["MsgType"];
switch(MsgType)
{
case "InitialPlacement"
{
int player = (int)msg["Player"];
int[,] Placement = (int[,])msg["Placement"];
m_Placement[player] = Placement;
if (m_Placement[0] != null && m_Placement[1] != null)
{
// Send out the first update
Hashtable updateMessage = new Hashtable();
updateMessage.Add("MsgType", "Update");
updateMessage.Add("Winner", -1); // No winner yet
updateMessage.Add("Turn", 0);
// NOTE: No 'AttackCoordinates' or 'Hits' are added
// to this message since this is the first time.
MsgToGroupEvent(m_id, updateMessage);
}
}
// . . .
的(The) Move
通过确定是否有获胜者,保留销毁船只的数量并确定哪个玩家采取下一步行动来处理消息.然后,它将带有此信息的消息发送给两个玩家.尽管这是一条自定义消息,但由于TrayGames游戏是回合制游戏,因此所有游戏通常都具有类似这样的消息:(message gets handled by determining if there is a winner, keeping the count of destroyed ships, and determining which player gets to make the next move. It then sends out a message with this information to both the players. Although this is a custom message, it’s typical for all games to have a message that is something like this, since TrayGames games are turn based:)
case "Move":
{
// Retrieve information from message
int player = (int)msg["Player"];
int coordinates =
(int)msg["AttackCoordinates"]; //0..24
int x = coordinates % 5;
int y = coordinates / 5;
// The other player is the target
int target = (player + 1) % NumPlayers;
// Record the shot
m_Placement[target][x,y] |= 2;
// See if it was a hit
bool hit = false;
if ((m_Placement[target][x,y] & 1) > 0)
hit = true;
int winner = -1; // No winner
// Increment the turn
m_turn = (m_turn + 1) % NumPlayers;
// Check for all ships destroyed
if (hit)
{
// If the 'target' player has no ships left
// that haven't been hit then they lose.
// The Grid value will be 1 for an undamaged
// ship, 2 for a shot that missed, and
// 3 for a destroyed ship
int UndamagedShips = 0;
for(int i = 0; i < 5; i++)
{
for(int j = 0; j < 5; j++)
{
if (m_Placement[target][i,j] == 1)
UndamagedShips++;
}
}
// The game is over
if (UndamagedShips == 0)
{
winner = player;
}
}
Hashtable updateMessage = new Hashtable();
updateMessage.Add("MsgType", "Update");
updateMessage.Add("Winner", winner);
updateMessage.Add("Turn", m_turn);
updateMessage.Add("AttackCoordinates", coordinates);
updateMessage.Add("Hit", hit);
if (winner > -1)
{
// This is so we can expose the
// winning players ship locations
// to the losing player
// (kind of rubbing his nose in it)
updateMessage.Add("Placement", m_Placement[winner]);
}
// Broadcast update message to both players
MsgToGroupEvent(m_id, updateMessage);
break;
}
// . . .
处理(To handle the) UserLeavingGame
消息我们需要做的就是通知另一位玩家他们的对手已经离开了游戏,并结束游戏,因为两人游戏不能只靠一个玩家继续.尽管这是一条自定义消息,但所有游戏通常都会收到这样的消息:(message all we need to do is notify the other player that their opponent has left the game, and end the game since a two player game can’t continue with only one player. Although this is a custom message, it’s typical for all games to have such a message:)
case "UserLeavingGame":
{
int leavingPlayerNum = Convert.ToInt32(msg["Player"]);
int opponent = (leavingPlayerNum + 1) % NumPlayers;
MsgToPlayerEvent(m_id, opponent, msg);
TG.Daemon.GamePerson leavingPlayer =
(TG.Daemon.GamePerson)m_group.Players[leavingPlayerNum];
leavingPlayer.PlayerStatus =
TG.Daemon.GamePerson.UserStatus.Dropped;
int numPlayersInTheGame = 0;
foreach(TG.Daemon.GamePerson player in m_group.Players.Values)
{
if (player.PlayerStatus ==
TG.Daemon.GamePerson.UserStatus.Connected)
numPlayersInTheGame += 1;
}
// If num players left in game is less
// than min players, then game is over
PlayerLeftGameEvent(m_id,
(Guid)m_group.PlayerNumberToGuid[leavingPlayerNum]);
if (numPlayersInTheGame < 2)
GameOverEvent(m_id);
break;
}
}
}
}
这就是TGSDK服务器端的全部内容.在设计游戏时,使用客户端-服务器模型而不是对等模型的优点之一是它可以防止作弊,这可能是一大优势.大部分代码(That’s all there is to the server side of the TGSDK. One of the advantages of using a client-server model instead of a peer-to-peer model when designing your game is that it prevents cheating, that can be a big plus. Most of the code in the) GameDaemon
class是样板代码,您可以从这样的样本中获取并重复使用.如果您的游戏很简单,那么对于(class is boiler plate code that you can take from a sample like this and re-use. If your game is simple then the same can be said for the) Game
类.实际上,我从Tic-Tac-Toe示例的C#实现中复制了海军战斗的大多数守护程序代码.请记住,在运行客户端或服务器时,需要将TrayGames支持库与可执行文件放在同一文件夹中.(class. In fact I copied most of the daemon code for Navy Battle from the C# implementation of our Tic-Tac-Toe sample. Remember when you run your client or server you need to have the TrayGames support libraries in the same folder as your executable.)
兴趣点(Points of interest)
这是在TGSDK中运行简单的多人游戏所要做的全部工作.如您所见,API使客户端-服务器系统完全无缝,对于开发人员来说是不可见的.您无需使用TCP/IP,配对或托管就可以做任何事情.这里有许多我们无法利用的高级功能,例如对Managed DirectX的支持,强大的Skinning库以及用于声音的Ogg Vorbis文件播放器.如果您有兴趣查看完整的TGSDK来制作自己的多人在线游戏,可以在(That’s all there is to do to get a simple multi-player game working in the TGSDK. As you can see the APIs make the client-server system totally seamless and invisible to you as a developer. You don’t have to do anything at all with TCP/IP, matchmaking or hosting. There are many more advanced features that we are not taking advantage of here, such as support for Managed DirectX, a powerful Skinning Library, and an Ogg Vorbis file player for sounds. If you are interested in checking out the full TGSDK for producing your own multi-player online games, you can get it at the) 托盘游戏(TrayGames) 网站.(web site.)
致谢(Acknowledgments)
特别感谢游戏编程大师Paul Naylor,他为使这款游戏的外观和性能达到最佳状态提供了帮助.(A special thanks to Paul Naylor, a game programming guru, for his help in making this game look and work as good as it does.)
修订记录(Revision history)
- 11(11)日(th)2005年8月-初始修订.(August, 2005 - Initial revision.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# WinXP Windows .NET .NET1.1 Visual-Studio VS.NET2003 Dev 新闻 翻译