DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía

"Sé que no sé nada" Sócrates

Para quien: ¡para la gente de TI que escupe a todos los desarrolladores y quiere jugar sus juegos!

Acerca de: ¡cómo empezar a escribir juegos en C/C++ si lo necesitas!

¿Por qué deberías leer esto? El desarrollo de aplicaciones no es mi especialidad laboral, pero trato de codificar todas las semanas. ¡Porque me encantan los juegos!

Hola, mi nombre es Andrey Grankin, soy DevOps en Luxoft. El desarrollo de aplicaciones no es mi especialidad laboral, pero trato de codificar todas las semanas. ¡Porque me encantan los juegos!

La industria de los juegos de computadora es enorme, incluso más rumoreada hoy que la industria del cine. Los juegos se han escrito desde el comienzo del desarrollo de las computadoras, utilizando, según los estándares modernos, métodos de desarrollo complejos y básicos. Con el tiempo, comenzaron a aparecer motores de juegos con gráficos, física y sonido ya programados. Te permiten concentrarte en el desarrollo del juego en sí y no preocuparte por su base. Pero junto con ellos, con los motores, los desarrolladores "se quedan ciegos" y se degradan. La misma producción de juegos se pone en el transportador. Y la cantidad de producción comienza a prevalecer sobre su calidad.

Al mismo tiempo, cuando jugamos a los juegos de otras personas, estamos constantemente limitados por las ubicaciones, la trama, los personajes y las mecánicas de juego que se les ocurrieron a otras personas. Entonces me di cuenta de que...

… es hora de crear sus propios mundos, sujetos solo a mí. ¡Mundos donde Yo soy el Padre, y el Hijo, y el Espíritu Santo!

Y creo sinceramente que al escribir su propio motor de juego y un juego en él, podrá abrir los ojos, limpiar las ventanas y bombear su cabina, convirtiéndose en un programador más experimentado e integral.

En este artículo intentaré contarles cómo comencé a escribir pequeños juegos en C/C++, cuál es el proceso de desarrollo y dónde encuentro tiempo para un pasatiempo en un entorno ajetreado. Es subjetivo y describe el proceso de un inicio individual. Material sobre la ignorancia y la fe, sobre mi imagen personal del mundo en este momento. En otras palabras, "¡La administración no es responsable de sus cerebros personales!".

Práctica

“El conocimiento sin práctica es inútil, la práctica sin conocimiento es peligrosa.” Confucio

¡Mi cuaderno es mi vida!


Entonces, en la práctica, puedo decir que todo para mí comienza con un cuaderno. No solo escribo allí mis tareas diarias, sino que también dibujo, programo, diseño diagramas de flujo y resuelvo problemas, incluidos los matemáticos. Siempre use un bloc de notas y escriba solo con un lápiz. Es limpio, cómodo y confiable, en mi humilde opinión.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Mi cuaderno (ya lleno). Así es como se ve. Contiene tareas cotidianas, ideas, dibujos, diagramas, soluciones, contabilidad negra, código, etc.

En esta etapa, logré completar tres proyectos (esto es en mi comprensión de "finalidad", porque cualquier producto puede desarrollarse de manera relativamente interminable).

  • Proyecto 0: esta es una escena de Architect Demo 3D escrita en C# usando el motor de juego Unity. Para plataformas macOS y Windows.
  • Juego 1: juego de consola Simple Snake (conocido por todos como "Snake") para Windows. escrito en c
  • Juego 2: juego de consola Crazy Tanks (conocido por todos como "Tanks"), ya escrito en C ++ (usando clases) y también bajo Windows.

Proyecto 0 Arquitecto Demostración

  • Plataforma: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Idioma: C#
  • Motor de juegos: La Unidad
  • Inspiración: darrin lile
  • Repositorio: GitHub

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Demostración de Arquitecto de escena 3D

El primer proyecto no se implementó en C/C++, sino en C# utilizando el motor de juego Unity. Este motor no era tan exigente con el hardware como Motor irreal, y además me pareció más fácil de instalar y usar. No consideré otros motores.

Para mí, el objetivo de Unity no era desarrollar algún tipo de juego. Quería crear una escena en 3D con algún tipo de personaje. Él, o más bien Ella (yo modelé a la chica de la que estaba enamorado =) tenía que moverse e interactuar con el mundo exterior. Solo era importante entender qué es Unity, cuál es el proceso de desarrollo y cuánto esfuerzo se necesita para crear algo. Así nació el proyecto Architect Demo (el nombre se inventó casi de la chorrada). Programar, modelar, animar, texturizar me tomó probablemente dos meses de trabajo diario.

