kiwiengine 0.7.0 → 1.0.0
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/lib/dom/dom-particle.js +23 -74
- package/lib/dom/dom-particle.js.map +1 -1
- package/lib/node/core/game-object.js +1 -4
- package/lib/node/core/game-object.js.map +1 -1
- package/lib/node/core/renderable.js +6 -4
- package/lib/node/core/renderable.js.map +1 -1
- package/lib/node/core/transformable.js +11 -42
- package/lib/node/core/transformable.js.map +1 -1
- package/lib/node/ext/animated-sprite.js +1 -8
- package/lib/node/ext/animated-sprite.js.map +1 -1
- package/lib/node/ext/bitmap-text.js +4 -61
- package/lib/node/ext/bitmap-text.js.map +1 -1
- package/lib/node/ext/circle.js +1 -2
- package/lib/node/ext/circle.js.map +1 -1
- package/lib/node/ext/particle.js +15 -65
- package/lib/node/ext/particle.js.map +1 -1
- package/lib/node/ext/rectangle.js +1 -2
- package/lib/node/ext/rectangle.js.map +1 -1
- package/lib/renderer/camera.js +0 -8
- package/lib/renderer/camera.js.map +1 -1
- package/lib/renderer/renderer.js +2 -7
- package/lib/renderer/renderer.js.map +1 -1
- package/lib/types/dom/dom-particle.d.ts +0 -1
- package/lib/types/dom/dom-particle.d.ts.map +1 -1
- package/lib/types/node/core/game-object.d.ts +1 -3
- package/lib/types/node/core/game-object.d.ts.map +1 -1
- package/lib/types/node/core/renderable.d.ts +1 -0
- package/lib/types/node/core/renderable.d.ts.map +1 -1
- package/lib/types/node/core/transformable.d.ts.map +1 -1
- package/lib/types/node/ext/animated-sprite.d.ts.map +1 -1
- package/lib/types/node/ext/bitmap-text.d.ts.map +1 -1
- package/lib/types/node/ext/circle.d.ts.map +1 -1
- package/lib/types/node/ext/particle.d.ts +0 -1
- package/lib/types/node/ext/particle.d.ts.map +1 -1
- package/lib/types/node/ext/rectangle.d.ts.map +1 -1
- package/lib/types/renderer/camera.d.ts.map +1 -1
- package/lib/types/renderer/renderer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dom/dom-particle.ts +24 -91
- package/src/node/core/game-object.ts +2 -10
- package/src/node/core/renderable.ts +5 -4
- package/src/node/core/transformable.ts +11 -49
- package/src/node/ext/animated-sprite.ts +1 -10
- package/src/node/ext/bitmap-text.ts +4 -70
- package/src/node/ext/circle.ts +1 -2
- package/src/node/ext/particle.ts +16 -80
- package/src/node/ext/rectangle.ts +1 -2
- package/src/renderer/camera.ts +0 -6
- package/src/renderer/renderer.ts +2 -9
package/src/dom/dom-particle.ts
CHANGED
|
@@ -19,20 +19,11 @@ export type DomParticleSystemOptions = {
|
|
|
19
19
|
orientToVelocity: boolean
|
|
20
20
|
|
|
21
21
|
blendMode?: BLEND_MODES // ex) 'screen', 'multiply'
|
|
22
|
-
|
|
23
|
-
// [성능 최적화] 오브젝트 풀 크기 설정 (기본값: 100)
|
|
24
|
-
poolSize?: number
|
|
25
22
|
} & DomGameObjectOptions
|
|
26
23
|
|
|
27
24
|
interface Particle {
|
|
28
25
|
el: HTMLDivElement
|
|
29
26
|
|
|
30
|
-
// [성능 최적화] 활성 상태 플래그 및 위치/투명도 캐시
|
|
31
|
-
active: boolean
|
|
32
|
-
x: number
|
|
33
|
-
y: number
|
|
34
|
-
opacity: number
|
|
35
|
-
|
|
36
27
|
age: number
|
|
37
28
|
lifespan: number
|
|
38
29
|
|
|
@@ -62,10 +53,6 @@ export class DomParticleSystem extends DomGameObject {
|
|
|
62
53
|
#loadTexturePromise: Promise<void>
|
|
63
54
|
#particles: Particle[] = []
|
|
64
55
|
|
|
65
|
-
// [성능 최적화] DOM 엘리먼트 풀 - 재사용으로 DOM 조작 및 GC 부담 감소
|
|
66
|
-
#elementPool: HTMLDivElement[] = []
|
|
67
|
-
#poolSize: number
|
|
68
|
-
|
|
69
56
|
constructor(options: DomParticleSystemOptions) {
|
|
70
57
|
super(options)
|
|
71
58
|
this.el.style.pointerEvents = 'none'
|
|
@@ -80,7 +67,6 @@ export class DomParticleSystem extends DomGameObject {
|
|
|
80
67
|
this.#fadeRate = options.fadeRate
|
|
81
68
|
this.#orientToVelocity = options.orientToVelocity
|
|
82
69
|
this.#blendMode = options.blendMode
|
|
83
|
-
this.#poolSize = options.poolSize ?? 100
|
|
84
70
|
|
|
85
71
|
this.#loadTexturePromise = this.#loadTexture()
|
|
86
72
|
}
|
|
@@ -94,22 +80,19 @@ export class DomParticleSystem extends DomGameObject {
|
|
|
94
80
|
}
|
|
95
81
|
}
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
let el = this.#elementPool.pop()
|
|
83
|
+
async burst({ x, y }: { x: number; y: number }) {
|
|
84
|
+
if (!this.#texture) await this.#loadTexturePromise
|
|
100
85
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// 풀이 비어있으면 새로 생성
|
|
112
|
-
el = document.createElement('div')
|
|
86
|
+
const count = random(this.#count.min, this.#count.max)
|
|
87
|
+
for (let i = 0; i < count; i++) {
|
|
88
|
+
const lifespan = random(this.#lifespan.min, this.#lifespan.max)
|
|
89
|
+
const angle = random(this.#angle.min, this.#angle.max)
|
|
90
|
+
const sin = Math.sin(angle)
|
|
91
|
+
const cos = Math.cos(angle)
|
|
92
|
+
const velocity = random(this.#velocity.min, this.#velocity.max)
|
|
93
|
+
const scale = random(this.#scale.min, this.#scale.max)
|
|
94
|
+
|
|
95
|
+
const el = document.createElement('div')
|
|
113
96
|
setStyle(el, {
|
|
114
97
|
position: 'absolute',
|
|
115
98
|
left: `${x}px`,
|
|
@@ -123,48 +106,17 @@ export class DomParticleSystem extends DomGameObject {
|
|
|
123
106
|
opacity: `${this.#startAlpha ?? 1}`,
|
|
124
107
|
mixBlendMode: this.#blendMode ?? 'normal',
|
|
125
108
|
})
|
|
126
|
-
this.el.appendChild(el)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return el
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// [성능 최적화] 엘리먼트를 풀로 반환 (DOM에서 제거 대신)
|
|
133
|
-
#releaseElement(el: HTMLDivElement) {
|
|
134
|
-
if (this.#elementPool.length < this.#poolSize) {
|
|
135
|
-
el.style.display = 'none'
|
|
136
|
-
this.#elementPool.push(el)
|
|
137
|
-
} else {
|
|
138
|
-
el.remove()
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async burst({ x, y }: { x: number; y: number }) {
|
|
143
|
-
if (!this.#texture) await this.#loadTexturePromise
|
|
144
|
-
|
|
145
|
-
const count = random(this.#count.min, this.#count.max)
|
|
146
|
-
for (let i = 0; i < count; i++) {
|
|
147
|
-
const lifespan = random(this.#lifespan.min, this.#lifespan.max)
|
|
148
|
-
const angle = random(this.#angle.min, this.#angle.max)
|
|
149
|
-
const sin = Math.sin(angle)
|
|
150
|
-
const cos = Math.cos(angle)
|
|
151
|
-
const velocity = random(this.#velocity.min, this.#velocity.max)
|
|
152
|
-
const scale = random(this.#scale.min, this.#scale.max)
|
|
153
|
-
|
|
154
|
-
const el = this.#acquireElement(x, y, scale, angle)
|
|
155
109
|
|
|
156
110
|
this.#particles.push({
|
|
157
111
|
el,
|
|
158
|
-
active: true,
|
|
159
|
-
x,
|
|
160
|
-
y,
|
|
161
|
-
opacity: this.#startAlpha ?? 1,
|
|
162
112
|
age: 0,
|
|
163
113
|
lifespan,
|
|
164
114
|
velocityX: velocity * cos,
|
|
165
115
|
velocityY: velocity * sin,
|
|
166
116
|
fadeRate: this.#fadeRate,
|
|
167
117
|
})
|
|
118
|
+
|
|
119
|
+
this.el.appendChild(el)
|
|
168
120
|
}
|
|
169
121
|
}
|
|
170
122
|
|
|
@@ -172,46 +124,27 @@ export class DomParticleSystem extends DomGameObject {
|
|
|
172
124
|
super.update(dt)
|
|
173
125
|
|
|
174
126
|
const ps = this.#particles
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for (let readIdx = 0; readIdx < ps.length; readIdx++) {
|
|
179
|
-
const p = ps[readIdx]
|
|
127
|
+
for (let i = 0; i < ps.length; i++) {
|
|
128
|
+
const p = ps[i]
|
|
129
|
+
const e = p.el
|
|
180
130
|
|
|
181
131
|
p.age += dt
|
|
182
|
-
|
|
183
132
|
if (p.age > p.lifespan) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
133
|
+
e.remove()
|
|
134
|
+
ps.splice(i, 1)
|
|
135
|
+
i--
|
|
187
136
|
continue
|
|
188
137
|
}
|
|
189
138
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
p.opacity += p.fadeRate * dt
|
|
194
|
-
|
|
195
|
-
setStyle(p.el, { left: `${p.x}px`, top: `${p.y}px`, opacity: `${p.opacity}` })
|
|
139
|
+
const x = parseFloat(e.style.left) + p.velocityX * dt
|
|
140
|
+
const y = parseFloat(e.style.top) + p.velocityY * dt
|
|
141
|
+
const opacity = parseFloat(e.style.opacity) + p.fadeRate * dt
|
|
196
142
|
|
|
197
|
-
|
|
198
|
-
if (writeIdx !== readIdx) {
|
|
199
|
-
ps[writeIdx] = p
|
|
200
|
-
}
|
|
201
|
-
writeIdx++
|
|
143
|
+
setStyle(e, { left: `${x}px`, top: `${y}px`, opacity: `${opacity}` })
|
|
202
144
|
}
|
|
203
|
-
|
|
204
|
-
// 비활성 파티클 제거 (한 번에 배열 크기 조정)
|
|
205
|
-
ps.length = writeIdx
|
|
206
145
|
}
|
|
207
146
|
|
|
208
147
|
override remove() {
|
|
209
|
-
// 풀에 있는 엘리먼트도 정리
|
|
210
|
-
for (const el of this.#elementPool) {
|
|
211
|
-
el.remove()
|
|
212
|
-
}
|
|
213
|
-
this.#elementPool.length = 0
|
|
214
|
-
|
|
215
148
|
domTextureLoader.release(this.#textureSrc)
|
|
216
149
|
super.remove()
|
|
217
150
|
}
|
|
@@ -2,18 +2,10 @@ import { EventMap } from '@webtaku/event-emitter'
|
|
|
2
2
|
import { Container as PixiContainer } from 'pixi.js'
|
|
3
3
|
import { TransformableNode, TransformableNodeOptions } from './transformable'
|
|
4
4
|
|
|
5
|
-
export type GameObjectOptions = {
|
|
6
|
-
// [성능 최적화] sortableChildren 선택적 활성화
|
|
7
|
-
// 기본값: useYSort가 true이거나 drawOrder를 사용하는 경우에만 true
|
|
8
|
-
// 모든 컨테이너에서 sortableChildren을 켜면 정렬 비용이 발생함
|
|
9
|
-
sortableChildren?: boolean
|
|
10
|
-
} & TransformableNodeOptions
|
|
5
|
+
export type GameObjectOptions = {} & TransformableNodeOptions
|
|
11
6
|
|
|
12
7
|
export class GameObject<E extends EventMap = {}> extends TransformableNode<PixiContainer, E> {
|
|
13
8
|
constructor(options?: GameObjectOptions) {
|
|
14
|
-
|
|
15
|
-
// useYSort 사용 시 또는 명시적으로 요청한 경우에만 정렬 활성화
|
|
16
|
-
const needsSorting = options?.sortableChildren ?? options?.useYSort ?? false
|
|
17
|
-
super(new PixiContainer({ sortableChildren: needsSorting }), options ?? {})
|
|
9
|
+
super(new PixiContainer({ sortableChildren: true }), options ?? {})
|
|
18
10
|
}
|
|
19
11
|
}
|
|
@@ -53,15 +53,16 @@ export abstract class RenderableNode<C extends PixiContainer, E extends EventMap
|
|
|
53
53
|
super.remove()
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// [성능 최적화] 기존 2-pass → 1-pass로 통합
|
|
57
|
-
// 기존: _updateWorldTransform() + _resetWorldTransformDirty() 를 별도 호출
|
|
58
|
-
// 개선: 한 번의 순회에서 트랜스폼 업데이트와 dirty 리셋을 동시 처리
|
|
59
56
|
_updateWorldTransform() {
|
|
60
57
|
for (const child of this.children) {
|
|
61
58
|
if (isRenderableNode(child)) child._updateWorldTransform()
|
|
62
59
|
}
|
|
60
|
+
}
|
|
63
61
|
|
|
64
|
-
|
|
62
|
+
_resetWorldTransformDirty() {
|
|
63
|
+
for (const child of this.children) {
|
|
64
|
+
if (isRenderableNode(child)) child._resetWorldTransformDirty()
|
|
65
|
+
}
|
|
65
66
|
this.worldTransform.resetDirty()
|
|
66
67
|
this.worldAlpha.resetDirty()
|
|
67
68
|
}
|
|
@@ -27,12 +27,6 @@ export abstract class TransformableNode<C extends PixiContainer, E extends Event
|
|
|
27
27
|
#layer?: string
|
|
28
28
|
#useYSort = false
|
|
29
29
|
|
|
30
|
-
// [성능 최적화] useYSort용 이전 y값 캐시 - y가 변할 때만 drawOrder 업데이트
|
|
31
|
-
#prevY = NaN
|
|
32
|
-
|
|
33
|
-
// [성능 최적화] 이전 alpha 캐시 - 변경 시에만 Pixi에 반영
|
|
34
|
-
#prevAlpha = NaN
|
|
35
|
-
|
|
36
30
|
constructor(pixiContainer: C, options: TransformableNodeOptions) {
|
|
37
31
|
super(pixiContainer)
|
|
38
32
|
|
|
@@ -72,54 +66,22 @@ export abstract class TransformableNode<C extends PixiContainer, E extends Event
|
|
|
72
66
|
|
|
73
67
|
const pc = this._pixiContainer
|
|
74
68
|
const renderer = this.renderer
|
|
75
|
-
const wt = this.worldTransform
|
|
76
69
|
|
|
77
70
|
// 레이어 상에 있는 경우, 독립적으로 업데이트
|
|
78
71
|
if (this.#layer && renderer) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (wt.scaleX.dirty || wt.scaleY.dirty) {
|
|
85
|
-
pc.scale.set(wt.scaleX.v, wt.scaleY.v)
|
|
86
|
-
}
|
|
87
|
-
if (wt.rotation.dirty) {
|
|
88
|
-
pc.rotation = wt.rotation.v
|
|
89
|
-
}
|
|
90
|
-
if (this.worldAlpha.dirty) {
|
|
91
|
-
pc.alpha = this.worldAlpha.v
|
|
92
|
-
}
|
|
72
|
+
const wt = this.worldTransform
|
|
73
|
+
pc.position.set(wt.x.v, wt.y.v)
|
|
74
|
+
pc.scale.set(wt.scaleX.v, wt.scaleY.v)
|
|
75
|
+
pc.rotation = wt.rotation.v
|
|
76
|
+
pc.alpha = this.worldAlpha.v
|
|
93
77
|
} else {
|
|
94
78
|
const lt = this.localTransform
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// [성능 최적화] useYSort: y가 실제로 변경된 경우에만 drawOrder 업데이트
|
|
103
|
-
// 기존: 매 프레임 zIndex 설정 → 부모 컨테이너 정렬 비용 발생
|
|
104
|
-
// 개선: y 변경 시에만 zIndex 업데이트
|
|
105
|
-
if (this.#useYSort && lt.y !== this.#prevY) {
|
|
106
|
-
this.drawOrder = lt.y
|
|
107
|
-
this.#prevY = lt.y
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (wt.scaleX.dirty || wt.scaleY.dirty) {
|
|
111
|
-
pc.pivot.set(lt.pivotX, lt.pivotY)
|
|
112
|
-
pc.scale.set(lt.scaleX, lt.scaleY)
|
|
113
|
-
}
|
|
114
|
-
if (wt.rotation.dirty) {
|
|
115
|
-
pc.rotation = lt.rotation
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// [성능 최적화] alpha 변경 시에만 업데이트
|
|
119
|
-
if (this.alpha !== this.#prevAlpha) {
|
|
120
|
-
pc.alpha = this.alpha
|
|
121
|
-
this.#prevAlpha = this.alpha
|
|
122
|
-
}
|
|
79
|
+
pc.position.set(lt.x, lt.y)
|
|
80
|
+
if (this.#useYSort) this.drawOrder = lt.y
|
|
81
|
+
pc.pivot.set(lt.pivotX, lt.pivotY)
|
|
82
|
+
pc.scale.set(lt.scaleX, lt.scaleY)
|
|
83
|
+
pc.rotation = lt.rotation
|
|
84
|
+
pc.alpha = this.alpha
|
|
123
85
|
}
|
|
124
86
|
|
|
125
87
|
super._updateWorldTransform()
|
|
@@ -22,9 +22,6 @@ export class AnimatedSpriteNode<E extends EventMap = {}> extends GameObject<E &
|
|
|
22
22
|
#sprite?: PixiAnimatedSprite
|
|
23
23
|
#baseFps = 60
|
|
24
24
|
|
|
25
|
-
// [성능 최적화] 이전 worldTimeScale 캐시 - 변경 시에만 animationSpeed 업데이트
|
|
26
|
-
#prevWorldTimeScale = NaN
|
|
27
|
-
|
|
28
25
|
constructor(options: AnimatedSpriteNodeOptions) {
|
|
29
26
|
super(options)
|
|
30
27
|
this.#src = options.src
|
|
@@ -65,7 +62,6 @@ export class AnimatedSpriteNode<E extends EventMap = {}> extends GameObject<E &
|
|
|
65
62
|
s.loop = a.loop
|
|
66
63
|
this.#baseFps = a.fps
|
|
67
64
|
s.animationSpeed = (a.fps / 60) * this.worldTimeScale
|
|
68
|
-
this.#prevWorldTimeScale = this.worldTimeScale
|
|
69
65
|
s.play()
|
|
70
66
|
s.onLoop = () => (this as any).emit('animationend', this.#animation)
|
|
71
67
|
s.onComplete = () => (this as any).emit('animationend', this.#animation)
|
|
@@ -105,13 +101,8 @@ export class AnimatedSpriteNode<E extends EventMap = {}> extends GameObject<E &
|
|
|
105
101
|
|
|
106
102
|
protected override update(dt: number) {
|
|
107
103
|
super.update(dt)
|
|
108
|
-
|
|
109
|
-
// [성능 최적화] worldTimeScale이 변경된 경우에만 animationSpeed 업데이트
|
|
110
|
-
// 기존: 매 프레임 animationSpeed 재설정
|
|
111
|
-
// 개선: 캐시된 값과 비교하여 변경 시에만 업데이트
|
|
112
|
-
if (this.#sprite && this.worldTimeScale !== this.#prevWorldTimeScale) {
|
|
104
|
+
if (this.#sprite) {
|
|
113
105
|
this.#sprite.animationSpeed = (this.#baseFps / 60) * this.worldTimeScale
|
|
114
|
-
this.#prevWorldTimeScale = this.worldTimeScale
|
|
115
106
|
}
|
|
116
107
|
}
|
|
117
108
|
|
|
@@ -18,13 +18,6 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
18
18
|
#font?: BitmapFont
|
|
19
19
|
#sprites: PixiSprite[] = []
|
|
20
20
|
|
|
21
|
-
// [성능 최적화] 글리프 텍스처 캐시 - charCode → PixiTexture 매핑
|
|
22
|
-
// 동일 폰트에서 같은 문자에 대해 텍스처를 재생성하지 않음
|
|
23
|
-
#glyphTextureCache = new Map<number, PixiTexture>()
|
|
24
|
-
|
|
25
|
-
// [성능 최적화] 스프라이트 풀 - 재사용으로 GC 부담 감소
|
|
26
|
-
#spritePool: PixiSprite[] = []
|
|
27
|
-
|
|
28
21
|
constructor(options: BitmapTextNodeOptions) {
|
|
29
22
|
super(options)
|
|
30
23
|
this.#fnt = options.fnt
|
|
@@ -33,48 +26,9 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
33
26
|
this.#load()
|
|
34
27
|
}
|
|
35
28
|
|
|
36
|
-
// [성능 최적화] 글리프 텍스처 가져오기 (캐시 활용)
|
|
37
|
-
#getGlyphTexture(charCode: number): PixiTexture | undefined {
|
|
38
|
-
if (!this.#font) return undefined
|
|
39
|
-
|
|
40
|
-
let texture = this.#glyphTextureCache.get(charCode)
|
|
41
|
-
if (texture) return texture
|
|
42
|
-
|
|
43
|
-
const c = this.#font.chars[charCode]
|
|
44
|
-
if (!c) return undefined
|
|
45
|
-
|
|
46
|
-
const frame = new PixiRectangle(c.x, c.y, c.width, c.height)
|
|
47
|
-
texture = new PixiTexture({ source: this.#font.texture.source, frame })
|
|
48
|
-
this.#glyphTextureCache.set(charCode, texture)
|
|
49
|
-
|
|
50
|
-
return texture
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// [성능 최적화] 풀에서 스프라이트 가져오거나 새로 생성
|
|
54
|
-
#acquireSprite(texture: PixiTexture): PixiSprite {
|
|
55
|
-
let sprite = this.#spritePool.pop()
|
|
56
|
-
|
|
57
|
-
if (sprite) {
|
|
58
|
-
sprite.texture = texture
|
|
59
|
-
sprite.visible = true
|
|
60
|
-
} else {
|
|
61
|
-
sprite = new PixiSprite({ texture, zIndex: -999999 })
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return sprite
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// [성능 최적화] 스프라이트를 풀로 반환
|
|
68
|
-
#releaseSprite(sprite: PixiSprite) {
|
|
69
|
-
sprite.visible = false
|
|
70
|
-
this._pixiContainer.removeChild(sprite)
|
|
71
|
-
this.#spritePool.push(sprite)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
29
|
#updateText() {
|
|
75
|
-
// [성능 최적화] 기존 스프라이트를 풀로 반환 (destroy 대신)
|
|
76
30
|
for (const sprite of this.#sprites) {
|
|
77
|
-
|
|
31
|
+
sprite.destroy()
|
|
78
32
|
}
|
|
79
33
|
this.#sprites = []
|
|
80
34
|
|
|
@@ -96,12 +50,9 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
96
50
|
const c = this.#font.chars[charCode]
|
|
97
51
|
if (!c) continue
|
|
98
52
|
|
|
99
|
-
|
|
100
|
-
const texture = this.#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// [성능 최적화] 풀에서 스프라이트 재사용
|
|
104
|
-
const sprite = this.#acquireSprite(texture)
|
|
53
|
+
const frame = new PixiRectangle(c.x, c.y, c.width, c.height)
|
|
54
|
+
const texture = new PixiTexture({ source: this.#font.texture.source, frame })
|
|
55
|
+
const sprite = new PixiSprite({ texture, zIndex: -999999 })
|
|
105
56
|
|
|
106
57
|
const x0 = xPos + c.xoffset
|
|
107
58
|
const y0 = yPos + c.yoffset
|
|
@@ -150,9 +101,6 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
150
101
|
this.#font = await bitmapFontLoader.load(this.#fnt, this.#src)
|
|
151
102
|
}
|
|
152
103
|
|
|
153
|
-
// 폰트가 바뀌면 글리프 캐시 초기화
|
|
154
|
-
this.#glyphTextureCache.clear()
|
|
155
|
-
|
|
156
104
|
this.#updateText()
|
|
157
105
|
}
|
|
158
106
|
|
|
@@ -166,8 +114,6 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
166
114
|
}
|
|
167
115
|
|
|
168
116
|
set text(text: string | undefined) {
|
|
169
|
-
// [성능 최적화] 동일 텍스트면 스킵
|
|
170
|
-
if (this.#text === text) return
|
|
171
117
|
this.#text = text
|
|
172
118
|
this.#updateText()
|
|
173
119
|
}
|
|
@@ -177,18 +123,6 @@ export class BitmapTextNode<E extends EventMap = {}> extends GameObject<E> {
|
|
|
177
123
|
}
|
|
178
124
|
|
|
179
125
|
override remove() {
|
|
180
|
-
// 풀에 있는 스프라이트도 정리
|
|
181
|
-
for (const sprite of this.#spritePool) {
|
|
182
|
-
sprite.destroy()
|
|
183
|
-
}
|
|
184
|
-
this.#spritePool.length = 0
|
|
185
|
-
|
|
186
|
-
// 글리프 텍스처 캐시 정리
|
|
187
|
-
for (const texture of this.#glyphTextureCache.values()) {
|
|
188
|
-
texture.destroy()
|
|
189
|
-
}
|
|
190
|
-
this.#glyphTextureCache.clear()
|
|
191
|
-
|
|
192
126
|
bitmapFontLoader.release(this.#fnt)
|
|
193
127
|
super.remove()
|
|
194
128
|
}
|
package/src/node/ext/circle.ts
CHANGED
|
@@ -14,8 +14,7 @@ export class CircleNode extends TransformableNode<Graphics, EventMap> {
|
|
|
14
14
|
#stroke?: StrokeInput
|
|
15
15
|
|
|
16
16
|
constructor(options: CircleNodeOptions) {
|
|
17
|
-
|
|
18
|
-
super(new Graphics(), options)
|
|
17
|
+
super(new Graphics({ sortableChildren: true }), options)
|
|
19
18
|
|
|
20
19
|
this.#radius = options.radius
|
|
21
20
|
this.#fill = options.fill
|
package/src/node/ext/particle.ts
CHANGED
|
@@ -18,18 +18,11 @@ export type ParticleSystemOptions = {
|
|
|
18
18
|
orientToVelocity: boolean
|
|
19
19
|
|
|
20
20
|
blendMode?: BLEND_MODES // ex) 'screen', 'multiply'
|
|
21
|
-
|
|
22
|
-
// [성능 최적화] 오브젝트 풀 크기 설정 (기본값: 100)
|
|
23
|
-
// 풀 크기를 늘리면 메모리를 더 사용하지만 GC 스파이크를 줄일 수 있음
|
|
24
|
-
poolSize?: number
|
|
25
21
|
} & GameObjectOptions
|
|
26
22
|
|
|
27
23
|
interface Particle {
|
|
28
24
|
sprite: PixiSprite
|
|
29
25
|
|
|
30
|
-
// [성능 최적화] 활성 상태 플래그 - 풀링에서 재사용 여부 판단
|
|
31
|
-
active: boolean
|
|
32
|
-
|
|
33
26
|
age: number
|
|
34
27
|
lifespan: number
|
|
35
28
|
|
|
@@ -59,10 +52,6 @@ export class ParticleSystem extends GameObject {
|
|
|
59
52
|
#loadTexturePromise: Promise<void>
|
|
60
53
|
#particles: Particle[] = []
|
|
61
54
|
|
|
62
|
-
// [성능 최적화] 오브젝트 풀 - 스프라이트 재사용으로 GC 부담 감소
|
|
63
|
-
#spritePool: PixiSprite[] = []
|
|
64
|
-
#poolSize: number
|
|
65
|
-
|
|
66
55
|
constructor(options: ParticleSystemOptions) {
|
|
67
56
|
super(options)
|
|
68
57
|
|
|
@@ -76,7 +65,6 @@ export class ParticleSystem extends GameObject {
|
|
|
76
65
|
this.#fadeRate = options.fadeRate
|
|
77
66
|
this.#orientToVelocity = options.orientToVelocity
|
|
78
67
|
this.#blendMode = options.blendMode
|
|
79
|
-
this.#poolSize = options.poolSize ?? 100
|
|
80
68
|
|
|
81
69
|
this.#loadTexturePromise = this.#loadTexture()
|
|
82
70
|
}
|
|
@@ -90,46 +78,6 @@ export class ParticleSystem extends GameObject {
|
|
|
90
78
|
}
|
|
91
79
|
}
|
|
92
80
|
|
|
93
|
-
// [성능 최적화] 풀에서 스프라이트 가져오거나 새로 생성
|
|
94
|
-
#acquireSprite(x: number, y: number, scale: number, angle: number): PixiSprite {
|
|
95
|
-
let sprite = this.#spritePool.pop()
|
|
96
|
-
|
|
97
|
-
if (sprite) {
|
|
98
|
-
// 풀에서 가져온 스프라이트 재설정
|
|
99
|
-
sprite.x = x
|
|
100
|
-
sprite.y = y
|
|
101
|
-
sprite.scale.set(scale, scale)
|
|
102
|
-
sprite.alpha = this.#startAlpha ?? 1
|
|
103
|
-
sprite.rotation = this.#orientToVelocity ? angle : 0
|
|
104
|
-
sprite.visible = true
|
|
105
|
-
} else {
|
|
106
|
-
// 풀이 비어있으면 새로 생성
|
|
107
|
-
sprite = new PixiSprite({
|
|
108
|
-
x, y,
|
|
109
|
-
texture: this.#texture,
|
|
110
|
-
anchor: { x: 0.5, y: 0.5 },
|
|
111
|
-
scale: { x: scale, y: scale },
|
|
112
|
-
alpha: this.#startAlpha,
|
|
113
|
-
blendMode: this.#blendMode,
|
|
114
|
-
rotation: this.#orientToVelocity ? angle : undefined,
|
|
115
|
-
})
|
|
116
|
-
this._pixiContainer.addChild(sprite)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return sprite
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// [성능 최적화] 스프라이트를 풀로 반환 (destroy 대신)
|
|
123
|
-
#releaseSprite(sprite: PixiSprite) {
|
|
124
|
-
if (this.#spritePool.length < this.#poolSize) {
|
|
125
|
-
sprite.visible = false
|
|
126
|
-
this.#spritePool.push(sprite)
|
|
127
|
-
} else {
|
|
128
|
-
// 풀이 가득 차면 destroy
|
|
129
|
-
sprite.destroy()
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
81
|
async burst({ x, y }: { x: number; y: number }) {
|
|
134
82
|
if (!this.#texture) await this.#loadTexturePromise
|
|
135
83
|
|
|
@@ -142,17 +90,26 @@ export class ParticleSystem extends GameObject {
|
|
|
142
90
|
const velocity = random(this.#velocity.min, this.#velocity.max)
|
|
143
91
|
const scale = random(this.#scale.min, this.#scale.max)
|
|
144
92
|
|
|
145
|
-
const sprite =
|
|
93
|
+
const sprite = new PixiSprite({
|
|
94
|
+
x, y,
|
|
95
|
+
texture: this.#texture,
|
|
96
|
+
anchor: { x: 0.5, y: 0.5 },
|
|
97
|
+
scale: { x: scale, y: scale },
|
|
98
|
+
alpha: this.#startAlpha,
|
|
99
|
+
blendMode: this.#blendMode,
|
|
100
|
+
rotation: this.#orientToVelocity ? angle : undefined,
|
|
101
|
+
})
|
|
146
102
|
|
|
147
103
|
this.#particles.push({
|
|
148
104
|
sprite,
|
|
149
|
-
active: true,
|
|
150
105
|
age: 0,
|
|
151
106
|
lifespan,
|
|
152
107
|
velocityX: velocity * cos,
|
|
153
108
|
velocityY: velocity * sin,
|
|
154
109
|
fadeRate: this.#fadeRate,
|
|
155
110
|
})
|
|
111
|
+
|
|
112
|
+
this._pixiContainer.addChild(sprite)
|
|
156
113
|
}
|
|
157
114
|
}
|
|
158
115
|
|
|
@@ -160,46 +117,25 @@ export class ParticleSystem extends GameObject {
|
|
|
160
117
|
super.update(dt)
|
|
161
118
|
|
|
162
119
|
const ps = this.#particles
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
// 기존: splice(i, 1)은 배열 요소를 이동시켜 O(n) 비용 발생
|
|
166
|
-
// 개선: 마지막 요소와 교체 후 pop()으로 O(1) 제거
|
|
167
|
-
let writeIdx = 0
|
|
168
|
-
for (let readIdx = 0; readIdx < ps.length; readIdx++) {
|
|
169
|
-
const p = ps[readIdx]
|
|
120
|
+
for (let i = 0; i < ps.length; i++) {
|
|
121
|
+
const p = ps[i]
|
|
170
122
|
const g = p.sprite
|
|
171
123
|
|
|
172
124
|
p.age += dt
|
|
173
|
-
|
|
174
125
|
if (p.age > p.lifespan) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
126
|
+
g.destroy({ children: true })
|
|
127
|
+
ps.splice(i, 1)
|
|
128
|
+
i--
|
|
178
129
|
continue
|
|
179
130
|
}
|
|
180
131
|
|
|
181
132
|
g.x += p.velocityX * dt
|
|
182
133
|
g.y += p.velocityY * dt
|
|
183
134
|
g.alpha += p.fadeRate * dt
|
|
184
|
-
|
|
185
|
-
// 활성 파티클을 앞으로 이동
|
|
186
|
-
if (writeIdx !== readIdx) {
|
|
187
|
-
ps[writeIdx] = p
|
|
188
|
-
}
|
|
189
|
-
writeIdx++
|
|
190
135
|
}
|
|
191
|
-
|
|
192
|
-
// 비활성 파티클 제거 (한 번에 배열 크기 조정)
|
|
193
|
-
ps.length = writeIdx
|
|
194
136
|
}
|
|
195
137
|
|
|
196
138
|
override remove() {
|
|
197
|
-
// 풀에 있는 스프라이트도 정리
|
|
198
|
-
for (const sprite of this.#spritePool) {
|
|
199
|
-
sprite.destroy()
|
|
200
|
-
}
|
|
201
|
-
this.#spritePool.length = 0
|
|
202
|
-
|
|
203
139
|
textureLoader.release(this.#textureSrc)
|
|
204
140
|
super.remove()
|
|
205
141
|
}
|
|
@@ -16,8 +16,7 @@ export class RectangleNode extends TransformableNode<Graphics, EventMap> {
|
|
|
16
16
|
#stroke?: StrokeInput
|
|
17
17
|
|
|
18
18
|
constructor(options: RectangleNodeOptions) {
|
|
19
|
-
|
|
20
|
-
super(new Graphics(), options)
|
|
19
|
+
super(new Graphics({ sortableChildren: true }), options)
|
|
21
20
|
|
|
22
21
|
this.#width = options.width
|
|
23
22
|
this.#height = options.height
|
package/src/renderer/camera.ts
CHANGED
|
@@ -13,18 +13,12 @@ export class Camera extends EventEmitter<{
|
|
|
13
13
|
get scale() { return this.#scale }
|
|
14
14
|
|
|
15
15
|
setPosition(x: number, y: number) {
|
|
16
|
-
// [성능 최적화] 값이 변경된 경우에만 이벤트 emit
|
|
17
|
-
// 기존: 매번 positionChanged 이벤트 발생 → 불필요한 월드 트랜스폼 업데이트
|
|
18
|
-
// 개선: 값 비교 후 변경 시에만 emit
|
|
19
|
-
if (this.#x === x && this.#y === y) return
|
|
20
16
|
this.#x = x
|
|
21
17
|
this.#y = y
|
|
22
18
|
this.emit('positionChanged')
|
|
23
19
|
}
|
|
24
20
|
|
|
25
21
|
setScale(scale: number) {
|
|
26
|
-
// [성능 최적화] 값이 변경된 경우에만 이벤트 emit
|
|
27
|
-
if (this.#scale === scale) return
|
|
28
22
|
this.#scale = scale
|
|
29
23
|
this.emit('scaleChanged')
|
|
30
24
|
}
|
package/src/renderer/renderer.ts
CHANGED
|
@@ -162,18 +162,11 @@ export class Renderer extends RenderableNode<PixiContainer, {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
#render(dt: number) {
|
|
165
|
-
// [성능 최적화] 렌더러가 준비되지 않은 경우 불필요한 업데이트 스킵
|
|
166
|
-
if (!this.#pixiRenderer) return
|
|
167
|
-
|
|
168
165
|
const scaledDt = dt * this.timeScale
|
|
169
166
|
this.update(scaledDt)
|
|
170
|
-
|
|
171
|
-
// [성능 최적화] 2-pass → 1-pass로 통합
|
|
172
|
-
// 기존: _updateWorldTransform() + _resetWorldTransformDirty() 별도 호출
|
|
173
|
-
// 개선: _updateWorldTransform() 내부에서 dirty 리셋까지 처리
|
|
174
167
|
this._updateWorldTransform()
|
|
175
|
-
|
|
176
|
-
this.#pixiRenderer
|
|
168
|
+
this._resetWorldTransformDirty()
|
|
169
|
+
this.#pixiRenderer?.render(this._pixiContainer)
|
|
177
170
|
this.fpsDisplay?.update()
|
|
178
171
|
|
|
179
172
|
this._isSizeDirty = false
|