Game Development 37 min read

Designing a Scalable Game Achievement System with Observer Pattern and Redis

The article presents a complete, event‑driven architecture for a game achievement system that uses the Observer pattern and Redis to achieve high cohesion, low coupling, millisecond‑level performance, and easy extensibility for millions of users and thousands of achievement types.

Cloud Architecture
Cloud Architecture
Cloud Architecture
Designing a Scalable Game Achievement System with Observer Pattern and Redis

Introduction

In modern games, achievement systems are crucial for player retention but often become tightly coupled "hot spots" where changes to combat, quests, or login logic require modifications to achievement code. The goal is to build a highly cohesive, loosely coupled, and easily extensible achievement system.

Core Challenges and Design Goals

Decoupling : Business modules should not need to know the achievement system exists.

Extensibility : Adding new achievement types must not require changes to core code.

Performance : Support high‑concurrency event handling with millisecond response times.

Configurability : Designers can configure achievement rules without code changes.

Overall Architecture

┌──────────────────────────────────────────────────────────────────┐
│                Game Business Layer                               │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐      │
│  │ Combat   │  │ Quest    │  │ Social   │  │ Economy  │      │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘      │
│       │            │            │            │                │
│       └─────────────┴──────┬──────┴─────────────┘                │
│                     │      ▼      │                           │
│                     │ ┌─────────────────┐                     │
│                     │ │   Event Bus      │ (Observer)          │
│                     │ └────────┬────────┘                     │
│                              │                           │
└──────────────────────────────┼─────────────────────────────┘
                               ▼
┌──────────────────────────────────────────────────────────────────┐
│                     Achievement Layer                           │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │               Achievement Listener                      │   │
│  └────────────────────────┬────────────────────────────────┘   │
│                           │                                    │
│                           ▼                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                 Achievement Rule Engine                 │   │
│  │   ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │
│  │   │ Count   │  │ Condition│ │ Composite│ │ Sequence│   │
│  │   └─────────┘  └─────────┘  └─────────┘  └─────────┘   │
│  └────────────────────────┬────────────────────────────────┘   │
│                           │                                    │
│                           ▼                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                     Achievement Data (Redis)            │   │
│  │   ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │
│  │   │ Progress│ │ Status  │ │ Rank    │ │ Stats   │   │
│  │   └─────────┘  └─────────┘  └─────────┘  └─────────┘   │
│  └─────────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────────┘

Core Design Principles

"Achievements are not handed out by the system; they are triggered by you!"

Core ideas:
1. Business modules only "emit events" and do not care who listens.
2. The achievement system only "listens to events" and does not interfere with business logic.
3. Redis provides fast data storage for real‑time progress tracking.

Key Implementations

1. Event Definition

// pkg/event/event.go
package event

import "time"

type EventType string

const (
    // combat related
    EventMonsterKill    EventType = "monster_kill"
    EventPlayerKill     EventType = "player_kill"
    EventDamageDealt    EventType = "damage_dealt"
    EventDamageReceived EventType = "damage_received"
    EventDeath          EventType = "death"
    // quest related
    EventQuestComplete  EventType = "quest_complete"
    EventQuestAccept    EventType = "quest_accept"
    EventQuestAbandon   EventType = "quest_abandon"
    // social related
    EventFriendAdd      EventType = "friend_add"
    EventChatSend       EventType = "chat_send"
    EventGuildJoin      EventType = "guild_join"
    // economy related
    EventItemGet        EventType = "item_get"
    EventItemUse        EventType = "item_use"
    EventGoldEarn       EventType = "gold_earn"
    EventGoldSpend      EventType = "gold_spend"
    // role related
    EventLevelUp        EventType = "level_up"
    EventLogin          EventType = "login"
    EventLogout         EventType = "logout"
    EventPlayTime       EventType = "play_time"
)

type Event struct {
    ID        string                 `json:"id"`
    Type      EventType              `json:"type"`
    PlayerID  string                 `json:"player_id"`
    Timestamp time.Time              `json:"timestamp"`
    Data      map[string]interface{} `json:"data"`
}

