Author Topic: Effect Architecture  (Read 8973 times)

penguin_buddha

  • Newcomer
  • Posts: 19
  • Karma: +0/-0
    • View Profile
    • Email
Effect Architecture
« on: August 23, 2013, 03:17:27 PM »
I've been working on a roguelike game for a few months now and I've gotten to the hardest part yet, effects! I'm talking about everything from an effect that represents fire by continuously damaging an object to an effect that summons a creature, to an effect that modifies the stats of an object. So obviously there are a lot of potential effects. The question is how to go about implementing effects in an object oriented manner.

My current approach is to have an Effect class that is really broad. It contains multiple hashmaps that can contain game objects, simple values, locations, and other effects. Most instances of the class won't utilize all of these hashmaps. Each effect also has a list of types. These define how the game interprets the effect.

Examples Types: damage, healing, armorModifier, summon, etc.

For example, when an agent suffers damage the game will look for effects on that agent of the type "armorModifier". If an effect with that type exists on the agent, the game knows there should be a key/value pair in one of the hashmaps that tells how much armor should be added/subtracted. This would be for a passive effect, however many effects are active. Such effects have an activate() method that is called. The Effect class then acts based on the types of the effect. So if its a damage effect it will look for a key/value pair for how much damage, what targets to damage, what damage type it is and so on.

So if it wasn't complex enough for you already, there are also effects that really only modify other effects. For example, the activateOverTime type of effect just activates another effect at a certain frequency. The onHit type of effect applies a new effect to whatever an Agent attacks.

Here are some examples of effects and what types they would have:

  • Fireball: damage, aoeBurst
  • Vampirism: damage, healing
  • Alien Parasite: damage, activateOverTime, onTargetDeath (would create the Summon Parasite effect)
  • Summon Parasite: summon


So, I'd like your input on how to best implement this system. My current method can work, but its getting more and more complicated. I've considered creating a child class for each "type" of effect, though that would be a lot of child classes.

Thanks for any help,
Nathan
« Last Edit: August 23, 2013, 03:47:04 PM by penguin_buddha »

Aeris130

  • Newcomer
  • Posts: 1
  • Karma: +0/-0
    • View Profile
Re: Effect Architecture
« Reply #1 on: August 23, 2013, 03:54:07 PM »
Events and systems should be fairly easy to implement, and to extend when adding future effects. An event class can contain anything, as long as it extends the general Event class/interface.

Systems only have a single receiveEvent(event, objectTargetedByEvent) signature that is called for every event, every time an event is sent to an object. This can be optimized in the future by having systems subscribe to certain events sent to certain objects.

Example: Damage. When dealing damage to an object A, a damage event E is sent to the object by having every system call receiveEvent(E, A). Some of these systems will do nothing since they don't handle damage events, but the DealDamage system will confirm that A has health, and if so reduce the health by the damage amount in E.

Example: Armor. A damage event E is sent to A again, but this time A has armor. E is again sent to DealDamage, but before that it is sent to the ArmorSystem using its receiveEvent(E, A). ArmorSystem checks if A has armor, and if so reduces the damage amount in E by the armor amount to max(damage - armor, 0).

Note that this only works if ArmorSystem gets to handle the event before DealDamage, or else you get a bug.

Example: Element immunity (if a target has immunity to fire, any fire damage is null). An ElementImmunitySystem handles damage events, and checks if the damage dealt is of an element, and if the target has element immunity. If so, it cancels the event (every event needs an isCanceled value that prevents them from being sent to any further systems). Lowering the damage to 0 isn't enough, since future systems could increase it again.

Example: Armor parasite, a bug that spawns whenever damage is reduced by armor. There are two ways to implement this. The easiest is to simply have a SpawnArmorBug system that handles damage events and checks if the target has armor > 0. If so, the damage will obviously be reduced later on by the ArmorSystem. The SpawnArmorBug now creates a bug unit on whatever location it should spawn on.

