当前位置: 华文问答 > 游戏

如何评价【守望先锋】架构设计?

2018-04-02游戏

如果你看不明白原帖中的ECS(似乎很多人有这个反应),我觉得其实是很正常的,尤其是从开始写代码到今天只接触过OOP的人,而OW本身是一个很宏大的项目,你没法很好的去理解这个例子,假如你真的有兴趣了解ECS,那么我给你们举个很简单的例子,以及说一下ECS的基本Motif(调性):

首先是规则区别,我还是要重复一个Key(我记得似乎在别的帖子说过):

1,OOP的核心思想是:我是什么——我是一个角色对象,我是一个子弹对象。

2,ECS的核心思想是:我有什么——我有render,我有move,我有motion。

就拿子弹和角色来说,如果我们是一个很简单的射击游戏,或者是Top-Down-Shooter的,或者更简单直白的,FC上的坦克相信都知道吧?

如果用OOP的方式做,它里面有角色(坦克)对象和子弹对象,伪代码

class tank {

//这里仅说一些有代表性的,就不细节设计了

public tank()

public Vector2 move(direction) //有一个move方法来移动坦克,在这个方法里面,我们根据面向、速度等因素来让坦克的坐标发生位移,同时我们让坦克的动作发生了变化。注意:我们把坦克的渲染其实也放在了这里,所谓的渲染包括了移动时候播放的动画等。

public bullet fire() //我们通过这个来让坦克开火,产生子弹

public void kill() //我们通过这个来让坦克在HP小于等于0的时候死亡。

}

class bullet {

public bullet(){}

public Vector2 move(direction) //子弹也会根据方向移动,这毫无疑问。注意,我们把子弹的渲染也放在了这里,所谓渲染包括了子弹移动的时候贴出他的位置。

public void hit() //子弹命中了什么东西之后要调用这个,用来消除子弹。

}

我相信不用写game,大多人也知道这个game要怎么写,毕竟这么简单的游戏。


那么我们来看看ECS是怎么做这个的?伪代码

moveComponent{

enum direction //移动方向,不做进一步解释了

float moveSpeed //移动速度,我想你也明白

}

positionComponent{

float x; //位于地图的坐标,不解释了

float y;

}

function moveSystem(entities){ //这个system只关心带有positionComponent和moveComponent的entity,所以entities里面的所有entity都必然有个moveComponent和positionComponent

for (entity in entities){

这里就不详细写了,执行的事情就是根据moveComponent中的direction和moveSpeed,为positionComponent中的x,y重新赋值。

}

}

renderComponent{

string resource; //贴图的信息,可以是资源名,等等,老做游戏了你应该理解这个

}

function renderSystem(entities){

//这个系统的工作,就是根据自己关心的带有positionComponent和renderComponent的对象,来进行渲染工作。

}

collisionComponent{

rect body; //FC的坦克大战中,所有的子弹和坦克,其实都是矩形的。

int side; //可以理解为阵营,同一阵营的互相忽略碰撞,当然写在这里面并不是最好的方案,具体要看需求,目前的需求下这还算凑合。

hitEntities: array<entity>; //用于记录碰撞到的entity

}

tankComponent{

//证明这是一个tank,并且记录坦克需要的数据

int hp

}

bulletComponent{

//证明这是一个子弹,并记录子弹需要的信息

int power

bool hitSomeThing

}

function collisionSystem(entities){

//这个system关心的是collisionComponent和positionComponent,他的工作就是根据position和collisionComponent.rect来判断2个entity是否碰撞,如果碰撞,则将彼此加入到对方的collisionComponent.hitEntities数组中去。

}

