用户友好但仍然灵活的基于组件的实体系统有哪些设计?


23

我对基于组件的实体系统感兴趣已有一段时间了,并阅读了无数文章(Insomiac游戏,相当标准的Evolve Your HierarchyT-MachineChronoclast ...等)。

它们似乎都在某种东西的外部具有结构:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

而且,如果您引入了共享数据的概念(这是到目前为止我所见过的最好的设计,因为这并不是在所有地方都复制数据)

e.GetProperty<string>("Name").Value = "blah";

是的,这非常有效。但是,它并不是最简单的读写方法。感觉很笨拙,而且对您不利。

我个人想做这样的事情:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

当然,获得这种设计的唯一方法是回到Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn这种设计试图避免的层次结构。

有没有人看到这种组件系统的设计既灵活又保持了用户友好性?就此而言,设法以一种很好的方式解决(可能是最困难的问题)数据存储?

用户友好但仍然灵活的基于组件的实体系统有哪些设计?

Answers:


13

Unity所做的一件事是在父游戏对象上提供一些辅助访问器,以提供对公共组件的更加用户友好的访问。

例如,您可能将自己的位置存储在Transform组件中。使用您的示例,您将必须编写类似

e.GetComponent<Transform>().position = new Vector3( whatever );

但是在Unity中,这简化为

e.transform.position = ....;

transform字面上看,哪里只是基GameObject类(Entity在您的情况下为类)中的简单辅助方法,

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

Unity还执行其他一些操作,例如在游戏对象本身而不是其子组件中设置“名称”属性。

我个人不喜欢您的“按名称共享数据”设计的想法。通过名称而不是通过变量来访问属性,并且让用户必须知道它是什么类型,这对我来说似乎真的很容易出错。Unity所做的是,他们使用与类中的GameObject transform属性类似的方法Component来访问同级组件。因此,您编写的任何任意组件都可以简单地做到这一点:

var myPos = this.transform.position;

访问位置。该transform属性在哪里做这样的事情

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

当然,这比说只是有些冗长e.position = whatever,但是您已经习惯了,它看起来不像泛型属性那么讨厌。是的,您将不得不为客户端组件采用一种绕行方式,但其想法是所有通用的“引擎”组件(渲染器,对撞机,音频源,变换等)都具有易于访问的访问器。


我可以理解为什么按名称共享数据系统可能是一个问题,但是我还能如何避免多个组件需要数据的问题(即,我将数据存储在哪个组件中)?在设计阶段只是非常小心?
共产主义鸭子

另外,就“让用户也必须知道它是什么类型”而言,我不能仅将其转换为get()方法中的property.GetType()吗?
共产党鸭子

1
您将哪种全局(在实体范围内)数据推送到这些共享数据存储库?任何类型的游戏对象数据都可以通过您的“引擎”类(变换,对撞机,渲染器等)轻松访问。如果您要基于此“共享数据”进行游戏逻辑更改,那么拥有一个类并真正确保该组件位于该游戏对象上比盲目地询问是否存在某些命名变量要安全得多。是的,您应该在设计阶段进行处理。如果确实需要,您总是可以制作一个只存储数据的组件。
四分

@Tetrad-您能否简要阐述所建议的GetComponent <Transform>方法如何工作?它将遍历所有GameObject的组件并返回类型T的第一个组件吗?还是那里正在发生其他事情?
2012年

@迈克尔会工作。您可以使调用者有责任在本地缓存该内容,以提高性能。
Tetrad 2012年

3

我建议您的Entity对象使用某种Interface类会很好。它可以通过检查以确保实体在某个位置也包含适当值的组件来进行错误处理,因此您不必在访问值的任何地方都进行处理。或者,我曾经使用基于组件的系统完成过的大多数设计,我直接处理这些组件,例如,请求其位置组件,然后直接访问/更新该组件的属性。

该类在高层次上非常基础,它接受所涉及的实体,并为基础组件提供易于使用的界面,如下所示:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

如果我假设只需要处理NoPositionComponentException之类的东西,那么与仅将所有内容都放入Entity类相比,这样做的好处是什么?
共产主义鸭子

不确定您的实体类实际上包含什么,我不能说有没有优势。我个人提倡组件系统,其中每个组件都没有基本实体,只是为了避免出现“其中包含什么”的问题。但是,我认为这样的接口类是现有的一个很好的论据。
詹姆斯

