C# DarkRift 游戏服务端框架教程 03 生成玩家

第一个网络代码

生成玩家

首先,我们需要在有人登录时让玩家出现。我们需要确保登录的人收到当前在线玩家列表,并确保其他所有人在他们的游戏中生成我们的新玩家。

但是让我们先退一步,规划一下我们需要存储有关玩家的内容:

  • 它们在游戏世界中的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项目中有一个相同的副本。

现在,您应该能够进行测试并在客户端上看到一个玩家生成。下一步是移动!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值