Administrar múltiples referencias de la misma entidad de juego en diferentes lugares usando ID

8

He visto excelentes preguntas sobre temas similares, pero ninguna que aborda este método en particular:

Dado que tengo varias colecciones de entidades de juego en mi juego [XNA Game Studio], con muchas entidades que pertenecen a varias listas, estoy considerando formas de hacer un seguimiento de cada entidad que se destruye y eliminarla de las listas a las que pertenece .

Muchos métodos potenciales parecen descuidados / complicados, pero recuerdo una forma que he visto antes en la que, en lugar de tener múltiples colecciones de entidades de juego, tienes colecciones de ID de entidades de juego . Estas ID se asignan a entidades de juego a través de una "base de datos" central (quizás solo una tabla hash).

Entonces, cada vez que un fragmento de código quiere acceder a los miembros de una entidad de juego, primero verifica si aún está en la base de datos. Si no, puede reaccionar en consecuencia.

¿Es este un enfoque sólido? Parece que eliminaría muchos de los riesgos / molestias de almacenar múltiples listas, con el compromiso de ser el costo de la búsqueda cada vez que desea acceder a un objeto.

vargonian
fuente

Respuestas:

3

Respuesta corta: sí.

Respuesta larga: en realidad, todos los juegos que he visto funcionaron de esta manera. La búsqueda de tabla hash es lo suficientemente rápida; y si no le importan los valores de ID reales (es decir, no se usan en ningún otro lugar), puede usar un vector en lugar de una tabla hash. Lo cual es aún más rápido.

Como beneficio adicional, esta configuración hace posible reutilizar los objetos del juego, reduciendo la tasa de asignación de memoria. Cuando se destruye un objeto del juego, en lugar de "liberarlo", simplemente limpia sus campos y coloca un "grupo de objetos". Luego, cuando necesite un nuevo objeto, simplemente tome uno existente del grupo. Si aparecen constantemente nuevos objetos, esto puede mejorar notablemente el rendimiento.

