关于 data-manager
介绍 Phaser DataManager 的开发技能,涵盖 GameObject、Scene 和全局 registry 层级的键值存储及变更事件。帮助开发者共享游戏状态,并让系统响应数据变化。
主要功能
- 01存储和读取键值数据
- 02通过通用及键专属事件追踪变更
- 03递增、切换、合并和删除值
- 04查询并遍历条目
- 05冻结修改或重置数据
适用场景
- 01分数变化时更新 HUD
- 02通过 registry 在场景间共享状态
- 03追踪对象的生命值或物品数据
- 04载入或合并已保存的游戏状态
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ phaserjs/ phaser --skill data-manager安装后在 Claude Code 中输入/data-manager确认已加载。
详细介绍
DataManager
Phaser 的 DataManager 提供键值存储,并支持事件驱动的变更跟踪。它在三个层级上运作:每个 GameObject(
sprite.setData/getData)、每个 Scene(this.data)以及全局(this.registry)。每次 set/change/remove 操作都会发出事件,从而实现游戏系统之间的响应式数据绑定,而无需紧密耦合。
关键源码路径: src/data/DataManager.js、src/data/DataManagerPlugin.js、src/data/events/、src/gameobjects/GameObject.js(setData/getData/incData/toggleData)
相关技能: ../scenes/SKILL.md、../events-system/SKILL.md
快速开始
// Per-GameObject data (auto-creates DataManager on first use)
const gem = this.add.sprite(100, 100, 'gem');
gem.setData('value', 50);
gem.setData({ color: 'red', level: 2 });
gem.getData('value'); // 50
gem.getData(['value', 'color']); // [50, 'red']
// Increment / toggle helpers
gem.incData('value', 10); // value is now 60
gem.incData('value', -5); // value is now 55 (negative to decrement)
gem.toggleData('active'); // false -> true (starts from false if unset)
// Scene-level data (this.data is a DataManagerPlugin)
this.data.set('score', 0);
this.data.get('score'); // 0
this.data.values.score += 100; // triggers changedata event
// Global registry (shared across ALL scenes)
this.registry.set('highScore', 9999);
// Any scene can read it:
this.registry.get('highScore'); // 9999
核心概念
DataManager(Phaser.Data.DataManager)
基类,将键值对存储在内部的 list 对象中。它提供:
set(key, value)—— 存储一个值;发出setdata(新键)或changedata+changedata-{key}(已存在的键)。可接受一个对象以一次设置多个键。get(key)—— 获取一个值,或传入一个键数组以获取值数组。inc(key, amount)—— 递增一个数值(默认为 +1)。如果键不存在,则从 0 开始创建。toggle(key)—— 翻转一个布尔值。如果键不存在,则从false开始创建。remove(key)—— 删除一个键;发出removedata。可接受一个键数组。has(key)—— 如果键存在则返回true。getAll()—— 以普通对象形式返回所有键值对的浅拷贝。query(regex)—— 返回所有键匹配给定 RegExp 的条目。each(callback, context, ...args)—— 遍历所有条目。回调签名:(parent, key, value, ...args)。merge(data, overwrite)—— 从一个对象批量导入。overwrite默认为true;设为false可跳过已存在的键。pop(key)—— 在一次调用中获取并删除一个键;发出removedata。reset()—— 清除所有数据并解冻。freeze/setFreeze(bool)—— 冻结时,所有 set/remove/inc/toggle 操作都会静默地无操作。count—— 只读属性,返回已存储条目的数量。
values 代理对象允许直接进行属性访问并发出事件:
// After set('gold', 100), you can do:
data.values.gold += 50; // emits changedata and changedata-gold
// But you MUST use set() to create a key first -- direct assignment
// to values for a new key will NOT set up the event proxy.
Scene 数据插件(Phaser.Data.DataManagerPlugin)
继承自 DataManager。注册为 data Scene 插件,可在任何 Scene 中通过 this.data 访问。它使用 Scene 的事件发射器(scene.sys.events),因此数据事件会在 Scene 的事件总线上触发。
// In a Scene's create():
this.data.set('lives', 3);
// Listen on the scene's event emitter
this.events.on('changedata-lives', (scene, value, previousValue) => {
console.log('Lives changed from', previousValue, 'to', value);
});
该插件在 Scene 关闭时自动清理(移除其 shutdown 监听器),并在 Scene 销毁时完全销毁。
Registry(全局数据存储)
registry 是 Game 对象上的一个普通 DataManager 实例(game.registry)。它有自己的专用 EventEmitter(不与任何 Scene 共享)。每个 Scene 都会通过注入映射获得一个引用,即 this.registry。
// Scene A sets global data
this.registry.set('currentLevel', 1);
// Scene B reads it
const level = this.registry.get('currentLevel');
// Listen for registry changes (note: events fire on registry.events, NOT this.events)
this.registry.events.on('changedata-currentLevel', (game, value, previousValue) => {
console.log('Level changed to', value);
});
registry 在 Game 的整个生命周期内持续存在。它绝不会在 Scene 重启或关闭时被自动清除。
每个 GameObject 的数据
GameObject 默认没有 DataManager。它会在首次调用 setData()、getData()、incData() 或 toggleData() 时惰性创建。你也可以显式调用 setDataEnabled()。
DataManager 的事件发射器就是 GameObject 本身(它继承自 EventEmitter),因此数据事件会直接在 GameObject 上触发:
const player = this.add.sprite(0, 0, 'player');
player.setData('hp', 100);
// Listen directly on the game object
player.on('changedata-hp', (gameObject, value, previousValue) => {
if (value <= 0) {
gameObject.destroy();
}
});
常见模式
设置和获取数据
// Single key
sprite.setData('speed', 200);
sprite.getData('speed'); // 200
// Multiple keys at once (object form)
sprite.setData({ speed: 200, direction: 'left', hp: 100 });
// Batch get with destructuring
const [speed, hp] = sprite.getData(['speed', 'hp']);
// Direct values access (read and write after initial set)
sprite.data.values.speed = 300; // emits changedata event
const s = sprite.data.values.speed; // 300
监听变更
// Listen for ANY data change on a game object
sprite.on('changedata', (gameObject, key, value, previousValue) => {
console.log(key, 'changed to', value);
});
// Listen for a SPECIFIC key change (preferred -- more efficient)
sprite.on('changedata-hp', (gameObject, value, previousValue) => {
this.hpBar.setValue(value);
});
// Listen for new data being set (first time only)
sprite.on('setdata', (gameObject, key, value) => {
console.log('New data key:', key);
});
// Listen for data removal
sprite.on('removedata', (gameObject, key, value) => {
console.log('Removed:', key, 'was', value);
});
用于跨 Scene 状态的全局 Registry
// HUD scene watches for score changes set by the Game scene
// In HUDScene.create():
this.registry.events.on('changedata-score', (game, value) => {
this.scoreText.setText('Score: ' + value);
});
this.events.once('shutdown', () => {
this.registry.events.off('changedata-score');
});
// In GameScene.update():
this.registry.set('score', this.score);
复杂数据和对象
// You can store any value type: numbers, strings, booleans, objects, arrays
sprite.setData('inventory', ['sword', 'shield']);
sprite.setData('stats', { str: 10, dex: 8, int: 12 });
// CAUTION: mutating a stored object/array does NOT trigger changedata
const inv = sprite.getData('inventory');
inv.push('potion');
// No event fired! The reference didn't change.
// To trigger the event, re-set the key:
sprite.setData('inventory', [...inv]); // new array reference triggers changedata
合并数据
// Merge defaults -- only sets keys that don't already exist
sprite.data.merge({ hp: 100, speed: 200, armor: 0 }, false);
// Merge and overwrite existing values
sprite.data.merge(savedState, true);
按模式查询数据
// Find all keys matching a regex
this.data.set('enemy_1_hp', 50);
this.data.set('enemy_2_hp', 80);
this.data.set('player_hp', 100);
const enemyData = this.data.query(/^enemy_/);
// { enemy_1_hp: 50, enemy_2_hp: 80 }
冻结数据
// Prevent all modifications (set, remove, inc, toggle all become no-ops)
sprite.data.freeze = true;
sprite.setData('hp', 0); // silently ignored
sprite.data.freeze = false;
sprite.setData('hp', 0); // works again
// Also available as a chainable method
sprite.data.setFreeze(true);
数据持久化模式
// Save to localStorage
function saveGame(scene) {
const state = scene.data.getAll();
localStorage.setItem('saveData', JSON.stringify(state));
}
// Restore from localStorage
function loadGame(scene) {
const raw = localStorage.getItem('saveData');
if (raw) {
scene.data.merge(JSON.parse(raw), true);
}
}
事件
所有数据事件都定义在 Phaser.Data.Events 中。事件发射器取决于上下文:对于 GameObject,它就是 GameObject 本身;对于 Scene 数据,它是 scene.sys.events;对于 registry,它是 registry.events。
| 常量 | 字符串 | 触发时机 | 回调参数 |
|---|---|---|---|
SET_DATA | 'setdata' | 创建了新键 | (parent, key, value) |
CHANGE_DATA | 'changedata' | 已有键的值发生变化 | (parent, key, value, previousValue) |
CHANGE_DATA_KEY | 'changedata-' | 特定键发生变化(追加键名) | (parent, value, previousValue) |
REMOVE_DATA | 'removedata' | 移除了某个键 | (parent, key, value) |
DESTROY | 'destroy' | DataManager 的 parent 被销毁 | (无) |
parent 参数是 DataManager 的所有者(即 GameObject、Scene 或 Game 实例)。
注意:CHANGE_DATA_KEY 是一个前缀。实际的事件字符串是 'changedata-' 加上键名。例如,设置一个名为 score 的键会触发 'changedata-score'。
API 快速参考
GameObject 便捷方法
| 方法 | 返回值 | 描述 |
|---|---|---|
setDataEnabled() | this | 显式创建 DataManager(通常会自动创建) |
setData(key, value) | this | 设置单个键,或传入对象以设置多个 |
getData(key) | * | 获取单个值,或传入数组以获取多个 |
incData(key, amount?) | this | 递增数值(默认 +1,传负值可递减) |
toggleData(key) | this | 切换布尔值 |
DataManager 方法
| 方法 | 返回值 | 描述 |
|---|---|---|
set(key, value) | this | 设置单个键,或传入键值对对象 |
get(key) | * | 获取值——传入字符串或字符串数组 |
getAll() | object | 所有条目的浅拷贝 |
query(regex) | object | 键匹配该 RegExp 的所有条目 |
each(cb, ctx, ...args) | this | 遍历所有条目 |
merge(data, overwrite?) | this | 批量导入;overwrite 默认为 true |
remove(key) | this | 删除键——传入字符串或数组 |
pop(key) | * | 在一次调用中获取并删除 |
has(key) | boolean | 检查键是否存在 |
inc(key, amount?) | this | 递增(默认 +1) |
toggle(key) | this | 切换布尔值 |
setFreeze(bool) | this | 冻结/解冻修改 |
reset() | this | 清除所有数据并解冻 |
count | number | 只读的条目数量 |
freeze | boolean | 获取/设置冻结状态 |
values | object | 用于直接属性访问的代理对象 |
get() 与 values——副本与实时引用
理解 get() 和 values 之间的区别对于正确更新数据至关重要:
// get() returns a copy for primitives -- modifying the local variable does NOT update the DataManager
let score = this.data.get('score');
score += 10; // local copy only, DataManager still has the old value
// values provides a live proxy -- assignment triggers CHANGE_DATA event
this.data.values.score += 10; // updates the DataManager AND emits changedata-score
// For objects, get() returns the stored reference (not a deep copy)
const stats = this.data.get('stats');
stats.hp -= 10; // mutates the stored object but does NOT trigger events
this.data.set('stats', { ...stats }); // re-set with new reference to trigger event
事件发射器路由
数据事件会根据 DataManager 所在的位置发射到不同的目标:
| DataManager 所有者 | 事件发射目标 | 监听器示例 |
|---|---|---|
| GameObject | GameObject 本身 | sprite.on('changedata-hp', ...) |
Scene(this.data) | Scene 事件总线(this.events) | this.events.on('changedata-score', ...) |
Registry(this.registry) | Registry 自己的发射器 | this.registry.events.on('changedata-score', ...) |
特定键事件 'changedata-{key}' 仅在该确切键发生变化时触发,而通用事件 'changedata' 会在每次键变化时触发。出于性能考虑,应始终优先使用特定键监听器。
注意事项
键区分大小写
'gold' 和 'Gold' 是两个不同的键。请保持命名约定的一致性。
values 代理需要先调用 set()
在通过 data.values.key 修改某个键之前,必须先调用 set(key, value) 来创建该键。对一个全新的键直接给 values 赋值不会创建事件代理——它只会创建一个普通属性,不发射任何事件。
对象/数组的可变修改不会触发事件
就地修改已存储的对象或数组不会触发 changedata,因为引用没有发生变化。你必须用一个新引用重新设置该键才能触发事件。
注册表监听器在场景重启后仍然存在
注册表存在于 Game 对象上。添加到 this.registry.events 的监听器在场景重启时不会被清理。务必在 SHUTDOWN 时移除它们:
create() {
const handler = (game, value) => { this.updateHUD(value); };
this.registry.events.on('changedata-score', handler);
this.events.once('shutdown', () => {
this.registry.events.off('changedata-score', handler);
});
}
冻结的 DataManager 会静默失败
当 freeze 为 true 时,所有写操作(set、remove、inc、toggle、pop、merge)都会静默地什么都不做。不会抛出错误,也不会触发事件。如果你忘记了自己冻结了数据,这可能会令人困惑。
场景数据插件:Shutdown 与 Destroy 的区别
DataManagerPlugin 在 shutdown 时会移除其 shutdown 监听器,但不会在 shutdown 时清除数据。如果场景重启,数据会继续保留。只有在场景 destroy 时才会完全重置。如果你需要在重启时获得干净的状态,请在 shutdown 监听器中手动重置:
this.events.once('shutdown', () => {
this.data.reset();
});
对非数值使用 inc()
inc() 内部使用 + 运算符。对字符串它会进行拼接,而不是数值相加。对布尔值,它们会在递增前被强制转换为 0 或 1。只对确定存储数值的键使用 inc()。
reset() 不触发任何事件
调用 reset() 会静默清除所有数据——不会为被删除的键触发 removedata 事件。如果你需要为每个键触发移除事件,请改为逐个遍历并调用 remove()。
冻结可能中断批量操作
setFreeze(true) 会立即生效。如果你在一系列 set() 调用的中途冻结,该批次中后续的调用会静默地不执行任何操作。务必在所有写入完成后再冻结。
getAll() 返回的是快照
getAll() 每次都会返回一个新的普通对象。它是浅拷贝——原始值是独立的,但对象/数组值仍然与 DataManager 的内部存储共享引用。
changedata-key 回调中的第一个参数
对于通用的 changedata 事件,回调接收 (parent, key, value, previousValue)。对于键特定的 changedata-{key} 事件,key 参数会被省略:(parent, value, previousValue)。这个差异很容易被忽略。
源文件映射
| 路径 | 描述 |
|---|---|
src/data/DataManager.js | 核心 DataManager 类——set、get、each、merge、remove、query、freeze、事件 |
src/data/DataManagerPlugin.js | 扩展 DataManager 的场景插件;注册为 this.data |
src/data/events/index.js | 事件常量导出(CHANGE_DATA、SET_DATA、REMOVE_DATA、CHANGE_DATA_KEY、DESTROY) |
src/data/events/SET_DATA_EVENT.js | 'setdata'——在创建新键时触发 |
src/data/events/CHANGE_DATA_EVENT.js | 'changedata'——在现有键的值发生变化时触发 |
src/data/events/CHANGE_DATA_KEY_EVENT.js | 'changedata-'——按键变更事件前缀 |
src/data/events/REMOVE_DATA_EVENT.js | 'removedata'——在键被移除时触发 |
src/data/events/DESTROY_EVENT.js | 'destroy'——DataManager 从其父级监听此事件 |
src/gameobjects/GameObject.js | setData、getData、incData、toggleData、setDataEnabled 便捷方法 |
src/core/Game.js | 创建 game.registry——全局 DataManager 实例 |
src/scene/Scene.js | 暴露 this.registry(从 Game 注入)和 this.data(DataManagerPlugin) |