La mejor forma de estructurar / administrar cientos de personajes 'en el juego'


10

He estado haciendo un juego RTS simple, que contiene cientos de personajes como Crusader Kings 2, en Unity. Para almacenarlos, la opción más fácil sería usar objetos programables, pero esa no es una buena solución, ya que no puede crear objetos nuevos en tiempo de ejecución.

Así que creé una clase C # llamada "Carácter" que contiene todos los datos. Todo funciona bien, pero a medida que el juego simula constantemente crea nuevos personajes y mata a algunos personajes (a medida que ocurren los eventos en el juego). A medida que el juego simula continuamente, crea miles de personajes. Agregué una simple verificación para asegurarme de que un personaje esté "Vivo" mientras procesa su función. Por lo tanto, ayuda al rendimiento, pero no puedo eliminar el "Personaje" si está muerto, porque necesito su información al crear el árbol genealógico.

¿Una lista es la mejor manera de guardar datos para mi juego? ¿O dará problemas una vez que se creen 10000 de un personaje? Una posible solución es hacer otra lista una vez que la lista alcanza una cierta cantidad y mover todos los caracteres muertos en ellos.


99
Una cosa que he hecho en el pasado cuando necesito un subconjunto de datos de un personaje después de su desaparición es crear un objeto "lápida" para ese personaje. La lápida puede llevar la información que necesito buscar más tarde, pero puede ser más pequeña e iterarse con menos frecuencia porque no necesita una simulación constante como un personaje vivo.
DMGregory


2
¿Es el juego como CK2, o es solo la parte de tener muchos personajes? Lo entendí como que todo el juego es como CK2. En ese caso, muchas de las respuestas aquí no son incorrectas y contienen buenos conocimientos, pero pierden el punto de la pregunta. No ayuda que hayas llamado CK2 un juego de estrategia en tiempo real cuando en realidad es un gran juego de estrategia . Eso puede parecer complicado, pero es muy relevante para los problemas que enfrenta.
Raphael Schmitz

1
Por ejemplo, cuando menciona "1000 de caracteres", las personas piensan en miles de modelos 3D o sprites en la pantalla al mismo tiempo , por lo tanto, en Unity, 1000 de GameObjects. En CK2, la cantidad máxima de personajes que vi al mismo tiempo fue cuando miré mi cancha y vi a 10-15 personas allí (aunque no jugué muy lejos). Del mismo modo, un ejército con 3000 soldados es solo uno GameObject, mostrando el número "3000".
Raphael Schmitz

1
@ R.Schmitz Sí, debería haber dejado en claro que cada personaje no tiene un objeto de juego adjunto. Siempre que sea necesario, como mover el personaje de un punto a otro. Se crea una entidad separada que contiene toda la información de ese personaje con lógica Ai.
Paul p

Respuestas:


24

Hay tres cosas que debes considerar:

  1. ¿ Realmente causa un problema de rendimiento? 1000s es, bueno, no muchos en realidad. Las computadoras modernas son terriblemente rápidas y pueden manejar muchas cosas. Controle cuánto tiempo tarda el procesamiento de caracteres y vea si realmente va a causar un problema antes de preocuparse demasiado por él.

  2. Fidelidad de personajes actualmente mínimamente activos. Un error frecuente de los programadores de juegos para principiantes es obsesionarse con la actualización precisa de los personajes fuera de la pantalla de la misma manera que los de la pantalla. Esto es un error, a nadie le importa. En su lugar, debe buscar crear la impresión de que los personajes fuera de la pantalla todavía están actuando. Al reducir la cantidad de caracteres de actualización que se reciben fuera de la pantalla, puede disminuir drásticamente los tiempos de procesamiento.

  3. Considere el diseño orientado a datos. En lugar de tener 1000 objetos de caracteres y llamar a la misma función para cada uno, tenga una matriz de datos para los 1000 caracteres y tenga un bucle de función sobre los 1000 caracteres actualizándose a su vez. Este tipo de optimización puede mejorar drásticamente el rendimiento.


3
Entidad / Componente / Sistema funciona bien para esto. Cree cada "sistema" para lo que necesita, haga que conserve los miles o decenas de miles de caracteres (sus componentes) y proporcione la "ID de personaje" al sistema. Esto le permite mantener los diferentes modelos de datos separados y más pequeños, y también puede eliminar los caracteres muertos de los sistemas que no los necesitan. (También puede descargar completamente un sistema, si no lo está usando en este momento.)
Der Kommissar