func NewEvent(eventType EventType, playerID string, data map[string]interface{}) *Event {
    return &Event{ID: generateEventID(), Type: eventType, PlayerID: playerID, Timestamp: time.Now(), Data: data}
}

func generateEventID() string { return fmt.Sprintf("evt_%d", time.Now().UnixNano()) }

2. Observer Pattern

// pkg/observer/observer.go
package observer

import "game/pkg/event"

type Observer interface {
    OnEvent(evt *event.Event)
    SubscribedEvents() []event.EventType
}

type Subject interface {
    Attach(observer Observer)
    Detach(observer Observer)
    Notify(evt *event.Event)
}

type EventBus struct { observers map[event.EventType][]Observer }

func NewEventBus() *EventBus { return &EventBus{observers: make(map[event.EventType][]Observer)} }

func (eb *EventBus) Attach(observer Observer) {
    for _, et := range observer.SubscribedEvents() { eb.observers[et] = append(eb.observers[et], observer) }
}

func (eb *EventBus) Detach(observer Observer) {
    for et, obs := range eb.observers {
        for i, o := range obs {
            if o == observer { eb.observers[et] = append(obs[:i], obs[i+1:]...); break }
        }
    }
}

func (eb *EventBus) Notify(evt *event.Event) {
    for _, o := range eb.observers[evt.Type] { go o.OnEvent(evt) }
}

func (eb *EventBus) Publish(evt *event.Event) { eb.Notify(evt) }

3. Achievement Listener

// pkg/achievement/listener.go
package achievement

import (
    "game/pkg/event"
    "game/pkg/observer"
    "log"
)

type AchievementListener struct { service *AchievementService }

func NewAchievementListener(s *AchievementService) *AchievementListener { return &AchievementListener{service: s} }

func (l *AchievementListener) SubscribedEvents() []event.EventType {
    return []event.EventType{event.EventMonsterKill, event.EventPlayerKill, event.EventQuestComplete, event.EventFriendAdd, event.EventItemGet, event.EventLevelUp, event.EventLogin, event.EventGoldEarn}
}

func (l *AchievementListener) OnEvent(evt *event.Event) {
    defer func() { if r := recover(); r != nil { log.Printf("achievement event handling failed: %v, event: %+v", r, evt) } }()
    achievements := l.service.GetActiveAchievements(evt.Type)
    for _, ach := range achievements {
        if l.service.IsAchievementCompleted(evt.PlayerID, ach.ID) { continue }
        progress := l.service.UpdateAchievementProgress(evt.PlayerID, ach, evt)
        if l.service.CheckAchievementCondition(ach, progress) { l.service.UnlockAchievement(evt.PlayerID, ach) }
    }
}

var _ observer.Observer = (*AchievementListener)(nil)

4. Core Service Logic

// pkg/achievement/service.go
package achievement

import (
    "context"
    "encoding/json"
    "fmt"
    "game/pkg/event"
    "game/pkg/redis"
    "time"
)

type Achievement struct {
    ID          string         `json:"id"`
    Name        string         `json:"name"`
    Description string         `json:"description"`
    Type        event.EventType `json:"type"`
    Condition   AchievementCondition `json:"condition"`
    Reward      AchievementReward    `json:"reward"`
    Priority    int                  `json:"priority"`
}

type AchievementCondition struct {
    Type      string        `json:"type"`
    Target    int           `json:"target"`
    Field     string        `json:"field"`
    Operator  string        `json:"operator"`
    TimeLimit time.Duration `json:"time_limit"`
}

type AchievementReward struct {
    Title     string `json:"title"`
    Exp       int    `json:"exp"`
    Gold      int    `json:"gold"`
    ItemID    string `json:"item_id"`
    ItemCount int    `json:"item_count"`
}

type AchievementProgress struct {
    Current   int       `json:"current"`
    Target    int       `json:"target"`
    Completed bool      `json:"completed"`
    UpdatedAt time.Time `json:"updated_at"`
}

