In-Game Achievement Notifications¶
The SDK provides real-time notifications for achievement events via WebSocket connection. These are in-game notifications displayed on the pinball machine's DMD or other display—separate from push notifications sent to the Scorbit mobile app.
This page covers how to set up and handle in-game achievement notifications effectively.
Event Types¶
The SDK delivers three types of achievement notifications via the Centrifugo WebSocket connection:
| Event | Description |
|---|---|
AchievementUnlocked |
A player has earned an achievement |
AchievementLocked |
A trophy achievement has been revoked |
AchievementProgress |
Progress toward a limited achievement has changed |
Notifications are generated for both in-session unlocks (triggered during gameplay) and post-game unlocks (server-evaluated after game completion, e.g., ACHIEVEMENT chains and global-scope rules).
Setting Up the Event Callback¶
Register a callback function to receive achievement events. Important: The callback must be set on the Config before creating the GameState.
// Event callback function
void on_event(const sb_event_t* event, void* user_data) {
sb_event_type_t type = sb_event_type(event);
switch (type) {
case SB_EVT_ACHIEVEMENT_UNLOCKED:
handle_achievement_unlocked(event);
break;
case SB_EVT_ACHIEVEMENT_LOCKED:
handle_achievement_locked(event);
break;
case SB_EVT_ACHIEVEMENT_PROGRESS:
handle_achievement_progress(event);
break;
default:
// Handle other event types
break;
}
}
// Register the callback during configuration
sb_config_t config = sb_config_create();
sb_config_set_event_callback(config, on_event, NULL);
// ... other config settings ...
// Create game state with the config
sb_game_handle_t gs = sb_create_game_state(config);
sb_config_destroy(config); // Config can be destroyed after creation
// Event callback function
void onEvent(const scorbit::Event& event) {
switch (event.type()) {
case scorbit::EventType::AchievementUnlocked:
handleAchievementUnlocked(event);
break;
case scorbit::EventType::AchievementLocked:
handleAchievementLocked(event);
break;
case scorbit::EventType::AchievementProgress:
handleAchievementProgress(event);
break;
default:
// Handle other event types
break;
}
}
// Register the callback during configuration
scorbit::Config config;
config.setEventCallback(onEvent);
// ... other config settings ...
// Create game state with the config
auto gs = scorbit::createGameState(config);
def on_event(event):
event_type = event.type()
if event_type == scorbit.EventType.AchievementUnlocked:
handle_achievement_unlocked(event)
elif event_type == scorbit.EventType.AchievementLocked:
handle_achievement_locked(event)
elif event_type == scorbit.EventType.AchievementProgress:
handle_achievement_progress(event)
# Register the callback during configuration
config = scorbit.Config()
config.set_event_callback(on_event)
# ... other config settings ...
# Create game state with the config
gs = scorbit.create_game_state(config)
Handling Unlock Notifications¶
When an achievement is unlocked, display a notification to the player:
void handle_achievement_unlocked(const sb_event_t* event) {
const char *key = NULL;
const char *name = NULL;
const char *user_id = NULL;
const char *username = NULL;
const char *icon_url = NULL;
bool is_trophy = false;
if (!sb_event_achievement_unlocked(event, &key, &name, &user_id,
&username, &icon_url, &is_trophy)) {
return;
}
printf("=== ACHIEVEMENT UNLOCKED ===\n");
printf("Player: %s\n", username ? username : "");
printf("Achievement: %s\n", name ? name : "");
// Trigger visual notification
show_unlock_popup(name, icon_url, is_trophy);
// Play sound effect
play_sound("achievement_unlock");
// Log for analytics
log_achievement_unlock(key, user_id);
}
void handleAchievementUnlocked(const scorbit::Event& event) {
std::string key, name, userId, username, iconUrl;
bool isTrophy = false;
if (!event.getAchievementUnlocked(key, name, userId, username, iconUrl, isTrophy)) {
return;
}
std::cout << "=== ACHIEVEMENT UNLOCKED ===\n";
std::cout << "Player: " << username << "\n";
std::cout << "Achievement: " << name << "\n";
// Trigger visual notification
showUnlockPopup(name, iconUrl, isTrophy);
// Play sound effect
playSound("achievement_unlock");
// Log for analytics
logAchievementUnlock(key, userId);
}
def handle_achievement_unlocked(event):
success, key, name, user_id, username, icon_url, is_trophy = \
event.get_achievement_unlocked()
if not success:
return
print("=== ACHIEVEMENT UNLOCKED ===")
print(f"Player: {username}")
print(f"Achievement: {name}")
# Trigger visual notification
show_unlock_popup(name, icon_url, is_trophy)
# Play sound effect
play_sound("achievement_unlock")
# Log for analytics
log_achievement_unlock(key, user_id)
Handling Lock Notifications (Trophies)¶
Trophy achievements can be revoked. Handle this gracefully:
void handle_achievement_locked(const sb_event_t* event) {
const char *key = NULL;
const char *name = NULL;
const char *user_id = NULL;
const char *username = NULL;
const char *icon_url = NULL;
if (!sb_event_achievement_locked(event, &key, &name, &user_id,
&username, &icon_url)) {
return;
}
printf("Trophy revoked: %s\n", name ? name : "");
printf("Player: %s\n", username ? username : "");
// Update UI to show trophy was lost
update_trophy_status(key, user_id, false);
// Optionally show notification
show_trophy_lost_notification(name, username);
}
void handleAchievementLocked(const scorbit::Event& event) {
std::string key, name, userId, username, iconUrl;
if (!event.getAchievementLocked(key, name, userId, username, iconUrl)) {
return;
}
std::cout << "Trophy revoked: " << name << "\n";
std::cout << "Player: " << username << "\n";
// Update UI to show trophy was lost
updateTrophyStatus(key, userId, false);
// Optionally show notification
showTrophyLostNotification(name, username);
}
def handle_achievement_locked(event):
success, key, name, user_id, username, icon_url = \
event.get_achievement_locked()
if not success:
return
print(f"Trophy revoked: {name}")
print(f"Player: {username}")
# Update UI to show trophy was lost
update_trophy_status(key, user_id, False)
# Optionally show notification
show_trophy_lost_notification(name, username)
Handling Progress Notifications¶
Show progress updates for limited achievements:
// Track previous percentages to detect milestone crossings
static float last_percent[MAX_ACHIEVEMENTS]; // User-managed storage
void handle_achievement_progress(const sb_event_t* event) {
const char *key = NULL;
const char *name = NULL;
const char *user_id = NULL;
const char *username = NULL;
const char *icon_url = NULL;
int current_value = 0;
int target_value = 0;
if (!sb_event_achievement_progress(event, &key, &name, &user_id,
&username, &icon_url,
¤t_value, &target_value)) {
return;
}
printf("Progress: %s - %d/%d\n", name ? name : "", current_value, target_value);
// Update progress bar/counter
update_progress_display(key, current_value, target_value);
// Show milestone notifications using state tracking
// This approach ensures milestones aren't missed on large increments
if (target_value > 0) {
int key_index = get_achievement_index(key); // User-implemented lookup
float old_percent = last_percent[key_index];
float new_percent = (float)current_value / target_value * 100;
if (old_percent < 50 && new_percent >= 50) {
show_milestone_notification(name, "Halfway there!");
}
if (old_percent < 75 && new_percent >= 75) {
show_milestone_notification(name, "Almost there!");
}
last_percent[key_index] = new_percent;
}
}
// Member variable to track previous percentages
std::unordered_map<std::string, float> m_lastPercent;
void handleAchievementProgress(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;
}
std::cout << "Progress: " << name
<< " - " << currentValue << "/" << targetValue << "\n";
// Update progress bar/counter
updateProgressDisplay(key, currentValue, targetValue);
// Show milestone notifications using state tracking
// This approach ensures milestones aren't missed on large increments
if (targetValue > 0) {
float oldPercent = m_lastPercent[key];
float newPercent = (float)currentValue / targetValue * 100;
if (oldPercent < 50 && newPercent >= 50) {
showMilestoneNotification(name, "Halfway there!");
}
if (oldPercent < 75 && newPercent >= 75) {
showMilestoneNotification(name, "Almost there!");
}
m_lastPercent[key] = newPercent;
}
}
# Module-level or class member to track previous percentages
last_percent = {}
def handle_achievement_progress(event):
success, key, name, user_id, username, icon_url, current_value, target_value = \
event.get_achievement_progress()
if not success:
return
print(f"Progress: {name} - {current_value}/{target_value}")
# Update progress bar/counter
update_progress_display(key, current_value, target_value)
# Show milestone notifications using state tracking
# This approach ensures milestones aren't missed on large increments
if target_value > 0:
old_percent = last_percent.get(key, 0)
new_percent = current_value / target_value * 100
if old_percent < 50 <= new_percent:
show_milestone_notification(name, "Halfway there!")
if old_percent < 75 <= new_percent:
show_milestone_notification(name, "Almost there!")
last_percent[key] = new_percent
Notification Queue Management¶
For games with many achievements, manage notification timing:
#define NOTIF_QUEUE_SIZE 32
typedef struct {
const char *type; // "unlock", "lock", "progress"
const char *key;
const char *name;
const char *icon_url;
} notification_t;
static notification_t s_queue[NOTIF_QUEUE_SIZE];
static int s_head = 0, s_tail = 0;
static bool s_displaying = false;
static float s_display_timer = 0.0f;
void notif_enqueue(notification_t n) {
s_queue[s_tail % NOTIF_QUEUE_SIZE] = n;
s_tail++;
}
void notif_process(float dt) {
if (s_displaying) {
s_display_timer -= dt;
if (s_display_timer <= 0) s_displaying = false;
return;
}
if (s_head < s_tail) {
notification_t n = s_queue[s_head % NOTIF_QUEUE_SIZE];
s_head++;
display_notification(n.name, n.icon_url); // User-implemented
s_displaying = true;
s_display_timer = 3.0f;
}
}
class AchievementNotificationQueue {
public:
struct Notification {
std::string type; // "unlock", "lock", "progress"
std::string key;
std::string name;
std::string iconUrl;
std::string userId;
std::chrono::steady_clock::time_point queuedAt;
};
void enqueue(const Notification& notification) {
std::lock_guard<std::mutex> lock(m_mutex);
m_queue.push(notification);
}
void processQueue() {
std::lock_guard<std::mutex> lock(m_mutex);
auto now = std::chrono::steady_clock::now();
// Process one notification at a time with delay
if (!m_queue.empty() && !m_isDisplaying) {
auto notification = m_queue.front();
m_queue.pop();
displayNotification(notification);
m_isDisplaying = true;
m_displayEndTime = now + std::chrono::seconds(3);
}
// Clear display flag after timeout
if (m_isDisplaying && now >= m_displayEndTime) {
m_isDisplaying = false;
}
}
private:
void displayNotification(const Notification& n);
std::queue<Notification> m_queue;
std::mutex m_mutex;
bool m_isDisplaying = false;
std::chrono::steady_clock::time_point m_displayEndTime;
};
import queue
import threading
import time
class AchievementNotificationQueue:
def __init__(self):
self.queue = queue.Queue()
self.is_displaying = False
self.display_duration = 3.0 # seconds
def enqueue(self, notification):
self.queue.put(notification)
def process_queue(self):
if self.queue.empty() or self.is_displaying:
return
notification = self.queue.get()
self.display_notification(notification)
def display_notification(self, notification):
self.is_displaying = True
# Show the notification
show_notification_ui(notification)
# Schedule clearing after duration
def clear():
time.sleep(self.display_duration)
self.is_displaying = False
threading.Thread(target=clear, daemon=True).start()
Best Practices¶
1. Don't Block the Callback¶
Event callbacks should return quickly. Queue heavy operations:
void onEvent(const scorbit::Event& event) {
// Quick: queue for later processing
Notification n;
n.type = "unlock";
// ... fill notification data ...
notificationQueue.enqueue(n);
// Don't do this: blocks the event loop
// loadImage(iconUrl); // Network call
// playAnimation(); // Long animation
}
2. Handle Reconnection¶
After reconnection, you may receive events for achievements unlocked while disconnected. Deduplicate:
std::set<std::string> shownNotifications;
void handleUnlock(const std::string& key, const std::string& userId) {
std::string notifId = key + "_" + userId;
if (shownNotifications.count(notifId)) {
return; // Already shown
}
shownNotifications.insert(notifId);
showNotification();
}
3. Respect Game State¶
Don't show notifications during critical gameplay moments:
std::vector<Notification> deferredNotifications;
void handleAchievementUnlocked(const scorbit::Event& event) {
Notification n = extractNotification(event);
if (isInCriticalGameplay()) {
// Queue for later
deferredNotifications.push_back(n);
} else {
showNotificationImmediately(n);
}
}
void onBallDrain() {
// Good time to show deferred notifications
for (const auto& n : deferredNotifications) {
showNotificationImmediately(n);
}
deferredNotifications.clear();
}
4. Provide Audio Feedback¶
Different sounds for different events enhance the experience:
void handleEvent(const scorbit::Event& event) {
if (event.type() == scorbit::EventType::AchievementUnlocked) {
std::string key, name, userId, username, iconUrl;
bool isTrophy = false;
if (event.getAchievementUnlocked(key, name, userId, username, iconUrl, isTrophy)) {
playSound(isTrophy ? "trophy_unlock" : "achievement_unlock");
}
} else if (event.type() == scorbit::EventType::AchievementLocked) {
playSound("trophy_lost");
} else if (event.type() == scorbit::EventType::AchievementProgress) {
playSound("progress_tick");
}
}