DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste

"Jeg ved, at jeg intet ved" Sokrates

Til hvem: for it-folk, der er ligeglade med alle udviklerne og gerne vil spille deres spil!

Om hvad: hvordan man begynder at skrive spil i C/C++, hvis du har brug for det!

Hvorfor skal du læse dette: Appudvikling er ikke mit arbejdsspeciale, men jeg forsøger at kode hver uge. Fordi jeg elsker spil!

Hej mit navn er Andrey Grankin, Jeg er DevOps hos Luxoft. Applikationsudvikling er ikke mit arbejdsspeciale, men jeg forsøger at kode hver uge. Fordi jeg elsker spil!

Computerspilindustrien er enorm, endnu mere rygtet i dag end filmindustrien. Spil er blevet skrevet siden begyndelsen af ​​udviklingen af ​​computere, ved at bruge, efter moderne standarder, komplekse og grundlæggende udviklingsmetoder. Med tiden begyndte spilmotorer at dukke op med allerede programmeret grafik, fysik og lyd. De giver dig mulighed for at fokusere på udviklingen af ​​selve spillet og ikke bekymre dig om dets fundament. Men sammen med dem, med motorerne, "blir udviklerne blinde" og degraderer. Selve produktionen af ​​spil lægges på båndet. Og mængden af ​​produktion begynder at sejre over dens kvalitet.

På samme tid, når vi spiller andres spil, er vi konstant begrænset af de steder, plot, karakterer og spilmekanik, som andre mennesker fandt på. Så jeg indså at...

… det er tid til at skabe jeres egne verdener, kun underlagt mig. Verdener, hvor jeg er Faderen og Sønnen og Helligånden!

Og jeg tror oprigtigt på, at ved at skrive din egen spilmotor og spille på den, vil du være i stand til at tage skoene af, tørre vinduerne og opgradere din kabine og blive en mere erfaren og komplet programmør.

I denne artikel vil jeg forsøge at fortælle dig, hvordan jeg begyndte at skrive små spil i C/C++, hvordan udviklingsprocessen er, og hvor jeg finder tid til en hobby i et travlt miljø. Den er subjektiv og beskriver processen med en individuel start. Materiale om uvidenhed og tro, om mit personlige billede af verden i øjeblikket. Med andre ord: "Administrationen er ikke ansvarlig for dine personlige hjerner!"

Praksis

"Viden uden øvelse er ubrugelig, øvelse uden viden er farlig" Confucius

Min notesbog er mit liv!


Så i praksis kan jeg sige, at alt for mig starter med en notesbog. Jeg skriver ikke kun mine daglige opgaver ned der, men tegner, programmerer, designer flowcharts og løser problemer, herunder matematiske. Brug altid en notesblok og skriv kun med en blyant. Det er rent, komfortabelt og pålideligt, IMHO.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Min (allerede fyldt) notesbog. Sådan ser det ud. Den indeholder hverdagsopgaver, ideer, tegninger, diagrammer, løsninger, sort bogføring, kode og så videre.

På dette stadium lykkedes det mig at gennemføre tre projekter (dette er i min forståelse af "fuldstændighed", fordi ethvert produkt kan udvikles relativt uendeligt).

  • Projekt 0: Dette er en 3D Architect Demo-scene skrevet i C# ved hjælp af Unity-spilmotoren. Til macOS og Windows platforme.
  • Spil 1: konsolspil Simple Snake (kendt for alle som "Snake") til Windows. skrevet i C.
  • Spil 2: konsolspil Crazy Tanks (kendt af alle som "Tanks"), allerede skrevet i C ++ (ved hjælp af klasser) og også under Windows.

Projekt 0 Arkitekt Demo

  • platform: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Sprog: C#
  • Spilmotor: Unity
  • Inspiration: Darrin Lile
  • Depot: GitHub

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
3D Scene Architect Demo

Det første projekt blev implementeret ikke i C/C++, men i C# ved hjælp af Unity-spilmotoren. Denne motor var ikke så krævende på hardware som Unreal Engine, og forekom mig også nemmere at installere og bruge. Jeg overvejede ikke andre motorer.

