Skip to content

Displaying Achievement Progress

This page covers how to effectively display progress toward limited achievements, including progress bars, milestone indicators, and real-time updates.

Understanding Progress Data

For limited achievements (those with count > 1), progress is tracked as:

Field Description
progress Current count toward the goal
count Target count required for unlock
unlocked Whether the achievement is complete

Reading Progress Data

// Get progress for a specific achievement
sb_achievement_progress_t progress;
sb_achievement_t achievement;

if (sb_get_cached_achievement(gs, "multiball_master", &achievement) &&
    sb_get_cached_progress(gs, user_id, "multiball_master", &progress)) {

    int current = progress.progress;
    int target = achievement.count;
    float percent = (float)current / target * 100;

    printf("%s: %d/%d (%.0f%%)\n",
           achievement.name, current, target, percent);
}
// Get progress for a specific achievement
auto achievement = gs.getAchievement("multiball_master");
auto progress = gs.getProgress(userId, "multiball_master");

if (achievement && progress) {
    int current = progress->progress;
    int target = achievement->count;
    float percent = (float)current / target * 100;

    std::cout << achievement->name << ": "
              << current << "/" << target
              << " (" << (int)percent << "%)\n";
}
# Get progress for a specific achievement
achievement = gs.get_achievement("multiball_master")
progress = gs.get_progress(user_id, "multiball_master")

if achievement and progress:
    current = progress.progress
    target = achievement.count
    percent = current / target * 100

    print(f"{achievement.name}: {current}/{target} ({int(percent)}%)")

Creating Progress Bars

Text-Based Progress Bar

void print_progress_bar(int current, int target, int width) {
    float pct = (target > 0) ? fminf((float)current / target, 1.0f) : 0.0f;
    int filled = (int)(pct * width);

    printf("[");
    for (int i = 0; i < width; i++)
        printf(i < filled ? "#" : "-");
    printf("] %d/%d\n", current, target);
}

void display_progress(sb_game_handle_t gs, int64_t user_id, const char *key) {
    sb_achievement_t ach;
    sb_achievement_progress_t prog;
    if (!sb_get_cached_achievement(gs, key, &ach) || ach.count <= 1) return;

    int current = sb_get_cached_progress(gs, user_id, key, &prog) ? prog.progress : 0;
    printf("%s\n  ", ach.name);
    print_progress_bar(current, ach.count, 20);
}
std::string createProgressBar(int current, int target, int width = 20) {
    float percent = (target > 0) ? std::min((float)current / target, 1.0f) : 0.0f;
    int filled = (int)(percent * width);

    std::string bar = "[";
    for (int i = 0; i < width; i++) {
        bar += (i < filled) ? "â–ˆ" : "â–‘";
    }
    bar += "]";

    return bar;
}

void displayProgress(scorbit::GameState& gs, int64_t userId, const std::string& key) {
    auto ach = gs.getAchievement(key);
    auto progress = gs.getProgress(userId, key);

    if (!ach || ach->count <= 1) return;

    int current = progress ? progress->progress : 0;
    int target = ach->count;

    std::cout << ach->name << "\n";
    std::cout << "  " << createProgressBar(current, target)
              << " " << current << "/" << target << "\n";
}
def create_progress_bar(current, target, width=20):
    percent = min(current / target, 1.0) if target > 0 else 0.0
    filled = int(percent * width)

    bar = "â–ˆ" * filled + "â–‘" * (width - filled)
    return f"[{bar}]"

def display_progress(gs, user_id, key):
    ach = gs.get_achievement(key)
    progress = gs.get_progress(user_id, key)

    if not ach or ach.count <= 1:
        return

    current = progress.progress if progress else 0
    target = ach.count

    print(ach.name)
    print(f"  {create_progress_bar(current, target)} {current}/{target}")

Graphical Progress Bar

typedef struct {
    int current, target;
    float display_pct, target_pct;
} progress_bar_t;

void progress_bar_set(progress_bar_t *pb, int current, int target) {
    pb->current = current;
    pb->target  = target;
    pb->target_pct = (target > 0) ? (float)current / target : 0.0f;
}

