2D Game Engine

Zapisy gry

ProjectRepository z atomowym zapisem, PlayerSaveRepository i sześć backendów SaveStore — od localStorage po SQL.

Dwa repozytoria#

RepozytoriumCo zapisujeJak
ProjectRepositoryProjekt gry (edytor)Atomowo: tmp → odczyt zwrotny → backup → commit
PlayerSaveRepositoryStan gracza (sloty)Klucz playerId + slot
ts
import {
  ProjectRepository,
  PlayerSaveRepository,
  LocalStorageStore,
  IndexedDbStore
} from '@engine'

const projectRepo = new ProjectRepository(new IndexedDbStore({ database: 'mygame' }))
await projectRepo.save(project)
const loaded = await projectRepo.load()
const backup = await projectRepo.restoreBackup()

const saves = new PlayerSaveRepository(new LocalStorageStore('mygame:'))
await saves.save('player-1', 'slot-1', { level: 4, score: 1280, collected: ['c1', 'c3'] })
const state = await saves.load('player-1', 'slot-1')
await saves.remove('player-1', 'slot-1')

Atomowy zapis projektu#

text
tmp → odczyt zwrotny → backup → commit

Zapis najpierw trafia do pliku tymczasowego, jest odczytywany z powrotem dla weryfikacji, poprzednia wersja staje się backupem, a dopiero potem następuje commit. Przerwanie w dowolnym momencie nie zostawia uszkodzonego projektu.

Backendy SaveStore#

SaveStore to kontrakt pięciu metod: read, write, remove, keys, clear.

BackendZastosowanie
MemoryStoreTesty, tryb incognito
LocalStorageStore(prefix = "b2d:")Proste zapisy w przeglądarce
IndexedDbStore({ database?, store? })Duże zapisy, projekt edytora
FileSystemStore(baseDir)Node / Electron
HttpStore({ endpoint, headers? })Zapisy na serwerze (/v1/entries)
SqlSaveStore(client, { dialect, table? })PostgreSQL / MySQL
ts
import { MemoryStore, FileSystemStore, HttpStore, SqlSaveStore, sqlSchema } from '@engine'

const memory = new MemoryStore()
const fs = new FileSystemStore('./saves')
const http = new HttpStore({ endpoint: 'https://api.example.com/v1/entries' })

const sql = new SqlSaveStore(pgClient, { dialect: 'postgres', table: 'player_saves' })
const ddl = sqlSchema({ dialect: 'postgres', table: 'player_saves' })

Zero sterowników w bundlu

SqlSaveStore przyjmuje dowolny obiekt z query(sql, params). Dzięki wstrzykiwanemu klientowi w bundlu gry nie ma pg ani mysql2 — sterownik żyje po stronie serwera.

Przykład: system slotów zapisu#

ts
class GameScene extends Scene {
  private saves = new PlayerSaveRepository(new IndexedDbStore({ database: 'mygame' }))
  private playerId = 'player-1'

  async saveGame(slot: string) {
    await this.saves.save(this.playerId, slot, {
      level: this.currentLevel,
      score: this.score,
      collected: this.collected,
      position: { x: this.player.x, y: this.player.y },
      savedAt: Date.now()
    })
  }

  async loadGame(slot: string) {
    const state = await this.saves.load<PlayerState>(this.playerId, slot)
    if (!state) return false
    this.currentLevel = state.level
    this.score = state.score
    this.collected = state.collected
    this.player.setPosition(state.position.x, state.position.y)
    return true
  }

  shutdown() {
    void this.saveGame('autosave')
  }
}

Przykład: zapisy serwerowe#

ts
const store = new HttpStore({
  endpoint: 'https://api.example.com/v1/entries',
  headers: { Authorization: `Bearer ${token}` }
})

const saves = new PlayerSaveRepository(store)
await saves.save(userId, 'cloud', playerState)

Ten sam kod działa lokalnie i w chmurze — zmienia się tylko backend.