游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

根据实体组件系统分析《炸弹人》的游戏机制

发布时间:2013-08-28 15:44:42 Tags:,,,,

作者:Philip Fortier

《炸弹人》系列是带有一整套有趣机制的简单游戏。通过在一些项目中使用ECS框架,我们将能够清楚如何使用这一模式去执行这些机制。

我会提供一款包含许多玩家对抗玩家核心机制的职业游戏,并着眼于ECS带给我们怎样的价值(以及哪里存在完善空间)。游戏基于许多其它方式使用了ECS,但是出于本篇文章的目的,我将只讨论核心的游戏机制。

为了能够清楚第进行解释,本文所提及的代码样本都将是伪代码。而如果你想要执行完整的C#,你可以着眼于样本本身。

同样的,我会使用粗体大写去提及组件。所以Bomb(粗体大写)便是指官方组件,而如果我是用bomb(细体小写),那便只是在说理念或代表该理念的实体。

让我们开始落实行动

在设计任何系统之前,我们必须理解你想要解决的问题范围。这便意味着列出各种游戏对象之间的所有互动。从那里,我们可以想出最合理的抽象进行使用。当然了,如果你的游戏仍出于开发阶段,或者你正尝试着去复制游戏,那么知道所有互动是不可能的,因为一开始你将会错失一些内容。ECS的一大优势便是能够根据需求做出改变,同时不会对其它代码造成太大影响。

关于《炸弹人》的PvP游戏玩法的总结如下。玩家种植炸弹去摧毁其他玩家并击碎砖块。被击碎的砖块将留下各种道具去增强你的弹药的破坏力,或影响玩家的速度。最后幸存下来的玩家将看到彻底的毁灭。

让我们立刻开始分析《炸弹人》的基本炸弹机制,并思考使用ECS模式去执行怎样的设计。

爆炸

explosion(from gamedev)

explosion(from gamedev)

当一个炸弹爆炸后,威力将波及不同方向。并会发生以下情况:

“硬模块”将阻碍爆炸路径

“软模块”将阻碍爆炸路径(通常),并会被摧毁;它们将随机呈现一种道具

未被触发的炸弹将爆炸

任何被炸到的玩家都将死去

bomb(from gamedev)

bomb(from gamedev)

这里存在两种理念:一个特殊对象将对这种爆炸做出何种反应(死亡,触发,碎裂),一个特殊对象将如何影响爆炸路径(阻止它或不阻止它)。

我们可以创造一个ExplosionImpact组件去描写这一内容。对象只能通过一些方法去影响爆炸路径,但是它们会基于多少种方式做出回应?在组件中描述对象对爆炸做出反应的无数方式是没有意义的,所以我们会在每个对象上留下定制信息处理程序。ExplosionImpact应该如下:

enum ExplosionBarrier
{
None = 0,
Soft = 1,
Hard = 2,
}
class ExplosionImpact : Component
{
ExplosionBarrier Barrier;
}

这真的很简单。接下来让我们着眼于爆炸的内在属性。这主要取决于炸弹的类型。但是我们必须区分炸弹和爆炸,因为炸弹拥有像定时器和拥有者这样的属性。

爆炸可以:

延伸到任何或所有的8个方向

有时候会延伸至软砖块(传递炸弹,产生蓝色的爆炸)

不同的延伸范围(或无限范围)

有时候会延伸至硬砖块

power bomb(from gamedev)

power bomb(from gamedev)

存在两种明显的方法去塑造爆炸。对于爆炸你可以设置一个单一实体,或者面向每个爆炸组件设置一个实体(《炸弹人》是基于网格机制,所以后者是可行的)。考虑到爆炸将基于撞击物而发生不同的延伸,我们将很难以一个单一实体进行呈现。似乎基于每次爆炸平方行的实体将更合理。注:这也许会让我们很难在屏幕上渲染一次爆炸的统一图像,但是如果你的爆炸是单一实体的话你也会遇到这一问题。单一实体将需要一种复杂的方式去描述整体爆炸的形状。

dangerous bomb(from gamedev)

dangerous bomb(from gamedev)

所以让我们试试Explosion组件:

class Explosion : Component
{
bool IsPassThrough;         // Does it pass through soft blocks?
bool IsHardPassThrough;     // Does it pass through hard blocks?
int Range;                  // How much further will it propagate?
PropagationDirection PropagationDirection;
float Countdown;            // How long does it last?
float PropagationCountdown; // How long does it take to propagate to the next square?
}