The downside to this method is that it links the logic in SpawnArmorBug to the logic in ArmorSystem, making it cumbersome to maintain them both. If for some reason you add additional rules to the ArmorSystem (such as, armor now only kicks in if the owner isn't affected by Confusion), you now have to add this rule to every system that checks for armor reduction since they can't just rely on the presence of armor any longer.

A better way would be to have the ArmorSystem spawn an event of its own after reducing a damage amount, an ArmorReducedEvent, and send it to A using receiveEvent (by now you should realize that you need some globally available mail system that takes and event E and an object A and calls receiveEvent(E, A) on every system it stores). SpawnArmorBug no longer listens for damage events, only ArmorReducedEvents. This way it knows that a damage amount was reduced whenever such an event is received without having to know why or how it happened, or by which system. It also means that if you decide to remove the armor system, but forget to remove SpawnArmorBug, SpawnArmorBug  will simply do nothing since no ArmorReducedEvents will be sent. If you add additional systems that reduce armor, SpawnArmorBug will automatically trigger off of all of them as long as they send ArmorReducedEvents.

If you want other systems to react to the spawning of the bug, rather than spawning it by itself, the SpawnArmorBug could send a SpawnUnit(bug) event to the object T representing the tile. Another system (SpawnUnitSystem) checks if T can have units on it (is it occupied by another unit? Is it within the map?) and creates it there.

Note that some systems still have to listen to events that "belong" to other systems. ArmorSystem for example, can't listen to ObjectHasRecievedDamage events, since by that time it's already too late to change anything.

Example: Vampirism. VampireSystem listens for DamageEvents and checks how much damage was dealt. It then increases the health of the unit responsible for the damage. Note that VampireSystem must receive events after ArmorSystem, otherwise it won't take armor effects into consideration since the event will not yet have its damage amount adjusted. Also note that VampireSystem requires a new piece of data, namely the source of the damage. This source is not present in receiveEvent(E, A), so the easiest solution is to include it in the damage event. This could cause problems later on however, since other systems may need other sources as well. If something takes damage from a fireball, one system may be interested in the fireball being the source, while others may care about the caster.

Last example: Alien Parasite: damage, activateOverTime, onTargetDeath (would create the Summon Parasite effect)

3 systems:

AlienParasiteSystem: Delas damage over time (i.e whenever a NewTurn event is recieved by an object hositing an alien parasite).
ParasiteOnDeathSystem: Handles ObjectDeathEvents, and checks if the object hosts a parasite. If so, sends a SpawnUnit(new parasite) event to the location where it died)
SpawnUnitSystem: Handles unit spawning, same as before.

If at some point you wish to add more functionality to the parasite (parasites now randomly applies Panic conditions every turn), add a ParasitePanicSystem that handles NewTurn events, checks if the receiver has a parasite and randomly applies Panic.
« Last Edit: August 23, 2013, 05:10:44 PM by Aeris130 »

Trystan

  • Rogueliker
  • ***
  • Posts: 164
  • Karma: +0/-0
    • View Profile
    • my blog
Re: Effect Architecture
« Reply #2 on: August 23, 2013, 05:17:57 PM »
I've tried a few things and I usually implement effects like these as simple classes with an update method that get added to the main update loop. Then each effect can modify the world however they want (you can see my "BurningFire" effect at https://github.com/trystan/PugnaciousWizards2/blob/master/src/features/BurningFire.as)

The real complexity that I've seen is when effects start to affect each other. Some people suggest using events to coordinate everything or making it so effects can query for other effects and do all kinds of other things. I find that to be just as unwieldy and confusing - and very hard to make sure the effects get applied in the right order and don't cause infinite loops, deadlocks, or other bugs. I prefer some judiciously placed if statements. For example, in a recent game of mine the "on fire" and "frozen" status effects are mutually exclusive (so being frozen when on fire puts the fire out or being burnt while frozen unfreezes you) so I just added some counters and if statements to the Creature class.

So, I'd like your input on how to best implement this system. My current method can work, but its getting more and more complicated. I've considered creating a child class for each "type" of effect, though that would be a lot of child classes.

I wouldn't consider "a lot of child classes" to be a problem if each one is simple and well factored.

AgingMinotaur

  • Rogueliker
  • ***
  • Posts: 805
  • Karma: +2/-0
  • Original Discriminating Buffalo Man
    • View Profile
    • Land of Strangers
