如何建立处理成绩的灵活框架?


54

具体地说,实现足够灵活的成就系统的最佳方法是什么,以处理超越简单的统计驱动的成就,例如“杀死x敌人”。

我正在寻找比基于统计的系统更健壮的东西,以及比“将它们全部硬编码为条件”更井井有条的东西。在基于统计的系统中一些不可能或难以处理的示例:“在草莓之后切西瓜”,“在立于不败之地下水管”等。

Answers:


39

我认为一种可靠的解决方案是采用面向对象的方式。

根据您要支持的成就类型,您需要一种查询游戏当前状态和/或游戏对象(例如玩家)所做的动作/事件的历史的方法。

假设您有一个基本的成就类,例如:

class AbstractAchievement
{
    GameState& gameState;
    virtual bool IsEarned() = 0;
    virtual string GetName() = 0;
};

AbstractAchievement持有游戏状态的参考。它用于查询正在发生的事情。

然后,您进行具体的实现。让我们使用您的示例:

class MasterSlicerAchievement : public AbstractAchievement
{
    string GetName() { return "Master Slicer"; }
    bool IsEarned()
    {
        Action lastAction = gameState.GetPlayerActionHistory().GetAction(0);
        Action previousAction = gameState.GetPlayerActionHistory().GetAction(1);
        if (lastAction.GetType() == ActionType::Slice &&
            previousAction.GetType() == ActionType::Slice &&
            lastAction.GetObjectType() == ObjectType::Watermelon &&
            previousAction.GetObjectType() == ObjectType::Strawberry)
            return true;
        return false;
    }
};
class InvinciblePipeRiderAchievement : public AbstractAchievement
{
    string GetName() { return "Invincible Pipe Rider"; }
    bool IsEarned()
    {
        if (gameState.GetLocationType(gameState.GetPlayerPosition()) == LocationType::OVER_PIPE &&
            gameState.GetPlayerState() == EntityState::INVINCIBLE)
            return true;
        return false;
    }
};

然后由您决定何时使用该IsEarned()方法进行检查。您可以在每次游戏更新时进行检查。

例如,一种更有效的方法是拥有某种事件管理器。然后将事件(例如PlayerHasSlicedSomethingEventPlayerGotInvicibleEvent或简单地PlayerStateChanged)注册到将取得成就作为参数的方法。例:

class Game
{
    void Initialize()
    {
        eventManager.RegisterAchievementCheckByActionType(ActionType::Slice, masterSlicerAchievement);
        // Each time an action of type Slice happens,
        // the CheckAchievement() method is invoked with masterSlicerAchievement as parameter.
        eventManager.RegisterAchievementCheckByPlayerState(EntityState::INVINCIBLE, invinciblePiperAchievement);
        // Each time the player gets the INVINCIBLE state,
        // the CheckAchievement() method is invoked with invinciblePipeRiderAchievement as parameter.
    }
    void CheckAchievement(const AbstractAchievement& achievement)
    {
        if (!HasAchievement(player, achievement) && achievement.IsEarned())
        {
            AddAchievement(player, achievement);
        }
    }
};

13
样式nitpick: if(...) return true; else return false;return (...)
BlueRaja-Danny Pflughoeft

2
这是实现成就系统的很好模板。而且,它仍然表明您必须拥有一种跟踪游戏状态的方法。我发现这可能是最复杂的想法。
布莱恩·哈灵顿

@Spio你是个高手...!:D简单优雅的解决方案。恭喜。
2013年

+1我认为事件发出/收集系统是解决此问题的好方法。
ashes999 2014年

14

好吧,简而言之,当满足特定条件时,成就就会解锁。因此,您需要能够产生if语句来检查所需的条件。

例如,如果您想知道某个级别已完成或老板被击败,则需要在这些事件发生时使布尔标志变为true。

然后:

if(GlobalFlags.MasterBossDefeated == true && AchievementClass.MasterBossDefeatedAchievement == false)
{
    AchievementClass.MasterBossDefeatedAchievement = true;
    showModalPopUp("You defeated the Master Boss!  30 gamerscore");
}

您可以根据需要将其设置为复杂或简单,以匹配所需的条件。

有关Xbox 360成就的一些信息可以在这里找到。


2
+1很棒的文章,基本上就是我要建议的内容。尽管我会让Modal从成就本身获取文本……但是如果您想更改某些内容,只是为了避免文本搜寻。
杰西·多西