Empecé con videos tutoriales en YouTube sobre cómo crear modelos 3D en Batidora de vaso - Blender. Blender es una excelente herramienta gratuita para el modelado 3D (y más) que no requiere instalación. Y aquí me esperaba un shock ... Resulta que el modelado, la animación, el texturizado son grandes temas separados sobre los que puedes escribir libros. Esto es especialmente cierto para los personajes. Para modelar dedos, dientes, ojos y otras partes del cuerpo, necesitará conocimientos de anatomía. ¿Cómo están dispuestos los músculos de la cara? ¿Cómo se mueve la gente? ¡Tuve que “insertar” huesos en cada brazo, pierna, dedo, nudillos!

Modele la clavícula, palancas óseas adicionales, para que la animación se vea natural. Después de tales lecciones, te das cuenta del gran trabajo que hacen los creadores de películas animadas, solo para crear 30 segundos de video. ¡Pero las películas en 3D duran horas! Y luego salimos de los cines y decimos algo como: “¡Ta, una caricatura/película de mierda! Podrían haberlo hecho mejor…” ¡Imbéciles!

Y una cosa más sobre la programación en este proyecto. Al final resultó que, la parte más interesante para mí fue la matemática. Si ejecuta la escena (enlace al repositorio en la descripción del proyecto), notará que la cámara gira alrededor del personaje femenino en una esfera. Para programar una rotación de cámara de este tipo, primero tuve que calcular las coordenadas del punto de posición en el círculo (2D) y luego en la esfera (3D). Lo gracioso es que odiaba las matemáticas en la escuela y las conocía con un menos. En parte, probablemente, porque en la escuela simplemente no te explican cómo diablos se aplican estas matemáticas en la vida. Pero cuando estás obsesionado con tu objetivo, sueñas, ¡entonces la mente se aclara, se revela! Y comienzas a percibir las tareas complejas como una aventura emocionante. Y luego piensas: "Bueno, ¿por qué *amado* matemático normalmente no podría decir dónde se pueden apoyar estas fórmulas?".

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Cálculo de fórmulas para calcular las coordenadas de un punto en un círculo y en una esfera (de mi cuaderno)

Juego 1

  • Plataforma: Windows (probado en Windows 7, 10)
  • Idioma: Creo que fue escrito en C puro.
  • Motor de juegos: consola de windows
  • Inspiración: javidx9
  • Repositorio: GitHub

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Juego de serpiente simple

La escena 3D no es un juego. Además, modelar y animar objetos 3D (especialmente personajes) es largo y difícil. Después de jugar con Unity, me di cuenta de que tenía que continuar, o más bien empezar, desde lo básico. Algo sencillo y rápido, pero a la vez global, para entender la estructura misma de los juegos.

¿Y qué tenemos simple y rápido? Así es, consola y 2D. Más precisamente, incluso la consola y los símbolos. Nuevamente, comencé a buscar inspiración en Internet (en general, considero que Internet es el invento más revolucionario y peligroso del siglo XXI). Desenterré un video de un programador que hizo la consola Tetris. Y a semejanza de su juego, decidió cortar la "serpiente". Del video, aprendí sobre dos cosas fundamentales: el bucle del juego (con tres funciones/partes básicas) y la salida al búfer.

El bucle del juego podría verse así:

int main()
   {
      Setup();
      // a game loop
      while (!quit)
      {
          Input();
          Logic();
          Draw();
          Sleep(gameSpeed);  // game timing
      }
      return 0;
   }

El código presenta la función main() completa a la vez. Y el ciclo del juego comienza después del comentario correspondiente. Hay tres funciones básicas en el ciclo: Input(), Logic(), Draw(). Primero, ingrese los datos de entrada (principalmente el control de las pulsaciones de teclas), luego procese la lógica de datos ingresada y luego muestre en la pantalla - Dibujar. Y así cada fotograma. La animación se crea de esta manera. Es como dibujos animados. Por lo general, el procesamiento de los datos de entrada lleva la mayor parte del tiempo y, hasta donde yo sé, determina la velocidad de fotogramas del juego. Pero aquí la función Logic() es muy rápida. Por lo tanto, la velocidad de fotogramas debe ser controlada por la función Sleep() con el parámetro gameSpeed, que determina esta velocidad.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
ciclo de juego Programación de serpientes en el bloc de notas.