No importa
fuente
En general, eso es lo suficientemente preciso, pero primero debemos tener en cuenta otras cosas. Si hay muchos objetos que se crean y destruyen rápidamente, y nuestra función hash es buena, la tabla hash podría encajar mejor que un vector. Sin embargo, si tienes dudas, ve con el vector.
hokiecsgrad el
Sí, tengo que estar de acuerdo: el vector no es absolutamente más rápido que una tabla hash. Pero es más rápido el 99% del tiempo (-8
importa
Si desea reutilizar las ranuras (que definitivamente desea), debe asegurarse de que se reconozcan las ID no válidas. ex. El objeto en la ID 7 está en la Lista A, B y C. El objeto se destruye y en la misma ranura se crea otro objeto. Este objeto se registra en la Lista A y C. La entrada del primer objeto en la Lista B ahora "apunta" al objeto recién creado, lo cual es incorrecto en este caso. Esto se puede resolver perfectamente con el Handle-Manager de Scott Bilas scottbilas.com/publications/gem-resmgr . Ya he usado esto en juegos comerciales y funciona de maravilla.
DarthCoder
3

¿Es este un enfoque sólido? Parece que eliminaría muchos de los riesgos / molestias de almacenar múltiples listas, con el compromiso de ser el costo de la búsqueda cada vez que desea acceder a un objeto.

Todo lo que ha hecho es mover la carga de realizar la verificación desde el momento de la destrucción hasta el momento del uso. No diría que nunca he hecho esto antes, pero no lo considero 'sonido'.

Sugiero usar una de varias alternativas más robustas. Si se conoce el número de listas, puede salirse simplemente eliminando el objeto de todas las listas. Eso es inofensivo si el objeto no está en una lista dada, y se garantiza que lo ha eliminado correctamente.

Si el número de listas es desconocido o poco práctico, almacene referencias a ellas en el objeto. La implementación típica de esto es el patrón de observación , pero probablemente pueda lograr un efecto similar con los delegados, que vuelven a llamar para eliminar el elemento de cualquier lista que contenga cuando sea necesario.

O a veces realmente no necesitas listas múltiples en absoluto. A menudo, puede mantener un objeto en una sola lista y utilizar consultas dinámicas para generar otras colecciones transitorias cuando las necesite. p.ej. En lugar de tener una lista separada para el inventario de un personaje, puede extraer todos los objetos donde "arrastrado" es igual al personaje actual. Esto puede parecer bastante ineficiente, pero es lo suficientemente bueno para las bases de datos relacionales y puede optimizarse mediante una indexación inteligente.

Kylotan
fuente
Sí, en mi implementación actual, cada lista u objeto individual que quiera una referencia a una entidad de juego debe suscribirse a su evento destruido y reaccionar en consecuencia. Esto podría ser tan bueno o mejor en algunos aspectos que la búsqueda de ID.
Vargonian
1
En mi libro, esto es exactamente lo contrario de una solución robusta. En comparación con la búsqueda por id, tiene MÁS código, MÁS lugares para errores, MÁS oportunidades para olvidar algo y la posibilidad de destruir objetos SIN informar a todos que arranquen. Con la búsqueda por identificación, tiene un pequeño código que casi nunca cambia, un único punto de destrucción y una garantía del 100% de que nunca accederá a un objeto destruido.
importa
1
Con lookup-by-id, tiene un código adicional en cada lugar donde necesita usar el objeto. Con un enfoque basado en el observador, no tiene código adicional, excepto cuando agrega y elimina elementos de las listas, lo que probablemente sea un puñado de lugares en todo el programa. Si hace referencia a elementos más de lo que crea, destruye o mueve entre listas, entonces el enfoque del observador dará como resultado menos código.
Kylotan el
Probablemente hay más lugares que hacen referencia a objetos que lugares que crean / destruyen. Sin embargo, cada referencia requiere solo un pequeño bit de código, o ninguno en absoluto si la búsqueda ya está envuelta por una propiedad (que debería ser la mayor parte del tiempo). Además, PUEDE olvidarse de suscribirse para la destrucción de objetos, pero NO PUEDE olvidarse de buscar un objeto.
importa
1

Esto parece ser solo una instancia más específica del problema "cómo eliminar objetos de una lista, mientras se itera a través de él", que es fácil de resolver (iterar en orden inverso es probablemente la forma más sencilla de una lista de estilo de matriz como C # 's List)

Si una entidad va a ser referenciada desde múltiples lugares, de todos modos debería ser un tipo de referencia . Por lo tanto, no tiene que pasar por un sistema de identificación complicado: una clase en C # ya proporciona la dirección indirecta necesaria.

Y finalmente, ¿por qué molestarse en "verificar la base de datos"? Simplemente dele una IsDeadbandera a cada entidad y verifique eso.

Andrew Russell
fuente
Realmente no conozco C #, pero esta arquitectura a menudo se usa en C y C ++, donde el tipo de referencia normal del lenguaje (punteros) no es seguro de usar si el objeto fue destruido. Hay varias formas de tratarlo, pero todas implican una capa de indirección, y esta es una solución común. (Una lista de backpointers, como sugiere Kylotan, es otra, de la que depende si desea pasar memoria o tiempo)
C # es basura recolectada, por lo que no importa si abandona las instancias. Para los recursos no gestionados (es decir, por el recolector de basura) existe el IDisposablepatrón, que es muy similar a marcar un objeto como IsDead. Obviamente, si necesita agrupar instancias, en lugar de tenerlas GC'd, entonces se requiere una estrategia más complicada.
Andrew Russell el
La bandera IsDead es algo que he usado con éxito en el pasado; Simplemente me gusta la idea de que el juego se bloquee si no puedo verificar un estado muerto en lugar de continuar alegremente, con resultados potencialmente extraños y difíciles de depurar. Usar una búsqueda en la base de datos sería lo primero; IsDead marca el último.
Vargonian
3
@vargonian Con el patrón Desechable, lo que suele suceder es que marque IsDisposed en la parte superior de cada función y propiedad de una clase. Si la instancia está dispuesta, la lanzas ObjectDisposedException(que generalmente se "bloqueará" con una excepción no administrada). Al mover el cheque al objeto mismo (en lugar de verificar el objeto externamente), permite que el objeto defina su propio comportamiento de "estoy muerto" y elimine la carga de la verificación de las personas que llaman. Para sus propósitos, podría implementar IDisposableo hacer su propia IsDeadbandera con una funcionalidad similar.
Andrew Russell el
1

A menudo uso este enfoque en mi propio juego C # / .NET. Además de los otros beneficios (¡y riesgos!) Descritos aquí, también puede ayudar a evitar problemas de serialización.

Si desea aprovechar las funciones de serialización binaria incorporadas en .NET Framework, el uso de ID de entidad puede ayudar a minimizar el tamaño del gráfico de objeto que se escribe. Por defecto, el formateador binario de .NET serializará un gráfico de objeto completo a nivel de campo. Digamos que quiero serializar una Shipinstancia. Si Shiptiene un _ownercampo que hace referencia al Playerpropietario, entonces esa Playerinstancia también se serializaría. Si Playercontiene un _shipscampo (de, digamos, ICollection<Ship>), todas las naves del jugador también se escribirán, junto con cualquier otro objeto referenciado en el nivel del campo (recursivamente). Es fácil serializar accidentalmente un gran gráfico de objetos cuando solo desea serializar una pequeña parte de él.

Si, en cambio, tengo un _ownerIdcampo, entonces puedo usar ese valor para resolver la Playerreferencia a pedido. Mi API pública incluso puede permanecer sin cambios, con la Ownerpropiedad simplemente realizando la búsqueda.

Si bien las búsquedas basadas en hash son generalmente muy rápidas, la sobrecarga adicional podría convertirse en un problema para conjuntos de entidades muy grandes con búsquedas frecuentes. Si se convierte en un problema para usted, puede almacenar en caché la referencia utilizando un campo que no se serializa. Por ejemplo, podría hacer algo como esto:

public class Ship
{
    private int _ownerId;
    [NonSerialized] private Lazy<Player> _owner;

    public Player Owner
    {
        get { return _owner.Value; }
    }

    public Ship(Player owner)
    {
        _ownerId = owner.PlayerID;
        EnsureCache();
    }

    private void EnsureCache()
    {
        if (_owner == null)
            _owner = new Lazy<Player>(() => Game.Current.Players[_ownerId]);
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        EnsureCache();
    }
}

Por supuesto, este enfoque también puede hacer que la serialización más difícil cuando realmente se desea serializar un gráfico de objetos de gran tamaño. En esos casos, necesitaría idear algún tipo de 'contenedor' para garantizar que se incluyan todos los objetos necesarios.

Mike Strobel
fuente
0

Otra forma de manejar su limpieza es invertir el control y usar un objeto desechable. Probablemente tenga un factor que asigne sus entidades, por lo que pase a la fábrica todas las listas a las que debe agregarse. La entidad mantiene una referencia a todas las listas de las que forma parte e implementa IDisposable. En el método de eliminación, se elimina de todas las listas. Ahora solo tiene que preocuparse por la gestión de listas en dos lugares: fábrica (creación) y disposición. Eliminar en el objeto es tan simple como entity.Dispose ().

La cuestión de los nombres frente a las referencias en C # solo es realmente importante para administrar componentes o tipos de valores. Si tiene listas de componentes que están vinculados a una entidad, puede ser útil usar un campo ID como clave de hashmap. Sin embargo, si solo tiene listas de entidades, simplemente use listas con una referencia. Las referencias de C # son seguras y fáciles de usar.

Para eliminar o agregar elementos de la lista, puede utilizar una Cola o cualquier otra solución para tratar de agregar y eliminar de las listas.

CodexArcanum
fuente