DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt

"Jag vet att jag inte vet något" Sokrates

För vem: för IT-folk som inte bryr sig om alla utvecklare och vill spela sina spel!

Om vad: om hur du börjar skriva spel i C/C++, om du plötsligt behöver det!

Varför ska du läsa detta: Applikationsutveckling är inte mitt expertområde, men jag försöker koda varje vecka. För jag älskar spel!

Hej mitt namn är Andrey Grankin, Jag är DevOps på Luxoft. Applikationsutveckling är inte min specialitet, men jag försöker koda varje vecka. För jag älskar spel!

Datorspelsindustrin är enorm, det ryktas vara ännu större än filmindustrin idag. Spel har skrivits sedan datorernas gryning, med hjälp av, med moderna standarder, komplexa och grundläggande utvecklingsmetoder. Med tiden började spelmotorer med redan programmerad grafik, fysik och ljud dyka upp. De låter dig fokusera på att utveckla själva spelet och inte oroa dig för dess grund. Men tillsammans med dem, med motorer, "blir utvecklare" och försämras. Själva produktionen av spel läggs på löpande band. Och mängden produkter börjar råda över dess kvalitet.

Samtidigt, när vi spelar andras spel, är vi ständigt begränsade av platserna, handlingen, karaktärerna och spelmekaniken som andra människor kom på. Så jag insåg att...

... det är dags att skapa mina egna världar, endast underställda mig. Världar där jag är Fadern, Sonen och den Helige Ande!

Och jag tror uppriktigt att genom att skriva din egen spelmotor och spela på den kommer du att kunna ta av dig skorna, torka av fönstren och uppgradera din stuga, och bli en mer erfaren och komplett programmerare.

I den här artikeln ska jag försöka berätta hur jag började skriva små spel i C/C++, hur utvecklingsprocessen är och var jag hittar tid för en hobby i en hektisk miljö. Den är subjektiv och beskriver processen för en individuell start. Material om okunnighet och tro, om min personliga bild av världen för tillfället. Med andra ord, "Administrationen är inte ansvarig för dina personliga hjärnor!"

Praxis

"Kunskap utan övning är värdelös, övning utan kunskap är farligt" Konfucius

Min anteckningsbok är mitt liv!


Så i praktiken kan jag säga att för mig börjar allt med ett anteckningsblock. Jag skriver inte bara ner mina dagliga uppgifter där, jag ritar, programmerar, designar flödesscheman och löser problem, inklusive matematiska. Använd alltid ett anteckningsblock och skriv bara med blyerts. Det är rent, bekvämt och pålitligt, IMHO.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Min (redan fylld med) anteckningsbok. Så här ser det ut. Den innehåller vardagliga uppgifter, idéer, ritningar, diagram, lösningar, svart bokföring, kod och så vidare

I det här skedet lyckades jag slutföra tre projekt (detta är enligt min uppfattning om "fullständighet", eftersom vilken produkt som helst kan utvecklas relativt oändligt).

  • Projekt 0: Detta är en 3D Architect Demo-scen skriven i C# med hjälp av Unity-spelmotorn. För macOS och Windows-plattformar.
  • Spel 1: konsolspel Simple Snake (känd för alla som "Snake") för Windows. Skrivet i C.
  • Spel 2: konsolspel Crazy Tanks (känd för alla som "Tanks"), skrivet i C++ (med klasser) och även för Windows.

Projekt 0 Arkitektdemo

  • plattform: Windows (Windows 7, 10), Mac OS (OS X El Capitan v. 10.11.6)
  • Språk: C#
  • Spelmotor: Unity
  • Inspiration: Darrin Lile
  • Förvar: GitHub

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
3D-scenarkitektdemo

Det första projektet implementerades inte i C/C++, utan i C# med hjälp av Unity-spelmotorn. Denna motor var inte lika krävande på hårdvara som Orealistisk motor, och verkade också lättare att installera och använda. Jag tänkte inte på andra motorer.

