Aprender sobre C++, Motores ECS e IA

Sitio: Moodle UA 2024-25
Curso: VIDEOJUEGOS I (21038)
Libro: Aprender sobre C++, Motores ECS e IA
Imprimido por: , Invitado
Día: lunes, 11 de mayo de 2026, 00:30

Cómo aprender y qué recursos usar

¿Qué haremos en V1 y V2?

En V1 y V2 nos vamos a centrar en programar el motor del juego y la IA. Para el motor del juego utilizaremos arquitectura Entity-Component-System. Dentro de la IA veremos los algoritmos esenciales como los Steering Behaviours y los Behaviour Trees. 

En todas las clases aprendemos a programar en C++ moderno (17, 20 y 23). Trabajaremos mucho C++ en los Challenges. Es muy importante que lo pongáis en práctica en vuestro ABP. Evitad programar como en cursos anteriores: cuánto más os esforcéis por adquirir nuevas y buenas prácticas de programación, más rápido irá vuestro proyecto y más aprenderéis.

Vale, ¿Cómo lo hago?

Primero de todo, conoce lo más básico de un ECS en C++ y aprende a hacerlo por ti mismx. Empieza por esta serie de 3 vídeos:

Una vez tengas las bases, organízate según tu rol: ¿Programarás parte del motor o te centrarás en la IA?

Para programadores del Motor

Hay 2 series de vídeos y un libro sobre programación del motor ECS. Te explico los detalles:

  • Serie de 2020 (2020. GameEngine ECS desde 0 en C++17)
    • Descripción: empieza desde 0, enseñando makefiles, C++, cómo abrir una ventana básica y dibujar tus propios sprites y cómo empezaríamos con un motor usando herencia, que es lo que conocéis de cursos anteriores. Llegado un punto, vemos que eso se complica y no es la forma de hacerlo. Eso nos enseña para qué sirven las templates. Vemos templates, cambiamos el motor y lo terminamos. Todo mientras aprendemos C++17.
    • Ventajas: es más sencillo y os ayuda a ir aprendiendo más paso a paso, desde lo que sabéis. El motor es más sencillo de implementar. Puedes apoyarte en el Libro de Laureano Canto Berná (ECS).
    • Inconvenientes: El motor es mucho menos flexible y menos óptimo. No aprendéis técnicas avanzadas.
  • Libro de Laureano Canto Berná (ECS)
    • Escrito por Laureano en su TFG en 2022, es un libro que explica cómo crear un ECS básico en base a 3 mini-proyectos. Cubre principalmente las ideas básicas y las ideas de la Serie de 2020, con algunos detalles de la Serie de 2022. Es un apoyo muy interesante porque da más ideas y ejemplos que no están en los vídeos de teoría y os puede ayudar a aprender más.
  • Serie de 2022 (2022. GameEngine ECS C++20 Avanzado)
    • Un ECS completo, desarrollado usando templates y técnicas de programación modernas. Es muchísimo más óptimo y flexible. Permite crear muchos más tipos de componentes, tags y sistemas. Los sistemas son más fáciles de manejar por el equipo de desarrollo y hace que el trabajo en equipo sea más rápido y avanzado. Se pueden realizar muchas más funcionalidades avanzadas con más facilidad y menos esfuerzo. Eso sí, es tecnológicamente más avanzado, por lo que requiere entender muchos más conceptos y alcanzar mayor nivel de programación. Este es el motor que se usa en la serie sobre IA.
    • Ventajas: Más flexible y óptimo. Se aprenden muchas cosas de C++ moderno y orientación a datos que son importantes para la industria. Da muchas más funcionalidades al equipo, permitiendo hacer sistemas y componentes más versátiles y fáciles de crear.
    • Inconvenientes: Más avanzado. Es recomendable haber empezado primero por el ECS más sencillo antes de abordar este (aunque no necesario). Muchos conceptos que entender. Hay que llegar a un nivel más alto en programación, por lo que requiere más esfuerzo. No está explicado en el libro, sólo en los vídeos.

