@nimbus2d/core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +18433 -0
- package/package.json +39 -0
- package/src/base/EventEmitter.js +187 -0
- package/src/base/Subsystem.js +183 -0
- package/src/base/index.js +6 -0
- package/src/core/Application/AppState.js +27 -0
- package/src/core/Application/Application.js +480 -0
- package/src/core/Application/DefaultConfig.js +63 -0
- package/src/core/Application/index.js +7 -0
- package/src/core/AssetManager/AssetManager.js +711 -0
- package/src/core/AssetManager/AssetTypes.js +108 -0
- package/src/core/AssetManager/adapters/IResourceAdapter.js +50 -0
- package/src/core/AssetManager/adapters/PixiAdapter.js +90 -0
- package/src/core/AssetManager/core/RefCounter.js +167 -0
- package/src/core/AssetManager/core/ResourceCache.js +281 -0
- package/src/core/AssetManager/core/ResourceEntry.js +191 -0
- package/src/core/AssetManager/core/ResourceEventBus.js +127 -0
- package/src/core/AssetManager/handlers/IResourceHandler.js +45 -0
- package/src/core/AssetManager/index.js +26 -0
- package/src/core/AssetManager/plugins/IResourcePlugin.js +56 -0
- package/src/core/Camera/Camera.js +534 -0
- package/src/core/Camera/CameraTypes.js +59 -0
- package/src/core/Camera/index.js +13 -0
- package/src/core/ConfigSystem/ConfigLayer.js +230 -0
- package/src/core/ConfigSystem/ConfigLayerType.js +31 -0
- package/src/core/ConfigSystem/ConfigMerger.js +50 -0
- package/src/core/ConfigSystem/ConfigPath.js +96 -0
- package/src/core/ConfigSystem/ConfigSystem.js +378 -0
- package/src/core/ConfigSystem/ConfigTypes.js +29 -0
- package/src/core/ConfigSystem/ConfigWatcher.js +84 -0
- package/src/core/ConfigSystem/index.js +19 -0
- package/src/core/EventBus/EventBus.js +797 -0
- package/src/core/EventBus/Interceptor.js +26 -0
- package/src/core/EventBus/PerformanceStats.js +105 -0
- package/src/core/EventBus/index.js +7 -0
- package/src/core/Lifecycle/Lifecycle.js +265 -0
- package/src/core/Lifecycle/LifecycleState.js +31 -0
- package/src/core/Lifecycle/index.js +6 -0
- package/src/core/Logger/ConsoleHandler.js +55 -0
- package/src/core/Logger/LogHandler.js +34 -0
- package/src/core/Logger/LogLevel.js +38 -0
- package/src/core/Logger/Logger.js +234 -0
- package/src/core/Logger/index.js +8 -0
- package/src/core/PluginSystem/Plugin.js +156 -0
- package/src/core/PluginSystem/PluginEntry.js +102 -0
- package/src/core/PluginSystem/PluginState.js +30 -0
- package/src/core/PluginSystem/PluginSystem.js +530 -0
- package/src/core/PluginSystem/index.js +8 -0
- package/src/core/PoolManager/DefaultConfig.js +19 -0
- package/src/core/PoolManager/ObjectPool.js +326 -0
- package/src/core/PoolManager/PoolManager.js +253 -0
- package/src/core/PoolManager/PoolTypes.js +92 -0
- package/src/core/PoolManager/index.js +15 -0
- package/src/core/SceneManager/FadeTransition.js +64 -0
- package/src/core/SceneManager/MaskTransition.js +234 -0
- package/src/core/SceneManager/Scene.js +221 -0
- package/src/core/SceneManager/SceneManager.js +658 -0
- package/src/core/SceneManager/SceneState.js +29 -0
- package/src/core/SceneManager/SlideTransition.js +113 -0
- package/src/core/SceneManager/Transition.js +102 -0
- package/src/core/SceneManager/ZoomTransition.js +119 -0
- package/src/core/SceneManager/index.js +12 -0
- package/src/core/SpriteGPULayer/SpriteGPULayer.js +321 -0
- package/src/core/SpriteGPULayer/SpriteGPULayerTypes.js +49 -0
- package/src/core/SpriteGPULayer/index.js +11 -0
- package/src/core/Timeline/Clip.js +101 -0
- package/src/core/Timeline/DefaultConfig.js +41 -0
- package/src/core/Timeline/Easing.js +176 -0
- package/src/core/Timeline/Timeline.js +458 -0
- package/src/core/Timeline/TimelineTypes.js +89 -0
- package/src/core/Timeline/Track.js +202 -0
- package/src/core/Timeline/index.js +22 -0
- package/src/errors/ConfigError.js +47 -0
- package/src/errors/LifecycleError.js +58 -0
- package/src/errors/NimbusError.js +43 -0
- package/src/errors/PluginError.js +138 -0
- package/src/errors/ResourceError.js +166 -0
- package/src/errors/SceneError.js +124 -0
- package/src/errors/SubsystemError.js +68 -0
- package/src/errors/index.js +53 -0
- package/src/index.js +86 -0
- package/src/types/index.js +141 -0
- package/src/utils/MemoryChecker.js +374 -0
- package/src/utils/Validator.js +60 -0
- package/src/utils/index.js +84 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - 滑动过渡效果
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 源场景滑出,目标场景滑入。支持四个方向:left/right/up/down。
|
|
5
|
+
//
|
|
6
|
+
// 继承关系:
|
|
7
|
+
// Transition → SlideTransition
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
import { Transition } from './Transition.js'
|
|
11
|
+
|
|
12
|
+
class SlideTransition extends Transition {
|
|
13
|
+
/**
|
|
14
|
+
* 构造函数
|
|
15
|
+
* @param {Object} [options={}] - 选项
|
|
16
|
+
* @param {string} [options.direction='left'] - 滑动方向(left/right/up/down)
|
|
17
|
+
*/
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
super(options)
|
|
20
|
+
|
|
21
|
+
/** @type {string} 滑动方向 */
|
|
22
|
+
this._direction = options.direction || 'left'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 执行滑动过渡
|
|
27
|
+
* @param {import('./Scene.js').Scene} fromScene - 源场景
|
|
28
|
+
* @param {import('./Scene.js').Scene} toScene - 目标场景
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async execute(fromScene, toScene) {
|
|
32
|
+
// 触发开始回调
|
|
33
|
+
if (this._onTransitionStart) this._onTransitionStart()
|
|
34
|
+
|
|
35
|
+
// 获取画布尺寸
|
|
36
|
+
const width = (fromScene && fromScene.stage && fromScene.stage.parent) ? fromScene.stage.parent.width : 800
|
|
37
|
+
const height = (fromScene && fromScene.stage && fromScene.stage.parent) ? fromScene.stage.parent.height : 600
|
|
38
|
+
|
|
39
|
+
// 设置目标场景初始位置
|
|
40
|
+
if (toScene && toScene.stage) {
|
|
41
|
+
switch (this._direction) {
|
|
42
|
+
case 'left':
|
|
43
|
+
toScene.stage.x = width
|
|
44
|
+
toScene.stage.y = 0
|
|
45
|
+
break
|
|
46
|
+
case 'right':
|
|
47
|
+
toScene.stage.x = -width
|
|
48
|
+
toScene.stage.y = 0
|
|
49
|
+
break
|
|
50
|
+
case 'up':
|
|
51
|
+
toScene.stage.x = 0
|
|
52
|
+
toScene.stage.y = height
|
|
53
|
+
break
|
|
54
|
+
case 'down':
|
|
55
|
+
toScene.stage.x = 0
|
|
56
|
+
toScene.stage.y = -height
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 同时滑动:源场景滑出,目标场景滑入
|
|
62
|
+
await this._animate(this._duration, (progress) => {
|
|
63
|
+
if (fromScene && fromScene.stage) {
|
|
64
|
+
switch (this._direction) {
|
|
65
|
+
case 'left':
|
|
66
|
+
fromScene.stage.x = -progress * width
|
|
67
|
+
break
|
|
68
|
+
case 'right':
|
|
69
|
+
fromScene.stage.x = progress * width
|
|
70
|
+
break
|
|
71
|
+
case 'up':
|
|
72
|
+
fromScene.stage.y = -progress * height
|
|
73
|
+
break
|
|
74
|
+
case 'down':
|
|
75
|
+
fromScene.stage.y = progress * height
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (toScene && toScene.stage) {
|
|
81
|
+
switch (this._direction) {
|
|
82
|
+
case 'left':
|
|
83
|
+
toScene.stage.x = width * (1 - progress)
|
|
84
|
+
break
|
|
85
|
+
case 'right':
|
|
86
|
+
toScene.stage.x = -width * (1 - progress)
|
|
87
|
+
break
|
|
88
|
+
case 'up':
|
|
89
|
+
toScene.stage.y = height * (1 - progress)
|
|
90
|
+
break
|
|
91
|
+
case 'down':
|
|
92
|
+
toScene.stage.y = -height * (1 - progress)
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// 确保最终位置
|
|
99
|
+
if (fromScene && fromScene.stage) {
|
|
100
|
+
fromScene.stage.x = 0
|
|
101
|
+
fromScene.stage.y = 0
|
|
102
|
+
}
|
|
103
|
+
if (toScene && toScene.stage) {
|
|
104
|
+
toScene.stage.x = 0
|
|
105
|
+
toScene.stage.y = 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 触发结束回调
|
|
109
|
+
if (this._onTransitionEnd) this._onTransitionEnd()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { SlideTransition }
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - 过渡效果基类
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 所有过渡效果必须继承此基类,提供 execute 方法契约。
|
|
5
|
+
//
|
|
6
|
+
// 设计决策:
|
|
7
|
+
// - 独立设计(不继承任何类)
|
|
8
|
+
// - 支持 4 种缓动函数:linear/easeIn/easeOut/easeInOut
|
|
9
|
+
// - 子类重写 execute(fromScene, toScene) 实现过渡逻辑
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 缓动函数映射表
|
|
14
|
+
* @type {Object<string, Function>}
|
|
15
|
+
*/
|
|
16
|
+
const EASING_FUNCTIONS = {
|
|
17
|
+
/** 线性(匀速) */
|
|
18
|
+
linear: (t) => t,
|
|
19
|
+
/** 先慢后快 */
|
|
20
|
+
easeIn: (t) => t * t,
|
|
21
|
+
/** 先快后慢 */
|
|
22
|
+
easeOut: (t) => t * (2 - t),
|
|
23
|
+
/** 先慢后快再慢(默认) */
|
|
24
|
+
easeInOut: (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class Transition {
|
|
28
|
+
/**
|
|
29
|
+
* 构造函数
|
|
30
|
+
* @param {Object} [options={}] - 选项
|
|
31
|
+
* @param {number} [options.duration=500] - 持续时间(ms)
|
|
32
|
+
* @param {string} [options.easing='easeInOut'] - 缓动函数
|
|
33
|
+
* @param {Function} [options.onTransitionStart] - 开始回调
|
|
34
|
+
* @param {Function} [options.onTransitionEnd] - 结束回调
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
/** @type {number} 持续时间(ms) */
|
|
38
|
+
this._duration = options.duration || 500
|
|
39
|
+
|
|
40
|
+
/** @type {string} 缓动函数名称 */
|
|
41
|
+
this._easing = EASING_FUNCTIONS[options.easing] ? options.easing : 'easeInOut'
|
|
42
|
+
|
|
43
|
+
/** @type {Function|null} 开始回调 */
|
|
44
|
+
this._onTransitionStart = options.onTransitionStart || null
|
|
45
|
+
|
|
46
|
+
/** @type {Function|null} 结束回调 */
|
|
47
|
+
this._onTransitionEnd = options.onTransitionEnd || null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 执行过渡(子类重写)
|
|
52
|
+
* @param {import('./Scene.js').Scene} fromScene - 源场景
|
|
53
|
+
* @param {import('./Scene.js').Scene} toScene - 目标场景
|
|
54
|
+
* @returns {Promise<void>}
|
|
55
|
+
*/
|
|
56
|
+
async execute(fromScene, toScene) {
|
|
57
|
+
// 基类空实现,子类必须重写
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 应用缓动函数
|
|
62
|
+
* @param {number} t - 进度值 [0, 1]
|
|
63
|
+
* @param {string} [easing] - 缓动函数名
|
|
64
|
+
* @returns {number} 缓动后的值
|
|
65
|
+
* @protected
|
|
66
|
+
*/
|
|
67
|
+
_applyEasing(t, easing) {
|
|
68
|
+
const fn = EASING_FUNCTIONS[easing || this._easing]
|
|
69
|
+
return fn ? fn(t) : EASING_FUNCTIONS.easeInOut(t)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 等待指定时长(带缓动动画辅助)
|
|
74
|
+
* @param {number} duration - 时长(ms)
|
|
75
|
+
* @param {Function} onFrame - 每帧回调 (progress: number) => void
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
* @protected
|
|
78
|
+
*/
|
|
79
|
+
_animate(duration, onFrame) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const startTime = performance.now()
|
|
82
|
+
|
|
83
|
+
const tick = () => {
|
|
84
|
+
const elapsed = performance.now() - startTime
|
|
85
|
+
const rawProgress = Math.min(elapsed / duration, 1)
|
|
86
|
+
const easedProgress = this._applyEasing(rawProgress)
|
|
87
|
+
|
|
88
|
+
onFrame(easedProgress)
|
|
89
|
+
|
|
90
|
+
if (rawProgress < 1) {
|
|
91
|
+
requestAnimationFrame(tick)
|
|
92
|
+
} else {
|
|
93
|
+
resolve()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
requestAnimationFrame(tick)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { Transition }
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - ZoomTransition 缩放转场效果
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 实现缩放转场动画(源场景缩小消失 + 目标场景放大出现)
|
|
6
|
+
// 2. 支持 scaleFrom/scaleTo 配置源场景缩放范围
|
|
7
|
+
// 3. 目标场景从 0 缩放到 1,并同步淡入
|
|
8
|
+
// 4. 复用 _animate/_applyEasing 提供缓动效果
|
|
9
|
+
//
|
|
10
|
+
// 继承关系:
|
|
11
|
+
// Transition → ZoomTransition
|
|
12
|
+
//
|
|
13
|
+
// 设计决策:
|
|
14
|
+
// - 继承现有 Transition 基类(execute 契约),复用 _animate/_applyEasing
|
|
15
|
+
// - 源场景缩放范围可配置(scaleFrom→scaleTo),目标场景固定 0→1
|
|
16
|
+
// - 同步淡入淡出 alpha 增强转场视觉效果
|
|
17
|
+
// - 完成后恢复 stage 的 scale/alpha 到正常值(避免状态残留)
|
|
18
|
+
//
|
|
19
|
+
// 四期新增(任务 1.5/3.2):对标 Phaser 的 ZoomTransition
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
import { Transition } from './Transition.js'
|
|
23
|
+
|
|
24
|
+
class ZoomTransition extends Transition {
|
|
25
|
+
/**
|
|
26
|
+
* 构造函数
|
|
27
|
+
* @param {Object} [options={}] - 选项(继承自 Transition)
|
|
28
|
+
* @param {number} [options.scaleFrom=1] - 源场景起始缩放(默认 1,正常大小)
|
|
29
|
+
* @param {number} [options.scaleTo=0] - 源场景结束缩放(默认 0,完全缩小)
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
super(options)
|
|
33
|
+
|
|
34
|
+
/** @type {number} 源场景起始缩放 */
|
|
35
|
+
this._scaleFrom = options.scaleFrom ?? 1
|
|
36
|
+
|
|
37
|
+
/** @type {number} 源场景结束缩放 */
|
|
38
|
+
this._scaleTo = options.scaleTo ?? 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 执行缩放转场
|
|
43
|
+
* @param {import('./Scene.js').Scene} fromScene - 源场景
|
|
44
|
+
* @param {import('./Scene.js').Scene} toScene - 目标场景
|
|
45
|
+
* @returns {Promise<void>}
|
|
46
|
+
*/
|
|
47
|
+
async execute(fromScene, toScene) {
|
|
48
|
+
// 触发开始回调
|
|
49
|
+
if (this._onTransitionStart) this._onTransitionStart()
|
|
50
|
+
|
|
51
|
+
/** @type {import('pixi.js').Container|null} 源场景 stage */
|
|
52
|
+
const fromStage = fromScene ? fromScene.stage : null
|
|
53
|
+
/** @type {import('pixi.js').Container|null} 目标场景 stage */
|
|
54
|
+
const toStage = toScene ? toScene.stage : null
|
|
55
|
+
|
|
56
|
+
// 设置目标场景初始状态(缩放为 0,alpha 为 0,完全不可见)
|
|
57
|
+
if (toStage) {
|
|
58
|
+
toStage.scale.set(0, 0)
|
|
59
|
+
toStage.alpha = 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 设置源场景初始状态(缩放为 scaleFrom,alpha 为 1,完全可见)
|
|
63
|
+
if (fromStage) {
|
|
64
|
+
fromStage.scale.set(this._scaleFrom, this._scaleFrom)
|
|
65
|
+
fromStage.alpha = 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
// 执行缩放动画
|
|
70
|
+
await this._animate(this._duration, (progress) => {
|
|
71
|
+
// 源场景:从 scaleFrom 缩放到 scaleTo,同步淡出
|
|
72
|
+
if (fromStage) {
|
|
73
|
+
/** @type {number} 当前源场景缩放值(线性插值) */
|
|
74
|
+
const fromScale = this._scaleFrom + (this._scaleTo - this._scaleFrom) * progress
|
|
75
|
+
fromStage.scale.set(fromScale, fromScale)
|
|
76
|
+
fromStage.alpha = 1 - progress
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 目标场景:从 0 放大到 1,同步淡入
|
|
80
|
+
if (toStage) {
|
|
81
|
+
toStage.scale.set(progress, progress)
|
|
82
|
+
toStage.alpha = progress
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
} finally {
|
|
86
|
+
// 无论动画成功或失败,都恢复 stage 到正常状态
|
|
87
|
+
this._restoreStages(fromStage, toStage)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 触发结束回调
|
|
91
|
+
if (this._onTransitionEnd) this._onTransitionEnd()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --------------------------------------------------------------------------
|
|
95
|
+
// 私有方法:状态恢复
|
|
96
|
+
// --------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 恢复 stage 的 scale/alpha 到正常值
|
|
100
|
+
* 避免转场异常时 stage 状态残留(scale=0 或 alpha=0 导致场景不可见)
|
|
101
|
+
* @param {import('pixi.js').Container|null} fromStage - 源场景 stage
|
|
102
|
+
* @param {import('pixi.js').Container|null} toStage - 目标场景 stage
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
_restoreStages(fromStage, toStage) {
|
|
106
|
+
// 源场景:恢复到正常显示状态(转场结束后源场景会被 stop,但状态仍需恢复)
|
|
107
|
+
if (fromStage) {
|
|
108
|
+
fromStage.scale.set(1, 1)
|
|
109
|
+
fromStage.alpha = 1
|
|
110
|
+
}
|
|
111
|
+
// 目标场景:恢复到正常显示状态(scale=1, alpha=1)
|
|
112
|
+
if (toStage) {
|
|
113
|
+
toStage.scale.set(1, 1)
|
|
114
|
+
toStage.alpha = 1
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { ZoomTransition }
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - SceneManager 模块统一导出
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export { SceneManager } from './SceneManager.js'
|
|
6
|
+
export { Scene } from './Scene.js'
|
|
7
|
+
export { SceneState } from './SceneState.js'
|
|
8
|
+
export { Transition } from './Transition.js'
|
|
9
|
+
export { FadeTransition } from './FadeTransition.js'
|
|
10
|
+
export { SlideTransition } from './SlideTransition.js'
|
|
11
|
+
export { MaskTransition } from './MaskTransition.js'
|
|
12
|
+
export { ZoomTransition } from './ZoomTransition.js'
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - SpriteGPULayer GPU 批量精灵容器
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 继承 PIXI.Container,将添加到其中的 Sprite 自动合批 GPU 渲染
|
|
6
|
+
// 2. 按纹理分组管理 Sprite,便于统计 batch 数量
|
|
7
|
+
// 3. 通过 EventBus 广播渲染和容量事件
|
|
8
|
+
// 4. 提供 setTexture 批量替换纹理的能力
|
|
9
|
+
//
|
|
10
|
+
// 设计决策:
|
|
11
|
+
// - 继承 PIXI.Container:利用 PixiJS v8 BatchRenderer 自动合批,无需手动实现
|
|
12
|
+
// - 按纹理分组:以 Texture 实例为 Map key(Map 支持对象 key,O(1) 查找)
|
|
13
|
+
// - 渲染事件:重写 render(),先调用 super.render() 后发射事件(确保渲染完成)
|
|
14
|
+
// - 容量事件:addSprite 时检测纹理组容量达上限立即发射
|
|
15
|
+
// - EventBus 注入:通过 setEventBus 注入,避免构造函数耦合
|
|
16
|
+
//
|
|
17
|
+
// 四期新增(任务 1.1):对标 Phaser 的 SpriteGPULayer
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
import { Container, Sprite } from 'pixi.js'
|
|
21
|
+
import { SpriteGPULayerEventType, SpriteGPULayerError } from './SpriteGPULayerTypes.js'
|
|
22
|
+
|
|
23
|
+
class SpriteGPULayer extends Container {
|
|
24
|
+
/**
|
|
25
|
+
* 构造函数
|
|
26
|
+
* @param {Object} [options={}] - 配置
|
|
27
|
+
* @param {number} [options.maxBatchSize=4096] - 单 batch 最大 Sprite 数
|
|
28
|
+
* 超过此值时发射 gpu:spriteBatch:full 事件(仅提示,不阻止添加)
|
|
29
|
+
* @param {string} [options.label] - 容器标签(用于事件识别)
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
super()
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 单 batch 最大 Sprite 数
|
|
36
|
+
* @type {number}
|
|
37
|
+
* @private
|
|
38
|
+
*/
|
|
39
|
+
this._maxBatchSize = options.maxBatchSize ?? 4096
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 容器标签(用于事件识别,优先使用 options.label,其次 PIXI.label)
|
|
43
|
+
*/
|
|
44
|
+
if (options.label) {
|
|
45
|
+
this.label = options.label
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 管理的 Sprite 列表
|
|
50
|
+
* @type {Sprite[]}
|
|
51
|
+
* @private
|
|
52
|
+
*/
|
|
53
|
+
this._sprites = []
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 按纹理分组的 Sprite 集合
|
|
57
|
+
* key: Texture 实例(Map 支持对象 key)
|
|
58
|
+
* value: Set<Sprite>
|
|
59
|
+
* @type {Map<import('pixi.js').Texture, Set<Sprite>>}
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
this._textureGroups = new Map()
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 事件总线(通过 setEventBus 注入)
|
|
66
|
+
* @type {import('../../core/EventBus/EventBus.js').EventBus|null}
|
|
67
|
+
* @private
|
|
68
|
+
*/
|
|
69
|
+
this._eventBus = null
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 是否已销毁(与 PIXI 的 destroyed 字段独立,用于我们的销毁逻辑)
|
|
73
|
+
* @type {boolean}
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
this._isDestroyed = false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --------------------------------------------------------------------------
|
|
80
|
+
// Sprite 管理
|
|
81
|
+
// --------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 添加 Sprite 到 GPU 层
|
|
85
|
+
* 同纹理 Sprite 由 PixiJS BatchRenderer 自动合批为单个 draw call
|
|
86
|
+
*
|
|
87
|
+
* @param {Sprite} sprite - Sprite 实例
|
|
88
|
+
* @returns {SpriteGPULayer} this(链式调用)
|
|
89
|
+
* @throws {SpriteGPULayerError} sprite 不是 PIXI.Sprite 实例
|
|
90
|
+
*/
|
|
91
|
+
addSprite(sprite) {
|
|
92
|
+
if (this._isDestroyed) return this
|
|
93
|
+
|
|
94
|
+
// 参数校验:必须是 PIXI.Sprite 实例
|
|
95
|
+
if (!(sprite instanceof Sprite)) {
|
|
96
|
+
throw new SpriteGPULayerError('addSprite 参数必须是 PIXI.Sprite 实例')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 防止重复添加:若已存在则直接返回(幂等)
|
|
100
|
+
if (this._sprites.indexOf(sprite) !== -1) {
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @type {import('pixi.js').Texture|null} Sprite 的纹理 */
|
|
105
|
+
const texture = sprite.texture
|
|
106
|
+
if (!texture) {
|
|
107
|
+
throw new SpriteGPULayerError('Sprite 必须有纹理')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 1. 添加到管理列表
|
|
111
|
+
this._sprites.push(sprite)
|
|
112
|
+
|
|
113
|
+
// 2. 按纹理分组(以 Texture 实例为 key)
|
|
114
|
+
let group = this._textureGroups.get(texture)
|
|
115
|
+
if (!group) {
|
|
116
|
+
group = new Set()
|
|
117
|
+
this._textureGroups.set(texture, group)
|
|
118
|
+
}
|
|
119
|
+
group.add(sprite)
|
|
120
|
+
|
|
121
|
+
// 3. 添加到 PIXI.Container(实际合批由 PixiJS BatchRenderer 自动处理)
|
|
122
|
+
this.addChild(sprite)
|
|
123
|
+
|
|
124
|
+
// 4. 检测纹理组是否达到上限,发射容量事件
|
|
125
|
+
if (group.size >= this._maxBatchSize) {
|
|
126
|
+
this._emitEvent(SpriteGPULayerEventType.BATCH_FULL, {
|
|
127
|
+
name: this.label,
|
|
128
|
+
textureGroupSize: group.size,
|
|
129
|
+
maxBatchSize: this._maxBatchSize
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return this
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 从 GPU 层移除 Sprite
|
|
138
|
+
* @param {Sprite} sprite - Sprite 实例
|
|
139
|
+
* @returns {boolean} 是否移除成功
|
|
140
|
+
*/
|
|
141
|
+
removeSprite(sprite) {
|
|
142
|
+
if (this._isDestroyed) return false
|
|
143
|
+
if (!(sprite instanceof Sprite)) return false
|
|
144
|
+
|
|
145
|
+
// 1. 从管理列表移除
|
|
146
|
+
const index = this._sprites.indexOf(sprite)
|
|
147
|
+
if (index === -1) return false
|
|
148
|
+
this._sprites.splice(index, 1)
|
|
149
|
+
|
|
150
|
+
// 2. 从纹理分组移除(处理 texture 被外部修改的情况)
|
|
151
|
+
this._removeSpriteFromGroups(sprite)
|
|
152
|
+
|
|
153
|
+
// 3. 从 PIXI.Container 移除
|
|
154
|
+
this.removeChild(sprite)
|
|
155
|
+
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 批量替换所有 Sprite 的纹理
|
|
161
|
+
* 替换后所有 Sprite 共享同一纹理,重新分组为单个纹理组
|
|
162
|
+
*
|
|
163
|
+
* @param {import('pixi.js').Texture} texture - 新纹理
|
|
164
|
+
*/
|
|
165
|
+
setTexture(texture) {
|
|
166
|
+
if (this._isDestroyed) return
|
|
167
|
+
if (!texture) return
|
|
168
|
+
|
|
169
|
+
// 遍历设置新纹理
|
|
170
|
+
for (let i = 0; i < this._sprites.length; i++) {
|
|
171
|
+
this._sprites[i].texture = texture
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 重新分组:纹理统一后所有 Sprite 在同一组
|
|
175
|
+
this._textureGroups.clear()
|
|
176
|
+
if (this._sprites.length > 0) {
|
|
177
|
+
const newGroup = new Set(this._sprites)
|
|
178
|
+
this._textureGroups.set(texture, newGroup)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --------------------------------------------------------------------------
|
|
183
|
+
// 统计与查询
|
|
184
|
+
// --------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 获取 GPU 层统计信息
|
|
188
|
+
* @returns {{ spriteCount: number, batchCount: number, maxBatchSize: number, textureGroupCount: number }}
|
|
189
|
+
*/
|
|
190
|
+
getStats() {
|
|
191
|
+
return {
|
|
192
|
+
spriteCount: this._sprites.length,
|
|
193
|
+
batchCount: this._textureGroups.size,
|
|
194
|
+
maxBatchSize: this._maxBatchSize,
|
|
195
|
+
textureGroupCount: this._textureGroups.size
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @type {boolean} 是否已销毁 */
|
|
200
|
+
get isDestroyed() {
|
|
201
|
+
return this._isDestroyed
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --------------------------------------------------------------------------
|
|
205
|
+
// EventBus 注入
|
|
206
|
+
// --------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 注入事件总线
|
|
210
|
+
* 由 Scene 或用户调用,注入后才能发射 gpu:spriteBatch:* 事件
|
|
211
|
+
*
|
|
212
|
+
* @param {import('../../core/EventBus/EventBus.js').EventBus} eventBus - 事件总线实例
|
|
213
|
+
*/
|
|
214
|
+
setEventBus(eventBus) {
|
|
215
|
+
this._eventBus = eventBus
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --------------------------------------------------------------------------
|
|
219
|
+
// 渲染钩子(重写 PIXI.Container.render)
|
|
220
|
+
// --------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 重写 PIXI.Container.render
|
|
224
|
+
* 先调用 super.render(renderer) 完成 PIXI 标准渲染流程,
|
|
225
|
+
* 然后发射 gpu:spriteBatch:rendered 事件(仅在 EventBus 已注入时)
|
|
226
|
+
*
|
|
227
|
+
* @param {import('pixi.js').Renderer} renderer - 渲染器
|
|
228
|
+
*/
|
|
229
|
+
render(renderer) {
|
|
230
|
+
// 先完成 PIXI 标准渲染(含可见性检查、变换、子节点渲染等)
|
|
231
|
+
super.render(renderer)
|
|
232
|
+
|
|
233
|
+
// 渲染完成后发射事件(仅当 EventBus 已注入)
|
|
234
|
+
if (this._eventBus) {
|
|
235
|
+
this._emitEvent(SpriteGPULayerEventType.RENDERED, {
|
|
236
|
+
name: this.label,
|
|
237
|
+
spriteCount: this._sprites.length,
|
|
238
|
+
batchCount: this._textureGroups.size
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --------------------------------------------------------------------------
|
|
244
|
+
// 私有方法
|
|
245
|
+
// --------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 发射事件(内部方法)
|
|
249
|
+
* @param {string} eventType - 事件类型
|
|
250
|
+
* @param {Object} payload - 事件数据
|
|
251
|
+
* @private
|
|
252
|
+
*/
|
|
253
|
+
_emitEvent(eventType, payload) {
|
|
254
|
+
if (this._eventBus) {
|
|
255
|
+
this._eventBus.emit(eventType, payload)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 从纹理分组中移除 Sprite(内部方法)
|
|
261
|
+
*
|
|
262
|
+
* 处理 sprite.texture 被外部修改导致用当前 texture 找不到原组的情况:
|
|
263
|
+
* 1. 先用 sprite.texture 快速查找(O(1))—— 覆盖正常场景
|
|
264
|
+
* 2. 快速查找失败时遍历所有组(O(n))—— 覆盖 texture 被外部修改场景
|
|
265
|
+
*
|
|
266
|
+
* 这样保证 _textureGroups 中不会残留 sprite 引用,避免内存泄漏
|
|
267
|
+
*
|
|
268
|
+
* @param {Sprite} sprite - Sprite 实例
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_removeSpriteFromGroups(sprite) {
|
|
272
|
+
// 1. 先用当前 texture 快速查找(正常场景,O(1))
|
|
273
|
+
const texture = sprite.texture
|
|
274
|
+
if (texture) {
|
|
275
|
+
const group = this._textureGroups.get(texture)
|
|
276
|
+
if (group && group.has(sprite)) {
|
|
277
|
+
group.delete(sprite)
|
|
278
|
+
// 纹理组为空时清理,避免 Map 无限增长
|
|
279
|
+
if (group.size === 0) {
|
|
280
|
+
this._textureGroups.delete(texture)
|
|
281
|
+
}
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 2. 快速查找失败,遍历所有组查找(texture 被外部修改场景,O(n))
|
|
287
|
+
for (const [key, group] of this._textureGroups) {
|
|
288
|
+
if (group.has(sprite)) {
|
|
289
|
+
group.delete(sprite)
|
|
290
|
+
if (group.size === 0) {
|
|
291
|
+
this._textureGroups.delete(key)
|
|
292
|
+
}
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// --------------------------------------------------------------------------
|
|
299
|
+
// 销毁
|
|
300
|
+
// --------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 销毁 GPU 层
|
|
304
|
+
* 清理所有内部引用和 PIXI 资源
|
|
305
|
+
* @param {import('pixi.js').DestroyOptions|boolean} [options] - PIXI 销毁选项
|
|
306
|
+
*/
|
|
307
|
+
destroy(options) {
|
|
308
|
+
if (this._isDestroyed) return
|
|
309
|
+
this._isDestroyed = true
|
|
310
|
+
|
|
311
|
+
// 清理内部引用
|
|
312
|
+
this._sprites.length = 0
|
|
313
|
+
this._textureGroups.clear()
|
|
314
|
+
this._eventBus = null
|
|
315
|
+
|
|
316
|
+
// 调用 PIXI.Container.destroy 销毁子节点和资源
|
|
317
|
+
super.destroy(options)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export { SpriteGPULayer }
|