void progress_bar_update(progress_bar_t *pb, float dt) {
    if (pb->display_pct < pb->target_pct) {
        pb->display_pct += 2.0f * dt;  // Speed: 2x per second
        if (pb->display_pct > pb->target_pct)
            pb->display_pct = pb->target_pct;
    }
}

void progress_bar_render(const progress_bar_t *pb,
                         float x, float y, float w, float h) {
    draw_rect(x, y, w, h, COLOR_DARK_GRAY);          // Background
    draw_rect(x, y, w * pb->display_pct, h, COLOR_GREEN); // Fill
    draw_rect_outline(x, y, w, h, COLOR_WHITE);       // Border
}
class ProgressBar {
public:
    void setProgress(int current, int target) {
        m_current = current;
        m_target = target;
        m_targetPercent = (target > 0) ? (float)current / target : 0.0f;
    }

    void update(float deltaTime) {
        // Smooth animation toward target
        float speed = 2.0f;  // Percent per second
        if (m_displayPercent < m_targetPercent) {
            m_displayPercent = std::min(
                m_displayPercent + speed * deltaTime,
                m_targetPercent
            );
        }
    }

    void render(float x, float y, float width, float height) {
        // Background
        drawRect(x, y, width, height, Color::DarkGray);

        // Filled portion
        float fillWidth = width * m_displayPercent;
        drawRect(x, y, fillWidth, height, Color::Green);

        // Border
        drawRectOutline(x, y, width, height, Color::White);

        // Text
        std::string text = std::to_string(m_current) + "/" + std::to_string(m_target);
        drawTextCentered(text, x + width/2, y + height/2, Color::White);
    }

private:
    int m_current = 0;
    int m_target = 1;
    float m_displayPercent = 0.0f;
    float m_targetPercent = 0.0f;
};
class ProgressBar:
    def __init__(self):
        self.current = 0
        self.target = 1
        self.display_pct = 0.0
        self.target_pct = 0.0

    def set_progress(self, current, target):
        self.current = current
        self.target = target
        self.target_pct = current / target if target > 0 else 0.0

    def update(self, dt):
        if self.display_pct < self.target_pct:
            self.display_pct = min(
                self.display_pct + 2.0 * dt, self.target_pct)

    def render(self, x, y, width, height):
        fill_width = width * self.display_pct
        draw_rect(x, y, width, height, COLOR_DARK_GRAY)
        draw_rect(x, y, fill_width, height, COLOR_GREEN)
        draw_rect_outline(x, y, width, height, COLOR_WHITE)

Milestone Indicators

Show milestones for long-running achievements:

void display_milestones(int current, int target) {
    float pct = (float)current / target * 100;
    const int thresholds[] = {25, 50, 75, 100};
    const char *labels[]   = {"25%", "Halfway!", "Almost!", "Complete!"};

    printf("Milestones: ");
    for (int i = 0; i < 4; i++) {
        if (pct >= thresholds[i])
            printf("[✓ %s] ", labels[i]);
        else
            printf("[â—‹ %d%%] ", thresholds[i]);
    }
    printf("\n");
}
struct Milestone {
    int threshold;  // Percentage
    std::string label;
    bool achieved;
};

std::vector<Milestone> getMilestones(int current, int target) {
    float percent = (float)current / target * 100;

    return {
        {25, "25%", percent >= 25},
        {50, "Halfway!", percent >= 50},
        {75, "Almost there!", percent >= 75},
        {100, "Complete!", percent >= 100}
    };
}

void displayMilestones(int current, int target) {
    auto milestones = getMilestones(current, target);

    std::cout << "Milestones: ";
    for (const auto& m : milestones) {
        if (m.achieved) {
            std::cout << "[✓ " << m.label << "] ";
        } else {
            std::cout << "[â—‹ " << m.threshold << "%] ";
        }
    }
    std::cout << "\n";
}
def get_milestones(current, target):
    percent = current / target * 100

    return [
        {"threshold": 25, "label": "25%", "achieved": percent >= 25},
        {"threshold": 50, "label": "Halfway!", "achieved": percent >= 50},
        {"threshold": 75, "label": "Almost there!", "achieved": percent >= 75},
        {"threshold": 100, "label": "Complete!", "achieved": percent >= 100}
    ]

