skills/abstrucked/hytopia-skills/hytopia-multiplayer

hytopia-multiplayer

SKILL.md

HYTOPIA Multiplayer

This skill helps you implement multiplayer features in HYTOPIA SDK games.

Documentation: https://dev.hytopia.com/sdk-guides/multiplayer

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

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

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

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

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

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

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

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

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

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

Common Patterns

Player Spawn System

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

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);
}
Weekly Installs
5
First Seen
Feb 2, 2026
Installed on
opencode5
gemini-cli4
claude-code4
github-copilot4
codex4
cursor4