Si está desarrollando un juego de consola simbólico, la visualización de datos en la pantalla utilizando la salida de flujo habitual 'cout' no funcionará, es muy lento. Por lo tanto, la salida debe realizarse en el búfer de pantalla. Mucho más rápido y el juego funcionará sin fallas. Para ser honesto, no entiendo muy bien qué es un búfer de pantalla y cómo funciona. Pero daré un ejemplo de código aquí, y quizás alguien en los comentarios pueda aclarar la situación.

Obtener el búfer de pantalla (si se me permite decirlo):

// create screen buffer for drawings
   HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0,
 							   NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
   DWORD dwBytesWritten = 0;
   SetConsoleActiveScreenBuffer(hConsole);

Salida directa a la pantalla de una determinada línea scoreLine (la línea para mostrar puntajes):

// draw the score
   WriteConsoleOutputCharacter(hConsole, scoreLine, GAME_WIDTH, {2,3}, &dwBytesWritten);

En teoría no hay nada complicado en este juego, me parece un buen ejemplo de juego de nivel de entrada. El código está escrito en un archivo y organizado en varias funciones. Sin clases, sin herencia. Usted mismo puede ver todo en el código fuente del juego yendo al repositorio en GitHub.

Juego 2 tanques locos

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
juego de tanques locos

Imprimir caracteres en la consola es probablemente lo más simple que puedes convertir en un juego. Pero luego aparece un problema: los caracteres tienen diferentes alturas y anchuras (la altura es mayor que la anchura). Así, todo parecerá desproporcionado, y moverse hacia abajo o hacia arriba parecerá mucho más rápido que moverse hacia la izquierda o hacia la derecha. Este efecto es muy notable en "Snake" (Juego 1). Los "Tanques" (Juego 2) no tienen ese inconveniente, ya que la salida se organiza pintando los píxeles de la pantalla con diferentes colores. Se podría decir que escribí un renderizador. Cierto, esto ya es un poco más complicado, aunque mucho más interesante.

Para este juego, será suficiente describir mi sistema para mostrar píxeles en la pantalla. Creo que esta es la parte principal del juego. Y todo lo demás se te ocurre a ti mismo.

Entonces, lo que ves en la pantalla es solo un conjunto de rectángulos de colores en movimiento.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Conjunto de rectángulo

Cada rectángulo está representado por una matriz llena de números. Por cierto, puedo resaltar un matiz interesante: todas las matrices del juego están programadas como una matriz unidimensional. ¡No bidimensional, sino unidimensional! Las matrices unidimensionales son mucho más fáciles y rápidas de trabajar.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Un ejemplo de una matriz de tanque de juego

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Representación de la matriz de un tanque de juego con una matriz unidimensional

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Un ejemplo más ilustrativo de una representación de matriz por una matriz unidimensional

Pero el acceso a los elementos del arreglo ocurre en un doble bucle, como si no fuera un arreglo unidimensional, sino bidimensional. Esto se hace porque todavía estamos trabajando con matrices.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Recorriendo una matriz unidimensional en un bucle doble. Y es el ID de la fila, X es el ID de la columna

Tenga en cuenta que en lugar de los identificadores de matriz habituales i, j, utilizo los identificadores x e y. Entonces, me parece, más agradable a la vista y más claro para el cerebro. Además, tal notación hace posible proyectar convenientemente las matrices utilizadas sobre los ejes de coordenadas de una imagen bidimensional.

Ahora sobre píxeles, color y pantalla. La función StretchDIBits (Encabezado: windows.h; Biblioteca: gdi32.lib) se utiliza para la salida. Entre otras cosas, a esta función se le pasa lo siguiente: el dispositivo en el que se muestra la imagen (en mi caso, esta es la consola de Windows), las coordenadas de inicio de la visualización de la imagen, su ancho/alto y la imagen en forma de mapa de bits (bitmap), representado por una matriz de bytes. ¡Mapa de bits como una matriz de bytes!

La función StretchDIBits() en el trabajo:

// screen output for game field
   StretchDIBits(
               deviceContext,
               OFFSET_LEFT, OFFSET_TOP,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               0, 0,
               PMATRIX_WIDTH, PMATRIX_HEIGHT,
               m_p_bitmapMemory, &bitmapInfo,
               DIB_RGB_COLORS,
               SRCCOPY
               );

La memoria se asigna de antemano para este mapa de bits utilizando la función VirtualAlloc(). Es decir, se reserva la cantidad requerida de bytes para almacenar información sobre todos los píxeles, que luego se mostrarán en la pantalla.

Creación de un mapa de bits m_p_bitmapMemory:

