#Help designing an effects system

13 messages · Page 1 of 1 (latest)

still blaze
#

Hi! I'm going mad trying to make an effects system for a card game, I've remade it 3 times and it's slowly driving me round the bend, I'd like a hand with design I think I might be over engineering it or way off on my understanding of what is needed!

A bit about the problem:

  • Making a card game and want to build a card effects system.

  • I'm basing it off hearthstone's as in a card is played and has an effect, the conditions for the effect need to be checked (is card X on board?, is hp below a certain amount? has a card been destroyed?)

  • In hearthstone this can trigger an effect chain like a stack of dominos which is where a lot of the complexity is coming from.

The Issue ive been having is in my three attempts at this complexity quickly spirals until i stop understanding my own code, i feel like ive been stuck on this so long now even another opinion would be a help, I've been lost in the world of observer pattern forms for two weeks. I also feel my lack of experience with the Godot signals system is maybe causing me to miss an obvious solution to cut down on complexity.

What I'm looking for:

-Ideally a centralized way to track game changes that effects might rely on i.e not signals all over my code when events happen.

-A way to process these effects in batches ( I think ill need this?) so I can get closer to that hearthstone behavior.

-Expandability, as in adding a new effect is adding a new effect function and subscribing to a signal, not 5 code changes over 4 classes.

I understand this is quite a broad area to ask for help on but id be grateful for any ideas or recourses on the topic, Thanks!

celest plank
still blaze
# celest plank Look into the mediator pattern. It can be a great replacement of observers in th...

This looks closer to what I need, I actually got something kind of like this in one of my attempts, I was trying to make an 'effects broadcast' class which would check signals against which cards had subscribed to them and then call a helper to run the effects it needed but it became a bit of a black box and I ended up abandoning it. I think I understand how this works the issue is more how I actually implementing something like it. could you set something up like any action at all calls to the mediator which has a list of all possible current card effects, it checks the conditions, bundles up all that are met processes them then it communicates to a board manager or something that can then add stats destroy cards ect ?

celest plank
celest plank
#

I've never done a card system to bear with with me, but here are my thoughts.
So lets say you have a card that says "Deal 5 damage to player and then gain 5 life." First you need to create two "Effects": DealDamageEffect and GainLifeEffect. The card then has a list of Effects and adds one of each. Now, based on player input and card data, the card plugs in the information required by the effects:

effects[DEAL_DAMAGE].target(player_2)
effects[DEAL_DAMAGE].damage(5)
effects[GAIN_LIFE].target(player_1)
effects[GAIN_LIFE].heal_amount(5)

From here, instead of activating these effects directly, the card sends them to the mediator. The mediator makes any changes required, and then activates the effects. We want the changes made to be dynamic of course, so the mediator has a list of Modifiers that it passes the effects to.

#Mediator
func process_effects(effects: Array[]):
  for e in effects:
    for m in modifiers:
      m.modify(e)

#GainLifeModifer
func modify(effect: Effect):
  if effect is GainLifeEffect:
    effect.heal_ammount += 2

Now if you want to create global modifiers, you simply add it to the mediator.
How does this sound so far?

still blaze
#

Yes this makes sense then you'd have a helper class to manage the prioity of effects and process them in phases that the mediator calls ? I think the part I'm struggling with is how the actual triggers will work, as in an effect could be played based on lots of conditions e.g. -on_card_played
-on_card_destroyed
-on_board_change
-on_board_configuration (like if there are 3 cards of type A on the board then do X)
-on_proximity (if there is card A to the direct left then do X)
just a few I could see it expanding as game needs change, but while some of these could maybe be signals from the card some may also be from the board or maybe else where, how do you see the triggers actually working? I feel like I've been looking at this so long that the answer might be easy but I'm just not seeing it. Thanks for the response

celest plank
#

Event buses*

still blaze
#

I am using a global signal bus atm i was toying about with a dedicated effects one

celest plank
#

So I haven't completely thought this through, but I'm thinking something like a mediator/bus hybrid.

#Card
func destroy():
  mediator.on_card_destroyed(self)
  queue_free()

#Mediator
func on_card_destroyed(card: Card)
  for l in card_destroyed_listeners:
    l.on_card_destroyed(card)

#CardDestroyedListener
#Owner of destroyed card looses 1 life
func on_card_destroyed(card: Card):
  var effect := DealDamageEffect.new()
  effect.target(card.owner)
  effect.damage(1)
  mediator.process_effects({effect}) #process_effects takes an array. Not sure if this is the right syntax

So it's sort of like an event bus, except structurally it's the same as the modifier code from earlier. Event bus in spirit maybe. But the events are sent to the mediator. The mediator has lists of interested parties (like an event bus) and the event is sent to those registered objects. Those objects produce a new effect based on the event and send it to the mediator, where it goes through the regular modifier process

still blaze
#

Okay so, when an effect trigger happens it calls out to the mediator to let it know, the mediator hold as list of cards that are listening for this event, for every card that is the mediator then performs the effect logic by calling out to the effect class with the listing card ( this stage could maybe be moved to a like process effect manager class or something as the effects need to be processed in a specific branching type way and in phases and the process manager can do that) I think the only gap I have now is how the listener arrays are populated in the mediator.

This is straying into another problem but I was planning to have a JSON with card effects that I load in for each card would something like

{
id: 123
name: effectName
desc: add one hp to this card if another card is destroyed
effectType: StatAugment
listener: on_card_destroyed
}

would it being set in here then when the effect is built work ? im thinking for expandability ? or maybe ive got the wrong understanding of how a listener is subscribed to?

celest plank
still blaze
#

Okay, this is a massive help and I think there's enough here to have another go at it, thank you very much