DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo

“So di non sapere nulla” Socrate

Per chi: per gli IT a cui non interessano tutti gli sviluppatori e vogliono giocare ai loro giochi!

Riguardo a cosa: su come iniziare a scrivere giochi in C/C++, se all'improvviso ne hai bisogno!

Perché dovresti leggere questo: Lo sviluppo di applicazioni non è la mia area di competenza, ma provo a programmare ogni settimana. Perché adoro i giochi!

Ciao il mio nome è Andrey Grankin, Sono DevOps presso Luxoft. Lo sviluppo di applicazioni non è la mia specialità, ma provo a programmare ogni settimana. Perché adoro i giochi!

L'industria dei giochi per computer è enorme e si dice che sia ancora più grande dell'industria cinematografica odierna. I giochi sono stati scritti fin dagli albori dei computer, utilizzando, secondo gli standard moderni, metodi di sviluppo complessi e basilari. Nel corso del tempo, iniziarono ad apparire motori di gioco con grafica, fisica e suono già programmati. Ti consentono di concentrarti sullo sviluppo del gioco stesso e di non preoccuparti delle sue fondamenta. Ma insieme a loro, con i motori, gli sviluppatori “diventano ciechi” e si degradano. La produzione stessa dei giochi viene messa sul nastro trasportatore. E la quantità dei prodotti comincia a prevalere sulla qualità.

Allo stesso tempo, quando giochiamo ai giochi di altre persone, siamo costantemente limitati dai luoghi, dalla trama, dai personaggi e dalle meccaniche di gioco inventate da altre persone. Quindi ho capito che...

... è giunto il momento di creare i miei mondi, soggetti solo a me. Mondi dove Io sono il Padre, il Figlio e lo Spirito Santo!

E credo sinceramente che scrivendo il tuo motore di gioco e giocandoci, potrai toglierti le scarpe, pulire i finestrini e migliorare la tua cabina, diventando un programmatore più esperto e completo.

In questo articolo cercherò di raccontarti come ho iniziato a scrivere piccoli giochi in C/C++, com'è il processo di sviluppo e dove trovo il tempo per un hobby in un ambiente frenetico. È soggettivo e descrive il processo di avvio individuale. Materiale sull'ignoranza e sulla fede, sulla mia immagine personale del mondo in questo momento. In altre parole: “L’amministrazione non è responsabile del vostro cervello personale!”

Pratica

“La conoscenza senza pratica è inutile, la pratica senza conoscenza è pericolosa” Confucio

Il mio quaderno è la mia vita!


Quindi, in pratica, posso dire che per me tutto inizia con un blocco note. Lì non solo scrivo le mie attività quotidiane, ma disegno, programma, progetto diagrammi di flusso e risolvo problemi, compresi quelli matematici. Usa sempre un blocco note e scrivi solo a matita. È pulito, conveniente e affidabile, IMHO.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Il mio quaderno (già pieno). Questo è quello che sembra. Contiene attività quotidiane, idee, disegni, diagrammi, soluzioni, contabilità nera, codice e così via

A questo punto, sono riuscito a completare tre progetti (questo rientra nel mio concetto di "completezza", perché qualsiasi prodotto può essere sviluppato in modo relativamente infinito).

  • Progetto 0: Questa è una scena demo di architetto 3D scritta in C# utilizzando il motore di gioco Unity. Per piattaforme macOS e Windows.
  • Gioco 1: gioco per console Simple Snake (noto a tutti come “Snake”) per Windows. Scritto in C.
  • Gioco 2: gioco per console Crazy Tanks (noto a tutti come “Tanks”), scritto in C++ (usando le classi) e anche per Windows.

Progetto 0. Demo dell'architetto

  • Piattaforma: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Lingua: C#
  • Motore di gioco: Unità
  • Ispirazione: Darrin Lile
  • Deposito: GitHub

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Demo dell'architetto di scene 3D

Il primo progetto è stato implementato non in C/C++, ma in C# utilizzando il motore di gioco Unity. Questo motore non era così esigente in termini di hardware come Unreal Enginee sembrava anche più facile da installare e utilizzare. Non ho considerato altri motori.

Il mio obiettivo in Unity non era sviluppare un gioco. Volevo creare una scena 3D con qualche personaggio. Lui, o meglio Lei (ho fatto da modello alla ragazza di cui ero innamorato =) doveva muoversi e interagire con il mondo che lo circondava. Era importante solo capire cos'è Unity, qual è il processo di sviluppo e quanto impegno è necessario per creare qualcosa. È così che è nato il progetto Architect Demo (il nome è stato inventato quasi dal nulla). Programmazione, modellazione, animazione e texturing mi hanno richiesto probabilmente due mesi di lavoro quotidiano.

Ho iniziato con video tutorial su YouTube sulla creazione di modelli 3D in Frullatore. Blender è un ottimo strumento gratuito per la modellazione 3D (e non solo) che non richiede installazione. E qui mi aspettava uno shock... Si scopre che la modellazione, l'animazione, la texturizzazione sono enormi argomenti separati su cui puoi scrivere libri. Ciò è particolarmente vero per i personaggi. Per modellare dita, denti, occhi e altre parti del corpo, avrai bisogno della conoscenza dell'anatomia. Come sono strutturati i muscoli facciali? Come si muovono le persone? Ho dovuto "inserire" le ossa in ogni braccio, gamba, dito, falangi delle dita!

Modella le clavicole e le ossa delle leve aggiuntive per rendere l'animazione naturale. Dopo tali lezioni, ti rendi conto di quanto lavoro fanno i creatori di film d'animazione solo per creare 30 secondi di video. Ma i film in 3D durano ore! E poi usciamo dai cinema e diciamo qualcosa del tipo: “Quello è un cartone animato/film di merda! Potevano fare meglio..."Sciocchi!

E ancora una cosa riguardante la programmazione in questo progetto. A quanto pare, la parte più interessante per me è stata quella matematica. Se esegui la scena (link al repository nella descrizione del progetto), noterai che la telecamera ruota attorno al personaggio della ragazza in una sfera. Per programmare tale rotazione della telecamera, ho dovuto prima calcolare le coordinate del punto di posizione sul cerchio (2D) e poi sulla sfera (3D). La cosa divertente è che odiavo la matematica a scuola e la conoscevo con un C-meno. In parte, probabilmente, perché a scuola semplicemente non ti spiegano come diavolo si applica questa matematica nella vita. Ma quando sei ossessionato dal tuo obiettivo, dal tuo sogno, la tua mente si schiarisce e si apre! E inizi a percepire i compiti difficili come un'avventura emozionante. E poi pensi: “Bene, perché il tuo matematico *preferito* non potrebbe dirti normalmente dove possono essere applicate queste formule?”

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Calcolo delle formule per calcolare le coordinate di un punto su un cerchio e su una sfera (dal mio taccuino)

Gioco 1. Serpente semplice

  • Piattaforma: Windows (testato su Windows 7, 10)
  • Lingua: Penso di averlo scritto in puro C
  • Motore di gioco: Consolle Windows
  • Ispirazione: javidx9
  • Deposito: GitHub

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Semplice gioco del serpente

Una scena 3D non è un gioco. Inoltre, modellare e animare oggetti 3D (soprattutto personaggi) è difficile e dispendioso in termini di tempo. Dopo aver giocato con Unity, mi sono reso conto che dovevo continuare, o meglio iniziare, dalle basi. Qualcosa di semplice e veloce, ma allo stesso tempo globale, per comprendere la struttura stessa dei giochi.

Cosa è semplice e veloce? Esatto, console e 2D. Più precisamente, anche la console e i simboli. Ancora una volta sono andato a cercare ispirazione su Internet (in generale, penso che Internet sia l'invenzione più rivoluzionaria e pericolosa del XNUMX° secolo). Ho scovato un video di un programmatore che ha realizzato la console Tetris. E a somiglianza del suo gioco ho deciso di creare un “serpente”. Dal video ho imparato due cose fondamentali: il loop del gioco (con tre funzioni/parti di base) e l'output nel buffer.

Il ciclo del gioco potrebbe assomigliare a questo:

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

Il codice presenta l'intera funzione main() contemporaneamente. E il ciclo di gioco inizia dopo il commento appropriato. Ci sono tre funzioni di base nel ciclo: Input(), Logic(), Draw(). Innanzitutto, input dati Input (principalmente controllo della pressione dei tasti), quindi elaborazione dei dati inseriti Logic, quindi output sullo schermo - Draw. E così ad ogni fotogramma. Ecco come viene creata l'animazione. È come nei cartoni animati. In genere, l'elaborazione dei dati inseriti richiede più tempo e, per quanto ne so, determina il frame rate del gioco. Ma qui la funzione Logic() viene eseguita molto rapidamente. Pertanto, devi controllare il frame rate utilizzando la funzione Sleep() con il parametro gameSpeed, che determina questa velocità.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Ciclo di gioco. Programmare un "serpente" in un blocco note

Se stai sviluppando un gioco per console basato su personaggi, non sarai in grado di visualizzare i dati sullo schermo utilizzando il normale flusso di output 'cout': è molto lento. Pertanto, l'output deve essere inviato al buffer dello schermo. Questo è molto più veloce e il gioco funzionerà senza problemi. Ad essere sincero, non capisco bene cosa sia un buffer dello schermo e come funzioni. Ma darò qui un codice di esempio e forse qualcuno potrà chiarire la situazione nei commenti.

Ottenere un buffer dello schermo (per così dire):

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

Visualizzazione diretta di una determinata stringa scoreLine (riga di visualizzazione del punteggio):

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

In teoria, non c'è nulla di complicato in questo gioco; penso che sia un buon esempio entry-level. Il codice è scritto in un file e formattato in diverse funzioni. Nessuna classe, nessuna eredità. Puoi vedere tutto tu stesso nel codice sorgente del gioco andando al repository su GitHub.

Gioco 2. Carri armati pazzi

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Gioco Carri armati pazzi

Stampare i personaggi sulla console è probabilmente la cosa più semplice che puoi trasformare in un gioco. Ma poi sorge un problema: i simboli hanno altezze e larghezze diverse (l'altezza è maggiore della larghezza). In questo modo, tutto sembrerà sproporzionato e lo spostamento verso il basso o l'alto sembrerà molto più veloce dello spostamento verso sinistra o destra. Questo effetto è molto evidente in Snake (Gioco 1). "Tanks" (Gioco 2) non presenta questo inconveniente, poiché l'output è organizzato dipingendo i pixel dello schermo con colori diversi. Si potrebbe dire che ho scritto un renderer. È vero, questo è un po 'più complicato, anche se molto più interessante.

Per questo gioco basterà descrivere il mio sistema di visualizzazione dei pixel sullo schermo. Considero questa la parte principale del gioco. E puoi inventare tutto il resto da solo.

Quindi, quello che vedi sullo schermo è solo una serie di rettangoli multicolori in movimento.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Insieme di rettangoli

Ogni rettangolo è rappresentato da una matrice piena di numeri. A proposito, posso evidenziare una sfumatura interessante: tutte le matrici nel gioco sono programmate come un array unidimensionale. Non bidimensionale, ma monodimensionale! È molto più semplice e veloce lavorare con gli array unidimensionali.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Esempio di matrice di un carro armato da gioco

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Rappresentazione della matrice del carro armato di gioco come array unidimensionale

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Un esempio più visivo di rappresentazione di una matrice come array unidimensionale

Ma l'accesso agli elementi dell'array avviene in un doppio loop, come se si trattasse di un array non unidimensionale, ma bidimensionale. Questo viene fatto perché lavoriamo ancora con le matrici.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Attraversamento di un array unidimensionale in un doppio ciclo. Y - identificatore di riga, X - identificatore di colonna

Nota: invece dei soliti identificatori di matrice i, j, utilizzo gli identificatori x e y. In questo modo, mi sembra, è più gradevole alla vista e più comprensibile al cervello. Inoltre, tale notazione permette di proiettare convenientemente le matrici utilizzate sugli assi coordinati di un'immagine bidimensionale.

Ora parliamo di pixel, colore e output dello schermo. La funzione StretchDIBits viene utilizzata per l'output (intestazione: windows.h; libreria: gdi32.lib). Questa funzione, tra le altre cose, riceve: il dispositivo su cui viene visualizzata l'immagine (nel mio caso è la console Windows), le coordinate iniziali di visualizzazione dell'immagine, la sua larghezza/altezza e l'immagine stessa nel forma di una bitmap, rappresentata da un array di byte. Bitmap come array di byte!

Funzione StretchDIBits() in azione:

// 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 viene allocata in anticipo per questa bitmap utilizzando la funzione VirtualAlloc(). Cioè, il numero richiesto di byte è riservato per memorizzare le informazioni su tutti i pixel, che verranno poi visualizzate sullo schermo.

Creazione della 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);

In parole povere, una bitmap è costituita da un insieme di pixel. Ogni quattro byte nell'array c'è un pixel RGB. Un byte per valore di colore rosso, un byte per valore di colore verde (G) e un byte per valore di colore blu (B). Inoltre rimane un byte per il rientro. Questi tre colori - Rosso/Verde/Blu (RGB) - vengono mescolati tra loro in proporzioni diverse per creare il colore pixel risultante.

Ora, ancora una volta, ogni rettangolo, o oggetto di gioco, è rappresentato da una matrice numerica. Tutti questi oggetti di gioco vengono inseriti in una raccolta. E poi vengono posizionati sul campo di gioco, formando un'unica grande matrice numerica. Ho associato ogni numero della matrice ad un colore specifico. Ad esempio, il numero 8 corrisponde al blu, il numero 9 al giallo, il numero 10 al grigio scuro e così via. Quindi, possiamo dire che abbiamo una matrice del campo di gioco, dove ogni numero è un colore.

Quindi, abbiamo una matrice numerica dell'intero campo di gioco da un lato e una bitmap per visualizzare l'immagine dall'altro. Finora la bitmap è "vuota": non contiene ancora informazioni sui pixel del colore desiderato. Ciò significa che l'ultimo passaggio sarà popolare la bitmap con informazioni su ciascun pixel in base alla matrice numerica del campo di gioco. Un chiaro esempio di tale trasformazione è nella foto qui sotto.

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Un esempio di riempimento di una bitmap (matrice pixel) con informazioni basate sulla matrice digitale del campo di gioco (gli indici di colore non corrispondono agli indici nel gioco)

Presenterò anche un pezzo di codice reale del gioco. Alla variabile colorIndex ad ogni iterazione del ciclo viene assegnato un valore (indice di colore) dalla matrice numerica del campo di gioco (mainDigitalMatrix). La variabile colore viene quindi impostata sul colore stesso in base all'indice. Il colore risultante viene quindi diviso nel rapporto tra rosso, verde e blu (RGB). E insieme al pixelPadding, queste informazioni vengono scritte ripetutamente nel pixel, formando un'immagine a colori nella bitmap.

Il codice utilizza puntatori e operazioni bit a bit, che possono essere difficili da comprendere. Quindi ti consiglio di leggere da qualche parte separatamente come funzionano tali strutture.

Riempiendo la bitmap con informazioni basate sulla matrice numerica del campo di gioco:

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

Secondo il metodo sopra descritto, nel gioco Crazy Tanks viene formata un'immagine (cornice) e visualizzata sullo schermo nella funzione Draw(). Dopo aver registrato le sequenze di tasti nella funzione Input() e la loro successiva elaborazione nella funzione Logic(), viene formata una nuova immagine (cornice). È vero, gli oggetti del gioco potrebbero già avere una posizione diversa sul campo di gioco e, di conseguenza, essere disegnati in un posto diverso. Ecco come avviene l'animazione (movimento).

In teoria (se non ho dimenticato nulla), comprendere il loop del gioco del primo gioco ("Snake") e il sistema di visualizzazione dei pixel sullo schermo del secondo gioco ("Tanks") è tutto ciò che serve per scrivere qualsiasi dei tuoi giochi 2D sotto Windows. Silenzioso! 😉 Il resto delle parti è solo un volo di fantasia.

Naturalmente, il gioco "Tanks" è molto più complesso di "Snake". Utilizzavo già il linguaggio C++, ovvero descrivevo diversi oggetti di gioco con classi. Ho creato la mia raccolta: il codice può essere visualizzato in headers/Box.h. A proposito, molto probabilmente la raccolta presenta una perdita di memoria. Puntatori utilizzati. Ha lavorato con la memoria. Devo dire che il libro mi ha aiutato molto Inizio C ++ attraverso la programmazione del gioco. Questo è un ottimo inizio per i principianti in C++. È piccolo, interessante e ben organizzato.

Ci sono voluti circa sei mesi per sviluppare questo gioco. Scrivevo principalmente durante il pranzo e gli spuntini al lavoro. Si sedeva nella cucina dell'ufficio, calpestava il cibo e scriveva codici. Oppure a cena a casa. Così mi sono ritrovata con queste “guerre in cucina”. Come sempre, ho utilizzato attivamente un taccuino e tutte le cose concettuali sono nate in esso.

Per completare la parte pratica farò qualche scansione del mio quaderno. Per mostrare cosa esattamente ho scritto, disegnato, contato, progettato...

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Progettare immagini di carri armati. E determinare quanti pixel ciascun carro armato dovrebbe occupare sullo schermo

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Calcolo dell'algoritmo e formule per la rotazione del serbatoio attorno al proprio asse

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
Schema della mia collezione (quella in cui c'è una perdita di memoria, molto probabilmente). La raccolta viene creata in base al tipo Elenco collegato

DevOps C++ e "kitchen wars", o come ho iniziato a scrivere giochi mentre mangiavo
E questi sono inutili tentativi di collegare l’intelligenza artificiale al gioco

Teoria

“Anche un viaggio di mille miglia inizia con il primo passo” (Antica saggezza cinese)

Passiamo dalla pratica alla teoria! Come trovare il tempo per il tuo hobby?

  1. Determina cosa vuoi veramente (ahimè, questa è la parte più difficile).
  2. Stabilisci le priorità.
  3. Sacrificare tutto ciò che è “extra” per il bene di priorità più elevate.
  4. Andare verso obiettivi ogni giorno.
  5. Non aspettarti due o tre ore di tempo libero da dedicare a un hobby.

Da un lato, devi determinare cosa vuoi e stabilire le priorità. D'altro canto, è possibile abbandonare alcune attività/progetti a favore di queste priorità. In altre parole, dovrai sacrificare tutto ciò che è “extra”. Ho sentito da qualche parte che nella vita dovrebbero esserci al massimo tre attività principali. Allora sarai in grado di farli con la massima qualità. E ulteriori progetti/direzioni inizieranno semplicemente a sovraccaricarsi. Ma tutto questo è probabilmente soggettivo e individuale.

Esiste una regola d'oro: non avere mai una giornata allo 0%! L'ho scoperto in un articolo di uno sviluppatore indipendente. Se stai lavorando a un progetto, fai qualcosa ogni giorno. E non importa quanto fai. Scrivi una parola o una riga di codice, guarda un video tutorial o pianta un chiodo su una tavola: fai semplicemente qualcosa. La cosa più difficile è iniziare. Una volta iniziato, probabilmente finirai per fare un po' più di quanto volevi. In questo modo ti sposterai costantemente verso il tuo obiettivo e, credimi, molto rapidamente. Dopotutto, l’ostacolo principale a tutte le cose è la procrastinazione.

Ed è importante ricordare che non bisogna sottovalutare e ignorare la “segatura” gratuita del tempo di 5, 10, 15 minuti, attendere dei “tronchi” di grandi dimensioni della durata di un'ora o due. Sei in fila? Pensa a qualcosa per il tuo progetto. Prendere la scala mobile? Scrivi qualcosa su un taccuino. Viaggi in autobus? Ottimo, leggi qualche articolo. Approfitta di ogni opportunità. Smettila di guardare cani e gatti su YouTube! Non inquinare il tuo cervello!

E un'ultima cosa. Se, dopo aver letto questo articolo, ti è piaciuta l'idea di creare giochi senza utilizzare motori di gioco, allora ricorda il nome Casey Muratori. Questo ragazzo ha сайт. Nella sezione “guarda -> EPISODI PRECEDENTI” troverai meravigliosi video tutorial gratuiti su come creare da zero un gioco professionale. In cinque lezioni di Introduzione al C per Windows probabilmente imparerai di più che in cinque anni di studi universitari (qualcuno ne ha scritto nei commenti sotto il video).

Casey spiega inoltre che sviluppando il proprio motore di gioco, avrai una migliore comprensione di tutti i motori esistenti. In un mondo di framework in cui tutti cercano di automatizzare, impari a creare piuttosto che a utilizzare. Capisci la natura stessa dei computer. E diventerai anche un programmatore molto più intelligente e maturo, un professionista.

Buona fortuna per il percorso scelto! E rendiamo il mondo più professionale.

Autore: Grankin Andrey, DevOps



Fonte: habr.com