具有重叠行为的对象组合

Object composition with overlapping behaviors

如果没有示例,这将很难解释,所以我将使用 here 中的示例。

const canCast = (state) => ({
    cast: (spell) => {
        console.log(`${state.name} casts ${spell}!`);
        state.mana--;
    }
})

const mage = (name) => {
  let state = {
    name,
    health: 100,
    mana: 100
  }
  return Object.assign(state, canCast(state));
}

简单地说,我们有一个'mage'对象和一个'cast'行为。

现在假设我们想要一种新型法师,它可以在施放法术时吸取对手的生命值。一开始似乎很容易;只需创建一个新行为:

const canCastDrain = (state) => ({
    cast: (spell) => {
        console.log(`${state.name} casts ${spell}!`);
        state.mana--;
        target.health--;
        state.health++;
    }
})

但是,这迫使我们复制原始演员代码。你可以想象,对于更复杂的行为,这将是一个巨大的问题。如何避免这种情况?

如果我们使用经典继承,drain spell cast 可以扩展base cast 然后调用parent 方法。但随后我们陷入了继承问题。如果我们添加新的法术施法,就很难混合搭配它们。

这个答案是在某种 JS 伪代码 中给出的,因为我对我的面向对象的 JS 没有信心(我通常使用 TS)。

你的法师可能有一个 class 的角色基础,或者什么的。毕竟,每个人都有名字和健康。我省略了它,因为它与答案并不相关。问题是关于你的法术是如何构造的。

我非常有信心命令模式就是您所需要的。

法师有一些属性和两种施法方法。第一个决定法师是否可以施放该法术。你可以让法术有一个类别(或法术学校),或者你想检查权限。

pre-cast 和 post-cast 的方法虽然不是您问题的明确部分,但很可能会出现。也许咒语需要在调用其施法方法之前检查目标是否有效。

class Mage {
    mana: number;
    health: number;
    name: string;

    canCast(spell) {
        // check if the mage knows the spell, or knows the school of magic, or whatever.
        // can also check that the mage has the mana, though since this is common to every cast and doesn't vary, that can be moved into the actual cast method.

        // return true/false
        // this method can vary as needed
    }

    // should be the same for all mages.
    // we call the spells pre-cast hooks before casting, for composite spells this ensures each sub-spell pre-hook is called before any of the spells
    // are cast.  This hook can be used to verify the spell *can* be cast (e.g. you have enough health)
    cast(spell, target) {
        if (spell.getCost() > mana) {
            // not enough mana.
            // this isn't part of canCast, because this applies to every mage, and canCast can vary.
            // return or throw an error
        }
        console.log("Casting....");

        if (!spell.preCast(this, target)) {
            // maybe the target isn't valid for this spell?
            // we do this before *any* spells are cast, so if one of them is not valid, 
            // there's nothing to "roll back" or "undo".
            // either throw an error or return.  either way, don't continue casting.
        }
        spell.cast(this, target);
        spell.postCast(this, target); 

        this.deductMana(spell.getCost());
        console.log("Done casting.  Did we win?");
    }
}

基本法术,没有任何功能,但充满了叫做 'love':

的东西
class Spell {
    getName(): string;
    getCost(): number;

    preCast(target, caster, etc.) {}
    cast(target, caster, etc.) {}
    postCast(target, caster, etc.) {}       
}

你的复合法术。一个 class 应该可以让你进行任意数量的组合,除非你需要非常专业的东西。例如,结合两个火焰法术可能会增加伤害,同时降低总法力消耗。那将需要一个特殊的复合咒语,SynergizingCompositeSpell 也许吧?

class CompositeSpell : Spell {
    spells: Spell[];

    getName { 
        // return the subspell names
    }

    getCost (
        // return sum of subspell costs.
    }

    preCast(target, caster, etc.) {
        // call each subspell's preCast
        // if any return false, return false.  otherwise, return true.
    }
    cast(target, caster, etc.) {
        // call each spell's cast
    }
    postCast(target, caster, etc.) {
        // call each spells post-cast
    }

    constructor(spell, spell, spell, etc). // stores the spells into the property
}

示例拼写:

class Drain : Spell {
    getName() { return "Drain!"; }
    getCost() { return 3; }  // costs 3 mana


    cast(target, caster, etc.) {
        target.harm(1);   // let the target deduct its hp
        console.log(`The ${target.name} was drained for 3 hp and looks hoppin' mad.`)
    }
}

这在使用中的样子,施放了一个耗尽生命值并使我的牙齿闪亮的咒语 chrome

var mage = ... // a mage
var target = ... // you, obviously
var spellToCast = new CompositeSpell(new Drain(), new ShinyAndChrome());
mage.cast(spellToCast, target);

CompositeSpell 构造函数可以检查它所给出的法术是否为 "compatible",无论这在您的游戏中可能意味着什么。法术也可以有一个 canBeCastWith(spell) 方法来验证兼容性。也许将 DrainHeal 组合在一起毫无意义且不应该被允许?或者一个法术接受一个目标,但另一个不接受?

值得注意的是 preCast / cast / postCast 方法应该采用相同的参数,即使它们并不总是需要。您使用的是一种千篇一律的模式,因此您需要包括任何法术可能需要的一切。我想该列表仅限于:

  • 施法者
  • 目标
  • 区域(效果法术的区域)
  • 咒语选项(在龙与地下城中,施法者选择将某人变形为什么)

我想指出的一件事是,不要直接使用 addition/subtraction 来处理你的生命值或法力值(例如 state.mana--),而是使用函数调用(例如 state.useMana(1)。这让您的选择在未来的发展中保持开放。

例如,如果您的法师具有在 his/her 生命值减少时 触发 的能力怎么办?该咒语不知道它应该触发任何东西。那要看性格了。这也让您可以覆盖该方法,这是您无法使用简单的 addition/subtraction.

希望这个回答对您有所帮助。