LiteNetLib 시작

56256 단어 csharptutorial
라이브러리에 튜토리얼이나 문서가 많지 않기 때문에, 가이드를 작성하기로 결정했습니다. 기본 다중 게임을 설정할 수 있습니다.
익숙하지 않다면, LiteNetLib은 게임 개발에 사용되는 경량급 C#네트워크 라이브러리로 신뢰할 수 있는 UDP 프로토콜을 실현합니다.그것은 이미 상업 게임에 성공적으로 사용되었다. 예를 들어 LiteNetLib.
본고의 코드 세션은 Godot을 사용하지만, 코드를 Unity나 다른 플랫폼으로 쉽게 개편할 수 있습니다.

7일 남았어 클라이언트 및 서버


클라이언트와 서버의 기본 클래스부터 시작합시다.만약 Godot을 사용한다면, 클라이언트를 자동으로 불러오는 데 추가해서 장면 사이에서 불러오는 상태를 유지해야 할 수도 있습니다.
서버 클래스를 위한 단독 장면을 만들어야 합니다.CLI Godot은 게임을 동시에 실행하는 두 가지 인스턴스에 사용할 수 있습니다.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 에 전달할 수 있습니다.
  • 이러한 방법은 모두 리셋이기 때문에 현재 그것들을 대부분 비워 둘 수 있다.
  • 서버와 달리 바로 시작하지 않기 때문에 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.Connectconnection.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.이것은 매우 유용하다. 왜냐하면 당신은 구조의 복제 의미를 얻을 수 있기 때문이다.
    그러나 우리는 여전히 약간의 물건이 부족하다.PlayerStatereader.GetVector2 함수가 실제로 존재하지 않는다는 것을 알 수 있습니다.기본적으로 LiteNetLib은 대부분의 기본 데이터 형식을 서열화할 수 있지만, Godotwriter.Put(Vector2)은 하나의 구조이기 때문에 할 수 없습니다.Vector2NetDataWriter 확장 방법:
    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);
            }
        }
    
    참고 사항:
  • NetDataReader 새로운 실례를 만드는 것이 아니라 같은 패키지 클래스의 실례를 다시 사용할 것입니다. 따라서 그 실례나 내용에 대한 인용을 저장하지 마십시오!이것은 제작packetProcessor.SubscribeReusable 구조의 또 다른 장점이기 때문에 우리는 그것을 쉽게 복제할 수 있다.
  • 이전에 PlayerState 를 사용하여 구조를 정의한 경우 INetSerializable 를 사용하여 등록해야 합니다.
  • LiteNetLib에는 두 가지 다른 패킷 전달 방법이 있습니다.대부분의 경우 packetProcessor.RegisterNestedType<YourType> (가장 안전한 것) 을 사용하고 싶지만, 빠른 업데이트는 DeliveryMethod.ReliableOrdered 를 사용해야 합니다. 그 중 한두 개는 버려진 패키지가 중요하지 않습니다.
  • 패킷 리셋에 사용할 수 있도록 두 번째 매개 변수로 원하는 것을 전달할 수 있습니다.이런 상황에서 우리는 동행만 하면 된다.
  • 만약 네가 지금 게임을 테스트한다면, 모든 것이 정상적이어야 한다.클라이언트는 DeliveryMethod.Unreliable, 서버는 적당한 packetProcessor.ReadAllPackets로 응답합니다.

    설명 파일 예 재생기 업데이트


    인터넷을 통해 유저 정보를 보내기 위해서 우리는 우선 유저 클래스가 필요하다.너는 이미 너의 게임이 하나 있을 것이다.
    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();
        }
    }
    
    우리는 인터넷을 통해 발송할 것이기 때문에JoinPacket, 우리는 그것을 구조로 전환하고 실현할 것이다JoinAcceptPacket.
    이제 이러한 패킷을 보내려면 다음과 같이 하십시오.
        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은 존재하지 않는 노드에 접근해서 게임을 붕괴시킵니다.간단하게 보기 위해서, 이 검사들은 이미 예시 코드에서 배제되었다.
  • 만약 당신이 지금 게임을 실행하고 있다면, 다른 관련 유저들이 당신과 함께 이동하는 것을 볼 수 있을 것입니다.

    계속 전진하다


    이것이 바로 Lite NetLib을 사용하여 멀티플레이어 게임을 개발하는 데 필요한 모든 것이다.이 안내서는 사기나 클라이언트 예측을 방지하기 위해 권위 있는 서버를 만드는 등 더 높은 주제를 포함하지 않지만, 이러한 주제는 이 코드가 제공하는 토대에서 실현될 수 있다.
    더욱 복잡한 기능의 예시를 에서 찾을 수 있습니다. 이것은 제가 LiteNetLib를 배우고 이 모든 내용을 작성하는 자원 중 하나이기 때문에 통독을 권장합니다.
    LiteNetLib의 소스 코드는 대부분의 함수에 대해 문서 주석이 있기 때문에 적어도 한 번 보는 것은 좋은 생각이다.여기에는 이 라이브러리의 많은 기능과 특성을 소개하지 않았다.
    본문이 당신에게 도움이 되기를 바랍니다.읽어주셔서 감사합니다!

    좋은 웹페이지 즐겨찾기