17
\$\begingroup\$

I'm making a top down 2D game and I want to have a lot of different attack types. I'd like to make the attacks very flexible and combine-able the way The Binding of Isaac works. Here's a list of all the collectibles in the game. To find a good example, lets look at the Spoon Bender item.

Spoon Bender gives Isaac the ability to shoot homing tears.

If you look at the "synergies" section, you'll see it can be combined with other collectibles for interesting yet intuitive effects. For example, if it combines with The Inner Eye, it "Will enable Isaac to fire multiple homing shots at once". This makes sense, because The Inner Eye

Gives Isaac a triple shot

What's a good architecture to design things like this? Here's a brute force solution:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

But that will get out of hand very fast. What's a better way to design a system like this?

\$\endgroup\$
  • \$\begingroup\$ Personally I would just keep all equipped items in a list and have them implement some sort of common interface that takes an object you mutate from object to object. "for each item modify planned attack" so one item could duplicate the amount of projectiles, one could add it's tint and change damage (so if one item made red bolts and another did yellow you would have an orange attack after both modify the attack). You could also just have one generic item class that has parameters to decide how it modifies the planned attack. \$\endgroup\$ – Benjamin Danger Johnson Oct 10 '13 at 22:57
  • 2
    \$\begingroup\$ I'd like to point out that Kirby 64 took the brute-force approach and hard-programmed different effects for all possible combinations of abilities, so it is doable. \$\endgroup\$ – Kevin - Reinstate Monica Jul 2 '14 at 19:48
16
\$\begingroup\$

You totally don't need to hand-code combinations. You can instead focus on the properties that each item gives you. For instance, Item A sets Projectile=Fireball,Targetting=Homing. Item B sets FireMode=ArcShot,Count=3. The ArcShot logic is responsible for sending out Count number of Projectile items in an arc.

These two items can be combined with any other items that modify these (or other) properties freely. If you add a new type of projectile it'll just automatically work with ArcShot, and if you add a new firing mode it'll automatically work with Fireball projectiles. Likewise, Targetting is a property that sets the controller for the projectiles while FireMode creates the projectiles, so they can be easily and trivially combined in any combination as makes sense.

You might also set of property dependencies and such. For instance, ArcShot requires that you have a provider of Projectile (which might just be the default). You might set priorities so that if you have two active items that both provide Projectile the code knows which one to use. Or you can provide UI to let the user select which projectile type to use, or simply require the player to unequip high-priority items he doesn't want, or use the most recent item, etc. You can further allow a system of incompatibilities, e.g. such that two items that both just modify Projectile cannot both be equipped simultaneously.

In general, when possible, prefer any kind of data-driven approach (or declarative) over procedural approaches (the big if-else messes) when it comes to the objects and such in your game. Higher-level generic logic that is configurable by simple data is much preferable over hard-coded lists of special-cased rules.

\$\endgroup\$
  • \$\begingroup\$ Small nit, but you don't seem to have the correct properties for the examples you're using. "Spoon Bender" adds homing and "Inner Eye" adds triple shot. Neither add arc and both are tears. If you're using arbitrary properties in your example to abstract the design, it'd be easier to read if they weren't named in misleading ways. I'd prefer "Item A" and "Item B" to this. \$\endgroup\$ – Daniel Kaplan Oct 11 '13 at 2:12
  • 1
    \$\begingroup\$ @tieTYT: I generalized the names and expanded the example to include targetting modes in addition to projectile types and firing modes. Never played BoI for more than a few minutes so I don't have the names as internalized as others might. :) \$\endgroup\$ – Sean Middleditch Oct 11 '13 at 2:20
  • \$\begingroup\$ It seems to me that the tricky part is identifying the categories of properties. eg: Targetting is one, FireMode is another, Count may be a 3rd... or it may not. Maybe 3 projectiles could be a fireball and 5 could be grenades even though it's one weapon. \$\endgroup\$ – Daniel Kaplan Oct 11 '13 at 22:16
  • \$\begingroup\$ @tieTYT: absolutely. You can extend the system further to allow special combinations or logic, certainly, but that should be the exception rather than the rule. Optimize for the common case, not the corner case. \$\endgroup\$ – Sean Middleditch Oct 11 '13 at 22:38
  • \$\begingroup\$ Here's a great video about why you're wrong regarding procedural programming youtu.be/QM1iUe6IofM , also the data driven way you describe maps any if/else blocks into individual data sets, essentially doing nothing to reduce how many special case scenarios you have to program, as you have to program them all... \$\endgroup\$ – RenaissanceProgrammer Jul 14 '17 at 3:14
8
\$\begingroup\$

If you are using an OOP language, this sounds like a good place to employ the Decorator Pattern. When you want to modify how an attack happens just decorate it with the appropriate augmentation.

Crude c++ Example:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

This method would be best if you have a very large number of attacks and you need to have them all behave in more or less the same way. If you want to substantially change the way the attack happens with the modifier (e.g. new animation with modifier) then this method is not for you.

\$\endgroup\$
  • \$\begingroup\$ If I want to change the animation, why is this not for me? Also, what if I'm working in a more functional language? Do you know of a design pattern that's a fit for them? \$\endgroup\$ – Daniel Kaplan Oct 10 '13 at 23:19
  • \$\begingroup\$ Its more rigid because the modifier will always end up calling the Attack method of the object it aggregates. The TripleAttack class should not know about the TearAttack class. If this were true then it would lead to just as many headaches as the else-if block. This means that any tear animations have to reside inside the TearAttackBehaviour object. This object doesn't (and shouldn't) know it's been decorated by a TripleAttack object. The result is that the 3 tear animations proceed independently, because are independent. \$\endgroup\$ – NauticalMile Oct 11 '13 at 0:20
  • \$\begingroup\$ I'm having a hard time explaining this in words, if someone else wants to take a stab at it, be my guest. \$\endgroup\$ – NauticalMile Oct 11 '13 at 0:21
  • \$\begingroup\$ As for implementing this in a more functional language, I will think about it for a while and amend my answer when I am ready. \$\endgroup\$ – NauticalMile Oct 11 '13 at 0:23
1
\$\begingroup\$

As a fan of Binding of Isaac, I too have wondered how to go about something like this. The system in the game is robust enough where emergent behaviors come about from the combination of effects (the one that comes to my mind is getting mirror, spoon bender, and some range boosters results in a swirling, homing tear wall around Isaac, Magneto style). The sheer number of them would make an "if" block impractical.

My conclusion is that Isaac and his tears are two entities in the center of a massive Component-Entity Framework. The entities have some basic stats (movespeed, life, range, damage, sprite, etc) and each component would bring along a stat modifier and a verb.

In code, Isaac and his tears would each have a list that would contain things of an interface. Isaac would have a list of things that subscribe to interface IsaacMutator, and his tears tearMutator. IsaacMutator would have functions to modify Isaacs health, speed, range, looks, and some special verb. TearMutator would be similar. Once per game loop, Isaac would loop through all IsaacMutators that it has, and all living tears would too. To go by your English example, it would read like:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

and so on. Because the types are additive, you can stack and add and remove to your hearts content.

\$\endgroup\$
-5
\$\begingroup\$

I think your way works best. These sort of items each give a condition, if used together they produce a different condition, then you would effectively need all 3 possible conditions defined.

You could also go about it by creating a new type of definition when both items are present, but this actually adds to the convolution:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
\$\endgroup\$

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.