DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía

"Sei que non sei nada" Sócrates

Para quen: para os informáticos que cuspen a todos os desenvolvedores e queren xogar aos seus xogos.

Sobre o que: como comezar a escribir xogos en C/C++ se o necesitas!

Por que deberías ler isto: O desenvolvemento de aplicacións non é a miña especialidade laboral, pero intento codificar todas as semanas. Porque me encantan os xogos!

Ola, chámome Andrei Grankin, son DevOps en Luxoft. O desenvolvemento de aplicacións non é a miña especialidade laboral, pero intento codificar todas as semanas. Porque me encantan os xogos!

A industria dos xogos de ordenador é enorme, aínda máis rumoreado hoxe que a industria do cine. Os xogos escribíronse dende o inicio do desenvolvemento dos ordenadores, utilizando, segundo os estándares modernos, métodos de desenvolvemento complexos e básicos. Co paso do tempo, comezaron a aparecer motores de xogos con gráficos, física e son xa programados. Permítenche centrarse no desenvolvemento do propio xogo e non preocuparse polo seu fundamento. Pero xunto con eles, cos motores, os desenvolvedores "quedan cegos" e degradan. A propia produción de xogos ponse na cinta transportadora. E a cantidade de produción comeza a prevalecer sobre a súa calidade.

Ao mesmo tempo, cando xogamos aos xogos doutras persoas, estamos constantemente limitados polas localizacións, a trama, os personaxes e as mecánicas de xogo que outras persoas inventaron. Entón decateime de que...

... é hora de crear os teus propios mundos, suxeitos só a min. Mundos onde eu son o Pai, e o Fillo e o Espírito Santo!

E creo sinceramente que escribindo o teu propio motor de xogo e un xogo nel, poderás abrir os ollos, limpar as fiestras e bombear a túa cabina, converténdote nun programador máis experimentado e integral.

Neste artigo tratarei de contarvos como comecei a escribir pequenos xogos en C/C++, cal é o proceso de desenvolvemento e onde atopo tempo para un hobby nun ambiente ocupado. É subxectivo e describe o proceso dun inicio individual. Material sobre a ignorancia e a fe, sobre a miña imaxe persoal do mundo neste momento. Noutras palabras, "¡A administración non é responsable do teu cerebro persoal!".

Práctica

"O coñecemento sen práctica é inútil, a práctica sen coñecemento é perigoso." Confucio

O meu caderno é a miña vida!


Entón, na práctica, podo dicir que para min todo comeza cun caderno. Alí non só anoto as miñas tarefas diarias, senón que tamén debuxo, programa, deseño diagramas de fluxo e resolvo problemas, incluídos os matemáticos. Use sempre un bloc de notas e escriba só cun lapis. É limpo, cómodo e fiable, en mi humilde opinión.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
O meu caderno (xa cheo). Así se ve. Contén tarefas cotiás, ideas, debuxos, diagramas, solucións, contabilidade negra, código, etc.

Nesta fase, conseguín completar tres proxectos (isto é na miña comprensión de "finalidade", porque calquera produto pode desenvolverse de forma relativamente infinita).

  • Proxecto 0: esta é unha escena 3D de Demo de arquitecto escrita en C# usando o motor de xogos Unity. Para plataformas macOS e Windows.
  • Xogo 1: xogo de consola Simple Snake (coñecido por todos como "Snake") para Windows. escrito en C.
  • Xogo 2: xogo de consola Crazy Tanks (coñecido por todos como "Tanks"), xa escrito en C++ (usando clases) e tamén en Windows.

Proxecto 0 Arquitecto Demo

  • Plataforma: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Idioma: C#
  • Motor de xogo: Unidade
  • Inspiración: Darrin Lile
  • Repositorio: GitHub

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Demostración de arquitecto de escena 3D

O primeiro proxecto non foi implementado en C/C++, senón en C# usando o motor de xogos Unity. Este motor non era tan esixente no hardware como Unreal Engine, e tamén me pareceu máis fácil de instalar e usar. Non considerei outros motores.