Mitt mål i Unity var inte att utveckla ett spel. Jag ville skapa en 3D-scen med någon karaktär. Han, eller snarare Hon (jag modellerade tjejen jag var kär i =) var tvungen att röra på sig och interagera med världen omkring honom. Det var bara viktigt att förstå vad Unity är, vad utvecklingsprocessen är och hur mycket ansträngning som krävs för att skapa något. Så här föddes projektet Architect Demo (namnet uppfanns nästan från ingenstans). Programmering, modellering, animering, texturering tog mig förmodligen två månaders dagligt arbete.

Jag började med instruktionsvideor på YouTube om att skapa 3D-modeller i Blandare. Blender är ett utmärkt gratisverktyg för 3D-modellering (och mer) som inte kräver installation. Och här väntade mig en chock... Det visar sig att modellering, animation, texturering är enorma separata ämnen som du kan skriva böcker om. Detta gäller särskilt för karaktärer. För att modellera fingrar, tänder, ögon och andra kroppsdelar behöver du kunskaper i anatomi. Hur är ansiktsmusklerna uppbyggda? Hur rör sig människor? Jag var tvungen att "sätta in" ben i varje arm, ben, finger, falanger på fingrarna!

Modellera nyckelbenen och ytterligare spakben för att få animationen att se naturlig ut. Efter sådana lektioner inser du hur mycket arbete skaparna av animerade filmer gör bara för att skapa 30 sekunders video. Men 3D-filmer håller i timmar! Och så lämnar vi biograferna och säger något i stil med: "Det är en skit tecknad film/film! De kunde ha gjort det bättre..." Fools!

Och en sak till angående programmering i detta projekt. Som det visade sig var den mest intressanta delen för mig den matematiska. Om du kör scenen (länk till förvaret i projektbeskrivningen) kommer du att märka att kameran roterar runt tjejkaraktären i en sfär. För att programmera en sådan rotation av kameran var jag tvungen att först beräkna koordinaterna för positionspunkten på cirkeln (2D) och sedan på sfären (3D). Det roliga är att jag hatade matematik i skolan och kunde det med C-minus. Delvis, förmodligen, för i skolan förklarar de helt enkelt inte för dig hur fan denna matematik tillämpas i livet. Men när du är besatt av ditt mål, din dröm, klarnar ditt sinne och öppnar sig! Och du börjar uppfatta svåra uppgifter som ett spännande äventyr. Och då tänker du: "Tja, varför kunde inte din *favorit* matematiker berätta för dig normalt var dessa formler kan tillämpas?"

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Beräkning av formler för att beräkna koordinaterna för en punkt på en cirkel och på en sfär (från min anteckningsbok)

Spel 1. Simple Snake

  • plattform: Windows (testat på Windows 7, 10)
  • Språk: Jag tror att det är skrivet i ren C
  • Spelmotor: Windows-konsol
  • Inspiration: javidx9
  • Förvar: GitHub

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Enkelt Snake-spel

En 3D-scen är inget spel. Dessutom är det tidskrävande och svårt att modellera och animera 3D-objekt (särskilt karaktärer). Efter att ha lekt med Unity kom insikten till mig att jag behövde fortsätta, eller snarare börja, från grunderna. Något enkelt och snabbt, men samtidigt globalt, för att förstå själva strukturen i spel.

Vad är enkelt och snabbt? Just det, konsol och 2D. Mer exakt, även konsolen och symbolerna. Återigen letade jag efter inspiration på Internet (i allmänhet tror jag att Internet är XNUMX-talets mest revolutionerande och farligaste uppfinning). Jag grävde upp en video av en programmerare som gjorde konsolen Tetris. Och i likhet med hans spel bestämde jag mig för att göra en "orm". Från videon lärde jag mig om två grundläggande saker - spelslingan (med tre grundläggande funktioner/delar) och utdata till bufferten.

Spelslingan kan se ut ungefär så här:

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

