← Docs

Web Games Integration

Overview

This guide covers integrating SubakoAchv into HTML5 games built with Canvas, WebGL, or game frameworks like Phaser, PixiJS, or custom engines. We'll cover game loop integration, real-time notifications, and performance considerations.

Game Manifest Example

Here's a complete manifest for a typical web game with various achievement types:

{
  "gameId": "space-shooter",
  "name": "Space Shooter",
  "version": "1.0.0",
  "achievements": [
    {
      "id": "first_kill",
      "name": "First Blood",
      "description": "Destroy your first enemy",
      "points": 5,
      "icon": "assets/achievements/first_kill.svg",
      "trigger": { "type": "event-once", "event": "enemy_destroyed" }
    },
    {
      "id": "enemy_slayer",
      "name": "Enemy Slayer",
      "description": "Destroy 50 enemies",
      "points": 20,
      "icon": "assets/achievements/slayer.svg",
      "trigger": { "type": "event-count", "event": "enemy_destroyed", "value": 50 }
    },
    {
      "id": "score_master",
      "name": "Score Master",
      "description": "Reach a score of 10,000",
      "points": 30,
      "icon": "assets/achievements/score.svg",
      "trigger": { "type": "stat-gte", "stat": "score", "value": 10000 }
    },
    {
      "id": "survivor",
      "name": "Survivor",
      "description": "Complete a level without taking damage",
      "points": 25,
      "icon": "assets/achievements/survivor.svg",
      "trigger": { "type": "manual" }
    },
    {
      "id": "completionist",
      "name": "Completionist",
      "description": "Unlock all other achievements",
      "points": 50,
      "icon": "assets/achievements/completionist.svg",
      "hidden": true,
      "trigger": { "type": "manual" }
    }
  ]
}

Basic Game Integration

Initialize the SDK when your game loads:

import { createSubako, loadManifest } from './vendor/subako-achv/index.js';

class Game {
  constructor() {
    this.canvas = document.getElementById('gameCanvas');
    this.ctx = this.canvas.getContext('2d');
    this.sdk = null;
    this.score = 0;
    this.enemiesDestroyed = 0;
  }

  async init() {
    // Load the achievement manifest
    const manifest = await loadManifest('./manifest.json');
    
    // Initialize SDK with player info
    this.sdk = createSubako({
      gameId: manifest.gameId,
      manifest: manifest,
      playerId: this.getPlayerId(),
      displayName: this.getPlayerName()
    });

    // Set up achievement notifications
    this.setupNotifications();

    // Start the game
    this.start();
  }

  getPlayerId() {
    // Use localStorage or your auth system
    return localStorage.getItem('playerId') || 'guest-' + Date.now();
  }

  getPlayerName() {
    return localStorage.getItem('playerName') || 'Player';
  }

  setupNotifications() {
    // Show toast when achievement unlocks
    this.sdk.on('unlock', ({ achievement }) => {
      this.showAchievementToast(achievement);
    });

    // Update progress bars
    this.sdk.on('progress', ({ achievement, ratio }) => {
      this.updateProgressUI(achievement.id, ratio);
    });
  }

  showAchievementToast(achievement) {
    // Create a toast notification
    const toast = document.createElement('div');
    toast.className = 'achievement-toast';
    toast.innerHTML = `
      <img src="${achievement.icon}" alt="${achievement.name}" />
      <div>
        <div class="title">Achievement Unlocked!</div>
        <div class="name">${achievement.name}</div>
        <div class="desc">${achievement.description}</div>
      </div>
    `;
    document.body.appendChild(toast);
    
    // Remove after 3 seconds
    setTimeout(() => toast.remove(), 3000);
  }

  updateProgressUI(achievementId, ratio) {
    const progressBar = document.getElementById(`progress-${achievementId}`);
    if (progressBar) {
      progressBar.style.width = `${ratio * 100}%`;
    }
  }
}

Game Loop Integration

Emit events and update stats during gameplay:

