第一个网络代码
生成玩家
首先,我们需要在有人登录时让玩家出现。我们需要确保登录的人收到当前在线玩家列表,并确保其他所有人在他们的游戏中生成我们的新玩家。
但是让我们先退一步,规划一下我们需要存储有关玩家的内容:
- 它们在游戏世界中的X,Y位置。
- 他们的半径(当他们吃掉其他玩家时,他们会变得更大)。
- 他们的颜色
- 另外,在未来,我们可能想要存储一个名称。
好的,请继续创建一个新的Player类在服务器上,以存储这些信息。如果您和我一样懒得写代码,可以复制并粘贴以下代码:
class Player
{
public ushort ID { get; set; }
public float X { get; set; }
public float Y { get; set; }
public float Radius { get; set; }
public byte ColorR { get; set; }
public byte ColorG { get; set; }
public byte ColorB { get; set; }
public Player(ushort ID, float x, float y, float radius, byte colorR, byte colorG, byte colorB)
{
this.ID = ID;
this.X = x;
this.Y = y;
this.Radius = radius;
this.ColorR = colorR;
this.ColorG = colorG;
this.ColorB = colorB;
}
}
我使用字节表示颜色,因为这意味着我们需要存储和发送的数据比使用浮点数少得多,而且它将起到完全相同的作用。在发送消息多次时,采取这样的小步骤可以真正改善带宽。
要生成玩家,我们需要告诉DarkRift在玩家连接时通知我们。在AgarPlayerManager的构造函数中添加以下代码行:
Server.ClientManager.ClientConnected += OnPlayerConnected;
并添加一个新方法:
void ClientConnected(object sender, ClientConnectedEventArgs e)
{
}
ClientManager负责跟踪连接到服务器的任何客户端,因此您可以使用它来访问客户端,并且它可以在客户端连接或断开连接时通知您。在这行代码中,我们订阅了ClientConnected事件,所以每当有人连接时,我们的方法将被调用,并将ClientManager作为发送方和连接详细信息作为ClientConnectedArgs传递进来。
让我们给我们新连接的玩家分配他们自己的Player对象。在我们的ClientConnected方法中添加代码,生成一个新的玩家设置与随机值:
Random r = new Random();
Player newPlayer = new Player(
e.Client.ID,
(float)r.NextDouble() * MAP_WIDTH - MAP_WIDTH / 2,
(float)r.NextDouble() * MAP_WIDTH - MAP_WIDTH / 2,
1f,
(byte)r.Next(0, 200),
(byte)r.Next(0, 200),
(byte)r.Next(0, 200)
);
在类的顶部添加一个MAP_WIDTH常量:
const float MAP_WIDTH = 20;
您可能会注意到其中隐藏着e.Client.ID。为了在Unity端使用玩家时能够标识它,由于客户端只有一个控制的Player对象,因此只使用DarkRift连接时分配给客户端的服务器ID即可。
我在每个颜色通道中使用了0到200之间的值,因为如果它太接近255, 255, 255,我们将无法看到客户端与背景的区别!
接下来,让我们将新的Player对象发送给所有其他客户端,以便他们可以在他们的游戏中生成一个新的玩家。在以下代码之后添加:
using (DarkRiftWriter newPlayerWriter = DarkRiftWriter.Create())
{
newPlayerWriter.Write(newPlayer.ID);
newPlayerWriter.Write(newPlayer.X);
newPlayerWriter.Write(newPlayer.Y);
newPlayerWriter.Write(newPlayer.Radius);
newPlayerWriter.Write(newPlayer.ColorR);
newPlayerWriter.Write(newPlayer.ColorG);
newPlayerWriter.Write(newPlayer.ColorB);
using (Message newPlayerMessage = Message.Create(0, newPlayerWriter))
{
foreach (IClient client in ClientManager.GetAllClients().Where(x => x != e.Client))
client.SendMessage(newPlayerMessage, SendMode.Reliable);
}
}
哇哦!那变得很复杂了,是吧!
让我们逐个查看每个部分。我们需要将我们的Player转换为可以通过网络发送的东西,而目前它是一个无符号短整型、3个浮点数和3个字节,这与互联网不兼容。DarkRiftWriter和DarkRiftReader是提供给您的对象,使该转换更加容易,我们将所有字段写入writer中,在Unity端,我们将以相同的顺序使用reader读取它们。
接下来,我们构建一个Message。消息本质上包装了数据(我们将所有这些值放入DarkRiftWriter中),并赋予它们一些简单的头标识,以标识数据实际包含的内容。第一个参数,标签,是一个ushort,用于标识消息的内容(例如移动玩家、使用库存物品、发送聊天消息等),第二个参数是我们的数据。现在我们可以将0作为标签使用。
最后,我们使用ClientManager.GetAllClients()获取当前连接到服务器的所有客户端,利用一些LINQ来移除刚刚连接的客户端(我们稍后再处理他),然后将消息发送给每个客户端。不要忘记,当您调用GetAllClients()时,已经添加了刚刚连接的客户端!
看到了吗?很容易!你担心什么呢?
但是说真的,这基本上就是发送DarkRift消息的全部内容,包括客户端和服务器:创建一个用于数据的writer,一个用于包装数据的消息,然后发送它们。
然而,我刚刚忽略的一件事是SendMode。这标识我们应该如何将消息发送给客户端:
- Unreliable 在使用Unreliable发送模式时,不能保证另一个设备收到消息,但发送速度更快。这适用于频繁更新的数据,这些数据很快就会过时(例如位置/旋转更新)。
- Reliable 使用Reliable发送模式时,保证消息能够被成功发送到另一个设备,并且如果消息太大无法发送,它将被分成多个小消息进行发送。
接下来,我们需要快速地在新连接的玩家上生成所有其他玩家;由于我们希望在这里尽量减少使用的带宽,因此我们将把所有内容打包成一个单独的消息进行发送。在AgarPlayerManager的顶部添加以下行:
Dictionary<IClient, Player> players = new Dictionary<IClient, Player>();
然后,将以下代码添加到ClientConnected方法的末尾:
players.Add(e.Client, newPlayer);
using (DarkRiftWriter playerWriter = DarkRiftWriter.Create())
{
foreach (Player player in players.Values)
{
playerWriter.Write(player.ID);
playerWriter.Write(player.X);
playerWriter.Write(player.Y);
playerWriter.Write(player.Radius);
playerWriter.Write(player.ColorR);
playerWriter.Write(player.ColorG);
playerWriter.Write(player.ColorB);
}
using (Message playerMessage = Message.Create(0, playerWriter))
e.Client.SendMessage(playerMessage, SendMode.Reliable);
}
希望这个过程已经很清楚了,唯一的区别是我们将所有数据写入同一个writer中,以便在一个消息中发送所有数据。请注意,在枚举players字典之前将新玩家添加到其中,以确保将其包括在发送给玩家的数据中(否则新玩家不会生成自己的玩家!)
我们忽略的另一件事是using语句。消息、DarkRiftWriter和DarkRiftReader都实现了IDisposable接口,这不是因为它们包含需要处理的资源,而是因为在调用Dispose方法时,它们将被返回到一个对象池中,以便下次调用Create时它们不必重新分配。严格来说,using语句是完全可选的,但是如果你留下它们,你将获得更好的性能!我们将在高级部分中更深入地讨论对象池的回收机制。
最后,最好定义标签的常量,以便可以轻松地引用它们并在没有副作用的情况下修改它们。添加一个名为Tags的新文件,并放入一个静态类来收集所有标签:
static class Tags
{
public static readonly ushort SpawnPlayerTag = 0;
}
将Message.Create调用中的tag参数都更改为:Tags.SpawnPlayerTag。
实际生成玩家
现在我们已经为生成玩家编写了服务器端代码,让我们添加客户端代码。在Unity项目中创建一个名为PlayerSpawner的新文件,并添加以下代码:
using DarkRift.Client.Unity;
public class PlayerSpawner : MonoBehaviour
{
const byte SPAWN_TAG = 0;
[SerializeField]
[Tooltip("The DarkRift client to communicate on.")]
UnityClient client;
[SerializeField]
[Tooltip("The controllable player prefab.")]
GameObject controllablePrefab;
[SerializeField]
[Tooltip("The network controllable player prefab.")]
GameObject networkPrefab;
void Awake()
{
if (client == null)
{
Debug.LogError("Client unassigned in PlayerSpawner.");
Application.Quit();
}
if (controllablePrefab == null)
{
Debug.LogError("Controllable Prefab unassigned in PlayerSpawner.");
Application.Quit();
}
if (networkPrefab == null)
{
Debug.LogError("Network Prefab unassigned in PlayerSpawner.");
Application.Quit();
}
client.MessageReceived += SpawnPlayer;
}
}
在这里,您会注意到我们定义了对UnityClient对象的引用,我们将从检查器中填充它。这是处理我们与服务器的连接的组件,我们之前将它添加到了Unity场景中的Network对象中,还记得吗?
我们订阅的MessageReceived事件会在客户端从服务器接收到消息时调用。您可以订阅任意数量的处理程序,但是较少的处理程序通常更易于维护并且速度更快。
另一个重要的事情要注意的是,我们从Awake调用这个方法。当您将UnityClient设置为在Start例程中连接时,重要的是要确保从Awake订阅所有初始消息,否则,如果它在您订阅之前连接,您可能会错过消息!
将以下代码添加到解码我们的生成数据包的方法中:
void SpawnPlayer(object sender, MessageReceivedEventArgs e)
{
using (Message message = e.GetMessage())
using (DarkRiftReader reader = message.GetReader())
{
if (message.Tag == Tags.SpawnPlayerTag)
{
if (reader.Length % 17 != 0)
{
Debug.LogWarning("Received malformed spawn packet.");
return;
}
while (reader.Position < reader.Length)
{
ushort id = reader.ReadUInt16();
Vector3 position = new Vector3(reader.ReadSingle(), reader.ReadSingle());
float radius = reader.ReadSingle();
Color32 color = new Color32(
reader.ReadByte(),
reader.ReadByte(),
reader.ReadByte(),
255
);
GameObject obj;
if (id == client.ID)
{
obj = Instantiate(controllablePrefab, position, Quaternion.identity) as GameObject;
}
else
{
obj = Instantiate(networkPrefab, position, Quaternion.identity) as GameObject;
}
AgarObject agarObj = obj.GetComponent<AgarObject>();
agarObj.SetRadius(radius);
agarObj.SetColor(color);
}
}
}
}
如果您仔细查看代码,就会发现我们只是反向打包过程:我们从消息中获取一个包含消息数据的读取器,然后简单地循环读取数据,按照我们之前编写消息时的顺序(注意,顺序很重要)读取数据,直到没有更多数据为止。在读取过程中,根据玩家对象的ID,我们创建必要的对象,如果玩家对象的ID是我们的ID(如果是,则应该是我们正在控制的对象)。
将PlayerSpawner组件添加到场景中的Network对象中,并在其相应位置上拖动Client和两个预制件。
最后,从插件中复制Tags文件,使我们的Unity项目中有一个相同的副本。
现在,您应该能够进行测试并在客户端上看到一个玩家生成。下一步是移动!