Obiekty i komponenty
GameObject, transformacje, tagi, hierarchia i komponenty z fazami systemowymi. Jak budować zachowania bez ECS.
GameObject#
GameObject to encja domenowa: transform lokalny, tagi, grupy, hierarchia, komponenty oraz deskryptory render i body. Nie trzyma instancji Phasera.
const player = this.createSprite('hero', { x: 100, y: 200 })
player
.setPosition(120, 220)
.setScale(2)
.setDepth(10)
.setAlpha(0.9)
.addTag('player')
.addTag('damageable')
API#
get destroyed(); get x(); get y(); get alpha(); get depth()
get rotation(); get scaleX(); get scaleY(); get world(): Transform
setPosition(x, y); translate(dx, dy); setRotation(rad)
setScale(x, y?); setVisible(bool); setActive(bool)
setAlpha(a); setDepth(d); setScrollFactor(f); setTint(color | null)
setRender(render | null); setBody(body | null)
addTag(tag); removeTag(tag); hasTag(tag); hasAnyTag(tags); hasAllTags(tags)
addChild(child); removeChild(child)
add<T>(component): T; get<T>(ctor); has(ctor); remove(ctor); getComponents()
destroy(): void
markWorldDirty(); markRenderDirty()
Settery są chainable i oznaczają obiekt jako brudny — renderer dostanie go w najbliższej klatce.
Transformacje i hierarchia#
const turret = this.createRect(24, 24, { x: 0, y: -20, fill: '#8899aa' })
const tank = this.createSprite('tank', { x: 400, y: 300 })
tank.addChild(turret)
tank.setPosition(500, 300)
turret.world.x
worldto transform po złożeniu z rodzicami — odczyt O(1) po przeliczeniu.- Zmiana transformacji rodzica unieważnia tylko jego poddrzewo.
setScrollFactor(0)przykleja obiekt do ekranu (HUD).
Tagi i wyszukiwanie#
enemy.addTag('enemy').addTag('flying')
this.findByTag('enemy')
this.findByTags(['enemy', 'flying'], 'all')
this.find((obj) => obj.hasTag('enemy') && obj.x > 800)
Indeks tagów jest aktualizowany przy addTag/removeTag, więc wyszukiwanie nie skanuje wszystkich obiektów.
Komponenty#
Komponent to klasa z fazą systemową i cyklami życia:
import { Component, type GameObject } from '@engine'
export class PlayerController extends Component {
phase = 'input' as const
private speed = 240
onUpdate(dt: number) {
const input = this.owner.getInput()
let vx = 0
let vy = 0
if (input.isActionDown('move-left')) vx -= this.speed
if (input.isActionDown('move-right')) vx += this.speed
if (input.isActionDown('move-up')) vy -= this.speed
if (input.isActionDown('move-down')) vy += this.speed
this.owner.translate(vx * dt, vy * dt)
}
}
player.add(new PlayerController())
const controller = player.get(PlayerController)
controller.enabled = false
player.remove(PlayerController)
Fazy systemowe#
| Faza | Kiedy w klatce | Typowe zastosowanie |
|---|---|---|
input | Przed Scene.update | Sterowanie |
behavior | Po Scene.update | AI, logika zachowań |
physics | Po behavior | Siły, prędkości |
animation | Po physics | Klatki animacji |
render | Na końcu | Przygotowanie renderu |
Cykl życia komponentu#
class Bobbing extends Component {
phase = 'behavior' as const
private baseY = 0
private t = 0
onAttach() {
this.baseY = this.owner.y
}
onUpdate(dt: number) {
this.t += dt
this.owner.setPosition(this.owner.x, this.baseY + Math.sin(this.t * 3) * 6)
}
onDetach() {}
onDestroy() {}
}
Deskryptory render i body#
player.setRender({ type: 'sprite', textureId: 'hero', frame: 0 })
player.setBody({ type: 'arcade', width: 24, height: 32, immovable: false })
Deskryptor to dane, nie instancja runtime. Adapter tłumaczy je na obiekty Phasera i synchronizuje tylko gdy są brudne.
Dlaczego nie ECS?
Prototyp ECS został oceniony i odrzucony (ADR 0003). Komponenty z fazami dają przewidywalną kolejność i prostszy model mentalny przy zachowaniu wydajności: 0,72 ms/klatkę dla 20 000 obiektów.
Usuwanie obiektów#
this.destroyObject(bullet)
this.destroyObject('hud-temp')
this.destroyAllObjects()
Usuwanie jest kolejkowane (pendingDestroy) i wykonywane na końcu klatki: potomkowie liść-pierwszy → onDestroy komponentów → usunięcie z indeksu → render.destroy. Nigdy nie mutujesz listy obiektów w trakcie iteracji.