Quick answer: Define achievements in a JSON structure with IDs, names, descriptions, icon frames, and target values. Track progress using global variables or a JavaScript object, update on game events, and trigger a slide-in notification when the target is reached. Persist unlock state in Local Storage. For Steam integration, use Greenworks or Steamworks.js in an NW.js/Electron export to call the Steam achievement API.

Achievements add replay value and give players goals beyond the main objective. Whether it is “Kill 100 Enemies,” “Complete the game without dying,” or “Find all hidden secrets,” an achievement system motivates exploration and mastery. Construct 3 does not have a built-in achievement system, but you can build a robust one with JSON data, event-driven tracking, Local Storage persistence, and optional Steam integration.

Defining Achievement Data

Start by defining all achievements in a JSON file. Each achievement needs a unique ID, display name, description, an icon frame reference, a target value (for progress-based achievements), and a type field to distinguish between one-time triggers and cumulative progress.

// achievements.json
{
  "achievements": [
    {
      "id": "first_kill",
      "name": "First Blood",
      "description": "Defeat your first enemy.",
      "icon": 0,
      "type": "trigger",
      "target": 1,
      "hidden": false
    },
    {
      "id": "kill_100",
      "name": "Centurion",
      "description": "Defeat 100 enemies.",
      "icon": 1,
      "type": "cumulative",
      "target": 100,
      "hidden": false
    },
    {
      "id": "no_death_run",
      "name": "Untouchable",
      "description": "Complete the game without dying.",
      "icon": 2,
      "type": "trigger",
      "target": 1,
      "hidden": true
    },
    {
      "id": "collect_all_gems",
      "name": "Gem Collector",
      "description": "Find all 50 hidden gems.",
      "icon": 3,
      "type": "cumulative",
      "target": 50,
      "hidden": false
    }
  ]
}

Load this at game start and store it alongside a runtime state object that tracks each achievement’s current progress and unlock status.

Tracking Progress with Game Events

The tracking system listens for game events and updates achievement progress. Create a central function that handles progress updates so every part of your game can report events through a single entry point.

// Scripting: Achievement tracker
let achievementDefs = [];  // loaded from JSON
let achievementState = {}; // { id: { progress: N, unlocked: bool } }

function initAchievements(defs) {
  achievementDefs = defs;
  for (const def of defs) {
    if (!achievementState[def.id]) {
      achievementState[def.id] = { progress: 0, unlocked: false };
    }
  }
}

function updateAchievement(id, amount) {
  const state = achievementState[id];
  if (!state || state.unlocked) return;

  const def = achievementDefs.find(a => a.id === id);
  if (!def) return;

  state.progress = Math.min(state.progress + amount, def.target);

  if (state.progress >= def.target) {
    state.unlocked = true;
    showUnlockNotification(def);
    saveAchievements();
  }
}

// Call from event sheets or other scripts:
// When an enemy is destroyed:
updateAchievement("first_kill", 1);
updateAchievement("kill_100", 1);

In your event sheets, call the tracking function whenever a relevant event occurs:

// Event sheet: Enemy destroyed
Enemy: On destroyed
  System: Add 1 to TotalKills
  Browser: ExecJS "updateAchievement('first_kill', 1)"
  Browser: ExecJS "updateAchievement('kill_100', 1)"

// Event sheet: Gem collected
Player: On collision with Gem
  Gem: Destroy
  System: Add 1 to GemsCollected
  Browser: ExecJS "updateAchievement('collect_all_gems', 1)"

// Event sheet: Game completed
System: DeathCount = 0
  Browser: ExecJS "updateAchievement('no_death_run', 1)"

Unlock Notifications

When an achievement unlocks, display a notification banner that slides in from the top or side of the screen. Create a sprite called AchievementBanner on the UI layer, initially positioned off-screen. Use the Tween behavior to animate it in.

// Scripting: Show achievement notification
function showUnlockNotification(def) {
  const banner = runtime.objects.AchievementBanner.getFirstInstance();
  const icon = runtime.objects.AchievementIcon.getFirstInstance();
  const title = runtime.objects.AchievementTitle.getFirstInstance();
  const desc = runtime.objects.AchievementDesc.getFirstInstance();

  icon.animationFrame = def.icon;
  title.text = def.name;
  desc.text = "Achievement Unlocked!";

  // Slide in from top
  banner.y = -80;
  banner.behaviors.Tween.startTween("y", 20, 0.4, "ease-out-back");

  // Slide out after 3 seconds
  setTimeout(() => {
    banner.behaviors.Tween.startTween("y", -80, 0.3, "ease-in");
  }, 3000);
}

// Alternative: pure event sheet approach
// Function "ShowAchievementBanner" - params: Name, IconFrame
AchievementBanner: Set Y to -80
AchievementIcon: Set animation frame to IconFrame
AchievementTitle: Set text to Name
AchievementBanner: Tween "SlideIn" Y to 20
  Duration: 0.4  Ease: "ease-out-back"

AchievementBanner: On Tween "SlideIn" finished
  System: Wait 3.0 seconds
  AchievementBanner: Tween "SlideOut" Y to -80
    Duration: 0.3  Ease: "ease-in"

If multiple achievements unlock simultaneously (for example, first kill and kill-100 if they somehow trigger together), queue them so notifications display one after another rather than overlapping.

Persistent Storage and Steam Integration

Save achievement state to Local Storage so progress and unlocks survive between sessions. Save after every unlock and periodically during gameplay.

// Scripting: Save and load achievements
function saveAchievements() {
  localStorage.setItem(
    "game_achievements",
    JSON.stringify(achievementState)
  );
}

function loadAchievements() {
  const raw = localStorage.getItem("game_achievements");
  if (raw) {
    try {
      achievementState = JSON.parse(raw);
    } catch (e) {
      console.warn("Could not load achievements, starting fresh");
      achievementState = {};
    }
  }
}

// Steam integration (NW.js export only)
// Requires the greenworks addon or steamworks.js
function unlockSteamAchievement(steamId) {
  if (typeof greenworks === "undefined") return;

  try {
    greenworks.activateAchievement(steamId, () => {
      console.log("Steam achievement unlocked:", steamId);
    });
  } catch (e) {
    console.warn("Steam API error:", e.message);
  }
}

// Map your game achievement IDs to Steam API names
const steamMapping = {
  "first_kill": "ACH_FIRST_BLOOD",
  "kill_100": "ACH_CENTURION",
  "no_death_run": "ACH_UNTOUCHABLE",
  "collect_all_gems": "ACH_GEM_COLLECTOR"
};

// Call when achievement unlocks:
if (steamMapping[id]) {
  unlockSteamAchievement(steamMapping[id]);
}

The Steam integration layer is optional. Your local achievement system works independently, so web exports and non-Steam builds still have full achievement functionality. Only the Steam notification and cloud sync are lost without the Steam API.

"Build your achievement system to work without any platform integration first. Steam, Google Play, and Game Center are optional layers on top. If your base system is solid, adding platform-specific hooks is straightforward."

Related Issues

If achievement data does not persist between sessions, see Fix: Construct 3 Local Storage Data Not Persisting. If your achievement banner animation does not play correctly, check Fix: Construct 3 Animation Not Playing or Stuck. For issues with the save/load system interfering with achievement state, see Fix: Construct 3 Save/Load System Not Restoring State. And if your exported NW.js build cannot find the Greenworks addon, check Fix: Construct 3 Exported Game Not Running.

Track everything. Unlock once. Save immediately.