DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste

"Jeg vet at jeg ikke vet noe" Sokrates

For hvem: for IT-folk som spytter på alle utviklerne og vil spille spillene deres!

Om hva: hvordan begynne å skrive spill i C/C++ hvis du trenger det!

Hvorfor bør du lese dette: Apputvikling er ikke min arbeidsspesialitet, men jeg prøver å kode hver uke. Fordi jeg elsker spill!

Hei mitt navn er Andrey Grankin, jeg er DevOps hos Luxoft. Applikasjonsutvikling er ikke min arbeidsspesialitet, men jeg prøver å kode hver uke. Fordi jeg elsker spill!

Dataspillindustrien er enorm, enda mer ryktet i dag enn filmindustrien. Spill har blitt skrevet siden begynnelsen av utviklingen av datamaskiner, ved å bruke, etter moderne standarder, komplekse og grunnleggende utviklingsmetoder. Over tid begynte spillmotorer å dukke opp med allerede programmert grafikk, fysikk og lyd. De lar deg fokusere på utviklingen av selve spillet og ikke bry deg om grunnlaget. Men sammen med dem, med motorene, "blir" utviklerne og degraderer. Selve produksjonen av spill legges på løpende bånd. Og produksjonsmengden begynner å råde over kvaliteten.

Samtidig, når vi spiller andres spill, er vi hele tiden begrenset av lokasjoner, plot, karakterer, spillmekanikk som andre kom på. Så jeg skjønte at...

… det er på tide å skape dine egne verdener, kun underlagt meg. Verdener hvor jeg er Faderen og Sønnen og Den Hellige Ånd!

Og jeg tror oppriktig at ved å skrive din egen spillmotor og et spill på den, vil du kunne åpne øynene, tørke vinduene og pumpe hytta, og bli en mer erfaren og integrert programmerer.

I denne artikkelen vil jeg prøve å fortelle deg hvordan jeg begynte å skrive små spill i C/C++, hva som er utviklingsprosessen og hvor jeg finner tid til en hobby i et travelt miljø. Den er subjektiv og beskriver prosessen med en individuell start. Materiale om uvitenhet og tro, om mitt personlige bilde av verden for tiden. Med andre ord: "Administrasjonen er ikke ansvarlig for dine personlige hjerner!".

Praksis

"Kunnskap uten praksis er ubrukelig, praksis uten kunnskap er farlig." Confucius

Min notatbok er livet mitt!


Så i praksis kan jeg si at alt for meg starter med en notatbok. Jeg skriver ned ikke bare mine daglige oppgaver der, men tegner, programmerer, designer flytskjemaer og løser problemer, inkludert matematiske. Bruk alltid en notisblokk og skriv kun med blyant. Det er rent, komfortabelt og pålitelig, IMHO.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Min (allerede fylte) notatbok. Slik ser det ut. Den inneholder hverdagsoppgaver, ideer, tegninger, diagrammer, løsninger, svart bokføring, kode og så videre.

På dette stadiet klarte jeg å fullføre tre prosjekter (dette er i min forståelse av "finality", fordi ethvert produkt kan utvikles relativt uendelig).

  • Prosjekt 0: dette er en Architect Demo 3D-scene skrevet i C# med Unity-spillmotoren. For macOS og Windows-plattformer.
  • Spill 1: konsollspill Simple Snake (kjent for alle som "Snake") for Windows. skrevet i C.
  • Spill 2: konsollspill Crazy Tanks (kjent for alle som "Tanks"), allerede skrevet i C ++ (ved hjelp av klasser) og også under Windows.

Prosjekt 0 Arkitektdemo

  • plattform: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Språk: C#
  • Spillmotor: Enhet
  • Inspirasjon: Darrin Lile
  • Oppbevaringssted: GitHub

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
3D Scene Architect Demo

Det første prosjektet ble implementert ikke i C/C++, men i C# ved å bruke Unity-spillmotoren. Denne motoren var ikke så krevende på maskinvare som Unreal Engine, og virket også lettere å installere og bruke. Jeg vurderte ikke andre motorer.