我可以看到这是多么容易(我不必通过GetProperty查询位置),但是我没有看到这种方法的优点,而不是将其放在Entity类本身中。
共产主义鸭子

2

在Python中,您可以e.SetPosition(4,5,6)通过__getattr__在Entity 上声明一个函数来拦截'setPosition'部分。该函数可以遍历组件,找到合适的方法或属性并将其返回,以便函数调用或赋值到达正确的位置。也许C#具有类似的系统,但是由于它是静态类型的,因此可能无法实现-可能无法编译e.setPosition除非e在接口的某个位置具有setPosition,否则。

您还可以使用存根方法引发异常,使所有实体为可能添加到它们的所有组件实现相关的接口。然后,当您调用addComponent时,只需将该组件接口的实体功能重定向到该组件。有点不客气。

但也许最简单的将是超载[]运营商对你的实体类搜索的附加组件的有效属性的存在,并返回,因此它可以被分配给像这样:e["Name"] = "blah"。每个组件都需要实现自己的GetPropertyByName,并且Entity依次调用每个组件,直到找到负责所讨论属性的组件。


我曾想过使用索引器..这真的很好。我将稍等一下,看看还会出现什么,但我正在将其与通常的GetComponent()以及一些建议使用的属性(例如@James)推荐用于常见组件。
共产主义鸭子

我可能会错过这里所说的内容,但这似乎更替代了e.GetProperty <type>(“ NameOfProperty”)。无论使用e [“ NameOfProperty”]。
詹姆斯

@James是它的包装器(非常类似于您的设计)。除了它更具动态性-它比方法更自动,更干净GetProperty。.唯一的缺点是字符串操作和比较。但这是有争议的。
共产主义鸭子

啊,我的误会在那里。我假设GetProperty是实体的一部分(因此将执行上述操作),而不是C#方法。
詹姆斯

2

要扩展Kylotan的答案,如果您使用的是C#4.0,则可以静态地将变量键入为dynamic

如果从System.Dynamic.DynamicObject继承,则可以重写TryGetMember和TrySetMember(在许多虚拟方法中)来拦截组件名称并返回所请求的组件。

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

这些年来,我写了一些有关实体系统的文章,但我想我无法与巨头竞争。ES的一些注释(有些过时,不一定反映我对实体系统的当前理解,但是值得一读,恕我直言)。


谢谢您的帮助,):)动态方式不会像强制转换为property.GetType()一样具有相同的效果吗?
共产党鸭子

由于TryGetMember具有out object参数,因此有时会进行强制转换-但所有这些都将在运行时解决。我认为这是一种保持流利的接口(例如,entity.TryGetComponent(compName,out component))的荣耀方式。我不确定我是否确实理解了这个问题,但是:)
Raine

问题是我可以在任何地方使用动态类型,也可以使用(AFAICS)返回(property.GetType())属性。
共产主义鸭子

1

我在C#的ES上看到了很多兴趣。查看我出色的ES实施Artemis的移植:

https://github.com/thelinuxlich/artemis_CSharp

另外,一个示例游戏可让您入门(使用XNA 4)如何工作:https : //github.com/thelinuxlich/starwarrior_CSharp

欢迎提出建议!


当然看起来不错。我个人不喜欢这么多EntityProcessingSystems的想法-在代码中,从使用角度来看如何实现?
共产党鸭子

每个系统都是处理其相关组件的方面。看到这个想法:github.com/thelinuxlich/starwarrior_CSharp/tree/master/…– thelinuxlich 2011
6

0

我决定使用C#的出色反射系统。我将其基于通用组件系统,再加上此处答案的混合体。

组件很容易添加:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

可以按名称,类型或(如果使用诸如“健康”和“位置”之类的常见属性)属性来查询:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

并删除:

e.RemoveComponent<HealthComponent>();

同样,属性通常也可以访问数据:

e.MaxHP

存储在组件端;如果没有HP,则开销会更少:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

VS中的Intellisense可以帮助您进行大量键入操作,但是在大多数情况下,几乎没有键入操作-这正是我一直在寻找的东西。

在幕后,我存储了一个Dictionary<Type, Component>Components覆盖了ToString用于名称检查的方法。我最初的设计主要使用字符串来表示名称和事物,但后来却变成了可以使用的猪(哦,瞧,我必须在各处进行转换,而且缺少易于访问的属性)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.