Re: Effect Architecture
« Reply #3 on: August 24, 2013, 09:27:51 PM »
In Squirm, I got a decently flexible system up and going, where new effects could be quite conveniently added to data files. I ended up dividing into two classes: Effects and Events, which were basically meta-Effects. Effects inflict the actual effects to a target, such as reducing health, modifying stats, (un)setting flags, etc. Events are used to calculate exactly which Effects should be applied to which Targets, and how. Using a skill or item typically invoked an Event, which might in its turn prompt for a direction/target, perform die rolls, calculate range/area of effect, check for resistances and so on and so forth. Finally (and even in the course of its execution), the Event passes one or more Effects or Events to one or more targets. At all times, the Events would keep track of an "agent" (who is doing?), "focus" (which item/tool, if any, is used), "target" (who/what is affected), as well as numbers for strength, range and duration of the action.

So, let's say a sword triggers the "Sword Attack" Event. The Event then retrieves info about how strong the sword and the attacker is, checks if the defendant may dodge or soak the blow, before passing a simple Effect "Damage" to the target.

That's the gist of it, although the system did end up having lots of nooks and crannies. Some Events were pure switches or condition checkers (for instance, imagine an Event to tax magic users for mana points: If the agent has no mana left, abort the action, else reduce mana by X points (by sending a certain Effect to the agant) and move on with executing the mother Event).

Hope this might give you some inspiration. You might get an impression of how I organized my system by downloading the game and looking in the directory called "kits/", which contains all the data files, for Events as well as stuff like monsters and quests.

As always,
Minotauros

Edit: My system is definitely guilty of the crimes outlined in Trystan's post, but worked well enough for me ;)
« Last Edit: August 24, 2013, 09:29:25 PM by AgingMinotaur »
This matir, as laborintus, Dedalus hous, hath many halkes and hurnes ... wyndynges and wrynkelynges.

Z

  • Rogueliker
  • ***
  • Posts: 905
  • Karma: +0/-0
    • View Profile
    • Z's Roguelike Stuff
Re: Effect Architecture
« Reply #4 on: August 27, 2013, 06:26:44 PM »
You can read about my approach to this problem at http://www.roguetemple.com/z/vapors/vaporgs.php

Azathotep

  • Newcomer
  • Posts: 18
  • Karma: +0/-0
    • View Profile
    • The Creepy Shoebox
    • Email
Re: Effect Architecture
« Reply #5 on: September 11, 2013, 09:17:22 PM »
Quote
Alien Parasite: damage, activateOverTime, onTargetDeath (would create the Summon Parasite effect)

You could add a new creature to do that rather than a chain of abstract effects. The alien parasite creature would be added to the turn system but would not be part of the map.

Code: [Select]
AlienParasite::DoTurn()
{
   if (Size < Juvenile)
   {
      Grow();
      //You could do anything to the host here
      Host.Hunger++;
   }
   else
      HatchFromHost();
}

AlienParasite::HatchFromHost()
{
    Host.Kill();
    Die();
    Host.Tile.Summon(new AlienJuvenile());
}

Infecting a creature:

Code: [Select]
FaceHugger::MeleeAttack(Creature target)
{
    Game.TurnSystem.Add(new AlienParasite(Host=target));
    target.Paralyze(turns=20);
    Die();
}

Code: [Select]
SurgeryMachineMkII::CaesareanSection(Creature patient)
{
    foreach (Creature creature in patient.AttachedCreatures)
    {
        if (creature.GetType() == typeof(AlienParasite))
        {
           //To reduce the risk of giant alien squids in the operating theatre
           //the Mk II surgery machine kills the parasite rather than just removing it
           creature.Kill();
        }
    }
}

guest509

  • Guest
Re: Effect Architecture
« Reply #6 on: September 12, 2013, 04:24:49 AM »
Dunno if this helps. I've been working on a button based system. Click a button and it loads your cursor with that effect. Click on a target to apply.

For example. Click on wolverine's claws, then click on an adjacent target and apply 'damage 10'.

Trying to make a super hero system I think relies on just TONS of custom click and apply effects.