Koden presenterar hela main()-funktionen på en gång. Och spelcykeln börjar efter lämplig kommentar. Det finns tre grundläggande funktioner i slingan: Input(), Logic(), Draw(). Först, mata in data Inmatning (främst kontroll av tangenttryckningar), sedan bearbeta de inmatade data Logic, sedan ut till skärmen - Rita. Och så på varje bildruta. Så här skapas animation. Det är som i tecknade serier. Bearbetningen av den inmatade informationen tar vanligtvis mest tid och, så vitt jag vet, avgör spelets bildhastighet. Men här körs Logic()-funktionen väldigt snabbt. Därför måste du styra bildhastigheten med Sleep()-funktionen med gameSpeed ​​​​parametern, som bestämmer denna hastighet.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Spelcykel. Programmera en "orm" i ett anteckningsblock

Om du utvecklar ett karaktärsbaserat konsolspel kommer du inte att kunna mata ut data till skärmen med den vanliga "cout"-strömmen - det är väldigt långsamt. Därför måste utdata skickas till skärmbufferten. Detta är mycket snabbare och spelet kommer att fungera utan problem. För att vara ärlig så förstår jag inte riktigt vad en skärmbuffert är och hur den fungerar. Men jag ska ge en exempelkod här, och kanske kan någon förtydliga situationen i kommentarerna.

Skaffa en skärmbuffert (så att säga):

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

Direkt visning av en viss strängscoreLine (poängvisningsrad):

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

I teorin är det inget komplicerat i det här spelet, jag tycker att det är ett bra exempel på instegsnivå. Koden är skriven i en fil och formaterad i flera funktioner. Inga klasser, inget arv. Du kan se allt själv i spelets källkod genom att gå till förvaret på GitHub.

Spel 2. Crazy Tanks

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Spel Crazy Tanks

Att skriva ut karaktärer till konsolen är förmodligen det enklaste du kan förvandla till ett spel. Men sedan dyker ett problem upp: symbolerna har olika höjd och bredd (höjden är större än bredden). På så sätt kommer allt att se ur proportion och att flytta ner eller upp kommer att se mycket snabbare ut än att flytta åt vänster eller höger. Denna effekt är mycket märkbar i Snake (spel 1). "Tanks" (Spel 2) har inte denna nackdel, eftersom utgången där är organiserad genom att måla skärmpixlarna med olika färger. Man kan säga att jag skrev en renderare. Det är sant att det här är lite mer komplicerat, även om det är mycket mer intressant.

För det här spelet kommer det att räcka med att beskriva mitt system för att visa pixlar på skärmen. Jag anser att detta är huvuddelen av spelet. Och du kan hitta på allt annat själv.

Så det du ser på skärmen är bara en uppsättning rörliga flerfärgade rektanglar.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Uppsättning av rektanglar

Varje rektangel representeras av en matris fylld med siffror. Förresten kan jag lyfta fram en intressant nyans - alla matriser i spelet är programmerade som en endimensionell array. Inte tvådimensionell, utan endimensionell! Endimensionella arrayer är mycket enklare och snabbare att arbeta med.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Exempel på en speltankmatris

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Representation av speltankmatrisen som en endimensionell array

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Ett mer visuellt exempel på att representera en matris som en endimensionell matris

Men åtkomst till elementen i arrayen sker i en dubbel loop, som om det inte vore en endimensionell array, utan en tvådimensionell. Detta görs för att vi fortfarande arbetar med matriser.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Att korsa en endimensionell array i en dubbel loop. Y - radidentifierare, X - kolumnidentifierare

Observera: istället för de vanliga matrisidentifierarna i, j använder jag identifierarna x och y. På det här sättet, förefaller det mig, är det mer tilltalande för ögat och mer förståeligt för hjärnan. Dessutom gör en sådan notation det möjligt att bekvämt projicera de använda matriserna på koordinataxlarna i en tvådimensionell bild.

Nu om pixlar, färg och skärmutdata. StretchDIBits-funktionen används för utdata (Header: windows.h; Library: gdi32.lib). Den här funktionen får bland annat följande: enheten som bilden visas på (i mitt fall är det Windows-konsolen), startkoordinaterna för bildvisningen, dess bredd/höjd och själva bilden i form av en bitmapp, representerad av en array av byte. Bitmapp som en byte-array!