1
Al reducir la cantidad de caracteres de actualización que se reciben fuera de la pantalla, puede aumentar drásticamente los tiempos de procesamiento. ¿No te refieres a disminuir?
Tejas Kale

1
@TejasKale: Sí, corregido.
Jack Aidley

2
Mil caracteres no es mucho, pero cuando cada uno de ellos está constantemente comprobando si pueden castrarse entre sí, comienza a tener un gran impacto en el rendimiento general ...
curiousdannii

1
Siempre es mejor verificar, pero en general es una suposición de trabajo segura que los romanos van a querer castrarse unos a otros;)
curiousdannii

11

En esta situación, sugeriría usar Composición :

El principio de que las clases deben lograr un comportamiento polimórfico y la reutilización del código por su composición (al contener instancias de otras clases que implementan la funcionalidad deseada)


En este caso, parece que tu Characterclase se ha vuelto divina y contiene todos los detalles de cómo opera un personaje en todas las etapas de su ciclo de vida.

Por ejemplo, observa que los caracteres muertos todavía son necesarios, ya que se usan en los árboles genealógicos. Sin embargo, es poco probable que toda la información y la funcionalidad de sus personajes vivos sigan siendo necesarios solo para mostrarlos en un árbol genealógico. Es posible que, por ejemplo, simplemente necesiten nombres, fecha de nacimiento y un ícono de retrato.


La solución es dividir las partes separadas de su Characteren subclases, de las cuales el Characterpropietario posee una instancia. Por ejemplo:

  • CharacterInfo puede ser una estructura de datos simple con el nombre, fecha de nacimiento, fecha de fallecimiento y facción,

  • Equipmentpuede tener todos los elementos que tiene tu personaje o sus activos actuales. También puede tener la lógica que gestiona estos en funciones.

  • CharacterAIo CharacterControllerpuede tener toda la información necesaria sobre el objetivo actual del personaje, sus funciones de utilidad, etc. Y también puede tener la lógica de actualización real que coordina la toma de decisiones / interacción entre sus partes individuales.

Una vez que haya dividido el personaje, ya no necesita verificar una bandera Vivo / Muerto en el ciclo de actualización.

En su lugar, usted sólo tiene que hacer una AliveCharacterObjectque tiene el CharacterController, CharacterEquipmenty CharacterInfosecuencias de comandos adjunta. Para "matar" al personaje, simplemente elimine las partes que ya no son relevantes (como el CharacterController); ahora no perderá memoria ni tiempo de procesamiento.

Tenga en cuenta que CharacterInfoes probable que los únicos datos realmente necesarios para el árbol genealógico. Al descomponer sus clases en piezas más pequeñas de funcionalidad, puede mantener más fácilmente ese pequeño objeto de datos después de la muerte, sin necesidad de mantener todo el personaje impulsado por la IA.


Vale la pena mencionar que este paradigma es para el que Unity fue creado para usar, y es por eso que maneja las cosas con muchos scripts separados. Construir grandes objetos divinos rara vez es la mejor manera de manejar sus datos en Unity.


8

Cuando tiene que manejar una gran cantidad de datos y no todos los puntos de datos están representados por un objeto de juego real, entonces no es una mala idea renunciar a clases específicas de Unity y simplemente ir con objetos C # viejos. De esa manera minimizas los gastos generales. Así que parece que estás en el camino correcto aquí.

Almacenar todos los caracteres, vivos o muertos, en una Lista ( o matriz ) puede ser útil porque el índice en esa lista puede servir como una identificación de carácter canónico. Acceder a una posición de lista por índice es una operación muy rápida. Pero podría ser útil mantener una lista separada de los ID de todos los personajes vivos, porque es probable que necesite repetirlos con mucha más frecuencia de la que necesitará los caracteres muertos.

A medida que avanza la implementación de la mecánica del juego, es posible que también desee ver qué otro tipo de búsquedas realiza más. Como "todos los personajes vivos en una ubicación específica" o "todos los antepasados ​​vivos o muertos de un personaje específico". Puede ser beneficioso crear algunas estructuras de datos secundarias más optimizadas para este tipo de consultas. Solo recuerde que cada uno de ellos debe mantenerse actualizado. Esto requiere programación adicional y será una fuente de errores adicionales. Así que solo hágalo si espera un aumento notable en el rendimiento.