type AchievementService struct {
    redisClient *redis.Client
    achievements map[string]*Achievement
}

func NewAchievementService(rc *redis.Client) *AchievementService {
    s := &AchievementService{redisClient: rc, achievements: make(map[string]*Achievement)}
    s.loadAchievements()
    return s
}

func (s *AchievementService) loadAchievements() {
    s.achievements = map[string]*Achievement{
        "ach_kill_monster_100": {
            ID: "ach_kill_monster_100", Name: "怪物猎人", Description: "累计击杀 100 只怪物", Type: event.EventMonsterKill,
            Condition: AchievementCondition{Type: "count", Target: 100, Field: "count", Operator: ">="},
            Reward: AchievementReward{Title: "怪物猎人", Exp: 1000, Gold: 500},
        },
        "ach_kill_monster_1000": {
            ID: "ach_kill_monster_1000", Name: "传奇猎手", Description: "累计击杀 1000 只怪物", Type: event.EventMonsterKill,
            Condition: AchievementCondition{Type: "count", Target: 1000, Field: "count", Operator: ">="},
            Reward: AchievementReward{Title: "传奇猎手", Exp: 10000, Gold: 5000},
        },
        "ach_level_50": {
            ID: "ach_level_50", Name: "登峰造极", Description: "角色达到 50 级", Type: event.EventLevelUp,
            Condition: AchievementCondition{Type: "count", Target: 50, Field: "level", Operator: ">="},
            Reward: AchievementReward{Title: "登峰造极", Exp: 5000, Gold: 2000},
        },
        "ach_login_7days": {
            ID: "ach_login_7days", Name: "忠诚勇士", Description: "累计登录 7 天", Type: event.EventLogin,
            Condition: AchievementCondition{Type: "count", Target: 7, Field: "days", Operator: ">="},
            Reward: AchievementReward{Title: "忠诚勇士", Exp: 2000, Gold: 1000},
        },
        "ach_gold_10000": {
            ID: "ach_gold_10000", Name: "富甲一方", Description: "累计获得 10000 金币", Type: event.EventGoldEarn,
            Condition: AchievementCondition{Type: "accumulate", Target: 10000, Field: "amount", Operator: ">="},
            Reward: AchievementReward{Title: "富甲一方", Exp: 3000, Gold: 500},
        },
    }
}

func (s *AchievementService) GetActiveAchievements(eventType event.EventType) []*Achievement {
    var res []*Achievement
    for _, a := range s.achievements {
        if a.Type == eventType { res = append(res, a) }
    }
    return res
}

func (s *AchievementService) UpdateAchievementProgress(playerID string, ach *Achievement, evt *event.Event) *AchievementProgress {
    ctx := context.Background()
    key := s.getProgressKey(playerID, ach.ID)
    data, err := s.redisClient.Get(ctx, key).Bytes()
    var prog *AchievementProgress
    if err != nil || data == nil {
        prog = &AchievementProgress{Target: ach.Condition.Target}
    } else { json.Unmarshal(data, &prog) }
    switch ach.Condition.Type {
    case "count":
        prog.Current++
    case "accumulate":
        if v, ok := evt.Data[ach.Condition.Field]; ok {
            switch val := v.(type) {
            case int: prog.Current += val
            case float64: prog.Current += int(val)
            }
        }
    case "max":
        if v, ok := evt.Data[ach.Condition.Field]; ok {
            switch val := v.(type) {
            case int:
                if val > prog.Current { prog.Current = val }
            case float64:
                if int(val) > prog.Current { prog.Current = int(val) }
            }
        }
    }
    prog.UpdatedAt = time.Now()
    switch ach.Condition.Operator {
    case ">=": prog.Completed = prog.Current >= ach.Condition.Target
    case ">":  prog.Completed = prog.Current > ach.Condition.Target
    case "==": prog.Completed = prog.Current == ach.Condition.Target
    case "<=": prog.Completed = prog.Current <= ach.Condition.Target
    }
    b, _ := json.Marshal(prog)
    s.redisClient.Set(ctx, key, b, 0)
    return prog
}

