@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,534 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - Camera 高级视口相机
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 封装 PIXI.Container 作为视口容器,提供世界坐标系到屏幕坐标系的变换
|
|
6
|
+
// 2. 提供跟随目标(lerp 插值 + 死区)能力
|
|
7
|
+
// 3. 提供屏幕抖动(衰减 + 时间约束)能力
|
|
8
|
+
// 4. 提供缩放、旋转、边界约束能力
|
|
9
|
+
// 5. 通过 EventBus 广播相机事件(follow/shake/moved)
|
|
10
|
+
//
|
|
11
|
+
// 设计决策:
|
|
12
|
+
// - 不继承 PIXI 类:独立设计,内部持有 PIXI.Container 作为视口
|
|
13
|
+
// 好处是 Camera 不污染 PIXI 命名空间,可独立测试和复用
|
|
14
|
+
// - lerp 插值帧率无关:使用 `1 - Math.pow(1 - lerp, delta * 60)` 公式
|
|
15
|
+
// 确保不同帧率下行为一致(以 60fps 为基准)
|
|
16
|
+
// - 事件发射防重:每帧最多发射一次 camera:moved 事件(通过 _movedEmitted 标记)
|
|
17
|
+
// 避免同一帧内 setter 与 update 重复发射事件
|
|
18
|
+
// - EventBus 注入:通过公开方法 setEventBus 注入(不放在构造函数)
|
|
19
|
+
// 避免 Camera 构造时强制依赖 EventBus,符合依赖注入最佳实践
|
|
20
|
+
// - 销毁幂等:通过 _isDestroyed 标记避免重复销毁导致崩溃
|
|
21
|
+
// - zoom 最小值保护:Math.max(0.01, val) 防止零或负值导致渲染异常
|
|
22
|
+
// - follow 参数校验:避免传入无效目标导致后续 update 中 NaN 污染
|
|
23
|
+
//
|
|
24
|
+
// 四期新增(任务 1.4):对标 Phaser 的 Camera 系统
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
import { Container } from 'pixi.js'
|
|
28
|
+
import { CameraEventType, CameraError } from './CameraTypes.js'
|
|
29
|
+
|
|
30
|
+
class Camera {
|
|
31
|
+
// --------------------------------------------------------------------------
|
|
32
|
+
// 构造函数
|
|
33
|
+
// --------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 创建 Camera 实例
|
|
37
|
+
* @param {Object} [options={}] - 配置
|
|
38
|
+
* @param {number} [options.width=0] - 视口宽度(建议传入 PIXI 屏幕宽度)
|
|
39
|
+
* @param {number} [options.height=0] - 视口高度(建议传入 PIXI 屏幕高度)
|
|
40
|
+
* @param {number} [options.x=0] - 初始 X 位置(世界坐标)
|
|
41
|
+
* @param {number} [options.y=0] - 初始 Y 位置(世界坐标)
|
|
42
|
+
* @param {number} [options.zoom=1] - 初始缩放系数(>0,最小 0.01)
|
|
43
|
+
* @param {number} [options.rotation=0] - 初始旋转角度(弧度)
|
|
44
|
+
*/
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
/** @type {number} 相机在世界坐标系的 X 位置(左上角) */
|
|
47
|
+
this._x = options.x ?? 0
|
|
48
|
+
/** @type {number} 相机在世界坐标系的 Y 位置(左上角) */
|
|
49
|
+
this._y = options.y ?? 0
|
|
50
|
+
/** @type {number} 视口宽度(用于跟随计算居中点) */
|
|
51
|
+
this._width = options.width ?? 0
|
|
52
|
+
/** @type {number} 视口高度(用于跟随计算居中点) */
|
|
53
|
+
this._height = options.height ?? 0
|
|
54
|
+
/** @type {number} 缩放系数(>0) */
|
|
55
|
+
this._zoom = Math.max(0.01, options.zoom ?? 1)
|
|
56
|
+
/** @type {number} 旋转角度(弧度) */
|
|
57
|
+
this._rotation = options.rotation ?? 0
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PIXI 视口容器
|
|
61
|
+
* 所有世界元素应添加到此容器的子级,相机的变换会自动应用到子级
|
|
62
|
+
* 通过 viewport.position/scale/rotation 实现世界坐标到屏幕坐标的变换
|
|
63
|
+
* @type {import('pixi.js').Container}
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
this._viewport = new Container()
|
|
67
|
+
this._viewport.label = 'nimbus_camera_viewport'
|
|
68
|
+
|
|
69
|
+
// === 跟随状态 ===
|
|
70
|
+
/**
|
|
71
|
+
* 跟随目标对象(需有 x/y 属性)
|
|
72
|
+
* @type {Object|null}
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
this._followTarget = null
|
|
76
|
+
/**
|
|
77
|
+
* 跟随插值系数(0=不跟随,1=立即跟随)
|
|
78
|
+
* @type {number}
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
this._followLerp = 1
|
|
82
|
+
/**
|
|
83
|
+
* 死区矩形 { x, y, width, height }
|
|
84
|
+
* 目标在死区内时相机不移动,超出死区时相机跟随
|
|
85
|
+
* @type {{x:number,y:number,width:number,height:number}|null}
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
this._deadzone = null
|
|
89
|
+
|
|
90
|
+
// === 抖动状态 ===
|
|
91
|
+
/** @type {number} 抖动强度(像素) */
|
|
92
|
+
this._shakeIntensity = 0
|
|
93
|
+
/** @type {number} 抖动持续时间(毫秒) */
|
|
94
|
+
this._shakeDuration = 0
|
|
95
|
+
/** @type {number} 抖动衰减系数(每帧乘以此值,0-1) */
|
|
96
|
+
this._shakeDecay = 0.95
|
|
97
|
+
/** @type {number} 抖动 X 偏移(像素,当前帧) */
|
|
98
|
+
this._shakeOffsetX = 0
|
|
99
|
+
/** @type {number} 抖动 Y 偏移(像素,当前帧) */
|
|
100
|
+
this._shakeOffsetY = 0
|
|
101
|
+
/** @type {number} 抖动已经过时间(毫秒) */
|
|
102
|
+
this._shakeElapsed = 0
|
|
103
|
+
|
|
104
|
+
// === 边界约束 ===
|
|
105
|
+
/**
|
|
106
|
+
* 相机活动边界 { x, y, width, height }
|
|
107
|
+
* 设置后相机位置会被约束在边界内
|
|
108
|
+
* @type {{x:number,y:number,width:number,height:number}|null}
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
this._bounds = null
|
|
112
|
+
|
|
113
|
+
// === 事件相关 ===
|
|
114
|
+
/**
|
|
115
|
+
* camera:moved 事件防重标记
|
|
116
|
+
* 每帧开始时重置为 false,发射后置 true,确保每帧最多发射一次
|
|
117
|
+
* @type {boolean}
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
this._movedEmitted = false
|
|
121
|
+
/**
|
|
122
|
+
* 事件总线(通过 setEventBus 注入)
|
|
123
|
+
* @type {import('../../core/EventBus/EventBus.js').EventBus|null}
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
this._eventBus = null
|
|
127
|
+
|
|
128
|
+
/** @type {boolean} 是否已销毁(用于幂等销毁) */
|
|
129
|
+
this._isDestroyed = false
|
|
130
|
+
|
|
131
|
+
// 构造完成后立即应用一次视口变换,确保初始状态正确
|
|
132
|
+
this._updateViewport()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// --------------------------------------------------------------------------
|
|
136
|
+
// 公共访问器:位置
|
|
137
|
+
// --------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/** @type {number} 相机 X 位置(世界坐标) */
|
|
140
|
+
get x() { return this._x }
|
|
141
|
+
set x(val) {
|
|
142
|
+
// 销毁后保护:避免访问已销毁的 _viewport 导致崩溃
|
|
143
|
+
if (this._isDestroyed) return
|
|
144
|
+
this._x = val
|
|
145
|
+
this._updateViewport()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** @type {number} 相机 Y 位置(世界坐标) */
|
|
149
|
+
get y() { return this._y }
|
|
150
|
+
set y(val) {
|
|
151
|
+
// 销毁后保护:避免访问已销毁的 _viewport 导致崩溃
|
|
152
|
+
if (this._isDestroyed) return
|
|
153
|
+
this._y = val
|
|
154
|
+
this._updateViewport()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --------------------------------------------------------------------------
|
|
158
|
+
// 公共访问器:尺寸
|
|
159
|
+
// --------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
/** @type {number} 视口宽度 */
|
|
162
|
+
get width() { return this._width }
|
|
163
|
+
|
|
164
|
+
/** @type {number} 视口高度 */
|
|
165
|
+
get height() { return this._height }
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 设置视口尺寸(用于窗口大小变化时同步更新)
|
|
169
|
+
* @param {number} width - 新宽度
|
|
170
|
+
* @param {number} height - 新高度
|
|
171
|
+
*/
|
|
172
|
+
setSize(width, height) {
|
|
173
|
+
if (this._isDestroyed) return
|
|
174
|
+
this._width = width
|
|
175
|
+
this._height = height
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// --------------------------------------------------------------------------
|
|
179
|
+
// 公共访问器:缩放与旋转
|
|
180
|
+
// --------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
/** @type {number} 缩放系数 */
|
|
183
|
+
get zoom() { return this._zoom }
|
|
184
|
+
set zoom(val) {
|
|
185
|
+
// 销毁后保护:避免访问已销毁的 _viewport 导致崩溃
|
|
186
|
+
if (this._isDestroyed) return
|
|
187
|
+
// 最小值保护,避免零或负值导致渲染异常
|
|
188
|
+
this._zoom = Math.max(0.01, val)
|
|
189
|
+
this._updateViewport()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** @type {number} 旋转角度(弧度) */
|
|
193
|
+
get rotation() { return this._rotation }
|
|
194
|
+
set rotation(val) {
|
|
195
|
+
// 销毁后保护:避免访问已销毁的 _viewport 导致崩溃
|
|
196
|
+
if (this._isDestroyed) return
|
|
197
|
+
this._rotation = val
|
|
198
|
+
this._updateViewport()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --------------------------------------------------------------------------
|
|
202
|
+
// 公共访问器:状态与视口
|
|
203
|
+
// --------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/** @type {boolean} 是否正在抖动 */
|
|
206
|
+
get isShaking() { return this._shakeIntensity > 0 }
|
|
207
|
+
|
|
208
|
+
/** @type {boolean} 是否已销毁 */
|
|
209
|
+
get isDestroyed() { return this._isDestroyed }
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* PIXI 视口容器(用于添加到场景 stage)
|
|
213
|
+
* 用户应将此容器添加到 PIXI stage,并将世界元素添加到此容器的子级
|
|
214
|
+
* @type {import('pixi.js').Container}
|
|
215
|
+
*/
|
|
216
|
+
get viewport() { return this._viewport }
|
|
217
|
+
|
|
218
|
+
// --------------------------------------------------------------------------
|
|
219
|
+
// 跟随
|
|
220
|
+
// --------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 跟随目标对象
|
|
224
|
+
* @param {Object} target - 目标对象(需有数值类型的 x/y 属性)
|
|
225
|
+
* @param {Object} [options={}] - 跟随选项
|
|
226
|
+
* @param {number} [options.lerp=1] - 插值系数(0=不跟随,1=立即跟随)
|
|
227
|
+
* @param {{x:number,y:number,width:number,height:number}} [options.deadzone] - 死区矩形
|
|
228
|
+
* @throws {CameraError} target 为 null/undefined 或缺少数值类型的 x/y 属性
|
|
229
|
+
*/
|
|
230
|
+
follow(target, options = {}) {
|
|
231
|
+
if (this._isDestroyed) return
|
|
232
|
+
|
|
233
|
+
// 参数校验:防止无效目标导致后续 update 中 NaN 污染
|
|
234
|
+
if (!target || typeof target.x !== 'number' || typeof target.y !== 'number') {
|
|
235
|
+
throw new CameraError('follow 目标必须包含数值类型的 x/y 属性')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this._followTarget = target
|
|
239
|
+
|
|
240
|
+
// 插值系数范围约束 [0, 1]
|
|
241
|
+
const lerp = options.lerp ?? 1
|
|
242
|
+
this._followLerp = Math.max(0, Math.min(1, lerp))
|
|
243
|
+
|
|
244
|
+
this._deadzone = options.deadzone ?? null
|
|
245
|
+
|
|
246
|
+
this._emitEvent(CameraEventType.FOLLOW_CHANGED, {
|
|
247
|
+
target,
|
|
248
|
+
lerp: this._followLerp,
|
|
249
|
+
deadzone: this._deadzone
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 停止跟随 */
|
|
254
|
+
stopFollow() {
|
|
255
|
+
this._followTarget = null
|
|
256
|
+
this._deadzone = null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --------------------------------------------------------------------------
|
|
260
|
+
// 抖动
|
|
261
|
+
// --------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 触发抖动效果
|
|
265
|
+
* 若已有抖动进行中,新参数会覆盖旧参数(不发射 SHAKE_END,直接重新开始)
|
|
266
|
+
*
|
|
267
|
+
* @param {Object} [options={}] - 抖动选项
|
|
268
|
+
* @param {number} [options.intensity=10] - 抖动强度(像素)
|
|
269
|
+
* @param {number} [options.duration=500] - 持续时间(毫秒)
|
|
270
|
+
* @param {number} [options.decay=0.95] - 衰减系数(0-1,越小衰减越快)
|
|
271
|
+
*/
|
|
272
|
+
shake(options = {}) {
|
|
273
|
+
if (this._isDestroyed) return
|
|
274
|
+
|
|
275
|
+
this._shakeIntensity = options.intensity ?? 10
|
|
276
|
+
this._shakeDuration = options.duration ?? 500
|
|
277
|
+
this._shakeDecay = options.decay ?? 0.95
|
|
278
|
+
this._shakeElapsed = 0
|
|
279
|
+
this._shakeOffsetX = 0
|
|
280
|
+
this._shakeOffsetY = 0
|
|
281
|
+
|
|
282
|
+
this._emitEvent(CameraEventType.SHAKE_START, {
|
|
283
|
+
intensity: this._shakeIntensity,
|
|
284
|
+
duration: this._shakeDuration
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// --------------------------------------------------------------------------
|
|
289
|
+
// 边界约束
|
|
290
|
+
// --------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 设置相机活动边界
|
|
294
|
+
* 设置后相机位置会被约束在 [bounds.x, bounds.x + bounds.width - width] 范围内
|
|
295
|
+
* @param {{x:number,y:number,width:number,height:number}} bounds - 边界矩形
|
|
296
|
+
*/
|
|
297
|
+
setBounds(bounds) {
|
|
298
|
+
if (this._isDestroyed) return
|
|
299
|
+
this._bounds = bounds
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** 移除边界约束 */
|
|
303
|
+
removeBounds() {
|
|
304
|
+
this._bounds = null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// --------------------------------------------------------------------------
|
|
308
|
+
// 帧更新
|
|
309
|
+
// --------------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 每帧更新(由 Scene.update() 调用)
|
|
313
|
+
* @param {number} delta - 帧间隔(秒,建议使用 PixiJS ticker.deltaMS / 1000)
|
|
314
|
+
*/
|
|
315
|
+
update(delta) {
|
|
316
|
+
if (this._isDestroyed) return
|
|
317
|
+
|
|
318
|
+
// 0. 每帧开始重置事件发射标记
|
|
319
|
+
this._movedEmitted = false
|
|
320
|
+
|
|
321
|
+
// 1. 跟随逻辑(更新 _x/_y)
|
|
322
|
+
this._updateFollow(delta)
|
|
323
|
+
|
|
324
|
+
// 2. 抖动逻辑(更新 _shakeOffsetX/_shakeOffsetY)
|
|
325
|
+
this._updateShake(delta)
|
|
326
|
+
|
|
327
|
+
// 3. 边界约束(约束 _x/_y 到 _bounds 范围内)
|
|
328
|
+
this._applyBounds()
|
|
329
|
+
|
|
330
|
+
// 4. 更新视口变换(同步到 PIXI 容器,并发射 camera:moved 事件)
|
|
331
|
+
this._updateViewport()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --------------------------------------------------------------------------
|
|
335
|
+
// EventBus 注入
|
|
336
|
+
// --------------------------------------------------------------------------
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* 注入事件总线
|
|
340
|
+
* 由 Scene 或用户调用,注入后才能发射 camera:* 事件
|
|
341
|
+
*
|
|
342
|
+
* 注意:设计文档中此方法名为 _setEventBus(下划线前缀),
|
|
343
|
+
* 但根据任务 1.1 的经验,需要外部调用的方法应为公开方法,
|
|
344
|
+
* 故改为 setEventBus(与 SpriteGPULayer.setEventBus 保持一致)
|
|
345
|
+
*
|
|
346
|
+
* @param {import('../../core/EventBus/EventBus.js').EventBus} eventBus - 事件总线实例
|
|
347
|
+
*/
|
|
348
|
+
setEventBus(eventBus) {
|
|
349
|
+
this._eventBus = eventBus
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// --------------------------------------------------------------------------
|
|
353
|
+
// 私有方法:跟随更新
|
|
354
|
+
// --------------------------------------------------------------------------
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 跟随更新(含 lerp 插值与死区处理)
|
|
358
|
+
* @param {number} delta - 帧间隔(秒)
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
_updateFollow(delta) {
|
|
362
|
+
if (!this._followTarget) return
|
|
363
|
+
|
|
364
|
+
// 计算目标位置(让目标居中显示在视口中央)
|
|
365
|
+
let targetX = this._followTarget.x - this._width / 2
|
|
366
|
+
let targetY = this._followTarget.y - this._height / 2
|
|
367
|
+
|
|
368
|
+
// 死区处理:目标在死区内时相机不移动
|
|
369
|
+
if (this._deadzone) {
|
|
370
|
+
const dz = this._deadzone
|
|
371
|
+
const dx = this._followTarget.x - this._x - this._width / 2
|
|
372
|
+
const dy = this._followTarget.y - this._y - this._height / 2
|
|
373
|
+
|
|
374
|
+
if (Math.abs(dx) > dz.width / 2) {
|
|
375
|
+
targetX = this._followTarget.x - (dz.width / 2 + dz.x) * Math.sign(dx)
|
|
376
|
+
} else {
|
|
377
|
+
targetX = this._x
|
|
378
|
+
}
|
|
379
|
+
if (Math.abs(dy) > dz.height / 2) {
|
|
380
|
+
targetY = this._followTarget.y - (dz.height / 2 + dz.y) * Math.sign(dy)
|
|
381
|
+
} else {
|
|
382
|
+
targetY = this._y
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// lerp 插值(帧率无关公式)
|
|
387
|
+
// 假设 60fps 为基准:delta=1/60 时,lerpFactor = lerp
|
|
388
|
+
// 其他帧率下通过 Math.pow 调整,确保行为一致
|
|
389
|
+
const lerpFactor = 1 - Math.pow(1 - this._followLerp, delta * 60)
|
|
390
|
+
this._x += (targetX - this._x) * lerpFactor
|
|
391
|
+
this._y += (targetY - this._y) * lerpFactor
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// --------------------------------------------------------------------------
|
|
395
|
+
// 私有方法:抖动更新
|
|
396
|
+
// --------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* 抖动更新
|
|
400
|
+
* @param {number} delta - 帧间隔(秒)
|
|
401
|
+
* @private
|
|
402
|
+
*/
|
|
403
|
+
_updateShake(delta) {
|
|
404
|
+
if (this._shakeIntensity <= 0) return
|
|
405
|
+
|
|
406
|
+
// 累计已经过时间(毫秒)
|
|
407
|
+
this._shakeElapsed += delta * 1000
|
|
408
|
+
|
|
409
|
+
// 超过持续时间,结束抖动
|
|
410
|
+
if (this._shakeElapsed >= this._shakeDuration) {
|
|
411
|
+
this._shakeIntensity = 0
|
|
412
|
+
this._shakeOffsetX = 0
|
|
413
|
+
this._shakeOffsetY = 0
|
|
414
|
+
this._emitEvent(CameraEventType.SHAKE_END, {})
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// 随机偏移(-1 到 1 乘以强度)
|
|
419
|
+
this._shakeOffsetX = (Math.random() * 2 - 1) * this._shakeIntensity
|
|
420
|
+
this._shakeOffsetY = (Math.random() * 2 - 1) * this._shakeIntensity
|
|
421
|
+
|
|
422
|
+
// 强度衰减(每帧乘以衰减系数)
|
|
423
|
+
this._shakeIntensity *= this._shakeDecay
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// --------------------------------------------------------------------------
|
|
427
|
+
// 私有方法:边界约束
|
|
428
|
+
// --------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 应用边界约束
|
|
432
|
+
* 将 _x/_y 约束在 _bounds 范围内
|
|
433
|
+
* @private
|
|
434
|
+
*/
|
|
435
|
+
_applyBounds() {
|
|
436
|
+
if (!this._bounds) return
|
|
437
|
+
|
|
438
|
+
// 限制 X 范围:[bounds.x, bounds.x + bounds.width - width]
|
|
439
|
+
this._x = Math.max(
|
|
440
|
+
this._bounds.x,
|
|
441
|
+
Math.min(this._x, this._bounds.x + this._bounds.width - this._width)
|
|
442
|
+
)
|
|
443
|
+
// 限制 Y 范围:[bounds.y, bounds.y + bounds.height - height]
|
|
444
|
+
this._y = Math.max(
|
|
445
|
+
this._bounds.y,
|
|
446
|
+
Math.min(this._y, this._bounds.y + this._bounds.height - this._height)
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// --------------------------------------------------------------------------
|
|
451
|
+
// 私有方法:视口变换更新
|
|
452
|
+
// --------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 更新 PIXI 视口变换
|
|
456
|
+
* 注意:此方法在 update() 和 x/y/zoom/rotation setter 中均被调用,
|
|
457
|
+
* 为避免同一帧内多次发射 camera:moved 事件,使用 _movedEmitted 标记防重。
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
_updateViewport() {
|
|
461
|
+
// 同步位置(含抖动偏移)
|
|
462
|
+
this._viewport.position.set(
|
|
463
|
+
-this._x + this._shakeOffsetX,
|
|
464
|
+
-this._y + this._shakeOffsetY
|
|
465
|
+
)
|
|
466
|
+
// 同步缩放
|
|
467
|
+
this._viewport.scale.set(this._zoom, this._zoom)
|
|
468
|
+
// 同步旋转
|
|
469
|
+
this._viewport.rotation = this._rotation
|
|
470
|
+
|
|
471
|
+
// 发射 camera:moved 事件(每帧最多一次)
|
|
472
|
+
if (this._eventBus && !this._movedEmitted) {
|
|
473
|
+
this._emitEvent(CameraEventType.MOVED, { x: this._x, y: this._y })
|
|
474
|
+
this._movedEmitted = true
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// --------------------------------------------------------------------------
|
|
479
|
+
// 私有方法:事件发射
|
|
480
|
+
// --------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* 发射事件(内部方法,安全包装,避免 emit 异常影响主流程)
|
|
484
|
+
* @param {string} eventType - 事件类型
|
|
485
|
+
* @param {Object} payload - 事件数据
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
488
|
+
_emitEvent(eventType, payload) {
|
|
489
|
+
if (this._eventBus) {
|
|
490
|
+
try {
|
|
491
|
+
this._eventBus.emit(eventType, payload)
|
|
492
|
+
} catch (e) {
|
|
493
|
+
// 事件发射异常不影响相机主流程
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// --------------------------------------------------------------------------
|
|
499
|
+
// 销毁
|
|
500
|
+
// --------------------------------------------------------------------------
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 销毁相机
|
|
504
|
+
* 清理所有内部引用和 PIXI 资源(幂等,可重复调用)
|
|
505
|
+
*/
|
|
506
|
+
destroy() {
|
|
507
|
+
if (this._isDestroyed) return
|
|
508
|
+
this._isDestroyed = true
|
|
509
|
+
|
|
510
|
+
// 停止跟随
|
|
511
|
+
this.stopFollow()
|
|
512
|
+
|
|
513
|
+
// 清理抖动状态
|
|
514
|
+
this._shakeIntensity = 0
|
|
515
|
+
this._shakeOffsetX = 0
|
|
516
|
+
this._shakeOffsetY = 0
|
|
517
|
+
this._shakeDuration = 0
|
|
518
|
+
this._shakeElapsed = 0
|
|
519
|
+
|
|
520
|
+
// 清理边界
|
|
521
|
+
this._bounds = null
|
|
522
|
+
|
|
523
|
+
// 销毁视口容器(PIXI 资源)
|
|
524
|
+
if (this._viewport) {
|
|
525
|
+
this._viewport.destroy()
|
|
526
|
+
this._viewport = null
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 清理事件总线引用(避免内存泄漏)
|
|
530
|
+
this._eventBus = null
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export { Camera }
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - Camera 类型定义
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 定义 Camera 相关的事件类型枚举(CameraEventType)
|
|
6
|
+
// 2. 定义 Camera 错误类(CameraError)
|
|
7
|
+
//
|
|
8
|
+
// 设计决策:
|
|
9
|
+
// - 事件类型使用 Object.freeze 冻结,防止运行时被篡改
|
|
10
|
+
// - 错误类继承 NimbusError,附加 module='Camera' 字段,便于统一错误识别
|
|
11
|
+
// - 与 SpriteGPULayer/PoolManager 的类型定义风格保持一致
|
|
12
|
+
//
|
|
13
|
+
// 四期新增(任务 1.4):Camera 系统的类型定义
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
import { NimbusError } from '../../errors/NimbusError.js'
|
|
17
|
+
|
|
18
|
+
// ----------------------------------------------------------------------------
|
|
19
|
+
// 事件类型枚举
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Camera 事件类型
|
|
24
|
+
* @enum {string}
|
|
25
|
+
* @readonly
|
|
26
|
+
*/
|
|
27
|
+
const CameraEventType = Object.freeze({
|
|
28
|
+
/** 跟随目标改变时发射(follow/stopFollow) */
|
|
29
|
+
FOLLOW_CHANGED: 'camera:follow:changed',
|
|
30
|
+
/** 抖动开始时发射(shake 调用瞬间) */
|
|
31
|
+
SHAKE_START: 'camera:shake:start',
|
|
32
|
+
/** 抖动结束时发射(持续时间到或强度衰减完毕) */
|
|
33
|
+
SHAKE_END: 'camera:shake:end',
|
|
34
|
+
/** 相机位置变化时发射(每帧最多一次,防重) */
|
|
35
|
+
MOVED: 'camera:moved'
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// ----------------------------------------------------------------------------
|
|
39
|
+
// 错误类
|
|
40
|
+
// ----------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Camera 错误基类
|
|
44
|
+
* 所有 Camera 错误构造函数设置 module='Camera'
|
|
45
|
+
* @extends NimbusError
|
|
46
|
+
*/
|
|
47
|
+
class CameraError extends NimbusError {
|
|
48
|
+
/**
|
|
49
|
+
* 构造函数
|
|
50
|
+
* @param {string} message - 错误消息
|
|
51
|
+
* @param {string} [code='CAMERA_ERROR'] - 错误码
|
|
52
|
+
* @param {Error|null} [cause=null] - 原始错误
|
|
53
|
+
*/
|
|
54
|
+
constructor(message, code = 'CAMERA_ERROR', cause = null) {
|
|
55
|
+
super(message, code, 'Camera', cause)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { CameraEventType, CameraError }
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Nimbus2D - Camera 模块入口
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 导出高级视口相机的所有公开 API
|
|
5
|
+
//
|
|
6
|
+
// 四期新增(任务 1.4):Camera 系统模块入口
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
// Camera 主类
|
|
10
|
+
export { Camera } from './Camera.js'
|
|
11
|
+
|
|
12
|
+
// 类型定义(事件枚举 + 错误类)
|
|
13
|
+
export { CameraEventType, CameraError } from './CameraTypes.js'
|