hytopia-multiplayer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

HYTOPIA Multiplayer

HYTOPIA 多人游戏功能

This skill helps you implement multiplayer features in HYTOPIA SDK games.
本技能可帮助你在HYTOPIA SDK游戏中实现多人游戏功能。

When to Use This Skill

何时使用本技能

Use this skill when the user:
  • Wants to manage multiple players in a game
  • Needs server-authoritative gameplay mechanics
  • Asks about player data persistence
  • Wants to optimize network performance
  • Needs player authentication or identification
  • Asks about player teams, groups, or parties
当用户有以下需求时,可使用本技能:
  • 希望在游戏中管理多名玩家
  • 需要服务器权威的游戏机制
  • 询问玩家数据持久化相关问题
  • 希望优化网络性能
  • 需要玩家身份验证或识别功能
  • 询问玩家队伍、群组或小队相关内容

Core Multiplayer Concepts

核心多人游戏概念

Player Management

玩家管理

typescript
import { World, Player } from 'hytopia';

// Access all players
const players = world.players;

// Get specific player
const player = world.getPlayer(playerId);

// Iterate over players
for (const player of world.players) {
  player.sendMessage('Hello!');
}

// Count players
const playerCount = world.players.length;
typescript
import { World, Player } from 'hytopia';

// Access all players
const players = world.players;

// Get specific player
const player = world.getPlayer(playerId);

// Iterate over players
for (const player of world.players) {
  player.sendMessage('Hello!');
}

// Count players
const playerCount = world.players.length;

Player Events

玩家事件

typescript
import { World, Player } from 'hytopia';

world.onPlayerJoin = (player: Player) => {
  console.log(`${player.username} joined (${player.id})`);
  
  // Send welcome message
  player.sendMessage(`Welcome ${player.username}!`);
  
  // Broadcast to others
  world.broadcast(`${player.username} has joined the game!`, [player.id]);
  
  // Spawn player at location
  player.setPosition({ x: 0, y: 100, z: 0 });
};

world.onPlayerLeave = (player: Player) => {
  console.log(`${player.username} left`);
  world.broadcast(`${player.username} has left the game.`);
};
typescript
import { World, Player } from 'hytopia';

world.onPlayerJoin = (player: Player) => {
  console.log(`${player.username} joined (${player.id})`);
  
  // Send welcome message
  player.sendMessage(`Welcome ${player.username}!`);
  
  // Broadcast to others
  world.broadcast(`${player.username} has joined the game!`, [player.id]);
  
  // Spawn player at location
  player.setPosition({ x: 0, y: 100, z: 0 });
};

world.onPlayerLeave = (player: Player) => {
  console.log(`${player.username} left`);
  world.broadcast(`${player.username} has left the game.`);
};

Player Data

玩家数据

typescript
import { Player } from 'hytopia';

// Set custom data on player
player.setData('score', 0);
player.setData('kills', 0);
player.setData('inventory', []);

// Get player data
const score = player.getData('score');
const inventory = player.getData('inventory') || [];

// Persist data (saved across sessions)
player.setPersistedData('level', 5);
const level = player.getPersistedData('level');
typescript
import { Player } from 'hytopia';

// Set custom data on player
player.setData('score', 0);
player.setData('kills', 0);
player.setData('inventory', []);

// Get player data
const score = player.getData('score');
const inventory = player.getData('inventory') || [];

// Persist data (saved across sessions)
player.setPersistedData('level', 5);
const level = player.getPersistedData('level');

Server Authority

服务器权威

Server-Authoritative Movement

服务器权威的移动控制

typescript
import { Player } from 'hytopia';

// Server controls all movement - client sends inputs only
player.onInput = (input) => {
  // Process input on server
  if (input.isPressed('w')) {
    // Calculate new position server-side
    const newPosition = calculateMovement(player, input);
    player.setPosition(newPosition);
  }
};

// Never trust client position
// Always validate: check speed, bounds, collision
typescript
import { Player } from 'hytopia';

// Server controls all movement - client sends inputs only
player.onInput = (input) => {
  // Process input on server
  if (input.isPressed('w')) {
    // Calculate new position server-side
    const newPosition = calculateMovement(player, input);
    player.setPosition(newPosition);
  }
};

