
Phaser 事件系统完全指南EventEmitter、场景事件与跨模块通信实战【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser本文以 Phaser 官方技能文档 skills/events-system/SKILL.md 为核心骨架结合仓库源码逐层展开系统讲解 Phaser 的事件驱动架构从Phaser.Events.EventEmitter基类 API、内置事件的命名常量体系到场景生命周期、输入、加载、动画、相机、声音、补间、物理、纹理等十余个命名空间的事件参考再到跨场景通信与内存泄漏防治的实战模式。读完本文你将掌握在 Phaser 4 中正确监听、派发与清理事件写出无泄漏、可维护的事件驱动游戏代码。Phaser 在整个框架中普遍采用事件发射器EventEmitter模式基于eventemitter3库。Game、Scene、Input、Loader、Cameras、Sound、Tweens、Physics、Textures、Animations 等每一个核心系统要么本身是 EventEmitter要么内部包含一个。所有内置事件统一使用小写字符串键并提供了对应的命名常量Named Constants以避免拼写错误并获得 IDE 自动补全支持。EventEmitter 基类从源码看事件内核类定义与继承关系Phaser.Events.EventEmitter是全部事件能力的基类其完整源码位于 src/events/EventEmitter.js。从源码结构看它通过Class工具类继承自eventemitter3并在initialize构造器中调用EE.call(this)完成初始化var EE require(eventemitter3); var EventEmitter new Class({ Extends: EE, initialize: function EventEmitter () { EE.call(this); }, ... });关键增强在于它额外实现了两个生命周期方法shutdown()与destroy()二者的实现都是调用removeAllListeners()。这正是 Phaser 场景系统在场景关闭时能自动清空所有事件绑定、避免残留引用的底层保证源码注释明确说明由 Scene Systems 在父场景关闭时自动调用。此外类末尾通过PluginCache.register(EventEmitter, EventEmitter, events)将EventEmitter注册为场景系统插件因此在任何场景中都可以直接通过this.events访问它。完整 API继承自 eventemitter3方法说明on(event, fn, context?)添加持久监听器。返回this支持链式调用addListener(event, fn, context?)on的别名once(event, fn, context?)一次性监听器首次触发后自动移除off(event, fn?, context?, once?)移除监听器。要移除特定监听器必须传入与on时相同的fn引用removeListener(event, fn?, context?, once?)off的别名removeAllListeners(event?)移除某事件的全部监听器不传参则移除所有事件的全部监听器emit(event, ...args)触发事件。若存在监听器则返回true否则返回falselisteners(event)返回某事件的监听器函数数组listenerCount(event)返回某事件的监听器数量eventNames()返回已注册监听器的事件名数组shutdown()调用removeAllListeners()由场景关闭流程自动触发destroy()调用removeAllListeners()由对象销毁流程自动触发快速上手五个核心方法// on — 监听一个事件持续生效直到被移除 this.input.on(pointerdown, (pointer) { console.log(clicked at, pointer.x, pointer.y); }); // once — 监听一个事件首次触发后自动移除 this.events.once(shutdown, () { console.log(scene shutting down); }); // off — 移除特定监听器必须传入相同的函数引用 const handler (pointer) { /* ... */ }; this.input.on(pointerdown, handler); this.input.off(pointerdown, handler); // emit — 触发自定义事件并携带参数 this.events.emit(player-died, this.player, this.score); // removeAllListeners — 移除某个事件或所有事件的全部监听器 this.events.removeAllListeners(player-died); this.events.removeAllListeners(); // 移除所有事件事件字符串与命名常量每一个内置事件都是导出为常量的小写字符串。常量名与字符串之间存在可预测的映射关系Phaser.Scenes.Events.UPDATE // update Phaser.Scenes.Events.PRE_UPDATE // preupdate Phaser.Scenes.Events.SHUTDOWN // shutdown Phaser.Core.Events.BOOT // boot Phaser.Input.Events.POINTER_DOWN // pointerdown以源码为例src/scene/events/UPDATE_EVENT.js 中module.exports update;src/scene/events/SHUTDOWN_EVENT.js 中module.exports shutdown;src/input/events/GAMEOBJECT_POINTER_DOWN_EVENT.js 中module.exports pointerdown;。而 src/scene/events/index.js、src/core/events/index.js、src/input/events/index.js 等索引文件将这些常量统一聚合到Phaser.Scenes.Events、Phaser.Core.Events、Phaser.Input.Events等命名空间下。因此官方建议始终优先使用常量而非裸字符串以借助 IDE 自动补全并杜绝拼写错误。键后缀模式Key-Suffix Pattern部分事件采用常量 具体键的后缀拼接模式用于针对特定对象/文件监听// Loader监听某个具体文件加载完成 this.load.on(Phaser.Loader.Events.FILE_KEY_COMPLETE image-logo, (key, type, data) {}); // 实际字符串值filecomplete-image-logo // Animations监听某个精灵上的指定动画播放完成 sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE_KEY walk, () {}); // 实际字符串值animationcomplete-walk // Textures监听某个具体纹理被添加 this.textures.on(Phaser.Textures.Events.ADD_KEY myTexture, () {}); // 实际字符串值addtexture-myTexture类似的模式还存在于Phaser.Data的注册表中见下文跨场景通信一节changedata-key事件用于监听某个具体数据键的变化其常量定义在 src/data/events/CHANGE_DATA_KEY_EVENT.js值为changedata-。Contexton/once的第三个参数on/once的第三个参数用于设置回调内部的this指向默认为发射器emitter本身。// this 在处理器内指向 scene this.input.on(pointerdown, function (pointer) { this.cameras.main.shake(100); // this scene }, this); // 箭头函数会忽略 context 参数它们捕获词法作用域的 this this.input.on(pointerdown, (pointer) { this.cameras.main.shake(100); // this 外层作用域create 中的 scene });从源码看on的 JSDoc 注释明确标注param {*} [contextthis] - The context to invoke the listener with.即不传时默认以发射器为上下文。使用普通函数时务必传入this作为第三参或改用箭头函数否则回调内的this不会指向场景。常见模式场景生命周期、游戏事件与自定义事件场景生命周期事件每一帧场景的事件流顺序为preupdate→update→Scene.update()→postupdate→prerender→render这一顺序在 src/scene/events/UPDATE_EVENT.js 的文档注释中有完整记载。create() { this.events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); // 关键必须在 shutdown 时清理监听防止场景重启导致泄漏 this.events.on(Phaser.Scenes.Events.SHUTDOWN, () { this.events.off(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); this.input.off(pointerdown, this.onPointerDown, this); }); }游戏级事件game.events挂在 Game 实例上跨所有场景共享在场景内通过this.game.events访问。this.game.events.on(Phaser.Core.Events.BLUR, this.handleBlur, this); this.game.events.on(Phaser.Core.Events.VISIBLE, this.handleVisible, this);跨场景通信三种主流方法// 方法一game.events —— 全局事件总线所有场景均可访问 // 场景 A 派发 this.game.events.emit(score-changed, this.score); // 场景 B 监听 this.game.events.on(score-changed, (score) { this.scoreText.setText(score); }); // 方法二this.registry —— 挂载在 Game 实例上的共享 DataManager // 场景 A 写入数据 this.registry.set(score, 100); // 场景 B 监听变化 this.registry.events.on(changedata-score, (parent, value, previousValue) { this.scoreText.setText(value); }); // 方法三通过 ScenePlugin 直接访问其他场景的 events this.scene.get(UIScene).events.emit(update-health, hp);方法二的底层实现可在 src/data/DataManager.js 中验证当已存在的键被更新时会触发changedata/changedata-key事件例如更新PlayerLives键会发射changedata-PlayerLives其事件处理器签名依次接收parentDataManager 自身、value新值与previousValue旧值。自定义事件// 携带任意数据参数派发自定义事件 this.events.emit(player-died, this.player, { lives: this.lives }); this.events.on(player-died, (player, data) { console.log(Lives remaining:, data.lives); });独立创建 EventEmitterconst bus new Phaser.Events.EventEmitter(); bus.on(inventory-changed, (items) { console.log(items.length); }); bus.emit(inventory-changed, this.inventory);全部事件命名空间参考场景事件Phaser.Scenes.Events发射器this.events场景 Systems 的 EventEmitter。对应源码聚合文件为 src/scene/events/index.js共 22 个事件。常量字符串触发时机BOOTboot场景 Systems 启动供插件使用READYready场景 Systems 完全就绪STARTstart场景开始运行CREATEcreateScene.create()完成后PRE_UPDATEpreupdate每帧更新之前UPDATEupdate每帧主更新POST_UPDATEpostupdate每帧更新之后PRE_RENDERprerender每帧渲染之前RENDERrender每帧渲染期间PAUSEpause场景暂停RESUMEresume场景从暂停恢复SLEEPsleep场景进入休眠WAKEwake场景从休眠唤醒SHUTDOWNshutdown场景关闭可能再次启动DESTROYdestroy场景被永久销毁ADDED_TO_SCENEaddedtoscene游戏对象被添加到场景REMOVED_FROM_SCENEremovedfromscene游戏对象从场景移除TRANSITION_INITtransitioninit场景过渡初始化目标场景TRANSITION_STARTtransitionstart场景过渡开始目标场景TRANSITION_OUTtransitionout场景过渡切出源场景TRANSITION_COMPLETEtransitioncomplete场景过渡完成TRANSITION_WAKEtransitionwake过渡唤醒目标场景游戏事件Phaser.Core.Events发射器this.game.events或game.events。对应源码聚合文件为 src/core/events/index.js共 16 个事件。常量字符串触发时机BOOTbootGame 实例完成启动READYreadyGame 准备开始运行SYSTEM_READYsystemready所有全局系统就绪PRE_STEPprestep游戏循环步进之前STEPstep游戏主循环步进POST_STEPpoststep游戏循环步进之后PRE_RENDERprerender渲染所有场景之前POST_RENDERpostrender渲染所有场景之后PAUSEpause游戏暂停RESUMEresume游戏恢复BLURblur浏览器标签页失去焦点FOCUSfocus浏览器标签页获得焦点HIDDENhiddenPage Visibility API页面隐藏VISIBLEvisiblePage Visibility API页面可见CONTEXT_LOSTcontextlostWebGL 上下文丢失DESTROYdestroyGame 被销毁输入事件Phaser.Input.Events发射器this.input场景级或具体的游戏对象。事件存在于三个层级场景级this.input、场景级带游戏对象前缀、以及直接在交互式游戏对象上。完整用法可参考 skills/input-keyboard-mouse-touch/SKILL.md。对应源码聚合文件为 src/input/events/index.js。场景级指针事件在this.input上POINTER_DOWNpointerdown|POINTER_UPpointerup|POINTER_MOVEpointermove|POINTER_OVERpointerover|POINTER_OUTpointerout|POINTER_WHEELwheel|POINTER_DOWN_OUTSIDEpointerdownoutside|POINTER_UP_OUTSIDEpointerupoutside场景级游戏对象事件在this.input上GAMEOBJECT_DOWNgameobjectdown|GAMEOBJECT_UPgameobjectup|GAMEOBJECT_MOVEgameobjectmove|GAMEOBJECT_OVERgameobjectover|GAMEOBJECT_OUTgameobjectout|GAMEOBJECT_WHEELgameobjectwheel单游戏对象事件在游戏对象自身上发射需先setInteractive()GAMEOBJECT_POINTER_DOWNpointerdown|GAMEOBJECT_POINTER_UPpointerup|GAMEOBJECT_POINTER_MOVEpointermove|GAMEOBJECT_POINTER_OVERpointerover|GAMEOBJECT_POINTER_OUTpointerout|GAMEOBJECT_POINTER_WHEELwheel拖拽事件在this.input与游戏对象上字符串相同DRAG_START/GAMEOBJECT_DRAG_STARTdragstart|DRAG/GAMEOBJECT_DRAGdrag|DRAG_END/GAMEOBJECT_DRAG_ENDdragend|DRAG_ENTER/GAMEOBJECT_DRAG_ENTERdragenter|DRAG_OVER/GAMEOBJECT_DRAG_OVERdragover|DRAG_LEAVE/GAMEOBJECT_DRAG_LEAVEdragleave|DROP/GAMEOBJECT_DROPdrop其他GAME_OUTgameout|GAME_OVERgameover|POINTERLOCK_CHANGEpointerlockchange加载器事件Phaser.Loader.Events发射器this.load。常量字符串触发时机ADDaddfile文件加入加载队列STARTstart加载器开始PROGRESSprogress整体进度更新0–1FILE_LOADload单个文件加载完成FILE_PROGRESSfileprogress单个文件加载进度FILE_COMPLETEfilecomplete单个文件完成处理FILE_KEY_COMPLETEfilecomplete-具体文件完成追加type-keyFILE_LOAD_ERRORloaderror文件加载失败POST_PROCESSpostprocess全部文件已加载进入后处理COMPLETEcomplete全部加载完成动画事件Phaser.Animations.Events发射器单个精灵逐精灵或this.anims全局 AnimationManager。常量字符串触发时机ADD_ANIMATIONadd动画被添加到管理器REMOVE_ANIMATIONremove动画从管理器移除PAUSE_ALLpauseall所有动画暂停RESUME_ALLresumeall所有动画恢复ANIMATION_STARTanimationstart动画在精灵上开始播放ANIMATION_RESTARTanimationrestart动画在精灵上重新开始ANIMATION_REPEATanimationrepeat动画在精灵上重复ANIMATION_UPDATEanimationupdate动画在精灵上切换帧ANIMATION_COMPLETEanimationcomplete动画在精灵上完成ANIMATION_COMPLETE_KEYanimationcomplete-指定动画完成追加动画键ANIMATION_STOPanimationstop动画在精灵上被停止相机事件Phaser.Cameras.Scene2D.Events发射器单个相机实例如this.cameras.main。每个相机效果都有一对 START 与 COMPLETE 事件。DESTROYcameradestroy|FADE_IN_STARTcamerafadeinstart|FADE_IN_COMPLETEcamerafadeincomplete|FADE_OUT_STARTcamerafadeoutstart|FADE_OUT_COMPLETEcamerafadeoutcomplete|FLASH_STARTcameraflashstart|FLASH_COMPLETEcameraflashcomplete|PAN_STARTcamerapanstart|PAN_COMPLETEcamerapancomplete|ROTATE_STARTcamerarotatestart|ROTATE_COMPLETEcamerarotatecomplete|SHAKE_STARTcamerashakestart|SHAKE_COMPLETEcamerashakecomplete|ZOOM_STARTcamerazoomstart|ZOOM_COMPLETEcamerazoomcomplete|FOLLOW_UPDATEfollowupdate|PRE_RENDERprerender|POST_RENDERpostrender声音事件Phaser.Sound.Events发射器单个声音实例或this.soundSoundManager。单声音实例事件PLAYplay|PAUSEpause|RESUMEresume|STOPstop|COMPLETEcomplete|LOOPloop|LOOPEDlooped|SEEKseek|MUTEmute|VOLUMEvolume|RATErate|DETUNEdetune|PANpan|DECODEDdecoded|DESTROYdestroySoundManager 级事件在this.sound上GLOBAL_MUTEmute|GLOBAL_VOLUMEvolume|GLOBAL_RATErate|GLOBAL_DETUNEdetune|PAUSE_ALLpauseall|RESUME_ALLresumeall|STOP_ALLstopall|DECODED_ALLdecodedall|UNLOCKEDunlocked补间事件Phaser.Tweens.Events发射器单个补间实例。常量字符串触发时机TWEEN_ACTIVEactive补间变为激活TWEEN_STARTstart补间首次开始播放TWEEN_UPDATEupdate补间更新某个值TWEEN_YOYOyoyo补间回弹反向TWEEN_REPEATrepeat补间重复TWEEN_LOOPloop补间循环TWEEN_PAUSEpause补间暂停TWEEN_RESUMEresume补间恢复TWEEN_COMPLETEcomplete补间完成TWEEN_STOPstop补间被手动停止物理事件Phaser.Physics.Arcade.Events发射器this.physics.world。常量字符串触发时机COLLIDEcollide两个刚体碰撞OVERLAPoverlap两个刚体重叠TILE_COLLIDEtilecollide刚体与瓦片碰撞TILE_OVERLAPtileoverlap刚体与瓦片重叠WORLD_BOUNDSworldbounds刚体触碰世界边界WORLD_STEPworldstep物理世界完成一次步进PAUSEpause物理世界暂停RESUMEresume物理世界恢复纹理事件Phaser.Textures.Events发射器this.texturesTextureManager。常量字符串触发时机ADDaddtexture任何纹理被添加ADD_KEYaddtexture-指定纹理被添加追加键REMOVEremovetexture任何纹理被移除REMOVE_KEYremovetexture-指定纹理被移除追加键LOADonload纹理源加载完成ERRORonerror纹理源加载出错READYready纹理管理器就绪游戏对象事件Phaser.GameObjects.Events发射器单个游戏对象。常量字符串触发时机ADDED_TO_SCENEaddedtoscene游戏对象被添加到场景REMOVED_FROM_SCENEremovedfromscene游戏对象从场景移除DESTROYdestroy游戏对象被销毁Video 游戏对象事件在 Video 游戏对象上VIDEO_PLAYplay|VIDEO_PLAYINGplaying|VIDEO_COMPLETEcomplete|VIDEO_LOOPloop|VIDEO_STOPstop|VIDEO_CREATEDcreated|VIDEO_ERRORerror|VIDEO_LOCKEDlocked|VIDEO_UNLOCKEDunlocked|VIDEO_METADATAmetadata|VIDEO_SEEKEDseeked|VIDEO_SEEKINGseeking|VIDEO_STALLEDstalled|VIDEO_TEXTUREtextureready|VIDEO_UNSUPPORTEDunsupported时间事件Phaser.Time.Events发射器Phaser.Time.TimerEvent实例。常量字符串触发时机COMPLETEcompleteTimerEvent 完成全部重复次数场景事件 vs 游戏事件选择正确的发射器this.events——场景专属。触发场景生命周期事件update、shutdown、create 等。场景被销毁时自动清理。this.game.events——全局共享。触发游戏级事件blur、focus、pause、resume 等。跨场景重启依然存在必须在 SHUTDOWN 时手动清理。create() { this.events.on(Phaser.Scenes.Events.UPDATE, this.onUpdate, this); this.game.events.on(Phaser.Core.Events.BLUR, this.onBlur, this); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () { this.game.events.off(Phaser.Core.Events.BLUR, this.onBlur, this); }); }事件移除安全与常用工具方法off() 要求精确引用必须向off()传入与on()完全相同的函数引用与相同的上下文/作用域。匿名或内联箭头函数无法被移除——请保存引用或使用类方法// 错误箭头函数后续无法移除 this.events.on(update, () { this.doStuff(); }); // 正确具名方法可以被移除 this.events.on(update, this.onUpdate, this); this.events.off(update, this.onUpdate, this);once() 自动移除once()首次触发后自动移除监听器无需手动清理this.events.once(Phaser.Scenes.Events.CREATE, this.onFirstCreate, this);工具方法一览emitter.listenerCount(update); // 某事件的监听器数量 emitter.eventNames(); // [update, player-died] —— 所有已注册事件名 emitter.removeAllListeners(player-died); // 移除单个事件的全部监听器 emitter.removeAllListeners(); // 移除所有事件的全部监听器常见陷阱Gotchas内存泄漏未移除的监听器这是最常见的 Bug 来源。若场景使用on()且通过scene.restart()重启旧监听器不会自动移除因为on()不具备自动清理能力。每次重启都会叠加重复监听器// 错误每次场景重启都会泄漏监听器 create() { this.input.on(pointerdown, this.shoot, this); } // 正确在 shutdown 中清理 create() { this.input.on(pointerdown, this.shoot, this); this.events.once(Phaser.Scenes.Events.SHUTDOWN, () { this.input.off(pointerdown, this.shoot, this); }); } // 同样正确只需触发一次的事件使用 once() create() { this.events.once(Phaser.Scenes.Events.CREATE, this.onFirstCreate, this); }shutdown 与 destroy 的区别SHUTDOWN场景停止但之后可能重启。在此清理监听器。DESTROY场景被永久移除。用于最终清理。场景重启的事件顺序为SHUTDOWN→START→CREATE不会触发DESTROY。上下文绑定Context Bindingon/once的第三个参数设置回调内的this不传时this默认为发射器而非场景。使用普通函数时请把this作为第三参传入或使用捕获词法this的箭头函数。输入事件层级Input Event Hierarchy输入事件按以下顺序触发1游戏对象上的GAMEOBJECT_POINTER_DOWN2this.input上的GAMEOBJECT_DOWN3this.input上的POINTER_DOWN。这一从具体到抽象的传播顺序在 src/input/events/GAMEOBJECT_POINTER_DOWN_EVENT.js 的注释中有完整记载。层级较高的处理器可以调用事件对象的stopPropagation()阻止事件继续向下传播。游戏事件与场景事件不可混淆this.game.events与this.events是不同的发射器游戏事件每个游戏循环 tick 触发一次、横跨所有场景场景事件按场景独立触发。game.events上的监听器在场景重启后依然存在务必在 SHUTDOWN 时清理见上文代码示例。源码文件地图路径说明src/events/EventEmitter.jsEventEmitter 基类封装 eventemitter3新增 shutdown/destroysrc/scene/events/场景生命周期事件22 个src/core/events/游戏级事件16 个src/input/events/输入/指针/拖拽事件48 个src/loader/events/资源加载事件10 个src/animations/events/动画播放事件11 个src/cameras/2d/events/相机效果事件18 个src/sound/events/声音播放事件24 个src/tweens/events/补间生命周期事件10 个src/physics/arcade/events/Arcade 物理事件8 个src/textures/events/纹理管理器事件7 个src/gameobjects/events/游戏对象生命周期 Video 事件18 个src/time/events/TimerEvent 事件1 个src/data/events/DataManager 数据变更事件含changedata-键后缀模式相关技能文档场景系统技能指南、输入/键盘/鼠标/触控技能指南。【免费下载链接】phaserPhaser is a fun, free and fast 2D game framework for making HTML5 games for desktop and mobile web browsers, supporting Canvas and WebGL rendering.项目地址: https://gitcode.com/gh_mirrors/ph/phaser创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考