func (s *AchievementService) CheckAchievementCondition(ach *Achievement, prog *AchievementProgress) bool { return prog.Completed }

func (s *AchievementService) UnlockAchievement(playerID string, ach *Achievement) {
    ctx := context.Background()
    unlockKey := s.getUnlockKey(playerID)
    exists, _ := s.redisClient.SIsMember(ctx, unlockKey, ach.ID).Result()
    if exists { return }
    s.redisClient.SAdd(ctx, unlockKey, ach.ID)
    s.redisClient.Set(ctx, s.getRecordKey(playerID, ach.ID), time.Now().Unix(), 0)
    s.grantReward(playerID, ach.Reward)
    s.publishUnlockEvent(playerID, ach)
    log.Printf("玩家 %s 解锁成就:%s", playerID, ach.Name)
}

func (s *AchievementService) grantReward(playerID string, r AchievementReward) {
    ctx := context.Background()
    if r.Exp > 0 { s.redisClient.HIncrBy(ctx, s.getPlayerExpKey(playerID), "exp", int64(r.Exp)) }
    if r.Gold > 0 { s.redisClient.HIncrBy(ctx, s.getPlayerGoldKey(playerID), "gold", int64(r.Gold)) }
    if r.ItemID != "" && r.ItemCount > 0 {
        s.redisClient.IncrBy(ctx, s.getPlayerItemKey(playerID, r.ItemID), int64(r.ItemCount))
    }
}

func (s *AchievementService) IsAchievementCompleted(playerID, achievementID string) bool {
    ctx := context.Background()
    unlockKey := s.getUnlockKey(playerID)
    exists, _ := s.redisClient.SIsMember(ctx, unlockKey, achievementID).Result()
    return exists
}

func (s *AchievementService) getProgressKey(p, a string) string { return fmt.Sprintf("achievement:progress:%s:%s", p, a) }
func (s *AchievementService) getUnlockKey(p string) string { return fmt.Sprintf("achievement:unlocked:%s", p) }
func (s *AchievementService) getRecordKey(p, a string) string { return fmt.Sprintf("achievement:record:%s:%s", p, a) }
func (s *AchievementService) getPlayerExpKey(p string) string { return fmt.Sprintf("player:exp:%s", p) }
func (s *AchievementService) getPlayerGoldKey(p string) string { return fmt.Sprintf("player:gold:%s", p) }
func (s *AchievementService) getPlayerItemKey(p, i string) string { return fmt.Sprintf("player:item:%s:%s", p, i) }

func (s *AchievementService) publishUnlockEvent(playerID string, ach *Achievement) {
    // Example: publish to Redis Pub/Sub for notifications, leaderboard updates, etc.
    // event := map[string]interface{}{ "player_id": playerID, "achievement_id": ach.ID, "achievement_name": ach.Name, "timestamp": time.Now() }
    // s.redisClient.Publish("achievement_unlock", event)
}

5. Integration Examples

Combat System

// services/combat/service.go
package combat

import (
    "game/pkg/event"
    "game/pkg/observer"
)

type CombatService struct { eventBus observer.Subject }

func NewCombatService(bus observer.Subject) *CombatService { return &CombatService{eventBus: bus} }

func (s *CombatService) KillMonster(playerID, monsterID, monsterType string) {
    // 1. combat logic (damage, loot, etc.)
    // 2. publish event
    evt := event.NewEvent(event.EventMonsterKill, playerID, map[string]interface{}{ "monster_id": monsterID, "monster_type": monsterType, "count": 1 })
    s.eventBus.Publish(evt)
    // 3. continue other logic (drop, exp, …)
}

func (s *CombatService) DealDamage(playerID, targetID string, damage int, isCritical bool) {
    evt := event.NewEvent(event.EventDamageDealt, playerID, map[string]interface{}{ "target_id": targetID, "damage": damage, "is_critical": isCritical })
    s.eventBus.Publish(evt)
}