O obxectivo en Unity para min non era desenvolver algún tipo de xogo. Quería crear unha escena 3D con algún tipo de personaxe. El, ou mellor dito Ela (eu modelei á moza da que estaba namorada =) tiña que moverse e interactuar co mundo exterior. Só era importante entender o que é Unity, cal é o proceso de desenvolvemento e canto esforzo é necesario crear algo. Así naceu o proxecto Architect Demo (o nome foi inventado case da merda). A programación, o modelado, a animación, a texturización levoume probablemente dous meses de traballo diario.

Comecei con vídeos tutoriales en YouTube sobre como crear modelos 3D Blender. Blender é unha excelente ferramenta gratuíta para modelado 3D (e máis) que non require instalación. E aquí me esperaba un choque... Acontece que o modelado, a animación, a texturización son enormes temas separados sobre os que podes escribir libros. Isto é especialmente certo para os personaxes. Para modelar dedos, dentes, ollos e outras partes do corpo, necesitarás coñecementos de anatomía. Como están dispostos os músculos da cara? Como se move a xente? Tiven que "inserir" ósos en cada brazo, perna, dedo, nudillos!

Modela a clavícula, pancas óseas adicionais, para que a animación pareza natural. Despois de tales leccións, dás conta do enorme traballo que fan os creadores de películas de animación, só para crear 30 segundos de vídeo. Pero as películas en 3D duran horas! E despois saímos dos teatros e dicimos algo así como: “Ta, un debuxo animado/película de merda! Eles poderían ter feito mellor..." Parvos!

E unha cousa máis sobre a programación neste proxecto. Como se viu, a parte máis interesante para min foi a matemática. Se executas a escena (ligazón ao repositorio na descrición do proxecto), notarás que a cámara xira arredor do personaxe da rapaza nunha esfera. Para programar tal rotación da cámara, primeiro tiven que calcular as coordenadas do punto de posición no círculo (2D) e despois na esfera (3D). O curioso é que odiaba as matemáticas na escola e sabíao cun menos. En parte, probablemente, porque na escola simplemente non che explican como diaños se aplican estas matemáticas na vida. Pero cando estás obsesionado co teu obxectivo, soña, entón a mente está despexada, revelada! E comezas a percibir tarefas complexas como unha aventura emocionante. E entón pensas: "Ben, por que o *querido* matemático normalmente non podería dicir onde se poden inclinar estas fórmulas?".

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Cálculo de fórmulas para calcular as coordenadas dun punto nun círculo e nunha esfera (do meu caderno)

Xogo 1

  • Plataforma: Windows (probado en Windows 7, 10)
  • Idioma: Creo que estaba escrito en C puro
  • Motor de xogo: Consola Windows
  • Inspiración: javidx9
  • Repositorio: GitHub

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Xogo sinxelo de serpe

A escena 3D non é un xogo. Ademais, modelar e animar obxectos 3D (especialmente personaxes) é longo e difícil. Despois de xogar con Unity, decateime de que tiña que continuar, ou máis ben comezar, dende o básico. Algo sinxelo e rápido, pero ao mesmo tempo global, para comprender a propia estrutura dos xogos.

E que temos sinxelo e rápido? Así é, consola e 2D. Máis precisamente, ata a consola e os símbolos. De novo, comecei a buscar inspiración en Internet (en xeral, considero que Internet é o invento máis revolucionario e perigoso do século XXI). Destei un vídeo dun programador que fixo a consola Tetris. E a semellanza do seu xogo, decidiu beber unha "serpe". A partir do vídeo, aprendín dúas cousas fundamentais: o bucle do xogo (con tres funcións/partes básicas) e a saída ao búfer.

O bucle do xogo pode parecer algo así:

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