Målet i Unity for meg var ikke å utvikle et slags spill. Jeg ønsket å lage en 3D-scene med en slags karakter. Han, eller rettere sagt Hun (jeg modellerte jenta jeg var forelsket i =) måtte bevege seg og samhandle med omverdenen. Det var bare viktig å forstå hva Unity er, hva utviklingsprosessen er, og hvor mye innsats det tar å skape noe. Dette er hvordan Architect Demo-prosjektet ble født (navnet ble oppfunnet nesten fra bullshit). Programmering, modellering, animasjon, teksturering tok meg sannsynligvis to måneder med daglig arbeid.

Jeg startet med opplæringsvideoer på YouTube om hvordan man lager 3D-modeller i Blender. Blender er et flott gratisverktøy for 3D-modellering (og mer) som ikke krever installasjon. Og her ventet et sjokk på meg ... Det viser seg at modellering, animasjon, teksturering er enorme separate emner du kan skrive bøker om. Dette gjelder spesielt for karakterene. For å modellere fingre, tenner, øyne og andre deler av kroppen trenger du kunnskap om anatomi. Hvordan er musklene i ansiktet ordnet? Hvordan beveger folk seg? Jeg måtte "sette inn" bein i hver arm, ben, finger, knoker!

Modeller kragebenet, ekstra benspaker, slik at animasjonen ser naturlig ut. Etter slike leksjoner innser du hvilket enormt arbeid skaperne av animasjonsfilmer gjør, bare for å lage 30 sekunder med video. Men 3D-filmer varer i timevis! Og så kommer vi ut av kinoene og sier noe sånt som: «Ta, en dritt tegneserie/film! De kunne ha gjort det bedre...” Fools!

Og en ting til om programmering i dette prosjektet. Som det viste seg, var den mest interessante delen for meg den matematiske. Hvis du kjører scenen (lenke til depotet i prosjektbeskrivelsen), vil du legge merke til at kameraet roterer rundt jentefiguren i en sfære. For å programmere en slik kamerarotasjon måtte jeg først beregne koordinatene til posisjonspunktet på sirkelen (2D), og deretter på kulen (3D). Det morsomme er at jeg hatet matte på skolen og kunne det med minus. Delvis, sannsynligvis, fordi de på skolen rett og slett ikke forklarer deg hvordan i helvete denne matematikken brukes i livet. Men når du er besatt av målet ditt, drømmen, så blir sinnet ryddet, avslørt! Og du begynner å oppfatte komplekse oppgaver som et spennende eventyr. Og så tenker du: "Vel, hvorfor kunne ikke *kjære* matematiker normalt fortelle hvor disse formlene kan lenes?".

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Beregning av formler for å beregne koordinatene til et punkt på en sirkel og på en kule (fra min notatbok)

Spill 1

  • plattform: Windows (testet på Windows 7, 10)
  • Språk: Jeg tror det ble skrevet i ren C
  • Spillmotor: Windows-konsoll
  • Inspirasjon: javidx9
  • Oppbevaringssted: GitHub

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Enkelt Snake-spill

3D-scenen er ikke et spill. I tillegg er det langt og vanskelig å modellere og animere 3D-objekter (spesielt karakterer). Etter å ha lekt med Unity, skjønte jeg at jeg måtte fortsette, eller snarere starte, fra det grunnleggende. Noe enkelt og raskt, men samtidig globalt, for å forstå selve strukturen til spill.

Og hva har vi enkelt og raskt? Det stemmer, konsoll og 2D. Mer presist, til og med konsollen og symbolene. Igjen begynte jeg å lete etter inspirasjon på Internett (generelt sett anser jeg Internett som den mest revolusjonerende og farlige oppfinnelsen i det XNUMX. århundre). Jeg gravde opp en video av en programmerer som laget konsollen Tetris. Og i likhet med spillet hans bestemte han seg for å kutte ned "slangen". Fra videoen lærte jeg om to grunnleggende ting - spillløkken (med tre grunnleggende funksjoner / deler) og utgang til bufferen.

Spillløkken kan se omtrent slik ut:

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

