ホームページ  >  記事  >  ウェブフロントエンド  >  Unity および NodeJS 上のゲームで安全かつ高速なマルチプレイヤーを作成する例を示します

Unity および NodeJS 上のゲームで安全かつ高速なマルチプレイヤーを作成する例を示します

WBOY
WBOYオリジナル
2024-09-01 21:02:321044ブラウズ

導入

マルチプレイヤー ゲーム開発へのアプローチの計画 - 本当に高品質の製品を作成する際に考慮すべき多くの基準が含まれるため、プロジェクト全体のさらなる開発において最も重要な役割の 1 つを果たします。今日のマニフェスト チュートリアルでは、すべてのセキュリティと不正行為防止ルールを尊重しながら、非常に高速なゲームを作成できるアプローチの例を見ていきます。

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 を使用すると、Web ソケットを使用してクライアントとサーバー間のリアルタイムの双方向通信を簡単に実装できます。

基本的なサーバー実装
クライアント接続を処理し、データを取得し、重要な状態を計算して、すべてのクライアント間でそれらを同期する単純なサーバーを作成しましょう。

// 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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。