编码成就系统的最佳方式

Posted

技术标签:

【中文标题】编码成就系统的最佳方式【英文标题】:Best way to code Achievements system 【发布时间】:2011-05-10 17:02:18 【问题描述】:

我正在考虑设计用于我的网站的成就系统的最佳方式。数据库结构可以在Best way to tell 3 or more consecutive records missing 找到,这个线程真的是从开发人员那里获得想法的扩展。

我在这个网站上有很多关于徽章/成就系统的讨论的问题就是 - 都是空谈,没有代码。实际的代码实现示例在哪里?

我在这里提出一个设计,希望人们可以为可扩展成就系统的编码做出贡献,并希望创建一个好的设计。我并不是说这是最好的,远非如此,但它是一个可能的起点。

请随时贡献您的想法。


我的系统设计理念

似乎普遍的共识是创建一个“基于事件的系统”——无论何时发生已知事件,例如创建、删除帖子等,它都会像这样调用事件类..

$event->trigger('POST_CREATED', array('id' => 8));

然后事件类找出哪些标记正在“侦听”该事件,然后它requires 那个文件,并创建该类的一个实例,如下所示:

require '/badges/' . $file;
$badge = new $class;

然后调用默认事件,传递调用trigger时收到的数据;

$badge->default_event($data);

徽章

这就是真正的魔法发生的地方。每个徽章都有自己的查询/逻辑来确定是否应该授予徽章。每个徽章都列在例如这种格式:

class Badge_Name extends Badge

 const _BADGE_500 = 'POST_500';
 const _BADGE_300 = 'POST_300';
 const _BADGE_100 = 'POST_100';

 function get_user_post_count()
 
  $escaped_user_id = mysql_real_escape_string($this->user_id);

  $r = mysql_query("SELECT COUNT(*) FROM posts
                    WHERE userid='$escaped_user_id'");
  if ($row = mysql_fetch_row($r))
  
   return $row[0];
  
  return 0;
 

 function default_event($data)
 
  $post_count = $this->get_user_post_count();
  $this->try_award($post_count);
 

 function try_award($post_count)
 
  if ($post_count > 500)
  
   $this->award(self::_BADGE_500);
  
  else if ($post_count > 300)
  
   $this->award(self::_BADGE_300);
  
  else if ($post_count > 100)
  
   $this->award(self::_BADGE_100);
  

 

award 函数来自扩展类Badge,它基本上检查用户是否已经获得了该徽章,如果没有,将更新徽章数据库表。徽章类还负责检索用户的所有徽章并将其以数组等形式返回(因此徽章可以例如显示在用户个人资料中)

当系统首次在已经上线的网站上实施时会怎样?

还有一个可以添加到每个徽章的“cron”作业查询。这样做的原因是,当徽章系统第一次实施和初始化时,应该已经获得的徽章还没有被授予,因为这是一个基于事件的系统。因此,CRON 作业会根据需要为每个徽章运行,以奖励任何需要的东西。例如,上面的 CRON 作业如下所示:

class Badge_Name_Cron extends Badge_Name


 function cron_job()
 
  $r = mysql_query('SELECT COUNT(*) as post_count, user_id FROM posts');

  while ($obj = mysql_fetch_object($r))
  
   $this->user_id = $obj->user_id; //make sure we're operating on the right user

   $this->try_award($obj->post_count);
  
 


由于上面的cron类扩展了主badge类,它可以重用逻辑函数try_award

我为此创建一个专门的查询的原因是虽然我们可以“模拟”以前的事件,即遍历每个用户帖子并触发像$event->trigger() 这样的事件类,但它会非常慢,尤其是对于许多徽章。所以我们改为创建一个优化的查询。

哪些用户获得了奖励?所有关于根据事件奖励其他用户

Badgeaward 函数作用于user_id——他们将永远获得奖励。默认情况下,徽章被授予导致事件发生的人,即会话用户 id(这对于 default_event 函数是正确的,尽管 CRON 作业显然会遍历所有用户并奖励单独的用户)

让我们举个例子,在编码挑战网站上,用户提交他们的编码条目。管理员然后判断条目,并在完成后将结果发布到挑战页面以供所有人查看。发生这种情况时,会调用 POSTED_RESULTS 事件。

如果您想为发布的所有条目授予用户徽章,比如说,如果他们排名在前 5 位,您应该使用 cron 作业(但请记住,这将为所有用户更新,而不仅仅是针对结果发布的挑战)

如果您想针对更具体的区域使用 cron 作业进行更新,让我们看看是否有办法将过滤参数添加到 cron 作业对象中,并获取 cron_job 函数来使用它们。例如:

class Badge_Top5 extends Badge

   const _BADGE_NAME = 'top5';

   function try_award($position)
   
     if ($position <= 5)
     
       $this->award(self::_BADGE_NAME);
     
   


class Badge_Top5_Cron extends Badge_Top5

   function cron_job($challenge_id = 0)
   
     $where = '';
     if ($challenge_id)
     
       $escaped_challenge_id = mysql_real_escape_string($challenge_id);
       $where = "WHERE challenge_id = '$escaped_challenge_id'";
     

     $r = mysql_query("SELECT position, user_id
                       FROM challenge_entries
                       $where");

    while ($obj = mysql_fetch_object($r))
   
      $this->user_id = $obj->user_id; //award the correct user!
      $this->try_award($obj->position);
   

即使没有提供参数,cron 函数仍然可以工作。

【问题讨论】:

相关(可能重复):***.com/questions/1744747/achievements-badges-system 相关但不重复。请阅读第二段。 “这个网站上有很多关于徽章/成就系统的讨论,我遇到的问题就是——都是空谈,没有代码。实际的代码实现示例在哪里?” 好吧,编写工作代码只是在一定程度上可行。我想说人们只给你理论是很正常的,一旦任何实现都太复杂了。 【参考方案1】:

我曾经在你所谓的面向文档的数据库中实施了奖励系统(这对玩家来说是一团糟)。我的实现中的一些亮点,翻译成 php 和 MySQL:

关于徽章的每个细节都存储在用户数据中。如果您使用 MySQL,我会确保此数据在数据库中每个用户的一条记录中以提高性能。

每当相关人员执行某项操作时,代码都会触发带有给定标志的徽章代码,例如 flag('POST_MESSAGE')。

一个事件也可以触发一个计数器,例如帖子数的计数。增加计数('POST_MESSAGE')。在这里,您可以检查(通过钩子,或仅在此方法中进行测试),如果 POST_MESSAGE 计数 > 300,那么您应该奖励一个徽章,例如:flag("300_POST")。

在 flag 方法中,我会将代码用于奖励徽章。例如,如果发送了 Flag 300_POST,则应该调用徽章 reward_badge("300_POST")。

在标志方法中,您还应该存在用户以前的标志。所以你可以说当用户有 FIRST_COMMENT, FIRST_POST, FIRST_READ 你授予徽章("NEW USER"),当你获得 100_COMMENT, 100_POST, 300_READ 你可以授予徽章("EXPERIENCED_USER")

所有这些标志和徽章都需要以某种方式存储。使用某种方式,将标志视为位。如果您希望真正有效地存储它,您可以将它们视为位并使用下面的代码:(或者如果您不希望这种复杂性,您可以使用裸字符串“000000001111000”。

$achievments = 0;
$bits = sprintf("%032b", $achievements);

/* Set bit 10 */
$bits[10] = 1;

$achievements = bindec($bits);

print "Bits: $bits\n";
print "Achievements: $achievements\n";

/* Reload */

$bits = sprintf("%032b", $achievments);

/* Set bit 5 */
$bits[5] = 1;

$achievements = bindec($bits);

print "Bits: $bits\n";
print "Achievements: $achievements\n";

为用户存储文档的一种好方法是使用 json 并将用户数据存储在单个文本列中。使用 json_encode 和 json_decode 来存储/检索数据。

为了跟踪由其他用户操作的某些用户数据的活动,请在项目上添加数据结构并在那里使用计数器。例如读取计数。使用与上述相同的技术来授予徽章,但更新当然应该进入拥有用户的帖子。 (比如文章阅读1000次徽章)。

【讨论】:

徽章系统的经典趋势是为您的表格中的新统计数据添加一个新字段。对我来说,这似乎是一个简单的出路和坏主意,因为您存储的镜像数据可以从表中已有的数据中计算出来(可能是一个简单的 COUNT(),它在 MyISAM 表上非常快,将是 100%准确的)。如果性能是您的目标,您需要进行更新并选择以获取当前的例如post_count 值以检查是否应授予徽章。您可能只需要一个查询,COUNT(*)。我同意对于更复杂的数据,有充分的理由添加一个字段 @Gary Green 这不仅是一种简单的出路,而且是可扩展的方式,并且与文档数据库兼容。至于正确性,您是对的,但对于徽章系统,我宁愿让它快速且最有可能正确,而不是 100% 正确且缓慢。一次计数可能很快,但是当您的系统扩展并且您拥有大量用户时,该策略并不成立。 我喜欢只有徽章定义表和链接表来将用户链接到徽章及其当前进度的想法。这样做 noSQL 会将您锁定在当时的任何模式中,并且当突然发现徽章中的拼写错误或添加 1000 个新徽章时,它是不可维护的。您总是可以让批处理将这些缓存到更多的文档存储中以便快速检索,但我会留下链接。【参考方案2】:

UserInfuser 是一个开源游戏化平台,它实现了徽章/积分服务。你可以在这里查看它的 API: http://code.google.com/p/userinfuser/wiki/API_Documentation

我实现了它并尝试将函数的数量保持在最低限度。这是一个 php 客户端的 API:

class UserInfuser($account, $api_key)

    public function get_user_data($user_id);
    public function update_user($user_id);
    public function award_badge($badge_id, $user_id);
    public function remove_badge($badge_id, $user_id);
    public function award_points($user_id, $points_awarded);
    public function award_badge_points($badge_id, $user_id, $points_awarded, $points_required);
    public function get_widget($user_id, $widget_type);

最终结果是通过使用小部件以有意义的方式显示数据。这些小部件包括:奖杯盒、排行榜、里程碑、实时通知、排名和积分。

API 的实现可以在这里找到:http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py

【讨论】:

这是基于 PHP 的吗?该问题基于PHP 它有 PHP 绑定,但服务器端代码是用 Python 编写的。【参考方案3】:

成就可能是繁重的,如果您以后必须添加它们,则更是如此,除非您有一个格式良好的 Event 类。

这涉及到我实现成就的技巧。

我喜欢先将它们分成“类别”,然后在这些类别中划分成就等级。即游戏中的kills 类别可能会在首杀、10 杀、100 万杀等方面获得 1 奖。

然后是任何优秀应用程序的核心,即处理您的事件的类。再次想象一场杀戮游戏;当玩家杀死某些东西时,事情就会发生。杀戮被记录下来,等等,最好在一个集中的位置处理,比如 Events 类,它可以将信息发送到其他相关的地方。

它完全到位,以适当的方法实例化您的Achievements 类并检查播放器是否到期。

构建Achievements 类是微不足道的,只需检查数据库以查看玩家的击杀数是否达到下一个成就所需的数量。

我喜欢使用 Redis 将用户的成就存储在 BitField 中,但在 MySQL 中也可以使用相同的技术。也就是说,您可以将玩家的成就存储为int,然后将and 与您定义为该成就的位存储在一起,以查看他们是否已经获得它。这样,它只使用数据库中的一个 int 列。

这样做的缺点是您必须将它们组织好,并且您可能需要在代码中创建一些 cmets,以便稍后记住 2^14 对应的内容。如果您的成就在他们自己的表中枚举,那么您只需执行 2^pk 其中pk 是成就表的主键。这使得检查类似于

if(((2**$pk) & ($usersAchInt)) > 0)
  // fire off the giveAchievement() event 
 

这样你以后可以添加成就,它会很好地吻合,只是永远不要改变已经授予的成就的主键。

【讨论】:

以上是关于编码成就系统的最佳方式的主要内容,如果未能解决你的问题,请参考以下文章

PHP/IOS:为 Web 服务编码 json 的最佳方式是啥?

邮政编码/城市多对多桥接表的最佳设计

智能合约最佳实践 之 Solidity 编码规范

智能合约最佳实践 之 Solidity 编码规范

IOS 最佳 Web 视频编码实践 (FFMpeg)

Python中的幽灵—编码方式