UNITY 3D – 网络游戏编程





5.00/5 (12投票s)
本文将涵盖使用 Unity 3D 中的 Network View 进行网络编程的基础知识。我们将创建一个基于权威服务器的网络环境,展示使用 Unity 3D 和 C# 进行网络编程的基本功能。
引言
本文是我们系列文章的延伸,旨在涵盖 Unity 3D 中网络编程的基础知识。这篇独立文章的目的是简化一般主题的解释和代码库。这种方法也让我能够发布一些通用的内容,然后您可以将其用于自己的项目。
如果您是第一次阅读 Unity 3D 文章,我已在下面列出了该系列的链接:
Unity 3D 网络文章
- Unity 3D - 网络游戏编程
Unity 3D Leap Motion 和 Oculus Rift 文章
以上列出的文章将为您提供关于 Unity 3D 的良好入门基础。
本文旨在演示并希望解释 Unity 3D 使用内置网络功能实现的网络能力。
注意:还有许多其他第三方解决方案,例如 Photon,它们将扩展 Unity 3D 的网络功能。
之所以选择 Network View 作为切入点,首先是为了不依赖第三方,其次即使您决定使用第三方解决方案,升级和理解其库也会非常容易。
背景
本文假定读者熟悉一般的编程概念。还假定读者对 C# 语言有理解和经验。还建议本文读者熟悉面向对象编程和设计概念。我们将在本文中根据需要简要介绍它们,但不会深入探讨细节,因为它们是完全独立的主题。我们还假定您对学习 3D 编程充满热情,并具备 3D 图形和向量数学的基本理论概念。
最后,本文使用 Unity 3D 4.6.1 版本,这是截至首次发布日期的最新公开版本。系列中讨论的大部分主题将与旧版本的游戏引擎兼容,也许也与预计今年某个时候发布的新版本兼容。然而,有一个主题在当前的 4.6.1 版本与旧版本的游戏引擎相比有显著不同,那就是 UI(用户界面)管道。这是由于引擎中新的 UI 架构,它远优于此版本之前我们所拥有的。就我个人而言,我对新的 UI 架构非常满意。
使用代码
下载服务器的项目/源代码:下载 Server.zip。
下载客户端的项目/源代码:下载 Client.zip。
随着后续文章的提交,项目/源代码也将随之扩展。新的项目文件和源文件将包含系列中较旧的部分。
走向多人游戏的步骤
网络编程通常是一个复杂的话题,我们不会在本文中深入探讨网络包的打包和解包细节。我们将讨论的是如何通过 Unity 3D 中可用的网络框架,使您的游戏支持多人游戏。
游戏设计和编程本身就是一个相当复杂的话题,除此之外,当您开始考虑多人游戏设计和编程时,事情会变得更加复杂。您的整个设计和架构都必须围绕着什么类型的信息将通过网络广播展开。谁能够消费这些信息/数据,以及如果数据存在视觉组件,它将如何以视觉方式呈现。换句话说,您需要进行大量的核算!
首先,当您开始项目时,您应该问以下问题:
-
您的游戏是单人游戏吗?
-
您的游戏是多人游戏吗?
-
您的游戏是否同时包含单人和多人组件?
提前了解这些将显著改变您的游戏设计和架构,并减少项目生命周期后期的复杂性。从一开始就设计和开发一款支持多人游戏的游戏要比以后将现有的单人游戏转换为多人游戏容易得多。
例如,我们在十篇系列文章中一直在创建的游戏纯粹是面向单人玩家的。我们甚至可能疯狂到为了好玩而将这个简单的游戏改成多人游戏!所以让我们思考一下。
需要问的一些问题
-
我们为什么要将游戏转换为多人游戏?
-
在多人环境中,游戏机制和逻辑将如何改变?
-
需要通过网络共享哪些数据/信息?
第一个问题的答案很简单:挑战自己!此外,还开始讨论网络和多人游戏编程与设计。
问题 2 和 3 需要更多的计划。
但在我们回答最后两个问题之前,让我们看看 Unity 3D 网络和一个独立于我们游戏的通用示例。
最终,我们的目标是将本文中发现和学到的知识应用到另一篇文章中的游戏中。
使用 Unity 3D Network View 进行网络编程
要理解并启用 Unity 3D 中的网络,您需要理解 NetworkView 组件。当然,阅读 Unity 提供的文档是最好的方式。这是链接:NetworkView 文档。
简而言之,Network View 是涉及网络数据共享的主要组件。它们允许两种网络通信:**状态同步**和**远程过程调用**。
网络视图会密切关注特定对象以检测变化。然后,这些变化会共享给网络上的其他客户端,以确保所有客户端都注意到状态变化。这个概念被称为**状态同步**,您可以在 状态同步 页面上进一步阅读。
在某些情况下,您可能不希望在客户端之间同步状态带来额外的开销,例如,当发送新对象的位置或玩家重新生成时。由于此类事件不频繁,同步所涉及对象的状态没有意义。相反,您可以使用远程过程调用来告诉客户端或服务器执行此类操作。有关**远程过程调用**的更多信息,请参阅 RPC 手册页面。
创建服务器
我们将首先为我们的网络演示创建服务器代码。让我们创建一个新的空 Unity 项目。在“项目”选项卡下,在“资产”中创建两个新文件夹。一个应该命名为 Game,另一个命名为 Server。
现在让我们创建第一个脚本。点击 *Server* 文件夹并创建一个名为 *ServerNetworkManager.cs* 的 C# 脚本。
这个脚本将负责启动和停止我们游戏的服务器。它还将处理一些服务器端事件,正如您将在脚本中看到的那样。
*ServerNetworkManager.cs* 的列表
using UnityEngine; using UnityEngine.UI; using System.Collections; [RequireComponent(typeof(ServerPlayerManager))] public class ServerNetworkManager : MonoBehaviour { private bool serverStarted = false; public Text lblServerCommandCaption; public Text lblServerIP; public Text lblServerPort; public Transform serverMessagePanel; public GameObject serverMessageInfo; private ServerPlayerManager spm; void Awake() { spm = gameObject.GetComponent<ServerPlayerManager>() as ServerPlayerManager; this.serverStarted = false; if (this.lblServerCommandCaption != null) { this.lblServerCommandCaption.text = "Start Server"; } } /// <summary> /// This function was added to perform the new GUI commands /// </summary> public void ServerCommand() { if (!this.serverStarted) { this.startServer(); this.serverStarted = !this.serverStarted; this.lblServerCommandCaption.text = "Stop Server"; } else { this.stopServer(); this.serverStarted = !this.serverStarted; this.lblServerCommandCaption.text = "Start Server"; this.lblServerIP.text = "IP Address ..."; this.lblServerPort.text = "Port ..."; } } void Update() { if(this.serverStarted) { if (Network.peerType == NetworkPeerType.Connecting) { //GUILayout.Label("Network server is starting up..."); Debug.Log("Network server is starting up..."); } else { //GUILayout.Label("Network server is running."); Debug.Log("Network server is running."); showServerInformation(); showClientInformation(); } } } public int listenPort = 25000; public int maxClients = 5; private void CaptureServerEvent(string msg) { GameObject newButton = GameObject.Instantiate(this.serverMessageInfo) as GameObject; ServerInfoItem butItem = newButton.GetComponent<ServerInfoItem>(); butItem.lblServerInfo.text = string.Format(msg); newButton.transform.SetParent(this.serverMessagePanel); } public void startServer() { this.CaptureServerEvent(">>>STARTING SERVER ...."); Network.InitializeServer(maxClients, listenPort, false); } public void stopServer() { Network.Disconnect(); } public void OnServerInitialized() { //Debug.Log("Network server initialized and ready"); this.CaptureServerEvent("Network server initialized and ready"); } public void OnDisconnectedFromServer(NetworkDisconnection info) { //Debug.Log("Network server disconnected"); this.CaptureServerEvent("Network server disconnected"); } public void OnPlayerConnected(NetworkPlayer player) { //Debug.Log("Player " + player + " connected from ip/port: " + player.ipAddress + "/" + player.port); this.CaptureServerEvent("Player " + player + " connected from ip/port: " + player.ipAddress + "/" + player.port); spm.spawnPlayer(player); } public void OnPlayerDisconnected(NetworkPlayer player) { //Debug.Log("Player disconnected"); this.CaptureServerEvent("Player disconnected"); spm.deletePlayer(player); } // this function will show client information when called ... public void showClientInformation() { /*Debug.Log ("Clients: " + Network.connections.Length + "/" + maxClients); foreach(NetworkPlayer p in Network.connections) { Debug.Log(" Player " + p + " from ip/port: " + p.ipAddress + "/" + p.port); }*/ } public void showServerInformation() { //GUILayout.Label("IP: " + Network.player.ipAddress + " Port: " + Network.player.port); if(this.serverStarted) { this.lblServerIP.text = string.Format("IP: {0}", Network.player.ipAddress.ToString()); this.lblServerPort.text = string.Format("Port: {0}", Network.player.port.ToString()); } } }
让我们仔细研究一下这个脚本,以便更好地理解它。我们来看看 startServer()
和 stopServer()
这两个函数。正如名称所示,它们用于启动和停止服务器。查看函数体,我们只对其中一条重要的行感兴趣,那就是 Network.InitializeServer(maxClients, listenPort, false);
Network
类是 Unity 中的主要网络类。函数 InitializeServer()
启动服务器,它接收允许的最大客户端数量、用于监听传入客户端的端口,最后是一个指示服务器是否应使用 NAT 穿透以允许客户端连接的参数。
在 stopServer()
函数中,Network.Disconnect()
行停止服务器。
在代码的更新版本中,GUI(图形用户界面)已更新为使用新的用户界面框架。这提供了更好的 GUI 表示,也为我们提供了更大的灵活性和能力来显示我们需要的信息。
函数 OnGUI()
已被替换,新的代码库旨在与新的 GUI 架构协同工作。处理 GUI 命令的新函数是 ServerCommand()
。它由一个 GUI 按钮驱动,并检查布尔变量 serverStarted
的状态。如果值为 false,它将通过调用 startServer()
函数来启动服务器并设置适当的显示标签。如果值为 true,它将通过调用 stopserver()
函数来停止服务器并更新适当的显示标签。
注意:我们将在不同的部分讨论 UI 元素。
Network.peerType 可以有以下值:
-
Disconnected – 服务器未初始化,且没有客户端连接正在运行。
-
Server – 我们作为服务器运行。
-
Client – 我们作为客户端运行。
-
Connecting – 我们正在尝试连接到服务器。
其他函数用于处理网络上的特定事件。
-
OnServerInitialized()
-
OnDisconnectedFromServer(NetworkDisconnection info)
-
OnPlayerConnected(NetworkPlayer player)
-
OnPlayerDisconnected(NetworkPlayer player)
前两个是自解释的,当服务器初始化时,会调用 OnServerInitialized()
函数,当服务器断开连接时,会调用 OnDisconnectedFromServer(NetowkrDisconnection info)
。另外两个与玩家连接和断开连接相关的函数在这一点上对我们更感兴趣。
当玩家/客户端连接到服务器时,会调用 OnPlayerConnected(NetworkPlayer player)
。此函数将使用 ServerPlayerManager
类为我们生成新连接的玩家到场景中。
如果你看一下 Awake()
函数,我们正在获取一个名为 ServerPlayerManager
的组件,它附加到持有我们脚本的 GameObject 上,并将其分配给 spm
变量,该变量的类型是 ServerPlayerManager
。现在你一定想知道 ServerPlayerManager
类是什么以及它做什么。
简而言之,ServerPlayerManager
类负责:
-
将玩家生成到场景中;
-
从场景中删除玩家并清理所有相关内容;
-
处理玩家输入;
*ServerPlayerManager.cs* 的列表
using UnityEngine; using System.Collections; public class ServerPlayerManager : MonoBehaviour { public Hashtable players = new Hashtable(); public void spawnPlayer(NetworkPlayer player) { Debug.Log("Spawning player game object for player " + player); PlayerInfo ply = GameObject.FindObjectOfType(typeof(PlayerInfo)) as PlayerInfo; GameObject go = Network.Instantiate(ply.playerInfo, Vector3.up*3, Quaternion.identity, 0) as GameObject; players[player] = go; } public void deletePlayer(NetworkPlayer player) { Debug.Log("Deleting player game object for player " + player); GameObject go = players[player] as GameObject; Network.RemoveRPCs(go.networkView.viewID); // remove buffered Instantiate calls Network.Destroy(go); // destroy the game object on all clienst players.Remove(player); // remove player from server list } [RPC] public void handlePlayerInput(NetworkPlayer player, float vertical, float horizontal) { Debug.Log("Received move from player " + player); GameObject go = players[player] as GameObject; if (horizontal > 0) { go.transform.Translate(Vector3.forward * Time.deltaTime); } else { go.transform.Translate(Vector3.back * Time.deltaTime); } if (vertical > 0) { go.transform.Rotate(Vector3.up, 10 * Time.deltaTime); } else { go.transform.Rotate(Vector3.up, -10 * Time.deltaTime); } } [RPC] public void handlePlayerInputV2(NetworkPlayer player, string key) { Debug.Log("Received move from player - V2" + player); GameObject go = players[player] as GameObject; if (key.Equals("W")) { go.transform.Translate(Vector3.forward * Time.deltaTime); } if(key.Equals("S")) { go.transform.Translate(Vector3.back * Time.deltaTime); } if (key.Equals("A")) { go.transform.Rotate(Vector3.up, -1); } if (key.Equals("D")) { go.transform.Rotate(Vector3.up, 1); } } }
所以 spawnPlayer(NetworkPlayer player)
函数根据代表该玩家的预制件生成一个玩家。该预制件附加到另一个名为 PlayerInfo
的类。在这一点上,PlayerInfo
类的全部目的是保留我们玩家的预制件。最终,您可以扩展此类别以包含更多信息等等……
首先,我们从 GameObject 获取 PlayerInfo 组件,然后创建一个新的 GameObject 来保存网络上的玩家实例。如果您想通过网络实例化一个 GameObject,您需要使用 Network.Instantiate(…)
函数!我们使用以下行实例化我们的玩家对象:
GameObject go = Network.Instantiate(ply.playerInfo, Vector3.up*3, Quaternion.identity, 0) as GameObject;
最后,我们将玩家的引用存储到哈希表中。我们使用一个 Hashtable 来跟踪玩家。这样我们就可以正确地删除他们以及该特定玩家的相关 RPC。
当玩家从服务器断开连接时,将调用 deletePlayer(NetworkPlayer player)
。我们从哈希表中检索玩家引用,并使用 Network
类从网络中删除所有与 RPC 相关的内容,然后最终通过网络销毁对象并从哈希表中删除条目。
handlePlayerInput(NetworkPlayer player, float vertical, float horizontal)
函数将提供一种方式,将玩家位置传达给网络上的所有人。这是一个 RPC 调用。有关 远程过程调用 的更多信息,请参见 RPC 手册页面。基本上,这里发生的是每个客户端将其位置信息发送给服务器,然后服务器将信息传输给所有客户端。
handlePlayerInputV2(NetworkView player, string key)
函数的创建是为了处理玩家根据客户端以字符串值形式传递给服务器的按键输入而进行的移动。这同样是一个 RPC 调用。如果将来需要,此函数将为您提供执行更多命令的选项,例如发射炮弹等。
注意:连接中的每个客户端都拥有所有玩家的哈希表副本。请注意,我们使用哈希表来获取特定的玩家 GameObject 并将更改应用到该特定玩家。
现在我们有了脚本,接下来让我们构建场景和用于演示的简单关卡。
创建游戏
下一步是创建用于演示的场景或关卡。如前所述,关卡设计和您可以实现的功能将非常简单。
继续在您的场景中创建以下项:
-
一个平面 GameObject。这可以通过从主菜单中选择 GameObject->3D Object->Plane 来完成。
-
场景中应该已经存在一个主摄像机。如果没有,请使用相同的方法创建一个:从主菜单中选择 GameObject->Camera。
-
一个方向光,您可以通过从主菜单中选择 GameObject->Light->Directional Light 来实现。
-
一个名为 GameController 的空游戏对象。
注意:如果您不熟悉 GameObject 创建和/或 Unity IDE,我建议您阅读系列文章或自己进行一些实验以了解基本知识。
完成以上步骤后,您的层级窗口应该如下所示:
将场景保存到您之前创建的 Game 文件夹中。将场景命名为 Game。
接下来创建一个 Cube 原始体,并将以下组件附加到 Cube GameObject:
-
网络视图组件
-
刚体组件
这可以通过选择 Cube GameObject,并使用 Inspector Window,选择 Add Component 并找到所需的列出组件来实现。
保持刚体和网络视图组件的默认值不变。
通过将立方体拖到 Game 文件夹中来制作一个 Prefab。一个预制件已经创建。您现在可以从场景中删除立方体。我们不再需要它了。
如果您还记得上一节,我们需要另一个脚本来处理 PlayerInfo
。这个脚本也将放置在 *Game* 文件夹中。继续创建一个新的 C# 脚本并将其命名为 *PlayerInfo.cs*。
*PlayerInfo.cs* 的列表
using UnityEngine; using System.Collections; public class PlayerInfo : MonoBehaviour { public GameObject playerInfo; }
它只是一个引用我们预制件的脚本。您的最终环境在视觉上应该大致如下所示:
至此,我们拥有了所有的资产。现在我们需要正确配置它们,以获得我们想要的结果。
选择 GameController GameObject
,并将 PlayerInfo
和 ServerNetworkManager
脚本附加到 GameController。同时将 Network View 组件添加到 GameController GameObject。
请注意,当您将 ServerNetworkManager
脚本附加到 GameController
时,ServerPlayerManager
也会自动附加,这是因为在我们的 ServerNetworkManager 中,我们通过在类声明之前放置以下命令来声明 ServerNetworkManager
需要 ServerPlayerManager
:[RequireComponent(typeof(ServerPlayerManager))]
GameController 上的最后一步配置是将网络视图的属性更改为以下内容:
-
状态同步应设置为关闭
-
观察者应设置为无
差点忘了,还有一个最后的配置需要完成。请注意,PlayerInfo
组件有一个用于我们玩家预制件的占位符。这里将附加我们的立方体预制件,所以继续将 立方体 预制件拖放到 Player Info 插槽 中。
如果一切正确,您现在就可以测试服务器代码了。继续运行程序。您将看到以下屏幕:
如您所见,我们的 GUI 显示服务器未运行,并显示一个按钮供我们启动服务器。继续点击“启动服务器”按钮。
如您所见,我们已经启动了服务器,并且它已成功初始化。现在我们可以将注意力转向客户端代码了。
创建客户端
我们将为客户端启动一个新项目。是的,我很长时间以后才意识到最好将服务器和客户端代码分开。这让生活变得更轻松,也让初学者更容易理解解释。
然而,尽管这是一个新项目,它必须与服务器项目完全相同。只有一些区别。所以我们首先要做的是创建目录结构。继续创建两个文件夹:
-
客户端
-
游戏
Game 文件夹将与服务器项目中的 Game 文件夹完全相同,所以请您将服务器项目中的 Game 文件夹内容复制并粘贴到客户端项目中的 Game 文件夹。
现在我们来创建客户端脚本。在 Client 文件夹中,创建一个新的 C# 脚本并将其命名为 *ClientManger.cs*。ClientManager 负责,您猜对了,就是客户端。它是处理与服务器的连接以及客户端发送到服务器的更新等的代码。
*ClientManager.cs* 的列表
using UnityEngine; using UnityEngine.UI; using System.Collections; public class ClientManager : MonoBehaviour { private bool clientStarted = false; public Text lblClientCommandCaption; public Text lblClientIP; public Text lblClientPort; public Transform clientMessagePanel; public GameObject clientMessageInfo; /// <summary> /// This function was added to perform the new GUI commands /// </summary> public void ClientCommand() { if (!this.clientStarted) { this.connectToServer(); this.clientStarted = !this.clientStarted; this.lblClientCommandCaption.text = "Disconnect"; } else { this.disconnectFromServer(); this.clientStarted = !this.clientStarted; this.lblClientCommandCaption.text = "Connect to Server"; this.lblClientIP.text = "IP Address ..."; this.lblClientPort.text = "Port ..."; } } private void CaptureClientEvent(string msg) { GameObject newButton = GameObject.Instantiate(this.clientMessageInfo) as GameObject; ClientInfoItem butItem = newButton.GetComponent<ClientInfoItem>(); butItem.lblClientInfo.text = string.Format(msg); newButton.transform.SetParent(this.clientMessagePanel); } void Update() { if(Input.anyKey) { sendInputToServer(); } } public void sendInputToServer() { if(Input.GetKey(KeyCode.W)) networkView.RPC("handlePlayerInputV2", RPCMode.Server, Network.player, "W"); if(Input.GetKey(KeyCode.S)) networkView.RPC("handlePlayerInputV2", RPCMode.Server, Network.player, "S"); if (Input.GetKey(KeyCode.A)) networkView.RPC("handlePlayerInputV2", RPCMode.Server, Network.player, "A"); if (Input.GetKey(KeyCode.D)) networkView.RPC("handlePlayerInputV2", RPCMode.Server, Network.player, "D"); } [RPC] public void handlePlayerInput(NetworkPlayer player, float vertical, float horizontal) { } [RPC] public void handlePlayerInputV2(NetworkPlayer player, string key) { } public string remoteIP = "127.0.0.1"; // NOTE: You will replace with your server IP public int remotePort = 25000; public void connectToServer() { //Debug.Log("Tyring to connect to Server..."); this.CaptureClientEvent("Tyring to connect to Server..."); Network.Connect(remoteIP, remotePort); } public void disconnectFromServer() { //Debug.Log("Tyring to disconnect from the Server..."); this.CaptureClientEvent("Tyring to disconnect from the Server..."); Network.Disconnect(); } public void OnConnectedToServer() { //Debug.Log("Successfully connected to server as player " + Network.player); this.CaptureClientEvent("Successfully connected to server as player " + Network.player); this.lblClientIP.text = string.Format("IP: {0}", Network.player.ipAddress.ToString()); this.lblClientPort.text = string.Format("Port: {0}", Network.player.port.ToString()); } public void OnDisconnectedFromServer (NetworkDisconnection info) { if (info == NetworkDisconnection.LostConnection) //Debug.Log("Lost connection to the server"); this.CaptureClientEvent("Lost connection to the server"); else //Debug.Log("Disconnected from the server"); this.CaptureClientEvent("Disconnected from the server"); GameObject[] gos = GameObject.FindGameObjectsWithTag("Player"); foreach(GameObject go in gos) { Destroy(go); } } public void OnFailedToConnect (NetworkConnectionError error) { //Debug.Log("Failed to connect to Server: " + error); this.CaptureClientEvent("Failed to connect to Server: " + error); } }
如您所见,代码结构与 ServerNetworkManager
代码非常相似。但是有一些不同之处。让我们来看看这些不同之处。
以下是已定义函数的列表:
-
connectToServer()
-
disconnectFromServer()
-
OnConnectedToServer()
-
OnDisconnectedFromServer(NetworkDisconnection info)
-
OnFailedToConnect(NetworkConnectionError error)
-
handlePlayerInput(NetworkPlayer player, float vertical, float horizontal)
-
sendInputToserver()
-
Update()
connectToServer()
函数使用 Network.Connect()
函数连接到指定的服务器。disconnectFromServer()
函数使用 Network.Disconnect()
从服务器断开连接。OnConnectedToServer()
函数只是吐出谁已连接到服务器。OnFailedToConnect()
函数吐出我们无法连接到服务器的调试错误。
OnDisconnectedFromServer (NetworkDisconnection info)
函数至关重要,因为它执行客户端的清理工作。它查找所有带有“Player”标签的 GameObject
并将其从本地场景中销毁。
Update()
函数检查是否有任何键被按下,然后调用 sendInputToServer()
函数,该函数获取 垂直 和 水平 轴,如果它们不等于 0,则使用以下代码行通过网络向服务器调用 RPC 函数:
networkView.RPC("handlePlayerInputV2", RPCMode.Server, Network.player, string key);
RPC 函数是可以使用网络视图组件通过网络调用的函数。它有三个参数:
-
RPC 函数名 – handlePlayerInput
-
RPC 模式 – 在本例中,我们只将此消息发送到服务器,使用 RPCMode.Server
-
函数参数:Network.player、vertical 和 horizontal
客户端配置
现在我们已经编写好了客户端脚本。我们需要修改从服务器项目复制过来的 GameController GameObject。选择 GameController,并删除服务器脚本:*ServerNetworkManager.cs* 和 *ServerPlayerManager.cs*。
现在将 ClientManager 脚本附加到 Client 项目中的 GameController。您现在应该只在 GameController 上附加了 *PlayerInfo.cs* 和 *ClientManager.cs* 脚本。
测试我们的客户端/服务器代码
为了测试我们的客户端和服务器代码协同工作,您需要启动两个独立的 Unity 3D 实例,一个打开服务器代码,另一个打开客户端代码。
假设您的服务器代码已启动并运行,现在我们可以启动客户端代码。当您第一次运行客户端时,将出现以下内容:
让我们点击“连接到服务器”按钮,看看会发生什么。
客户端控制台
服务器端控制台
如您所见,我们的代码按预期运行。停止客户端项目,让我们继续构建客户端的独立可执行文件。选择文件->构建...
选择 PC、Mac 和 Linux 独立版,然后选择您的平台。确保在“播放器设置”中选中“在后台运行”,然后构建您的项目。
连接多个客户端到服务器
使用新创建的服务器可执行文件来执行和运行服务器。我们还将使用客户端可执行文件启动三个客户端,以演示多玩家模拟中的服务器/客户端关系。
启动服务器,通过执行您刚刚构建的可执行文件来启动一个客户端。选择最低分辨率并选中“窗口化”复选框,这样我们就不会占用太多屏幕空间。点击“播放”。客户端启动,我们看到“连接到服务器”按钮,继续选择它。
您的屏幕现在应该如下所示:
继续使用客户端,通过键盘箭头键移动方块(玩家)。请注意,方块在两个屏幕上都移动了。还要注意服务器上的控制台。我们正在捕获谁正在进行移动!
同时请注意,服务器控制台已更新了新的连接信息。就是这样!我们现在已经实现了我们基本的**权威服务器网络基础**。既然您已经了解了基础知识,希望您可以在此基础上进行扩展,创建更复杂、更令人印象深刻的多人游戏。
请注意,当我们停止服务器时,所有连接到服务器的剩余客户端都会自动断开连接。
关注点
如前所述,网络编程通常既复杂又繁琐。创建多人游戏是一项非常具有挑战性的任务,需要花费大量时间学习该主题的总体知识。然后,您还需要花费大量时间学习 Unity 3D 提供的网络设计和架构,这可能又是另一个里程碑。
这篇文章是基于我研究的几篇文章,以学习 Unity 3D 网络机制,希望它能帮助某些人比我更快地掌握!
对于一直关注 Unity 3D 系列文章的读者,我将尝试看看我们能否创建我们的游戏《淘金热》的多人版本!
Unity 3D 系列文章
- Unity 3D – 游戏编程 – 第 1 部分
-
Unity 3D – 游戏编程 – 第 10 部分
Unity 3D 网络文章
- Unity 3D - 网络游戏编程
Unity 3D Leap Motion 和 Oculus Rift 文章