O código presenta toda a función main() á vez. E o ciclo de xogo comeza despois do comentario correspondente. Hai tres funcións básicas no bucle: Input(), Logic(), Draw(). En primeiro lugar, entrada de datos de entrada (principalmente control de pulsacións de teclas), a continuación, procesar os datos introducidos Lóxica, a continuación, mostrar na pantalla - Debuxar. E así cada cadro. A animación créase deste xeito. É como debuxos animados. Normalmente procesar os datos de entrada leva máis tempo e, polo que sei, determina a velocidade de cadros do xogo. Pero aquí a función Logic() é moi rápida. Polo tanto, a velocidade de fotogramas debe ser controlada pola función Sleep() co parámetro gameSpeed ​​​​, que determina esta taxa.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
ciclo de xogo. Programación de serpe no bloc de notas

Se estás a desenvolver un xogo de consola simbólico, mostrar datos na pantalla usando a saída de fluxo habitual "cout" non funcionará; é moi lento. Polo tanto, a saída debe realizarse no búfer da pantalla. Moito máis rápido e o xogo funcionará sen fallos. Para ser honesto, non entendo moi ben o que é un búfer de pantalla e como funciona. Pero vou dar un exemplo de código aquí, e quizais alguén nos comentarios poida aclarar a situación.

Conseguindo o búfer de pantalla (se me permite dicir iso):

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

Saída directa á pantalla dunha determinada liña scoreLine (a liña para mostrar as puntuacións):

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

En teoría, non hai nada complicado neste xogo, paréceme un bo exemplo de xogo de nivel de entrada. O código está escrito nun ficheiro e organizado en varias funcións. Sen clases, sen herdanza. Ti mesmo podes ver todo o código fonte do xogo indo ao repositorio de GitHub.

Xogo 2 Crazy Tanks

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Xogo Crazy Tanks

Imprimir personaxes na consola é probablemente o máis sinxelo que podes converter nun xogo. Pero entón aparece un problema: os personaxes teñen diferentes alturas e anchos (a altura é maior que o ancho). Así, todo parecerá desproporcionado e moverse cara abaixo ou cara arriba parecerá moito máis rápido que moverse á esquerda ou á dereita. Este efecto é moi perceptible en "Snake" (xogo 1). Os "tanques" (xogo 2) non teñen tal inconveniente, xa que a saída alí está organizada pintando os píxeles da pantalla con cores diferentes. Poderíase dicir que escribín un renderizador. É certo que isto xa é un pouco máis complicado, aínda que moito máis interesante.

Para este xogo, será suficiente describir o meu sistema para mostrar píxeles na pantalla. Creo que esta é a parte principal do xogo. E todo o demais que podes facer contigo mesmo.

Entón, o que ves na pantalla é só un conxunto de rectángulos de cores en movemento.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Conxunto de rectángulos

Cada rectángulo está representado por unha matriz chea de números. Por certo, podo destacar un matiz interesante: todas as matrices do xogo están programadas como unha matriz unidimensional. Non bidimensional, senón unidimensional! As matrices unidimensionales son moito máis fáciles e rápidas de traballar.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Un exemplo de matriz de tanques de xogo

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Representando a matriz dun tanque de xogo cunha matriz unidimensional

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Un exemplo máis ilustrativo dunha representación matricial mediante unha matriz unidimensional

Pero o acceso aos elementos da matriz prodúcese nun dobre bucle, coma se non fose unha matriz unidimensional, senón bidimensional. Isto faise porque aínda estamos traballando con matrices.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Percorrendo unha matriz unidimensional nun bucle dobre. Y é o ID da fila, X é o ID da columna

Teña en conta que en lugar dos identificadores matriciales habituais i, j, uso os identificadores x e y. Entón, paréceme, máis agradable á vista e máis claro para o cerebro. Ademais, tal notación permite proxectar convenientemente as matrices utilizadas nos eixes de coordenadas dunha imaxe bidimensional.

Agora sobre píxeles, cor e pantalla. A función StretchDIBits (Encabezado: windows.h; Biblioteca: gdi32.lib) úsase para a saída. Entre outras cousas, pásase a esta función o seguinte: o dispositivo no que se mostra a imaxe (no meu caso, esta é a consola de Windows), as coordenadas do inicio da visualización da imaxe, o seu ancho/alto e a imaxe. en forma de mapa de bits (mapa de bits), representado por unha matriz de bytes. Mapa de bits como unha matriz de bytes!