Målet i Unity for mig var ikke at udvikle en form for spil. Jeg ville skabe en 3D-scene med en form for karakter. Han, eller rettere hun (jeg modellerede den pige, jeg var forelsket i =) skulle bevæge sig og interagere med omverdenen. Det var kun vigtigt at forstå, hvad Unity er, hvad udviklingsprocessen er, og hvor meget indsats det kræver at skabe noget. Sådan blev Architect Demo-projektet født (navnet blev næsten opfundet af bullshit). Programmering, modellering, animation, teksturering tog mig nok to måneders dagligt arbejde.

Jeg startede med vejledningsvideoer på YouTube om at skabe 3D-modeller i Blender. Blender er et fantastisk gratis værktøj til 3D-modellering (og mere), der ikke kræver installation. Og her ventede mig et chok ... Det viser sig, at modellering, animation, teksturering er enorme separate emner, som du kan skrive bøger om. Dette gælder især for karaktererne. For at modellere fingre, tænder, øjne og andre dele af kroppen skal du have kendskab til anatomi. Hvordan er musklerne i ansigtet arrangeret? Hvordan bevæger folk sig? Jeg var nødt til at "indsætte" knogler i hver arm, ben, finger, knoer!

Model nøglebenet, ekstra knoglehåndtag, så animationen ser naturlig ud. Efter sådanne lektioner indser du, hvilket kæmpe arbejde skaberne af animerede film gør, bare for at skabe 30 sekunders video. Men 3D-film holder i timevis! Og så kommer vi ud af biograferne og siger noget i stil med: “Ta, en lorte tegneserie/film! De kunne have gjort det bedre...” Fjolser!

Og en ting mere om programmering i dette projekt. Som det viste sig, var den mest interessante del for mig den matematiske. Hvis du kører scenen (link til depotet i projektbeskrivelsen), vil du bemærke, at kameraet roterer rundt om pigekarakteren i en kugle. For at programmere en sådan rotation af kameraet skulle jeg først beregne koordinaterne for positionspunktet på cirklen (2D) og derefter på kuglen (3D). Det sjove er, at jeg hadede matematik i skolen og vidste det med et minus. Dels sandsynligvis, fordi de i skolen simpelthen ikke forklarer dig, hvordan fanden denne matematik bruges i livet. Men når du er besat af dit mål, drøm, så er sindet renset, åbenbaret! Og du begynder at opfatte komplekse opgaver som et spændende eventyr. Og så tænker du: "Nå, hvorfor kunne din *favorit* matematiker ikke fortælle dig normalt, hvor disse formler kan anvendes?"

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Beregning af formler til beregning af koordinaterne for et punkt på en cirkel og på en kugle (fra min notesbog)

Spil 1

  • platform: Windows (testet på Windows 7, 10)
  • Sprog: Jeg tror det er skrevet i ren C
  • Spilmotor: Windows konsol
  • Inspiration: javidx9
  • Depot: GitHub

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Simpelt Snake spil

3D-scenen er ikke et spil. Derudover er modellering og animering af 3D-objekter (især karakterer) lang og vanskelig. Efter at have leget med Unity, indså jeg, at jeg var nødt til at fortsætte, eller rettere begynde, fra det grundlæggende. Noget enkelt og hurtigt, men samtidig globalt, for at forstå selve strukturen i spil.

Hvad er nemt og hurtigt? Det er rigtigt, konsol og 2D. Mere præcist, selv konsollen og symbolerne. Igen ledte jeg efter inspiration på internettet (generelt tror jeg, at internettet er den mest revolutionerende og farlige opfindelse i det XNUMX. århundrede). Jeg gravede en video op af en programmør, der lavede konsollen Tetris. Og i lighed med hans spil besluttede jeg at lave en "slange". Fra videoen lærte jeg om to grundlæggende ting - spilløkken (med tre grundlæggende funktioner/dele) og output til bufferen.

Spilløkken kan se sådan ud:

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