// create bitmap
   int bitmapMemorySize = (PMATRIX_WIDTH * PMATRIX_HEIGHT) * BYTES_PER_PIXEL;
   void* m_p_bitmapMemory = VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);

En términos generales, un mapa de bits consta de un conjunto de píxeles. Cada cuatro bytes en la matriz es un píxel RGB. Un byte por valor rojo, un byte por valor verde (G) y un byte por color azul (B). Además, hay un byte por sangría. Estos tres colores, rojo, verde y azul (RGB), se mezclan entre sí en diferentes proporciones y se obtiene el color de píxel resultante.

Ahora, de nuevo, cada rectángulo u objeto del juego está representado por una matriz numérica. Todos estos objetos del juego se colocan en una colección. Y luego se colocan en el campo de juego, formando una gran matriz numérica. Asigné cada número en la matriz a un color específico. Por ejemplo, el número 8 es azul, el número 9 es amarillo, el número 10 es gris oscuro, etc. Así, podemos decir que tenemos una matriz del campo de juego, donde cada número es una especie de color.

Entonces, tenemos una matriz numérica de todo el campo de juego por un lado y un mapa de bits para mostrar la imagen por el otro. Hasta ahora, el mapa de bits está "vacío": aún no tiene información sobre los píxeles del color deseado. Esto significa que el último paso será llenar el mapa de bits con información sobre cada píxel en función de la matriz numérica del campo de juego. Un ejemplo ilustrativo de tal transformación se encuentra en la siguiente imagen.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Un ejemplo de cómo llenar un mapa de bits (matriz de píxeles) con información basada en la matriz numérica (matriz digital) del campo de juego (los índices de color no coinciden con los índices del juego)

También presentaré una pieza de código real del juego. A la variable colorIndex en cada iteración del bucle se le asigna un valor (índice de color) de la matriz numérica del campo de juego (mainDigitalMatrix). Luego, el color en sí se escribe en la variable de color según el índice. Además, el color resultante se divide en la proporción de rojo, verde y azul (RGB). Y junto con la sangría (pixelPadding), esta información se escribe en el píxel una y otra vez, formando una imagen en color en el mapa de bits.

El código usa punteros y operaciones bit a bit, que pueden ser difíciles de entender. Así que le aconsejo que lea por separado en algún lugar cómo funcionan tales estructuras.

Llenar un mapa de bits con información basada en la matriz numérica del campo de juego:

// set pixel map variables
   int colorIndex;
   COLORREF color;
   int pitch;
   uint8_t* p_row;
 
   // arrange pixels for game field
   pitch = PMATRIX_WIDTH * BYTES_PER_PIXEL;     // row size in bytes
   p_row = (uint8_t*)m_p_bitmapMemory;       //cast to uint8 for valid pointer arithmetic
   							(to add by 1 byte (8 bits) at a time)   
   for (int y = 0; y < PMATRIX_HEIGHT; ++y)
   {
       uint32_t* p_pixel = (uint32_t*)p_row;
       for (int x = 0; x < PMATRIX_WIDTH; ++x)
       {
           colorIndex = mainDigitalMatrix[y * PMATRIX_WIDTH + x];
           color = Utils::GetColor(colorIndex);
           uint8_t blue = GetBValue(color);
           uint8_t green = GetGValue(color);
           uint8_t red = GetRValue(color);
           uint8_t pixelPadding = 0;
 
           *p_pixel = ((pixelPadding << 24) | (red << 16) | (green << 8) | blue);
           ++p_pixel;
       }
       p_row += pitch;
   }

De acuerdo con el método descrito anteriormente, se forma una imagen (cuadro) en el juego Crazy Tanks y se muestra en la pantalla en la función Draw(). Después de registrar las pulsaciones de teclas en la función Input() y su posterior procesamiento en la función Logic(), se forma una nueva imagen (marco). Es cierto que los objetos del juego ya pueden tener una posición diferente en el campo de juego y, en consecuencia, se dibujan en un lugar diferente. Así es como ocurre la animación (movimiento).

En teoría (si no has olvidado nada), comprender el bucle del juego del primer juego ("Serpiente") y el sistema de visualización de píxeles en la pantalla del segundo juego ("Tanques") es todo lo que necesitas para escribir cualquier de tus juegos 2D para Windows. ¡Silencioso! 😉 El resto de las partes son solo un vuelo de fantasía.