def display_milestones(current, target):
    milestones = get_milestones(current, target)

    print("Milestones: ", end="")
    for m in milestones:
        if m["achieved"]:
            print(f"[✓ {m['label']}] ", end="")
        else:
            print(f"[â—‹ {m['threshold']}%] ", end="")
    print()

Real-Time Progress Updates

Handle progress events to update displays in real-time.

Event Handling Pattern

The examples below show how to handle progress events from the SDK. The onProgressEvent method extracts data from the SDK event using event.getAchievementProgress() (C++) or event.get_achievement_progress() (Python) as shown in Achievement Notifications.

// Simple progress tracking with milestone detection
#define MAX_TRACKED 64
static float s_last_pct[MAX_TRACKED];

void on_progress_event(const char *key, const char *name,
                       int current, int target, int index) {
    if (target <= 0 || index >= MAX_TRACKED) return;

    float old_pct = s_last_pct[index];
    float new_pct = (float)current / target * 100;

    if (old_pct < 50 && new_pct >= 50)
        show_milestone(name, "Halfway there!");
    else if (old_pct < 75 && new_pct >= 75)
        show_milestone(name, "Almost there!");

    s_last_pct[index] = new_pct;
    update_progress_display(key, current, target);  // User-implemented
}
class AchievementProgressDisplay {
public:
    void onProgressEvent(const scorbit::Event& event) {
        std::string key, name, userId, username, iconUrl;
        int currentValue = 0, targetValue = 0;

        if (!event.getAchievementProgress(key, name, userId, username, iconUrl,
                                          currentValue, targetValue)) {
            return;
        }

        // Find or create progress bar for this achievement
        auto& bar = m_progressBars[key];
        bar.setProgress(currentValue, targetValue);

        // Check for milestone using state tracking
        if (targetValue > 0) {
            float oldPercent = m_lastPercent[key];
            float newPercent = (float)currentValue / targetValue * 100;

            if (oldPercent < 50 && newPercent >= 50) {
                showMilestonePopup(name, "Halfway there!");
            } else if (oldPercent < 75 && newPercent >= 75) {
                showMilestonePopup(name, "Almost there!");
            }

            m_lastPercent[key] = newPercent;
        }
    }

    void update(float deltaTime) {
        for (auto& [key, bar] : m_progressBars) {
            bar.update(deltaTime);
        }
    }

    void render() {
        // Render all active progress bars
        float y = 10;
        for (auto& [key, bar] : m_progressBars) {
            bar.render(10, y, 200, 20);
            y += 30;
        }
    }

private:
    std::unordered_map<std::string, ProgressBar> m_progressBars;
    std::unordered_map<std::string, float> m_lastPercent;
};
class AchievementProgressDisplay:
    def __init__(self):
        self.progress_bars = {}
        self.last_percent = {}

    def on_progress_event(self, event):
        success, key, name, user_id, username, icon_url, current_value, target_value = \
            event.get_achievement_progress()

        if not success:
            return

        # Update progress bar
        self.progress_bars[key] = {
            'current': current_value,
            'target': target_value,
            'name': name
        }

        # Check for milestone using state tracking
        if target_value > 0:
            old_percent = self.last_percent.get(key, 0)
            new_percent = current_value / target_value * 100

            if old_percent < 50 <= new_percent:
                show_milestone_popup(name, "Halfway there!")
            elif old_percent < 75 <= new_percent:
                show_milestone_popup(name, "Almost there!")

            self.last_percent[key] = new_percent

    def render(self):
        for key, bar in self.progress_bars.items():
            print(f"{bar['name']}: {create_progress_bar(bar['current'], bar['target'])}")