Koden præsenterer hele main()-funktionen på én gang. Og spilcyklussen starter efter den tilsvarende kommentar. Der er tre grundlæggende funktioner i løkken: Input(), Logic(), Draw(). Først skal du indtaste datainput (hovedsageligt kontrol af tastetryk), derefter behandle de indtastede data Logik, derefter vise på skærmen - Tegn. Og så hver eneste ramme. Animation skabes på denne måde. Det er ligesom tegnefilm. Normalt tager behandlingen af ​​inputdata mest tid og bestemmer, så vidt jeg ved, spillets billedhastighed. Men her er Logic()-funktionen meget hurtig. Derfor skal billedhastigheden styres af Sleep()-funktionen med gameSpeed ​​​​parameteren, som bestemmer denne hastighed.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
spil cyklus. Snake programmering i notesblok

Hvis du udvikler et karakterbaseret konsolspil, vil du ikke være i stand til at udlæse data til skærmen ved at bruge det almindelige 'cout'-streamoutput – det er meget langsomt. Derfor skal outputtet udføres i skærmbufferen. Så meget hurtigere, og spillet vil fungere uden fejl. For at være ærlig forstår jeg ikke helt, hvad en skærmbuffer er, og hvordan den virker. Men jeg vil give et eksempel på kode her, og måske kan nogen afklare situationen i kommentarerne.

Få en skærmbuffer (så at sige):

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

Direkte output til skærmen for en bestemt linjescoreLine (linjen til visning af scoringer):

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

I teorien er der ikke noget kompliceret i dette spil; jeg synes, det er et godt eksempel på entry-level. Koden er skrevet i én fil og arrangeret i flere funktioner. Ingen klasser, ingen arv. Du kan selv se alt i spillets kildekode ved at gå til repository på GitHub.

Spil 2 Crazy Tanks

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Crazy Tanks spil

At printe karakterer til konsollen er nok det enkleste, du kan gøre til et spil. Men så dukker et problem op: Symbolerne har forskellige højder og bredder (højden er større end bredden). På denne måde vil alt se ud af proportioner, og at flytte ned eller op vil se meget hurtigere ud end at flytte til venstre eller højre. Denne effekt er meget mærkbar i "Snake" (spil 1). "Tanks" (Spil 2) har ikke denne ulempe, da outputtet der er organiseret ved at male skærmens pixels med forskellige farver. Man kan sige, at jeg skrev en renderer. Sandt nok er dette allerede lidt mere kompliceret, selvom det er meget mere interessant.

Til dette spil vil det være nok at beskrive mit system til at vise pixels på skærmen. Jeg tror, ​​det er hoveddelen af ​​spillet. Og alt det andet kan du selv finde på.

Så det, du ser på skærmen, er bare et sæt bevægelige farvede rektangler.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Rektangel sæt

Hvert rektangel er repræsenteret af en matrix fyldt med tal. I øvrigt kan jeg fremhæve en interessant nuance - alle matricerne i spillet er programmeret som en endimensionel array. Ikke todimensionelt, men endimensionelt! Endimensionelle arrays er meget nemmere og hurtigere at arbejde med.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Et eksempel på en spiltankmatrix

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Repræsenterer matrixen af ​​en spiltank med et endimensionelt array

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Et mere illustrativt eksempel på en matrixrepræsentation ved en endimensionel matrix

Men adgang til elementerne i arrayet sker i en dobbelt sløjfe, som om det ikke var et endimensionelt array, men et todimensionelt. Dette gøres, fordi vi stadig arbejder med matricer.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
At krydse et endimensionelt array i en dobbelt sløjfe. Y er række-id'et, X er kolonne-id'et

Bemærk venligst: I stedet for de sædvanlige matrixidentifikatorer i, j, bruger jeg identifikatorerne x og y. Så det forekommer mig, mere behageligt for øjet og klarere for hjernen. Derudover gør en sådan notation det muligt bekvemt at projicere de anvendte matricer på koordinatakserne i et todimensionalt billede.

Nu om pixels, farve og display. StretchDIBits-funktionen (Header: windows.h; Library: gdi32.lib) bruges til output. Blandt andet overføres følgende til denne funktion: den enhed, som billedet vises på (i mit tilfælde er dette Windows-konsollen), koordinaterne for begyndelsen af ​​visningen af ​​billedet, dets bredde/højde og billedet sig selv i form af en bitmap (bitmap), repræsenteret ved en række bytes. Bitmap som en række bytes!