Por supuesto, el juego "Tanques" está diseñado mucho más complicado que el "Serpiente". Ya usé el lenguaje C++, es decir, describí diferentes objetos del juego con clases. Creé mi propia colección; puedes ver el código en headers/Box.h. Por cierto, lo más probable es que la colección tenga una pérdida de memoria. Punteros usados. Trabajó con la memoria. Debo decir que el libro me ayudó mucho. Comenzando con C++ a través de la programación de juegos. Este es un gran comienzo para los principiantes en C++. Es pequeño, interesante y bien organizado.

Se necesitaron unos seis meses para desarrollar este juego. Escribía principalmente durante el almuerzo y la merienda en el trabajo. Se sentó en la cocina de la oficina, pisoteó la comida y escribió código. O en casa para la cena. Así que tengo tales "guerras de cocina". Como siempre, utilicé activamente un cuaderno, y todas las cosas conceptuales nacieron en él.

Al final de la parte práctica, sacaré algunos escaneos de mi libreta. Para mostrar lo que estaba escribiendo, dibujando, contando, diseñando…

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Diseño de imagen de tanque. Y la definición de cuantos pixeles debe ocupar cada tanque en la pantalla

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Cálculo del algoritmo y fórmulas para la rotación del tanque alrededor de su eje.

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Diagrama de mi colección (la que tiene la fuga de memoria, probablemente). La colección se crea como una lista enlazada

DevOps C++ y "guerras de cocina", o cómo empecé a escribir juegos mientras comía
Y estos son intentos inútiles de introducir inteligencia artificial en el juego.

Теория

"Incluso un viaje de mil millas comienza con el primer paso" (Sabiduría china antigua)

¡Pasemos de la práctica a la teoría! ¿Cómo encuentras tiempo para tu hobby?

  1. Determina lo que realmente quieres (por desgracia, esto es lo más difícil).
  2. Establecer prioridades.
  3. Sacrificar todo lo "superfluo" en aras de prioridades más altas.
  4. Avanza hacia tus metas todos los días.
  5. No espere que haya dos o tres horas de tiempo libre para un pasatiempo.

Por un lado, necesitas determinar lo que quieres y priorizar. Por otro lado, es posible abandonar algunos casos/proyectos a favor de estas prioridades. En otras palabras, tendrás que sacrificar todo lo "superfluo". Escuché en alguna parte que en la vida debe haber un máximo de tres actividades principales. Entonces podrá tratar con ellos de la mejor manera posible. Y los proyectos/direcciones adicionales comenzarán a sobrecargar cursi. Pero todo esto es, probablemente, subjetivo e individual.

Hay una cierta regla de oro: ¡nunca tengas un día 0%! Lo aprendí en un artículo de un desarrollador independiente. Si está trabajando en un proyecto, entonces haga algo al respecto todos los días. Y no importa cuánto ganes. Escriba una palabra o una línea de código, mire un video tutorial o clave un clavo en el tablero, simplemente haga algo. La parte más difícil es empezar. Una vez que empieces, probablemente harás un poco más de lo que querías. Así te moverás constantemente hacia tu objetivo y, créeme, muy rápido. Después de todo, el principal freno de todas las cosas es la procrastinación.

Y es importante recordar que no debe subestimar e ignorar el "aserrín" gratuito del tiempo en 5, 10, 15 minutos, espere algunos "registros" grandes que duren una hora o dos. ¿Estás parado en la fila? Piensa en algo para tu proyecto. ¿Estás subiendo la escalera mecánica? Escribe algo en un cuaderno. ¿Comes en el autobús? Bien, lee algún artículo. Usa todas las oportunidades. ¡Deja de ver gatos y perros en YouTube! ¡No juegues con tu cerebro!

Y el último. Si, después de leer este artículo, te gustó la idea de crear juegos sin usar motores de juegos, entonces recuerda el nombre de Casey Muratori. este chico tiene sitio web. En la sección "ver -> EPISODIOS ANTERIORES" encontrarás fantásticos tutoriales en vídeo gratuitos sobre cómo crear un juego profesional desde cero. En cinco lecciones de Introducción a C para Windows, puede aprender más que en cinco años de estudio en la universidad (alguien escribió sobre esto en los comentarios debajo del video).

Casey también explica que al desarrollar su propio motor de juego, tendrá una mejor comprensión de los motores existentes. En el mundo de los marcos, donde todos intentan automatizar, aprenderá a crear, no a usar. Comprender la naturaleza misma de las computadoras. Y también te convertirás en un programador mucho más inteligente y maduro: un profesional.

¡Buena suerte en tu camino elegido! Y hagamos el mundo más profesional.

autor: grankin andrey, DevOps



Fuente: habr.com