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.
Files changed (49) hide show
  1. package/lib/dom/dom-particle.js +23 -74
  2. package/lib/dom/dom-particle.js.map +1 -1
  3. package/lib/node/core/game-object.js +1 -4
  4. package/lib/node/core/game-object.js.map +1 -1
  5. package/lib/node/core/renderable.js +6 -4
  6. package/lib/node/core/renderable.js.map +1 -1
  7. package/lib/node/core/transformable.js +11 -42
  8. package/lib/node/core/transformable.js.map +1 -1
  9. package/lib/node/ext/animated-sprite.js +1 -8
  10. package/lib/node/ext/animated-sprite.js.map +1 -1
  11. package/lib/node/ext/bitmap-text.js +4 -61
  12. package/lib/node/ext/bitmap-text.js.map +1 -1
  13. package/lib/node/ext/circle.js +1 -2
  14. package/lib/node/ext/circle.js.map +1 -1
  15. package/lib/node/ext/particle.js +15 -65
  16. package/lib/node/ext/particle.js.map +1 -1
  17. package/lib/node/ext/rectangle.js +1 -2
  18. package/lib/node/ext/rectangle.js.map +1 -1
  19. package/lib/renderer/camera.js +0 -8
  20. package/lib/renderer/camera.js.map +1 -1
  21. package/lib/renderer/renderer.js +2 -7
  22. package/lib/renderer/renderer.js.map +1 -1
  23. package/lib/types/dom/dom-particle.d.ts +0 -1
  24. package/lib/types/dom/dom-particle.d.ts.map +1 -1
  25. package/lib/types/node/core/game-object.d.ts +1 -3
  26. package/lib/types/node/core/game-object.d.ts.map +1 -1
  27. package/lib/types/node/core/renderable.d.ts +1 -0
  28. package/lib/types/node/core/renderable.d.ts.map +1 -1
  29. package/lib/types/node/core/transformable.d.ts.map +1 -1
  30. package/lib/types/node/ext/animated-sprite.d.ts.map +1 -1
  31. package/lib/types/node/ext/bitmap-text.d.ts.map +1 -1
  32. package/lib/types/node/ext/circle.d.ts.map +1 -1
  33. package/lib/types/node/ext/particle.d.ts +0 -1
  34. package/lib/types/node/ext/particle.d.ts.map +1 -1
  35. package/lib/types/node/ext/rectangle.d.ts.map +1 -1
  36. package/lib/types/renderer/camera.d.ts.map +1 -1
  37. package/lib/types/renderer/renderer.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/dom/dom-particle.ts +24 -91
  40. package/src/node/core/game-object.ts +2 -10
  41. package/src/node/core/renderable.ts +5 -4
  42. package/src/node/core/transformable.ts +11 -49
  43. package/src/node/ext/animated-sprite.ts +1 -10
  44. package/src/node/ext/bitmap-text.ts +4 -70
  45. package/src/node/ext/circle.ts +1 -2
  46. package/src/node/ext/particle.ts +16 -80
  47. package/src/node/ext/rectangle.ts +1 -2
  48. package/src/renderer/camera.ts +0 -6
  49. package/src/renderer/renderer.ts +2 -9
@@ -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
- #acquireElement(x: number, y: number, scale: number, angle: number): HTMLDivElement {
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
- if (el) {
102
- // 풀에서 가져온 엘리먼트 재설정
103
- setStyle(el, {
104
- left: `${x}px`,
105
- top: `${y}px`,
106
- transform: `translate(-50%, -50%) scale(${scale})${this.#orientToVelocity ? ` rotate(${angle}rad)` : ''}`,
107
- opacity: `${this.#startAlpha ?? 1}`,
108
- display: 'block',
109
- })
110
- } else {
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
- // [성능 최적화] swap-and-pop 패턴으로 O(n) splice 비용 제거
177
- let writeIdx = 0
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
- // [성능 최적화] DOM에서 제거 대신 풀로 반환
185
- this.#releaseElement(p.el)
186
- p.active = false
133
+ e.remove()
134
+ ps.splice(i, 1)
135
+ i--
187
136
  continue
188
137
  }
189
138
 
190
- // [성능 최적화] parseFloat 대신 캐시된 사용
191
- p.x += p.velocityX * dt
192
- p.y += p.velocityY * dt
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
- // [성능 최적화] sortableChildren 필요한 경우에만 활성화
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
- // [성능 최적화] dirty 리셋을 별도 pass 대신 여기서 바로 처리
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
- // [성능 최적화] dirty 체크 - 값이 변경된 경우에만 Pixi 속성 업데이트
80
- // Pixi 내부에서도 dirty 체크를 하지만, 함수 호출 자체를 줄이는 것이 더 효율적
81
- if (wt.x.dirty || wt.y.dirty) {
82
- pc.position.set(wt.x.v, wt.y.v)
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
- // [성능 최적화] 로컬 트랜스폼도 dirty 체크 적용
97
- // DOM 쪽(dom-game-object.ts)과 동일한 패턴
98
- if (wt.x.dirty || wt.y.dirty) {
99
- pc.position.set(lt.x, lt.y)
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
- this.#releaseSprite(sprite)
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.#getGlyphTexture(charCode)
101
- if (!texture) continue
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
  }
@@ -14,8 +14,7 @@ export class CircleNode extends TransformableNode<Graphics, EventMap> {
14
14
  #stroke?: StrokeInput
15
15
 
16
16
  constructor(options: CircleNodeOptions) {
17
- // [성능 최적화] Graphics 자식 노드를 가지지 않으므로 sortableChildren 불필요
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
@@ -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 = this.#acquireSprite(x, y, scale, angle)
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
- // [성능 최적화] swap-and-pop 패턴으로 O(n) splice 비용 제거
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
- // [성능 최적화] destroy 대신 풀로 반환
176
- this.#releaseSprite(g)
177
- p.active = false
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
- // [성능 최적화] Graphics 자식 노드를 가지지 않으므로 sortableChildren 불필요
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
@@ -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
  }
@@ -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.render(this._pixiContainer)
168
+ this._resetWorldTransformDirty()
169
+ this.#pixiRenderer?.render(this._pixiContainer)
177
170
  this.fpsDisplay?.update()
178
171
 
179
172
  this._isSizeDirty = false