¡Qué gran carga de preguntas! Podría avergonzarme intentando este con mis pensamientos extravagantes (y me encantaría escuchar sugerencias si realmente estoy fuera). Pero para mí, lo más útil que he aprendido últimamente en mi dominio (que incluía los juegos en el pasado, ahora VFX) ha sido reemplazar las interacciones entre interfaces abstractas con datos como un mecanismo de desacoplamiento (y finalmente reducir la cantidad de información requerida entre cosas y unos de otros al mínimo absoluto). Esto puede sonar completamente loco (y podría estar usando todo tipo de terminología deficiente).
Sin embargo, digamos que te doy un trabajo razonablemente manejable. Tiene este archivo que contiene datos de escena y animación para renderizar. Hay documentación que cubre el formato del archivo. Su único trabajo es cargar el archivo, renderizar imágenes bonitas para la animación usando el trazado de ruta y generar los resultados en archivos de imagen. Esa es una aplicación de pequeña escala que probablemente no abarcará más de decenas de miles de LOC incluso para un renderizador bastante sofisticado (definitivamente no millones).
Tienes tu propio pequeño mundo aislado para este renderizador. No se ve afectado por el mundo exterior. Aísla su propia complejidad. Más allá de las preocupaciones de leer este archivo de escena y enviar sus resultados a archivos de imagen, puede enfocarse por completo en el renderizado. Si algo sale mal en el proceso, sabes que está en el renderizador y nada más, ya que no hay nada más involucrado en esta imagen.
Mientras tanto, digamos que debe hacer que su renderizador funcione en el contexto de un gran software de animación que en realidad tiene millones de LOC. En lugar de simplemente leer un formato de archivo simplificado y documentado para obtener los datos necesarios para la representación, debe pasar por todo tipo de interfaces abstractas para recuperar todos los datos que necesita para hacer lo suyo:
De repente, tu renderizador ya no está en su propio pequeño mundo aislado. Esto se siente mucho, mucho más complejo. Debe comprender el diseño general de una gran parte del software como un todo orgánico con potencialmente muchas partes móviles, y tal vez incluso a veces tenga que pensar en implementaciones de elementos como mallas o cámaras si golpea un cuello de botella o un error en uno de Las funciones.
Funcionalidad versus datos optimizados
Y una de las razones es porque la funcionalidad es mucho más compleja que los datos estáticos. También hay tantas formas en que una llamada a la función podría salir mal de una manera que la lectura de datos estáticos no puede. Hay tantos efectos secundarios ocultos que pueden ocurrir al llamar a esas funciones a pesar de que conceptualmente solo está recuperando datos de solo lectura para renderizar. También puede tener muchas más razones para cambiar. En unos pocos meses a partir de ahora, es posible que la interfaz de malla o textura cambie o desaproveche partes de tal forma que requiera reescribir secciones importantes de su renderizador y mantenerse al día con esos cambios, aunque esté obteniendo exactamente los mismos datos, aunque la entrada de datos a su procesador no ha cambiado en absoluto (solo la funcionalidad requerida para acceder a todo).
Entonces, cuando es posible, descubrí que los datos simplificados son un mecanismo de desacoplamiento muy bueno de un tipo que realmente le permite evitar tener que pensar en todo el sistema en su conjunto y le permite concentrarse en una parte muy específica del sistema para hacer mejoras, agregar nuevas funciones, arreglar cosas, etc. Sigue una mentalidad muy de E / S para las piezas voluminosas que componen su software. Ingrese esto, haga lo suyo, elabore eso y, sin pasar por docenas de interfaces abstractas, finalice llamadas de función sin fin en el camino. Y está empezando a parecerse, hasta cierto punto, a la programación funcional.
Entonces, esta es solo una estrategia y puede no ser aplicable para todas las personas. Y, por supuesto, si está volando solo, todavía tiene que mantener todo (incluido el formato de los datos en sí), pero la diferencia es que cuando se sienta para hacer mejoras en ese renderizador, realmente puede concentrarse en el renderizador en su mayor parte y nada más. Se vuelve tan aislado en su propio pequeño mundo, casi tan aislado como podría estar con los datos que requiere para que la entrada sea tan simple.
Y utilicé el ejemplo de un formato de archivo, pero no tiene que ser un archivo que proporcione los datos simplificados de interés para la entrada. Podría ser una base de datos en memoria. En mi caso, es un sistema de entidad-componente con los componentes que almacenan los datos de interés. Sin embargo, he encontrado que este principio básico de desacoplamiento hacia los datos simplificados (sin importar cómo lo hagas) es mucho menos exigente para mi capacidad mental que los sistemas anteriores en los que trabajé, que giraban en torno a abstracciones y montones y montones de interacciones entre todos estos interfaces abstractas que hacían imposible simplemente sentarse con una cosa y pensar solo en eso y poco más. Mi cerebro se llenó hasta el borde con esos tipos de sistemas anteriores y quería explotar porque había muchas interacciones entre tantas cosas,
Desacoplamiento
Si desea minimizar la cantidad de bases de código más grandes que gravan su cerebro, haga que cada parte considerable del software (un sistema de representación completo, un sistema de física completo, etc.) viva en el mundo más aislado posible. Minimice la cantidad de comunicación e interacción que pasa a los mínimos más mínimos a través de los datos más optimizados. Incluso podría aceptar cierta redundancia (algún trabajo redundante para el procesador, o incluso para usted mismo) si el intercambio es un sistema mucho más aislado que no tiene que hablar con docenas de otras cosas antes de que pueda hacer su trabajo.
Y cuando comienzas a hacer eso, parece que estás manteniendo una docena de aplicaciones a pequeña escala en lugar de una gigantesca. Y eso me parece mucho más divertido también. Puedes sentarte y trabajar en un sistema a tu gusto sin preocuparte por el mundo exterior. Simplemente se convierte en la entrada de los datos correctos y la salida de los datos correctos al final a algún lugar donde otros sistemas puedan acceder (en ese momento, otro sistema podría ingresar eso y hacer lo suyo, pero no tiene que preocuparse por eso cuando trabajas en tu sistema). Por supuesto, todavía tiene que pensar en cómo se integra todo en la interfaz de usuario, por ejemplo (todavía me encuentro teniendo que pensar en el diseño de todo para las GUI), pero al menos no cuando se sienta y trabaja en ese sistema existente. o decide agregar uno nuevo.
Quizás estoy describiendo algo obvio para las personas que se mantienen actualizadas con los últimos métodos de ingeniería. No lo sé. Pero no fue obvio para mí. Quería abordar el diseño de software en torno a objetos que interactúan entre sí y a las funciones que se requieren para software a gran escala. Y los libros que leí originalmente sobre diseño de software a gran escala se centraron en diseños de interfaz por encima de cosas como implementaciones y datos (el mantra en ese entonces era que las implementaciones no importan tanto, solo las interfaces, porque las primeras podrían intercambiarse fácilmente o sustituirse ) Al principio, no se me ocurrió intuitivamente pensar que las interacciones de un software se reducen a solo ingresar y emitir datos entre enormes subsistemas que apenas se comunican entre sí, excepto a través de estos datos simplificados. Sin embargo, cuando comencé a cambiar mi enfoque al diseño en torno a ese concepto, hizo las cosas mucho más fáciles. Podría agregar mucho más código sin que mi cerebro explote. Se sentía como si estuviera construyendo un centro comercial en lugar de una torre que podría derrumbarse si agregué demasiado o si hubo una fractura en alguna parte.
Implementaciones complejas versus interacciones complejas
Este es otro que debo mencionar porque pasé una buena parte de mi primera parte de mi carrera buscando las implementaciones más simples. Así que descompuse las cosas en los pedazos más pequeños y simples, pensando que estaba mejorando la capacidad de mantenimiento.
En retrospectiva, no me di cuenta de que estaba cambiando un tipo de complejidad por otro. Al reducir todo a las partes más simples, las interacciones que ocurrieron entre esas piezas pequeñas se convirtieron en la red más compleja de interacciones con llamadas a funciones que a veces fueron 30 niveles de profundidad en la pila de llamadas. Y, por supuesto, si observa cualquier función, es muy, muy simple y fácil saber lo que hace. Pero no está obteniendo mucha información útil en ese punto porque cada función está haciendo muy poco. Luego, terminas teniendo que rastrear todo tipo de funciones y saltar a través de todo tipo de aros para descubrir realmente lo que suman hacer de maneras que pueden hacer que tu cerebro quiera explotar más de uno más grande,
Eso no es sugerir objetos de Dios ni nada de eso. Pero tal vez no necesitemos dividir nuestros objetos de malla en las cosas más pequeñas, como un objeto de vértice, un objeto de borde, un objeto de cara. Tal vez podríamos mantenerlo en "malla" con una implementación moderadamente más compleja detrás de él a cambio de radicalmente menos interacciones de código. Puedo manejar una implementación moderadamente compleja aquí y allá. No puedo manejar millones de interacciones con efectos secundarios que ocurren quién sabe dónde y en qué orden.
Al menos eso me resulta mucho, mucho menos exigente para el cerebro, porque son las interacciones las que hacen que mi cerebro duela en una gran base de código. No hay una cosa específica.
Generalidad versus especificidad
Tal vez atado a lo anterior, me encantaba la generalidad y la reutilización de código, y solía pensar que el mayor desafío de diseñar una buena interfaz era satisfacer la más amplia gama de necesidades porque la interfaz sería utilizada por todo tipo de cosas diferentes con diferentes necesidades. Y cuando haces eso, inevitablemente tienes que pensar en cien cosas a la vez, porque estás tratando de equilibrar las necesidades de cien cosas a la vez.
Generalizar las cosas lleva mucho tiempo. Basta con mirar las bibliotecas estándar que acompañan a nuestros idiomas. La biblioteca estándar de C ++ contiene muy poca funcionalidad, pero requiere equipos de personas para mantener y sintonizar con comités enteros de personas que debaten y hacen propuestas sobre su diseño. Eso se debe a que esa pequeña funcionalidad está tratando de manejar las necesidades del mundo entero.
Quizás no necesitamos llevar las cosas tan lejos. Quizás esté bien tener un índice espacial que solo se use para la detección de colisiones entre mallas indexadas y nada más. Tal vez podamos usar otro para otros tipos de superficies, y otro para renderizar. Solía centrarme tanto en eliminar este tipo de redundancias, pero parte de la razón era porque estaba tratando con estructuras de datos muy ineficientes implementadas por una amplia gama de personas. Naturalmente, si tiene un octree que toma 1 gigabyte por una simple malla triangular de 300k, no querrá tener otro más en la memoria.
Pero, ¿por qué los octrees son tan ineficientes en primer lugar? Puedo crear octreos que solo toman 4 bytes por nodo y toman menos de un megabyte para hacer lo mismo que la versión de gigabytes mientras construyen en una fracción del tiempo y realizan consultas de búsqueda más rápidas. En ese punto, algo de redundancia es totalmente aceptable.
Eficiencia
Por lo tanto, esto solo es relevante para los campos críticos para el rendimiento, pero cuanto mejor obtenga en cosas como la eficiencia de la memoria, más podrá permitirse desperdiciar un poco más (tal vez acepte un poco más de redundancia a cambio de una menor generalidad o desacoplamiento) a favor de la productividad . Y allí ayuda a ser bastante bueno y cómodo con sus perfiladores y aprender sobre la arquitectura de la computadora y la jerarquía de la memoria, porque entonces puede permitirse hacer más sacrificios a la eficiencia a cambio de la productividad porque su código ya es tan eficiente y puede permitirse el lujo de ser un poco menos eficiente, incluso en las áreas críticas, mientras sigue superando a la competencia. Descubrí que mejorar en esta área también me ha permitido salir con implementaciones más y más simples,
Fiabilidad
Esto es algo obvio, pero bien podría mencionarlo. Sus cosas más confiables requieren el mínimo gasto intelectual. No tienes que pensar mucho en ellos. Ellos solo trabajan. Como resultado, cuanto más grande sea su lista de piezas ultra confiables que también son "estables" (no es necesario cambiar) a través de pruebas exhaustivas, menos tendrá que pensar.
Detalles específicos
Entonces, todo lo anterior cubre algunas cosas generales que me han sido útiles, pero pasemos a aspectos más específicos para su área:
En mis proyectos más pequeños, es fácil recordar un mapa mental de cómo funciona cada parte del programa. Al hacer esto, puedo ser plenamente consciente de cómo cualquier cambio afectará al resto del programa y evitar errores de manera muy efectiva, así como ver exactamente cómo una nueva característica debe encajar en la base del código. Sin embargo, cuando intento crear proyectos más grandes, me resulta imposible mantener un buen mapa mental que conduzca a un código muy desordenado y numerosos errores no intencionados.
Para mí, esto tiende a estar relacionado con efectos secundarios complejos y flujos de control complejos. Esa es una vista de nivel bastante bajo de las cosas, pero todas las interfaces más bonitas y todo el desacoplamiento del concreto al abstracto no pueden hacer que sea más fácil razonar sobre los efectos secundarios complejos que ocurren en los flujos de control complejos.
Simplifique / reduzca los efectos secundarios y / o simplifique los flujos de control, idealmente ambos. y generalmente le resultará mucho más fácil razonar sobre lo que hacen los sistemas mucho más grandes, y también sobre lo que sucederá en respuesta a sus cambios.
Además de este problema del "mapa mental", me resulta difícil mantener mi código desacoplado de otras partes de sí mismo. Por ejemplo, si en un juego multijugador hay una clase para manejar la física del movimiento de los jugadores y otra para manejar las redes, entonces no veo forma de que una de estas clases no dependa de la otra para llevar los datos de movimiento de los jugadores al sistema de redes para enviarlo a través de la red. Este acoplamiento es una fuente importante de la complejidad que interfiere con un buen mapa mental.
Conceptualmente tienes que tener algo de acoplamiento. Cuando las personas hablan de desacoplamiento, generalmente significan reemplazar un tipo con otro, un tipo más deseable (generalmente hacia abstracciones). Para mí, dado mi dominio, la forma en que funciona mi cerebro, etc., el tipo más deseable para reducir los requisitos de "mapa mental" a un mínimo es que los datos simplificados discutidos anteriormente. Un recuadro negro escupe datos que se introducen en otro recuadro negro, y ambos completamente ajenos a la existencia del otro. Solo conocen algún lugar central donde se almacenan los datos (por ejemplo: un sistema de archivos central o una base de datos central) a través del cual obtienen sus entradas, hacen algo y escupen una nueva salida que luego podría ingresar otra caja negra.
Si lo hace de esta manera, el sistema de física dependería de la base de datos central y el sistema de red dependería de la base de datos central, pero no sabrían nada el uno del otro. Ni siquiera tendrían que conocerse entre sí. Ni siquiera tendrían que saber que existen interfaces abstractas entre sí.
Por último, a menudo me encuentro con una o más clases de "gerente" que coordinan otras clases. Por ejemplo, en un juego, una clase manejaría el ciclo principal y llamaría a métodos de actualización en las clases de redes y jugadores. Esto va en contra de una filosofía de lo que he encontrado en mi investigación de que cada clase debe ser comprobable por unidad y utilizable independientemente de los demás, ya que cualquier clase de administrador por su propio propósito se basa en la mayoría de las otras clases en el proyecto. Además, una orquestación de clases de gerente del resto del programa es una fuente importante de complejidad no mapeable mental.
Tiende a necesitar algo para organizar todos los sistemas de su juego. Central es quizás al menos menos complejo y más manejable que como un sistema de física que invoca un sistema de renderizado una vez hecho. Pero aquí inevitablemente necesitamos que se invoquen algunas funciones, y preferiblemente son abstractas.
Por lo tanto, puede crear una interfaz abstracta para un sistema con una update
función abstracta . Luego puede registrarse con el motor central y su sistema de red puede decir: "Hola, soy un sistema y aquí está mi función de actualización. Llámeme de vez en cuando". Y luego su motor puede recorrer todos esos sistemas y actualizarlos sin necesidad de codificar llamadas de función a sistemas específicos.
Eso permite que sus sistemas vivan más como en su propio mundo aislado. El motor del juego ya no tiene que saber sobre ellos específicamente (de manera concreta). Y luego su sistema de física podría tener su función de actualización llamada, en cuyo punto ingresa los datos que necesita de la base de datos central para todo el movimiento, aplica la física y luego devuelve el movimiento resultante.
Después de eso, su sistema de red puede tener su función de actualización llamada, en cuyo punto ingresa los datos que necesita de la base de datos central y emite, por ejemplo, datos de socket a los clientes. Una vez más, el objetivo, como lo veo, es aislar cada sistema tanto como sea posible para que pueda vivir en su propio pequeño mundo con un conocimiento mínimo del mundo exterior. Ese es básicamente el tipo de enfoque adoptado en ECS que es popular entre los motores de juegos.
ECS
Supongo que debería cubrir ECS un poco, ya que muchos de mis pensamientos anteriores giran en torno a ECS y tratar de racionalizar por qué este enfoque orientado a datos para el desacoplamiento ha hecho que el mantenimiento sea mucho más fácil que los sistemas orientados a objetos y basados en COM que he mantenido en el pasado a pesar de violar casi todo lo que consideraba sagrado originalmente que aprendí sobre SE. También podría tener mucho sentido para ti si estás tratando de construir juegos a gran escala. Entonces ECS funciona así:
Y como en el diagrama anterior, la función MovementSystem
podría tener update
llamada. En este punto, puede consultar los PosAndVelocity
componentes de la base de datos central como los datos a ingresar (los componentes son solo datos, sin funcionalidad). Entonces podría recorrerlos, modificar las posiciones / velocidades y generar los nuevos resultados de manera efectiva. Luego, se RenderingSystem
podría llamar a su función de actualización, en cuyo punto consulta la base de datos PosAndVelocity
y los Sprite
componentes, y genera imágenes en la pantalla en función de esos datos.
Todos los sistemas son completamente ajenos a la existencia del otro, y ni siquiera necesitan entender qué Car
es. Solo necesitan conocer componentes específicos del interés de cada sistema que componen los datos necesarios para representar uno. Cada sistema es como una caja negra. Introduce datos y genera datos con un conocimiento mínimo del mundo exterior, y el mundo exterior también tiene un conocimiento mínimo de él. Puede haber algún evento empujando desde un sistema y explotando desde otro para que, por ejemplo, la colisión de dos entidades en el sistema de física pueda hacer que el audio vea un evento de colisión que haga que suene, pero los sistemas aún pueden ser ajenos acerca de cada uno. Y he encontrado que tales sistemas son mucho más fáciles de razonar. No hacen que mi cerebro quiera explotar, incluso si tienes docenas de sistemas, porque cada uno está muy aislado. Usted no No tiene que pensar en la complejidad de todo en su conjunto cuando se acerca y trabaja en cualquiera. Y debido a eso, también es muy fácil predecir los resultados de sus cambios.