[翻译]Oreilly.Learning.XNA.3.0之eight
PS:自己翻译的,转载请著明出处
第十七章 多人游戏
在这本书的前几节,我们讨论创造一个真正的人工智能的难度。同时我们涉及到了轻微的人工智能,这个真正的复杂运算使用在最新的游戏中已经超出了这本书的范围了。你可能意识到了现在它有多复杂去真实的模拟一个人类玩家。这是如此复杂,在许多情况下,在某些游戏下,它几乎完全不可能。
可能你已经玩过一个RTS(即时战略)的游戏,这里面的电脑玩家离开它基于没有防备,或者一个模拟足球游戏,它里面一个防守打法可以停止每一次的进攻,不管什么样的攻击型打法被调用。这是其中一个原因为什么多人游戏是这么令人愉快的经验-真的是像对付一个真的人类对手。赢了这些游戏让人很高兴,多人游戏容易让人上瘾。
在本节中,我们将讨论不同的多人功能的实施方案在你的游戏中。首先,我们考虑先添加一个分割屏幕功能到你的游戏中,然后我们通过讨论创建一个新游戏使用XNA框架网络API。
Split-Screen Functionality(分割屏幕的功能)
一个方法去添加多人游戏功能到你的游戏中是执行一个分割屏幕在一个显示器上(PC机)或者电视设备上(xbox360)。分割屏幕是典型的一个支持一个,两个,三个,或者四个玩家同时在同一个机器上游戏。
当实施你的游戏的分屏,你需要考虑几个因素:
Input controls
通常,你想支持唯一的Xbox游戏平台的输入,因为你不想要两个,三个,四个人拥挤在一个键盘上。
Cameras and angle
你可能有一个独立的相机为每个玩家。你需要去考虑下相机的角度,它给每个玩家最好的视觉范围从他的或者她的观察点在游戏中。
Real estate
屏幕的大小将被挤压当你试图压挤多个视阈到一个屏幕中。如果你执行一个两个玩家的游戏,实现这个不同视阈的功能是左右分割还是上下分割呢?
当绘制一个屏幕在XNA中,这里有一个图形设备的属性称为Viewport,它还没有在这本书提起过。Viewport属性从本质上来讲一个矩形它代表屏幕的坐标到它的图形设备,将影射它的屏幕当绘制的时候。默认的,Viewport被设置到客户窗口的大小,它导致图形设备去绘制整个游戏窗口。
拆分屏幕通过修改图形设备的Viewport被执行,然后多次绘制一个特殊的屏幕(给每个玩家),使用相机为玩家作为游戏窗口。
这听起来象有很多信息,但是不要让它吓唬你。看下图17-1,这是两个玩家分屏的窗口。
为了绘制一个屏幕以一个垂直划分的两个玩家游戏窗口,如图17-1所示,你最好创建一个viewport为每个玩家,它可能包含屏幕坐标代表为这些玩家绘制的区域。
在你的Draw方法中,你首先想要调用GraphicsDevice.Clear为整个屏幕。这将清除当中的缓冲区域。这个颜色,你指定的在Clear方法中,将会在两个拆分屏幕的边界的颜色。
注意:为什么要清除整个屏幕而去清除缓冲区域呢?清除整个后备缓冲是非常快的和最优化的操作为GPU。它同样重置别的状态它使绘制场景非常快。
接下来,你最好设置GraphicsDevice.Viewport属性到玩家1的viewport,并且绘制这个场景从玩家1的相机的观察点。然后你最好对玩家2进行同样的操作。
那么,你如何绘制这个场景从玩家1的相机的观察角度呢?你最有可能让每个玩家有一个不同相机(毕竟,如果你绘制准备的相同的事情在每个玩家的屏幕部分用什么去分这个屏幕呢?)记住,一个相机有两个矩阵分别代表view和projection。这些矩阵被传递到你的BasicEffect或者你的HLSL效果当你绘制时。为了绘制使用玩家1的相机,你传入到矩阵代表这个相机。为了从一个不同的相机的观察点去绘制,你只要传入矩阵到相应的相机。
这是基本的想法,现在,让我们来实现两个玩家拆分屏幕吧。为了这节,你将使用这个代码,你创建在第10章的。如果你没有这段代码或者你略过了第10章,你可以下载这个代码从这本书的资源代码中。
打开第10章的代码,并且你会看见,在这个工程中,你执行了一个相机的组成部分,作为你已经作了为所有的3D例子在这本书中。由于所有的玩家将有他们的自己的相机和他们自己的Viewports,并且由于一个viewport代表一个相机可以看见的在3D到2D游戏窗口上的一个矩形,它的意义是添加这个viewport到Camera类。
打开相机类,并且添加下面的变量:
你的当前的结构应该看起来象这样;
2 {
3 view = Matrix.CreateLookAt(pos, target, up);
4 projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,(float)Game.Window.ClientBounds.Width /(float)Game.Window.ClientBounds.Height,1, 3000);
5 }
2 {
3 view = Matrix.CreateLookAt(pos, target, up);
4 projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,(float)viewport.Width /(float)viewport.Height,1, 3000);this.viewport = viewport;
5 }
2 public Camera camera2 { get; protected set; }
2 camera = new Camera(this, new Vector3(0, 0, 50),Vector3.Zero, Vector3.Up);
3 Components.Add(camera);
2 int offset = 1;
3 Viewport vp1 = GraphicsDevice.Viewport;
4 Viewport vp2 = GraphicsDevice.Viewport;
5 vp1.Height = (GraphicsDevice.Viewport.Height / 2) - offset;
6 vp2.Y = vp1.Height + (offset * 2);
7 vp2.Height = vp1.Height;
8 // Add camera components
9 camera1 = new Camera(this, new Vector3(0, 0, 50),Vector3.Zero, Vector3.Up, vp1);
10 Components.Add(camera1);
11 camera2 = new Camera(this, new Vector3(0, 0, -50),Vector3.Zero, Vector3.Up, vp2);
12 Components.Add(camera2);
vp2的viewport的X和Width属性同样是和这些游戏窗口相同的,所以你只需要改变Y的属性,使viewport上边的viewport正好低于vp1的viewport的底部一点,并且改变viewport的高度与vp1的viewport的高度相同。
然后你创建每个相机,并且传到相应的viewports到结构中。
接下来,你需要修改绘制场景的代码。你在两个不同的地方绘制:在Game1类,和在ModelManager类中。好,在技术上你不用绘制在ModelManager类中,但是你调用Draw在每个模型上在你的模型表单中,从它里面传入一个相机对象去绘制,在你的ModelManager类中使用下面的代码去实现它:
2 {
3 // Loop through and draw each model
4 foreach (BasicModel bm in models)
5 {
6 bm.Draw(((Game1)Game).camera);
7 }
8 base.Draw(gameTime);
9 }
2 {
3 GraphicsDevice.Clear(Color.CornflowerBlue);
4 // TODO: Add your drawing code here
5 base.Draw(gameTime);
6 }
2 {
3 // Clear border between screens
4 GraphicsDevice.Clear(Color.Black);
5 // Set current drawing camera for player 1
6 // and set the viewport to player 1's viewport,
7 // then clear and call base.Draw to invoke
8 // the Draw method on the ModelManager component
9 currentDrawingCamera = camera1;
10 GraphicsDevice.Viewport = camera1.viewport;
11 GraphicsDevice.Clear(Color.CornflowerBlue);
12 base.Draw(gameTime);
13 // Set current drawing camera for player 2
14 // and set the viewport to player 2's viewport,
15 //then clear and call base.Draw to invoke
16 // the Draw method on the ModelManager component
17 currentDrawingCamera = camera2;
18 GraphicsDevice.Viewport = camera2.viewport;
19 GraphicsDevice.Clear(Color.CornflowerBlue);
20 base.Draw(gameTime);
21 }
编译运行游戏在这时,你应该可以看见两个飞船在两个不同的viewports,如图17-2所示。
重要的是要注意,你没有真正看到两只不同的飞船。你只绘制了一个模型,所以你实际上看到同样的飞船从两个不同的视角。当你创建你的两个相机时,你放置一个相机在(0,0,50)朝着初始方向(那里是飞船绘制的地方),另外一个相机在(0,0,-50),同样看这初始的地方。这就解释了为什么一个viewport显示飞船面向右,另一个显示它面向左----两个都是同样的飞船,就是方向反了。
在这个游戏中有一个问题:它不能做任何事情。因为它令人兴奋的是它是盯住一个飞船,你可能应该给玩家提供更多一点。我们不会开发这个例子成一个真实的游戏,但它会帮助你看到两个不同的相机独立的移动在这个例子中。现在,你绘制了一个飞船在初始的位置,并看着它从相机的一边,并且从另一个相机相反的一边。每个相机被用来绘制飞船在viewport,viewport只有游戏窗口的一半,它的拆分屏幕的效果如图17-2所示。
因为你要使两个相机在这个例子中移动,你应该首先使这个飞船停止自转。这将使它很容易看到每个相机都发生了什么,当你在3D空间中移动它。为了停止飞船自转,使用BasicModel类代替SpinningEnemy类为你创建的飞船。
在你的ModelManager类的LoadContent方法中,改变下面这行,去使用BasicModel创建飞船,如下:
2 Vector3 cameraPosition;
3 Vector3 cameraDirection;
4 Vector3 cameraUp;
5 // Speed
6 float speed = 3;
最终的变量将会用来检测相机移动的速度。
接下来,你需要去添加下面的方法到你的Camera类去考虑重建这个view矩阵:
2 {
3 view = Matrix.CreateLookAt(cameraPosition,cameraPosition + cameraDirection, cameraUp);
4 }
2 cameraPosition = pos;
3 cameraDirection = target - pos;
4 cameraDirection.Normalize( );
5 cameraUp = up;
6 CreateLookAt( );
每一次Update方法被调用,你的相机现在需要去重建view矩阵,添加下面的代码行到Camera类的Update方法中:
2 {
3 // Move forward/backward
4 if (forward)
5 cameraPosition += cameraDirection * speed;
6 else
7 cameraPosition -= cameraDirection * speed;
8 }
9 public void MoveStrafeLeftRight(bool left)
10 {
11 // Strafe
12 if (left)
13 {
14 cameraPosition +=Vector3.Cross(cameraUp, cameraDirection) * speed;
15 }
16 else
17 {
18 cameraPosition -=Vector3.Cross(cameraUp, cameraDirection) * speed;
19 }
20 }
2 KeyboardState keyboardState = Keyboard.GetState( );
3 // Move camera1 with WASD keys
4 if (keyboardState.IsKeyDown(Keys.W))
5 camera1.MoveForwardBackward(true);
6 if (keyboardState.IsKeyDown(Keys.S))
7 camera1.MoveForwardBackward(false);
8 if (keyboardState.IsKeyDown(Keys.A))
9 camera1.MoveStrafeLeftRight(true);
10 if (keyboardState.IsKeyDown(Keys.D))
11 camera1.MoveStrafeLeftRight(false);
12 // Move camera2 with IJKL keys
13 if (keyboardState.IsKeyDown(Keys.I))
14 camera2.MoveForwardBackward(true);
15 if (keyboardState.IsKeyDown(Keys.K))
16 camera2.MoveForwardBackward(false);
17 if (keyboardState.IsKeyDown(Keys.J))
18 camera2.MoveStrafeLeftRight(true);
19 if (keyboardState.IsKeyDown(Keys.L))
20 camera2.MoveStrafeLeftRight(false);
这个代码允许你移动上部的view(相机1)用WASD键,并且移动下面的view(相机2)用IJKL键。编译运行游戏在这时,你会看见两个相机互不依赖的移动。如果你想添加旋转到你的相机,你可以做同样的通过使用这个相机旋转代码在前面的章节我们讨论过的代码,执行它在某种程度上,它类似于你刚刚添加的代码移动每个相机。
Where’s the Camera?(相机在哪?)
当你围绕这一个相机移动,你可能会感觉到另外一个相机在哪,但你不能看见他,为什么呢?
你不能看见相机2坐落在3D空间,当移动相机1,因为一个相机不是一个对象不能被绘制。也就是说,在这个例子中你可以移动每个相机朝着某点,这点正好是另一个相机的位置,但是你不能看见任何东西在哪,是因为相机是不可见的对象在游戏中。
比方说,你想使这个游戏成为空间射手,每个玩家驾驶一只飞船在3D空间,互相射击。为了执行这个,你需要接收你当前所有的代码,为每个相机,绘制一只飞船在相机的位置,旋转面对这个方向,相机互相面对。只有这样你将会看见一些事情在这个游戏本身,它代表了另一个玩家。
这一切就这么简单!您可以轻松地添加分割画面的功能。为了添加支持三个玩家,使用三个viewports和三个相机。为了添加支持四个玩家,使用四个viewports和四只相机。
根据你游戏的细节,你同样可以需要添加功能去独立的移动每个相机,以及执行其他动作去与独立的世界互动,基于从玩家分配给相机的输入(译者:玩家操作相机)。
Network Game Development(网络游戏的开发)
网络已经是一个很热很久的话题,在微软的图形API系。直到DirectX的出现和DirctPlay库的产生,有无数反复使用,达到不同层度的成功。然而,DirectPlay被创造在TCP/IP成为今天的标准之前,所以它最终被废弃。代替了DirectPlay,DirectX开发者被告知Windows scorkets(套接字)库将成为首选的开发工具,为游戏的开发使用网络游戏功能。
XNA1.0同样不支持网络的API在System.net外面,并且不支持网络对战在Xbox360上。原因是什么?一个新的完整的网络API是XNA1.0开发者的更多的要求。就因为这个,XNA游戏Studio2.0开始,微软运行开发者去使用Live 为WindowsAPIs 在Windows和Xbox360上。
根据Shawn Hargreaves的介绍(微软的XNA游戏开发团队的工程师)在2008游戏开发者大会上,XNA团队的设计目标包括:
1。使多人游戏成为可能。
2。很容易使用API。
3。使API为用户处理低级别的网络信息。
4。支持Xbox LIVE和Windows LIVE游戏。
5。允许用一个Xbox360和PC开发。
6。不需要专用的服务器。
最好的事情关于XNA网络API是它是如何被简单使用。如果你从来没有处理过网络代码用别的语言或者类库,你最有可能发现XNA在易用性方面实施的清新升级。
XNA使用Xbox LIVE和Windows LIVE平台的多人游戏。你也许有点熟悉Xbox LIVE是如何工作的,但是你可能不熟悉对Windows LIVE游戏。从本质上讲,Windows LIVE游戏带有Windows 游戏的玩家代号和在线唯一身份,XboxLIVE也是同样的方式。事实上,他们使用相同的在线游戏玩家代号和唯一身份。在本章的后面将会看到,为Windows Live平台的游戏,甚至还使用了一系列屏幕,非常类似于Xbox 360显示板和其他登录帐户维修活动。
XNA创建者俱乐部的表单和LIVE成员要求不同的游戏类型在PC和Xbox360如表17-1所示。
令人惊讶的是,你为一个PC游戏写的大部分代码,为Windows LIVE使用游戏程序将会与Xbox360和Zune是一致的。网络API将工作在这些平台上,虽然这里只有很少的资料,不用担心Zune(不支持游戏玩家代号)。
Network Configurations(网络的构造)
最重要的一件事是考虑什么时候写一个网络游戏,你将使用的是什么样类型的网络(点对点,客/服,或者混合的)。你选择的网络类型将有一个大的影响在你如何处理你的网络流量,和你的程序性能。
一个点对点的网络,所有的参与者都是客户端。当某些改变在一个电脑上,这个电脑发送一个信息到所有的另他电脑上告诉他们,发生了什么。对空间射击游戏而言,假设你正在玩一个游戏和五个参与者(玩家)。如果其中一个玩家的电脑射出一个子弹,这台电脑发送一个信息到所有其他电脑告诉他们,一个子弹已经被发射。一典型的点对点体系结构图表如17-3所示。
和一个点对点的网络形成对比,一个客户/服务器网络结构代表性的有一个服务器,剩下的机制是客户端。所有的通讯是通过服务器运行的。如果你采取了前面五个人玩一个空间射击游戏,一个玩家发射一个子弹,在一个客户/服务器网络,这台电脑将会发送一个信息到这个服务器(除非这台电脑是服务器),然后服务器将会发送信息到别的客户端。
一个典型的客户机/服务器配置如图17-4。
你可以想下一个客户/服务器结构是有一点瓶颈的,因为所有的通讯都跑回到一台机器上。在某种情况下,它可能。然而,在所有箭头(代表网络信息)在点对点网络图表17-3中。想象一下这个网络被应用到一个想魔兽世界这样的游戏中,它里面有成千上百的玩家正在同时的玩这个游戏。随着信息返回并且在每个单一电脑之间前进,你可以看看如何通讯的,处理信息就很快失去控制。
这并不是说,一个点对点的网络不是一个好想法。在客户/服务器模式,如果这个服务器被破坏,这个游戏结束。在点对点网络。这算不上问题,"主机"的游戏可能很容易的在一个电脑到另一个之间传送。最好的网络结构实际上依赖于在一个游戏中你要跟踪多少个信息,并且多少个玩家将会同时参加这个游戏。
Writing an XNA Network Game(写一个XNA网络游戏)
整个本章的剩余部分,我们将会创建一个游戏,他使用XNA网络APIs去使多人功能可以使用。同样的代码可以被应用到Xbox360系统连接网络功能。
在本节中,你将开始一个新的项目,但是你将会使用一些代码和资源从这个你在本书第7章完成工程中。如果你没有第7章的代码,它可以被下载从本书中。
注意:我写的这章不是简单的介绍了网络API,而是选择在一个网络游戏上展示API。虽然,由于这决定,本章会有一个大量的代码。如果你对编写这么多的代码感觉到厌倦,那么就下载这个资源代码从本章中,并通过它阅读本章。它可以帮你节省时间避免头疼。
本章假定你已经读了这本书,并且熟悉Visual studio2008和XNA Game Studio 3.0.如果你找到你自己不明白这些原理在本章中,请你复习本书的前面的章节。
另外,由于本书的其他所有的游戏使用了XACT为声音处理,我假定现在你有一个好的感觉对于XACT并且知道它如何工作的。因此,本章将使用被XNA框架3.0支持的简单的声音API代替执行的声音。如果你正在学习更多关于XACT,请你参阅本书的其他例子。
开始吧,开始创建一个新的XNA 3.0Windows游戏功能在Visual Studio。命名工程为Catch。
你需要添加两个文件到你的工程中从本书的第7章的资源代码。右击在你的解决方案中的项目,选择AddExisting Item...,并且浏览到第7章的资源代码,选择下面的文件添加到你的项目中:
1。Sprite.cs
2。UserControlledSprite.cs
你创建一个2D网络游戏,在这个里面一个玩家围绕屏幕追赶另外一个玩家,就为了和别的玩家碰撞。这个玩家被追赶,停留的时间越长就将获得更多的得分。你将会修改你的现有的精灵类去处理精灵对象在多人网络游戏中。
Modifying the Sprite Class(修改精灵类)
你首先需要做的在这个Sprite类中是从AnimateSprites到Catch改变类的命名空间:
2 public Point sheetSize { get; set; }
3 public Vector2 speed { get; set; }
4 public Vector2 originalSpeed { get; set; }
2 {
3 get { return position; }
4 set { position = value; }
5 }
接下来,让我们一起改变UserControlledSprite类。首先,改变命名空间从AnimatedSprite到Catch:
2 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
3 Vector2 speed, bool isChasing)
4 : base(textureImage, position, frameSize, collisionOffset, currentFrame,
5 sheetSize, speed, null, 0)
6 {
7 score = 0;
8 this.isChasing = isChasing;
9 }
10 public UserControlledSprite(Texture2D textureImage, Vector2 position,
11 Point frameSize, int collisionOffset, Point currentFrame, Point sheetSize,
12 Vector2 speed, int millisecondsPerFrame, bool isChasing)
13 : base(textureImage, position, frameSize, collisionOffset, currentFrame,
14 sheetSize, speed, millisecondsPerFrame, null, 0)
15 {
16 score = 0;
17 this.isChasing = isChasing;
18 }
修改的Update方法应该象这个样子:
2 {
3 if (moveSprite)
4 {
5 // Move the sprite according to the direction property
6 position += direction;
7 // If the sprite is off the screen, put it back in play
8 if (position.X < 0)
9 position.X = 0;
10 if (position.Y < 0)
11 position.Y = 0;
12 if (position.X > clientBounds.Width - frameSize.X)
13 position.X = clientBounds.Width - frameSize.X;
14 if (position.Y > clientBounds.Height - frameSize.Y)
15 position.Y = clientBounds.Height - frameSize.Y;
16 }
17 base.Update(gameTime, clientBounds);
18 }
这是一个好时机去谈论下网络数据。传递的数据通过一个网络是在性能方面的瓶颈。虽然在网络上表现的非常快,根本无法跟上你的电脑或内部速度Xbox 360游戏机。正因为如此,你要限制的数据,它被围绕网络传递。
在这个游戏中,您将实现一个点对点的网络,意思是每个PC将发送数据到另外一台PC上,让它知道发生了什么在这个游戏例子中。这是一个好的例子,当一个玩家移动一个精灵在这个游戏中。假设你有两个电脑玩一个游戏。一个玩家正追赶另外一个玩家围绕屏幕。如果正追赶的玩家通过压下在键盘上的一个键或者压下手柄的某个键,另外的一个电脑如何知道他被移动了呢?答案是,它不知道。
这是返回的信息。当正追赶的玩家移动向左,需要更新玩家的位置,然后通知另一台电脑上的游戏,这个玩家已经向左移动了。一个方法可以实现这个是发送正个UserControlledSprite对象从正追赶的玩家的电脑到另外一个玩家的电脑。另外的电脑关闭网络,并使用它作为正追赶的玩家在这个游戏的实例上。
然而,UserControlledSprite也许有所有的数据,这个数据别的电脑也可能有,它同样有大量的别的数据(纹理,祯大小,规格,缩放,和其他的信息)。别的电脑已经有所有这些信息,并且不需要再一次被给予。更多的有效的方式是发送给其他的电脑一个信息,它包含唯一个信息,它被改变了(在这中情况下,玩家的位置)。正接收的电脑能取出正追赶的玩家的位置从网络中,并使用它作为正追赶玩家的新的位置在这个游戏实例中。这样,正在追赶的玩家将围绕屏幕移动在被追赶者玩家的电脑上,即使正追赶的玩家正在一个不同的电脑上玩耍。
复杂的是除了更新正追赶玩家的位置之外,被追赶的玩家的电脑同样需要去移动。另一种方法是,你可以实现这个去传递的不仅仅是精灵的位置到别的电脑,而是当前精灵的祯。但是为什么你又不想这样做呢?
有两个原因:它将通过网络包括传送更多的数据,并且不会成功。任何人会注意如果正追赶玩家的精灵是一祯或者两祯在它的第二个电脑的动画顺序之后?不在这个游戏里。在另外的游戏中,它是可能的,但是在这个游戏中你有一个单一的,连续的动画为每一个精灵,并且没有人将会注意它稍有点不同步。因此,它不值得通过网络发送额外的数据。
相反,你需要一个方法去更新UserControlledSprite的位置,代表其他的玩家,然后更新玩家的动画不用基于用户的输入移动它--因此,你添加的这个参数,它将导致Update方法去更新动画单独的祯。
Coding Your Game1 Class(编写你的Game1类)
在你的Game1类中第一件事情你需要去做的是添加一个enum,你使用它去代表游戏的状态。在前面的章节中我们讨论过游戏的状态,但是他们没有网络游戏重要。除了在游戏中的典型的状态(一个开始的状态,你显示游戏的说明,一个in-game状态,和end-game状态),在一个网络游戏中,你通常也有一个sign-in状态,玩家进入了Xbox LIVE或者Windows LIVE游戏中。
你实际上想在Game1类的外面添加下面的enum,在声明的Catch命名空间之间。这将允许你稍后添加任何的其他类,更容易的访问游戏的状态:
2 {
3 // Represents different states of the game
4 public enum GameState { SignIn, FindSession,CreateSession, Start, InGame, GameOver }
5 public class Game1 : Microsoft.Xna.Framework.Game
6 {
7
为了解决这个问题,你可以发送一个值在每次的消息的开始部分。它告诉接收的电脑,这个消息的类型的数据来了。在这种情况下,你将发送数据告诉别的电脑,要么启动游戏,要么结束游戏,要么重起游戏,重新进入游戏大厅,或者更新玩家的位置。这样,立即添加下面的enmu在GameState enum的后面:
2 public enum MessageType { StartGame, EndGame, RestartGame,RejoinLobby, UpdatePlayerPos }
2 SpriteFont scoreFont;
3 // Current game state
4 GameState currentGameState = GameState.SignIn;
5 // Audio variables
6 SoundEffectInstance trackInstance;
7 // Sprite speeds
8 Vector2 chasingSpeed = new Vector2(4, 4);
9 Vector2 chasedSpeed = new Vector2(6, 6);
10 // Network stuff
11 NetworkSession networkSession;
12 PacketWriter packetWriter = new PacketWriter( );
13 PacketReader packetReader = new PacketReader( );
其中大部分对你来说应该熟悉。你将使用scoreFont变量去绘制文本在屏幕上。currentGameState变量保留一个值来自GameState enum代表当前的游戏状态。这个trackInstance变量保留音频声音的实例,所以你可以停止它,当游戏结束时,两个Vector2变量保留数据代表每个精灵的速度(正追赶的精灵将比被追赶的精灵移动的稍微缓慢些).
三个新变量,你从来没有见过,在这段代码的结尾被列出来:networkSession,packerWritere,和packetReader.
在XNA中任何网络游戏的主干是NetworkSession类。这个类代表你游戏的唯一的多人会话(session)。通过这个类你可以访问所有的会话成员(通过AllGamer属性,它是Gamer对象的集合),游戏的主机(通过Host元素,它是一个NetworkGamer对象),和其他相关多人的会话的属性。
另外两个被使用的变量发送数据通过这个网络到另外的电脑上。PacketWrite写数据包到网络上,并且PacketReader读取信息包从网络上。
包
什么是数据包?我们谈论这些MSG包,我使用它在我的拉面里?
不完全是。网络和包真的是远远超过了本书的范围----我会略微谈到一些高级别的网络构造术语,但是我不会去尝试挖掘到的数据包和较低级别的网络通信。如果你有兴趣在这些方面,这有大量的资源,你可以去学习。
对于这本书的目的,只知道,当你发送数据到另外的电脑上,你发送它用一些称做"包"。你的包可以包含大量数据的变量(一个包可以包含的是int,同时另外可以包含一个string,int,两个Vector2s,和五个floats)
你写数据到一个包,然后再发送它,当你读时,你读一个包,然后分解这个包去提取你需要的数据。
想象一下,你储存的拉面的口味包(译者:作者很搞笑),你与你的朋友通过这些包通讯(这是被强烈推荐的)。你写一个便条("HI,Brant!")并且保存它在这个包中,丢给你的朋友。你在等一会,会获得另一个返回的包。打开它读它,"HI,Aaron.你好吗?" 然后你在写另外一个便条("好,你呢?对不起今天把你的壁球搞坏了"),把它放入另外一个包中,丢给另一端的你的朋友。
这是一个伟大的通信方式,而是一种良好味MSG风味包....(译者:不管我的事,作者要这么写)
接下来的事情,你需要去添加下面的代码到你的Game1类的Initialize方法中,正好在base.Initialize调用之前:
注意:如果你的游戏的服务组件,你运行你的游戏在任何PC上,要全部安装完整的XNA Game Studio---基本的XNA不支持游戏的服务器。
接下来,添加一个新的文件夹在你的解决方案中,在Content节点的下面,并命名这个文件夹为Fonts.添加一个新的叫做Arial.spritefont的spritefont文件到这个文件夹里。然后,加载字体在这个Game1类的LoadContent中:
Adding Update Code(添加更新代码)
现在,您需要修改您的Game1类的Update方法去调用一个基于当前游戏状态的不同的方法(稍后你会添加这些代码):
2 {
3 if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
4 this.Exit( );
5 // Only run the Update code if the game is currently active.
6 // This prevents the game from progressing while
7 // gamer services windows are open.
8 if (this.IsActive)
9 {
10 // Run different methods based on game state
11 switch (currentGameState)
12 {
13 case GameState.SignIn:
14 Update_SignIn( );
15 break;
16 case GameState.FindSession:
17 Update_FindSession( );
18 break;
19 case GameState.CreateSession:
20 Update_CreateSession( );
21 break;
22 case GameState.Start:
23 Update_Start(gameTime);
24 break;
25 case GameState.InGame:
26 Update_InGame(gameTime);
27 break;
28 case GameState.GameOver:
29 Update_GameOver(gameTime);
30 break;
31 }
32 }
33 // Update the network session and pump network messages
34 if (networkSession != null)
35 networkSession.Update( );
36 base.Update(gameTime);
37 }
其次,接近这个方法的结束,是调用Update在NetworkSession对象上。正如前面所提到的,NetworkSession处理所有的会话信息,玩家的信息,和一些相关当前游戏的会话,你不得不调用Update在对象上,为了得到更新会话并且抽出网络信息从这些会话中。如果你不能调用Update在NetworkSession对象,你不可能接收信息的发送从别的玩家。
Updating While in the SignIn Game State
接下来,添加Update_SignIn方法到Game1类中:
2 {
3 // If no local gamers are signed in, show sign-in screen
4 if (Gamer.SignedInGamers.Count < 1)
5 {
6 Guide.ShowSignIn(1, false);
7 }
8 else
9 {
10 // Local gamer signed in, move to find sessions
11 currentGameState = GameState.FindSession;
12 }
13 }
如果你以前从来没有用一个帐户登陆这个电脑,这个游戏服务登陆器窗口将看起来象图17-5那样。
如果你之前登入了这台电脑,你的游戏窗口将会看起来象图17-6
一旦一个游戏已经登陆,这个游戏状态向前移动到FindSession状态。
Updating While in the FindSession Game State
接下来的事情你需要去做的是添加Update_FindSession方法:
2 {
3 // Find sesssions of the current game
4 AvailableNetworkSessionCollection sessions =NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
5 if (sessions.Count == 0)
6 {
7 // If no sessions exist, move to the CreateSession game state
8 currentGameState = GameState.CreateSession;
9 }
10 else
11 {
12 // If a session does exist, join it, wire up events,
13 // and move to the Start game state
14 networkSession = NetworkSession.Join(sessions[0]);
15 WireUpEvents( );
16 currentGameState = GameState.Start;
17 }
18 }
这个方法将会寻找一个正在流动的当前游戏的会话,通过使用NetworkSession.Find方法。因为你使用NetworkSessionTypeSystemLink去创建这个游戏,电脑创建会话并且电脑收寻会话,必须在同样的分子网络里为了找到彼此。你同样指定一些特殊的标准为了找到另外的会话通过传入参数到Find方法中:你正在寻找游戏,它使用SystemLink并且,它只允许一个本地玩家。
如果没有会话被找到,这个游戏状态被改变成CreateSession状态,这里一个新的会话被创建。如果一个会话被发现,游戏加入到那个会话中。然后你wire up一些游戏的事件使用这个WireUpEvent方法,稍后你就会用到了。最终,这个游戏状态然后移动到Start状态。
现在,添加WireUpEvents方法并且事件处理方法,如下:
2 {
3 // Wire up events for gamers joining and leaving
4 networkSession.GamerJoined += GamerJoined;
5 networkSession.GamerLeft += GamerLeft;
6 }
7 void GamerJoined(object sender, GamerJoinedEventArgs e)
8 {
9 // Gamer joined. Set the tag for the gamer to a new UserControlledSprite.
10 // If the gamer is the host, create a chaser; if not, create a chased.
11 if (e.Gamer.IsHost)
12 {
13 e.Gamer.Tag = CreateChasingSprite( );
14 }
15 else
16 {
17 e.Gamer.Tag = CreateChasedSprite( );
18 }
19 }
当玩家加入游戏,GamerJoined方法将会被调用。这个方法将指定一个属性称为Tag为这个玩家到一个新的UserControlledSprite.这个Tag属性是一个对象类型,它的意思是,你可以使用它去保存实质上的任何东西。你通常使用它去保存数据,代表一个特定的玩家在游戏中----在这种情况下,一个UserControlledSprite。
重要的是要注意到NetworkGamer对象的Tag属性将不会被发送通过网络。你不用使用这个属性去同步你的对象,但是,你可以使用这个对象去跟踪每个本地的玩家在每个游戏的实例中。这里你将会做的是保存一个UserControlledSprite在NetworkGamer对象的Tag属性给每个玩家。如果一个玩家移动了,那个玩家的电脑将发送一个消息给其他玩家,告诉其他电脑这个玩家的新位置。那台电脑然后指派UserControlledSprite对象(保存在NetworkGamer.Tag属性中)的位置属性给这个玩家,并且使用这个NetworkGamer.Tag属性(它是一个UserControlledSprite)去绘制相对的玩家。
如果这样做没有什么意义,那好。在本章后面剩下的代码,希望它是更清楚的,让我们继续。
NetworkGamer.Tag属性被设置依赖于谁是主机谁是参与的玩家,通过使用其中一个方法来实现:
2 {
3 // Create a new chased sprite
4 // using the gears sprite sheet
5 return new UserControlledSprite(Content.Load<Texture2D>(@"Images/gears"),new Vector2((Window.ClientBounds.Width / 2) + 150,(Window.ClientBounds.Height / 2) + 150),new Point(100, 100), 10, new Point(0, 0),new Point(6, 8), chasedSpeed, false);
6 }
7 private UserControlledSprite CreateChasingSprite( )
8 {
9 // Create a new chasing sprite
10 // using the dynamite sprite sheet
11 return new UserControlledSprite(Content.Load<Texture2D>(@"Images/dynamite"),new Vector2((Window.ClientBounds.Width / 2) - 150,(Window.ClientBounds.Height / 2) - 150),new Point(100, 100), 10, new Point(0, 0),new Point(6, 8), chasingSpeed, true);
12 }
你需要添加这些图象到你的项目中在你继续学习之前。这个图象和本章的资源代码都在Catch\Content\Images文件夹里,添加一个新的文件夹在Content节点只下,命名为Images,并且添加这个dynamite.png和gear.png文件从资源代码到你的项目中的这个文件夹中。
最后,如果玩家离开,你想要检查看看是否是本地玩家。如果是,处理会话并且改变游戏的状态到FindSession状态:
2 {
3 // Dispose of the network session, set it to null.
4 // Stop the soundtrack and go
5 // back to searching for sessions.
6 networkSession.Dispose( );
7 networkSession = null;
8 trackInstance.Stop( );
9 currentGameState = GameState.FindSession;
10 }
Updating While in the CreateSession GameState
接下来,添加Update_CreateSession方法:
2 {
3 // Create a new session using SystemLink with a max of 1 local player
4 // and a max of 2 total players
5 networkSession = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 2);
6 networkSession.AllowHostMigration = true;
7 networkSession.AllowJoinInProgress = false;
8 // Wire up events and move to the Start game state
9 WireUpEvents( );
10 currentGameState = GameState.Start;
11 }
这个方法创建一个新的会话使用NetworkSession.Create方法。这个参数是session(会话)类型(在这种情况下,SystemLink),最大的本地玩家(每台电脑允许的一个玩家),最大的玩家总数(每个会话允许两个玩家)。
它被创建之后,这个会话被设置去允许主机迁移(意思是如果主机掉线,另外一个玩家成为主机),并不允许玩家加入当游戏正在进行时。
然后,同样的事件wired up,你使用它为了加如一个会话,并且游戏的状态被设置成Start.
Updating While in the Start Game State(游戏开始时同时更新)
现在,你想要去添加这个逻辑,当Update被调用时它会运行,并且游戏在Start的游戏状态:
2 {
3 // Get local gamer
4 LocalNetworkGamer localGamer = networkSession.LocalGamers[0];
5 // Check for game start key or button press
6 // only if there are two players
7 if (networkSession.AllGamers.Count == 2)
8 {
9 // If space bar or Start button is pressed, begin the game
10 if (Keyboard.GetState( ).IsKeyDown(Keys.Space) ||GamePad.GetState(PlayerIndex.One).Buttons.Start ==ButtonState.Pressed)
11 {
12 // Send message to other player that we're starting
13 packetWriter.Write((int)MessageType.StartGame);
14 localGamer.SendData(packetWriter, SendDataOptions.Reliable);
15 // Call StartGame
16 StartGame( );
17 }
18 }
19 // Process any incoming packets
20 ProcessIncomingData(gameTime);
21 }
这个方法的主要目标是检测游戏是否开始了。当你在Start的游戏状态中绘制,你将会绘制一些文本告诉玩家去等待其他玩家(如果这里只是一个玩家在这个会话中)或者单击空格键或者开始案键去开始游戏(如果有两个玩家在这个会话中)。
这里有两中方法使这个游戏可以开始,为游戏的每个实例:
1。本地玩家可以按下空格键或者Start按键--用这种方法,你可以添加代码去开始这个游戏。
2。其他玩家(在其他的电脑上)可以开始这个游戏,在任何一种情况下,你会收到一个网络信息告诉你,另外的玩家已经开始这个游戏,并且现在,你应该开始这个游戏(在这种情况下,本地玩家不需要按下空格键或者Start按键去开始,因为被的玩家已经这样做了)。
When the local player starts the game(当本地玩家开始了这个游戏)
第一关的场景,你正在找空格键或者Start按键压下在Update_Start方法,不仅仅是当这关有两个玩家在这个关卡场景中。如果本地的玩家开始这游戏,你发送一个信息给其他电脑通过写数据到packetWriter对象使用Write的方法。正如前面本章所讨论的,你总是用一个MessageType enum值开始你的包(在这种情况下,MessageType.StartGame).这将告诉游戏实例,它读了这个包,这个包是一个start-game信息。为了一个Start-game信息,没有其它的数据被需要,所以这就是在指定的包中全部所写的。
然后该包使用本地玩家对象的SendData方法发送。用这种方法,你传递packetWriter和指定一些SendDataOptions.发送的操作包括:
None
包的传递不能保证,并不能保证数据包被传递用任何特定的顺序(一些包在其它之后发送的,也许在其他的之前到)。
InOrde
包的传递不能被保证,但是其他的是被保证的(数据包被交付将不会交付次序颠倒)。
Reliable
包被保证去交付,但是在没有指定顺序。因为更多的工作被做是保证包的交付,这是比None和InOrder更缓慢。
ReliableInOrder
包被保证交付,保证用正确的顺序(这是一种很慢的方法去发送包,并且被保守的使用)。
注意:为什么我们使用SendDataOptions.Reliable在前面的代码中,当它是一个很慢的操作呢?这都是些重要的信息---它们必须到达。它是一件事错过一个包,这个包更新精灵的位置。下一个包同样包含精灵的位置,所以它不会是一个大问题。错一个命令告诉这个游戏去结束或者开始或者从一个状态到另外一个,但是,这是一个重要的问题。
接下来,StartGame方法被调用。这个方法应该是这样的:
2 {
3 // Set game state to InGame
4 currentGameState = GameState.InGame;
5 // Start the soundtrack audio
6 SoundEffect se = Content.Load<SoundEffect>(@"audio\track");
7 trackInstance = se.Play(1, 0, 0, true);
8 // Play the start sound
9 se = Content.Load<SoundEffect>(@"audio\start");
10 se.Play( );
11 }
这个方法设置当前的游戏状态到InGame,然后播放一些声音效果去开始这个游戏。为了这些声音去工作,你需要去包括它们在你的项目中(记得,你将会使用简单的音频API在这个工程中,而不是XACT)。
查找本章的源代码,在Catch\Content\Audio文件夹,这有三个音频文件:boom.wav,start.wav,和track.wav。创建一个文件夹在Content节点下,命名为Audio并添加这些文件到这个文件夹下。
When the remote player starts the game
考虑到开始游戏的第二种方法(当别的玩家开始它,并且你接收一个网络信息告诉你去开始一个游戏),Update_Start代表MessageType(假定第一件事你总是写在你的包中,当你发送它们是一个MessageType enum值)。基于这个值,这个适当的方法被调用处理这个方法。
在这种特殊的情况下,这个你写的包在Update_Start方法包含一个MessageType.StartGame类型的信息。在发送这个信息后,这个方法调用StartGame方法。同样注意ProcessIncomingData方法,当一个MessageType.StartGame类型的信息被接收,这个StartGame方法被调用。这样,StartGame方法最终被调用在两台计算机上。
图17-7显示一个流程图,表示这个处理过程是怎么样的,并且StartGame方法最终如何被调用在两台PC上。当玩家1开始这个游戏,一个信息被发送到玩家2,并且玩家1的电脑调用StartGame.玩家2的电脑不断的找新的信息。当一个StartGame信息被读,StartGame被调用在玩家2的电脑上。
在移动到Update方法里其他方法调用之前,基于不同的游戏状态,让我们添加剩下的方法,参考ProcessIncomingData方法。这些方法将所有的功能象这个StartGame方法,因为在两台电脑上被调用,使用这个信息技术就象刚才所说的。
首先,添加EndGame方法:
2 {
3 // Play collision sound effect
4 // (game ends when players collide)
5 SoundEffect se = Content.Load<SoundEffect>(@"audio\boom");
6 se.Play( );
7 // Stop the soundtrack music
8 trackInstance.Stop( );
9 // Move to the game-over state
10 currentGameState= GameState.GameOver;
11 }
注意:这是至关重要的,例如StartGame和EndGame被调用在两台电脑上在这个会话中,否则你的数据和游戏状态将会不同步。为什么这种情况也许会更明显,当你了解到这两种方法播放音频效果。如果你有两个电脑在同一个会话中,并且EndGame被调用在它们其中之一,end-game声音效果可能播放在那一台电脑(译者:调用EndGame的电脑)。同时,音轨将停止唯一的那台电脑,并且游戏的状态将不会设置在另一台电脑上,意思是两台电脑将是完全不同的游戏状态。不好!
RejoinLobby和RestartGame方法非常相似:
2 {
3 //Switch dynamite and gears sprites
4 // as well as chaser vs. chased
5 SwitchPlayersAndReset(false);
6 currentGameState = GameState.Start;
7 }
8 private void RestartGame( )
9 {
10 // Switch dynamite and gears sprites
11 // as well as chaser vs. chased
12 SwitchPlayersAndReset(true);
13 StartGame( );
14 }
RestartGame方法调用StartGame方法,它实际上重起这个游戏。
2 {
3 // Get the other (non-local) player
4 NetworkGamer theOtherGuy = GetOtherPlayer( );
5 //Get the UserControlledSprite representing the other player
6 UserControlledSprite theOtherSprite = ((UserControlledSprite)theOtherGuy.Tag);
7 // Read in the new position of the other player
8 Vector2 otherGuyPos = packetReader.ReadVector2( );
9 // If the sprite is being chased,
10 // retrieve and set the score as well
11 if (!theOtherSprite.isChasing)
12 {
13 int score = packetReader.ReadInt32( );
14 theOtherSprite.score = score;
15 }
16 // Set the position
17 theOtherSprite.Position = otherGuyPos;
18 // Update only the frame of the other sprite
19 // (no need to update position because you just did!)
20 theOtherSprite.Update(gameTime, Window.ClientBounds, false);
21 }
22 protected NetworkGamer GetOtherPlayer( )
23 {
24 // Search through the list of players and find the
25 // one that's remote
26 foreach (NetworkGamer gamer in networkSession.AllGamers)
27 {
28 if (!gamer.IsLocal)
29 {
30 return gamer;
31 }
32 }
33 return null;
34 }
这个方法将重新找回远程的玩家通过调用GetOtherPlay的方法(上面显示),它收索在这个会话中所有的游戏玩家并且找不是本地的一个玩家。接下来,这个方法为这个玩家从这个Tag属性中,重新找回UserControlledSprite对象,并且读一个Vector2从这个包读取器中,你发送它给所有的UpdatePlayerPos MessageTypes。你同样发送分数给这个玩家如果远程玩家是被追赶的精灵玩家。这个方法读取这个数据并设置相应的成员在UserControlledSprite中。然后,这个方法更新远程玩家的精灵的动画祯。
Updating While in the InGame Game State
现在,你需要添加Update_InGame方法,这Update方法将会调用,当游戏在InGame的游戏状态:
2 {
3 // Update the local player
4 UpdateLocalPlayer(gameTime);
5 // Read any incoming data
6 ProcessIncomingData(gameTime);
7 // Only host checks for collisions
8 if (networkSession.IsHost)
9 {
10 // Only check for collisions if there are two players
11 if (networkSession.AllGamers.Count == 2)
12 {
13 UserControlledSprite sprite1 =(UserControlledSprite)networkSession.AllGamers[0].Tag;
14 UserControlledSprite sprite2 =(UserControlledSprite)networkSession.AllGamers[1].Tag;
15 if (sprite1.collisionRect.Intersects(sprite2.collisionRect))
16 {
17 // If the two players intersect, game over.
18 // Send a game-over message to the other player
19 // and call EndGame.
20 packetWriter.Write((int)MessageType.EndGame);
21 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
22 EndGame( );
23 }
24 }
25 }
26 }
下一步,end-game碰撞检查运行。但是如果玩家是主机呢。为什么只设有碰撞的主机检查呢?如果两个玩家都检查碰撞,它们可能都发送信息同时说,这里有一个碰撞--或者更糟,一个认为那里有一个冲突当其他的不这样认为。你可以添加一些代码去解析这段信息去回避这个问题,但是仍然会卷入更多个工作量。它常常是用于一个客户端成为主要的象碰撞,游戏开始,游戏结束,等等这类的检查。
所以,主机检查碰撞,如果发生了一个,它发送一个信息到另外的玩家,信息上说游戏结束了。然后它调用EndGame.
这个方法它更新本地的玩家(它被调用在Update_InGame的开始)被列在这里。添加这个方法到你的Game1类中:
2 {
3 // Get local player
4 LocalNetworkGamer localGamer = networkSession.LocalGamers[0];
5 // Get the local player's sprite
6 UserControlledSprite sprite = (UserControlledSprite)localGamer.Tag;
7 // Call the sprite's Update method, which will process user input
8 // for movement and update the animation frame
9 sprite.Update(gameTime, Window.ClientBounds, true);
10 // if this sprite is being chased, increment the score
11 // (score is just the num milliseconds that the chased player
12 // survived)
13 if(!sprite.isChasing)
14 sprite.score += gameTime.ElapsedGameTime.Milliseconds;
15 // Send message to other player with message tag and
16 // new position of sprite
17 packetWriter.Write((int)MessageType.UpdatePlayerPos);
18 packetWriter.Write(sprite.Position);
19 // If this player is being chased, add the score to the message
20 if (!sprite.isChasing)
21 packetWriter.Write(sprite.score);
22 // Send data to other player
23 localGamer.SendData(packetWriter, SendDataOptions.InOrder);
24 }
这个方法得到本地玩家,然后本地玩家的精灵。它然后调用Update在这个精灵上,它将处理用户的输入和更新这个动画的祯。
如果玩家正在被追赶,这个分数(这仅仅是他已经存活的毫秒数)被递增。然后,一个消息被发送到其他的玩家以新的玩家的位置和分数。
Updating While in the GameOver Game State
Update代码的最后的一部分是为GameOver游戏状态。添加这个方法到Game1类中:
2 {
3 KeyboardState keyboardState = Keyboard.GetState( );
4 GamePadState gamePadSate = GamePad.GetState(PlayerIndex.One);
5 // If player presses Enter or A button, restart game
6 if (keyboardState.IsKeyDown(Keys.Enter) ||gamePadSate.Buttons.A == ButtonState.Pressed)
7 {
8 // Send restart game message
9 packetWriter.Write((int)MessageType.RestartGame);
10 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
11 RestartGame( );
12 }
13 // If player presses Escape or B button, rejoin lobby
14 if (keyboardState.IsKeyDown(Keys.Escape) ||gamePadSate.Buttons.B == ButtonState.Pressed)
15 {
16 // Send rejoin lobby message
17 packetWriter.Write((int)MessageType.RejoinLobby);
18 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
19 RejoinLobby( );
20 }
21 // Read any incoming messages
22 ProcessIncomingData(gameTime);
23 }
这种方法将读取玩家的输入,并且,如果玩家表示她想要去重起这个游戏,发送一个信息到其他的玩家,并且调用RestartGame。同样为RegoinLobby做这个。然后,任何的输入数据被读取。
Adding Draw Code(添加了绘制代码)
最后一步是添加代码去绘制这个游戏。取代你存在Game1类的Draw方法中的代码:
2 {
3 // Only draw when game is active
4 if (this.IsActive)
5 {
6 // Based on the current game state,
7 // call the appropriate method
8 switch (currentGameState)
9 {
10 case GameState.SignIn:
11 case GameState.FindSession:
12 case GameState.CreateSession:
13 GraphicsDevice.Clear(Color.DarkBlue);
14 break;
15 case GameState.Start:
16 DrawStartScreen( );
17 break;
18 case GameState.InGame:
19 DrawInGameScreen(gameTime);
20 break;
21 case GameState.GameOver:
22 DrawGameOverScreen( );
23 break;
24 }
25 }
26 base.Draw(gameTime);
27 }
这个方法,象Update方法,将执行确定的动作,仅当游戏被激活时。当游戏服务窗口打开,这个方法阻止绘制。然后这个方法调用其他基于游戏状态的方法。
请注意SignIn,FindSession,和CreatSession游戏状态除了调用GraphicsDevice绘制一个黑屏其他的什么都不做。这是因为其他的游戏玩家的服务在这游戏状态下运行,并且不需要绘制到屏幕上。
现在,让我们开始下一步。添加下面的DrawStartScreen方法到你的Game1类中:
2 {
3 // Clear screen
4 GraphicsDevice.Clear(Color.AliceBlue);
5 // Draw text for intro splash screen
6 spriteBatch.Begin( );
7 // Draw instructions
8 string text = "The dynamite player chases the gears\n";
9 text += networkSession.Host.Gamertag +" is the HOST and plays as dynamite first";
10 spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(text).Y / 2)),Color.SaddleBrown);
11 // If both gamers are there, tell gamers to press space bar or Start to begin
12 if (networkSession.AllGamers.Count == 2)
13 {
14 text = "(Game is ready. Press Spacebar or Start button to begin)";
15 spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(text).Y / 2) + 60),Color.SaddleBrown);
16 }
17 // If only one player is there, tell gamer you're waiting for players
18 else
19 {
20 text = "(Waiting for players)";
21 spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2) + 60),Color.SaddleBrown);
22 }
23 // Loop through all gamers and get their gamertags,
24 // then draw list of all gamers currently in the game
25 text = "\n\nCurrent Player(s):";
26 foreach (Gamer gamer in networkSession.AllGamers)
27 {
28 text += "\n" + gamer.Gamertag;
29 }
30 spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2) + 90),Color.SaddleBrown);
31 spriteBatch.End( );
32 }
这个方法不能包含任何你以前看到的代码,除了在方法的末尾,在末尾你循环所有的玩家在这个网络会话中,并且取出它们的玩家代号去显示在屏幕上。方法的剩下部分绘制简单的提示,玩家可以读它在启动画面上。
接下来,在游戏中,添加下面的方法去绘制游戏画面:
2 {
3 // Clear device
4 GraphicsDevice.Clear(Color.White);
5 spriteBatch.Begin( );
6 // Loop through all gamers in session
7 foreach (NetworkGamer gamer in networkSession.AllGamers)
8 {
9 // Pull out the sprite for each gamer and draw it
10 UserControlledSprite sprite = ((UserControlledSprite)gamer.Tag);
11 sprite.Draw(gameTime, spriteBatch);
12 // If the sprite is being chased, draw the score for that sprite
13 if (!sprite.isChasing)
14 {
15 string text = "Score: " + sprite.score.ToString( );
16 spriteBatch.DrawString(scoreFont, text,
17 new Vector2(10, 10),
18 Color.SaddleBrown);
19 }
20 }
21 spriteBatch.End( );
22 }
这种方法循环所有的玩家在这个会话中,并取出它们的UserControlledSprite对象,然后绘制它。如果这个精灵被绘制是一个被追赶的,这个精灵的分数同样绘制在屏幕上。
最后,添加DrawGameOverScreen方法,它将循环所有的精灵,找出一个,它被追赶的,并绘制它的分数在屏幕上。然后它绘制提示给玩家为进一步的输入:
2 {
3 // Clear device
4 GraphicsDevice.Clear(Color.Navy);
5 spriteBatch.Begin( );
6 // Game over. Find the chased sprite and draw his score.
7 string text = "Game Over\n";
8 foreach (NetworkGamer gamer in networkSession.AllGamers)
9 {
10 UserControlledSprite sprite = ((UserControlledSprite)gamer.Tag);
11 if (!sprite.isChasing)
12 {
13 text += "Score: " + sprite.score.ToString( );
14 }
15 }
16 // Give players instructions from here
17 text += "\nPress ENTER or A button to switch and play again";
18 text += "\nPress ESCAPE or B button to exit to game lobby";
19 spriteBatch.DrawString(scoreFont, text,new Vector2((Window.ClientBounds.Width / 2)- (scoreFont.MeasureString(text).X / 2),(Window.ClientBounds.Height / 2)- (scoreFont.MeasureString(text).Y / 2)),Color.WhiteSmoke);
20 spriteBatch.End( );
21 }
哇。大量的代码!你现在已经它一个旋转。拉上一个朋友来玩下这个游戏在两台在同一域,子网,工作组不同的电脑上。你需要关闭两台电脑的防火墙。
一旦游戏运行,你可以看见登陆画面类似于图17-5和17-6所显示的那样。
在你们都登陆后,第一台电脑首先创建了一个会话,其他的电脑加入其中。这时你看见画面类似于图17-8。
游戏在某个人按下键盘或者单击Start按键在两台电脑上开始。两个玩家可能移动它们的自己电脑上,并且继续通过你已经执行的网络信息来反射到其他玩家的电脑上。你的游戏将看起来象图17-9所示。
最后,当精灵碰撞,game-over画面将显示(如图17-10所显示的)
Adding Biohazard Bombs of Insanity!
让我们修改这个游戏使事情变的有趣一点。而不是仅仅有一个玩家围绕屏幕追赶另一个玩家,我们将让被追赶的玩家放生物炸弹每5秒一次,它将减缓追赶精灵的速度使它在5秒钟内速度变成原来的50%。
首先,你需要添加一点资源。添加hazardhit.wav和hazardplant.wav文件到你的Content\Audio文件夹在Visual Studio(本文件位于本章的资源代码的Catch\Content\Audio文件夹),然后添加hazard.png图象到你的项目的Content\Images文件夹(这个文件位于本章资源代码的Catch\Content\Images文件夹)。
接下来,你需要发送两个新信息类型在两台电脑之间,当被追赶的精灵放置一个炸弹并且当追赶的精灵撞击到炸弹。添加两个新信息类型到MessageTypes enum中:
2 public enum MessageType { StartGame, EndGame, RestartGame,RejoinLobby, UpdatePlayerPos, DropBomb, ChaserHitBomb }
接下来,添加下面的类别变量到Game1类:
2 int bombCooldown = 0;
3 List<UserControlledSprite> bombList = new List<UserControlledSprite>( );
4 int bombEffectCooldown = 0;
该bombCooldown将会是一个冷却的计时器,表示当下一个炸弹可以被布置。这个bombList是一个炸弹对象列表。这个bombEffectCooldown将告诉你当炸弹的效果周期结束时。
因为你添加了两个信息类型为这个炸弹,你需要这个ProcessIncomingData方法并添加一些代码去做些事情,当这些消息被接收。添加下面的case语句到在这个方法中的switch语句里:
2 AddBomb(packetReader.ReadVector2( ));
3 break;
4 case MessageType.ChaserHitBomb:
5 ChaserHitBomb(packetReader.ReadInt32( ));
6 break;
2 {
3 // Add a bomb to the list of bombs
4 bombList.Add(new UserControlledSprite(Content.Load<Texture2D>(@"images\hazard"),position, new Point(100, 100), 10, new Point(0, 0),new Point(6, 8), Vector2.Zero, false));
5 // Play plant bomb sound effect
6 SoundEffect se = Content.Load<SoundEffect>(@"audio\hazardplant");
7 se.Play( );
8 }
接下来,添加下面的方法:
2 {
3 // Get the chaser player
4 NetworkGamer chaser = GetChaser( );
5 // Set the chaser's speed to 50% its current value
6 ((UserControlledSprite)chaser.Tag).speed *= .5f;
7 // Set the effect cooldown to 5 seconds
8 bombEffectCooldown = 5000;
9 // Remove the bomb
10 bombList.RemoveAt(index);
11 // Play the hazardhit sound
12 SoundEffect se = Content.Load<SoundEffect>(@"audio\hazardhit");
13 se.Play( );
14 }
15 protected NetworkGamer GetChaser( )
16 {
17 // Loop through all gamers and find the one that is chasing
18 foreach (NetworkGamer gamer in networkSession.AllGamers)
19 {
20 if (((UserControlledSprite)gamer.Tag).isChasing)
21 {
22 return gamer;
23 }
24 }
25 return null;
26 }
接下来,你需要有一个方法给这个玩家去设置一个炸弹。你想要你的本地玩家去设置炸弹,然后这台电脑发送一个信息告诉其它电脑,这里有一个炸弹被设置。为了实现这个,添加到下面的代码块在你的UpdateLocalPlayer方法的结尾:
2 if (!sprite.isChasing)
3 {
4 // If it's time to plant a bomb, let the user do it;
5 // otherwise, subtract gametime from the timer
6 if (bombCooldown <= 0)
7 {
8 // If user pressed X or X button, plant a bomb
9 if (Keyboard.GetState( ).IsKeyDown(Keys.X) ||GamePad.GetState(PlayerIndex.One).Buttons.X == ButtonState.Pressed)
10 {
11 // Add a bomb
12 AddBomb(sprite.Position);
13 bombCooldown = 5000;
14 packetWriter.Write((int)MessageType.DropBomb);
15 packetWriter.Write(sprite.Position);
16 localGamer.SendData(packetWriter, SendDataOptions.InOrder);
17 }
18 }
19 else
20 bombCooldown -= gameTime.ElapsedGameTime.Milliseconds;
21 }
如果玩家放置一个炸弹,AddBomb被调用,这个炸弹的冷却计时器被设置到5秒,并且一个包含炸弹位置的信息被发送到其他玩家。
接下来,由于你在游戏结束后可以重新起动游戏,你想要清除这个在StartGame方法中的炸弹表单,这样你每次可以从一个干净的游戏窗口开始游戏。添加下面的代码在StartGame方法的开头部分:
2 // during this instance of the application
3 bombList.Clear( );
2 foreach (UserControlledSprite bomb in bombList)
3 bomb.Update (gameTime, Window.ClientBounds, false);
2 if (networkSession.IsHost)
3 {
4 // Only check for collisions if there are two players
5 if (networkSession.AllGamers.Count == 2)
6 {
7 UserControlledSprite sprite1 =(UserControlledSprite)networkSession.AllGamers[0].Tag;
8 UserControlledSprite sprite2 =(UserControlledSprite)networkSession.AllGamers[1].Tag;
9 if (sprite1.collisionRect.Intersects(sprite2.collisionRect))
10 {
11 // If the two players intersect, game over.
12 // Send a game over message to the other player
13 // and call EndGame.
14 packetWriter.Write((int)MessageType.EndGame);
15 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
16 EndGame( );
17 }
18 }
19 }
2 if (networkSession.IsHost)
3 {
4 // Only check for collisions if there are two players
5 if (networkSession.AllGamers.Count == 2)
6 {
7 UserControlledSprite sprite1 =(UserControlledSprite)networkSession.AllGamers[0].Tag;
8 UserControlledSprite sprite2 =(UserControlledSprite)networkSession.AllGamers[1].Tag;
9 if (sprite1.collisionRect.Intersects(sprite2.collisionRect))
10 {
11 // If the two players intersect, game over.
12 // Send a game over message to the other player
13 // and call EndGame.
14 packetWriter.Write((int)MessageType.EndGame);
15 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
16 EndGame( );
17 }
18 // Check for collisions between chaser and bombs.
19 // First, get chaser.
20 UserControlledSprite chaser =(UserControlledSprite)GetChaser( ).Tag;
21 // Loop through bombs
22 for (int i = 0; i < bombList.Count; ++i)
23 {
24 UserControlledSprite bomb = bombList[i];
25 // If bombs and chaser collide, call ChaserHitBomb
26 // and send message to other player passing the index
27 // of the bomb hit
28 if (bomb.collisionRect.Intersects(chaser.collisionRect))
29 {
30 ChaserHitBomb(i);
31 packetWriter.Write((int)MessageType.ChaserHitBomb);
32 packetWriter.Write(i);
33 networkSession.LocalGamers[0].SendData(packetWriter,SendDataOptions.Reliable);
34 }
35 }
36 }
37 }
2 {
3 // Is there a bomb effect in place?
4 if (bombEffectCooldown > 0)
5 {
6 // Subtract game time from the timer
7 bombEffectCooldown -= gameTime.ElapsedGameTime.Milliseconds;
8 // If the timer has expired, expire the bomb effect
9 if (bombEffectCooldown <= 0)
10 {
11 ExpireBombEffect( );
12 }
13 }
14 }
15 private void ExpireBombEffect( )
16 {
17 // Get the chaser and restore the speed
18 // to the original speed
19 NetworkGamer chaser = GetChaser( );
20 ((UserControlledSprite)chaser.Tag).speed =((UserControlledSprite)chaser.Tag).originalSpeed;
21 }
第一方法检查去看炸弹效果的周期是否已满。如果它是,它调用第二个方法,它将得到追赶精灵并且恢复它初始的速度。
现在,电泳第一个方法在你的Update_InGame方法的结尾:
2 foreach (UserControlledSprite sprite in bombList)
3 sprite.Draw(gameTime, spriteBatch);
最后,你需要让玩家知道,按下X键或者X按钮就将会放置一个炸弹。当你在DrawStartScree方法期间,绘制一个说明在开始的画面上。第一个给用户的说明被保存在text变量中在下面的代码行里:
2 text += "Chased player can plant bombs with X key or X button\n";
正如你所看到的,微软的开发小组做了很伟大的工作在网络API上。它很容易去使用,并且一旦你得到一个处理在什么样的需要模拟的网络类型,什么样的数据类型会被通过它发送,并且你如何去表示玩家和另一个玩家对象在你的游戏中,你将和你的方法去创建下一个伟大的网络XNA游戏。
虽然这段代码在本章的焦点是创建一个网络游戏在Windows里,使用Games for Windows LIVE,同样的代码,它可以发送信息回来或者前进从PC到PC之间,可以被使用在Xbox360和Zune上。你同样可以应用同样的思想到这些平台上,网络体系结构方面,游戏状态, 等等。
<<Oreilly.Learning.XNA.3.0>>所有翻译结束!感谢收看!
源代码:http://shiba.hpe.cn/jiaoyanzu/WULI/soft/xna.aspx?classId=4
(完)