¿Qué diseños hay para un sistema de entidad basado en componentes que sea fácil de usar pero flexible?

23

He estado interesado en el sistema de entidades basado en componentes por un tiempo, y leí innumerables artículos sobre él (los juegos Insomiac , el bastante estándar Evolve Your Hierarchy , T-Machine , Chronoclast ... solo por nombrar algunos).

Todos parecen tener una estructura en el exterior de algo como:

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

Y si trae la idea de datos compartidos (este es el mejor diseño que he visto hasta ahora, en términos de no duplicar datos en todas partes)

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

Sí, esto es muy eficiente. Sin embargo, no es exactamente el más fácil de leer o escribir; se siente muy torpe y trabajando contra ti.

Personalmente me gustaría hacer algo como:

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

Aunque, por supuesto, la única forma de llegar a ese tipo de diseño es de vuelta en el tipo de jerarquía Entity-> NPC-> Enemy-> FlyingEnemy-> FlyingEnemyWithAHatOn que este diseño intenta evitar.

¿Alguien ha visto un diseño para este tipo de sistema de componentes que aún sea flexible, pero que mantenga un nivel de facilidad de uso? Y para el caso, ¿logra sortear el almacenamiento de datos (probablemente el problema más difícil) de una buena manera?

¿Qué diseños hay para un sistema de entidad basado en componentes que sea fácil de usar pero flexible?

El pato comunista
fuente

Respuestas:

13

Una de las cosas que hace Unity es proporcionar algunos accesos de ayuda en el objeto del juego principal para proporcionar un acceso más fácil de usar a los componentes comunes.

Por ejemplo, puede tener su posición almacenada en un componente Transformar. Usando su ejemplo, tendría que escribir algo como

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

Pero en Unity, eso se simplifica a

e.transform.position = ....;

Donde transformes literalmente un simple método auxiliar en la GameObjectclase base ( Entityclase en su caso) que hace

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

Unity también hace algunas otras cosas, como establecer una propiedad "Nombre" en el objeto del juego en lugar de en sus componentes secundarios.

Personalmente, no me gusta la idea de su diseño de datos compartidos por nombre. Acceder a las propiedades por nombre en lugar de por una variable y hacer que el usuario también tenga que saber de qué tipo es, me parece muy propenso a errores. Lo que hace Unity es que usan métodos similares como la GameObject transformpropiedad dentro de las Componentclases para acceder a los componentes hermanos. Entonces, cualquier componente arbitrario que escriba simplemente puede hacer esto:

var myPos = this.transform.position;

Para acceder al puesto. Donde esa transformpropiedad hace algo como esto

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

Claro, es un poco más detallado que solo decir e.position = whatever, pero te acostumbras y no parece tan desagradable como las propiedades genéricas. Y sí, tendría que hacerlo de la manera indirecta para los componentes de su cliente, pero la idea es que todos sus componentes comunes de "motor" (renderizadores, colisionadores, fuentes de audio, transformaciones, etc.) tengan los accesos fáciles.

Tétrada
fuente
Puedo ver por qué el sistema de datos compartidos por nombre puede ser un problema, pero ¿cómo podría evitar el problema de que múltiples componentes necesiten datos (es decir, en cuál guardo algo)? ¿Solo ser muy cuidadoso en la etapa de diseño?
El pato comunista
Además, en términos de 'tener que el usuario también tenga que saber de qué tipo es', ¿no podría simplemente convertirlo en property.GetType () en el método get ()?
El pato comunista
1
¿Qué tipo de datos globales (en el ámbito de la entidad) está enviando a estos repositorios de datos compartidos? Se debe poder acceder fácilmente a cualquier tipo de datos de objetos de juego a través de sus clases de "motor" (transformación, colisionadores, renderizadores, etc.). Si está haciendo cambios en la lógica del juego basados ​​en estos "datos compartidos", es mucho más seguro tener una clase propia y asegurarse de que ese componente esté en ese objeto del juego que simplemente preguntar ciegamente para ver si existe alguna variable con nombre. Entonces sí, debe manejar eso en la fase de diseño. Siempre puede crear un componente que solo almacene datos si realmente lo necesita.
Tetrad
@Tetrad: ¿Puede explicar brevemente cómo funcionaría su método GetComponent <Transform> propuesto? ¿Recorrería todos los componentes de GameObject y devolvería el primer componente de tipo T? ¿O está pasando algo más allí?
Michael
@Michael eso funcionaría. Podría hacer que la persona que llama tenga la responsabilidad de almacenarlo en caché localmente como una mejora del rendimiento.
Tetrad
3

Sugeriría algún tipo de clase de interfaz para sus objetos de entidad sería bueno. Podría hacer el manejo de errores con la verificación para asegurarse de que una entidad contenga un componente del valor apropiado también en una ubicación para que no tenga que hacerlo en todas partes donde acceda a los valores. Alternativamente, la mayoría de los diseños que he hecho con un sistema basado en componentes, trato directamente con los componentes, solicitando, por ejemplo, su componente posicional y luego accediendo / actualizando las propiedades de ese componente directamente.

Muy básico en un nivel alto, la clase abarca la entidad en cuestión y proporciona una interfaz fácil de usar para las partes componentes subyacentes de la siguiente manera:

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;
        }
    }
}
James
fuente
Si asumo que solo tendría que manejar NoPositionComponentException o algo así, ¿cuál es la ventaja de esto sobre solo poner todo eso en la clase Entity?
El pato comunista
No estoy seguro de qué contiene realmente su clase de entidad. No puedo decir que haya una ventaja o no. Personalmente, defiendo los sistemas de componentes donde no hay una entidad base por decir simplemente para evitar la pregunta de 'Bueno, ¿qué le pertenece?' Sin embargo, consideraría una clase de interfaz como esta un buen argumento para que exista.
James
Puedo ver cómo esto es más fácil (no tengo que consultar la posición a través de GetProperty), pero no veo la ventaja de esta manera en lugar de colocarlo en la clase Entity.
El pato comunista
2