Puedes realizar cualquiera de los dos. Con los dos podréis hacer vuestros juegos y tener buenos resultados. Debes decidir, sobre todo, en función de cuáles sean vuestros objetivos de aprendizaje, producto y portfolio. También, en función de vuestro nivel de programación, el tiempo que esperéis dedicar y el nivel que queráis alcanzar.

Otra posibilidad es empezar por el más sencillo y progresar al más avanzado si os véis avanzando bien y entendiendo el material. En el motor sencillo tenéis vídeos y libro para apoyaros.

Para programadores de gameplay e IA

Este es tu caso si vas a usar el motor, pero no vas a involucrarte en su programación de forma muy directa. Aunque no lo vayas a programar, necesitas tener conocimientos de cómo funciona y cómo está programado. Por eso, es conveniente que trabajes cómo mínimo el libro de laureano y algunos vídeos de la serie con la que está implementado tu motor.

Los vídeos de IA de 2021 utilizan un motor como el de la serie de 2022. Si cuando veas la serie no entiendes bien cómo creo y utilizo los sistemas, apoyate en los vídeos de 2022 para entenderlo y haz algunas prácticas de crear partes de tu propio motor. Necesitas entenderlo bien y saber usarlo.

Serie de IA (2021. IA en C++ desde 0)

  • Steering Behaviours
  • Decision Trees
  • Behaviour Trees
  • ImGui
  • Ensamblador x86-32
Quiero saber más

Si quieres saber más sobre motores y orientación a datos, te recomiendo el libro de Richard Fabian, Data Oriented Design. Es un libro más teórico y avanzando, pero muy bueno. Si de verdad quieres profundizar en estos temas y entender muchas más cosas, estando preparado para hacer mejores diseños y para trabajar en esto en la industria, este es tu libro. 

Entendiendo los objetos en C++

En C++, todo lo que los programas hacen es manipular objetos: los crean y destruyen, los leen, escriben y operan. Necesitamos comprender y dominar cómo los objetos se representan en memoria y cómo se manejan sus propiedades. Veamos qué es un objeto en C++, qué características tiene, cómo se gestiona su memoria y cómo funciona.

Objetos en C++

  • Definición: un objeto es una región de memoria con una semántica asociada.
  • Región de memoria: es un término genérico, pero se refiere a un espacio de memoria accesible por la CPU, es decur,  RAM, Registros o incluso ROM. Como la unidad mínima es el byte, este espacio tendrá un número entero de bytes.
  • Semántica asociada: define qué significan los posibles valores almacenados en el objeto, y qué operaciones están lógicamente permitidas sobre ese objeto (y, por tanto, qué otras cosas no lo están). Es la forma de explicar qué significa cualquier valor concreto de ese objeto y, también, de definir el conjunto de valores permitidos (llamados invariantes de un objeto).

Los programas crean, destruyen, se refieren a, y manipulan objetos.

Nota: para referirse a un objeto en memoria, sin importar cuanto ocupa, se indica siempre la dirección de su primer byte. Como sabemos su tamaño, sabiendo su primer byte, podemos leerlo o modificarlo sin equivocarnos.