@Noctrine-不要忘记此处发布的任何代码都应视为伪代码-通常有必要使用简化代码来理解这一点。
克里斯·弗里德(ChrisF)2010年

1
Xbox 360成就链接已死。
hangy 2011年


8

如果玩家采取的每一项操作都会向该消息发布消息AchievementManager怎么办?然后,经理可以在内部检查是否满足某些条件。第一个对象发布消息:

AchievementManager::PostMessage("Jump", "162");

AchievementManager::PostMessage("Slice", "Strawberry");
AchievementManager::PostMessage("Slice", "Watermelon");

AchievementManager::PostMessage("Kill", "Goomba");

然后AchievementManager检查是否需要做任何事情:

if (!strcmp(m_Message.name, "Slice") && !strcmp(m_LastMessage.name, "Slice"))
{
    if (!strcmp(m_Message.value, "Watermelon") && !strcmp(m_LastMessage.value, "Strawberry"))
    {
        // achievement unlocked!
    }
}

您可能会希望使用枚举而不是字符串来实现。;)


这符合我的想法,但仍然依赖于在一个函数中进行硬编码的一堆东西。除了可维护性,每次都要至少通过功能检查每个成就的资格。
lti

2
可以?您可以将条件语句放入外部脚本中,只要您有某种方式可以跟踪游戏中发生的情况即可。
knight666

6
-1设计不良的脸颊。如果您要直接调用AchivementManager,只需将每个“消息”设置为一个单独的函数即可。如果您要使用消息,请创建一个消息管理器,以便其他类也可以使用该消息(我确信Goomba会对知道他被杀害感兴趣),并AchievementManager在每个类(OP最初要求如何避免)。并为消息使用枚举或单独的类,而不是字符串文字-使用字符串文字传递状态始终是一个坏主意。
BlueRaja-Danny Pflughoeft

4

我使用的最后一个设计是基于一组每个用户的持久计数器,然后使成就从某个计数器达到某个值成为关键。大多数是单个成就/计数器对,其中计数器只会是0或1(并且成就在> = 1时触发),但是您也可以将其用于“杀死X个帅哥”或“找到X个箱子”。这也意味着您可以为没有成就的事物设置计数器,并且仍将对其进行跟踪以备将来使用。


3

当我在上一个游戏中实现成就时,我将其全部基于统计。当我们的统计数据达到一定值时,成就便会解锁。考虑一下《现代战争2》:游戏正在追踪大量统计数据!您使用SCAR-H拍摄了几张照片?使用轻量级振作时,您冲刺了多少英里?

因此,在实现过程中,我只创建了一个统计引擎,然后创建了一个成就管理器,该管理器运行非常简单的查询来检查整个游戏过程中成就的状态。

虽然我的实现非常简单,但是可以完成工作。我写了有关它,并在这里分享了我的疑问。


2

使用事件演算。然后制定一些先决条件和满足先决条件后要执行的操作:

  • 前提条件:您杀死了1000名敌人,您有2条腿
  • 行动:给我棒棒糖,给我super-duper-shotgun-13
  • 首次动作:说“你真棒!”

像这样使用(未针对速度进行优化!):

  • 存储所有统计信息。
  • 查询统计信息以获取前提条件。
  • 应用动作。
  • 一次应用一次动作。

如果您想使其更快:

  • 缓存所需的任何内容,将其部分存储在一些树,哈希表中...
  • 进行增量更改,因此您不必一直应用所有操作,但是这些操作是新的...)

注意

由于万事都有利弊,因此很难给出最佳建议。

  • “什么是最好的数据结构?” 意味着“您最想对它执行什么操作?搜索,删除,添加...”
  • 您基本上会考虑以下属性:易于编码,速度,清晰度,大小...

0

成就事件发生后,IF检查有什么问题?

if (Cinimatics.done)
   Achievement.get(CINIMATICS_SEEN);

if (EnemiesKiled > 200)
   Achievement.get(KILLER);

if (Damage > 2000 && TimeSinceFirstDamage < 2000)
   Achievement.get(MEAT_SHIELD);

InvitationAccepted = Invite.send(BestFriend);
if (InvitationAccepted)
   Achievement.get(A_FRIEND_IN_NEED);

3
“我正在寻找一种比“将所有条件都硬编码为条件”更具组织性和可维护性的东西。'。尽管这绝对是小型游戏的一种很好的KISS方法。
共产主义鸭子
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.