function damageSystem(entities){

//这是一个很有意思的system,他只关心带有collisionComponent,然后会根据collisionComponent.hitEntities中的entity提供的信息配合被捕捉的entity的其他component(比如tankComponent和bulletComponent)进行特殊处理。

//这个system的工作之一:如果这个entity并没有tankComponent,同时还没有bulletComponent,for循环执行continue,检查下一个entity。

//如果这个entity拥有tankComponent,证明这是坦克,则遍历collisionComponent.hitEntities,根据其中带有bulletComponent的entity进行处理,你可以同时销毁他们全部,并且都对这个坦克进行伤害,也可以根据被认为是子弹(带有bulletComponent的entity)的个数特殊处理,比如被3个子弹命中则hp提高10000等,根据设计需求来实现。

//值得注意的是:即使entity.tankComponent.hp<=0,也不由我来进行下一步操作,我就是一个damageSystem,我负责的就是damage(造成伤害),造成伤害的后果,管我鸟事儿。

}

function tankDestroySystem(entities){

//我只关心带有tankComponent的entity,我的任务是如果entity.tankComponent.hp<=0,则将他们杀死。

}

function bulletDestroySystem(entities){

//和tankDestroySystem差不多,但我只负责对bulletComponent.hitSomeThing==true的对象进行dispose()

}

这就是一个典型的ECS的游戏写法,他没有坦克对象,子弹对象,但是他有坦克和子弹

tankEntity = entities.new()

.add(new positionComponent(x,y))

.add(new renderComponent(xxxx))

.add(new collisionComponent(rect(x,x,x,x), y)

.add(new tankComponent(x))

bulletEntity = entities.new()

.add(new positionComponent(x,y))

.add(new renderComponent(xxxx))

.add(new collisionComponent(rect(x,x,x,x), y)

.add(new bulletComponent(x))

你会发现,子弹和坦克唯一的区别就是一个是子弹,一个是坦克。你可能要问,为什么没有moveComponent?因为moveComponent只有在移动的时候才需要添加上去,当移动完毕,就可以把它移除掉,这对于效率来说,还算是一种优化。


到这里,我们是不是可以思考另外一个问题——在一个3岁小孩子眼里,画出来的羊和活着的羊有什么区别?区别就是:活着的羊会动,他有motionComponent和moveComponent。当然话题收回来,现在,我们发现游戏中还没有设计地形,对,没有地形玩什么?

OOP方式:

class ObstacleGrid{

//tilebased游戏,地图格当然不能没有,当然这种游戏需要的只是会阻挡的地形格子信息而已。

string resource; //贴图资源

bool breakable; //能否被子弹击穿

Vector2 position; //位置

.....

public void break() //被子弹销毁了

}

看起来加一个对象不复杂,但是当地图上有了这些obstacleGrids之后,你要在game中coding什么呢?很多,比如坦克移动的时候,你不能穿越他,再比如子弹命中他的时候……等等等等。


ECS方式,我们要做的事情很清晰:

wallComponent{

//证明我是阻挡地形,别在意名字的细节……

bool breakable;

}

function obstacleSystem(entities){

//这个system关心的,只有带有wallComponent和collisionComponent的entity,根据collisionComponent.hitEntities进行对应的处理,相信你动动脑子能想明白。

//这里有一个独特与oop的地方,如果我(墙壁entity)阻挡了坦克或者子弹,我要做的是对对方entity.remove(moveComponent),导致对方不能发生移动。

}

阻挡地形:

obstacleGrid = entities.new() .add(new positionComponent(x,y)) .add(new renderComponent(xxxx)) .add(new collisionComponent(rect(x,x,x,x), y) .add(new wallComponent(x))

看到这里,发现为啥他和子弹和坦克如此接近?是不是能突然冒出一个有意思的想法?如果策划说了,我的阻挡地形也会移动!是不是OOP很懵逼?但ECS就很好处理了?


以上是一个简单的思路上的例子,区别了ECS和OOP,ECS中你说一点耦合不存在可能吗?是可能的,system之间是否必须耦合(其实必须优先执行aSystem再执行bSystem就可以认为是耦合)取决于你的programming和对业务的理解能力。当然,我想说的是,请看


是不是符合Component中没有任何行为?no functions

是不是符合没有状态?任何system都不关心游戏状态

第三个、第四个因为业务简单就没有体现出来

最后一个是不是system之间互不关心?当然!


所以,请认真的去理解ECS的特性和好处,不要轻易的做出危险的类比!


算了,吐槽就去掉吧,还是讲讲道理。