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.
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 unoGameObject
, mostrando el número "3000".Respuestas:
Hay tres cosas que debes considerar:
¿ 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.
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.
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.
fuente
En esta situación, sugeriría usar Composición :
En este caso, parece que tu
Character
clase 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
Character
en subclases, de las cuales elCharacter
propietario 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,Equipment
puede tener todos los elementos que tiene tu personaje o sus activos actuales. También puede tener la lógica que gestiona estos en funciones.CharacterAI
oCharacterController
puede 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
AliveCharacterObject
que tiene elCharacterController
,CharacterEquipment
yCharacterInfo
secuencias de comandos adjunta. Para "matar" al personaje, simplemente elimine las partes que ya no son relevantes (como elCharacterController
); ahora no perderá memoria ni tiempo de procesamiento.Tenga en cuenta que
CharacterInfo
es 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.
fuente
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.
fuente
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.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,
fuente
Aclaracion de la pregunta
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.
Character
Clases sin funcionalidadBien, 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
Character
clases 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étodoGetFullName()
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 claseBirther
con un métodoCharacter CreateCharacter(Character father, Character mother)
resultará mucho más limpia que tener esa funcionalidad en laCharacter
clase.No almacene datos en código
No. Almacénelos en formato JSON, utilizando JsonUtility de Unity. Con esas
Character
clases 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
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é
ActingCharacter
yFamilyTreeEntry
.FamilyTreeEntry
No es necesario actualizar los caracteres muertos y probablemente necesite muchos menos datos que los activosActingCharacter
.fuente
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.
Entonces, aquí es donde iría con esto:
En primer lugar, crea una
ID
para tus personajes. Anint
,Guid
lo 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
Pathfinder
sistema probablemente querrá unVector2 NextPosition(int entity)
. De esta manera, puede mantener esos elementos en matrices o listas estrechamente controladas. Puede usarstruct
tipos 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 ejemploName
).Pero, y no puedo enfatizar esto lo suficiente, ahora un
Entity
es solo unID
, 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 sistemasSprite
yMovement
(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 elSprite
sistema puede controlar, o elPaperdoll
sistema puede controlar). AhoraNextPosition
puede reescribirse ligeramente:Vector2? NextPosition(int entity)
y puede devolver unanull
posición para entidades que no le importan. También aplicamos esto a nuestroNameSystem.GetName(int entity)
, vuelvenull
para á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).
fuente
GameObject
que no hacen mucho pero tienen una lista deComponent
clases 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 suGameObject
sistema es y siempre fue en gran medida un ECS. OP ya está utilizando un ECS.Mientras hace esto en Unity, el enfoque más sencillo es el siguiente:
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.
fuente