>  기사  >  웹 프론트엔드  >  예제를 통해 Unity 및 NodeJS 게임에서 안전하고 빠른 멀티플레이어 만들기

예제를 통해 Unity 및 NodeJS 게임에서 안전하고 빠른 멀티플레이어 만들기

WBOY
WBOY원래의
2024-09-01 21:02:321076검색

소개

멀티플레이어 게임 개발에 대한 접근 방식 계획 - 정말 고품질 제품을 만들 때 고려해야 할 많은 기준이 포함되어 있기 때문에 전체 프로젝트의 추가 개발에서 가장 중요한 역할 중 하나를 수행합니다. 오늘의 선언문 튜토리얼에서는 모든 보안 및 안티칫 규칙을 준수하면서 매우 빠른 게임을 만들 수 있는 접근 방식의 예를 살펴보겠습니다.

Creating safe and fast multiplayer in games on Unity and NodeJS with examples

그러므로 주요 기준을 정의해 보겠습니다.

  1. 멀티플레이어 게임에는 특히 실시간인 경우 네트워크 동기화 관리에 대한 특별한 접근 방식이 필요합니다. 바이너리 프로토콜은 클라이언트 간의 데이터 동기화 속도를 높이는 데 사용되며 반응 필드는 지연 시간을 최소화하고 메모리를 절약하면서 플레이어 위치를 업데이트하는 데 도움이 됩니다.
  2. 서버 권한은 중요한 데이터가 서버에서만 처리되어 게임 무결성을 보장하고 치터로부터 보호하는 중요한 원칙입니다. 그러나 성능을 최대화하기 위해 서버는 중요한 업데이트만 수행하고 나머지는 클라이언트 치트 방지에 맡깁니다.
  3. 서버에 추가 부하 없이 덜 중요한 데이터를 처리하기 위해 클라이언트 안티칫 구현.

Creating safe and fast multiplayer in games on Unity and NodeJS with examples

아키텍처의 주요 구성 요소

  1. 클라이언트 측(Unity): 클라이언트 측에서는 게임 상태를 표시하고, 플레이어 작업을 서버에 보내고, 서버에서 업데이트를 수신하는 역할을 담당합니다. 여기서는 반응형 필드를 사용하여 플레이어 위치를 동적으로 업데이트합니다.
  2. 서버 측(Node.js): 서버는 중요한 데이터(예: 이동, 충돌, 플레이어 동작)를 처리하고 연결된 모든 클라이언트에 업데이트를 보냅니다. 중요하지 않은 데이터는 클라이언트에서 처리하고 서버를 사용하여 다른 클라이언트로 전달할 수 있습니다.
  3. 바이너리 프로토콜: 바이너리 데이터 직렬화는 전송되는 데이터의 양을 줄이고 성능을 향상시키는 데 사용됩니다.
  4. 동기화: 지연 시간을 최소화하고 원활한 게임 플레이를 보장하기 위해 클라이언트 간 빠른 데이터 동기화가 제공됩니다.
  5. 클라이언트 치트 방지: 클라이언트에서 변경하여 다른 클라이언트로 보낼 수 있는 데이터 종류에 사용됩니다.

1단계: Node.js에서 서버 구현

먼저 Node.js에 서버를 설정해야 합니다. 서버는 모든 중요한 계산을 담당하고 업데이트된 데이터를 플레이어에게 전송합니다.

환경 설치
Node.js에서 서버를 생성하려면 필요한 종속성을 설치하세요.

mkdir multiplayer-game-server
cd multiplayer-game-server
npm init -y
npm install socket.io

Socket.io를 사용하면 웹 소켓을 사용하여 클라이언트와 서버 간의 실시간 양방향 통신을 쉽게 구현할 수 있습니다.

기본 서버 구현
클라이언트 연결을 처리하고, 데이터를 검색하고, 중요한 상태를 계산하고, 모든 클라이언트 간에 동기화하는 간단한 서버를 만들어 보겠습니다.

// Create a simple socket IO server
const io = require('socket.io')(3000, {
    cors: {
        origin: '*'
    }
});

// Simple example of game states
let gameState = {};
let playerSpeedConfig = {
    maxX: 1,
    maxY: 1,
    maxZ: 1
};

// Work with new connection
io.on('connection', (socket) => {
    console.log('Player connected:', socket.id);

    // Initialize player state for socket ID
    gameState[socket.id] = { x: 0, y: 0, z: 0 };

    // work with simple player command for movement
    socket.on('playerMove', (data) => {
        const { id, dx, dy, dz } = parsePlayerMove(data);

        // Check Maximal Values
        if(dx > playerSpeedConfig.maxX) dx = playerSpeedConfig.maxX;
        if(dy > playerSpeedConfig.maxY) dx = playerSpeedConfig.maxY;
        if(dz > playerSpeedConfig.maxZ) dx = playerSpeedConfig.maxZ;

        // update game state for current player
        gameState[id].x += dx;
        gameState[id].y += dy;
        gameState[id].z += dz;

        // Send new state for all clients
        const updatedData = serializeGameState(gameState);
        io.emit('gameStateUpdate', updatedData);
    });

    // Work with unsafe data
    socket.on('dataupdate', (data) => {
        const { id, unsafe } = parsePlayerUnsafe(data);

        // update game state for current player
        gameState[id].unsafeValue += unsafe;

        // Send new state for all clients
        const updatedData = serializeGameState(gameState);
        io.emit('gameStateUpdate', updatedData);
    });

    // Work with player disconnection
    socket.on('disconnect', () => {
        console.log('Player disconnected:', socket.id);
        delete gameState[socket.id];
    });
});

