0

I'm writing a "generic" achievement system for my MMORPG project, it needs to be friendly & efficient for my game designers (without having to write code to add new achievements). If anyone got some suggestions about good alternatives, I'll be glad to give it a check. (Lua, C# scripting...), many things I've not yet done and I'm feeling more "comfortable" on that option.

For that, I thought about a solution, which is a basic configuration file that is linked to an enumeration of "eventType" and provide a list of optional args (based on the Event properties) Here is an example

name: ACHIEVEMENT_NAME
event_type: ITEM_USAGE # enum as a string, there is a list of event types
count: 30 # 30 item usage
args: # every args are optional
  item_vnum: 1127 # vnum - optional
  map_id: 1 # on map id 1 only - optional
public enum AchievementEventType
{
    ITEM_USED,
    MONSTER_KILLED,
}

public interface IAchievementArgument
{
    AchievementEventType EventType { get;}
}

public class MonsterKilledAchievementArgument : IAchievementArgument
{
    public AchievementEventType EventType => AchievementEventType.MONSTER_KILLED;

    public long MonsterVnum { get; set; }
    public short? MapId { get; set; }
}

public class AchievementConfiguration
{
    public string Name { get; set; }
    public string EventType { get; set; }
    public long Count { get; set; }
    public Dictionary<string, object>? Args { get; set; }
}

public delegate bool AchievementFilter(IAchievementArgument achievementArgument);

I thought about generating an expression tree to build a delegate (AchievementFilter) for my "Achievement incremental condition" (Basically, the function that will check if the player's specific achievement counter can be incremented or not)

There are two solutions I thought, both have their pros/cons, but I'm looking for external point of view, what do you think about it. (or maybe, another kind of solution that you could present me)

1 expression tree per achievement

I generate 1 expression tree per achievement that will compare the IAchievementArgument given as parameter with the achievement configuration.

  • Pros :
    • faster at execution time (each achievement condition have their own delegate)
  • Cons:
    • Memory footprint

1 expression tree per args type

I generate 1 expression tree per IAchievementArgument type that will fetch and compare achievement configuration one by one

  • Cons :
    • More execution time (needs to check all key/value equality of each achievement configuration)
  • Pros :
    • Lighter memory footprint
0

Break your predicates down further, and use composition. Don't think of this as just a system for achievements. This is a rule system, of which achieving something is the effect of a rule.

Dead(monster['BigBadBoss']) -> unlock(achievement['First Boss Down'])

If its a quest:

has(Quest['death to marsh hairs']) && accumulate('death to marsh hairs', 30, Dead(monster.kind['marsh hair'])) -> complete(Quest['death to marsh hairs']) && play(music['victory dance']) && receive(gold[30]) && receive(experience[200]) && unlock(achievement['Hunting Wabbits'])

Now not only have you solved how to let the designers play with achievements, you've also solved how to let the designers customise just about any set of events in the game.


Now the designers are going to give this t the engine as a list of expressions trees. Applying each tree in turn for each change of state/event will be slow. However you can optimise things somewhat, by inverting the problem.

Consider the quest above, if the event Dead(monster.kind['marsh hair']) does not happen this rule, and perhaps fifty other rules containing this predicate cannot activate. Even more so, it is a specific kind of monster, and a specific event in that monsters life cycle. We could organise this by 'marsh hair' monsters, or by event such as dead, whichever fits the eventing model best. The handler would then check for the next criteria.

eg:

  • Monster generates a dead event.
  • Dead Event handler checks to see what kind it is: 'marsh hairs' here, 'ding bats' there, 'marsh haired ding bats' somewhere else
  • That then satisfies a predicate, so look at the parent predicate and ask if its okay.
  • The parent is an accumulator and counts off 30 of these events before it says yep all good.
  • Then it to asks the parent to see if all is well.
  • Is the has(Quest['death to marsh hairs']) predicate satisified?

The end result is a kind of inverted tree forest. Where every rule has been considered by ignoring any rule not directly related to the event being handled. It also optimises the search, as the common sub-parts of each rule are shared, amongst all rules.

|improve this answer|||||
  • I already have this kind of behavior, once an "AchievementType" is matched, events (like the MonsterKilledAchievementArgument event, which I should have named as MonsterKilledEvent in this example to be clearer) are being emitted to event subscribers (in my case, a microservice dedicated to achievements) Anyway, I decided to use NLua and make a Lua binding, which was way more customizable and my game designers felt comfortable using it for basic condition writing. – Blowa Jan 20 at 8:45
0

Looks like a good application for the event streaming pattern.

  1. Define a reasonably versatile event model, for example actor > action > target. Then, fire events like player > creates > dolphin stew.
  2. Emit events at suitable places (useful for other game stuff, too)
  3. Create stateful event aggregators. For example, for player > creates > dolphin stew events, increase the dolphin stew count for that player
  4. When the aggregators reaches the reward threshold, emit another event (apprentice chef)
  5. Have other event consumers react to the apprentice chef event

There's various frameworks to do most of the work, like Kafka or cloud vendor specific stuff.

|improve this answer|||||
  • That's exactly what I'm doing, but I use MQTT (just a MQ broker cause it's a small student project) and a own made ServiceBus protocol. I was interested into the "Aggregators" here, but I did some PoC around and a LUA binding seemed to be a really nice, maintainable and not too time consuming solution. – Blowa Jan 21 at 15:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.