CKII " ciruelas " personajes de su base de datos cuando se les considere como algo sin importancia para ahorrar recursos. Si su pila de personajes muertos consume demasiados recursos en un juego de larga duración, entonces es posible que desee hacer algo similar (no quiero llamar a esto "recolección de basura". ¿Quizás "incremental respetuoso"?).

Si realmente tienes un objeto de juego para cada personaje del juego, entonces el nuevo sistema Unity ECS y Jobs podría ser útil para ti. Está optimizado para manejar una gran cantidad de objetos de juego muy similares de manera eficiente. Pero obliga a su arquitectura de software a adoptar patrones muy rígidos.

Por cierto, me gusta mucho CKII y la forma en que simula un mundo con miles de personajes únicos controlados por IA, por lo que estoy ansioso por interpretar tu versión del género.


Hola gracias por la respuesta Todos los cálculos son realizados por un solo GameObject Manager. Solo estoy asignando objetos de juego a Actores individuales cuando es necesario (como mostrar movimientos de Army of Character de una posición a otra).
Paul p

1
Like "all living characters in a specific location" or "all living or dead ancestors of a specific character". It might be beneficial to create some more secondary data-structures optimized for these kinds of queries.Desde mi experiencia con el modding CK2, esto está cerca de cómo CK2 maneja los datos. CK2 parece usar índices que son esencialmente índices básicos de bases de datos, lo que hace que sea más rápido encontrar caracteres para una situación específica. En lugar de tener una lista de caracteres, parece tener una base de datos interna de caracteres, con todos los inconvenientes y beneficios que conlleva.
Morfildur

1

No necesita simular / actualizar miles de personajes cuando solo unos pocos están cerca del jugador. Solo necesita actualizar lo que el jugador puede ver realmente en el momento actual, por lo que los personajes que están más lejos del jugador deben suspenderse hasta que el jugador esté más cerca de ellos.

Si esto no funciona porque la mecánica de tu juego requiere personajes distantes para mostrar el paso del tiempo, puedes actualizarlos en una actualización "grande" cuando el jugador se acerque. Si la mecánica del juego requiere que cada personaje responda realmente a los eventos del juego a medida que suceden, sin importar dónde esté el personaje en relación con el jugador o el evento, entonces podría funcionar para reducir la frecuencia con la que los personajes están más lejos del jugador actualizado (es decir, todavía se actualizan en sincronización con el resto del juego, pero no con tanta frecuencia, por lo que habrá un ligero retraso antes de que los personajes distantes respondan a un evento, pero es poco probable que esto cause un problema o incluso que el jugador lo note) ) Alternativamente, es posible que desee utilizar un enfoque híbrido,


Es un RTS. Debemos suponer que en un momento dado, un número considerable de unidades está realmente en la pantalla.
Tom

En un RTS, el mundo necesita continuar mientras el jugador no está mirando. Una gran actualización llevaría el mismo tiempo, pero estaría en una gran explosión cuando mueva la cámara.
PStag

1

Aclaracion de la pregunta

He estado haciendo un simple juego de estrategia en tiempo real, que contiene cientos de personajes como Crusader Kings 2 en una Unidad.

En esta respuesta, supongo que se supone que todo el juego es como CK2, en lugar de solo la parte de tener muchos personajes. Todo lo que ve en la pantalla en CK2 es fácil de hacer y no pondrá en peligro su rendimiento ni será complicado de implementar en Unity. Los datos detrás de eso, ahí es donde se vuelve complejo.

CharacterClases sin funcionalidad

Así que creé una clase C # llamada "Carácter" que contiene todos los datos.

Bien, porque un personaje en tu juego es solo información. Lo que ves en la pantalla es solo una representación de esos datos. Estas Characterclases son el corazón del juego y, como tales, corren el peligro de convertirse en " objetos de Dios ". Por lo tanto, recomendaría medidas extremas contra eso: elimine toda la funcionalidad de esas clases. Un método GetFullName()que combina el nombre y el apellido, OK, pero ningún código que realmente "haga algo". Ponga ese código en clases dedicadas que realizan una acción; Por ejemplo, una clase Birthercon un método Character CreateCharacter(Character father, Character mother)resultará mucho más limpia que tener esa funcionalidad en la Characterclase.