Quest System

// services/quest/service.go
package quest

import (
    "game/pkg/event"
    "game/pkg/observer"
)

type QuestService struct { eventBus observer.Subject }

func NewQuestService(bus observer.Subject) *QuestService { return &QuestService{eventBus: bus} }

func (s *QuestService) CompleteQuest(playerID, questID string) error {
    // quest completion logic
    evt := event.NewEvent(event.EventQuestComplete, playerID, map[string]interface{}{ "quest_id": questID })
    s.eventBus.Publish(evt)
    return nil
}

Social System

// services/social/service.go
package social

import (
    "game/pkg/event"
    "game/pkg/observer"
)

type SocialService struct { eventBus observer.Subject }

func NewSocialService(bus observer.Subject) *SocialService { return &SocialService{eventBus: bus} }

func (s *SocialService) AddFriend(playerID, friendID string) error {
    // add‑friend logic
    evt := event.NewEvent(event.EventFriendAdd, playerID, map[string]interface{}{ "friend_id": friendID, "count": 1 })
    s.eventBus.Publish(evt)
    return nil
}

Main Program Assembly

// cmd/server/main.go
package main

import (
    "game/pkg/observer"
    "game/pkg/achievement"
    "game/pkg/redis"
    "game/services/combat"
    "game/services/quest"
    "game/services/social"
    "log"
)

func main() {
    // 1. Initialize Redis
    redisClient := redis.NewClient(redis.Config{ Addr: "localhost:6379", Password: "", DB: 0 })
    // 2. Create event bus
    eventBus := observer.NewEventBus()
    // 3. Create achievement service
    achievementService := achievement.NewAchievementService(redisClient)
    // 4. Register achievement listener
    achievementListener := achievement.NewAchievementListener(achievementService)
    eventBus.Attach(achievementListener)
    // 5. Create business services
    combatService := combat.NewCombatService(eventBus)
    questService := quest.NewQuestService(eventBus)
    socialService := social.NewSocialService(eventBus)
    // 6. Server start (omitted)
    log.Println("游戏服务器启动,成就系统已就绪")
    // Simulate test
    testAchievementSystem(combatService, questService, socialService, "player_001")
}

func testAchievementSystem(combatService *combat.CombatService, questService *quest.QuestService, socialService *social.SocialService, playerID string) {
    // Simulate killing 100 monsters
    for i := 0; i < 100; i++ { combatService.KillMonster(playerID, "monster_001", "goblin") }
    // Simulate completing a quest
    questService.CompleteQuest(playerID, "quest_001")
    // Simulate adding a friend
    socialService.AddFriend(playerID, "player_002")
}

Redis Data‑Structure Design

Key Design Convention

┌─────────────────────────────────────────────────────────────────┐
│                Redis Key 命名规范                           │
├─────────────────────────────────────────────────────────────────┤
│ achievement:progress:{player_id}:{achievement_id}   // progress │
│ achievement:unlocked:{player_id} (Set)           // unlocked │
│ achievement:record:{player_id}:{achievement_id}  // unlock time │
│ player:exp:{player_id} (Hash)                    // exp │
│ player:gold:{player_id} (Hash)                   // gold │
│ player:item:{player_id}:{item_id} (String)       // item count │
│ achievement:rank:{achievement_id} (ZSet)        // leaderboard │
│ achievement:stats:{date} (Hash)                 // global stats │
└─────────────────────────────────────────────────────────────────┘

Data Structure Selection

// pkg/redis/design.go
package redis

/*
Redis 数据结构选型说明:
1. String – simple counters, timestamps (e.g., player:item:{player_id}:{item_id})
2. Hash – object attributes (player:exp:{player_id}, player:gold:{player_id})
3. Set – unique collections (achievement:unlocked:{player_id})
4. Sorted Set – leaderboards (achievement:rank:{achievement_id})
5. List – event queues or history (achievement:history:{player_id})
6. Pub/Sub – real‑time notifications (achievement_unlock channel)
*/