StretchDIBits()-funktionen på arbejde:

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

Hukommelse allokeres på forhånd til denne bitmap ved hjælp af VirtualAlloc()-funktionen. Det vil sige, at det nødvendige antal bytes er reserveret til at gemme information om alle pixels, som så vil blive vist på skærmen.

Oprettelse af m_p_bitmapMemory bitmap:

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

Groft sagt består en bitmap af et sæt pixels. Hver fjerde byte i arrayet er en RGB-pixel. En byte pr. rød farveværdi, en byte pr. grøn farveværdi (G) og en byte pr. blå farveværdi (B). Plus der er en byte tilbage til indrykning. Disse tre farver - Rød/Grøn/Blå (RGB) - blandes med hinanden i forskellige proportioner for at skabe den resulterende pixelfarve.

Nu, igen, er hvert rektangel eller spilobjekt repræsenteret af en talmatrix. Alle disse spilobjekter er placeret i en samling. Og så placeres de på spillebanen og danner én stor numerisk matrix. Jeg kortlagde hvert tal i matrixen til en bestemt farve. For eksempel er tallet 8 blåt, tallet 9 er gult, tallet 10 er mørkegrå og så videre. Således kan vi sige, at vi har en matrix af spillefeltet, hvor hvert tal er en slags farve.

Så vi har en numerisk matrix af hele spillefeltet på den ene side og en bitmap til at vise billedet på den anden side. Indtil videre er bitmappet "tomt" - det har endnu ikke information om pixels i den ønskede farve. Det betyder, at det sidste trin vil være at fylde bitmappet med information om hver pixel baseret på spillefeltets numeriske matrix. Et illustrativt eksempel på en sådan transformation er på billedet nedenfor.

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Et eksempel på udfyldning af en bitmap (Pixel matrix) med information baseret på den numeriske matrix (Digital matrix) af spillefeltet (farveindekser matcher ikke indeksene i spillet)

Jeg vil også præsentere et stykke ægte kode fra spillet. Det variable colorIndex ved hver iteration af løkken tildeles en værdi (farveindeks) fra spillefeltets numeriske matrix (mainDigitalMatrix). Derefter skrives selve farven til farvevariablen baseret på indekset. Yderligere er den resulterende farve opdelt i forholdet mellem rød, grøn og blå (RGB). Og sammen med indrykket (pixelPadding) bliver denne information skrevet til pixlen igen og igen og danner et farvebillede i bitmap'en.

Koden bruger pointere og bitvise operationer, som kan være svære at forstå. Så jeg råder dig til at læse separat et sted, hvordan sådanne strukturer fungerer.

Udfyldning af en bitmap med information baseret på den numeriske matrix af spillefeltet:

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

Ifølge metoden beskrevet ovenfor dannes ét billede (ramme) i Crazy Tanks-spillet og vises på skærmen i Draw()-funktionen. Efter registrering af tastetryk i Input()-funktionen og deres efterfølgende behandling i Logic()-funktionen, dannes et nyt billede (ramme). Sandt nok kan spilobjekter allerede have en anden position på spillefeltet og er derfor tegnet et andet sted. Sådan opstår animation (bevægelse).

I teorien (hvis jeg ikke har glemt noget), er det alt hvad du behøver at forstå spilløkken fra det første spil ("Snake") og systemet til visning af pixels på skærmen fra det andet spil ("Tanks"). af dine 2D-spil under Windows. Lydløs! 😉 Resten af ​​delene er bare en fantasi.

Selvfølgelig er spillet "Tanks" designet meget mere kompliceret end "Snake". Jeg brugte allerede C++ sproget, det vil sige, jeg beskrev forskellige spilobjekter med klasser. Jeg har lavet min egen samling - du kan se koden i headers/Box.h. Samlingen har i øvrigt højst sandsynligt en hukommelseslækage. Brugte pointer. Arbejdede med hukommelse. Jeg må sige, at bogen hjalp mig meget. Begynder C++ gennem spilprogrammering. Dette er en god start for begyndere i C++. Det er lille, interessant og velorganiseret.