Koden presenterer hele main()-funksjonen på en gang. Og spillsyklusen starter etter den tilsvarende kommentaren. Det er tre grunnleggende funksjoner i løkken: Input(), Logic(), Draw(). Først, skriv inn data Input (hovedsakelig kontroll av tastetrykk), deretter behandle de innlagte data Logic, deretter vise på skjermen - Draw. Og så hver ramme. Animasjon lages på denne måten. Det er som tegneserier. Vanligvis tar behandlingen av inndataene mest tid og bestemmer, så vidt jeg vet, bildefrekvensen til spillet. Men her er Logic()-funksjonen veldig rask. Derfor må bildefrekvensen kontrolleres av Sleep()-funksjonen med gameSpeed ​​​​parameteren, som bestemmer denne hastigheten.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
spillsyklus. Slangeprogrammering i notisblokk

Hvis du utvikler et symbolsk konsollspill, vil det ikke fungere å vise data på skjermen ved å bruke den vanlige strømutgangen 'cout' - det er veldig tregt. Derfor må utgangen utføres i skjermbufferen. Så mye raskere og spillet vil fungere uten feil. For å være ærlig forstår jeg ikke helt hva en skjermbuffer er og hvordan den fungerer. Men jeg skal gi et kodeeksempel her, og kanskje noen i kommentarfeltet kan oppklare situasjonen.

Får skjermbufferen (hvis jeg kan si det):

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

Direkte utdata til skjermen til en bestemt linjescoreLine (linjen for visning av poeng):

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

I teorien er det ikke noe komplisert i dette spillet, det virker for meg som et godt eksempel på et startnivåspill. Koden er skrevet i én fil og ordnet i flere funksjoner. Ingen klasser, ingen arv. Du kan selv se alt i kildekoden til spillet ved å gå til depotet på GitHub.

Spill 2 Crazy Tanks

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Crazy Tanks spill

Å skrive ut karakterer til konsollen er sannsynligvis det enkleste du kan gjøre om til et spill. Men så dukker det opp ett problem: karakterene har forskjellige høyder og bredder (høyden er større enn bredden). Dermed vil alt se uforholdsmessig ut, og å bevege seg ned eller opp vil virke mye raskere enn å flytte til venstre eller høyre. Denne effekten er veldig merkbar i "Snake" (Spill 1). "Tanks" (Spill 2) har ikke en slik ulempe, siden utgangen der er organisert ved å male skjermpiksler med forskjellige farger. Du kan si at jeg skrev en gjengivelse. Riktignok er dette allerede litt mer komplisert, men mye mer interessant.

For dette spillet vil det være nok å beskrive systemet mitt for å vise piksler på skjermen. Jeg tror dette er hoveddelen av spillet. Og alt annet kan du finne på selv.

Så det du ser på skjermen er bare et sett med bevegelige fargede rektangler.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Rektangelsett

Hvert rektangel er representert av en matrise fylt med tall. Jeg kan forresten fremheve én interessant nyanse - alle matrisene i spillet er programmert som en endimensjonal matrise. Ikke todimensjonal, men endimensjonal! Endimensjonale arrays er mye enklere og raskere å jobbe med.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Et eksempel på en spilltankmatrise

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Representerer matrisen til en spilltank med en endimensjonal matrise

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Et mer illustrerende eksempel på en matriserepresentasjon av en endimensjonal matrise

Men tilgang til elementene i matrisen skjer i en dobbel sløyfe, som om det ikke var en endimensjonal, men en todimensjonal matrise. Dette gjøres fordi vi fortsatt jobber med matriser.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Å krysse en endimensjonal matrise i en dobbel sløyfe. Y er rad-ID, X er kolonne-ID

Vær oppmerksom på at i stedet for de vanlige matriseidentifikatorene i, j, bruker jeg identifikatorene x og y. Så, det virker for meg, mer behagelig for øyet og klarere for hjernen. I tillegg gjør en slik notasjon det mulig å enkelt projisere matrisene som brukes på koordinataksene til et todimensjonalt bilde.

Nå om piksler, farge og skjerm. StretchDIBits-funksjonen (Header: windows.h; Library: gdi32.lib) brukes for utdata. Blant annet overføres følgende til denne funksjonen: enheten som bildet vises på (i mitt tilfelle er dette Windows-konsollen), koordinatene for begynnelsen av visningen av bildet, dets bredde/høyde og bildet seg selv i form av en bitmap (bitmap), representert av en rekke byte. Bitmap som en rekke byte!

