Grasscutter成就系统开发:条件与奖励设计

Grasscutter成就系统开发:条件与奖励设计

【免费下载链接】Grasscutter A server software reimplementation for a certain anime game. 【免费下载链接】Grasscutter 项目地址: https://gitcode.com/GitHub_Trending/gr/Grasscutter

引言:成就系统的核心价值与开发挑战

你是否在开发游戏服务器时遇到过成就系统设计难题?如何平衡成就获取难度与玩家成就感?如何设计灵活的条件判定机制?本文将深入剖析Grasscutter开源项目的成就系统架构,从数据结构设计到奖励分发逻辑,提供一套完整的实现方案。读完本文,你将掌握:

  • 成就系统的模块化设计模式
  • 多维度条件判定的实现方法
  • 动态进度跟踪与状态管理
  • 奖励分发的事务性处理
  • 命令式调试工具的开发技巧

一、成就系统核心数据结构设计

1.1 AchievementData核心模型

Grasscutter采用Excel配置驱动的设计模式,AchievementData类作为成就系统的基础数据模型,定义了成就的静态属性:

@Getter
@ResourceType(name = "AchievementExcelConfigData.json")
public class AchievementData extends GameResource {
    private int goalId;                  // 目标ID
    private int preStageAchievementId;   // 前置成就ID
    private Set<Integer> groupAchievementIdList = new HashSet<>(); // 成就组ID列表
    private boolean isParent;            // 是否为组内父成就
    private long titleTextMapHash;       // 标题文本哈希
    private long descTextMapHash;        // 描述文本哈希
    private int finishRewardId;          // 完成奖励ID
    private boolean isDeleteWatcherAfterFinish; // 完成后是否删除监听器
    private int id;                      // 成就ID
    private BattlePassMissionData.TriggerConfig triggerConfig; // 触发配置
    private int progress;                // 进度值
    private boolean isDisuse;            // 是否废弃
}

1.2 成就组关系设计

成就系统支持线性序列和组关系两种结构,通过divideIntoGroups()方法构建成就间的关联:

public static void divideIntoGroups() {
    if (isDivided.get()) return;
    isDivided.set(true);
    
    var map = GameData.getAchievementDataMap();
    var achievementDataList = map.values().stream()
        .filter(AchievementData::isUsed).toList();
    
    for (var data : achievementDataList) {
        if (!data.hasPreStageAchievement() || data.hasGroupAchievements()) continue;
        
        List<Integer> ids = Lists.newArrayList();
        int parentId = data.getId();
        // 查找后续成就构建序列
        while (true) {
            var next = map.get(parentId + 1);
            if (next == null || parentId != next.getPreStageAchievementId()) break;
            parentId++;
        }
        map.get(parentId).isParent = true;
        
        // 反向收集所有关联成就ID
        while (true) {
            ids.add(parentId);
            var previous = map.get(--parentId);
            if (previous == null) break;
            else if (!previous.hasPreStageAchievement()) {
                ids.add(parentId);
                break;
            }
        }
        
        // 建立组内关联
        for (int i : ids) {
            map.get(i).groupAchievementIdList.addAll(ids);
        }
    }
}

1.3 数据关系可视化

mermaid

二、成就条件判定机制

2.1 触发配置系统

Grasscutter成就系统采用基于TriggerConfig的事件驱动模型,定义了成就的激活条件和进度跟踪方式:

public class TriggerConfig {
    private String triggerName;       // 触发事件名称
    private List<String> paramList;   // 参数列表
    private int progress;             // 目标进度值
}

常见的触发类型包括:

  • KILL_MONSTER - 击杀特定类型怪物
  • COLLECT_ITEM - 收集指定物品
  • COMPLETE_QUEST - 完成特定任务
  • LEVEL_UP - 角色等级提升
  • WORLD_EXPLORE - 世界探索进度

2.2 多维度进度跟踪

成就进度管理通过Achievements类实现,支持三种核心操作:授予(grant)、撤销(revoke)和进度更新(progress):

public class Achievements {
    // 授予成就
    public AchievementControlReturns grant(int achievementId) {
        AchievementData data = GameData.getAchievementDataMap().get(achievementId);
        if (data == null || !data.isUsed()) {
            return AchievementControlReturns.builder()
                .ret(AchievementControlReturns.Return.ACHIEVEMENT_NOT_FOUND).build();
        }
        
        // 检查前置条件
        if (data.hasPreStageAchievement() && !isAchievementCompleted(data.getPreStageAchievementId())) {
            return AchievementControlReturns.builder()
                .ret(AchievementControlReturns.Return.PRECONDITION_NOT_MET).build();
        }
        
        // 检查是否已完成
        if (isAchievementCompleted(achievementId)) {
            return AchievementControlReturns.builder()
                .ret(AchievementControlReturns.Return.ALREADY_ACHIEVED).build();
        }
        
        // 执行授予逻辑
        this.completeAchievement(achievementId);
        this.sendAchievementUpdate();
        
        return AchievementControlReturns.builder()
            .ret(AchievementControlReturns.Return.SUCCESS)
            .changedAchievementStatusNum(1).build();
    }
    