StretchDIBits() funktion i aktion:

// 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 tilldelas i förväg för denna bitmapp med hjälp av VirtualAlloc()-funktionen. Det vill säga att det erforderliga antalet byte reserveras för att lagra information om alla pixlar, som sedan kommer att visas på skärmen.

Skapa m_p_bitmapMemory bitmapp:

// 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 bitmapp av en uppsättning pixlar. Var fjärde byte i arrayen är en RGB-pixel. En byte per röd färgvärde, en byte per grön färgvärde (G) och en byte per blå färgvärde (B). Plus att det finns en byte kvar för indrag. Dessa tre färger - Röd/Grön/Blå (RGB) - blandas med varandra i olika proportioner för att skapa den resulterande pixelfärgen.

Nu, återigen, varje rektangel, eller spelobjekt, representeras av en numerisk matris. Alla dessa spelobjekt placeras i en samling. Och sedan placeras de på spelplanen och bildar en stor numerisk matris. Jag associerade varje nummer i matrisen med en specifik färg. Till exempel motsvarar siffran 8 blått, siffran 9 gul, siffran 10 mörkgrå och så vidare. Således kan vi säga att vi har en matris av spelplanen, där varje nummer är en färg.

Så vi har en numerisk matris för hela spelplanen på ena sidan och en bitmapp för att visa bilden på den andra. Hittills är bitmappen "tom" - den innehåller ännu inte information om pixlarna i den önskade färgen. Detta innebär att det sista steget blir att fylla i bitmappen med information om varje pixel baserat på spelfältets numeriska matris. Ett tydligt exempel på en sådan transformation finns i bilden nedan.

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Ett exempel på att fylla en bitmapp (pixelmatris) med information baserad på spelfältets digitala matris (färgindex matchar inte indexen i spelet)

Jag kommer också att presentera en bit riktig kod från spelet. Variabeln colorIndex vid varje iteration av loopen tilldelas ett värde (färgindex) från spelfältets numeriska matris (mainDigitalMatrix). Färgvariabeln ställs sedan in på själva färgen baserat på indexet. Den resulterande färgen delas sedan upp i förhållandet rött, grönt och blått (RGB). Och tillsammans med pixelPadding skrivs denna information in i pixeln om och om igen och bildar en färgbild i bitmappen.

Koden använder pekare och bitvisa operationer, vilket kan vara svårt att förstå. Så jag råder dig att läsa någonstans separat hur sådana strukturer fungerar.

Fylla bitmappen med information baserad på spelfältets numeriska matris:

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

Enligt metoden som beskrivs ovan, i spelet Crazy Tanks bildas en bild (ram) och visas på skärmen i Draw()-funktionen. Efter att ha registrerat tangenttryckningar i Input()-funktionen och deras efterföljande bearbetning i Logic()-funktionen, bildas en ny bild (ram). Det är sant att spelobjekt redan kan ha en annan position på spelplanen och följaktligen ritas på en annan plats. Så här uppstår animation (rörelse).

I teorin (om jag inte har glömt något), är det allt du behöver att förstå spelslingan från det första spelet ("Snake") och systemet för att visa pixlar på skärmen från det andra spelet ("Tanks"). av dina 2D-spel under Windows. Ljudlös! 😉 Resten av delarna är bara en fantasi.

Naturligtvis är spelet "Tanks" mycket mer komplext än "Snake". Jag använde redan C++-språket, det vill säga jag beskrev olika spelobjekt med klasser. Jag skapade min egen samling - koden kan ses i headers/Box.h. Förresten har samlingen med största sannolikhet en minnesläcka. Använda pekare. Jobbade med minne. Jag måste säga att boken hjälpte mig mycket Börjar C++ genom spelprogrammering. Detta är en bra start för nybörjare i C++. Det är litet, intressant och välorganiserat.

