成就系统的最佳编码方法

我正在考虑设计一个在我的网站上使用的成就系统的最佳方法。数据库结构可以在 最好的方法来告诉3个或更多的连续记录丢失中找到,这个线程实际上是一个扩展,可以从开发人员那里获得想法。

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

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

请随意贡献你的想法。


我的系统设计理念

It seems the general consensus is to create an "event based system" -- whenever a known event occurs like a post is created, deleted, etc it calls the event class like so..

$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,它主要检查用户是否已经获得该徽章,如果没有,将更新徽章 db 表。徽章类还负责检索用户的所有徽章并以数组的形式返回(这样徽章就可以显示在用户配置文件中)

当系统首次在一个已经存在的站点上实现时会发生什么情况?

还有一个“ 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 类扩展了主徽章类,所以它可以重用逻辑函数 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 函数仍将工作。

16472 次浏览

I've implemented a reward system once in what you would call a document oriented database (this was a mud for players). Some highlights from my implementation, translated to PHP and MySQL:

  • Every detail about the badge is stored in the users data. If you use MySQL I would have made sure that this data is in one record per user in the database for performance.

  • Every time the person in question does something, the code triggers the badge code with a given flag, for instance flag('POST_MESSAGE').

  • One event could also trigger a counter, for instance a count of number of posts. increase_count('POST_MESSAGE'). In here you could have a check (either by a hook, or just having a test in this method) that if the POST_MESSAGE count is > 300 then you should have reward a badge, for instance: flag("300_POST").

  • In the flag method, I'd put the code to reward badges. For instance, if the Flag 300_POST is sent, then the badge reward_badge("300_POST") should be called.

  • In the flag method, you should also have the users previous flags present. so you could say when the user has FIRST_COMMENT, FIRST_POST, FIRST_READ you grant badge("NEW USER"), and when you get 100_COMMENT, 100_POST, 300_READ you can grant badge("EXPERIENCED_USER")

  • All of these flags and badges need to be stored somehow. Use some way where you think of the flags as bits. If you want this to be stored really efficiently, you think of them as bits and use the code below: (Or you could just use a bare string "000000001111000" if you don't want this complexity.

$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";
  • A nice way of storing a document for the user is to use json and store the users data in a single text column. Use json_encode and json_decode to store/retrieve the data.

  • For tracking activity on some of the users data manipulated by some other user, add a data structure on the item and use counters there as well. For instance read count. Use the same technique as described above for awarding badges, but the update should of course go into the owning users post. (For instance article read 1000 times badge).

UserInfuser is an open source gamification platform which implements a badging/points service. You can check out its API here: http://code.google.com/p/userinfuser/wiki/API_Documentation

I implemented it and tried to keep the number of functions minimal. Here is the API for a php client:

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);
}

The end result is to show the data in a meaningful way through the use of widgets. These widgets include: trophy case, leaderboard, milestones, live notifications, rank and points.

The implementation of the API can be found here: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py

Achievements can be burdensome and even more so if you have to add them in later, unless you have a well-formed Event class.

This segues into my technique of implementing achievements.

I like to split them first into 'categories' and within those have tiers of accomplishment. i.e. a kills category in a game may have an award at 1 for first kill, 10 ten kills, 1000 thousand kills etc.

Then to the spine of any good application, the class handling your events. Again imagining a game with kills; when a player kills something, stuff happens. The kill is noted, etc and that is best handled in a centralized location, like and Events class that can dispatch info to other places involved.

It falls perfectly into place there, that in the proper method, instantiate your Achievements class and check it the player is due one.

As building the Achievements class it is trivial, just something that checks the database to see if the player has as many kills as are required for the next achievement.

I like to store user's achievements in a BitField using Redis but the same technique can be used in MySQL. That is, you can store the player's achievements as an int and then and that int with the bit you have defined as that achievement to see if they have already gained it. That way it uses only a single int column in the database.

The downside to this is you have to have them organized well and you will likely need to make some comments in your code so you will remember what 2^14 corresponds to later. If your achievements are enumerated in their own table then you can just do 2^pk where pk is the primary key of the achievements table. That makes the check something like

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

This way you can add achievements later and it will dovetail fine, just NEVER change the primary key of the achievements already awarded.