Puntos clave
1. La arquitectura de software trata de gestionar el cambio y minimizar la carga cognitiva.
Para mí, un buen diseño significa que cuando hago un cambio, es como si todo el programa hubiera sido creado anticipándose a él.
El objetivo principal de la arquitectura. La arquitectura de software se basa fundamentalmente en facilitar el cambio. Un sistema bien diseñado anticipa modificaciones futuras, permitiendo a los desarrolladores implementar nuevas funciones o corregir errores con la mínima interrupción. La facilidad con la que una base de código se adapta a los cambios es la medida definitiva de su calidad arquitectónica.
Reducción de la carga cognitiva. Un aspecto clave de una buena arquitectura es minimizar la cantidad de información que un desarrollador necesita comprender antes de hacer un cambio. Los patrones de desacoplamiento y el diseño modular ayudan a reducir esta carga cognitiva al permitir que los desarrolladores razonen sobre componentes individuales de forma aislada. Esto minimiza el riesgo de efectos secundarios no deseados y hace que la base de código sea más fácil de navegar.
El flujo de programación. El flujo de programación implica entender el código existente, idear una solución, implementarla y reorganizar el código. La arquitectura de software se centra en la fase de aprendizaje. Cargar código en las neuronas es lento, por lo que vale la pena encontrar estrategias para reducir su volumen.
2. El desacoplamiento reduce el volumen de código necesario para entender un cambio.
Si los desacoplas, puedes razonar sobre cada lado de forma independiente.
Definición de desacoplamiento. El desacoplamiento se refiere al grado en que dos fragmentos de código pueden entenderse y modificarse de forma independiente. El código fuertemente acoplado obliga al desarrollador a comprender las complejidades de ambos componentes antes de hacer cualquier cambio, aumentando el riesgo de errores y dificultando el mantenimiento. El desacoplamiento busca minimizar estas dependencias, permitiendo que los desarrolladores se enfoquen en componentes individuales sin necesidad de entender todo el sistema.
Beneficios del desacoplamiento. El desacoplamiento reduce la cantidad de conocimiento que necesitas tener antes de avanzar. También significa que un cambio en una parte del código no requiere modificar otra. Cuanto menos acoplamiento haya, menos se propaga ese cambio por el resto del juego.
Estrategias de desacoplamiento. El desacoplamiento se puede lograr mediante diversos patrones de diseño y principios arquitectónicos, como interfaces, clases abstractas y colas de mensajes. Estas técnicas permiten que los componentes interactúen a través de contratos bien definidos, reduciendo la necesidad de dependencias directas y fomentando la modularidad.
3. La abstracción y la extensibilidad tienen un costo: complejidad y especulación.
Cada vez que añades una capa de abstracción o un punto donde se soporta la extensibilidad, estás especulando que necesitarás esa flexibilidad más adelante.
El atractivo de la abstracción. La abstracción, la modularidad y los patrones de diseño pueden dar lugar a programas bien arquitectados que resultan un placer para trabajar. Una buena arquitectura marca una gran diferencia en la productividad. Sin embargo, estos beneficios tienen un precio.
El costo de la flexibilidad. Una buena arquitectura requiere esfuerzo y disciplina reales. Debes pensar en qué partes del programa deben desacoplarse e introducir abstracciones en esos puntos. Asimismo, tienes que determinar dónde debe diseñarse la extensibilidad para que los cambios futuros sean más fáciles.
El principio YAGNI. La abstracción excesiva puede llevar a bases de código con interfaces, sistemas de plugins y métodos virtuales en exceso, dificultando rastrear el código real que realiza una tarea. El principio "You Aren't Gonna Need It" (YAGNI) recuerda evitar la optimización prematura y centrarse en resolver el problema inmediato.
4. La optimización del rendimiento se basa en suposiciones y limitaciones concretas.
El rendimiento se trata de suposiciones.
Flexibilidad vs. rendimiento. La arquitectura de software busca hacer tu programa más flexible, es decir, que requiera menos esfuerzo para cambiarlo. Eso implica codificar menos suposiciones en el programa. Pero el rendimiento se basa en suposiciones. La práctica de la optimización prospera con limitaciones concretas.
Compromisos en la optimización. Optimizar consume mucho tiempo de ingeniería. Una vez hecho, tiende a calcificar la base de código: el código altamente optimizado es inflexible y muy difícil de modificar.
El compromiso del prototipado. Una solución es mantener el código flexible hasta que el diseño se estabilice y luego eliminar parte de la abstracción para mejorar el rendimiento. Es más fácil hacer un juego divertido rápido que hacer un juego rápido divertido.
5. La simplicidad alivia las restricciones sobre arquitectura, rendimiento y velocidad de desarrollo.
Me esfuerzo mucho por escribir la solución más limpia y directa al problema.
La simplicidad como principio guía. Escribir soluciones limpias y directas minimiza la cantidad de código, lo que a su vez reduce la carga cognitiva necesaria para entenderlo y modificarlo. El código simple suele ejecutarse más rápido debido a la menor sobrecarga y menos líneas de ejecución.
Simplicidad y tiempo. El código simple no siempre toma menos tiempo escribirlo. Una buena solución no es una acumulación de código, sino su destilación. Encontrarla es como reconocer patrones o resolver un rompecabezas. Requiere esfuerzo ver más allá de la dispersión de casos de uso para descubrir el orden oculto que los subyace.
El valor de la elegancia. Las soluciones elegantes son generales: un pequeño fragmento de lógica que cubre correctamente un amplio espectro de casos. Encontrarlas es como reconocer patrones o resolver un rompecabezas. Requiere esfuerzo ver más allá de la dispersión de casos de uso para descubrir el orden oculto que los subyace.
6. El patrón Command convierte llamadas a métodos en objetos para entrada configurable y deshacer/rehacer.
Un comando es una llamada a método reificada.
Llamadas a métodos reificadas. El patrón Command encapsula una solicitud como un objeto, permitiendo parametrizar clientes con diferentes solicitudes, encolarlas, registrarlas y soportar operaciones deshacibles. Es una llamada a método envuelta en un objeto.
Configuración de entrada. El patrón Command permite asignar configuraciones de entrada personalizables al asociar cada pulsación de botón con un objeto Command. Esto permite a los jugadores personalizar controles sin cambiar la lógica central del juego.
Funcionalidad de deshacer/rehacer. Al implementar métodos execute()
y undo()
en las subclases de Command, el patrón simplifica la implementación de la funcionalidad de deshacer/rehacer. Cada comando almacena el estado necesario para revertir sus efectos, permitiendo a los usuarios revertir acciones fácilmente.
7. El patrón Flyweight conserva memoria compartiendo estado intrínseco.
Flyweight, como su nombre indica, se usa cuando tienes objetos que deben ser más livianos, generalmente porque hay demasiados.
Objetos livianos. El patrón Flyweight reduce el consumo de memoria separando el estado de un objeto en componentes intrínsecos (compartidos) y extrínsecos (únicos). El estado intrínseco se almacena en un objeto compartido, mientras que el extrínseco se pasa según sea necesario.
Ejemplo de terreno. El patrón Flyweight puede aplicarse a los mosaicos de terreno en un mundo de juego. Cada mosaico puede almacenar un puntero a un objeto Terrain compartido, que contiene las propiedades del tipo de terreno (por ejemplo, coste de movimiento, textura). Esto evita almacenar datos redundantes para cada mosaico.
Soporte de hardware. El patrón Flyweight puede ser el único patrón de diseño del Gang of Four con soporte real en hardware. Con renderizado instanciado, la tarjeta gráfica puede renderizar los datos compartidos una sola vez y luego enviar por separado los datos únicos de cada instancia — su posición, color y escala.
8. El patrón Observer permite notificaciones desacopladas, pero requiere gestión cuidadosa.
Permite que un fragmento de código anuncie que algo interesante ocurrió sin importar quién recibe la notificación.
Comunicación desacoplada. El patrón Observer permite que un objeto (el sujeto) notifique a múltiples otros objetos (observadores) sobre cambios de estado sin conocer sus tipos específicos. Esto fomenta un acoplamiento débil y modularidad.
Ejemplo de sistema de logros. El patrón Observer puede usarse para implementar un sistema de logros. El motor de física puede notificar a los observadores cuando una entidad cae, y el sistema de logros puede verificar si la entidad es el héroe y si cayó de un puente para desbloquear el logro "Caída de un puente".
Gestión de memoria. Al usar el patrón Observer, es importante gestionar la vida útil de sujetos y observadores para evitar punteros colgantes o fugas de memoria. Los observadores deben darse de baja de los sujetos cuando se destruyen.
9. El patrón Prototype ofrece clonación como alternativa a la instanciación tradicional.
La idea clave es que un objeto puede generar otros objetos similares a sí mismo.
Creación prototípica de objetos. El patrón Prototype permite crear nuevos objetos clonando objetos existentes (prototipos). Esto evita la necesidad de lógica compleja en constructores y permite crear objetos con estados iniciales personalizados.
Ejemplo de generador. El patrón Prototype puede usarse para implementar generadores de monstruos. En lugar de tener una clase generadora para cada tipo de monstruo, una sola clase puede clonar una instancia prototipo para crear nuevos monstruos.
Modelado de datos. La delegación prototípica es ideal para definir datos en juegos. La espada mágica que decapita, que en realidad es una espada larga con algunos bonos, puede expresarse así directamente:
json
{
"nombre": "Espada Decapitadora",
"prototipo": "espada larga",
"bonoDeDaño": "20"
}
10. El patrón State encapsula comportamientos específicos de estado, pero puede usarse en exceso.
Permite que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parecerá cambiar de clase.
Comportamiento dependiente del estado. El patrón State permite que un objeto cambie su comportamiento según su estado interno. Cada estado se representa con una clase separada, y el objeto delega las llamadas a métodos a su objeto de estado actual.
Ejemplo de heroína. El patrón State puede usarse para implementar el comportamiento de una heroína en un juego de plataformas. La heroína puede tener estados como estar de pie, saltar, agacharse y lanzarse. Cada clase de estado define el comportamiento para cada entrada.
Transiciones de estado. Para cambiar de estado, asignamos state_
para que apunte al nuevo. Si el objeto estado no tiene otros campos, solo almacena un puntero a la tabla virtual interna para que se puedan llamar sus métodos. En ese caso, no hay razón para tener más de una instancia.
11. El patrón Game Loop desacopla el tiempo del juego de la entrada del usuario y la velocidad del procesador.
Desacopla la progresión del tiempo del juego de la entrada del usuario y la velocidad del procesador.
Velocidad de juego consistente. El patrón Game Loop asegura que el juego funcione a una velocidad constante sin importar el hardware o la entrada del usuario. Esto se logra desacoplando la lógica de actualización del juego del proceso de renderizado.
Pasos de tiempo variables. En un bucle de juego con pasos de tiempo variables, el tiempo que pasa entre cada actualización no es fijo, sino que varía según la tasa de frames. El motor avanza el mundo del juego esa cantidad de tiempo.
Paso de actualización fijo, renderizado variable. El juego simula a una tasa constante usando pasos de tiempo fijos seguros en distintos hardware. Solo que la ventana visible para el jugador se vuelve más entrecortada en máquinas lentas.
12. El patrón Update Method simula objetos independientes mediante actualizaciones secuenciales.
Simula una colección de objetos independientes diciéndole a cada uno que procese un cuadro de comportamiento a la vez.
Simulación concurrente de objetos. El patrón Update Method simula una colección de objetos independientes llamando a un método update()
en cada objeto cada frame. Esto da a cada objeto la oportunidad de actualizar su estado y comportamiento.
Ejemplo de entidad. El patrón Update Method puede usarse para simular entidades en un mundo de juego. Cada entidad tiene un método update()
que se llama cada frame. Este método puede manejar IA, física y animación.
Consideraciones de rendimiento. El patrón Update Method puede optimizarse usando técnicas de localidad de datos. Esto implica almacenar los datos de todas las entidades en un bloque contiguo de memoria, mejorando el rendimiento de caché.
13. El doble buffer crea la ilusión de simultaneidad desacoplando acceso y modificación de datos.
Hace que una serie de operaciones secuenciales parezcan instantáneas o simultáneas.
Actualizaciones atómicas de estado. El patrón Double Buffer permite modificar el estado de forma incremental asegurando que el código externo siempre vea una instantánea consistente y atómica de los datos. Esto se logra manteniendo dos copias de los datos: un buffer actual y un buffer siguiente.
Ejemplo en renderizado gráfico. El patrón Double Buffer se usa comúnmente para evitar el tearing en gráficos. El código de renderizado escribe en el buffer siguiente, mientras el controlador de video lee del buffer actual. Al terminar el renderizado, se intercambian los buffers.
Consideraciones de rendimiento. El intercambio toma tiempo. El doble buffer requiere un paso de swap una vez que el estado ha sido modificado. Esa operación debe ser atómica: ningún código puede acceder a ninguno de los estados mientras se intercambian.
14. El patrón Service Locator ofrece un punto global de acceso a servicios minimizando el acoplamiento.
Proporciona un punto global de acceso a un servicio sin acoplar a los usuarios con la clase concreta que lo implementa.
Acceso desacoplado a servicios. El patrón Service Locator ofrece un punto global para acceder a un servicio sin acoplar el código que lo usa a la implementación concreta. Esto permite mayor flexibilidad y facilidad para pruebas.
Ejemplo de sistema de audio. El patrón Service Locator puede usarse para acceder al sistema de audio. La clase Locator
ofrece un método getAudio()
que devuelve una instancia de la interfaz Audio
. La implementación real del sistema de audio puede cambiar sin afectar el código que lo usa.
Servicio nulo. Si el servicio no se encuentra, el localizador puede devolver un servicio nulo. Este implementa la interfaz del servicio pero no hace nada. Así el juego puede seguir funcionando aunque el servicio no esté disponible.
15. El patrón Subclass Sandbox define comportamiento en subclases usando operaciones de la clase base.
Define el comportamiento en una subclase usando un conjunto de operaciones proporcionadas por su clase base.
Comportamiento restringido en subclases. El patrón Subclass Sandbox define el comportamiento en una subclase usando operaciones de la clase base. Esto limita el acceso de la subclase al resto del sistema, promoviendo encapsulación y reduciendo acoplamiento.
Ejemplo de superpoder. El patrón Subclass Sandbox puede usarse para implementar superpoderes en un juego. La clase base Superpower
ofrece métodos para mover al héroe, reproducir sonidos y generar partículas. Las subclases implementan superpoderes específicos llamando a estos métodos.
Jerarquías de herencia amplias. Este patrón conduce a una arquitectura con una jerarquía de clases poco profunda pero amplia. Las cadenas de herencia no son profundas, pero hay muchas clases derivadas directas de Superpower
. Tener una clase con muchas subclases directas ofrece un punto de apalancamiento en la base de código.
16. El patrón Bytecode da flexibilidad de datos al comportamiento mediante una máquina virtual.
Da al comportamiento la flexibilidad de los datos codificándolo como instrucciones para una máquina virtual.
Comportamiento dirigido por datos. El patrón Bytecode permite definir el comportamiento en datos en lugar de código. Esto facilita la flexibilidad, la modificación y el sandboxing seguro.
Ejemplo de sistema de hechizos. El patrón Bytecode puede usarse para implementar un sistema de hechizos en un juego. Cada hechizo se define como una secuencia de instrucciones bytecode ejecutadas por una máquina virtual. Esto permite a los diseñadores crear nuevos hechizos sin programar.
Máquina de pila. La VM mantiene una pila interna de valores. En nuestro ejemplo, los únicos valores con los que trabajan las instrucciones son números, por lo que usamos un simple arreglo de enteros. Cuando un dato debe pasar de una instrucción a otra, lo hace a través de la pila.
Última actualización:
FAQ
What's Game Programming Patterns about?
- Focus on Game Development: Game Programming Patterns by Robert Nystrom is a comprehensive guide that delves into design patterns specifically tailored for game development.
- Patterns for Better Code: It presents various design patterns that help create cleaner, more efficient, and maintainable code, crucial as game projects grow in complexity.
- Practical Examples: The book includes practical examples and code snippets, primarily in C++, to demonstrate how these patterns can be implemented in real-world scenarios.
Why should I read Game Programming Patterns?
- Improve Code Quality: The book introduces design patterns that promote better organization and structure in your code, enhancing your coding skills.
- Learn from Experience: Robert Nystrom shares insights from his industry experience, particularly from his time at Electronic Arts, offering valuable lessons to avoid common pitfalls.
- Accessible to All Levels: Designed to be approachable, it is a great resource for both beginners and experienced developers looking to improve their game programming skills.
What are the key takeaways of Game Programming Patterns?
- Importance of Design Patterns: The book emphasizes how design patterns can lead to cleaner and more maintainable code, with detailed discussions on patterns like Command, Observer, and State.
- Decoupling Code: A major theme is the need to decouple code to make it easier to manage and extend, especially in games where different systems must interact.
- Performance Considerations: It addresses performance issues specific to games, such as efficient memory management and optimizing code for real-time execution.
What are the best quotes from Game Programming Patterns and what do they mean?
- "Every kid has dreamed of being a superhero...": This introduces the Subclass Sandbox pattern, emphasizing a data-driven approach to manage multiple behaviors without redundancy.
- "A virtual machine... is often as simple as a stack, a loop, and a switch statement.": Highlights the simplicity of creating complex behaviors through a well-structured virtual machine.
- "The more you can use stuff in that cache line, the faster you go.": From the Data Locality section, it underscores the importance of organizing data in memory to optimize CPU cache usage.
How does the Command pattern work in Game Programming Patterns?
- Encapsulating Requests: The Command pattern encapsulates a request as an object, allowing for parameterization of clients with different requests.
- Decoupling Actions: It decouples the sender of a request from the object that handles it, making code more flexible and easier to manage.
- Example Implementation: Nystrom illustrates this with an input handler example, using command objects to represent actions like jumping or firing a weapon.
How does the State pattern function in Game Programming Patterns?
- Behavioral Changes: The State pattern allows an object to alter its behavior when its internal state changes, useful for managing complex behaviors in game characters.
- Encapsulation of State Logic: Each state is represented by a class implementing a common interface, keeping the code organized and simplifying transitions between states.
- Example Usage: Nystrom provides an example of a game character in different states, each handling its own input and behavior.
What is the Game Loop pattern in Game Programming Patterns?
- Continuous Execution: The Game Loop pattern is essential for running a game continuously, processing user input, updating game state, and rendering graphics.
- Decoupling Time from Input: It decouples game time progression from user input and processor speed, ensuring consistent gameplay across different hardware.
- Implementation Example: The book outlines a basic game loop structure, providing a foundation for any game engine.
How does the Observer pattern work in Game Programming Patterns?
- Decoupled Communication: The Observer pattern allows one object to notify multiple observers about changes in its state without being tightly coupled to them.
- Event Handling: Useful for implementing event-driven systems, such as notifying different parts of a game when a player achieves something.
- Example Implementation: Nystrom illustrates this with a physics engine example that notifies an achievement system when an event occurs.
What is the Double Buffer pattern in Game Programming Patterns?
- Preventing Tearing: The Double Buffer pattern prevents visual tearing in graphics rendering by maintaining two buffers for displaying and drawing frames.
- Atomic Updates: By swapping buffers at the end of each frame, the game presents a complete image without showing intermediate states.
- Implementation Details: The book provides a simple implementation, demonstrating efficient buffer management for smooth visual results.
How does the Object Pool pattern work in Game Programming Patterns?
- Reuse Objects Efficiently: The Object Pool pattern allows for the reuse of objects from a fixed pool, reducing memory fragmentation and improving performance.
- Fixed Pool Size: It creates a collection of objects upfront, avoiding the overhead of frequent memory allocation.
- Manage Active Objects: Objects can be marked as "in use" or available, allowing the pool to manage reuse without new memory allocation.
What is the purpose of the Dirty Flag pattern in Game Programming Patterns?
- Track Changes Efficiently: The Dirty Flag pattern avoids unnecessary recalculations by deferring updates until derived data is needed.
- Set and Clear Flags: When primary data changes, the dirty flag is set, and recalculations occur only if necessary when derived data is requested.
- Improve Performance: This ensures expensive calculations are performed only when required, leading to smoother performance.
How does the Data Locality pattern enhance performance in Game Programming Patterns?
- Optimize Memory Access: The Data Locality pattern arranges data in memory to take advantage of CPU caching, reducing cache misses.
- Contiguous Memory Layout: Organizing data structures contiguously allows the CPU to load larger data chunks efficiently.
- Impact on Game Performance: Optimizing data locality significantly enhances game speed and responsiveness, crucial for performance.
Reseñas
Patrones de Programación de Juegos es ampliamente reconocido por sus explicaciones claras, su estilo de escritura ameno y sus ejemplos prácticos de patrones de diseño aplicados al desarrollo de videojuegos. Los lectores valoran el humor del autor, sus reflexiones sobre consideraciones de rendimiento y la discusión equilibrada de las ventajas y desventajas de cada patrón. Muchos lo consideran valioso tanto para principiantes como para desarrolladores experimentados, y algunos destacan su relevancia más allá del ámbito de la programación de juegos. Entre las pocas críticas se mencionan ejemplos en C++ algo desactualizados y un enfoque centrado en ciertos tipos de juegos. En conjunto, la mayoría de los reseñadores lo consideran una lectura imprescindible para quienes crean videojuegos.
Similar Books