Leaderboard Implementation

// pkg/achievement/leaderboard.go
package achievement

import (
    "context"
    "fmt"
)

type LeaderboardService struct { redisClient *redis.Client }

func NewLeaderboardService(rc *redis.Client) *LeaderboardService { return &LeaderboardService{redisClient: rc} }

func (s *LeaderboardService) UpdateRank(ctx context.Context, achievementID, playerID string, score int64) {
    key := fmt.Sprintf("achievement:rank:%s", achievementID)
    s.redisClient.ZAdd(ctx, key, redis.Z{Score: float64(score), Member: playerID})
}

func (s *LeaderboardService) GetRank(ctx context.Context, achievementID, playerID string) int64 {
    key := fmt.Sprintf("achievement:rank:%s", achievementID)
    rank, _ := s.redisClient.ZRevRank(ctx, key, playerID).Result()
    return rank + 1 // Redis ranks start at 0
}

type RankPlayer struct {
    Rank     int    `json:"rank"`
    PlayerID string `json:"player_id"`
    Score    int64  `json:"score"`
}

func (s *LeaderboardService) GetTopPlayers(ctx context.Context, achievementID string, n int) []*RankPlayer {
    key := fmt.Sprintf("achievement:rank:%s", achievementID)
    results, _ := s.redisClient.ZRevRangeWithScores(ctx, key, 0, int64(n-1)).Result()
    var players []*RankPlayer
    for i, r := range results {
        players = append(players, &RankPlayer{Rank: i + 1, PlayerID: r.Member.(string), Score: int64(r.Score)})
    }
    return players
}

Advanced Features

Composite Achievements (AND/OR)

// pkg/achievement/composite.go
package achievement

type CompositeCondition struct {
    Type       string                `json:"type"`
    Conditions []AchievementCondition `json:"conditions"`
}

func CheckCompositeCondition(playerID string, comp CompositeCondition, svc *AchievementService) bool {
    switch comp.Type {
    case "AND":
        for _, c := range comp.Conditions { if !checkSingleCondition(playerID, c, svc) { return false } }
        return true
    case "OR":
        for _, c := range comp.Conditions { if checkSingleCondition(playerID, c, svc) { return true } }
        return false
    default:
        return false
    }
}

func checkSingleCondition(playerID string, c AchievementCondition, svc *AchievementService) bool { return true }

var legendaryAdventurer = &Achievement{ID: "ach_legendary_adventurer", Name: "传奇冒险家", Description: "击杀 1000 只怪物,完成 50 个任务,等级达到 50 级", Type: event.EventMonsterKill, Condition: AchievementCondition{Type: "composite", Composite: CompositeCondition{Type: "AND", Conditions: []AchievementCondition{{Type: "count", Target: 1000, Field: "count", Operator: ">="}, {Type: "count", Target: 50, Field: "count", Operator: ">="}, {Type: "max", Target: 50, Field: "level", Operator: ">="}}}}}

Limited‑Time Achievements

// pkg/achievement/limited.go
package achievement

import "time"

type LimitedAchievement struct {
    Achievement
    StartTime time.Time `json:"start_time"`
    EndTime   time.Time `json:"end_time"`
}

func (s *AchievementService) CheckTimeLimit(playerID string, a *LimitedAchievement) bool {
    now := time.Now()
    if now.Before(a.StartTime) || now.After(a.EndTime) { return false }
    firstLoginKey := fmt.Sprintf("player:first_login:%s", playerID)
    firstLogin, err := s.redisClient.Get(context.Background(), firstLoginKey).Time()
    if err != nil || firstLogin.Before(a.StartTime) { return false }
    return true
}

var springFestivalAchievement = &LimitedAchievement{Achievement: Achievement{ID: "ach_spring_festival", Name: "春节庆典", Description: "春节期间登录游戏 7 天", Type: event.EventLogin, Condition: AchievementCondition{Type: "count", Target: 7, Field: "days", Operator: ">="}}, StartTime: time.Date(2026,1,28,0,0,0,0,time.Local), EndTime: time.Date(2026,2,11,23,59,59,0,time.Local)}

