hytopia-persisted-data

📁 abstrucked/hytopia-skills 📅 10 days ago
1
总安装量
1
周安装量
#49679
全站排名
安装命令
npx skills add https://github.com/abstrucked/hytopia-skills --skill hytopia-persisted-data

Agent 安装分布

opencode 1
claude-code 1

Skill 文档

HYTOPIA Persisted Data

This skill helps you save and load persistent data in HYTOPIA SDK games.

Documentation: https://dev.hytopia.com/sdk-guides/persisted-data

When to Use This Skill

Use this skill when the user:

  • Wants to save player progress between sessions
  • Needs to create leaderboards or high scores
  • Asks about storing game configuration
  • Wants persistent inventories or unlocks
  • Needs to save world state
  • Asks about data persistence across server restarts

Data Types

Type Scope Use Cases
Global Data All game instances Leaderboards, game config, shared state
Player Data Per player Progress, inventory, stats, preferences

Global Data

Shared across all running game instances.

Set Global Data

import { PersistenceManager } from 'hytopia';

// Save global data
await PersistenceManager.instance.setGlobalData('leaderboard', [
  { name: 'Player1', score: 1000 },
  { name: 'Player2', score: 950 },
  { name: 'Player3', score: 900 }
]);

// Save game config
await PersistenceManager.instance.setGlobalData('game-config', {
  maxPlayers: 20,
  roundDuration: 300,
  difficulty: 'hard'
});

Get Global Data

import { PersistenceManager } from 'hytopia';

// Get leaderboard
const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard');
console.log('Top scores:', leaderboard);

// Get with default
const config = await PersistenceManager.instance.getGlobalData('game-config');
if (!config) {
  // Use defaults
}

Player Data

Persisted per player across sessions.

Set Player Data

import { Player } from 'hytopia';

// Save player progress
await player.setPersistedData('level', 15);
await player.setPersistedData('xp', 2500);
await player.setPersistedData('gold', 1000);

// Save complex data
await player.setPersistedData('inventory', [
  { id: 'sword', quantity: 1 },
  { id: 'potion', quantity: 5 },
  { id: 'key', quantity: 3 }
]);

await player.setPersistedData('unlocks', {
  skins: ['default', 'warrior', 'mage'],
  maps: ['forest', 'desert'],
  achievements: ['first-kill', 'speedrun']
});

Get Player Data

import { Player } from 'hytopia';

// Get player progress
const level = await player.getPersistedData('level') || 1;
const xp = await player.getPersistedData('xp') || 0;
const gold = await player.getPersistedData('gold') || 100;

// Get inventory
const inventory = await player.getPersistedData('inventory') || [];

// Load player on join
world.onPlayerJoin = async (player) => {
  const savedData = await player.getPersistedData('progress');

  if (savedData) {
    player.setData('level', savedData.level);
    player.setData('xp', savedData.xp);
    player.setHealth(savedData.health);
    console.log(`Loaded ${player.username}'s progress`);
  } else {
    // New player - set defaults
    player.setData('level', 1);
    player.setData('xp', 0);
    console.log(`New player: ${player.username}`);
  }
};

Shallow Merging

When updating object data, HYTOPIA performs shallow merging at the root level.

// Initial data
await player.setPersistedData('stats', {
  kills: 10,
  deaths: 5,
  playtime: 3600
});

// Update only kills - other fields preserved
await player.setPersistedData('stats', {
  kills: 15
});
// Result: { kills: 15, deaths: 5, playtime: 3600 }

// WARNING: Nested objects are replaced entirely
await player.setPersistedData('settings', {
  audio: { music: 0.5, sfx: 1.0 },
  video: { quality: 'high' }
});

await player.setPersistedData('settings', {
  audio: { music: 0.3 }  // sfx is LOST!
});
// Result: { audio: { music: 0.3 }, video: { quality: 'high' } }

Safe Nested Updates

// Always fetch, modify, and save for nested data
async function updateNestedSetting(player: Player, path: string, value: any) {
  const settings = await player.getPersistedData('settings') || {};

  // Deep update
  const keys = path.split('.');
  let obj = settings;
  for (let i = 0; i < keys.length - 1; i++) {
    obj[keys[i]] = obj[keys[i]] || {};
    obj = obj[keys[i]];
  }
  obj[keys[keys.length - 1]] = value;

  await player.setPersistedData('settings', settings);
}

// Usage
await updateNestedSetting(player, 'audio.music', 0.3);

Common Patterns

Leaderboard System

import { PersistenceManager } from 'hytopia';

async function updateLeaderboard(playerName: string, score: number) {
  // Get current leaderboard
  const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard') || [];

  // Check if player already on board
  const existingIndex = leaderboard.findIndex(e => e.name === playerName);

  if (existingIndex !== -1) {
    // Update if new score is higher
    if (score > leaderboard[existingIndex].score) {
      leaderboard[existingIndex].score = score;
    }
  } else {
    // Add new entry
    leaderboard.push({ name: playerName, score });
  }

  // Sort and keep top 100
  leaderboard.sort((a, b) => b.score - a.score);
  const top100 = leaderboard.slice(0, 100);

  await PersistenceManager.instance.setGlobalData('leaderboard', top100);
  return top100;
}

async function getLeaderboard(limit: number = 10) {
  const leaderboard = await PersistenceManager.instance.getGlobalData('leaderboard') || [];
  return leaderboard.slice(0, limit);
}

Auto-Save System

class AutoSave {
  private saveInterval: number = 60000;  // 1 minute
  private dirty: Set<string> = new Set();

  constructor() {
    setInterval(() => this.saveAll(), this.saveInterval);
  }

  markDirty(playerId: string) {
    this.dirty.add(playerId);
  }

  async saveAll() {
    for (const playerId of this.dirty) {
      const player = world.getPlayer(playerId);
      if (player) {
        await this.savePlayer(player);
      }
    }
    this.dirty.clear();
    console.log(`Auto-saved ${this.dirty.size} players`);
  }

  async savePlayer(player: Player) {
    await player.setPersistedData('progress', {
      level: player.getData('level'),
      xp: player.getData('xp'),
      gold: player.getData('gold'),
      inventory: player.getData('inventory'),
      lastSaved: Date.now()
    });
  }
}

const autoSave = new AutoSave();

// Mark player dirty when they change
function addXP(player: Player, amount: number) {
  const currentXP = player.getData('xp') || 0;
  player.setData('xp', currentXP + amount);
  autoSave.markDirty(player.id);
}

Save on Disconnect

world.onPlayerLeave = async (player) => {
  // Save all player data before they leave
  await player.setPersistedData('progress', {
    level: player.getData('level'),
    xp: player.getData('xp'),
    position: player.position,
    inventory: player.getData('inventory'),
    lastPlayed: Date.now()
  });

  console.log(`Saved ${player.username}'s progress`);
};

Unlockables System

async function unlockItem(player: Player, category: string, itemId: string) {
  const unlocks = await player.getPersistedData('unlocks') || {};

  if (!unlocks[category]) {
    unlocks[category] = [];
  }

  if (!unlocks[category].includes(itemId)) {
    unlocks[category].push(itemId);
    await player.setPersistedData('unlocks', unlocks);
    player.sendMessage(`Unlocked: ${itemId}!`);
    return true;
  }

  return false;  // Already unlocked
}

async function hasUnlock(player: Player, category: string, itemId: string) {
  const unlocks = await player.getPersistedData('unlocks') || {};
  return unlocks[category]?.includes(itemId) || false;
}

// Usage
await unlockItem(player, 'skins', 'golden-armor');
const hasSkin = await hasUnlock(player, 'skins', 'golden-armor');

Environment Configuration

Local Development

Data persists in auto-generated dev/ directory between restarts.

Tip: Use single browser tabs during local testing – player IDs are assigned sequentially starting at 1 after server restarts.

Production

Set environment variables:

  • NODE_ENV=production
  • HYTOPIA_API_KEY – Your API key
  • HYTOPIA_GAME_ID – Your game ID
  • HYTOPIA_LOBBY_ID – Your lobby ID

Upon deployment, HYTOPIA automatically configures persistence services.

Best Practices

  1. Save on important actions – Don’t wait for disconnect
  2. Use auto-save – Backup every few minutes
  3. Handle missing data – Always provide defaults
  4. Validate loaded data – Check for corruption/outdated formats
  5. Batch updates – Don’t save on every tiny change
  6. Version your data – Include version number for migrations
  7. Test with fresh data – Delete dev/ folder to test new player flow

Data Migration Example

const CURRENT_VERSION = 2;

async function loadPlayerData(player: Player) {
  const data = await player.getPersistedData('progress');

  if (!data) {
    return getDefaultData();
  }

  // Migrate old data formats
  if (!data.version || data.version < CURRENT_VERSION) {
    const migrated = migrateData(data);
    await player.setPersistedData('progress', migrated);
    return migrated;
  }

  return data;
}

function migrateData(data: any) {
  let migrated = { ...data };

  // v1 -> v2: inventory format changed
  if (!data.version || data.version < 2) {
    if (Array.isArray(data.inventory)) {
      migrated.inventory = data.inventory.map(item =>
        typeof item === 'string'
          ? { id: item, quantity: 1 }
          : item
      );
    }
  }

  migrated.version = CURRENT_VERSION;
  return migrated;
}

Common Mistakes

  • Not handling null/undefined when data doesn’t exist
  • Saving too frequently (causes performance issues)
  • Not saving before player disconnects
  • Forgetting shallow merge behavior for nested objects
  • Not testing with fresh player data