A función StretchDIBits() no traballo:

// 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
               );

A memoria asógase de antemán para este mapa de bits mediante a función VirtualAlloc(). É dicir, o número de bytes necesarios resérvase para almacenar información sobre todos os píxeles, que logo se mostrarán na pantalla.

Creando 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 liñas xerais, un mapa de bits consiste nun conxunto de píxeles. Cada catro bytes da matriz é un píxel RGB. Un byte por valor vermello, un byte por valor verde (G) e un byte por cor azul (B). Ademais, hai un byte por sangría. Estas tres cores -Vermello/Verde/Azul (RGB)- mestúranse entre si en diferentes proporcións- e obtense a cor do píxel resultante.

Agora, de novo, cada rectángulo, ou obxecto de xogo, está representado por unha matriz numérica. Todos estes obxectos do xogo colócanse nunha colección. E despois colócanse no terreo de xogo, formando unha gran matriz numérica. Asignei cada número da matriz a unha cor específica. Por exemplo, o número 8 é azul, o número 9 é amarelo, o número 10 é gris escuro, etc. Así, podemos dicir que temos unha matriz do terreo de xogo, onde cada número é algún tipo de cor.

Así, temos unha matriz numérica de todo o terreo de xogo por un lado e un mapa de bits para mostrar a imaxe por outro. Ata agora, o mapa de bits está "baleiro": aínda non ten información sobre os píxeles da cor desexada. Isto significa que o último paso será encher o mapa de bits con información sobre cada píxel en función da matriz numérica do campo de xogo. Un exemplo ilustrativo desta transformación está na imaxe de abaixo.

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Un exemplo de encher un mapa de bits (Matriz de píxeles) con información baseada na matriz numérica (Matriz dixital) do terreo de xogo (os índices de cores non coinciden cos índices do xogo)

Tamén presentarei un anaco de código real do xogo. Á variable colorIndex en cada iteración do bucle asígnaselle un valor (índice de cor) a partir da matriz numérica do campo de xogo (mainDigitalMatrix). A continuación, a propia cor escríbese na variable de cor en función do índice. Ademais, a cor resultante divídese na proporción de vermello, verde e azul (RGB). E xunto coa sangría (pixelPadding), esta información escríbese no píxel unha e outra vez, formando unha imaxe en cor no mapa de bits.

O código usa punteiros e operacións bit a bit, que poden ser difíciles de entender. Entón, recoméndoche que leas por separado nalgún lugar como funcionan esas estruturas.

Enchendo un mapa de bits con información baseada na matriz numérica do campo de xogo:

// 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;
   }

Segundo o método descrito anteriormente, fórmase unha imaxe (cadro) no xogo Crazy Tanks e móstrase na pantalla na función Draw(). Despois de rexistrar as pulsacións de tecla na función Input() e o seu procesamento posterior na función Logic(), fórmase unha nova imaxe (cadro). É certo que os obxectos do xogo xa poden ter unha posición diferente no terreo de xogo e, en consecuencia, están debuxados nun lugar diferente. Así ocorre a animación (o movemento).

En teoría (se non esqueceches nada), entender o bucle do xogo do primeiro xogo ("Snake") e o sistema para mostrar píxeles na pantalla do segundo xogo ("Tanques") é todo o que necesitas para escribir calquera dos teus xogos 2D para Windows. Sen son! 😉 O resto das partes son só un voo de fantasía.

Por suposto, o xogo "Tanques" está deseñado moito máis complicado que o "Snake". Xa usei a linguaxe C++, é dicir, describín diferentes obxectos de xogo con clases. Creei a miña propia colección; podes ver o código en headers/Box.h. Por certo, a colección moi probablemente teña unha fuga de memoria. Indicadores usados. Traballou coa memoria. Debo dicir que o libro axudoume moito. Iniciación en C++ a través da programación de xogos. Este é un gran comezo para principiantes en C++. É pequeno, interesante e ben organizado.