No almacene datos en código

Para almacenarlos, la opción más fácil sería usar objetos programables

No. Almacénelos en formato JSON, utilizando JsonUtility de Unity. Con esas Characterclases sin funcionalidad , debería ser trivial hacerlo. Eso funcionará para la configuración inicial del juego, así como para almacenarlo en partidas guardadas. Sin embargo, todavía es algo aburrido, así que solo di la opción más fácil en su situación. También puede usar XML o YAML o cualquier formato, siempre que los humanos puedan leerlo cuando esté almacenado en un archivo de texto. CK2 hace lo mismo, en realidad la mayoría de los juegos lo hacen. También es una excelente configuración para permitir que las personas modifiquen tu juego, pero eso es un pensamiento para mucho más tarde.

Pensar abstracto

Agregué una simple verificación para asegurarme de que el personaje esté "Vivo" durante el procesamiento, [...] pero no puedo eliminar el "Personaje" si está muerto porque necesito su información al crear el árbol genealógico.

Este es más fácil decirlo que hacerlo, porque a menudo choca con la forma natural de pensar. Estás pensando de forma "natural", en un "personaje". Sin embargo, en términos de su juego, parece que hay al menos 2 tipos diferentes de datos que "son un personaje": lo llamaré ActingCharactery FamilyTreeEntry. FamilyTreeEntryNo es necesario actualizar los caracteres muertos y probablemente necesite muchos menos datos que los activos ActingCharacter.


0

Voy a hablar de un poco de experiencia, pasando de un diseño OO rígido a un diseño de Sistema de Componentes de Entidad (ECS).

Hace un tiempo, era como tú , tenía un montón de diferentes tipos de cosas que tenían propiedades similares y construí varios objetos e intenté usar la herencia para resolverlo. Una persona muy inteligente me dijo que no hiciera eso y, en cambio, usara Entity-Component-System.

Ahora, ECS es un gran concepto, y es difícil acertar. Se necesita mucho trabajo para construir entidades, componentes y sistemas de manera adecuada. Sin embargo, antes de que podamos hacer eso, necesitamos definir los términos.

  1. Entidad : esta es la cosa , el jugador, el animal, el NPC, lo que sea . Es algo que necesita componentes unidos a él.
  2. Componente : este es el atributo o propiedad , como un "Nombre" o "Edad", o "Padres", en su caso.
  3. Sistema : esta es la lógica detrás de un componente o un comportamiento . Por lo general, construye un sistema por componente, pero eso no siempre es posible. Además, a veces los sistemas necesitan influir en otros sistemas.

Entonces, aquí es donde iría con esto:

En primer lugar, crea una IDpara tus personajes. An int, Guidlo que quieras. Esta es la "Entidad".

En segundo lugar, comience a pensar en los diferentes comportamientos que tiene. Cosas como el "árbol genealógico", es un comportamiento. En lugar de modelar eso como atributos en la entidad, cree un sistema que contenga toda esa información . El sistema puede decidir qué hacer con él.

Del mismo modo, queremos construir un sistema para "¿Está vivo o muerto el personaje?" Este es uno de los sistemas más importantes en su diseño, ya que influye en todos los demás. Algunos sistemas pueden eliminar los caracteres "muertos" (como el sistema "sprite"), otros sistemas pueden reorganizar internamente las cosas para soportar mejor el nuevo estado.

Construirá un sistema "Sprite" o "Drawing" o "Rendering", por ejemplo. Este sistema tendrá la responsabilidad de determinar con qué sprite debe mostrarse el personaje y cómo mostrarlo. Luego, cuando un personaje muere, retíralo.

Además, un sistema de "IA" que puede decirle a un personaje qué hacer, a dónde ir, etc. Esto debería interactuar con muchos de los otros sistemas y tomar decisiones basadas en ellos. Una vez más, los personajes muertos probablemente se puedan eliminar de este sistema, ya que en realidad ya no están haciendo nada.

Su sistema "Nombre" y el sistema "Árbol genealógico" probablemente deberían mantener al personaje (vivo o muerto) en la memoria. Este sistema necesita recuperar esa información, independientemente del estado del personaje. (Jim sigue siendo Jim, incluso después de enterrarlo).

