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%":