StretchDIBits()-funksjonen på jobb:

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

Minne tildeles på forhånd for denne bitmap ved hjelp av VirtualAlloc()-funksjonen. Det vil si at det nødvendige antallet byte er reservert for å lagre informasjon om alle piksler, som da vil vises på skjermen.

Opprette en 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);

Grovt sett består en bitmap av et sett med piksler. Hver fjerde byte i matrisen er en RGB-piksel. Én byte per rød verdi, én byte per grønn verdi (G) og én byte per blå farge (B). I tillegg er det én byte per innrykk. Disse tre fargene - Rød / Grønn / Blå (RGB) - blandes med hverandre i forskjellige proporsjoner - og den resulterende pikselfargen oppnås.

Nå, igjen, er hvert rektangel, eller spillobjekt, representert av en tallmatrise. Alle disse spillobjektene er plassert i en samling. Og så blir de plassert på spillefeltet, og danner en stor numerisk matrise. Jeg kartla hvert tall i matrisen til en bestemt farge. For eksempel er tallet 8 blått, tallet 9 er gult, tallet 10 er mørkegrå, og så videre. Dermed kan vi si at vi har en matrise av spillefeltet, der hvert tall er en slags farge.

Så vi har en numerisk matrise av hele spillefeltet på den ene siden og en bitmap for å vise bildet på den andre. Så langt er punktgrafikken "tom" - den har ennå ikke informasjon om pikslene i ønsket farge. Dette betyr at det siste trinnet vil være å fylle punktgrafikken med informasjon om hver piksel basert på den numeriske matrisen til spillefeltet. Et illustrerende eksempel på en slik transformasjon er på bildet nedenfor.

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Et eksempel på å fylle en bitmap (pikselmatrise) med informasjon basert på den numeriske matrisen (digital matrise) til spillefeltet (fargeindekser samsvarer ikke med indeksene i spillet)

Jeg vil også presentere et stykke ekte kode fra spillet. Variabelen colorIndex ved hver iterasjon av loopen tildeles en verdi (fargeindeks) fra den numeriske matrisen til spillefeltet (mainDigitalMatrix). Deretter skrives selve fargen til fargevariabelen basert på indeksen. Videre er den resulterende fargen delt inn i forholdet rød, grønn og blå (RGB). Og sammen med innrykk (pixelPadding) blir denne informasjonen skrevet til pikselen om og om igjen, og danner et fargebilde i punktgrafikken.

Koden bruker pekere og bitvise operasjoner, noe som kan være vanskelig å forstå. Så jeg anbefaler deg å lese separat et sted hvordan slike strukturer fungerer.

Fylle et punktgrafikk med informasjon basert på den numeriske matrisen for 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;
   }

I henhold til metoden beskrevet ovenfor, dannes ett bilde (ramme) i Crazy Tanks-spillet og vises på skjermen i Draw()-funksjonen. Etter å ha registrert tastetrykk i Input()-funksjonen og deres påfølgende behandling i Logic()-funksjonen, dannes et nytt bilde (ramme). Riktignok kan spillobjekter allerede ha en annen posisjon på spillefeltet og følgelig tegnes på et annet sted. Dette er hvordan animasjon (bevegelse) skjer.

I teorien (hvis du ikke har glemt noe), er det alt du trenger å forstå spillløkken fra det første spillet (“Snake”) og systemet for å vise piksler på skjermen fra det andre spillet (“Tanks”). av 2D-spillene dine for Windows. Lydløs! 😉 Resten av delene er bare en fantasi.

Selvfølgelig er spillet "Tanks" designet mye mer komplisert enn "Snake". Jeg brukte allerede C++-språket, det vil si at jeg beskrev forskjellige spillobjekter med klasser. Jeg har laget min egen samling - du kan se koden i headers/Box.h. Samlingen har forresten mest sannsynlig en minnelekkasje. Brukte pekere. Jobbet med hukommelse. Jeg må si at boka hjalp meg mye. Begynner C++ gjennom spillprogrammering. Dette er en flott start for nybegynnere i C++. Det er lite, interessant og godt organisert.