Levou uns seis meses desenvolver este xogo. Escribín principalmente durante o xantar e as merendas no traballo. Sentou na cociña da oficina, pisou a comida e escribiu código. Ou na casa para cear. Entón teño esas "guerras de cociña". Como sempre, usei activamente un caderno, e nel naceron todas as cousas conceptuais.

Ao final da parte práctica, sacarei uns cantos escaneos do meu caderno. Para mostrar o que exactamente estaba escribindo, debuxando, contando, deseñando...

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Deseño da imaxe do tanque. E a definición de cantos píxeles debe ocupar cada tanque na pantalla

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Cálculo do algoritmo e fórmulas para a rotación do tanque arredor do seu eixe

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
Diagrama da miña colección (o que ten fuga de memoria, moi probablemente). A colección créase como unha Lista vinculada

DevOps C++ e "guerras de cociña", ou Como comecei a escribir xogos mentres comía
E estes son intentos inútiles de parafusar a intelixencia artificial no xogo

Teoría

"Ata unha viaxe de mil millas comeza co primeiro paso" (Sabiduría chinesa antiga)

Pasemos da práctica á teoría! Como atopas tempo para a túa afección?

  1. Determina o que realmente queres (ai, este é o máis difícil).
  2. Establece prioridades.
  3. Sacrifica todo o "superfluo" en aras de prioridades máis altas.
  4. Avanza cada día cara aos teus obxectivos.
  5. Non esperes que haxa dúas ou tres horas de tempo libre para un hobby.

Por unha banda, cómpre determinar o que quere e priorizar. Por outra banda, é posible abandonar algúns casos/proxectos en favor destas prioridades. Noutras palabras, terás que sacrificar todo o "superfluo". Oín nalgún lugar que na vida debería haber un máximo de tres actividades principais. Entón poderás tratar con eles da mellor maneira posible. E os proxectos/direccións adicionais comezarán a sobrecargarse cursi. Pero isto é todo, probablemente, subxectivo e individual.

Hai unha certa regra de ouro: nunca teñas un día 0%! Souben diso nun artigo dun programador independente. Se estás a traballar nun proxecto, fai algo ao respecto todos os días. E non importa o que gañe. Escribe unha palabra ou unha liña de código, mira un vídeo tutorial ou mete un cravo no taboleiro, só fai algo. O máis difícil é comezar. Unha vez que comeces, probablemente fagas un pouco máis do que querías. Así que avanzarás constantemente cara ao teu obxectivo e, créame, moi rápido. Despois de todo, o principal freo a todas as cousas é a procrastinación.

E é importante lembrar que non debes subestimar e ignorar o "serrín" gratuíto do tempo en 5, 10, 15 minutos, esperar uns grandes "rexistros" dunha ou dúas horas. Estás facendo cola? Pensa en algo para o teu proxecto. Vai subindo as escaleiras mecánicas? Anota algo nun caderno. Comes no autobús? Vale, le algún artigo. Aproveita todas as oportunidades. Deixa de ver cans e gatos en YouTube! Non te metas co teu cerebro!

E o último. Se despois de ler este artigo che gustou a idea de crear xogos sen usar motores de xogos, lembra o nome de Casey Muratori. Este tipo ten sitio. Na sección "ver -> EPISODIOS ANTERIORS" atoparás sorprendentes videotutoriais gratuítos sobre como crear un xogo profesional desde cero. Podes aprender máis en cinco clases de Introdución a C para Windows que en cinco anos de estudo na universidade (alguén escribiu sobre isto nos comentarios baixo o vídeo).

Casey tamén explica que ao desenvolver o teu propio motor de xogo, entenderás mellor os motores existentes. No mundo dos frameworks, onde todos intentan automatizar, aprenderás a crear, non a usar. Comprender a propia natureza dos ordenadores. E tamén converteráste nun programador moito máis intelixente e maduro: un profesional.

Moita sorte no camiño escollido! E imos facer o mundo máis profesional.

autor: Grankin Andrey, DevOps



Fonte: www.habr.com