Esta pregunta es solo para obtener conocimiento sobre cómo un juego puede manejar tantos personajes a la vez. Soy nuevo en los juegos, así que te pido perdón por adelantado.
Ejemplo
Estoy creando un juego de defensa de torres en el que hay 15 ranuras de torres donde se construyen torres y cada torre expulsa proyectiles a una velocidad determinada; Digamos que cada segundo, cada una de las torres crea 2 proyectiles y hay enemigos que marchan en el campo de batalla, digamos 70 (cada uno con 10 tipos de atributos como HP, maná, etc., que cambiarán a medida que se mueven alrededor del campo de batalla).
Resumen
Conteo de
torres = 15 proyectiles creados por cada torre por segundo = 2
Número total de proyectiles creados por segundo = 30
unidades en el campo de batalla Conteo = 70
Ahora, ¿maneja el juego esos 30 proyectiles y 70 unidades al manejarlos en 100 hilos diferentes (que es demasiado para una PC) o 1 hilo que los mueve a todos, reduce su valor, etc. (que será un poco lento , Yo creo que)?
No tengo ni idea de esto, así que ¿alguien puede guiarme sobre cómo funcionará?
fuente
Respuestas:
No, nunca hagas eso. Nunca cree un nuevo hilo por recurso, esto no escala en redes, ni lo hace en las entidades de actualización. (¿Alguien recuerda los momentos en que tenía un hilo para leer por socket en Java?)
Sí, para empezar, este es el camino a seguir. Los "grandes motores" dividen algo de trabajo entre hilos, pero esto no es necesario para comenzar un juego simple como un juego de defensa de la torre. Probablemente haya aún más trabajo para hacer cada tic que también harás en este hilo. Ah, sí, y la representación, por supuesto.
Bueno ... ¿Cuál es tu definición de lento ? Para 100 entidades, no debería tomar más de medio milisegundo, probablemente incluso menos, dependiendo de la calidad de su código y el idioma con el que esté trabajando. E incluso si toma dos milisegundos completos, sigue siendo lo suficientemente bueno como para alcanzar los 60 tps (tics por segundo, sin hablar de fotogramas en este caso).
fuente
La regla número uno de subprocesos múltiples es: no lo use a menos que necesite paralelizar en múltiples núcleos de CPU para obtener rendimiento o capacidad de respuesta. Un requisito "x e y debe ocurrir simultáneamente desde el punto de vista de los usuarios" aún no es razón suficiente para usar el subprocesamiento múltiple.
¿Por qué?
Multithreading es difícil. No tiene control sobre cuándo se ejecuta cada subproceso, lo que puede ocasionar todo tipo de problemas imposibles de reproducir ("condiciones de carrera"). Hay métodos para evitar esto (bloqueos de sincronización, secciones críticas), pero estos vienen con su propio conjunto de problemas ("puntos muertos").
Por lo general, los juegos que se ocupan de una cantidad tan baja de objetos como unos pocos cientos (sí, esto no es mucho en el desarrollo de juegos) generalmente los procesan en serie cada tic-tic lógico usando un
for
bucle común .Incluso las CPU de teléfonos inteligentes relativamente más débiles pueden realizar miles de millones de instrucciones por segundo. Eso significa que incluso cuando la lógica de actualización de sus objetos es compleja y toma alrededor de 1000 instrucciones por objeto y tic, y está apuntando a unos generosos 100 tics por segundo, tiene suficiente capacidad de CPU para decenas de miles de objetos. Sí, este es un cálculo excesivamente simplificado del reverso del sobre, pero le da una idea.
Además, la sabiduría común en el desarrollo de juegos es que las lógicas de los juegos rara vez son el cuello de botella de un juego. La parte crítica del rendimiento es casi siempre los gráficos. Sí, incluso para juegos 2D.
fuente
Las otras respuestas han manejado los hilos y el poder de las computadoras modernas. Sin embargo, para abordar la pregunta más importante, lo que está tratando de hacer aquí es evitar situaciones "n al cuadrado".
Por ejemplo, si tienes 1000 proyectiles y 1000 enemigos, la solución ingenua es simplemente compararlos entre sí.
¡Esto significa que terminas con p * e = 1,000 * 1,000 = 1,000,000 cheques diferentes! Esto es O (n ^ 2).
Por otro lado, si organiza mejor sus datos, puede evitar mucho de eso.
Por ejemplo, si enumeras en cada cuadrado de la cuadrícula qué enemigos hay en ese cuadrado, entonces puedes recorrer tus 1000 proyectiles y simplemente verificar el cuadrado en la cuadrícula. Ahora solo necesita verificar cada proyectil contra el cuadrado, esto es O (n). En lugar de un millón de cheques por cuadro, solo necesitas mil.
Pensar en organizar sus datos y procesarlos de manera eficiente debido a esa organización es la mayor optimización individual que puede hacer.
fuente
No cree hilos por recurso / objeto sino por sección de la lógica de su programa. Por ejemplo:
La ventaja de esto es que su GUI (por ejemplo, botones) no necesariamente se atasca si su lógica es lenta. El usuario aún puede pausar y guardar el juego. También es bueno para preparar tu juego para multijugador, ahora que separas el gráfico de la lógica.
fuente
Incluso Space Invaders manejó docenas de objetos interactuando. Mientras que decodificar un cuadro de video HD H264 involucra cientos de millones de operaciones aritméticas. Tiene mucha potencia de procesamiento disponible.
Dicho esto, aún puedes hacerlo más lento si lo desperdicias. El problema no es tanto el número de objetos como el número de pruebas de colisión realizadas; El enfoque simple de verificar cada objeto uno contra otro cuadra el número de cálculos requeridos. Probar colisiones de 1001 objetos de esta manera requeriría un millón de comparaciones. A menudo, esto se soluciona, por ejemplo, no verificando la colisión entre proyectiles.
fuente
Voy a estar en desacuerdo con algunas de las otras respuestas aquí. Los hilos lógicos separados no solo son una buena idea, sino que son muy beneficiosos para la velocidad de procesamiento, si su lógica es fácilmente separable .
Su pregunta es un buen ejemplo de lógica que probablemente sea separable si puede agregarle lógica adicional. Por ejemplo, puede ejecutar varios subprocesos de detección de aciertos ya sea bloqueando los subprocesos en regiones específicas del espacio o haciendo una exclusión mutua de los objetos involucrados.
Probablemente NO desee un subproceso por cada posible colisión, solo porque es probable que esto bloquee el programador; También hay un costo asociado con la creación y destrucción de hilos. Es mejor hacer una cierta cantidad de subprocesos alrededor de los núcleos del sistema (o utilizar una métrica como la anterior
#cores * 2 + 4
), luego reutilizarlos cuando finalice su proceso.Sin embargo, no toda la lógica es fácilmente separable. A veces, sus operaciones pueden alcanzar todos los datos del juego a la vez, lo que haría que el enhebrado sea inútil (de hecho, dañino, porque necesitaría agregar controles para evitar problemas de enhebrado). Además, si varias etapas de la lógica dependen mucho unas de otras en órdenes específicas, tendrá que controlar la ejecución de subprocesos de manera que se garantice que no se obtengan resultados dependientes de las órdenes. Sin embargo, ese problema no se elimina al no usar hilos, los hilos simplemente lo exacerban.
La mayoría de los juegos no hacen esto simplemente porque es más complejo de lo que el desarrollador promedio de juegos está dispuesto / capaz de manejar para lo que generalmente no es el cuello de botella en primer lugar. La gran mayoría de los juegos están limitados por GPU, no por CPU. Si bien mejorar la velocidad de la CPU puede ayudar en general, generalmente no es el enfoque.
Dicho esto, los motores de física a menudo emplean múltiples hilos, y puedo nombrar varios juegos que creo que se habrían beneficiado de múltiples hilos lógicos (los juegos Paradox RTS como HOI3 y otros, por ejemplo).
Estoy de acuerdo con otras publicaciones en que probablemente no tendrías necesidad de emplear hilos en este ejemplo específico, incluso si pudiera ser beneficioso. El subproceso debe reservarse para casos en los que tiene una carga de CPU excesiva que no se puede optimizar mediante otros métodos. Es una tarea enorme y afectará la estructura fundamental de un motor; no es algo que puedas abordar después del hecho.
fuente
Creo que las otras respuestas pierden una parte importante de la pregunta al enfocarse demasiado en la parte de la pregunta.
Una computadora no maneja todos los objetos de un juego a la vez. Los maneja en secuencia.
Un juego de computadora progresa en pasos de tiempo discretos. Dependiendo del juego y la velocidad de la PC, estos pasos generalmente son 30 o 60 pasos por segundo, o tantos / pocos pasos como la PC pueda calcular.
En uno de esos pasos, una computadora calcula lo que hará cada uno de los objetos del juego durante ese paso y los actualiza en consecuencia, uno tras otro. Incluso podría hacerlo en paralelo, usando hilos para ser más rápido, pero como veremos pronto, la velocidad no es una preocupación en absoluto.
Una CPU promedio debe ser de 2 GHz o más rápida, lo que significa 10 9 ciclos de reloj por segundo. Si calculamos 60 timesteps por segundo, que las hojas 10 9 ciclos / 60 ciclos de reloj = 16666666 de reloj por el tiempo de paso. Con 70 unidades, todavía nos quedan unos 2,400,000 ciclos de reloj por unidad. Si tuviéramos que optimizar, podríamos ser capaces de actualizar cada unidad en tan solo 240 ciclos, dependiendo de la complejidad de la lógica del juego. Como puede ver, nuestra computadora es aproximadamente 10,000 veces más rápida de lo necesario para esta tarea.
fuente
Descargo de responsabilidad: mi tipo de juego favorito de todos los tiempos está basado en texto y escribo esto como un programador de mucho tiempo de un antiguo MUD.
Creo que una pregunta importante que debes hacerte es esta: ¿Necesitas hilos? Entiendo que un juego gráfico probablemente tenga más uso de MT, pero creo que también depende de la mecánica del juego. (También podría valer la pena considerar que con las GPU, las CPU y todos los demás recursos que tenemos hoy en día son mucho más potentes, lo que hace que sus preocupaciones sobre los recursos sean tan problemáticas como podría parecerle; de hecho, 100 objetos son prácticamente cero). También depende de cómo defina 'todos los personajes a la vez'. ¿Quieres decir exactamente al mismo tiempo? No lo tendrá como Peter lo señala con razón, así que todo de una vez es irrelevante en sentido literal; solo aparece de esta manera.
Suponiendo que irá con hilos: definitivamente no debe considerar 100 hilos (y ni siquiera voy a analizar si es demasiado para su CPU o no; me refiero solo a las complicaciones y la practicidad de la misma).
Pero recuerde esto: el enhebrado múltiple no es fácil (como señala Philipp) y tiene muchos problemas. Otros tienen mucha más experiencia (por mucho) que yo con MT, pero yo diría que también sugerirían lo mismo (a pesar de que serían más capaces que yo, especialmente sin práctica de mi parte).
Algunos argumentan que no están de acuerdo con que los hilos no son beneficiosos y algunos argumentan que cada objeto debe tener un hilo. Pero (y de nuevo, todo esto es texto, pero incluso si considera más de un hilo no necesita, y no debe considerarlo para cada objeto), ya que Philipp señala que los juegos tienden a recorrer las listas. Pero, sin embargo, no es solo (como sugiere, aunque me doy cuenta de que solo responde a los parámetros de tan pocos objetos) para tan pocos objetos. En MUD soy programador porque tenemos lo siguiente (y esta no es toda la actividad que ocurre en tiempo real, así que tenlo en cuenta también):
(El número de instancias varía, por supuesto, mayor y menor)
Móviles (NPC, es decir, personaje no jugador): 2614; prototipos: 1360 Objetos: 4457; prototipos: 2281 habitaciones: 7983; prototipos: 7983. Generalmente, cada habitación tiene su propia instancia, pero también tenemos habitaciones dinámicas, es decir, habitaciones dentro de una habitación; o habitaciones dentro de un móvil, por ejemplo, el estómago de un dragón; o habitaciones en objetos, por ejemplo, ingresas un objeto mágico). Tenga en cuenta que estas salas dinámicas existen por objeto / sala / móvil que realmente las tiene definidas. Sí, esto es muy parecido a la idea de instancias de World of Warcraft (no la juego, pero un amigo me hizo jugarla cuando tenía una máquina Windows, por un tiempo), excepto que la tuvimos mucho antes de que existiera World of Warcraft.
Scripts: 868 (actualmente) (curiosamente nuestro comando de estadísticas no muestra cuántos prototipos tenemos, por lo que agregaré eso). Todos estos se llevan a cabo en áreas / zonas y tenemos 103 de ellos. También tenemos procedimientos especiales que se procesan en diferentes momentos. También tenemos otros eventos. Entonces también tenemos enchufes conectados. Los móviles se mueven, realizan diferentes actividades (además del combate), tienen interacciones con los jugadores, etc. (También lo hacen otros tipos de entidades).
¿Cómo manejamos todo esto sin demora?
sockets: select (), colas (entrada, salida, eventos, otras cosas), buffers (entrada, salida, otras cosas), etc. Estos son encuestados 10 veces por segundo.
personajes, objetos, habitaciones, combate, todo: todo en un bucle central en diferentes pulsos.
También (mi implementación basada en la discusión entre el fundador / otro programador y yo) tenemos un extenso seguimiento de listas vinculadas y pruebas de validez del puntero y tenemos más que suficientes recursos gratuitos en caso de que realmente lo necesitemos. Todo esto (excepto que hemos expandido el mundo) existió hace años cuando había menos RAM, potencia de CPU, espacio en el disco duro, etc. Y de hecho, incluso entonces no tuvimos problemas. En los bucles descritos (las secuencias de comandos causan esto al igual que los reinicios / repoblaciones de área al igual que otras cosas) se están creando, liberando, etc., monstruos, objetos (elementos) y otras cosas. Las conexiones también son aceptadas, encuestadas y todo lo demás que cabría esperar.
fuente