The Holiness of the Update Cycle

Image of a space plane about to be hit by a fireball; there is a line T=0 in the middle and the plane is labelled chicken, fireball egg illustrating the problem of which came first.

Most games have what's called an "update cycle". One way or another, there are a bunch of entities/objects with an update () function, which is called periodically (usually every frame.) These might control enemy logic, particle positions, animations, etc.

Here is something that I have found to be incredibly helpful in reducing bugs.

Holy Means "Set Apart"

Consider that your entire update cycle is "holy"; that is, your objects are ONLY allowed to change their state meaningfully within it. For example:

  • Suppose you have an Enemy class, with an update () method and a hit (damage) method.
  • The hit (damage) method will typically have some logic to decide if the damage should destroy the object, and make it explode if necessary.
  • Then, there is a Bullet class, which within it's update () cycle checks to see if it's colliding with any Enemy objects, and if so, calls hit (5) for 5 damage.

How Something Gets Hit by a Bullet

Here's how I will structure this:

  • The hit (damage) method is outside the Enemy object's update method, so it's not allowed to meaningfully change the state of Enemy. In OO-terms, any changes it makes must be strictly private, and not change the public appearance of the object.
  • This means that the hit (damage) method is NOT allowed to explicitly kill the object; as well, its not even allowed to affect the outcome of a function such as isDead () (which might check if the hit points <= 0).
  • So, what you do is you have a request variable, such as "damage_accumulator" which accumulates the damage over a frame. the hit (damage) function just adds damage to the internal damage_accumulator.
  • Then, in the object's update () cycle, you perform hit_points -= damage_accumulator; damage_accumulator = 0; and a check like if (hit_points < 0) { explode (); };

Reasoning Why It's Better

I don't know exactly why this saves me so many headaches, but it does. For sure, it's something "like" a transactional system; changes are being collected (e.g., in the damage_accumulator variable, above) and then being applied later on, all at once. The update cycle for an object knows e.g., that the object will explode before it heals, or vice versa.

But I think it's actually more to do with unexpected side-effects where an object's external state changes more than once per frame. For instance, suppose object A had update () called, then object B's update () determined it needed to call A.changeSomething (). At this point, A has looked three different ways in the game's update cycle: before A.update (), after A.update () and after B.update ().

It definitely seems to be working for me-- maybe it'll help you, too!

Interesting, I think I probably do tend to write object code with this distinction largely in place, because it feels intuitively less risky. I'm rarely explicit about it however, and I can certainly see how being so might be advantageous.

I might have to have a play about with this approach and see if it suits me or not. Cheers.

 

There was a fantastic comment on my LJ (I mirror this blog in 4 places, this is the main place.) I'll paste it here without permission because I don't want to lose the comment (unfortunately it's Anonymous so I don't know who...)

"You need to take it a step further. Specifically with your last example, the problem with that case is that update order matters. If A causes some change to B, you will get a different result for that frame if A is updated first than if B is updated first. Ideally the affect of A's update upon B would always be delayed until the next frame. What you need is a two-part update. Using the example of the bullet strike, the first half of the object's update APPLIES the change. It applies the damage to the health, and sets the accumulator to 0. The second half ACTS ON the change. It checks the health, and destroys the object if appropriate. If you run the first half on every object, and then the second half on every object, you'll get the same result no matter the order objects are updated in."

 

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Internal paths in single or double quotes, written as "internal:node/99", for example, are replaced with the appropriate absolute URL or path. Paths to files in single or double quotes, written as "files:somefile.ext", for example, are replaced with the appropriate URL that can be used to download the file.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <img> <h2> <h3> <br>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options