hytopia-persisted-data
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=productionHYTOPIA_API_KEY– Your API keyHYTOPIA_GAME_ID– Your game IDHYTOPIA_LOBBY_ID– Your lobby ID
Upon deployment, HYTOPIA automatically configures persistence services.
Best Practices
- Save on important actions – Don’t wait for disconnect
- Use auto-save – Backup every few minutes
- Handle missing data – Always provide defaults
- Validate loaded data – Check for corruption/outdated formats
- Batch updates – Don’t save on every tiny change
- Version your data – Include version number for migrations
- 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