通过示例在 Unity 和 NodeJS 上的游戏中创建安全、快速的多人游戏
介绍
规划多人游戏开发方法 - 在整个项目的进一步开发中发挥着最重要的作用之一,因为它包含了我们在创建真正高质量的产品时应该考虑的许多标准。在今天的宣言教程中,我们将看一个方法示例,该方法使我们能够创建真正快速的游戏,同时尊重所有安全和反违规规则。
所以,让我们定义我们的主要标准:
- 多人游戏需要一种特殊的方法来管理网络同步,尤其是在实时情况下。 二进制协议用于加速客户端之间的数据同步,反应字段将有助于以最小的延迟和节省内存来更新玩家位置。
- 服务器权限是一项重要原则,关键数据仅在服务器上处理,确保游戏完整性并防止作弊。然而,为了让我们最大限度地提高性能 - 服务器只进行关键更新,剩下的交给客户端反作弊。
- 实施客户端防欺诈,以便在服务器上不增加负载的情况下处理不太关键的数据。
架构的主要组成部分
- 客户端(Unity):客户端负责显示游戏状态,将玩家操作发送到服务器并从服务器接收更新。这里还使用反应字段来动态更新玩家位置。
- 服务器端(Node.js):服务器处理关键数据(例如,移动、碰撞和玩家动作)并将更新发送到所有连接的客户端。非关键数据可以在客户端上处理并使用服务器转发到其他客户端。
- 二进制协议:二进制数据序列化用于减少传输的数据量并提高性能。
- 同步:提供客户端之间数据的快速同步,以最大程度地减少延迟并确保流畅的游戏体验。
- 客户端反作弊:它用于我们可以在客户端上更改并发送给其他客户端的数据。
第 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); }
此服务器执行以下操作:
- 处理客户端连接。
- 接收二进制格式的玩家移动数据,验证它,更新服务器上的状态并将其发送到所有客户端。
- 以最小延迟同步游戏状态,使用二进制格式来减少数据量。
- 简单地转发来自客户端的不安全数据。
要点:
- 服务器权限:所有重要数据均在服务器上处理和存储。客户端仅发送操作命令(例如,位置变化增量)。
- 二进制数据传输:使用二进制协议可以节省流量并提高网络性能,特别是对于频繁的实时数据交换。
第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:
- Use interpolation: Clients can use interpolation to smooth out movements between updates from the server. This compensates for small network delays.
- 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.
- 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中文网其他相关文章!

热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

Video Face Swap
使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

深入探讨console.log输出差异的根源本文将分析一段代码中console.log函数输出结果的差异,并解释其背后的原因。�...