En Python puede interceptar la parte 'setPosition' a e.SetPosition(4,5,6)través de declarar una __getattr__función en Entity. Esta función puede recorrer en iteración los componentes y encontrar el método o propiedad apropiados y devolverlos, de modo que la llamada a la función o la asignación vayan al lugar correcto. Tal vez C # tenga un sistema similar, pero podría no ser posible debido a que está tipado estáticamente; presumiblemente no puede compilarse a e.setPositionmenos que e haya establecido la posición en la interfaz en alguna parte.

También podría hacer que todas sus entidades implementen las interfaces relevantes para todos los componentes que pueda agregarles, con métodos de código auxiliar que generan una excepción. Luego, cuando llama a addComponent, simplemente redirige las funciones de la entidad para la interfaz de ese componente al componente. Aunque un poco complicado.

Pero quizás más fácil sería sobrecargar el operador [] en la clase de entidad para buscar los componentes unidos por la presencia de una propiedad válida y volver que, por lo que se puede asignar a este modo: e["Name"] = "blah". Cada componente deberá implementar su propio GetPropertyByName y la Entidad llama a cada uno por turnos hasta que encuentre al componente responsable de la propiedad en cuestión.

Kylotan
fuente
Pensé en usar indexadores ... esto es realmente agradable. Esperaré un poco para ver qué más aparece, pero me estoy desviando hacia una combinación de esto, el GetComponent habitual (), y algunas propiedades como @James sugeridas para componentes comunes.
El pato comunista
Puede que me esté perdiendo lo que se dice aquí, pero esto parece más un reemplazo para e.GetProperty <type> ("NameOfProperty"). Lo que sea con e ["NameOfProperty"].
James
@James Es un envoltorio para él (al igual que su diseño) ... excepto que es más dinámico: lo hace de forma automática y más limpia que el GetPropertymétodo ... Lo único negativo es la manipulación y comparación de cadenas. Pero ese es un punto discutible.
El pato comunista
Ah, mi malentendido allí. Asumí que GetProperty era parte de la entidad (y así haría lo que se describió anteriormente) y no el método C #.
James
2

Para ampliar la respuesta de Kylotan, si está utilizando C # 4.0, puede escribir estáticamente una variable para que sea dinámica .

Si hereda de System.Dynamic.DynamicObject, puede anular TryGetMember y TrySetMember (entre los muchos métodos virtuales) para interceptar nombres de componentes y devolver el componente solicitado.

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

Escribí un poco sobre los sistemas de entidades a lo largo de los años, pero supongo que no puedo competir con los gigantes. Algunas notas de ES (algo desactualizadas y no reflejan necesariamente mi comprensión actual de los sistemas de entidades, pero vale la pena leer, en mi humilde opinión).

Raine
fuente
Gracias por eso, ayudará :) ¿No tendría la forma dinámica el mismo tipo de efecto que simplemente enviar a la propiedad. Sin embargo, GetType ()?
El pato comunista
Habrá un lanzamiento en algún momento, ya que TryGetMember tiene un objectparámetro out , pero todo esto se resolverá en tiempo de ejecución. Creo que es una forma glorificada de mantener una interfaz fluida sobre, por ejemplo, la entidad. TryGetComponent (compName, fuera del componente). Sin embargo, no estoy seguro de haber entendido la pregunta :)
Raine
La pregunta es que podría usar tipos dinámicos en todas partes, o (AFAICS) devolviendo la propiedad (property.GetType ()).
El pato comunista
1

Veo mucho interés aquí en ES en C #. Echa un vistazo a mi puerto de la excelente implementación de ES Artemis:

https://github.com/thelinuxlich/artemis_CSharp

Además, un juego de ejemplo para que comiences cómo funciona (usando XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

¡Sugerencias son bienvenidas!

thelinuxlich
fuente
Ciertamente se ve bien. Personalmente, no soy fanático de la idea de tantos EntityProcessingSystems: en código, ¿cómo funciona eso en términos de uso?
El pato comunista
Cada sistema es un aspecto que procesa sus componentes dependientes. Vea esto para tener la idea: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich
0

Decidí usar el excelente sistema de reflexión de C #. Lo basé en el sistema de componentes generales, más una mezcla de las respuestas aquí.

Los componentes se agregan muy fácilmente:

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

Y se puede consultar por nombre, tipo o (si son comunes, como Salud y Posición) por propiedades:

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

Y eliminado:

e.RemoveComponent<HealthComponent>();

A los datos se accede nuevamente por propiedades:

e.MaxHP

Que se almacena en el lado del componente; menos gastos generales si no tiene 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;
}

Intellisense en VS ayuda a la gran cantidad de tipeo, pero en su mayor parte hay poco tipeo, lo que estaba buscando.

Detrás de escena, estoy almacenando Dictionary<Type, Component>y Componentsanulando el ToStringmétodo para verificar nombres. Mi diseño original usaba principalmente cadenas para nombres y cosas, pero se convirtió en un cerdo con el que trabajar (oh, mira, también tengo que lanzar en todas partes la falta de propiedades de fácil acceso).

El pato comunista
fuente