    // 更新进度
    public AchievementControlReturns progress(int achievementId, int progress) {
        // 实现进度更新逻辑
    }
    
    // 撤销成就
    public AchievementControlReturns revoke(int achievementId) {
        // 实现撤销逻辑
    }
}

2.3 条件判定流程

mermaid

2.4 成就组联动机制

当组内任一成就状态变化时,系统会自动检查并更新关联成就:

private void updateGroupAchievements(int achievementId) {
    AchievementData data = GameData.getAchievementDataMap().get(achievementId);
    if (!data.hasGroupAchievements()) return;
    
    int completedCount = 0;
    for (int groupId : data.getGroupAchievementIdList()) {
        if (isAchievementCompleted(groupId)) {
            completedCount++;
        }
    }
    
    // 如果组内所有成就都已完成,检查是否有隐藏成就
    if (completedCount == data.getGroupAchievementIdList().size()) {
        int hiddenAchievementId = data.getId() + 1000; // 示例逻辑
        AchievementData hiddenData = GameData.getAchievementDataMap().get(hiddenAchievementId);
        if (hiddenData != null && !isAchievementCompleted(hiddenAchievementId)) {
            grant(hiddenAchievementId);
        }
    }
}

三、奖励分发系统设计

3.1 奖励类型与数据结构

Grasscutter支持多样化的成就奖励类型,通过finishRewardId关联到奖励配置表:

public class RewardData {
    private int id;                 // 奖励ID
    private List<ItemParamData> rewardItemList; // 物品奖励列表
    private int mora;               // 摩拉数量
    private int primogem;           // 原石数量
    private int experience;         // 经验值
    private int talentPoint;        // 天赋点
}

3.2 事务性奖励分发

为确保奖励发放的原子性,系统采用事务式处理:

private void distributeReward(int achievementId) {
    AchievementData data = GameData.getAchievementDataMap().get(achievementId);
    if (data.getFinishRewardId() <= 0) return;
    
    RewardData reward = GameData.getRewardDataMap().get(data.getFinishRewardId());
    if (reward == null) return;
    
    // 开始事务
    try {
        player.getInventory().getLock().lock();
        
        // 发放物品
        for (ItemParamData item : reward.getRewardItemList()) {
            player.getInventory().addItem(item.getItemId(), item.getCount());
        }
        
        // 发放货币
        if (reward.getMora() > 0) {
            player.getInventory().addMora(reward.getMora());
        }
        
        // 发放原石
        if (reward.getPrimogem() > 0) {
            player.addPrimogem(reward.getPrimogem());
        }
        
        // 发送奖励通知
        player.sendPacket(new PacketItemAddHintNotify(
            reward.getRewardItemList(), reward.getMora(), reward.getPrimogem()
        ));
        
    } finally {
        player.getInventory().getLock().unlock();
    }
}

3.3 奖励配置示例

成就ID触发条件奖励内容难度系数
1001击杀10只史莱姆摩拉×1000, 新手经验书×2★☆☆☆☆
1002击杀50只丘丘人摩拉×5000, 原石×10★★☆☆☆
1003击杀100只深渊法师摩拉×20000, 原石×40, 紫色经验书×5★★★★☆
2001开启10个宝箱摩拉×2000, 冒险经验×50★☆☆☆☆
2002开启100个宝箱摩拉×10000, 原石×20★★★☆☆
2003开启500个宝箱摩拉×50000, 原石×100, 相遇之缘×1★★★★★
3001完成第一章主线任务摩拉×30000, 原石×60★★☆☆☆
3002完成所有主线任务摩拉×200000, 原石×500, 纠缠之缘×5★★★★★

四、开发实践:构建自定义成就

4.1 步骤1:定义成就配置

AchievementExcelConfigData.json中添加新成就:

{
  "id": 90001,
  "goalId": 90001,
  "preStageAchievementId": 0,
  "titleTextMapHash": 123456789,
  "descTextMapHash": 987654321,
  "finishRewardId": 10001,
  "isDeleteWatcherAfterFinish": true,
  "triggerConfig": {
    "triggerName": "CUSTOM_EVENT",
    "paramList": ["BOSS_DEFEAT", "10001"],
    "progress": 5
  },
  "progress": 5,
  "isDisuse": false
}

4.2 步骤2:实现触发逻辑

// 在对应的事件处理器中添加
public class BossBattleHandler {
    private void onBossDefeated(Player player, int bossId) {
        // 触发成就检查
        player.getAchievements().triggerProgress(
            "CUSTOM_EVENT", 
            List.of("BOSS_DEFEAT", String.valueOf(bossId)), 
            1
        );
    }
}

