7
\$\begingroup\$

I am working on a realtime dungeon crawler, focusing on a relatively complex and flexible skill system. Somewhat similar to MMORPGs with many compound spells, area effects, buffs/debuffs, ect. I am working on the last few subsystems of the engine.

I found a problem with actions happening in the same logic frame which change an actor's attributes.

For example, let's say two actors use a skill on eachother in the same logic tick, which deals 100 damage and leaves a debuff which halves damage done.
If I simply iterate over each actor and process their abilities, the one which gets processed sooner will deal full damage, the other will do so with the debuffed state, dealing only 50. Ideally, I'd like both of them to work similarly, dealing full damage and leaving the debuff.
What annoys me is the undetermined nature arising from the order of iteration.


One solution I thought about was to defer all the stat value changes and buff/debuff application until all the abilities are processed, since these are the only things which can affect the behaviour of abilties. So essentially I loop over actors, process their ability methods, but queue up the stat and status effect changes in a list.
Once I processed every ability I iterate over once more, this time changing the actual stats from the queued values.

This fixes the determinism problem, but causes another.

To use another example (since I can't exactly phrase the general case):
Let's say the player has 100 mana, and gets hit by ten manaburn attacks in one frame, each burning at most 50 mana, and dealing damage equal to the mana burned.
If I don't use the former fix, it'll work correctly, two will burn out the 100 mana of the player dealing 100 damage, the rest eight will do nothing.

With the fix however, since the player's mana value doesn't change yet, all ten attacks will land and deal 500 damage, more than the amount of mana the player has, which is an even more silly situation than before.


I'm quite an amateur, I have less experience than wit for designing complex systems. Is it possible to have determined behaviour for actions happening in the same frame while avoiding nastier consequences like the second example?

I know the player will unlikely notice the errors in the first case, and I don't think other developers bother with this issue either.

I'm still curious though, is such thing possible, a pipe dream, or just I have a warped idea about how the game logic should work?

Thank you!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ You can solve the manaburn problem itself by dealing max(50, mana) damage. \$\endgroup\$
    – jmegaffin
    Commented Jan 6, 2013 at 21:12
  • \$\begingroup\$ I agree - the manaburn problem is what should be fixed, since everything has to happen sequentially (it's a computer, after all). You might try processing the same attack at the same time, so you don't resolve 10 manaburn attacks, you resolve one manaburn x10 attack, and from that, calculate the damage that is actually done. \$\endgroup\$ Commented Jan 6, 2013 at 21:14
  • \$\begingroup\$ Maybe I should've emphasized more that the manaburn attack is just one example for a general category of problems, abilities which change, but also test for a value. \$\endgroup\$
    – Szoltomi
    Commented Jan 6, 2013 at 21:40
  • \$\begingroup\$ Whoops, I forgot enter posts the comment. There may be more potential problems too; corner cases suck. In a sequential approach, the manaburn attack is not an issue, only with the deferred attribute change one. I am currently thinking about subdividing the frame, where events can be marked as deferred. First I process each event in a sequential order and queue the deferred events/changes they generate. Then I process these, queue the deferred changes, and so on in a loop, while(!deferredEvents.isEmpty()). \$\endgroup\$
    – Szoltomi
    Commented Jan 6, 2013 at 21:55

2 Answers 2

3
\$\begingroup\$

Good question. Your 2 examples are obviously incompatible by design - either the effects of previous actions are available when calculating results of the current action, or they're not.

First, though: the phrase "I am working on a realtime dungeon crawler" implies that this sort of problem really shouldn't happen often enough to be a problem. The essence of real-time is that things happen so quickly that it's easy to accept that the other person acted just milliseconds more quickly than you, and so an arbitrarily sequential order of execution is almost never a problem for the player.

If, for some reason, everybody does tend to use abilities on the same tick, you could just use some system to spread them out so that this is a much rarer occurrence. Make the ticks shorter and more frequent, or have characters act on different ticks based on a speed or initiative value. Or, just randomise the order of logic processing so that nobody gets a persistent advantage.

But if you really want to 'solve' the problem, one way is to split the system up into phases, one for evaluating simultaneous actions, one for applying the results, and one for evaluating AND applying sequential ones. That way the buffs all get resolved fairly and only once that's done are effects like manaburn executed. Generally speaking the only actions that need to be sequential are the ones where the character being changed is the same character that's being examined, because each action needs to perform the whole read-then-write atomically for the system to make sense.

\$\endgroup\$
3
  • \$\begingroup\$ Thanks for the insight, I haven't even realized entities may get a persistent advantage. I'm implementing a sequential system either way, it's easier and faster to work with obviously. I'll think about your solution though; I have a feeling that it may still have issues with loops of draining spells for example. What do you think about that? Truth to be told, the more I think about the simultaneous approach, the more potential aberrations I notice. \$\endgroup\$
    – Szoltomi
    Commented Jan 7, 2013 at 2:24
  • 1
    \$\begingroup\$ I don't understand what you mean by 'loops of draining spells'. No single action can have both the properties you want because they contradict each other, but you can choose which of the two you want to apply on a per-action basis. \$\endgroup\$
    – Kylotan
    Commented Jan 7, 2013 at 2:34
  • \$\begingroup\$ By the drain loop I meant, for example, Actor A and B use a mana leech skill on eachother. I regret mentioning it though, it was a stupid idea I didn't even think out clearly. I had a theorical idea how to combine both delayed changes to actors, and having the updated world available. I'll detail it in an answer, since it's a bit long for a comment. Edit: Or would a different medium be more appropriate? \$\endgroup\$
    – Szoltomi
    Commented Jan 7, 2013 at 3:04
0
\$\begingroup\$

I had a theoretical idea how to combine delaying changes to the world with having the updated values available. I realized the issue mainly boils down to somehow have a defined order of events happening inside a frame, somehow subdividing the frame into smaller, but undefined units in time, both in length and count.

The theory:
Each event is a series of simple actions, which either sample the world, or change them by inserting new entities, changing attributes or applying effects. As Kylotan pointed out, if the changes to the world are delayed, they are not available when an event is processed.

I thought about putting some kind of breakpoints into an event, which mark that we need the updated state of the world to continue. Each segment between breakpoints is processed sequentially.

In the main game loop, the processing of the events is done in a second loop:

1) each event sequentially gets processed up to the next breakpoint.
2) The attributes and status effects of the entities are updated.

