litenetlibで始まる


図書館には多くのチュートリアルやドキュメンテーションがないので、設定を始めるためのガイドを書くことにしましたLiteNetLib 基本的なマルチプレイヤーゲーム.
あなたはそれに精通していない場合は、LitenetLibは、信頼性の高いUDPプロトコルを実装するゲーム開発のための軽量Cの経典ネットワークライブラリです.これは、商業ゲームで使用されている7 Days to Die .
この記事のコードスニペットはGodotを使用しますが、Unityのためのコードまたはあなたが使用しているどんな他のプラットホームでも簡単に適応できます.

クライアントとサーバ


クライアントとサーバの基本クラスから始めましょう.Godotを使用している場合は、autoloadにクライアントを追加して、シーン間でロードし続ける必要があります.
また、サーバークラスの別のシーンを作成します.CLI GODOTは同時にあなたのゲームの2つのインスタンスを実行するために使用することができます.cd プロジェクトのディレクトリにgodot-mono scenes/Server.tscn .
using Godot;
using System;
using LiteNetLib;
using LiteNetLib.Utils;

public class Client : Node, INetEventListener {
    private NetManager client;

    public void Connect() {
        client = new NetManager(this) {
            AutoRecycle = true,
        };
    }

    public override void _Process(float delta) {
        if (client != null) {
            client.PollEvents();
        }
    }

    // ... INetEventListener methods omitted
}
using Godot;
using System;
using LiteNetLib;
using LiteNetLib.Utils;

public class Server : Node, INetEventListener {
    private NetManager server;

    public override void _Ready() {
        server = new NetManager(this) {
            AutoRecycle = true,
        };
    }

    public override void _Process(float delta) {
        server.PollEvents();
    }