Det tog omkring seks måneder at udvikle dette spil. Jeg skrev hovedsageligt til frokost og mellemmåltider på arbejdet. Han sad i kontorkøkkenet, trampede på mad og skrev kode. Eller til middag derhjemme. Så jeg fik sådan nogle "køkkenkrige". Som altid brugte jeg aktivt en notesbog, og alle de konceptuelle ting blev født i den.

Til sidst i den praktiske del vil jeg trække et par scanninger af min notesbog frem. For at vise, hvad jeg præcist skrev ned, tegnede, talte, designede...

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Tank billede design. Og definitionen af, hvor mange pixels hver tank skal optage på skærmen

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Beregning af algoritmen og formlerne for tankens rotation omkring sin akse

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Diagram over min samling (den med hukommelseslækken, højst sandsynligt). Samlingen oprettes som en linket liste

DevOps C++ og "kitchen wars", eller hvordan jeg begyndte at skrive spil, mens jeg spiste
Og det er forgæves forsøg på at skrue kunstig intelligens ind i spillet

Теория

"Selv en rejse på tusind miles begynder med det første skridt" (gammel kinesisk visdom)

Lad os gå fra praksis til teori! Hvordan finder du tid til din hobby?

  1. Bestem, hvad du virkelig vil have (ak, dette er det sværeste).
  2. Sæt prioriteter.
  3. Ofre alt "overflødigt" af hensyn til højere prioriteter.
  4. Bevæg dig mod dine mål hver dag.
  5. Forvent ikke to eller tre timers fritid til at bruge på en hobby.

På den ene side skal du bestemme, hvad du vil og prioritere. På den anden side er det muligt at opgive nogle aktiviteter/projekter til fordel for disse prioriteringer. Du bliver med andre ord nødt til at ofre alt "overflødigt". Jeg hørte et sted, at der i livet højst skulle være tre hovedaktiviteter. Så vil du kunne håndtere dem bedst muligt. Og yderligere projekter/retninger vil begynde at overbelaste corny. Men alt dette er sandsynligvis subjektivt og individuelt.

Der er en vis gylden regel: Hav aldrig en 0%-dag! Jeg lærte om det i en artikel af en indie-udvikler. Hvis du arbejder på et projekt, så gør noget ved det hver dag. Og det er lige meget, hvor meget du gør. Skriv et ord eller en linje kode, se en vejledningsvideo eller slå et søm i et bræt - bare gør noget. Det sværeste er at starte. Når du først er startet, ender du sandsynligvis med at gøre lidt mere, end du ønskede. På denne måde vil du hele tiden bevæge dig mod dit mål og, tro mig, meget hurtigt. Den største hindring for alle ting er trods alt udsættelse.

Og det er vigtigt at huske, at du ikke skal undervurdere og ignorere det gratis "savsmuld" af tid på 5, 10, 15 minutter, vent på nogle store "logs", der varer en time eller to. Står du i kø? Tænk på noget til dit projekt. Skal du op ad rulletrappen? Skriv noget ned i en notesbog. Spiser du i bussen? Okay, læs en artikel. Brug enhver mulighed. Stop med at se katte og hunde på YouTube! Lad være med at rode med din hjerne!

Og den sidste. Hvis du efter at have læst denne artikel kunne lide ideen om at skabe spil uden at bruge spilmotorer, så husk navnet Casey Muratori. Det har denne fyr сайт. I afsnittet "se -> TIDLIGERE EPISODER" finder du vidunderlige gratis videotutorials om at skabe et professionelt spil fra bunden. I fem Intro til C til Windows-lektioner vil du sandsynligvis lære mere end på fem års universitetsstudier (nogen skrev om dette i kommentarerne under videoen).

Casey forklarer også, at ved at udvikle din egen spilmotor, vil du få en bedre forståelse af eksisterende motorer. I rammernes verden, hvor alle forsøger at automatisere, vil du lære at skabe, ikke bruge. Forstå selve computernes natur. Og du bliver også en meget mere intelligent og moden programmør - en proff.

Held og lykke på din valgte vej! Og lad os gøre verden mere professionel.

Forfatter: Grankin Andrey, DevOps



Kilde: www.habr.com