// Never trust client position
// Always validate: check speed, bounds, collision

State Synchronization

状态同步

typescript
import { Entity } from 'hytopia';

class GameEntity extends Entity {
  // Only sync what needs to be visible
  syncProperties = ['position', 'rotation', 'health'];
  
  tick(deltaTime: number) {
    // Server updates state
    this.updateAI(deltaTime);
    
    // Changes automatically sync to clients
    // for properties in syncProperties
  }
}
typescript
import { Entity } from 'hytopia';

class GameEntity extends Entity {
  // Only sync what needs to be visible
  syncProperties = ['position', 'rotation', 'health'];
  
  tick(deltaTime: number) {
    // Server updates state
    this.updateAI(deltaTime);
    
    // Changes automatically sync to clients
    // for properties in syncProperties
  }
}

Anti-Cheat Basics

反作弊基础

typescript
import { Player } from 'hytopia';

function validatePlayerMovement(player: Player, newPos: Vector3) {
  const oldPos = player.position;
  const distance = oldPos.distance(newPos);
  const maxDistance = player.maxSpeed * deltaTime;
  
  // Check if moved too fast
  if (distance > maxDistance * 1.1) {  // 10% tolerance
    console.warn(`Possible speed hack: ${player.username}`);
    player.setPosition(oldPos);  // Revert
    return false;
  }
  
  // Check if in bounds
  if (!world.isInBounds(newPos)) {
    player.setPosition(oldPos);
    return false;
  }
  
  return true;
}
typescript
import { Player } from 'hytopia';

function validatePlayerMovement(player: Player, newPos: Vector3) {
  const oldPos = player.position;
  const distance = oldPos.distance(newPos);
  const maxDistance = player.maxSpeed * deltaTime;
  
  // Check if moved too fast
  if (distance > maxDistance * 1.1) {  // 10% tolerance
    console.warn(`Possible speed hack: ${player.username}`);
    player.setPosition(oldPos);  // Revert
    return false;
  }
  
  // Check if in bounds
  if (!world.isInBounds(newPos)) {
    player.setPosition(oldPos);
    return false;
  }
  
  return true;
}

Network Optimization

网络优化

Efficient Broadcasting

高效广播

typescript
import { World } from 'hytopia';

// Send to all players
world.broadcast('Game starting!');

// Send to specific players
world.broadcast('Team message', [], [player1.id, player2.id]);

// Send to all except some
world.broadcast('Secret message', [player1.id]);

// Send to nearby players only
function broadcastToNearby(origin: Vector3, message: string, radius: number) {
  for (const player of world.players) {
    if (player.position.distance(origin) <= radius) {
      player.sendMessage(message);
    }
  }
}
typescript
import { World } from 'hytopia';

// Send to all players
world.broadcast('Game starting!');

// Send to specific players
world.broadcast('Team message', [], [player1.id, player2.id]);

// Send to all except some
world.broadcast('Secret message', [player1.id]);

// Send to nearby players only
function broadcastToNearby(origin: Vector3, message: string, radius: number) {
  for (const player of world.players) {
    if (player.position.distance(origin) <= radius) {
      player.sendMessage(message);
    }
  }
}

Property Sync Optimization

属性同步优化

typescript
import { Entity } from 'hytopia';

class OptimizedEntity extends Entity {
  // Only sync when changed
  private _health: number = 100;
  
  get health() { return this._health; }
  set health(value: number) {
    if (this._health !== value) {
      this._health = value;
      this.sync('health', value);  // Manual sync only on change
    }
  }
  
  // Don't sync internal state
  private pathfindingTarget: Vector3;  // Server-only
  private lastUpdate: number;          // Server-only
}
typescript
import { Entity } from 'hytopia';

class OptimizedEntity extends Entity {
  // Only sync when changed
  private _health: number = 100;
  
  get health() { return this._health; }
  set health(value: number) {
    if (this._health !== value) {
      this._health = value;
      this.sync('health', value);  // Manual sync only on change
    }
  }
  
  // Don't sync internal state
  private pathfindingTarget: Vector3;  // Server-only
  private lastUpdate: number;          // Server-only
}

Player Teams/Groups

玩家队伍/群组

typescript
import { Player } from 'hytopia';