This is repeated until there are no unfinished events left.

I am using Java, I don't have any idea how this could be implemented most cleanly. One idea I had is to break the event method into a switch, each segment going into a case.

This would translate the following (pseudo-code)

Event manadrain(){
  player.mana.add(-5); 
  waitWorldUpdate();
  int v = min(target.mana, 10);
  player-mana.add(v);

into:

public void processEvent() {
    switch (segment) {
        case 1:
            player.mana.add(-5);
            return true;
        case 2:
            int v = min(target.mana, 10);
            player - mana.add(v);
            return false;
    }
}

the segment variable can be incremented internally; the return value returns true if there are more segments to be processed. The events can be processed in a list, and removed when they return false.

This way, both of my examples in the question are solved. In the damage debuff example, there are two potential outcomes:

  1. If we do the damage and the debuff application in one segment, both of them will deal 100 damage, and receive the debuff.
  2. If we apply the debuff first, wait for an update then do the damage, both will do 50 damage with the debuff on.

The manadrain example is simpler, the events are not needed to be broken apart; since they are processed sequentially, the first two will land and burn 2*50 damage, the rest will fail.

Why would such a deterministic solution be important?
For example if two similar entities are created at the same time, and affect eachother in a similar way to the first question example, it may be desired that both of them change in the same way.

On the other hand, the more time I spend working on this problem, the more potential aberrations I find. Not to mention this would likely be a nightmare to debug and work with. I am not even sure this solution is good enough. Please tell me if there are any cases where the above solution fails.

\$\endgroup\$
1
  • \$\begingroup\$ All this does is move the problem and make it more complex. Each extra event you add will need to be carefully considered to make sure each segment doesn't rely on a value that another event may change in this segment. eg. Given your example (and assuming you made the same typo on the last important line twice), any other event that reads this players mana score in segment 2 can be wrong. You could add enough segments so that each variable gets its own, but I think you'd be better served just by making the events execute sequentially. \$\endgroup\$
    – Kylotan
    Commented Jan 7, 2013 at 13:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .