DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia

“Eu sei que não sei nada” Sócrates

Para quem: para profissionais de TI que não se importam com todos os desenvolvedores e querem jogar seus jogos!

Sobre o que: sobre como começar a escrever jogos em C/C++, se você precisar dele de repente!

Por que você deveria ler isto: Desenvolvimento de aplicativos não é minha área de especialização, mas tento codificar toda semana. Porque eu adoro jogos!

Olá meu nome é Andrey Grankin, sou DevOps na Luxoft. Desenvolvimento de aplicativos não é minha especialidade, mas tento programar toda semana. Porque eu adoro jogos!

A indústria de jogos de computador é enorme, e há rumores de que seja ainda maior do que a indústria cinematográfica hoje. Os jogos foram escritos desde os primórdios dos computadores, utilizando, pelos padrões modernos, métodos de desenvolvimento complexos e básicos. Com o tempo, motores de jogos com gráficos, física e som já programados começaram a aparecer. Eles permitem que você se concentre no desenvolvimento do jogo em si e não se preocupe com sua base. Mas junto com eles, com os motores, os desenvolvedores “ficam cegos” e se degradam. A própria produção dos jogos é colocada na esteira. E a quantidade dos produtos passa a prevalecer sobre a qualidade.

Ao mesmo tempo, ao jogar jogos de outras pessoas, somos constantemente limitados pelos locais, enredo, personagens e mecânica de jogo que outras pessoas criaram. Então eu percebi que...

... chegou a hora de criar meus próprios mundos, sujeitos apenas a mim. Mundos onde eu sou o Pai, o Filho e o Espírito Santo!

E acredito sinceramente que ao escrever seu próprio motor de jogo e jogar nele, você poderá tirar os sapatos, limpar as janelas e atualizar sua cabine, tornando-se um programador mais experiente e completo.

Neste artigo tentarei contar como comecei a escrever pequenos jogos em C/C++, como é o processo de desenvolvimento e onde encontro tempo para um hobby em um ambiente movimentado. É subjetivo e descreve o processo de um início individual. Material sobre ignorância e fé, sobre minha imagem pessoal do mundo no momento. Em outras palavras, “A administração não é responsável pelos seus cérebros pessoais!”

Prática

“O conhecimento sem prática é inútil, a prática sem conhecimento é perigosa” Confúcio

Meu caderno é minha vida!


Então, na prática, posso dizer que para mim tudo começa com um bloco de notas. Lá não só anoto minhas tarefas diárias, como também desenho, programo, desenho fluxogramas e resolvo problemas, inclusive matemáticos. Use sempre um bloco de notas e escreva apenas a lápis. É limpo, conveniente e confiável, IMHO.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Meu caderno (já preenchido). É assim que ele se parece. Ele contém tarefas cotidianas, ideias, desenhos, diagramas, soluções, contabilidade preta, código e assim por diante

Nesta fase, consegui concluir três projetos (isto está no meu entendimento de “completude”, porque qualquer produto pode ser desenvolvido de forma relativamente infinita).

  • Projeto 0: Esta é uma cena de demonstração 3D do Architect escrita em C# usando o mecanismo de jogo Unity. Para plataformas macOS e Windows.
  • Jogo 1: jogo de console Simple Snake (conhecido por todos como “Snake”) para Windows. Escrito em C.
  • Jogo 2: jogo de console Crazy Tanks (conhecido por todos como “Tanks”), escrito em C++ (usando classes) e também para Windows.

Projeto 0. Demonstração do Arquiteto

  • Plataforma: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • idioma: C#
  • Motor do jogo: Unity
  • Inspiração: Darrin Lile
  • Repositório: GitHub

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Demonstração do arquiteto de cena 3D

O primeiro projeto foi implementado não em C/C++, mas em C# usando o motor de jogo Unity. Este mecanismo não exigia tanto hardware quanto Unreal Engine, e também parecia mais fácil de instalar e usar. Não considerei outros motores.

Meu objetivo no Unity não era desenvolver um jogo. Eu queria criar uma cena 3D com algum personagem. Ele, ou melhor, Ela (eu modelei a garota por quem estava apaixonado =) teve que se movimentar e interagir com o mundo ao seu redor. Só era importante entender o que é Unity, qual é o processo de desenvolvimento e quanto esforço é necessário para criar algo. Foi assim que nasceu o projeto Architect Demo (o nome foi quase inventado do nada). Programação, modelagem, animação e texturização levaram provavelmente dois meses de trabalho diário.

Comecei com vídeos tutoriais no YouTube sobre como criar modelos 3D em liqüidificador. O Blender é uma excelente ferramenta gratuita para modelagem 3D (e muito mais) que não requer instalação. E aqui um choque me esperava... Acontece que modelagem, animação, texturização são grandes tópicos separados sobre os quais você pode escrever livros. Isto é especialmente verdadeiro para personagens. Para modelar dedos, dentes, olhos e outras partes do corpo, você precisará de conhecimentos de anatomia. Como os músculos faciais estão estruturados? Como as pessoas se movem? Tive que “inserir” ossos em cada braço, perna, dedo, falanges dos dedos!

Modele as clavículas e os ossos de alavanca adicionais para fazer a animação parecer natural. Depois dessas aulas, você percebe quanto trabalho os criadores de filmes de animação dão apenas para criar 30 segundos de vídeo. Mas os filmes 3D duram horas! E então saímos dos cinemas e dizemos algo como: “Isso é um desenho/filme de merda! Eles poderiam ter feito melhor...” Tolos!

E mais uma coisa em relação à programação deste projeto. No final das contas, a parte mais interessante para mim foi a matemática. Se você executar a cena (link para o repositório na descrição do projeto), notará que a câmera gira em torno da personagem feminina em uma esfera. Para programar essa rotação da câmera, tive que primeiro calcular as coordenadas do ponto de posição no círculo (2D) e depois na esfera (3D). O engraçado é que eu odiava matemática na escola e a conhecia com C menos. Em parte, provavelmente, porque na escola eles simplesmente não explicam como essa matemática é aplicada na vida. Mas quando você está obcecado com seu objetivo, seu sonho, sua mente clareia e se abre! E você começa a perceber as tarefas difíceis como uma aventura emocionante. E então você pensa: “Bem, por que seu matemático *favorito* não poderia lhe dizer normalmente onde essas fórmulas podem ser aplicadas?”

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Cálculo de fórmulas para calcular as coordenadas de um ponto em um círculo e em uma esfera (do meu caderno)

Jogo 1. Cobra Simples

  • Plataforma: Windows (testado no Windows 7, 10)
  • idioma: Acho que escrevi em C puro
  • Motor do jogo: console do Windows
  • Inspiração: javidx9
  • Repositório: GitHub

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Jogo de cobra simples

Uma cena 3D não é um jogo. Além disso, modelar e animar objetos 3D (especialmente personagens) é demorado e difícil. Depois de brincar com o Unity, percebi que precisava continuar, ou melhor, começar do básico. Algo simples e rápido, mas ao mesmo tempo global, para compreender a própria estrutura dos jogos.

O que é simples e rápido? Isso mesmo, console e 2D. Mais precisamente, até mesmo o console e os símbolos. Mais uma vez procurei inspiração na Internet (em geral, considero a Internet a invenção mais revolucionária e perigosa do século XXI). Desenterrei um vídeo de um programador que fez o console Tetris. E à semelhança do jogo dele resolvi fazer uma “cobra”. Com o vídeo aprendi duas coisas fundamentais - o loop do jogo (com três funções/partes básicas) e a saída para o buffer.

O loop do jogo pode ser parecido com isto:

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

O código apresenta toda a função main() de uma só vez. E o ciclo do jogo começa após o comentário apropriado. Existem três funções básicas no loop: Input(), Logic(), Draw(). Primeiro, insira os dados de entrada (principalmente o controle das teclas digitadas), depois processe os dados inseridos na lógica e, em seguida, envie para a tela - Desenhar. E assim por diante em cada quadro. É assim que a animação é criada. É como nos desenhos animados. Normalmente, o processamento dos dados inseridos leva mais tempo e, até onde eu sei, determina a taxa de quadros do jogo. Mas aqui a função Logic() é executada muito rapidamente. Portanto, você deve controlar a taxa de quadros usando a função Sleep() com o parâmetro gameSpeed, que determina essa velocidade.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Ciclo do jogo. Programando uma “cobra” em um bloco de notas

Se você estiver desenvolvendo um jogo de console baseado em personagens, não será capaz de enviar dados para a tela usando a saída de fluxo normal 'cout' - é muito lenta. Portanto, a saída deve ser enviada para o buffer de tela. Isso é muito mais rápido e o jogo rodará sem falhas. Para ser sincero, não entendo muito bem o que é um buffer de tela e como funciona. Mas vou dar aqui um exemplo de código, e talvez alguém possa esclarecer a situação nos comentários.

Obtendo um buffer de tela (por assim dizer):

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

Exibição direta de uma determinada string scoreLine (linha de exibição de pontuação):

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

Em teoria, não há nada complicado neste jogo; acho que é um bom exemplo para iniciantes. O código é escrito em um arquivo e formatado em diversas funções. Sem classes, sem herança. Você pode ver tudo no código-fonte do jogo acessando o repositório no GitHub.

Jogo 2. Tanques Malucos

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Jogo Tanques Loucos

Imprimir personagens no console é provavelmente a coisa mais simples que você pode transformar em um jogo. Mas então surge um problema: os símbolos têm alturas e larguras diferentes (a altura é maior que a largura). Dessa forma, tudo parecerá fora de proporção e mover-se para baixo ou para cima parecerá muito mais rápido do que mover-se para a esquerda ou para a direita. Este efeito é muito perceptível em Snake (Jogo 1). “Tanks” (Jogo 2) não tem esse inconveniente, já que a saída ali é organizada pintando os pixels da tela com cores diferentes. Você poderia dizer que escrevi um renderizador. É verdade que isto é um pouco mais complicado, embora muito mais interessante.

Para este jogo, bastará descrever meu sistema de exibição de pixels na tela. Considero esta a parte principal do jogo. E você pode inventar todo o resto sozinho.

Então, o que você vê na tela é apenas um conjunto de retângulos multicoloridos em movimento.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Conjunto de retângulos

Cada retângulo é representado por uma matriz preenchida com números. A propósito, posso destacar uma nuance interessante - todas as matrizes do jogo são programadas como um array unidimensional. Não bidimensional, mas unidimensional! Matrizes unidimensionais são muito mais fáceis e rápidas de trabalhar.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Exemplo de matriz de tanque de jogo

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Representação da matriz do tanque de jogo como uma matriz unidimensional

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Um exemplo mais visual de representação de uma matriz como uma matriz unidimensional

Mas o acesso aos elementos do array ocorre em loop duplo, como se não fosse um array unidimensional, mas bidimensional. Isso é feito porque ainda trabalhamos com matrizes.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Percorrendo um array unidimensional em um loop duplo. Y - identificador de linha, X - identificador de coluna

Observação: em vez dos identificadores de matriz usuais i, j, uso os identificadores x e y. Assim, parece-me, é mais agradável aos olhos e mais compreensível ao cérebro. Além disso, tal notação permite projetar convenientemente as matrizes utilizadas nos eixos coordenados de uma imagem bidimensional.

Agora sobre pixels, cores e saída de tela. A função StretchDIBits é usada para saída (Cabeçalho: windows.h; Biblioteca: gdi32.lib). Esta função, entre outras coisas, recebe o seguinte: o dispositivo no qual a imagem é exibida (no meu caso, é o console do Windows), as coordenadas de início da exibição da imagem, sua largura/altura e a própria imagem no forma de um bitmap, representado por uma matriz de bytes. Bitmap como uma matriz de bytes!

Função StretchDIBits() em ação:

// 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 memória é alocada antecipadamente para esse bitmap usando a função VirtualAlloc(). Ou seja, a quantidade necessária de bytes é reservada para armazenar informações sobre todos os pixels, que serão então exibidos na tela.

Criando o bitmap 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);

Grosso modo, um bitmap consiste em um conjunto de pixels. Cada quatro bytes na matriz é um pixel RGB. Um byte por valor de cor vermelha, um byte por valor de cor verde (G) e um byte por valor de cor azul (B). Além disso, resta um byte para recuo. Estas três cores - Vermelho/Verde/Azul (RGB) - são misturadas entre si em diferentes proporções para criar a cor do pixel resultante.

Agora, novamente, cada retângulo, ou objeto do jogo, é representado por uma matriz numérica. Todos esses objetos do jogo são colocados em uma coleção. E então eles são colocados no campo de jogo, formando uma grande matriz numérica. Associei cada número da matriz a uma cor específica. Por exemplo, o número 8 corresponde ao azul, o número 9 ao amarelo, o número 10 ao cinza escuro e assim por diante. Assim, podemos dizer que temos uma matriz do campo de jogo, onde cada número é uma cor.

Portanto, temos uma matriz numérica de todo o campo de jogo de um lado e um bitmap para exibir a imagem do outro. Até o momento, o bitmap está “vazio” - ainda não contém informações sobre os pixels da cor desejada. Isso significa que a última etapa será preencher o bitmap com informações sobre cada pixel com base na matriz numérica do campo de jogo. Um exemplo claro de tal transformação está na imagem abaixo.

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Um exemplo de preenchimento de um bitmap (matriz de pixels) com informações baseadas na matriz digital do campo de jogo (os índices de cores não correspondem aos índices do jogo)

Também apresentarei um trecho de código real do jogo. A variável colorIndex em cada iteração do loop recebe um valor (índice de cores) da matriz numérica do campo de jogo (mainDigitalMatrix). A variável color é então definida como a própria cor com base no índice. A cor resultante é então dividida na proporção de vermelho, verde e azul (RGB). E junto com o pixelPadding, essas informações são gravadas no pixel repetidamente, formando uma imagem colorida no bitmap.

O código usa ponteiros e operações bit a bit, que podem ser difíceis de entender. Portanto, aconselho você a ler em algum lugar separadamente como essas estruturas funcionam.

Preenchendo o bitmap com informações baseadas na matriz numérica do campo de jogo:

// 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 acordo com o método descrito acima, no jogo Crazy Tanks uma imagem (quadro) é formada e exibida na tela na função Draw(). Após registrar as teclas digitadas na função Input() e seu posterior processamento na função Logic(), uma nova imagem (quadro) é formada. É verdade que os objetos do jogo já podem ter uma posição diferente no campo de jogo e, conseqüentemente, serem desenhados em um local diferente. É assim que a animação (movimento) acontece.

Em teoria (se não esqueci de nada), entender o loop do jogo do primeiro jogo (“Snake”) e o sistema de exibição de pixels na tela do segundo jogo (“Tanks”) é tudo que você precisa para escrever qualquer dos seus jogos 2D no Windows. Sem som! 😉 O resto das peças é apenas um vôo de fantasia.

Claro, o jogo “Tanks” é muito mais complexo que “Snake”. Já utilizei a linguagem C++, ou seja, descrevi diversos objetos do jogo com classes. Criei minha própria coleção - o código pode ser visualizado em headers/Box.h. A propósito, a coleção provavelmente apresenta vazamento de memória. Ponteiros usados. Trabalhou com memória. Devo dizer que o livro me ajudou muito Iniciando C++ por meio da programação de jogos. Este é um ótimo começo para iniciantes em C++. É pequeno, interessante e bem organizado.

Demorou cerca de seis meses para desenvolver este jogo. Escrevi principalmente durante o almoço e lanches no trabalho. Ele sentou-se na cozinha do escritório, pisoteou a comida e escreveu códigos. Ou no jantar em casa. Então acabei com essas “guerras de cozinha”. Como sempre, usei ativamente um caderno e nele nasceram todas as coisas conceituais.

Para completar a parte prática, vou fazer algumas digitalizações no meu caderno. Para mostrar exatamente o que escrevi, desenhei, contei, projetei...

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Projetando imagens de tanques. E determinar quantos pixels cada tanque deve ocupar na tela

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Cálculo do algoritmo e fórmulas para rotação do tanque em torno de seu eixo

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
Esquema da minha coleção (aquela em que há vazamento de memória, provavelmente). A coleção é criada de acordo com o tipo Linked List

DevOps C++ e "guerras na cozinha", ou Como comecei a escrever jogos enquanto comia
E estas são tentativas fúteis de anexar inteligência artificial ao jogo

Теория

“Mesmo uma jornada de mil milhas começa com o primeiro passo” (Antiga sabedoria chinesa)

Vamos passar da prática à teoria! Como encontrar tempo para o seu hobby?

  1. Determine o que você realmente quer (infelizmente, esta é a parte mais difícil).
  2. Estabeleça prioridades.
  3. Sacrifique tudo o que é “extra” em prol de prioridades mais elevadas.
  4. Mova-se em direção às metas todos os dias.
  5. Não espere duas ou três horas de tempo livre para dedicar a um hobby.

Por um lado, você precisa determinar o que deseja e priorizar. Por outro lado, é possível abandonar algumas atividades/projetos em favor destas prioridades. Em outras palavras, você terá que sacrificar tudo “extra”. Ouvi em algum lugar que deveria haver no máximo três atividades principais na vida. Então você poderá fazê-los com a mais alta qualidade. E projetos/direções adicionais simplesmente começarão a ficar sobrecarregados. Mas tudo isso provavelmente é subjetivo e individual.

Existe uma certa regra de ouro: nunca tenha um dia de 0%! Aprendi sobre isso em um artigo de um desenvolvedor independente. Se você estiver trabalhando em um projeto, faça algo a respeito todos os dias. E não importa o quanto você faça. Escreva uma palavra ou uma linha de código, assista a um vídeo tutorial ou martele um prego em uma placa - basta fazer alguma coisa. O mais difícil é começar. Depois de começar, provavelmente acabará fazendo um pouco mais do que queria. Assim você avançará constantemente em direção ao seu objetivo e, acredite, muito rapidamente. Afinal, o principal obstáculo para todas as coisas é a procrastinação.

E é importante lembrar que não se deve subestimar e ignorar a “serragem” gratuita do tempo de 5, 10, 15 minutos, esperar por algumas “toras” grandes que duram uma ou duas horas. Você está na fila? Pense em algo para o seu projeto. Pegando a escada rolante? Escreva algo em um bloco de notas. Você está viajando de ônibus? Ótimo, leia algum artigo. Aproveite todas as oportunidades. Pare de assistir cães e gatos no YouTube! Não polua seu cérebro!

E uma última coisa. Se depois de ler este artigo você gostou da ideia de criar jogos sem usar motores de jogo, então lembre-se do nome Casey Muratori. Esse cara tem site. Na seção “assistir -> EPISÓDIOS ANTERIORES” você encontrará maravilhosos tutoriais em vídeo gratuitos sobre como criar um jogo profissional do zero. Em cinco lições de introdução ao C para Windows você provavelmente aprenderá mais do que em cinco anos de estudo universitário (alguém escreveu sobre isso nos comentários abaixo do vídeo).

Casey também explica que ao desenvolver seu próprio motor de jogo, você terá uma melhor compreensão de quaisquer motores existentes. Em um mundo de frameworks onde todos estão tentando automatizar, você aprende a criar em vez de usar. Você entende a própria natureza dos computadores. E você também se tornará um programador muito mais inteligente e maduro – um profissional.

Boa sorte no caminho escolhido! E vamos tornar o mundo mais profissional.

Autor: Grankin Andrei, DevOps



Fonte: habr.com