class Game {
  // ... previous code ...

  start() {
    this.gameLoop();
  }

  gameLoop() {
    this.update();
    this.render();
    requestAnimationFrame(() => this.gameLoop());
  }

  update() {
    // Update game state
    this.checkCollisions();
    this.updateEntities();
  }

  onEnemyDestroyed(enemy) {
    // Update game state
    this.enemiesDestroyed++;
    this.score += enemy.points;

    // Emit achievement events
    this.sdk.emit('enemy_destroyed');
    this.sdk.updateStat('score', this.score);

    // Check for special events
    if (this.enemiesDestroyed === 10) {
      this.sdk.emit('kill_streak_10');
    }
  }

  onPlayerHit(damage) {
    this.health -= damage;
    this.sdk.updateStat('damage_taken', (prev) => prev + damage);

    if (this.health <= 0) {
      this.gameOver();
    }
  }

  onLevelComplete(levelNum) {
    this.sdk.emit('level_completed');
    this.sdk.updateStat('level', levelNum);

    // Check for no-damage completion
    if (this.damageTakenThisLevel === 0) {
      this.sdk.unlock('survivor');
    }
  }

  onPowerUpCollected(type) {
    this.sdk.emit('powerup_collected');
    this.sdk.updateStat('powerups_collected', (prev) => prev + 1);
  }

  gameOver() {
    this.sdk.emit('game_over');
    this.sdk.updateStat('games_played', (prev) => prev + 1);
    this.sdk.updateStat('high_score', (prev) => Math.max(prev, this.score));
  }
}

Canvas Game Example

Here's a minimal Canvas game with achievement tracking (similar to the demo):

class CanvasGame {
  constructor() {
    this.canvas = document.getElementById('game');
    this.ctx = this.canvas.getContext('2d');
    this.player = { x: 100, y: 100, size: 20 };
    this.enemies = [];
    this.coins = 0;
  }

  async init() {
    const manifest = await loadManifest('./game-manifest.json');
    this.sdk = createSubako({
      gameId: manifest.gameId,
      manifest: manifest
    });

    this.sdk.on('unlock', ({ achievement }) => {
      console.log('Unlocked:', achievement.name);
      this.showNotification(achievement);
    });

    this.setupInput();
    this.spawnLoop();
    this.gameLoop();
  }

  setupInput() {
    document.addEventListener('keydown', (e) => {
      if (e.key === 'ArrowUp') this.player.y -= 5;
      if (e.key === 'ArrowDown') this.player.y += 5;
      if (e.key === 'ArrowLeft') this.player.x -= 5;
      if (e.key === 'ArrowRight') this.player.x += 5;
    });

    this.canvas.addEventListener('click', (e) => {
      this.shoot(e.offsetX, e.offsetY);
    });
  }

  shoot(x, y) {
    // ... bullet logic ...
    
    // Check if enemy hit
    const hitEnemy = this.checkHit(x, y);
    if (hitEnemy) {
      this.onEnemyKilled(hitEnemy);
    }
  }

  onEnemyKilled(enemy) {
    this.sdk.emit('enemy_killed');
    this.coins += enemy.coinValue;
    this.sdk.updateStat('coins', this.coins);
  }

  onCoinCollected(value) {
    this.coins += value;
    this.sdk.updateStat('coins', this.coins);
    this.sdk.emit('coin_collected');
  }

  showNotification(achievement) {
    // Display achievement unlock UI
    const notification = document.createElement('div');
    notification.className = 'achievement-notification';
    notification.textContent = `Achievement Unlocked: ${achievement.name}`;
    document.body.appendChild(notification);
    setTimeout(() => notification.remove(), 3000);
  }
}

Performance Considerations

  • Batch events: If you're emitting many events per frame, consider batching them or using stats instead
  • Avoid frequent stat updates: Instead of updating stats every frame, update them at key moments (level complete, enemy killed, etc.)
  • Cache achievement list: Call sdk.list() once and update your UI incrementally via events
  • Defer notifications: Queue achievement notifications and show them during natural breaks (between levels, after death)