Hidden Achievements

// pkg/achievement/hidden.go
package achievement

type HiddenAchievement struct {
    Achievement
    RevealCondition string `json:"reveal_condition"`
}

func (s *AchievementService) ShouldReveal(playerID string, a *HiddenAchievement) bool {
    prog := s.GetAchievementProgress(playerID, a.ID)
    return float64(prog.Current)/float64(prog.Target) >= 0.5
}

var mysteriousKiller = &HiddenAchievement{Achievement: Achievement{ID: "ach_mysterious_killer", Name: "???", Description: "???", Type: event.EventPlayerKill, Condition: AchievementCondition{Type: "count", Target: 100, Field: "count", Operator: ">="}}, RevealCondition: "progress >= 50%"}

Achievement Chains (Prerequisite)

// pkg/achievement/chain.go
package achievement

type AchievementChain struct {
    ID            string   `json:"id"`
    Name          string   `json:"name"`
    AchievementIDs []string `json:"achievement_ids"`
}

func (s *AchievementService) CanUnlock(playerID, achID string) bool {
    chain := s.findAchievementChain(achID)
    if chain == nil { return true }
    idx := s.findAchievementIndex(chain, achID)
    if idx <= 0 { return true }
    prev := chain.AchievementIDs[idx-1]
    return s.IsAchievementCompleted(playerID, prev)
}

func (s *AchievementService) findAchievementChain(id string) *AchievementChain { return nil }
func (s *AchievementService) findAchievementIndex(chain *AchievementChain, id string) int { for i, v := range chain.AchievementIDs { if v == id { return i } } return -1 }

var hunterChain = &AchievementChain{ID: "chain_hunter", Name: "猎人之路", AchievementIDs: []string{"ach_kill_monster_100", "ach_kill_monster_500", "ach_kill_monster_1000", "ach_kill_monster_5000"}}

Performance Optimizations

Batch Updates (Redis Pipeline)

// pkg/achievement/batch.go
package achievement

import (
    "context"
    "github.com/go-redis/redis/v8"
)

func (s *AchievementService) BatchUpdate(ctx context.Context, playerID string, events []*event.Event) error {
    pipe := s.redisClient.Pipeline()
    for _, evt := range events {
        achs := s.GetActiveAchievements(evt.Type)
        for _, ach := range achs {
            prog := s.UpdateAchievementProgress(playerID, ach, evt)
            if prog.Completed && !s.IsAchievementCompleted(playerID, ach.ID) { s.UnlockAchievement(playerID, ach) }
        }
    }
    _, err := pipe.Exec(ctx)
    return err
}

Asynchronous Processing

// pkg/achievement/async.go
package achievement

import (
    "context"
    "game/pkg/event"
    "sync"
)

type AsyncAchievementService struct {
    *AchievementService
    eventQueue chan *event.Event
    workerNum  int
    wg         sync.WaitGroup
}

func NewAsyncAchievementService(base *AchievementService, workers, queueSize int) *AsyncAchievementService {
    s := &AsyncAchievementService{AchievementService: base, eventQueue: make(chan *event.Event, queueSize), workerNum: workers}
    s.startWorkers()
    return s
}

func (s *AsyncAchievementService) startWorkers() {
    for i := 0; i < s.workerNum; i++ { s.wg.Add(1); go s.worker() }
}

func (s *AsyncAchievementService) worker() {
    defer s.wg.Done()
    for evt := range s.eventQueue { s.processEvent(evt) }
}

func (s *AsyncAchievementService) processEvent(evt *event.Event) {
    achs := s.GetActiveAchievements(evt.Type)
    for _, ach := range achs {
        if s.IsAchievementCompleted(evt.PlayerID, ach.ID) { continue }
        prog := s.UpdateAchievementProgress(evt.PlayerID, ach, evt)
        if prog.Completed { s.UnlockAchievement(evt.PlayerID, ach) }
    }
}