// Simple Parse our binary data
function parsePlayerMove(buffer) {
    const id = buffer.toString('utf8', 0, 16); // Player ID (16 bit)
    const dx = buffer.readFloatLE(16);         // Delta X
    const dy = buffer.readFloatLE(20);         // Delta  Y
    const dz = buffer.readFloatLE(24);         // Delta  Z
    return { id, dx, dy, dz };
}

// Simple Parse of unsafe data
function parsePlayerUnsafe(buffer) {
    const id = buffer.toString('utf8', 0, 16); // Player ID (16 bit)
    const unsafe = buffer.readFloatLE(16);     // Unsafe float
    return { id, unsafe };
}

// Simple game state serialization for binary protocol
function serializeGameState(gameState) {
    const buffers = [];
    for (const [id, data] of Object.entries(gameState)) {
        // Player ID
        const idBuffer = Buffer.from(id, 'utf8');

        // Position (critical) Buffer
        const posBuffer = Buffer.alloc(12);
        posBuffer.writeFloatLE(data.x, 0);
        posBuffer.writeFloatLE(data.y, 4);
        posBuffer.writeFloatLE(data.z, 8);

        // Unsafe Data Buffer
        const unsafeBuffer = Buffer.alloc(4);
        unsafeBuffer.writeFloatLE(data.unsafeValue, 0);

        // Join all buffers
        buffers.push(Buffer.concat([idBuffer, posBuffer, unsafeBuffer]));
    }
    return Buffer.concat(buffers);
}

이 서버는 다음을 수행합니다.

  1. 클라이언트 연결을 처리합니다.
  2. 바이너리 형식으로 플레이어 움직임 데이터를 수신하여 유효성을 검사하고 서버의 상태를 업데이트하여 모든 클라이언트에 보냅니다.
  3. 데이터 양을 줄이기 위해 바이너리 형식을 사용하여 대기 시간을 최소화하면서 게임 상태를 동기화합니다.
  4. 클라이언트에서 온 안전하지 않은 데이터를 단순히 전달합니다.

핵심 사항:

  1. 서버 권한: 모든 중요한 데이터는 서버에서 처리 및 저장됩니다. 클라이언트는 작업 명령(예: 위치 변경 델타)만 보냅니다.
  2. 바이너리 데이터 전송: 바이너리 프로토콜을 사용하면 트래픽이 절약되고 특히 빈번한 실시간 데이터 교환의 경우 네트워크 성능이 향상됩니다.

2단계: Unity에서 클라이언트 부분 구현

이제 Unity에서 서버와 상호 작용할 클라이언트 부분을 만들어 보겠습니다.

Unity를 Socket.IO의 서버에 연결하려면 Unity용으로 설계된 라이브러리를 연결해야 합니다.
이 경우 특정 구현에 구속되지 않고(사실 모두 유사함) 추상적인 예를 사용합니다.

Using reactive fields for synchronization
We will use reactive fields to update player positions. This will allow us to update states without having to check the data in each frame via the Update() method. Reactive fields automatically update the visual representation of objects in the game when the state of the data changes.

To get a reactive properties functional you can use UniRx.

Client code on Unity
Let's create a script that will connect to the server, send data and receive updates via reactive fields.

using UnityEngine;
using SocketIOClient;
using UniRx;
using System;
using System.Text;

// Basic Game Client Implementation
public class GameClient : MonoBehaviour
{
    // SocketIO Based Client
    private SocketIO client;

    // Our Player Reactive Position
    public ReactiveProperty<Vector3> playerPosition = new ReactiveProperty<Vector3>(Vector3.zero);

    // Client Initialization
    private void Start()
    {
        // Connect to our server
        client = new SocketIO("http://localhost:3000");

        // Add Client Events
        client.OnConnected += OnConnected;    // On Connected
        client.On("gameStateUpdate", OnGameStateUpdate); // On Game State Changed

        // Connect to Socket Async
        client.ConnectAsync();

        // Subscribe to our player position changed
        playerPosition.Subscribe(newPosition => {
            // Here you can interpolate your position instead
            // to get smooth movement at large ping
            transform.position = newPosition;
        });

        // Add Movement Commands
        Observable.EveryUpdate().Where(_ => Input.GetKey(KeyCode.W)).Subscribe(_ => ProcessInput(true));
        Observable.EveryUpdate().Where(_ => Input.GetKey(KeyCode.S)).Subscribe(_ => ProcessInput(false));
    }

