免责声明:
在我看来,有关软件体系结构的文章不应该也不是理想的。 所描述的任何解决方案都可能无法涵盖一个程序员所需的级别,而对于另一位程序员,这将不必要地使体系结构复杂化。 但是它必须为自己设定的任务提供解决方案。 这种经验以及程序员学习,组织信息,磨练新手并批评自己和他人的程序员的所有其他知识包–这种经验变成了出色的软件产品。 本文将在技术和技术部分之间切换。 这是一个小实验,我希望它会很有趣。-听着,我想到了一个很棒的游戏主意! -游戏设计师Vasya衣衫不整,他的眼睛是红色的。 我仍然在Habré上喝咖啡和霍利瓦来消磨站立前的时间。 他满怀期待地看着我,直到我写完评论给那个男人他错了。 瓦西亚知道,在伸张正义和捍卫真理之前,继续与我对话毫无意义。 我加了最后一句话,看着他。
简而言之,具有法术力的魔术师可以施展咒语,战士可以进行近距离战斗并消耗耐力。 魔术师和战士都可以移动。 是的,仍然有可能抢劫母牛,但简而言之,我们将在下一版本中进行。 站起来后,您会展示原型吗?
他逃离了自己的游戏设计业务,而我打开了IDE。
实际上,主题“组成与继承”,“香蕉-猴子问题”,“菱形问题(多重继承)”是面试中常见的问题,其形式多样且有充分的理由。 不正确地使用继承会使体系结构复杂化,没有经验的程序员不知道如何处理这种情况,结果开始批评整个OOP并开始编写过程代码。 因此,有经验的程序员(或在Internet上阅读过智能知识的程序员)认为有责任在面试中以各种形式询问此类知识。 普遍的答案是“组成比继承更好,并且不应应用任何灰色阴影。” 刚读完每个这样的答案的人将100%满意。
但是,正如我在本文开头所说的那样,每种体系结构都适合您的项目,如果继承足以满足您的项目的需要,而解决此问题所需的全部工作就是使用香蕉创建Monkey-创建它。 我们的程序员也处于类似情况。 仅仅因为FPS会嘲笑您而拒绝继承是没有意义的。
class Character { x = 0; y = 0; moveTo (x, y) { this.x = x; this.y = y; } } class Mage extends Character { mana = 100; castSpell () { this.mana--; } } class Warrior extends Character { stamina = 100; meleeHit () { this.stamina--; } }
像以往一样,站立式会议继续进行。 我摇摇椅子上挂了电话,而June Petya试图说服测试人员无法通过鼠标右键进行快速控制不是一个错误,因为在任何地方都没有这样的机会,这意味着您需要将任务交给生产前的部门。 测试人员认为,由于对用户来说,通过右键进行控制似乎是强制性的,因此这是一个错误,而不是功能。 实际上,作为我们团队中唯一在战场服务器上玩游戏的玩家,他想尽快添加此功能,但是他知道,如果您将其放到预生产部门中,那么官僚机器将使其在不迟于4的时间内发布。个月,并且已将其发布为错误-您可以在下一个版本中获得它。 该项目经理一如既往地迟到了,这些家伙是如此激烈地咒骂,以至于他们已经改用垫子了,而且,如果工作室的主管没有受到诅咒并且不带他们去他的办公室,事情很快就会大吵大闹。 可能再次被罚款300美元。
当我离开集会室时,一名游戏设计师跑到我身边,高兴地说,每个人都喜欢原型,他们接受了它,现在这是我们接下来六个月的新项目。 当我们走向桌子时,他热情地告诉我们游戏中将会有哪些新功能。 他发明了多少种不同的咒语,当然,还有一个既可以战斗又可以投掷魔法的圣骑士。 整个艺术家部门已经在开发新动画,而中国已经签署了一项协议,根据该协议,我们的游戏将在他们的市场上发行。 我无声地看了看我的原型代码,深思熟虑,突出显示了所有内容并将其删除。
我相信,随着时间的流逝,每个程序员根据他的经验,都会开始看到他可能遇到的明显问题。 特别是如果您与一个游戏设计师在团队中长期合作。 我们有很多新要求和功能。 而且,我们以前的“架构”显然无法处理。
当您在面试中被问到类似任务时,他们一定会设法抓住您的。 它们可以有许多不同的形式-鳄鱼,可以游泳和奔跑。 可以发射加农炮或机关枪等的坦克。 此类任务的最重要属性是您拥有一个可以执行多种不同操作的对象。 而且您的继承无法以任何方式处理,因为不可能同时从FlyingObject和SwimmingObject继承,并且不同的对象可以执行不同的操作。 在这一点上,我们放弃继承并继续进行合成:
class Character { abilities = []; addAbility (...abilities) { for (const a of abilities) { this.abilities.push(a); } return this; } getAbility (AbilityClass) { for (const a of this.abilities) { if (a instanceof AbilityClass) { return a; } } return null; } }
现在,每个可能的动作都是一个单独的类,具有各自的状态,并且如有必要,我们可以通过向其抛出所需数量的能力来创建独特的角色。 例如,创建不朽的魔法树非常容易:
createMagicTree () { return new Character().addAbility( new SpellCastAbility() ); }
我们失去了继承,取而代之的是合成。 现在我们创建一个角色并列出他可能的能力。 但这并不意味着继承总是不好的,只是在这种情况下不适合。 理解继承是否正确的最好方法是亲自回答问题,它代表什么关系。 如果此连接为“是”,则表明您是MeleeFightAbility是一项能力,那么它是完美的。 如果仅由于要添加操作并显示“ has-a”而创建了连接,则应考虑组成。
我很高兴看到一个很好的结果。 它运行灵巧,没有错误,梦想中的体系结构! 我确信它会经过多个时间的考验,而且很长一段时间我们将不必重写它。 我对自己的代码非常热情,以至于甚至都没有注意到June Petya是如何接近我的。
街道已经很暗了,这使他更加高兴地发光。 显然,他设法推迟了任务,并在同事的指导下摆脱了垫子的惩罚,这是上周宣布的。
他迅速说道:“艺术家们只绘制了神圣的动画,我迫不及待地想把它们搞砸了。” 当使用治疗咒语时,飞行道具尤为华丽。 它们是如此绿色,如此加油!
我对自己诅咒,因为我完全忘记了我们仍然必须扭转局面。 该死的,似乎不得不重写架构。
在此类文章中,通常只描述与模型一起使用的模型,因为它是抽象的且成年的,并且无论将要使用哪种架构,您都可以在“六月”展示“图片”。 尽管如此,我们的模型应该为视图提供最大的信息,以便它可以完成其工作。 在GameDev中,通常使用“ Team”模式。 简而言之-我们有一个没有逻辑的状态,任何变化都应该在相应的团队中发生。 这看起来很复杂,但它具有许多优点:
-当一个团队呼叫另一个团队时,它们结合得很好
-实际上,每个命令在执行时都是您可以订阅的事件
-我们可以轻松地序列化它们。
例如,损坏命令可能看起来像这样。 然后,战士将在用剑击中时使用它,而魔术师在被火咒击中时将使用魔术师。 现在,为简单起见,我通过异常实现了命令验证,但随后可以将它们重写为返回码。
class DealDamageCommand extends Command { constructor (target, damage) { this.target = target; this.damage = damage; } execute () { const healthAbility = this.target.getAbility(HealthAbility); if (healthAbility == null) { throw new Error('NoHealthAbility'); } const resultHealth = healthAbility.health - this.damage; healthAbility.health = Math.max( 0, resultHealth ); } }
我喜欢制作分层命令-执行一个命令时,它
会生出许多孩子,然后由引擎执行。 因此,既然我们有能力造成伤害,我们可以尝试实施近战打击
class MeleeHitCommand extends Command { constructor (source, target, damage) { this.source = source; this.target = target; this.damage = damage; } execute () { const fightAbility = this.source.getAbility(MeleeFightAbility); if (fightAbility == null) { throw new Error('NoFightAbility'); } this.addChildren([ new DealDamageCommand(this.target, fightAbility.power); ]); } }
这两个团队拥有制作动画所需的一切。 渲染器可以简单地订阅事件并使用以下代码显示艺术家所需的一切:
async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); }
我输了数,连续一次我一直呆到天黑。 从小开始,游戏的发展就吸引了我,在我看来,这是一种神奇的事物,即使现在,当我从事多年时,我也对此感到焦虑。 尽管我了解到有关它们如何创建的秘密-我并没有对魔术失去信心。 这种魔力使我在夜间充满灵感,并编写了代码。 瓦西亚向我走来。 他绝对不知道如何编程,但与我分享游戏的态度。
-在这里-游戏设计师在我面前摆了200张A4纸上印制的Talmud。 尽管设计文件是合在一起进行的,但我们还是喜欢在重要的阶段进行打印,以便在实际的实施中感觉到这项工作。 我在随机页面上打开它,并找到了法师和圣骑士可以做的各种各样的咒语的清单,包括它们的效果,智力要求,法力价格以及艺术家如何展示它们的大致描述。 工作了很多个月,因为今天我将继续工作。
我们的体系结构使创建咒语的复杂组合变得容易。 只是每个咒语都可以返回施法期间需要执行的命令列表
class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility); if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } this.addChildren(new PayManaCommand(this.source, this.spell.manaCost)); this.addChildren(this.spell.getCommands(this.source, this.target)); } } class Spell { manaCost = 0; getCommands (source, target) { return []; } } class DamageSpell extends Spell { manaCost = 3; constructor (damageValue) { this.damageValue = damageValue; } getCommands (source, target) { return [ new DealDamageCommand(target, this.damageValue) ]; } } class HealSpell extends Spell { manaCost = 2; constructor (healValue) { this.healValue = healValue; } getCommands (source, target) { return [ new HealDamageCommand(target, this.healValue) ]; } } class VampireSpell extends Spell { manaCost = 5; constructor (value) { this.value = value; } getCommands (source, target) { return [ new DealDamageCommand(target, this.value), new HealDamageCommand(source, this.value) ]; } }
一年半后
像以往一样,站立式会议继续进行。 我坐在椅子上摇晃,挂在笔记本电脑上,而中间的佩蒂娅(Middle Petya)与测试人员争论了已经开始的错误。 出于诚意,他试图说服测试人员,在我们的新游戏中通过鼠标右键缺乏控制不应被标记为错误,因为这样的任务从未实现,游戏设计师或小丑也不曾解决。 我有种似曾相识的感觉,但是不和谐的新讯息分散了我的注意力:
-听-游戏设计师写道-我有个好主意...