Un objeto en C++ tiene:

  • Tamaño (size): número de bytes que ocupa. Se puede saber usando el operador sizeof.
    • Ejemplo:
          int vidas = 25;
      std::print("Tamaño de 'vidas' = {}\n", sizeof(vidas));
  • Alineamiento requerido (alignment): múltiplo de byte en memoria donde puede estar el inicio del objeto. Podemos saberlo con el operador alignof.
  • Duración de almacenamiento (storage duration): es una condición que determina cuánto tiempo será válido el objeto.
    • Automático (automatic): se crea en un ámbito (scope) y se destruye al terminar ese ámbito. Solemos llamarlo "local",
    • Estático (static): se crea antes de empezar main() y se destruye tras terminar main(). Tiene la misma duración que el programa.
    • Dinámico (dynamic): lo creamos y destruimos manualmente a conveniencia (con new/malloc y  delete/free ).
    • Local a hilo (thread-local): que pertence exclusivamente a un hilo de ejecución, y su creación y destrucción dependerá de la vida del hilo.
  • Tiempo de vida (lifetime): intervalo de tiempo desde que un objeto se crea hasta que se destruye. Tiene dos posibles condiciones:
    • Está limitado por el tipo de duración de almacenamiento (storage duration) del objeto.
    • Puede ser temporal: en objetos que sólo existen durante la evaluación de la expresión en que están involucrados.
      • Ejemplo:
         // 25 es un objeto temporal. Deja de existir tras ejecutar esta línea
         int vidas = 25;
         
         // Se llama a random(1,10) y eso devuelve un int temporal.
         // Se suma 100 + el int temporal y eso produce otro int temporal.
         // El objeto health se inicializa con el último int temporal.
         // Todos los temporales dejan de existir tras esta línea.
         float health = 100 + random(1, 10); 
  • Tipo (type): clase dentro de los posibles tipos de objeto. Determina los invariantes y el conjunto de operaciones válidas.
  • Valor (value): el valor que tiene el objeto en un instante, que puede ser indeterminado, por ejemplo, en la inicialización por defecto (default-initialized).
    • Nota: indeterminado quiere decir que tiene un valor, pero que no podemos saber cuál es. Si se usa una posición de memoria para el objeto, pero no se escribe en ella un valor inicial (inicializar), tendrá un valor previo que no sabemos cuál es. Es indeterminado.
  • Nombre (name) --Opcional--: los objetos pueden tener un nombre y, entonces, pueden ser variables.

Variable: un objeto o referencia que se introduce con una (declaration) y que no es ni un dato miembro, ni un miembro no-estatico.

Nota: un dato miembro es un campo de una estructura o clase, que no es estático. Los datos estáticos, aunque se declaren dentro de una clase, son únicos y globales como cualquier estático. Se crean antes de que empiece el programa y existen hasta su terminación. Por eso son únicos y no dependen de la creación de objetos de estructura o clase. Son, a todos los efectos, datos estáticos globales a los que limitamos su visibilidad.

Ejemplos

Considera el siguiente código:

10 | int main () {
11 |    int vidas = 25; // Un objeto tipo int que ocupa 4 bytes
...|    //...
56 | }

En este ejemplo, vidas es un objeto en C++ con:

  • Nombre: vidas (al tener nombre, es una variable)
  • Tamaño: 4 bytes
  • Valor: 25
  • Tipo: int (indica sus semánticas asociadas: se representará en binario como un número aritmético estándar, y admitirá operaciones aritméticas como suma, resta, multiplicación, etc.) 
  • Alineamiento: 4 bytes (sólo puede estar en memoria en celdillas múltiplo de 4 bytes)
  • Storage duration: Automático (se crea en el stack y se libera al terminar su scope/ámbito)
  • Tiempo de vida: Líneas 11 a 56 (se crea al ejecutar la 11, se libera al ejecutar la 56)

Considera ahora este otro código:

04 | // Definición de estructura Entity que ocupará 32 bytes
05 | struct Entity { 
06 |    int x, y, vx, vy;    // 4 ints = 16 bytes
07 |    double attrs[2];     // 2 doubles = 16 bytes
08 | };
.. |
40 | int main () {
41 |    static Entity* ep = new Entity();
.. |    //...
56 | }

En este ejemplo se crean 2 objetos en la línea 41:

  • La variable ep
  • Un objeto de tipo Entity sin nombre

El objeto sin nombre se crea en el free-store (Heap) y sólo podemos acceder a él a través de su dirección de memoria. Para eso usamos la variable ep, para almacenar el valor del puntero a Entity que nos devuelve la expresión new y que nos dice dónde está el nuevo objeto sin nombre.

Así pues, hemos creado 2 objetos:

Propiedad ep Objeto Sin-Nombre (OSN)
Tamaño 8 bytes (en CPUs de 64 bits) 32 bytes
Alineamiento 8 bytes (en CPUs de 64 bits) 8 bytes (campo más grande, double)
Storage duration Estático (global) Dinámico (gestión manual)
Tiempo de Vida Desde la línea 41 hasta el final del programa Desde la línea 41 hasta que se llame a delete
Tipo Entity* Entity
Nombre ep No tiene nombre
Valor Dirección de memoria del OSN Indeterminado (Entity no tiene constructor ni valores por defecto)

Importante: cosas que NO son objetos

Estas "entidades" no son objetos:

  • Un valor (value)
  • Una referencia (reference)
  • Una función (function)
  • Un enumerador (enumerator)
  • Un tipo (type)
  • Un miembro de clase no-estático (non-static class member)
  • Una plantilla (template)
  • Una clase (class)
  • Una especialización de plantilla de función (function template specialization)
  • Un espacio de nombres (namespace)
  • Un pack de parámetros (parameter pack)
  • La expresión this (this)

Creación y Destrucción de Objetos en C++

Las palabras crear y destruir en C++ suelen dar problemas. Es importante que las conozcamos con precisión.

  • Crear: decidir un lugar exacto de memoria donde estará el objeto e inicializarlo (escribir su valor inicial). A partir de ese momento, se dice que el objeto ha sido creado y existe.
  • Destruir: dejar de usar la memoria del objeto, quedando libre para otros usos.

Esto es lo básico, pero hay matices y detalles. Vamos a explorarlos con precisión.

Creación de Objetos

La creación de un objeto implica reservar una región de memoria (elegir un sitio, decidir dónde va) e inicializar esa región con un valor específico. A la inicialización la llamamos construcción, y es la labor del constructor. Los objetos que no tienen un constructor por defecto, ni se les asigna un valor inicial, quedan sin inicializar, por lo que su valor es indeterminado.

Existen diversas formas de crear objetos dependiendo de la duración de almacenamiento:

  1. Automático: Los objetos se crean automáticamente cuando se entra en su ámbito (scope). Se elige un espacio en la pila (stack, que está en la RAM). 

  2. Estático: Los objetos con almacenamiento estático se crean antes de la ejecución del main() y se destruyen después de terminarlo. La región de memoria que usan suele elegirse (reservarse) al cargar el programa en memoria.

  3. Dinámico: Los objetos se crean cuando se ejecuta new o malloc. Se elige un sitio para ellos en el free-store (reserva en el Heap). Este espacio permanece disponible hasta que ejecuta delete o free, indicando que queda libre para otros usos.

  4. Local a hilo: Se crean cuando un hilo comienza y existen solo mientras el hilo esté activo.

Destrucción de Objetos

La destrucción de un objeto implica dejar de usar su región de memoria, por lo que quedará libre para otros usos. Algunos objetos complejos son responsables de la gestión de otros objetos o recursos, por lo que necesitan ejecutar código antes de destruirse, para liberar los recursos que gestionan. Esta lógica es la que va en su destructor. A estos objetos se les llama no-trivialmente-destruibles.

Según la duración de sus almacenamientos:

  1. Automático: se destruyen automáticamente cuando se sale del ámbito (scope) donde fueron creados. Es al final del bloque donde el objeto está definido.

  2. Estático: se destruyen después de finalizar main()

  3. Dinámico: deben ser liberados explícitamente usando delete o free. De lo contrario, se producirá una fuga de memoria (memory leak), pues sus regiones de memoria permanecen en uso.

  4. Local a hilo: se destruyen cuando el hilo termina.


Referencias

  1. Object en CPPReference
  2. sizeof en CPPReference.
  3. alignof en CPPReference
  4. storage duration en CPPReference
  5. lifetime en CPPReference
  6. type en CPPReference
  7. default-initialized en CPPReference
  8. declaration en CPPReference
  9. this en CPPReference