Det tog ungefär sex månader att utveckla detta spel. Jag skrev främst under lunch och mellanmål på jobbet. Han satt i kontorsköket, trampade mat och skrev kod. Eller på middag hemma. Så jag slutade med dessa "kökskrig". Som alltid använde jag aktivt en anteckningsbok, och alla konceptuella saker föddes i den.

För att slutföra den praktiska delen ska jag ta några skanningar av min anteckningsbok. För att visa exakt vad jag skrev ner, ritade, räknade, designade...

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Designa bilder av tankar. Och bestämma hur många pixlar varje tank ska uppta på skärmen

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Beräkning av algoritmen och formlerna för tankens rotation runt sin axel

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Schema för min samling (den där det finns en minnesläcka, troligen). Samlingen skapas enligt typen Länkad lista

DevOps C++ och "kitchen wars", eller hur jag började skriva spel medan jag åt
Och det här är meningslösa försök att koppla artificiell intelligens till spelet

Теория

"Även en resa på tusen miles börjar med det första steget" (Forntida kinesisk visdom)

Låt oss gå från praktik till teori! Hur får man tid för sin hobby?

  1. Bestäm vad du verkligen vill (det här är tyvärr den svåraste delen).
  2. Sätt prioriteringar.
  3. Offra allt "extra" för högre prioritets skull.
  4. Gå mot mål varje dag.
  5. Förvänta dig inte två eller tre timmars fritid att spendera på en hobby.

Å ena sidan måste du bestämma vad du vill och prioritera. Å andra sidan är det möjligt att överge vissa aktiviteter/projekt till förmån för dessa prioriteringar. Med andra ord måste du offra allt "extra". Jag hörde någonstans att det borde finnas max tre huvudaktiviteter i livet. Då kommer du att kunna göra dem med högsta kvalitet. Och ytterligare projekt/riktningar kommer helt enkelt att börja överbelastas. Men allt detta är förmodligen subjektivt och individuellt.

Det finns en viss gyllene regel: ha aldrig en 0%-dag! Jag lärde mig om det i en artikel av en indieutvecklare. Om du arbetar med ett projekt, gör något åt ​​det varje dag. Och det spelar ingen roll hur mycket du gör. Skriv ett ord eller en rad kod, titta på en instruktionsvideo eller slå en spik i en bräda - bara gör något. Det svåraste är att börja. När du väl börjar kommer du förmodligen att göra lite mer än du ville. På så sätt kommer du ständigt att röra dig mot ditt mål och, tro mig, mycket snabbt. När allt kommer omkring är det främsta hindret för allt förhalande.

Och det är viktigt att komma ihåg att du inte ska underskatta och ignorera det fria "sågspånet" av tid på 5, 10, 15 minuter, vänta på några stora "stockar" som varar en timme eller två. Står du i kö? Fundera på något för ditt projekt. Ta rulltrappan? Skriv ner något i ett anteckningsblock. Reser du med bussen? Bra, läs en artikel. Utnyttja alla möjligheter. Sluta titta på katter och hundar på YouTube! Förorena inte din hjärna!

Och en sista sak. Om du, efter att ha läst den här artikeln, gillade idén att skapa spel utan att använda spelmotorer, kom ihåg namnet Casey Muratori. Den här killen har сайт. I avsnittet "Titta -> FÖREGÅENDE EPISODER" hittar du underbara gratis videohandledningar om att skapa ett professionellt spel från grunden. På fem Intro till C för Windows-lektioner kommer du förmodligen att lära dig mer än på fem års universitetsstudier (någon skrev om detta i kommentarerna under videon).

Casey förklarar också att genom att utveckla din egen spelmotor kommer du att få en bättre förståelse för alla befintliga motorer. I en värld av ramverk där alla försöker automatisera lär du dig att skapa snarare än att använda. Du förstår själva naturen hos datorer. Och du kommer också att bli en mycket mer intelligent och mogen programmerare - ett proffs.

Lycka till på din valda väg! Och låt oss göra världen mer professionell.

Författare: Grankin Andrey, DevOps



Källa: will.com