    // On Player Connected
    private async void OnConnected(object sender, EventArgs e)
    {
        Debug.Log("Connected to server!");
    }

    // On Game State Update
    private void OnGameStateUpdate(SocketIOResponse response)
    {
        // Get our binary data
        byte[] data = response.GetValue<byte[]>();

        // Work with binary data
        int offset = 0;
        while (offset < data.Length)
        {
            // Get Player ID
            string playerId = Encoding.UTF8.GetString(data, offset, 16);
            offset += 16;

            // Get Player Position
            float x = BitConverter.ToSingle(data, offset);
            float y = BitConverter.ToSingle(data, offset + 4);
            float z = BitConverter.ToSingle(data, offset + 8);
            offset += 12;

            // Get Player unsafe variable
            float unsafeVariable = BitConverter.ToSingle(data, offset);

            // Check if it's our player position
            if (playerId == client.Id)
                playerPosition.Value = new Vector3(x, y, z);
            else
                UpdateOtherPlayerPosition(playerId, new Vector3(x, y, z), unsafeVariable);
        }
    }

    // Process player input
    private void ProcessInput(bool isForward){
        if (isForward)
            SendMoveData(new Vector3(0, 0, 1)); // Move Forward
        else
            SendMoveData(new Vector3(0, 0, -1)); // Move Backward
    }

    // Send Movement Data
    private async void SendMoveData(Vector3 delta)
    {
        byte[] data = new byte[28];
        Encoding.UTF8.GetBytes(client.Id).CopyTo(data, 0);
        BitConverter.GetBytes(delta.x).CopyTo(data, 16);
        BitConverter.GetBytes(delta.y).CopyTo(data, 20);
        BitConverter.GetBytes(delta.z).CopyTo(data, 24);

        await client.EmitAsync("playerMove", data);
    }

    // Send any unsafe data
    private async void SendUnsafeData(float unsafeData){
        byte[] data = new byte[20];
        Encoding.UTF8.GetBytes(client.Id).CopyTo(data, 0);
        BitConverter.GetBytes(unsafeData).CopyTo(data, 16);
        await client.EmitAsync("dataUpdate", data);
    }

    // Update Other players position
    private void UpdateOtherPlayerPosition(string playerId, Vector3 newPosition, float unsafeVariable)
    {
        // Here we can update other player positions and variables
    }

    // On Client Object Destroyed
    private void OnDestroy()
    {
        client.DisconnectAsync();
    }
}

Step 3: Optimize synchronization and performance

To ensure smooth gameplay and minimize latency during synchronization, it is recommended:

  1. Use interpolation: Clients can use interpolation to smooth out movements between updates from the server. This compensates for small network delays.
  2. Batch data sending: Instead of sending data on a per-move basis, use batch sending. For example, send updates every few milliseconds, which will reduce network load.
  3. Reduce the frequency of updates: Reduce the frequency of sending data to a reasonable minimum. For example, updating 20-30 times per second may be sufficient for most games.

How to simplify working with the binary protocol?

In order to simplify your work with a binary protocol - create a basic principle of data processing, as well as schemes of interaction with it.

For our example, we can take a basic protocol where:
1) The first 4 bits are the maxa of the request the user is making (e.g. 0 - move player, 1 - shoot, etc.);
2) The next 16 bits are the ID of our client.
3) Next we fill in the data that is passed through the loop (some Net Variables), where we store the ID of the variable, the size of the offset in bytes to the beginning of the next variable, the type of the variable and its value.

For the convenience of version and data control - we can create a client-server communication schema in a convenient format (JSON / XML) and download it once from the server to further parse our binary data according to this schema for the required version of our API.

Client Anti-Cheat

It doesn't make sense to process every data on the server, some of them are easier to modify on the client side and just send to other clients.

To make you a bit more secure in this scheme - you can use client-side anti-chit system to prevent memory hacks - for example, my GameShield - a free open source solution.

Conclusion

We took a simple example of developing a multiplayer game on Unity with a Node.js server, where all critical data is handled on the server to ensure the integrity of the game. Using a binary protocol to transfer data helps optimize traffic, and reactive programming in Unity makes it easy to synchronize client state without having to use the Update() method.

This approach not only improves game performance, but also increases protection against cheating by ensuring that all key calculations are performed on the server rather than the client.

And of course, as always thank you for reading the article. If you still have any questions or need help in organizing your architecture for multiplayer project - I invite you to my Discord


You can also help me out a lot in my plight and support the release of new articles and free for everyone libraries and assets for developers:

My Discord | My Blog | My GitHub

BTC: bc1qef2d34r4xkrm48zknjdjt7c0ea92ay9m2a7q55

ETH: 0x1112a2Ef850711DF4dE9c432376F255f416ef5d0
USDT (TRC20): TRf7SLi6trtNAU6K3pvVY61bzQkhxDcRLC

위 내용은 예제를 통해 Unity 및 NodeJS 게임에서 안전하고 빠른 멀티플레이어 만들기의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.