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