    // ... INetEventListener methods omitted
}
注意事項
  • 私は実装INetEventListener クラス自体です.うまくいけば、コードエディターは自動的にインタフェース実装を生成することができますEventBasedNetListener そして、NetManager 代わりにREADME example .
  • すべてのコールバックとして、今のところほとんどメソッドを空に保つことができます.
  • もしclient != null 通常、サーバーとは異なり、すぐに起動されません.
  • 次のステップはクライアントをサーバに接続します.
        private NetPeer server;
    
        public void Connect() {
            // ...
            client.Start();
            GD.Print("Connecting to server");
            client.Connect("localhost", 12345, "");
        }
    
        public void OnPeerConnected(NetPeer peer) {
            GD.Print("Connected to server");
            server = peer;
        }
    
        public override void _Ready() {
            // ...
            GD.Print("Starting server");
            server.Start(12345);
        }
    
        public void OnConnectionRequest(ConnectionRequest request) {
            GD.Print($"Incoming connection from {request.RemoteEndPoint.ToString()}");
            request.Accept();
        }
    
    注意事項
  • おそらく最終的にサーバーに接続できるピアの数を制限する必要があります.これはチェックで行うことができますserver.ConnectedPeerCounts 必要に応じて要求を拒否する.
  • サーバーへの「パスワード」(接続キー)を加えることができます.そして、それは時代遅れのクライアントがサーバーに加わるのを妨げるプレーヤーのようなものに役立ちます.のパラメータを見てくださいclient.Connect and connection.AcceptIfKey .
  • パケットとの通信


    クライアントとサーバ間で通信するには、まずいくつかのパケットを定義しなければなりません.
    public class JoinPacket {
        public string username { get; set; }
    }
    
    public class JoinAcceptPacket {
        public PlayerState state { get; set; }
    }
    
    public struct PlayerState : INetSerializable {
        public uint pid;
        public Vector2 position;
    
        public void Serialize(NetDataWriter writer) {
            writer.Put(pid);
            writer.Put(position);
        }
    
        public void Deserialize(NetDataReader reader) {
            pid = reader.GetUInt();
            position = reader.GetVector2();
        }
    }
    
    public class ClientPlayer {
        public PlayerState state;
        public string username;
    }
    
    public class ServerPlayer {
        public NetPeer peer;
        public PlayerState state;
        public string username;
    }
    
    パケットクラスは、litenetlibによって自動的にシリアル化されます.ただし、プロパティは{ get; set; } 必要です.代わりに構造体を定義し、実装することもできますINetSerializable 手動でPlayerState . これは、構造体のコピーセマンティクスを取得するので有用です.
    しかしながら、我々はまだ何かを逃しています.あなたは気づいたかもしれないreader.GetVector2 and writer.Put(Vector2) 関数は実際には存在しません.LitenetLibはデフォルトで最も基本的なデータ型をシリアル化することができますが、GodotのVector2 , それがstructであるので.拡張メソッドを実装しましょうNetDataWriter and NetDataReader :
    public static class SerializingExtensions {
        public static void Put(this NetDataWriter writer, Vector2 vector) {
            writer.Put(vector.x);
            writer.Put(vector.y);
        }
    
        public static Vector2 GetVector2(this NetDataReader reader) {
            return new Vector2(reader.GetFloat(), reader.GetFloat());
        }
    }
    
    ではパケットを送信しましょう.
        private NetDataWriter writer;
        private NetPacketProcessor packetProcessor;
        private ClientPlayer player = new ClientPlayer();
    
        public void Connect(string username) {
            player.username = username;
            writer = new NetDataWriter();
            packetProcessor = new NetPacketProcessor();
            packetProcessor.RegisterNestedType((w, v) => w.Put(v), reader => reader.GetVector2());
            packetProcessor.RegisterNestedType<PlayerState>();
            packetProcessor.SubscribeReusable<JoinAcceptPacket>(OnJoinAccept);
            // ...
        }
    
        public void SendPacket<T>(T packet, DeliveryMethod deliveryMethod) where T : class, new() {
            if (server != null) {
                writer.Reset();
                packetProcessor.Write(writer, packet);
                server.Send(writer, deliveryMethod);
            }
        }
    
        public void OnJoinAccept(JoinAcceptPacket packet) {
            GD.Print($"Join accepted by server (pid: {packet.state.pid})");
            player.state = packet.state;
        }
    
        public void OnPeerConnected(NetPeer peer) {
            // ...
            SendPacket(new JoinPacket { username = player.username }, DeliveryMethod.ReliableOrdered);
        }
    
        public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
            packetProcessor.ReadAllPackets(reader);
        }
    
        [Export] public Vector2 initialPosition = new Vector2();
        private NetDataWriter writer;
        private NetPacketProcessor packetProcessor;
        private Dictionary<uint, ServerPlayer> players = new Dictionary<uint, ServerPlayer>();
    
        public override void _Ready() {
            writer = new NetDataWriter();
            packetProcessor = new NetPacketProcessor();
            packetProcessor.RegisterNestedType((w, v) => w.Put(v), reader => reader.GetVector2());
            packetProcessor.RegisterNestedType<PlayerState>();
            packetProcessor.SubscribeReusable<JoinPacket, NetPeer>(OnJoinReceived);
            // ...
        }
    
        public void SendPacket<T>(T packet, NetPeer peer, DeliveryMethod deliveryMethod) where T : class, new() {
            if (peer != null) {
                writer.Reset();
                packetProcessor.Write(writer, packet);
                peer.Send(writer, deliveryMethod);
            }
        }
    
        public void OnJoinReceived(JoinPacket packet, NetPeer peer) {
            GD.Print($"Received join from {packet.username} (pid: {(uint)peer.Id})");
    
            ServerPlayer newPlayer = (players[(uint)peer.Id] = new ServerPlayer {
                peer = peer,
                state = new PlayerState {
                    pid = (uint)peer.Id,
                    position = initialPosition,
                },
                username = packet.username,
            });
    
            SendPacket(new JoinAcceptPacket { state = newPlayer.state }, peer, DeliveryMethod.ReliableOrdered);
        }
    
        public void OnNetworkReceive(NetPeer peer, NetPacketReader reader, DeliveryMethod deliveryMethod) {
            packetProcessor.ReadAllPackets(reader, peer);
        }
    
        public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) {
            if (peer.Tag != null) {
                players.Remove((uint)peer.Id);
            }
        }
    
    注意事項
  • packetProcessor.SubscribeReusable は新しいパケットを作成するのではなく、同じパケットクラスインスタンスを再利用するので、その内容やその内容への参照を格納しないようにしてください!これは製造の別の利点ですPlayerState 構造体、簡単にコピーできるように.
  • あなたが以前にstructを定義したならばINetSerializable , あなたはそれを使用して登録する必要がありますpacketProcessor.RegisterNestedType<YourType> .
  • LitenetLibは、カップルの異なるパケット配信方法を持っています.あなたは、使いたいですDeliveryMethod.ReliableOrdered ほとんどの物についてはDeliveryMethod.Unreliable 高速更新の場合、ドロップパケットや2つの問題はあまりにも問題はない.
  • あなたは2番目の引数として何を渡すことができますpacketProcessor.ReadAllPackets を使う.この場合、我々は同輩を必要とするだけです.
  • あなたが今ゲームをテストする場合は、すべてうまくいくはずです.クライアントはAを送信しますJoinPacket そして、サーバは適切に対応するでしょうJoinAcceptPacket .

    選手の更新


    ネットワーク上でプレイヤー情報を送信するには、まずプレイヤークラスが必要です.あなたはすでにあなたのゲームのいずれかを持っている必要があります.
    public class Player : Node2D {
        [Export] public float moveSpeed = 200;
        public static Player instance;
    
        public override void _Ready() {
            instance = this;
        }
    
        public override void _Process(float delta) {
            Vector2 velocity = new Vector2();
    
            if (Input.IsActionPressed("ui_left")) velocity.x -= 1;
            if (Input.IsActionPressed("ui_right")) velocity.x += 1;
            if (Input.IsActionPressed("ui_up")) velocity.y -= 1;
            if (Input.IsActionPressed("ui_down")) velocity.y += 1;
    
            Position += velocity * moveSpeed * delta;
        }
    }
    
    どんな些細なゲームでも、クライアントプレイヤーのすべての処理ロジックを必要としないので、他のプレイヤーを代表する別のクラスを必要とするでしょう.
    public class RemotePlayer : Node2D {}
    
    ではもう少しパケットを定義しましょう.
    public class PlayerSendUpdatePacket {
        public Vector2 position { get; set; }
    }
    
    public class PlayerReceiveUpdatePacket {
        public PlayerState[] states { get; set; }
    }
    
    public class PlayerJoinedGamePacket {
        public ClientPlayer player { get; set; }
    }
    
    public class PlayerLeftGamePacket {
        public uint pid { get; set; }
    }
    
    // ...
    
    public struct ClientPlayer : INetSerializable {
        public PlayerState state;
        public string username;
    
        public void Serialize(NetDataWriter writer) {
            state.Serialize(writer);
            writer.Put(username);
        }
    
        public void Deserialize(NetDataReader reader) {
            state.Deserialize(reader);
            username = reader.GetString();
        }
    }
    
    我々は送信されるのでClientPlayer ネットワーク上で、我々はそれを構造体と実装に変えますINetSerializable .
    では、これらのパケットを送信します.
        public override void Connect() {
            // ...
            packetProcessor.RegisterNestedType<ClientPlayer>();
            packetProcessor.SubscribeReusable<PlayerReceiveUpdatePacket>(OnReceiveUpdate);
            packetProcessor.SubscribeReusable<PlayerJoinedGamePacket>(OnPlayerJoin);
            packetProcessor.SubscribeReusable<PlayerLeftGamePacket>(OnPlayerLeave);
            // ...
        }
    
        public override void _Process(float delta) {
            if (client != null) {
                client.PollEvents();
                if (Player.instance != null) {
                    SendPacket(new PlayerSendUpdatePacket { position = Player.instance.Position }, DeliveryMethod.Unreliable);
                }
            }
        }
    
        public void OnJoinAccept(JoinAcceptPacket packet) {
            // ...
            Player.instance.Position = player.state.position;
        }
    
        public void OnReceiveUpdate(PlayerReceiveUpdatePacket packet) {
            foreach (PlayerState state in packet.states) {
                if (state.pid == player.state.pid) {
                    continue;
                }
    
                ((RemotePlayer)Player.instance.GetParent().GetNode(state.pid.ToString())).Position = state.position;
            }
        }
    
        public void OnPlayerJoin(PlayerJoinedGamePacket packet) {
            GD.Print($"Player '{packet.player.username}' (pid: {packet.player.state.pid}) joined the game");
            RemotePlayer remote = (RemotePlayer)((PackedScene)GD.Load("res://scenes/RemotePlayer.tscn")).Instance();
            remote.Name = packet.player.state.pid.ToString();
            remote.Position = packet.player.state.position;
            Player.instance.GetParent().AddChild(remote);
        }
    
        public void OnPlayerLeave(PlayerLeftGamePacket packet) {
            GD.Print($"Player (pid: {packet.pid}) left the game");
            ((RemotePlayer)Player.instance.GetParent().GetNode(packet.pid.ToString())).QueueFree();
        }
    
        public override void _Ready() {
            // ...
            packetProcessor.RegisterNestedType<ClientPlayer>();
            packetProcessor.SubscribeReusable<PlayerSendUpdatePacket, NetPeer>(OnPlayerUpdate);
            // ...
        }
    
        public override void _Process(float delta) {
            // ...
            PlayerState[] states = players.Values.Select(p => p.state).ToArray();
            foreach (ServerPlayer player in players.Values) {
                SendPacket(new PlayerReceiveUpdatePacket { states = states }, player.peer, DeliveryMethod.Unreliable);
            }
        }
    
        public void OnJoinReceived(JoinPacket packet, NetPeer peer) {
            // ...
            foreach (ServerPlayer player in players.Values) {
                if (player.state.pid != newPlayer.state.pid) {
                    SendPacket(new PlayerJoinedGamePacket {
                        player = new ClientPlayer {
                            username = newPlayer.username,
                            state = newPlayer.state,
                        },
                    }, player.peer, DeliveryMethod.ReliableOrdered);
    
                    SendPacket(new PlayerJoinedGamePacket {
                        player = new ClientPlayer {
                            username = player.username,
                            state = player.state,
                        },
                    }, newPlayer.peer, DeliveryMethod.ReliableOrdered);
                }
            }
        }
    
        public void OnPlayerUpdate(PlayerSendUpdatePacket packet, NetPeer peer) {
            players[(uint)peer.Id].state.position = packet.position;
        }
    
        public void OnPeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo) {
            GD.Print($"Player (pid: {(uint)peer.Id}) left the game");
            if (peer.Tag != null) {
                ServerPlayer playerLeft;
                if (players.TryGetValue(((uint)peer.Id), out playerLeft)) {
                    foreach (ServerPlayer player in players.Values) {
                        if (player.state.pid != playerLeft.state.pid) {
                            SendPacket(new PlayerLeftGamePacket { pid = playerLeft.state.pid }, player.peer, DeliveryMethod.ReliableOrdered);
                        }
                    }
                    players.Remove((uint)peer.Id);
                }
            }
        }
    
    注意事項
  • サーバから更新を送信するためのコードを、すべてのフレームで行うのではなく、タイマー上に置くべきでしょう.20秒から30回の更新は、ほとんどの場合に十分です.これは一般的にクライアントよりも重要ではありません.一般的にサーバよりも高速な更新を行うことができます.
  • サーバから受信したデータの一部を確認するのに便利かもしれません.たとえば、クライアントが無効なPIDを与えると、Godotは存在しないノードにアクセスしようとします.それらのチェックは、簡単のための例コードから除外されました.
  • あなたが今ゲームを実行する場合は、他の接続された選手があなたと一緒に移動を参照してくださいする必要があります.

    前進


    それはあなたがlitenetlibとマルチプレイヤーゲームを開発を開始するために必要なすべてについてです.このガイドでは、不正行為やクライアント側の予測を防ぐために権威のあるサーバーを作成するなどのより高度なトピックをカバーしていませんが、これらのコードはここでコードが提供する上に実装することができます.
    より複雑な機能の例の実装はNetGameExample , それは私がlitenetlibを学んで、これの全てを書くのに用いられた資源のうちの1つであったので、私もあなたを読むことを奨励します.
    litenetlibのソースコードにはほとんどの機能に対するdocコメントがありますので、少なくとも見てみてください.ここではカバーされていないライブラリの多くの機能と機能があります.
    うまくいけば、この記事はあなたに役に立ちました.読書ありがとう!