2D Game Engine

Przykłady

[object Object]

Przykład 1: scena z ruchem i kolizjami#

Kompletna scena: gracz sterowany akcjami, ściany immovable, granice świata i kamera podążająca.

tsCollectScene.ts
import { Scene, type GameObject } from '@engine'

const WORLD = { width: 2000, height: 1400 }

export class CollectScene extends Scene {
  private player!: GameObject
  private walls: GameObject[] = []
  private score = 0

  constructor() {
    super('collect')
  }

  declareAssets() {
    super.declareAssets([
      { id: 'hero', type: 'image', src: '/assets/hero.png' },
      { id: 'coin', type: 'image', src: '/assets/coin.png' },
      { id: 'pickup', type: 'audio', src: '/assets/pickup.ogg' }
    ])
  }

  async preload() {
    await this.getAssets().load()
  }

  create() {
    this.getPhysics().setWorldBounds(0, 0, WORLD.width, WORLD.height)

    this.player = this.createSprite('hero', { x: 200, y: 300, tags: ['player'] })
    this.getPhysics().attachBody(this.player)
    this.getPhysics().setCollideWorldBounds(this.player, true)

    const wallSpecs = [
      { x: 520, y: 420, width: 280, height: 24 },
      { x: 1200, y: 600, width: 24, height: 320 },
      { x: 900, y: 1050, width: 400, height: 24 }
    ]
    for (const spec of wallSpecs) {
      const wall = this.createRect(spec.width, spec.height, { x: spec.x, y: spec.y, fill: '#3a4256' })
      this.getPhysics().attachBody(wall)
      this.getPhysics().setImmovable(wall, true)
      this.walls.push(wall)
    }
    this.getPhysics().addCollider(this.player, this.walls)

    for (const pos of [{ x: 320, y: 220 }, { x: 780, y: 360 }, { x: 1650, y: 560 }]) {
      const coin = this.createSprite('coin', { ...pos, tags: ['coin'] })
      this.tween(coin, { y: coin.y - 8 }, { duration: 0.8, yoyo: true, repeat: -1, easing: 'sine.inOut' })
    }
    this.getPhysics().addOverlap(this.player, this.findByTag('coin'), (_p, coin) => {
      this.collect(coin)
    })

    this.getCamera().setBounds(0, 0, WORLD.width, WORLD.height)
    this.getCamera().follow(this.player, 0.08, 0.08)

    this.createText('hud', { x: 16, y: 16, text: '0', fontSize: '20px', color: '#ffffff' })
      .setScrollFactor(0)
      .setDepth(1000)
  }

  fixedUpdate() {
    const input = this.getInput()
    const vx = (input.isActionDown('move-right') ? 240 : 0) - (input.isActionDown('move-left') ? 240 : 0)
    const vy = (input.isActionDown('move-down') ? 240 : 0) - (input.isActionDown('move-up') ? 240 : 0)
    this.getPhysics().setVelocity(this.player, vx, vy)
  }

  private collect(coin: GameObject) {
    this.score += 1
    this.playSound('pickup', { channel: 'sfx' })
    this.getCamera().flash(80, 255, 215, 94)
    this.tween(coin, { alpha: 0, scaleX: 2, scaleY: 2 }, {
      duration: 0.25,
      onComplete: () => this.destroyObject(coin)
    })
    this.getObject<GameObject>('hud')?.setRender({ type: 'text', text: String(this.score) })
    this.getEvents().emit('score:changed', { score: this.score })
  }
}

Przykład 2: komponent zachowania#

AI przeciwnika: patrol między punktami i pościg, gdy gracz jest blisko.

tsPatrolChaser.ts
import { Component, type GameObject } from '@engine'

export class PatrolChaser extends Component {
  phase = 'behavior' as const
  private dir = 1
  private speed = 90

  constructor(
    private minX: number,
    private maxX: number,
    private target: GameObject,
    private chaseRange = 220
  ) {
    super()
  }

  onUpdate(dt: number) {
    const owner = this.owner
    const dist = Math.hypot(this.target.x - owner.x, this.target.y - owner.y)

    if (dist < this.chaseRange) {
      const dx = Math.sign(this.target.x - owner.x)
      owner.translate(dx * this.speed * 1.6 * dt, 0)
      owner.setTint(0xff6b6b)
      return
    }

    owner.setTint(null)
    owner.translate(this.dir * this.speed * dt, 0)
    if (owner.x <= this.minX) this.dir = 1
    if (owner.x >= this.maxX) this.dir = -1
  }
}
ts
const enemy = this.createSprite('enemy', { x: 600, y: 300, tags: ['enemy'] })
enemy.add(new PatrolChaser(400, 900, this.player))

Przykład 3: menu z przejściem i fade#

tsMenuScene.ts
import { Scene } from '@engine'

export class MenuScene extends Scene {
  constructor() {
    super('menu')
  }

  create() {
    this.getCamera().fadeIn(400)
    this.playMusic('theme', { loop: true })

    const title = this.createText('title', { x: 640, y: 240, text: 'MOJA GRA', fontSize: '48px', color: '#ffffff' })
      .setOrigin(0.5, 0.5)
    this.tweenFrom(title, { alpha: 0, y: 180 }, { duration: 0.6, easing: 'back.out' })

    const start = this.createText('start', { x: 640, y: 420, text: 'Start', fontSize: '24px', color: '#8f96f8' })
      .setOrigin(0.5, 0.5)
    this.tween(start, { alpha: 0.5 }, { duration: 0.7, yoyo: true, repeat: -1 })
  }

  update() {
    if (this.getInput().isActionPressed('interact') || this.getInput().getPointer().isDown) {
      this.getAudio().unlock()
      this.getCamera().fadeOut(350)
      this.getScheduler().delay(0.4, () => {
        this.getEvents().emit('request:scene', { to: 'collect' })
      }, { owner: this.id })
    }
  }
}

Przykład 4: autosave do IndexedDB#

tssave.ts
import { IndexedDbStore, PlayerSaveRepository } from '@engine'

const saves = new PlayerSaveRepository(new IndexedDbStore({ database: 'mygame' }))

export async function autosave(playerId: string, state: PlayerState) {
  await saves.save(playerId, 'autosave', { ...state, savedAt: Date.now() })
}

export async function loadAutosave(playerId: string) {
  return saves.load<PlayerState>(playerId, 'autosave')
}
ts
class CollectScene extends Scene {
  shutdown() {
    void autosave('player-1', {
      score: this.score,
      position: { x: this.player.x, y: this.player.y }
    })
  }
}

Rejestracja wszystkiego#

tsmain.ts
import { createEngine } from '@engine'
import { MenuScene } from './scenes/MenuScene'
import { CollectScene } from './scenes/CollectScene'

const engine = createEngine({
  parent: 'app',
  width: 1280,
  height: 720,
  fixedStep: { hz: 60 },
  seed: 20260
})

engine.registerScene(new MenuScene())
engine.registerScene(new CollectScene())

engine.on<{ to: string }>('request:scene', ({ to }) => engine.startScene(to))

engine.start('menu')

Wskazówka

Więcej działających przykładów znajdziesz w repozytorium: apps/playground/src/scenes/GameScene.ts (pełna gra z zapisami, colliderami i HUD) oraz apps/playground/src/components/ (PlayerController, Bobbing, CollectEffect, Sparkle).