Displaying Achievements¶
This page covers how to display achievements in your game, including loading badge images, handling obscured achievements, and creating effective achievement UI.
Achievement Badge Images¶
Each achievement has associated images for display:
| Property | Description |
|---|---|
image_url |
URL to the achievement badge (shown when unlocked or visible) |
obscure_image_url |
URL to the hidden badge (shown for obscured achievements) |
Loading Achievement Images¶
Retrieve image URLs from cached achievements:
// Get image URL for an achievement
sb_achievement_t achievement;
if (sb_get_cached_achievement(gs, "wizard_mode", &achievement)) {
// Check if obscured and not unlocked
sb_achievement_progress_t progress;
bool is_unlocked = false;
if (sb_get_cached_progress(gs, user_id, "wizard_mode", &progress)) {
is_unlocked = progress.unlocked;
}
const char* image_url;
if (achievement.obscure && !is_unlocked) {
image_url = achievement.obscure_image_url;
} else {
image_url = achievement.image_url;
}
// Load and display the image
load_image_from_url(image_url);
}
// Get image URL for an achievement
auto achievement = gs.getAchievement("wizard_mode");
if (achievement) {
auto progress = gs.getProgress(userId, "wizard_mode");
bool isUnlocked = progress && progress->unlocked;
std::string imageUrl;
if (achievement->obscure && !isUnlocked) {
imageUrl = achievement->obscureImageUrl;
} else {
imageUrl = achievement->imageUrl;
}
// Load and display the image
loadImageFromUrl(imageUrl);
}
# Get image URL for an achievement
achievement = gs.get_achievement("wizard_mode")
if achievement:
progress = gs.get_progress(user_id, "wizard_mode")
is_unlocked = progress and progress.unlocked
if achievement.obscure and not is_unlocked:
image_url = achievement.obscure_image_url
else:
image_url = achievement.image_url
# Load and display the image
load_image_from_url(image_url)
Pre-caching Images¶
For smooth UI, pre-cache achievement images when achievements are loaded:
// Pre-cache all achievement images
void preload_achievement_images(sb_game_handle_t gs) {
if (!sb_has_achievements(gs)) return;
size_t count = sb_get_cached_achievements_count(gs);
for (size_t i = 0; i < count; i++) {
sb_achievement_t ach;
if (sb_get_cached_achievement_at(gs, i, &ach)) {
if (ach.image_url && ach.image_url[0])
queue_image_download(ach.image_url); // User-implemented
if (ach.obscure_image_url && ach.obscure_image_url[0])
queue_image_download(ach.obscure_image_url);
}
}
}
class AchievementImageCache {
public:
void preloadAll(scorbit::GameState& gs) {
if (!gs.hasAchievements()) return;
auto achievements = gs.getAchievements();
for (const auto& ach : achievements) {
// Queue image download
if (!ach.imageUrl.empty()) {
downloadImage(ach.imageUrl);
}
if (!ach.obscureImageUrl.empty()) {
downloadImage(ach.obscureImageUrl);
}
}
}
void downloadImage(const std::string& url) {
if (m_cache.count(url)) return; // Already cached
// Download asynchronously
asyncDownload(url, [this, url](const std::vector<uint8_t>& data) {
std::lock_guard<std::mutex> lock(m_mutex);
m_cache[url] = data;
});
}
std::optional<std::vector<uint8_t>> getImage(const std::string& url) {
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_cache.find(url);
if (it != m_cache.end()) {
return it->second;
}
return std::nullopt;
}
private:
std::unordered_map<std::string, std::vector<uint8_t>> m_cache;
std::mutex m_mutex;
};
import threading
import urllib.request
class AchievementImageCache:
def __init__(self):
self.cache = {}
self.lock = threading.Lock()
def preload_all(self, gs):
if not gs.has_achievements():
return
achievements = gs.get_achievements()
for ach in achievements:
if ach.image_url:
self.download_image(ach.image_url)
if ach.obscure_image_url:
self.download_image(ach.obscure_image_url)
def download_image(self, url):
with self.lock:
if url in self.cache:
return
def download():
try:
data = urllib.request.urlopen(url).read()
with self.lock:
self.cache[url] = data
except Exception as e:
print(f"Failed to download {url}: {e}")
threading.Thread(target=download, daemon=True).start()
def get_image(self, url):
with self.lock:
return self.cache.get(url)
Handling Obscured Achievements¶
Obscured achievements have hidden names and descriptions until unlocked:
// Get display-ready name and image, handling obscured state
void get_display_info(sb_game_handle_t gs, int64_t user_id, const char *key,
const char **out_name, const char **out_desc,
const char **out_image) {
sb_achievement_t ach;
if (!sb_get_cached_achievement(gs, key, &ach)) return;
sb_achievement_progress_t prog;
bool unlocked = sb_get_cached_progress(gs, user_id, key, &prog)
&& prog.unlocked;
if (unlocked || !ach.obscure) {
*out_name = ach.name;
*out_desc = ach.description;
*out_image = ach.image_url;
} else {
*out_name = "???";
*out_desc = "Hidden achievement - unlock to reveal";
*out_image = ach.obscure_image_url;
}
}
struct AchievementDisplayInfo {
std::string name;
std::string description;
std::string imageUrl;
bool isUnlocked;
bool isObscured;
};
AchievementDisplayInfo getDisplayInfo(
scorbit::GameState& gs,
int64_t userId,
const std::string& key
) {
AchievementDisplayInfo info;
auto ach = gs.getAchievement(key);
if (!ach) return info;
auto progress = gs.getProgress(userId, key);
info.isUnlocked = progress && progress->unlocked;
info.isObscured = ach->obscure;
// Show real info if unlocked or not obscured
if (info.isUnlocked || !ach->obscure) {
info.name = ach->name;
info.description = ach->description;
info.imageUrl = ach->imageUrl;
} else {
// Show obscured info
info.name = "???";
info.description = "Hidden achievement - unlock to reveal";
info.imageUrl = ach->obscureImageUrl;
}
return info;
}
def get_display_info(gs, user_id, key):
"""Get display-ready achievement info, handling obscured state."""
ach = gs.get_achievement(key)
if not ach:
return None
progress = gs.get_progress(user_id, key)
is_unlocked = progress and progress.unlocked
info = {
'is_unlocked': is_unlocked,
'is_obscured': ach.obscure
}
# Show real info if unlocked or not obscured
if is_unlocked or not ach.obscure:
info['name'] = ach.name
info['description'] = ach.description
info['image_url'] = ach.image_url
else:
# Show obscured info
info['name'] = "???"
info['description'] = "Hidden achievement - unlock to reveal"
info['image_url'] = ach.obscure_image_url
return info
Creating an Achievement List Display¶
void display_achievement_list(sb_game_handle_t gs, int64_t user_id) {
if (!sb_has_achievements(gs)) {
printf("Loading achievements...\n");
return;
}
size_t count = sb_get_cached_achievements_count(gs);
for (size_t i = 0; i < count; i++) {
sb_achievement_t ach;
if (!sb_get_cached_achievement_at(gs, i, &ach)) continue;
sb_achievement_progress_t prog;
bool unlocked = sb_get_cached_progress(gs, user_id, ach.key, &prog)
&& prog.unlocked;
// Skip invisible unearned achievements
if (!ach.visible && !unlocked) continue;
const char *name, *desc, *image;
get_display_info(gs, user_id, ach.key, &name, &desc, &image);
printf("%s %s%s\n %s\n",
unlocked ? "[✓]" : "[ ]", name,
ach.is_trophy ? " 🏆" : "", desc);
if (!unlocked && ach.count > 1 && prog.progress > 0)
printf(" Progress: %d/%d\n", prog.progress, ach.count);
printf("\n");
}
}
void displayAchievementList(
scorbit::GameState& gs,
int64_t userId
) {
if (!gs.hasAchievements()) {
std::cout << "Loading achievements...\n";
return;
}
auto achievements = gs.getAchievements();
// Sort: unlocked first, then by name
std::sort(achievements.begin(), achievements.end(),
[&gs, userId](const auto& a, const auto& b) {
auto pa = gs.getProgress(userId, a.key);
auto pb = gs.getProgress(userId, b.key);
bool unlockedA = pa && pa->unlocked;
bool unlockedB = pb && pb->unlocked;
if (unlockedA != unlockedB) return unlockedA > unlockedB;
return a.name < b.name;
});
for (const auto& ach : achievements) {
// Skip invisible achievements that aren't unlocked
auto progress = gs.getProgress(userId, ach.key);
bool isUnlocked = progress && progress->unlocked;
if (!ach.visible && !isUnlocked) {
continue;
}
auto info = getDisplayInfo(gs, userId, ach.key);
// Display achievement
std::cout << (isUnlocked ? "[✓] " : "[ ] ");
std::cout << info.name;
if (ach.isTrophy) {
std::cout << " 🏆";
}
std::cout << "\n " << info.description << "\n";
// Show progress for limited achievements
if (!isUnlocked && ach.count > 1 && progress) {
std::cout << " Progress: " << progress->progress
<< "/" << ach.count << "\n";
}
std::cout << "\n";
}
}
def display_achievement_list(gs, user_id):
"""Display a formatted list of achievements."""
if not gs.has_achievements():
print("Loading achievements...")
return
achievements = gs.get_achievements()
# Sort: unlocked first, then by name
def sort_key(ach):
progress = gs.get_progress(user_id, ach.key)
is_unlocked = progress and progress.unlocked
return (not is_unlocked, ach.name)
achievements.sort(key=sort_key)
for ach in achievements:
progress = gs.get_progress(user_id, ach.key)
is_unlocked = progress and progress.unlocked
# Skip invisible achievements that aren't unlocked
if not ach.visible and not is_unlocked:
continue
info = get_display_info(gs, user_id, ach.key)
# Display achievement
status = "[✓]" if is_unlocked else "[ ]"
trophy = " 🏆" if ach.is_trophy else ""
print(f"{status} {info['name']}{trophy}")
print(f" {info['description']}")
# Show progress for limited achievements
if not is_unlocked and ach.count > 1 and progress:
print(f" Progress: {progress.progress}/{ach.count}")
print()
Creating Achievement Unlock Popups¶
typedef struct {
bool visible;
const char *name;
const char *icon_url;
bool is_trophy;
float elapsed;
float alpha;
} achievement_popup_t;
void popup_show(achievement_popup_t *p, const char *name,
const char *icon_url, bool is_trophy) {
p->visible = true;
p->name = name;
p->icon_url = icon_url;
p->is_trophy = is_trophy;
p->elapsed = 0.0f;
p->alpha = 0.0f;
}
void popup_update(achievement_popup_t *p, float dt) {
if (!p->visible) return;
p->elapsed += dt;
if (p->elapsed < 0.3f) p->alpha = p->elapsed / 0.3f;
else if (p->elapsed < 2.7f) p->alpha = 1.0f;
else if (p->elapsed < 3.0f) p->alpha = 1.0f - (p->elapsed - 2.7f) / 0.3f;
else p->visible = false;
}
class AchievementPopup {
public:
void show(
const std::string& name,
const std::string& iconUrl,
bool isTrophy
) {
m_visible = true;
m_name = name;
m_iconUrl = iconUrl;
m_isTrophy = isTrophy;
m_showTime = std::chrono::steady_clock::now();
m_alpha = 0.0f;
}
void update(float deltaTime) {
if (!m_visible) return;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - m_showTime
).count() / 1000.0f;
// Fade in (0-0.3s)
if (elapsed < 0.3f) {
m_alpha = elapsed / 0.3f;
}
// Hold (0.3-2.7s)
else if (elapsed < 2.7f) {
m_alpha = 1.0f;
}
// Fade out (2.7-3.0s)
else if (elapsed < 3.0f) {
m_alpha = 1.0f - (elapsed - 2.7f) / 0.3f;
}
// Done
else {
m_visible = false;
}
}
void render() {
if (!m_visible) return;
// Draw popup with current alpha
drawRect(/* popup background */);
drawImage(m_iconUrl, /* position */);
drawText(m_isTrophy ? "TROPHY UNLOCKED!" : "ACHIEVEMENT UNLOCKED!", /* position */);
drawText(m_name, /* position */);
}
private:
bool m_visible = false;
std::string m_name;
std::string m_iconUrl;
bool m_isTrophy = false;
std::chrono::steady_clock::time_point m_showTime;
float m_alpha = 0.0f;
};
import time
class AchievementPopup:
def __init__(self):
self.visible = False
self.name = ""
self.icon_url = ""
self.is_trophy = False
self.start_time = 0.0
self.alpha = 0.0
def show(self, name, icon_url, is_trophy):
self.visible = True
self.name = name
self.icon_url = icon_url
self.is_trophy = is_trophy
self.start_time = time.monotonic()
self.alpha = 0.0
def update(self):
if not self.visible:
return
elapsed = time.monotonic() - self.start_time
if elapsed < 0.3:
self.alpha = elapsed / 0.3
elif elapsed < 2.7:
self.alpha = 1.0
elif elapsed < 3.0:
self.alpha = 1.0 - (elapsed - 2.7) / 0.3
else:
self.visible = False
Best Practices¶
1. Handle Missing Images Gracefully¶
void loadAchievementImage(const std::string& url) {
auto image = imageCache.getImage(url);
if (image) {
displayImage(*image);
} else {
displayPlaceholder(); // Show default badge
imageCache.downloadImage(url); // Queue download
}
}
2. Respect Visibility Settings¶
// Only show achievements that should be visible
if (!achievement.visible) {
auto progress = gs.getProgress(userId, achievement.key);
if (!progress || !progress->unlocked) {
continue; // Don't show
}
}
3. Optimize Image Loading¶
- Pre-cache images when achievements are first loaded
- Use thumbnail versions for lists, full resolution for detail views
- Implement lazy loading for long achievement lists
4. Provide Visual Distinction¶
- Use different styles for unlocked vs locked achievements
- Highlight trophy achievements with special styling
- Show progress bars for limited achievements
- Use grayscale or dimmed images for locked achievements