2D Game Engine

Pluginy

PluginHost, manifest, uprawnienia, punkty rozszerzeń, zależności z detekcją cykli i budżety błędów. Jak pisać pluginy bezpiecznie.

Model pluginu#

Plugin to manifest + funkcje aktywacji. PluginHost zarządza cyklem życia, zależnościami i uprawnieniami.

ts
import { PluginHost, type PluginDefinition } from '@engine'

const host = new PluginHost({
  engineVersion: '0.1.0',
  logger: engine.getLogger(),
  events: engine.getEvents(),
  services: { assets: engine.getAssets() },
  maxFailures: 3
})

const plugin: PluginDefinition = {
  manifest: {
    id: 'my-plugin',
    version: '1.0.0',
    engine: '>=0.1.0',
    name: 'Mój plugin',
    permissions: ['systems', 'events']
  },
  activate(context) {
    context.registerSystem({
      name: 'spawn',
      update(dt) {
        context.events.emit('spawn:tick', { dt })
      }
    })
  },
  deactivate() {}
}

host.install(plugin)
host.activate('my-plugin')

Manifest i uprawnienia#

ts
type PluginManifest = {
  id: string
  version: string
  engine: string
  name?: string
  description?: string
  author?: string
  dependencies?: string[]
  permissions?: PluginPermission[]
}

type PluginPermission =
  | 'systems'
  | 'importers'
  | 'panels'
  | 'events'
  | 'scenes'
  | 'assets'
  | 'storage'

Manifest to nie piaskownica

Uprawnienia ograniczają API kontekstu, ale kod pluginu działa w tym samym realmie co gra. Manifest jest bramką API, nie sandboxem — instaluj tylko pluginy z zaufanych źródeł.

API hosta#

ts
install(plugin); activate(id); activateAll(); deactivate(id); uninstall(id); dispose()
update(deltaTime); get(id); has(id); list(); summary()
getSystems(); enableSystem(owner, name); getImporters(); getPanels()

parseVersion(input); compareVersions(a, b); isValidRange(range); satisfiesRange(version, range)

Przepływ aktywacji#

  1. install() waliduje manifest i unikalność id — nic więcej. Katalog może zainstalować cały zestaw przed aktywacją.
  2. activate() sprawdza zakres engine, aktywuje zależności (DFS z detekcją cykli), buduje kontekst z capability-gatingiem i woła activate(context).
  3. Rejestracje trafiają do rejestrów z właścicielem (owner = id), więc dezaktywacja usuwa je jednym przebiegiem.
  4. adapter.update woła plugins.update(deltaTime) po scenie.
  5. Wyjątek systemu jest liczony; po maxFailures system zostaje automatycznie wyłączony.
  6. deactivate() idzie od zależnych do zależności: hook pluginu, disposery LIFO, czyszczenie rejestrów.

Punkty rozszerzeń#

RejestrCo dodajePrzykład
SystemRegistrySystemy z kolejnością i budżetem błędówSpawn, AI, pogoda
ImporterRegistryImportery plików (wbudowany tiled)Własny format map
PanelRegistryPanele edytora (mount/unmount)Inspector, statystyki
eventsZdarzenia w przestrzeni nazw pluginumy-plugin:*

Przykład: system pogody#

ts
const weatherPlugin: PluginDefinition = {
  manifest: {
    id: 'weather',
    version: '1.0.0',
    engine: '>=0.1.0',
    permissions: ['systems', 'events']
  },
  activate(context) {
    let timer = 0
    context.registerSystem({
      name: 'weather',
      update(dt) {
        timer += dt
        if (timer > 30) {
          timer = 0
          context.events.emit('weather:change', {
            type: context.random.pick(['rain', 'clear', 'fog'])
          })
        }
      }
    })
  }
}

Diagnostyka pluginów#

Raport diagnostyczny silnika zawiera sekcję plugins z licznikami i audytem uprawnień:

ts
const report = engine.getDiagnosticsReport()
console.log(report.plugins)
console.log(host.summary())

Wersje#

ts
import { satisfiesRange } from '@engine'

satisfiesRange('0.1.0', '>=0.1.0')
satisfiesRange('0.2.0', '^0.1.0')

Zakresy wersji są walidowane przy aktywacji — plugin niezgodny z silnikiem nie wystartuje.