// 在Achievements类中添加触发器
public void triggerProgress(String triggerName, List<String> params, int increment) {
    GameData.getAchievementDataMap().values().stream()
        .filter(data -> !data.isDisuse() && data.getTriggerConfig() != null)
        .filter(data -> data.getTriggerConfig().getTriggerName().equals(triggerName))
        .filter(data -> paramsMatch(data.getTriggerConfig().getParamList(), params))
        .forEach(data -> {
            int currentProgress = getCurrentProgress(data.getId());
            if (currentProgress < data.getProgress()) {
                progress(data.getId(), currentProgress + increment);
            }
        });
}

4.3 步骤3:添加调试命令

利用现有的AchievementCommand框架添加自定义调试命令:

private void grantCustom(Player sender, Player targetPlayer, Achievements achievements, List<String> args) {
    if (args.size() < 2) {
        this.sendUsageMessage(sender);
        return;
    }
    
    parseInt(args.get(0)).ifPresent(achievementId -> {
        parseInt(args.get(1)).ifPresent(count -> {
            var ret = achievements.progress(achievementId, count);
            if (ret.getRet() == AchievementControlReturns.Return.SUCCESS) {
                sendSuccessMessage(sender, "custom_progress", targetPlayer.getNickname(), achievementId, count);
            }
        });
    });
}

4.4 步骤4:测试与验证

使用命令行工具测试成就:

# 授予进度
achievement progress 90001 3

# 授予完成
achievement grant 90001

# 撤销成就
achievement revoke 90001

# 授予全部
achievement grantall

五、性能优化与扩展性设计

5.1 数据缓存策略

public class AchievementDataCache {
    private LoadingCache<Integer, AchievementData> cache;
    
    public AchievementDataCache() {
        this.cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .build(new CacheLoader<Integer, AchievementData>() {
                @Override
                public AchievementData load(Integer id) {
                    return GameData.getAchievementDataMap().get(id);
                }
            });
    }
    
    public AchievementData getAchievement(int id) {
        try {
            return cache.get(id);
        } catch (ExecutionException e) {
            return null;
        }
    }
}

5.2 批量操作优化

AchievementCommand中的批量授予/撤销方法采用流式处理优化性能:

private static void grantAll(Player sender, Player targetPlayer, Achievements achievements) {
    var counter = new AtomicInteger();
    GameData.getAchievementDataMap().values().stream()
        .filter(AchievementData::isUsed)
        .filter(AchievementData::isParent)
        .parallel() // 并行处理
        .forEach(data -> {
            var success = achievements.grant(data.getId());
            if (success.getRet() == AchievementControlReturns.Return.SUCCESS) {
                counter.addAndGet(success.getChangedAchievementStatusNum());
            }
        });
    
    sendSuccessMessage(sender, "grantall", counter.intValue(), targetPlayer.getNickname());
}

5.3 扩展性架构

mermaid

六、常见问题与解决方案

6.1 问题1:成就进度不更新

排查流程

  1. 检查triggerConfig参数是否匹配
  2. 验证事件触发逻辑是否正确调用triggerProgress
  3. 确认成就ID是否在groupAchievementIdList
  4. 检查isDisuse是否设为false

6.2 问题2:奖励发放失败

解决方案

// 添加重试机制
private boolean distributeRewardWithRetry(int achievementId, int retryCount) {
    try {
        distributeReward(achievementId);
        return true;
    } catch (Exception e) {
        if (retryCount > 0) {
            // 短暂延迟后重试
            Thread.sleep(100);
            return distributeRewardWithRetry(achievementId, retryCount - 1);
        }
        // 记录失败日志,后续手动处理
        log.error("Failed to distribute reward for achievement {}", achievementId, e);
        return false;
    }
}

6.3 问题3:性能瓶颈

优化建议

  • 对高频触发的成就使用批处理更新
  • 非关键成就采用异步进度更新
  • 实现成就数据的分区存储
  • 添加成就缓存减少数据库访问

七、总结与展望

Grasscutter成就系统通过灵活的配置驱动设计和事件触发机制,实现了高度可扩展的成就管理框架。核心优势包括:

  1. 数据驱动设计:通过Excel配置文件实现成就定义与代码逻辑分离
  2. 多维度条件系统:支持复杂的触发条件组合和进度跟踪
  3. 事务性奖励分发:确保奖励发放的原子性和一致性
  4. 完善的调试工具:提供命令行接口便于测试和问题排查

未来发展方向:

  • 实现成就共享与排行榜系统
  • 添加成就难度动态调整机制
  • 支持玩家自定义成就目标
  • 引入成就组合奖励机制

通过本文介绍的设计模式和实现方法,开发者可以快速构建功能完善、性能优异的游戏成就系统,为玩家提供丰富的游戏目标和成长体验。

读完本文后,你可以

  • 理解游戏成就系统的核心架构与设计模式
  • 掌握多条件成就的实现方法
  • 学会设计事务性奖励分发系统
  • 能够扩展和定制Grasscutter成就系统

收藏本文,关注项目更新,获取更多游戏服务器开发实践指南!

【免费下载链接】Grasscutter A server software reimplementation for a certain anime game. 【免费下载链接】Grasscutter 项目地址: https://gitcode.com/GitHub_Trending/gr/Grasscutter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值