Optimized Event Emission

class Game {
  constructor() {
    this.pendingEvents = [];
    this.pendingStats = {};
  }

  // Instead of emitting immediately
  onEnemyKilled() {
    this.pendingEvents.push('enemy_killed');
    this.pendingStats.enemies_killed = (this.pendingStats.enemies_killed || 0) + 1;
  }

  // Flush events at the end of each frame
  flushAchievements() {
    for (const event of this.pendingEvents) {
      this.sdk.emit(event);
    }
    for (const [stat, value] of Object.entries(this.pendingStats)) {
      this.sdk.updateStat(stat, value);
    }
    this.pendingEvents = [];
    this.pendingStats = {};
  }

  gameLoop() {
    this.update();
    this.render();
    this.flushAchievements(); // Flush at end of frame
    requestAnimationFrame(() => this.gameLoop());
  }
}

UI Integration

Create an achievement panel that shows progress:

class AchievementUI {
  constructor(sdk) {
    this.sdk = sdk;
    this.container = document.getElementById('achievement-panel');
    this.render();
  }

  render() {
    const achievements = this.sdk.list();
    const points = this.sdk.totalPoints();

    this.container.innerHTML = `
      <div class="points-summary">
        ${points.earned} / ${points.possible} points
      </div>
      <div class="achievements-grid">
        ${achievements.map(a => this.renderAchievement(a)).join('')}
      </div>
    `;
  }

  renderAchievement({ achievement, unlocked, ratio, current, target }) {
    return `
      <div class="achievement ${unlocked ? 'unlocked' : 'locked'}">
        <img src="${achievement.icon || 'locked.svg'}" alt="${achievement.name}" />
        <div class="info">
          <div class="name">${achievement.name}</div>
          <div class="description">${achievement.description}</div>
          <div class="progress-bar">
            <div style="width: ${ratio * 100}%"></div>
          </div>
          <div class="progress-text">${current} / ${target}</div>
        </div>
        <div class="points">${achievement.points} pts</div>
      </div>
    `;
  }

  update() {
    this.render();
  }
}

// Usage
const ui = new AchievementUI(sdk);
sdk.on('unlock', () => ui.update());
sdk.on('progress', () => ui.update());

Save/Load Integration

Integrate achievement saves with your game's save system:

class GameSaveManager {
  constructor(sdk) {
    this.sdk = sdk;
  }

  saveGame() {
    const gameState = {
      level: this.level,
      score: this.score,
      playerPosition: this.player.position,
      // ... other game state
    };

    const achievementSave = this.sdk.exportSave();

    const saveData = {
      game: gameState,
      achievements: achievementSave,
      timestamp: Date.now()
    };

    localStorage.setItem('gameSave', JSON.stringify(saveData));
  }

  loadGame() {
    const raw = localStorage.getItem('gameSave');
    if (!raw) return false;

    const saveData = JSON.parse(raw);
    
    // Restore game state
    this.level = saveData.game.level;
    this.score = saveData.game.score;
    this.player.position = saveData.game.playerPosition;

    // Restore achievements
    this.sdk.importSave(saveData.achievements);

    return true;
  }
}

Framework-Specific Tips

Phaser

class GameScene extends Phaser.Scene {
  async create() {
    const manifest = await loadManifest('manifest.json');
    this.sdk = createSubako({ gameId: manifest.gameId, manifest });
    
    this.sdk.on('unlock', (data) => {
      this.showAchievement(data.achievement);
    });
  }

  onEnemyKilled() {
    this.sdk.emit('enemy_killed');
  }
}

PixiJS

const app = new PIXI.Application();
const manifest = await loadManifest('manifest.json');
const sdk = createSubako({ gameId: manifest.gameId, manifest });

// Use PixiJS events
app.stage.on('enemyKilled', () => {
  sdk.emit('enemy_killed');
});

Next Steps