Displaying All In-Progress Achievements

void display_in_progress(sb_game_handle_t gs, int64_t user_id) {
    if (!sb_has_achievements(gs)) return;

    size_t count = sb_get_cached_achievements_count(gs);
    printf("=== In Progress ===\n\n");

    for (size_t i = 0; i < count; i++) {
        sb_achievement_t ach;
        if (!sb_get_cached_achievement_at(gs, i, &ach)) continue;
        if (ach.count <= 1) continue;

        sb_achievement_progress_t prog;
        if (!sb_get_cached_progress(gs, user_id, ach.key, &prog)) continue;
        if (prog.unlocked || prog.progress == 0) continue;

        printf("%s\n  ", ach.name);
        print_progress_bar(prog.progress, ach.count, 20);
        printf("\n");
    }
}
void displayInProgressAchievements(scorbit::GameState& gs, int64_t userId) {
    if (!gs.hasAchievements()) return;

    auto achievements = gs.getAchievements();
    std::vector<std::pair<scorbit::Achievement, int>> inProgress;

    // Collect achievements with progress
    for (const auto& ach : achievements) {
        if (ach.count <= 1) continue;  // Skip unlimited

        auto progress = gs.getProgress(userId, ach.key);
        if (!progress) continue;
        if (progress->unlocked) continue;  // Already complete
        if (progress->progress == 0) continue;  // No progress yet

        inProgress.emplace_back(ach, progress->progress);
    }

    // Sort by completion percentage (highest first)
    std::sort(inProgress.begin(), inProgress.end(),
        [](const auto& a, const auto& b) {
            float pctA = (float)a.second / a.first.count;
            float pctB = (float)b.second / b.first.count;
            return pctA > pctB;
        });

    // Display
    std::cout << "=== In Progress ===\n\n";
    for (const auto& [ach, progress] : inProgress) {
        float pct = (float)progress / ach.count * 100;
        std::cout << ach.name << "\n";
        std::cout << "  " << createProgressBar(progress, ach.count)
                  << " " << (int)pct << "%\n\n";
    }
}
def display_in_progress_achievements(gs, user_id):
    if not gs.has_achievements():
        return

    achievements = gs.get_achievements()
    in_progress = []

    # Collect achievements with progress
    for ach in achievements:
        if ach.count <= 1:
            continue  # Skip unlimited

        progress = gs.get_progress(user_id, ach.key)
        if not progress:
            continue
        if progress.unlocked:
            continue  # Already complete
        if progress.progress == 0:
            continue  # No progress yet

        in_progress.append((ach, progress.progress))

    # Sort by completion percentage (highest first)
    in_progress.sort(key=lambda x: x[1] / x[0].count, reverse=True)

    # Display
    print("=== In Progress ===\n")
    for ach, prog in in_progress:
        pct = prog / ach.count * 100
        print(ach.name)
        print(f"  {create_progress_bar(prog, ach.count)} {int(pct)}%\n")

Best Practices

1. Only Show Progress for Limited Achievements

// Check if progress makes sense for this achievement
if (achievement.count <= 1 || achievement.inputTime != "limited") {
    // Show simple locked/unlocked state instead
    showLockedUnlockedState();
} else {
    showProgressBar();
}

2. Animate Progress Changes

Smooth animations make progress feel more rewarding:

// Don't jump immediately
progressBar.setProgress(newValue);  // Animates smoothly

// Not: progressBar.displayPercent = newValue / target;  // Jarring

3. Celebrate Milestones

// Show special feedback at key milestones
if (crossedMilestone(50)) {
    playSound("milestone");
    showToast("Halfway to " + achievementName + "!");
}

4. Handle Edge Cases

// Prevent division by zero
if (target == 0) target = 1;

// Clamp to valid range
float percent = std::clamp((float)current / target, 0.0f, 1.0f);

5. Show Remaining Count

Sometimes "5 more to go" is more motivating than "50%":

int remaining = target - current;
if (remaining <= 5) {
    std::cout << "Only " << remaining << " more to go!\n";
}