Esto también le brinda el beneficio de cambiar cuando un sistema reacciona de manera más eficiente: el sistema tiene su propio temporizador. Algunos sistemas necesitan dispararse rápidamente, otros no. Aquí es donde comenzamos a entrar en lo que hace que un juego funcione de manera eficiente. No necesitamos volver a calcular el clima cada milisegundo, probablemente podamos hacerlo cada 5 o más.

También le da más influencia creativa: puede construir un sistema "Pathfinder" que puede manejar el cálculo de una ruta de A a B, y puede actualizarse según sea necesario, permitiendo que el sistema de Movimiento diga "¿Dónde necesito? ¿siguiente?" Ahora podemos separar completamente estas preocupaciones y razonar sobre ellas de manera más efectiva. El movimiento no necesita encontrar el camino, solo necesita llevarte allí.

Querrá exponer algunas partes de un sistema al exterior. En su Pathfindersistema probablemente querrá un Vector2 NextPosition(int entity). De esta manera, puede mantener esos elementos en matrices o listas estrechamente controladas. Puede usar structtipos más pequeños, que pueden ayudarlo a mantener los componentes en bloques de memoria contiguos más pequeños, lo que puede hacer que las actualizaciones del sistema sean mucho más rápidas. (Especialmente si las influencias externas en un sistema son mínimas, ahora solo necesita preocuparse por su estado interno, como por ejemplo Name).

Pero, y no puedo enfatizar esto lo suficiente, ahora un Entityes solo un ID, incluyendo mosaicos, objetos, etc. Si una entidad no pertenece a un sistema, entonces el sistema no lo rastreará. Esto significa que podemos crear nuestros objetos "Árbol", almacenarlos en los sistemas Spritey Movement(los árboles no se moverán, pero tienen un componente de "Posición") y mantenerlos fuera de los otros sistemas. Ya no necesitamos una lista especial para los árboles, ya que renderizar un árbol no es diferente a un personaje, aparte del papel. (Lo que el Spritesistema puede controlar, o el Paperdollsistema puede controlar). Ahora NextPositionpuede reescribirse ligeramente: Vector2? NextPosition(int entity)y puede devolver una nullposición para entidades que no le importan. También aplicamos esto a nuestro NameSystem.GetName(int entity), vuelve nullpara árboles y rocas.


Voy a terminar esto, pero la idea aquí es darle algunos antecedentes sobre ECS, y cómo realmente puede aprovecharlo para obtener un mejor diseño en su juego. Puede aumentar el rendimiento, desacoplar elementos no relacionados y mantener las cosas de una manera más organizada. (Esto también se combina bien con lenguajes / configuraciones funcionales, como F # y LINQ, que recomiendo consultar F # si aún no lo ha hecho, se combina muy bien con C # cuando los usa en conjunto).


Hola, gracias por una respuesta tan detallada. Solo estoy usando One GameObject Manager que contiene la referencia a todos los demás personajes del juego.
Paul p

El desarrollo en Unity gira en torno a entidades llamadas GameObjectque no hacen mucho pero tienen una lista de Componentclases que hacen el trabajo real. Hubo un cambio de paradigma con respecto a la rotonda de los ECS hace una década , ya que poner el código de actuación en clases de sistema separadas es más limpio. Recientemente, Unity también implementó dicho sistema, pero su GameObjectsistema es y siempre fue en gran medida un ECS. OP ya está utilizando un ECS.
Raphael Schmitz

-1

Mientras hace esto en Unity, el enfoque más sencillo es el siguiente:

  • crear un objeto de juego por personaje o tipo de unidad
  • guardarlos como prefabricados
  • luego puede crear una instancia de una casa prefabricada cuando sea necesario
  • cuando un personaje muere, destruye el objeto del juego y ya no ocupará más CPU o memoria

En su código, puede mantener referencias a los objetos en algo así como una Lista para evitar que use Find () y sus variaciones todo el tiempo. Está intercambiando ciclos de CPU por memoria, pero una lista de punteros es bastante pequeña, por lo que incluso con unos pocos miles de objetos no debería ser un gran problema.

A medida que avanzas en el juego, encontrarás que tener objetos de juego individuales te brinda toneladas de ventajas, incluida la navegación y la IA.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.