enum PropagationDirection
{
None        = 0×00000000,
Left        = 0×00000001,
UpperLeft   = 0×00000002,
Up          = 0×00000004,
UpperRight  = 0×00000008,
Right       = 0×00000010,
BottomRight = 0×00000020,
Bottom      = 0×00000040,
BottomLeft  = 0×00000080,
NESW        = Left | Up | Right | Bottom,
All         = 0x000000ff,
}

当然了,因为我们正在使用ECS,所以其它组件将处理位置和爆炸图像等内容。

我已经在Explosion组件中添加了2个领域去支撑之前提到的内容。Countdown代表爆炸的持续事件。这并不是瞬间的—-它将持续较短时间,即在玩家走向死亡期间。我同样也添加了PropagationCountdown。在《炸弹人》中,爆炸将立即延伸。没有特殊原因,我决定做得与众不同。

所以这些是如何整合在一起?在ECS,系统将提供逻辑去控制组件。所以我们将拥有ExplosionSystem能够运行Explosion组件。你可以着眼于整体代码的样本项目,但让我们简单地列出它所包含的一些逻辑。首先,它将负责延伸爆炸。所以对于每个爆炸组件:

*更新Countdown和PropagationCountdown

*如果Countdown到达0,删除实体

*获得爆炸下的任何实体,并发送信息告诉它们自己正处于爆炸中

*如果PropagationCountdown到达0,在预想方向中创造全新的爆炸实体