func (s *AsyncAchievementService) Publish(evt *event.Event) {
    select { case s.eventQueue <- evt: default: s.processEvent(evt) }
}

func (s *AsyncAchievementService) Shutdown() { close(s.eventQueue); s.wg.Wait() }

Caching Layer

// pkg/achievement/cache.go
package achievement

import (
    "context"
    "time"
)

type CacheConfig struct {
    AchievementTTL time.Duration
    ProgressTTL    time.Duration
    MaxSize        int
}

type CachedAchievementService struct {
    *AchievementService
    achievementCache map[string]*Achievement
    progressCache    map[string]*AchievementProgress
    config           CacheConfig
}

func (s *CachedAchievementService) GetAchievementWithCache(id string) *Achievement {
    if a, ok := s.achievementCache[id]; ok { return a }
    a := s.loadAchievementFromDB(id)
    if a != nil { s.achievementCache[id] = a }
    return a
}

func (s *CachedAchievementService) GetProgressWithCache(ctx context.Context, playerID, achID string) *AchievementProgress {
    key := playerID + ":" + achID
    if p, ok := s.progressCache[key]; ok { return p }
    p := s.GetAchievementProgress(playerID, achID)
    s.progressCache[key] = p
    return p
}

func (s *CachedAchievementService) startCacheCleaner() {
    ticker := time.NewTicker(time.Minute)
    go func() { for range ticker.C { s.cleanExpiredCache() } }()
}

func (s *CachedAchievementService) cleanExpiredCache() { /* LRU or TTL logic */ }

Monitoring & Alerting

Metrics (Prometheus)

// pkg/achievement/metrics.go
package achievement

import "github.com/prometheus/client_golang/prometheus"

var (
    achievementUnlockTotal = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "achievement_unlock_total", Help: "Total number of achievement unlocks"}, []string{"achievement_id"})
    achievementProgressUpdate = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "achievement_progress_update_total", Help: "Total number of progress updates"}, []string{"achievement_id"})
    achievementProcessDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "achievement_process_duration_seconds", Help: "Processing latency", Buckets: prometheus.DefBuckets}, []string{"event_type"})
    achievementQueueSize = prometheus.NewGauge(prometheus.GaugeOpts{Name: "achievement_queue_size", Help: "Current size of the async queue"})
)

func init() {
    prometheus.MustRegister(achievementUnlockTotal, achievementProgressUpdate, achievementProcessDuration, achievementQueueSize)
}

Structured Logging

// pkg/achievement/logger.go
package achievement

import "go.uber.org/zap"

type AchievementLogger struct { logger *zap.Logger }

func (l *AchievementLogger) LogUnlock(playerID string, a *Achievement) {
    l.logger.Info("achievement_unlocked", zap.String("player_id", playerID), zap.String("achievement_id", a.ID), zap.String("achievement_name", a.Name))
}

func (l *AchievementLogger) LogProgress(playerID string, a *Achievement, cur, target int) {
    l.logger.Debug("achievement_progress", zap.String("player_id", playerID), zap.String("achievement_id", a.ID), zap.Int("progress", cur), zap.Int("target", target))
}

func (l *AchievementLogger) LogError(playerID string, err error) {
    l.logger.Error("achievement_error", zap.String("player_id", playerID), zap.Error(err))
}

Conclusion

The observer‑based, event‑driven architecture combined with Redis delivers a highly decoupled, scalable, and maintainable achievement system suitable for large‑scale games, mobile titles, and any platform that requires real‑time player progression tracking.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GolangRedisscalable architectureobserver patternEvent-drivengame achievement
Cloud Architecture
Written by

Cloud Architecture

Focuses on cloud‑native and distributed architecture engineering, sharing practical solutions and lessons learned. Covers microservice governance, Kubernetes, observability, and stability engineering to help your systems run stable, fast, and cost‑effectively.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.