Det tok omtrent seks måneder å utvikle dette spillet. Jeg skrev hovedsakelig under lunsj og snacks på jobben. Han satt på kontorkjøkkenet, trampet på mat og skrev kode. Eller hjemme til middag. Så jeg fikk sånne "kjøkkenkriger". Som alltid brukte jeg en notatbok aktivt, og alle de konseptuelle tingene ble født i den.

På slutten av den praktiske delen vil jeg trekke frem noen skanninger av notatboken min. For å vise nøyaktig hva jeg skrev ned, tegnet, telte, designet...

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Tankbildedesign. Og definisjonen av hvor mange piksler hver tank skal oppta på skjermen

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Beregning av algoritme og formler for rotasjon av tanken rundt sin akse

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Diagram over samlingen min (den med minnelekkasjen, mest sannsynlig). Samlingen opprettes som en lenket liste

DevOps C++ og "kitchen wars", eller hvordan jeg begynte å skrive spill mens jeg spiste
Og dette er fåfengte forsøk på å skru kunstig intelligens inn i spillet

Теория

"Selv en reise på tusen miles begynner med det første skrittet" (gammel kinesisk visdom)

La oss gå fra praksis til teori! Hvordan finner du tid til hobbyen din?

  1. Bestem hva du virkelig vil ha (dessverre, dette er det vanskeligste).
  2. Sett prioriteringer.
  3. Ofre alt "overflødig" av hensyn til høyere prioriteringer.
  4. Beveg deg mot målene dine hver dag.
  5. Ikke forvent at det blir to eller tre timer fritid til en hobby.

På den ene siden må du bestemme hva du vil og prioritere. På den annen side er det mulig å forlate enkelte saker/prosjekter til fordel for disse prioriteringene. Du vil med andre ord måtte ofre alt "overflødig". Jeg hørte et sted at i livet bør det være maks tre hovedaktiviteter. Da vil du kunne håndtere dem på best mulig måte. Og ytterligere prosjekter/veibeskrivelser vil begynne å overbelaste corny. Men alt dette er sannsynligvis subjektivt og individuelt.

Det er en viss gylden regel: aldri ha en 0%-dag! Jeg lærte om det i en artikkel av en indie-utvikler. Hvis du jobber med et prosjekt, så gjør noe med det hver dag. Og det spiller ingen rolle hvor mye du tjener. Skriv ett ord eller en kodelinje, se en opplæringsvideo eller slå en spiker inn i brettet – bare gjør noe. Det vanskeligste er å komme i gang. Når du først har startet, vil du sannsynligvis gjøre litt mer enn du ville. Så du vil hele tiden bevege deg mot målet ditt og, tro meg, veldig raskt. Tross alt er hovedbremsen på alle ting utsettelse.

Og det er viktig å huske at du ikke bør undervurdere og ignorere den gratis "sagflisen" av tid på 5, 10, 15 minutter, vent på noen store "logger" som varer en time eller to. Står du i kø? Tenk på noe for prosjektet ditt. Går du opp rulletrappen? Skriv ned noe i en notatbok. Spiser du på bussen? Ok, les en artikkel. Bruk enhver mulighet. Slutt å se på katter og hunder på YouTube! Ikke rot med hjernen din!

Og den siste. Hvis du etter å ha lest denne artikkelen likte ideen om å lage spill uten å bruke spillmotorer, så husk navnet Casey Muratori. Denne fyren har сайт. I delen "se -> TIDLIGERE EPISODER" finner du fantastiske gratis videoopplæringer om hvordan du lager et profesjonelt spill fra bunnen av. Du lærer kanskje mer i fem Intro to C for Windows-timer enn på fem års studier ved universitetet (noen skrev om dette i kommentarfeltet under videoen).

Casey forklarer også at ved å utvikle din egen spillmotor, vil du få en bedre forståelse av eksisterende motorer. I en verden av rammeverk, der alle prøver å automatisere, vil du lære å lage, ikke bruke. Forstå selve naturen til datamaskiner. Og du vil også bli en mye mer intelligent og moden programmerer - en proff.

Lykke til på din valgte vei! Og la oss gjøre verden mer profesjonell.

Forfatter: Grankin Andrey, DevOps



Kilde: www.habr.com