// Simple team system
const teams = new Map<string, Player[]>();

function assignTeam(player: Player, teamName: string) {
  // Remove from old team
  const oldTeam = player.getData('team');
  if (oldTeam) {
    const oldPlayers = teams.get(oldTeam) || [];
    teams.set(oldTeam, oldPlayers.filter(p => p.id !== player.id));
  }
  
  // Add to new team
  player.setData('team', teamName);
  const teamPlayers = teams.get(teamName) || [];
  teamPlayers.push(player);
  teams.set(teamName, teamPlayers);
  
  // Notify team
  for (const teammate of teamPlayers) {
    teammate.sendMessage(`${player.username} joined ${teamName}!`);
  }
}

function getTeamPlayers(teamName: string): Player[] {
  return teams.get(teamName) || [];
}
typescript
import { Player } from 'hytopia';

// Simple team system
const teams = new Map<string, Player[]>();

function assignTeam(player: Player, teamName: string) {
  // Remove from old team
  const oldTeam = player.getData('team');
  if (oldTeam) {
    const oldPlayers = teams.get(oldTeam) || [];
    teams.set(oldTeam, oldPlayers.filter(p => p.id !== player.id));
  }
  
  // Add to new team
  player.setData('team', teamName);
  const teamPlayers = teams.get(teamName) || [];
  teamPlayers.push(player);
  teams.set(teamName, teamPlayers);
  
  // Notify team
  for (const teammate of teamPlayers) {
    teammate.sendMessage(`${player.username} joined ${teamName}!`);
  }
}

function getTeamPlayers(teamName: string): Player[] {
  return teams.get(teamName) || [];
}

Best Practices

最佳实践

  1. Server is authoritative - Never trust client data
  2. Validate all inputs - Check bounds, rates, permissions
  3. Sync minimally - Only send what clients need to know
  4. Use spatial partitioning - Don't broadcast to distant players
  5. Rate limit - Prevent spam and DoS
  6. Graceful degradation - Handle lag and packet loss
  1. 服务器拥有权威 - 永远不要信任客户端数据
  2. 验证所有输入 - 检查边界、频率、权限
  3. 最小化同步 - 只发送客户端需要知道的内容
  4. 使用空间分区 - 不要向远距离玩家广播
  5. 速率限制 - 防止垃圾信息和拒绝服务攻击
  6. 优雅降级 - 处理延迟和数据包丢失

Common Patterns

常见模式

Player Spawn System

玩家重生系统

typescript
const spawnPoints = [
  { x: 10, y: 100, z: 10 },
  { x: -10, y: 100, z: 10 },
  { x: 10, y: 100, z: -10 },
  { x: -10, y: 100, z: -10 }
];

function spawnPlayer(player: Player) {
  const spawnIndex = world.players.length % spawnPoints.length;
  player.setPosition(spawnPoints[spawnIndex]);
  player.setHealth(100);
  player.clearInventory();
}
typescript
const spawnPoints = [
  { x: 10, y: 100, z: 10 },
  { x: -10, y: 100, z: 10 },
  { x: 10, y: 100, z: -10 },
  { x: -10, y: 100, z: -10 }
];

function spawnPlayer(player: Player) {
  const spawnIndex = world.players.length % spawnPoints.length;
  player.setPosition(spawnPoints[spawnIndex]);
  player.setHealth(100);
  player.clearInventory();
}

Leaderboard

排行榜

typescript
function updateLeaderboard() {
  const sorted = [...world.players].sort((a, b) => 
    (b.getData('score') || 0) - (a.getData('score') || 0)
  );
  
  const leaderboard = sorted.slice(0, 10).map((p, i) => 
    `${i + 1}. ${p.username}: ${p.getData('score') || 0}`
  ).join('\n');
  
  world.broadcast('=== Leaderboard ===\n' + leaderboard);
}
typescript
function updateLeaderboard() {
  const sorted = [...world.players].sort((a, b) => 
    (b.getData('score') || 0) - (a.getData('score') || 0)
  );
  
  const leaderboard = sorted.slice(0, 10).map((p, i) => 
    `${i + 1}. ${p.username}: ${p.getData('score') || 0}`
  ).join('\n');
  
  world.broadcast('=== Leaderboard ===\n' + leaderboard);
}