ExplosionSystem同样也包含延伸逻辑。它需要寻找该形式下带有ExplosionImpact组件的任何实体。然后它便会比较ExplosionImpact的ExplosionBarrier与当前Explosion的属性((IsHardPassThrough等等)。当然,任何全新的Explosion将少一个范围。

道具

接下来我们将追踪玩家收集道具以及投下炸弹的路径。我使用了《炸弹人》中12个经典道具。与之前一样,让我们着眼于情节—-道具能够做什么,并想出设计方法。

Bomb–Up:按照玩家同时放置的炸弹数量而增加

Fire–Up:增加炸弹的爆炸半径

Speed-Up:提高玩家速度

(上述商店也拥有“Down”版本)

Full–Fire:炸弹拥有无限范围(除了与Dangerous–Bomb结合在一起时)

Dangerous Bomb:爆炸在平方形范围内扩展着,并通过硬砖块

Pass-Through Bomb:爆炸穿过软砖块

Remote Bomb:只有你触发炸弹时它们才会爆炸

Land Mine Bomb:你的第一个炸弹只会在有人走过时爆炸

Power Bomb:你的第一个炸弹拥有无限范围(就像Full–Fire,但仅限于第一个炸弹)

various powerups(from gamedev)

various powerups(from gamedev)

你将看到在大多数道具影响你放置的炸弹类型的同时,它们也会影响着其它元素,如玩家速度。所以道具是与炸弹不一样的理念。此外,道具也不是专属的,它们是相结合的。所以如果你拥有几个带有Dangerous Bomb的Fire–Up,你便拥有一个具有较大爆炸范围的炸弹。

pass-through bombs(from gamedev)

pass-through bombs(from gamedev)

所以从本质上来看,玩家拥有一组属性能够预示着它们将放置怎样的炸弹。道具将会改变这些属性。让我们着眼于PlayerInfo组件看起来会是怎样。记住,这并不包含位置,当前速度或纹理等信息。该信息存在于玩家实体中的其它组件。另一方面,PlayerInfo组件包含针对于游戏中玩家实体的信息。

class PlayerInfo : Component
{
int PlayerNumber;    // Some way to identify the player – this could also be a player name string
float MaxSpeed;
int PermittedSimultaneousBombs;
bool FirstBombInfinite;
bool FirstBombLandMine;
bool CanRemoteTrigger;
bool AreBombsPassThrough;
bool AreBombsHardPassThrough;
int BombRange;
PropagationDirection BombPropagationDirection;
}

当玩家丢下炸弹,我们便可以通过PlayerInfo组件去明确我们该掉落怎样类型的炸弹。这么做的逻辑有点复杂。这里存在许多条件句:举个例子来说,Land Mine炸弹看起来与朝着所有方向爆炸的Dangerous Bomb并不相同。所以当你拥有Land Mine同时也是Dangerous Bomb时,游戏使用了何种纹理?同样的,Power Bomb道具给予我们无限的BombRange,但是当炸弹超所有方向延伸时我们并不想要无限的范围。

这里存在一些非常复杂的逻辑。复杂性是源自《炸弹人》的属性,而不是代码所具有的任何问题。它是作为代码一个单独组件而存在着。你可以无需影响其它代码去改变逻辑。

我们也需要考虑当前的玩家激活了多少炸弹。我们需要设置上限,同时也需要为他们放置的第一个炸弹设置一些独特的属性。比起储存玩家当前的炸弹数,我们可以通过列举世界上所有的Bomb组件去估算到底有多少炸弹。这将避免在PlayerInfo组件中隐藏一个UndetonatedBombs价值。这也会减少不同步所引起的漏洞风险,并基于bomb–dropping逻辑所需要的信息去避免搞乱PlayerInfo组件。

考虑到这点,让我们着眼于谜题的最后组件:炸弹。

class Bomb : Component
{
float FuseCountdown;  // Set to float.Max if the player can remotely trigger bombs.
int OwnerId;          // Identifies the player entity that owns the bomb. Lets us count
// how many undetonated bombs a player has on-screen
int Range;
bool IsPassThrough;
bool IsHardPassThrough;
PropagationDirection PropagationDirection;
}

然后我们将拥有一个BombSystem能够更新所有Bomb的FuseCountdown。当一个Bomb的countdown到达0,它便会删除所拥有的实体并创造一个全新的爆炸实体。

在我的ECS执行中,系统也能够处理信息。BombSyestem处理了2种信息:一种是ExplosionSystem发送给爆炸下的实体(这将触发炸弹所以我们能够拥有连锁反应),另一种是玩家的输入处理器所发送的,用于远距离触发炸弹(遥控炸弹)。

你将注意到的是Explosion,Bomb和Player组件拥有一些共享元素:范围,延伸范围,IsPassThrough,IsHardPassThrough。这是否暗示着它们应该是相同的组件?并不是。运行着所有这3种类型组件的逻辑是不同的,所以我们可以将其分割。我们可以创造包含类似数据的BombState组件。所以一个爆炸实体便包含了一个Explosion组件和一个BombState组件。而这只会无缘无故地添加额外基础设置—-并不存在只能基于BombState组件运转的系统。

我所选择的解决方法便是设置BombState结构(并不是完整的组件),而Explosion,Bomb和PlayerInfo拥有该结构。就像Bomb如下:

struct BombState
{
bool IsPassThrough;
bool IsHardPassThrough;
int Range;
PropagationDirection PropagationDirection;
}

class Bomb : Component
{
float FuseCountdown;  // Set to float.Max if the player can remotely trigger bombs.
int OwnerId;          // Identifies the player entity that owns the bomb. Lets us count
// how many undetonated bombs a player has on-screen
BombState State;
}

关于玩家和炸弹还有另外一点需要注意的。当创造了炸弹时,它将拥有玩家当时放置的能力(游戏邦注:范围等等),而不是玩家的能力。我相信真正的《炸弹人》逻辑也是不同的:如果你获得一个Fire–Up道具,它将影响已经放置的炸弹。无论如何,这是我所做出的一个重要决定。

soft block(from gamedev)

soft block(from gamedev)

最后让我们来说说道具本身。他们看起来怎样?我拥有一些非常简单的PowerUp组件:

class PowerUp : Component
{
PowerUpType Type;
int SoundId;          // The sound to play when this is picked up
}

PowerUpType是不同类型道具的一种枚举类型。操纵PowerUp组件并控制它们的PowerUpSystem拥有一个较大的切换语句,即将操纵实体的PlayerInfo组件。

我考虑将不同的信息处理程序附加在每个道具预制件中(包含了特殊道具的定制逻辑)。这是最可扩展且最灵活的系统。我们甚至不需要一个PowerUp组件或PowerUpSystem。我们只需要简单地定义“玩家与我发生碰撞”信息便可,而定制道具信息处理器将获取该信息。这对于我来说似乎是过度设计的,所以我将选择一种更加简单且快速的执行方法。

以下是切换语句的一些片段,我们将在此基于道具分配玩家的能力:

case PowerUpType.BombDown:
player.PermittedSimultaneousBombs = Math.Max(1, player.PermittedSimultaneousBombs – 1);
break;

case PowerUpType.DangerousBomb:
player.BombState.PropagationDirection = PropagationDirection.All;
player.BombState.IsHardPassThrough = true;
break;

预制件

我的ECS允许你去构造实体模版或预制件。这将赋予模版一个名字(如“BombUpPowerUp”),并联合一群组件及其价值。我们可以让EnitityManager对“BombUpPowerUp”进行实例化,这将创造一个带有所有合适组件的实体。

我认为罗列出我在《炸弹人》的复制品中使用的一些预制件会很有帮助。我不会详细描述每个预制件的细节;我只会列出每种实体所使用的组件,并且它们还带有一些有用的评论。为了获得更多谢姐你可以着眼于源代码。这些只是预制件的例子——如在真正的游戏中存在多种砖块类型(软砖块,硬砖块),并且它们的组件还带有不同的价值。

"Player"
    Placement
    Aspect
    Physics
    InputMap           // controls what inputs control the player (Spacebar, game pad button, etc...)
    InputHandlers      // and how the player reacts to those inputs (DropBomb, MoveUp)
    ExplosionImpact
    MessageHandler
    PlayerInfo
"Brick"
    Placement
    Aspect
    Physics
    ExplosionImpact
    MessageHandler     // to which we attach behavior to spawn powerups when a brick is destroyed
"Bomb"
    Placement
    Aspect
    Physics
    ExplosionImpact
    MessageHandler
    ScriptContainer    // we attach a script that makes the bomb "wobble" 
    Bomb
"Explosion"
    Placement
    Aspect
    Explosion
    FrameAnimation     // this one lets us animation the explosion image
"PowerUp"
    Placement
    Aspect
    ExplosionImpact
    ScriptContainer
    PowerUp

有趣的点

ECS同样也会提供给你一些灵活性去创造全新实体类型。它让你能够轻松地说“嘿,如果我将这一元素与其它元素结合在一起会怎样?”这能够激发头脑风暴去想出全新的机制。如果你能够控制一个炸弹的话会怎样?(添加InputMap到炸弹实体)。如果爆炸将导致其他玩家放慢速度的话会怎样?(添加PowerUp到一个爆炸实体)。如果爆炸是稳定的会怎样?(添加Physics到一个爆炸实体)。如果玩家可以调转爆炸的方向并朝着某些人发射会怎样?(添加一些逻辑,但仍然是琐碎的)。

你将会发现这能让你轻松地实验并添加新代码,而无需阻碍其它内容。组件间的独立性是明确且最低标准。

当然,我在这个小项目中也面对了一些问题。

我决定使用Farseer Physics程序库去处理玩家和其它对象间的碰撞检测。游戏是基于网格机制,但是玩家可以在更细粒度的关卡上移动着。所以这是帮助我们轻松做到这点的简单方法。然而许多游戏玩法都是基于网格(游戏邦注:例如炸弹智能在完整的场所掉落)。所以我也拥有自己简单的网格碰撞检测(让你能够咨询“在这个网格中存在怎样的实体?”)。有时候这两种方法也会出现矛盾。尽管这一问题并不是针对于ECS。实际上,ECS会告诉我Farseer Physics的使用是完全局限于Collision System。所以它能够轻松地置换出物理程序库并且未影响到其它代码。Physics组件本身并不依赖于Farseer。

我所面对的另一个问题便是存在一种倾向去创造符合ECS思考方式的特定问题。一个例子便是整体的游戏需要一种状态:剩余时间,棋盘的大小以及其它全局状态。我最终停止创造GameState组件和一个伴随着的GameStateSystem。GameStateSystem负责呈现剩余时间,并决定谁最终赢得游戏。这似乎不适合整合到ECS框架中–它只对GameState对象有意义。不过这也具有一些优势,即能让我们更轻松地执行一种保存游戏机制。我的组件必须支持序列化。所以我便确保了所有实体为流线形,然后对其进行反序列化,并最终回到之前所在的位置。

对于我常面对的情况我的决定是:“我是否该为此创造一个新的组件类型,或只是在定制行为上附加一个脚本?”有时候这是一种较为抽象的决定,即关于一块逻辑是否值得拥有自己的组件或系统。一个组件或系统看起来较为重要,所以你必须拥有将定制行为附加到实体上的能力。尽管这将导致我们很难去了解整个系统。

现在我拥有3种方法能够将定制行为附加到一个实体上:输入处理器,信息处理器和脚本。脚本是执行于每个更新循环。输入和信息处理器会在输入行动和发送信息时被召唤出来。对于该项目我尝试过一种全新的输入处理方法。它发挥了很大的作用(在与信息处理结合在一起也具有意义)。我使用了键盘去控制玩家。当是时候执行控制器支持时,它总共只花费了5分钟。

 powerup entity(from gamedev)

powerup entity(from gamedev)

脚本通常需要储存状态。举个例子来说,能让道具动起来或炸弹实体摇晃的脚本需要知道最小和最大规模,以及我们处在行动进程的哪个点上。我可以创造带有状态的成熟脚本,并确保每个实体都需要它。但是这也将引起序列化问题(每个脚本将需要清楚如何对自身进行序列化)。所以在我当前的执行中,脚本是简单的回调函数。它们需要的状态被储存在Scripts组件的通有性质包中(Scripts组件只是储存一列能够映射特殊回调函数的id)。这会让C#脚本代码显得较为笨拙,而变量的获取和设置便是属性包中的方法调用。在某种情况下,我计划支持一个带有糖衣语法的简单定制脚本语言去隐藏一些难看的内容。但我现在还没做到那里。

总结

纵使理论很棒,但是我希望这篇文章能够通过呈现有关ECS机制的执行而带来有效的帮助。

附加的样本项目是在XNA 4.0中执行的。除了上文所描述的机制外,它还呈现了其它有趣的内容:

*我如何处理像爆炸这样组件的动画

*我在上文简要描述的输入映射系统

*我如何在特定网格位置上处理对象检索

*像炸弹是如何摇晃或地雷是上升/下降

我没有时间在样本中执行AI玩家,但的确存在三种人类可以控制的角色。其中两种是使用键盘进行控制:(箭头键,空格和输入)以及(方向键,Q和E)。第三种使用的是控制器(如果你有的话)。我们也能轻松地执行鼠标控制玩家的方法。

Bomberman(from gamedev.net)

Bomberman(from gamedev.net)

样本突出的是12个具有完整功能的道具(有些更强大的道具会更频繁地出现),随机软砖块和在时间快耗尽时出现的“死亡砖块”。当然了,我们也忽视了许多优化,如图像还很丑陋,并不存在死亡动画或玩家行走动画。但这是因为本篇文章的关注点在于游戏机制。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Case Study: Bomberman Mechanics in an Entity-Component-System

By Philip Fortier

The Bomberman series of games are simple games with an interesting set of mechanics. Having used an ECS framework in a few projects, I thought it would be useful to see how we can implement these mechanics using this pattern.

I won’t go into a detailed introduction of ECS here. For a great primer on the topic, have a look at Understanding Component-Entity-Systems.

I also provide a working game that contains the bulk of the PvP core mechanics, and look at what value ECS provided us (and where there is room for improvement). The game leverages ECS in lots of other ways, but for the purpose of this article I will only discuss the core game mechanics.

For the purposes of clarity and language-independence, code samples in the article will be a sort of pseudo-code. For the full C# implementation, see the sample itself.

Also, I use bold capitalized names to refer to components. So a Bomb refers to the official component, while if I mention a bomb I’m just talking about the concept or the entity that represents the concept.

Let’s get to work

Before designing any system, it’s necessary to understand the scope of the problem you’re trying to solve. This means listing out all the interactions between various game objects. From there, we can figure out the right abstractions to use. Of course, knowing all the interactions is impossible if your game is under development – or even if you’re trying to make a clone – because there will be things you missed at first. One of the advantages of ECS is that it lends itself to making changes as needed while affecting a minimum of other code.

A rough summary of the PvP Bomberman gameplay is as follows. Players plant bombs that destroy other players and disintegrate blocks. Disintegrated blocks leave behind various powerups that augment the destructive power of your bombs, or affect player speed. Players complete to see who is the last to survive as a clock counts down to total destruction.

Without further ado, let’s look into the basic bomb mechanics Bomberman and come up with a design we can implement using the ECS pattern.

Explosions

Explosion showing behavior with (1) hard blocks, (2) soft blocks, and (3) empty space

When a bomb explodes, the explosion shoots out in various directions. The following things can happen:

“Hard blocks” block the path of the explosion

“Soft blocks” block the path of the explosion (usually), and are destroyed; they randomly reveal a powerup

Un-triggered bombs will detonate

Any players hit by the explosion will die

One bomb’s explosion can trigger another bomb

It seems there are two concepts here: how a particular object reacts to the explosion (dies, triggers, disintegrates), and how a particular object affects the path of the explosion (blocks it, or doesn’t).

We can create an ExplosionImpact component to describe this. There are only a few ways an object can affect the path of the explosion, but many ways it can then react. It’s doesn’t make sense to describe the myriad ways an object can react to an explosion in a component, so we’ll leave that up to a custom message handler on each object. So ExplosionImpact might look like this:

That’s pretty simple. Next, let’s look at the innate properties of an explosion. This basically depends on the type of the bomb. But it’s useful to distinguish bombs vs explosions, since bombs have additional properties like a countdown timer and an owner.

Explosions can:

propagate any or all of 8 directions

sometimes propagate through soft blocks (pass-through bombs, which have blue explosions)

different propagation ranges (or an infinite range)

sometimes propagate through hard blocks!

There are two obvious ways to model an explosion. You could have a single entity for an explosion, or an entity for each piece of the explosion as it propagates out (Bomberman is grid-based, so the latter is feasible). Given that an explosion can propagate unevenly depending on what it hits (as previously discussed), it would be somewhat tricky to represent with a single entity. It seems then, that one entity per explosion square would be reasonable. Note: this may make it seem tricky to render a cohesive image of an explosion to the screen, but you’d actually have a similar problem if your explosion was a single entity. A single entity would still need a complicated way to describe the shape of the overall explosion.

So let’s take a stab at an Explosion component:

Of course, since we’re using ECS, things like position and the explosion image are handled by other components.

I’ve added two more fields to the Explosion component that bear some mentioning: Countdown represents how long an explosion lasts. It’s not an instant in time – it lasts a short duration, during which a player can die if they walk into it. I also added a PropagationCountdown. In Bomberman, from what I can tell, explosions propagate instantaneously. For no particular reason, I’ve decided differently.

So how does this all tie together? In an ECS, systems provide the logic to manipulate the components. So we’ll have an ExplosionSystem that operates over the Explosion components. You can look at the sample project for the entire code, but let’s briefly outline some of the logic it contains. First of all, it’s responsible for propagating explosions. So for each Explosion component:

Update Countdown and PropagationCountdown

If Countdown reaches zero, delete the entity

Get any entities underneath the explosion, and send them a message telling them they are in an explosion

If PropagationCountdown reaches zero, create new child explosion entities in the desired directions (see below)

ExplosionSystem also contains the propagation logic. It needs to look for any entities underneath it with an ExplosionImpact component. Then it compares the ExplosionImpact’s ExplosionBarrier with properties of the current Explosion (IsHardPassThrough, etc…) and decides if it can propagate and in what directions. Any new Explosions have one less Range, of course.

Powerups

Next, we’ll trace the path from collecting powerups to the player dropping bombs. I’ve used a subset of 12 of the typical Bomberman powerups (I’ve left out the ones that let you kick, punch and pick up bombs – I didn’t have time to implement them for this article, but it could be a good follow-up). As before, let’s look at our scenarios – what the powerups can do – and come up with a design.

Bomb-Up: Increase by one the number of simultaneous bombs the player can place

Fire-Up: Increase the blast radius (propagation range) of the bombs’ explosions

Speed-Up: Increase player speed

(the above three also have “Down” versions)

Full-Fire: Bombs have infinite range (except when combined with Dangerous-Bomb)

Dangerous Bomb: Blast expands in a square, and goes through hard blocks

Pass-Through Bomb: Blast propagates through soft blocks

Remote Bomb: Bombs only detonate when you trigger them

Land Mine Bomb: Your first bomb only detonates when someone walks over it

Power Bomb: Your first bomb has infinite range (like Full-Fire but only for the first bomb)

You’ll see that while most powerups affect the kinds of bombs you place, they can also affect other things like player speed. So powerups are different concepts than bombs. Furthermore, powerups are not exclusive, they combine. So if you have a couple of Fire-Ups with a Dangerous Bomb, you get a bomb that explodes in a bigger square.

So essentially the player has a set of attributes that indicate what kinds of bombs they can place. The powerups modify those attributes. Let’s take a stab at what a PlayerInfo component would look like. Keep in mind, this won’t contain information like position, current speed or texture. That information exists in other components attached to the player entity. The PlayerInfo component, on the other hand, contains information that is specific to the player entities in the game.

When a player drops a bomb, we look at its PlayerInfo component to see what kind of bomb we should drop. The logic to do so is a bit complicated. There are lots of conditionals: for instance, Land Mine bombs look different than Dangerous Bombs that explode in all directions. So when you have a Land Mine that’s also a Dangerous Bomb, what texture is used? Also, Power Bombs powerups give us infinite BombRange, but we don’t want an infinite range when the bomb propagates in all directions (or else everything on the board will be destroyed).

So there can be some fairly complex logic here. The complexity arises from the nature of the Bomberman rules though, and not from any problem with code. It exists as one isolated piece of code. You can make changes to the logic without breaking other code.

We also need to consider how many bombs the player currently has active (undetonated): we need to cap how many they place, and also apply some unique attributes to the first bomb they place. Instead of storing a player’s current undetonated bomb count, we can just calculate how many there are by enumerating through all Bomb components in the world. This avoids needing to cache an UndetonatedBombs value in the PlayerInfo component. This can reduce the risk of bugs caused by this getting out of sync, and avoids cluttering the PlayerInfo component with information that happens to be needed by our bomb-dropping logic.

With that in mind, let’s take a look at the final piece of our puzzle: the bombs.

Then we’ll have a BombSystem that is responsible for updating the FuseCountdown for all Bombs. When a Bomb’s countdown reaches zero, it deletes the owning entity and creates an new explosion entity.

In my ECS implementation, systems can also handle messages. The BombSystem handles two messages: one sent by the ExplosionSystem to entities underneath an explosion (which will trigger the bomb so we can have chain reactions), and one sent by the player’s input handler which is used for remotely triggering bombs (for remote control bombs).

One thing you’ll notice is that the Explosion, Bomb and Player components share a lot in common: range, propagation direction, IsPassThrough, IsHardPassThrough. Does this suggest that they should actually all be the same component? Not at all. The logic that operates over those three types of components is very different, so it makes sense to separate them. We could create a BombState component that contains the similar data. So an explosion entity would contain both an Explosion component and a BombState component. However, this just adds extra infrastructure for no reason – there is no system that would operate only over BombState components.

The solution I’ve chosen is just to have a BombState struct (not a full on Component), and Explosion, Bomb and PlayerInfo have this inside them. For instance, Bomb looks like:

One more note on players and bombs. When a bomb is created, it inherits the abilities of its player at the time it is placed (Range, etc…) instead of referencing the player abilities. I believe the actual Bomberman logic might be different: if you acquire a Fire-Up powerup, it affects already-placed bombs. At any rate, it was an explicit decision I made that I was felt was important to note.

Let’s finally talk about the powerups themselves. What do they look like? I have a very simple PowerUp component:

PowerUpType is just an enum of the different kinds of powerups. PowerUpSystem, which operates over PowerUp components and controls picking them up, just has a large switch statement that manipulates the PlayerInfo component of the entity that picked it up. Oh the horror!

I did consider having different message handlers attached to each powerup prefab which contained the custom logic for that particular powerup. That is the most extensible and flexible system. We wouldn’t even need a PowerUp component or PowerUpSystem. We’d simply define a “a player is colliding with me” message which would be fired and picked up by the custom powerup-specific message handler. This really seemed like over-architecting to me though, so I went with a simpler quicker-to-implement choice.

Here’s a little snippet of the switch statement where we assign the player capabilities based on the powerup:

Prefabs

My ECS allows you to construct entity templates, or prefabs. These assign a name to a template (e.g. “BombUpPowerUp”), and associate with it a bunch of Components and their values. We can tell our EntityManager to instantiate a “BombUpPowerUp”, and it will create an Entity with all the right Components.

I think it would be useful to list some of the prefabs I use for the Bomberman clone. I won’t go into details on the values used in each; I’ll simply list which Components each type of entity uses, with some comments where useful. You can look at the source code for more details. These are just examples of prefabs – e.g. in the actual game there are multiple types of Brick (SoftBrick, HardBrick) with different values in their components.

Interesting Points

An ECS also gives you great flexibility at creating new types of entities. It makes it easy to say “hey, what if I combine this with that?”. This can be good for brainstorming new kinds of mechanics. What if you could take control of a bomb? (Add InputMap to a bomb entity). What if explosions could cause other players to slow down? (Add PowerUp to an explosion entity). What if explosions were solid? (Add Physics to an explosion entity). What if a player could defect an explosion back towards someone? (A little bit of logic to add, but still pretty trivial).

You’ll find that it is very easy to experiment and add new code without breaking other things. The dependencies between components are (hopefully) clear and minimal. Each pieces of code operates on the absolute minimum it needs to know.

Of course, I also faced some problems in this little project.

I decided to use the Farseer Physics library to handle collision detection between the player and other objects. The game is grid-based, but the player can move on a much more granular level. So that was an easy way to get that behavior without having to do much work. However, a lot of the gameplay is grid-based (bombs can only be dropped at integer locations, for instance). So I also have my own very simple grid collision detection (which lets you query: “what entities are in this grid square?”). Sometimes these two methods came into conflict. This problem isn’t anything specific to ECS though. In fact, ECS ecnourages my usage of Farseer Physics to be entirely limited to my CollisionSystem (which operates over Physics components). So it would be very easy to swap out the physics library with another and not have it affect any other code. The Physics component itself has no dependency on Farseer.

Another problem I faced is that there is a tendency to make certain problems fit into the ECS way of thinking. One example is the state needed for the overall game: the time remaining, the size of the board, and other global state. I ended up creating a GameState component and an accompanying GameStateSystem. GameStateSystem is responsible for displaying the time remaining, and determining who won the game. It seems a bit awkward to cram it into the ECS framework – it only ever makes sense for there to be one GameState object. It does have some benefits though, as it makes it easier to implement a save game mechanic. My Components are required to support serialization. So I can serialize all entities to a stream, and then deserialize them and end up exactly back where I was.

One decision that I often faced was: “Do I make a new Component type for this, or just attach a script for custom behavior?” Sometimes it is a fairly arbitrary decision as to whether a piece of logic merits its own Component and System or not. A Component and System can feel a bit heavyweight, so it is definitely essential to have the ability to attach custom behavior to entities. This can make it harder to grok the whole system though.

I currently have 3 ways of attaching custom behavior to an entity: input handlers, message handlers and scripts. Scripts are executed every update cycle. Input and message handlers are invoked in response to input actions or sending messages. I was trying out a new input handler methodology for this project. It worked well (but it might make sense to combine it with message handling). I was using the keyboard to control the player. When it came time to implement gamepad support, it took all of five minutes. I was inspired by this article.

Scripts often need to store state. For instance, a script that makes a powerup wiggle, or a bomb entity wobble (by changing the Size property in its Aspect component) needs to know the minimum and maximum sizes, and what point we are in the wobble progression. I could make scripts full-fledged classes with state, and instantiate a new one each each entity that needs it. However, this causes problems with serialization (each script would need to know how to serialize itself). So in my current implementation, scripts are simply callback functions. The state they need is stored in a generic property bag in the Scripts component (the Scripts component simply stores a list of ids that are mapped to a specific callback function). This makes the C# script code a little cumbersome, as each get and set of a variable is a method call on the property bag. At some point, I plan to support a very simple custom scripting language with syntactic sugar to hide the ugliness. But I haven’t done that yet.

Conclusion

Theory is nice, but I hope this article helped with showing a practical implementation of some mechanics with ECS.

The sample project attached is implemented in XNA 4.0. In addition to the mechanics described in this article, it shows some other things which might be interesting:

How I handle animating the components like explosions

The input mapping system I briefly described above

How I handle querying objects at a particular grid location

How little things like the bomb wobble or land mine rise/fall is done

I didn’t have time to implement AI players in the sample, but there are 3 human-controllable characters. Two of them use the keyboard: (arrow keys, space bar and enter) and (WASD, Q and E). The third uses the gamepad, if you have that. It should be possible to implement a mouse-controlled player without too much work.

The sample features 12 full-functioning powerups (some of the more powerful ones appear too frequently though), random soft blocks, and the “death blocks” that start appearing when time is running out. Of course, a lot of polish is missing: the graphics are ugly, there is no death animation or player walking animation. But the main focus is on the gameplay mechanics.(source:gamedev)


上一篇:

下一篇: