Maskinorienterad Programmering / Programmering av Inbyggda System

Hemuppgifter C-programmering

C - Föreläsning 3.

Nu skall vi öva på att strukturera vår programkod genom att använda structs och även mjukt börja att använda fler separata källfiler. Mer om separata källfiler (.h och .c-filer) följer nästa vecka. Varje .c-fil kompileras enskilt och länkas därefter ihop med varandra till en exekverbar fil. När ni bygger projektet i CodeLite kompileras automatiskt alla .c-filer och länkas ihop till den körbara filen.
      Vi skall också öva oss på arrayer av structs, typedef, funktionspekare samt structs med funktionspekare. Structs med funktionspekare används för att programmera med mer objektorienterad stil, dvs för att åstadkomma en motsvarighet till klasser med metoder, vilket finns i språk som Java, C++, c# mfl.


Uppg. 1
Structs och även flera källfiler
Vi fortsätter med förra veckans projekt och där vi slutade. (Om du behöver kan du hämta projektfilerna längst ned från förra veckans hemuppgifter)
      Redan i förra veckans uppgifter var det egentligen mycket lämpligt att kapsla in våra objekts variabler (position, angle, scale, speed...) med hjälp av structs och skapa en instans för skeppet respektive bakgrunden. Förmodligen grupperade du visserligen variablerna för skeppet för sig och variablerna för bakgrunden för sig, men har man många objekt blir koden lätt grötig utan ytterligare struktur.
      Structs möjliggör sådan struktur. Har man flera objekt blir det automatiskt tydligt vilken position och hastighet etc som tillhör vilket objekt. (Det blir senare också enkelt att skapa en array av dessa structs för att hantera flera objekt.)

Övningar på att använda structs samt att skapa ny .h-fil:

  1. Börja med att skapa en struct för 2D-vektorer som vi kan använda för positioner men även skulle kunna använda för riktnings- och hastighetsvektorer. Det är lämpligt att de två structmedlemmarna x,y är floats. Dels så att positioner och hastigheter inte är begränsade till heltal och dels för att inte avrundningsfel ska ackumuleras, t ex om du senare vill flytta dina objekt efter någon mjuk kurva som bygger på någon flyttalsformel. Kalla därför din struct för vec2f , dvs vektor av två floats, vilket är ett vanligt vedertaget namn för en sådan här struct. Det är vanligt att även behöva ha motsvarande för ints och doubles (dvs vec2i resp. vec2d). Alternativa vanligt vedertagna namn är float2, int2 osv.

      •   Lägg din struct vec2f i en ny källfil som du kallar för vecmath.h.
      •   Använd även så kallade include guards (se nedan).


    Se därefter till att du anger typen Header file (.h). Ange namnet vecmath.h.

    Include guards: Vi kommer till include guards först i nästa lektion, men du kan lika gärna lägga till det här och nu... En .h-fil är typiskt omgärdade av följande rader:

    #ifndef VECMATH_H // eller vad din fil heter. Versaler på #define VECMATH_H // filnamnet + "_H" är vanligt. ... resten av innehållet i .h-filen #endif // VECMATH_H -- här är VECMATH_H endast kommentar // för tydlighets skull

    Detta gör att .h-filen inte kommer att inkluderas mer än en gång när en .c-fil kompileras. Om den skulle inkluderas fler än en gång kommer innehållet att finnas med motsvarande antal gånger, vilket kan leda till felet "multiple definitions", t ex av structs. En .h-fil kan lätt råka inkluderas flera gånger om man inkluderar flera .h-filer som i sin tur råkar inkludera samma .h-fil. Detta är inte ovanligt, vilket du förmodligen snart kommer att märka.
          Konstruktionen fungerar enligt följande: #ifndef, #define och #endif är så kallade preprocessor directives: #ifndef betyder "if not defined". Dvs, allt som står mellan #ifndef VECMATH_H och #endif behandlas vid kompileringen (av .c-filen) endast om VECMATH_H inte är definierat. Första gången som preprocessorn ser #include "vecmath.h" och försöker inkludera vecmath.h så är VECMATH_H (naturligtvis) inte definierat och alltså inkluderas innehållet. Första efterföljande rad är just en #define VECMATH_H som alltså ser till att VECMATH_H blir definierat. Om preprocessorn finner på en till #include "vecmath.h" lite senare kommer VECMATH_H alltså redan att vara definierat och innehållet i .h-filen tas inte med någon mer gång. Detta är helt enkelt ett vanligt trick för att lösa ett vanligt problem. Vissa kompilatorer tillåter enklare sätt som t ex "#pragma once" för att lösa samma problem.

    I filen vecmath.h:
    #ifndef VECMATH_H #define VECMATH_H typedef struct { float x, y; } vec2f; #endif // VECMATH_H
  2. Byt ut skeppets x- och y-variabler mot en vec2f pos istället. Dvs:
    //int x = 400, y = 300, speed = 3; <- Byt detta vec2f pos = {400, 300}; // mot detta int speed = 3; // och detta

    Detta medför att du måste modifiera din kod överallt där x och y används. Gör detta. Ett tips är att kompilatorn kommer att klaga där du behöver modifiera. Är du ovan vid structs är detta en mycket nyttig övning.

    Tips: Bl a behöver även inparametrarna för shake() ändras från (int* x, int* y) till (vec2f* pos), och eftersom pos här är pekare behöver du inuti funktionen använda pilnotation:
    void shake(vec2f* pos) { ... pos->x += ... // istället för *x += ... pos->y += ... // istället för *y += ... ...
I main.c: ... #include "vecmath.h" ... int t = 0; bool bShake = false; int shakeStop = 0; void shake(vec2f* pos) { // om bShake == false, initiera shake med sannolikhet 1/60. // Sätt bShake = true; // Sätt shakeStop till t + (30 till 50) frames // om (bShake && t < shakeStop) // addera omväxlande -1 resp 1 varannan frame till x,y // om (bShake && t >= shakeStop) // sätt bShake = false; if( bShake == false && ((rand() % 60)==1) ) { bShake = true; shakeStop = t + (rand() % 20) + 30; } if( bShake && t < shakeStop) { pos->x += 2* ((t % 3) - 1); pos->y += ((rand() % 3) - 1); } if( bShake && (t >= shakeStop) ) { bShake = false; } t++; } int main( int argc, char* args[] ) { ... //int x = 400, y = 300, vec2f pos = {400, 300}; int speed = 3; ... while(true) // The real-time loop { ... // Update our object(s) shake(&pos); angle = fmod(angle+0.02, 360); scale += 1.0/2500.0; if (state[SDL_SCANCODE_RIGHT]) pos.x = (pos.x+speed >= 799) ? 799 : pos.x+speed; if (state[SDL_SCANCODE_LEFT]) pos.x = (pos.x-speed <= 0) ? 0 : pos.x-speed; if (state[SDL_SCANCODE_DOWN]) pos.y = (pos.y+speed >= 599) ? 599 : pos.y+speed; if (state[SDL_SCANCODE_UP]) pos.y = (pos.y-speed <= 0) ? 0 : pos.y-speed; ... // Render our object(s) - background objects first, and then forward objects (like a painter) renderGfxObject(&background, 400, 300, angle, scale); renderGfxObject(&ship, pos.x, pos.y, shipAngle, 1.0f); ...
Uppg. 1
  1. Skapa en struct för alla våra spelobjekt:

    Nu ska vi skapa en gemensam struct för alla våra existerande (dvs skeppet + background) och kommande spelobjekt. Med denna struct kapslar vi in skeppets och bakgrundens variabler med hjälp av en och samma lämpliga struct (som alltså kan användas av båda) och som vi lägger i en ny separat .h-fil. Dvs structen innehåller ett superset av de variabler som skeppet och bakgrunden använder. Vi kallar vår struct för GameObject och lägger implementationen i filen gameobject.h, enligt nedan.

    Här följer en stegvis beskrivning av vad du ska göra:
    • Skapa struct GameObject i filen gameobject.h enligt ovan.
    • I main.c: byt ut GfxObject ship, background; mot GameObject ship, background;. Dvs:
      ... #include "gameobject.h" GameObject ship, background; ...
    • Anpassa nu din kod så att den fungerar som förut, men alltså använder vår inkapsling av objektens variabler i struct GameObject. Det innebär att du behöver initiera ship och background i main()-funktionen samt modifiera din kod att använda struct-medlemmarna istället för de tidigare olika separata variablerna. Tips är att om du bara gör modifieringen ovan och kompilerar kommer kompilatorn klaga där du behöver ändra.

    När du gör din modifiering så inför inga globala variabler i .h-filen. Det behövs inte och först nästa vecka tar vi upp hur man representerar globala variabler i en .h-fil på ett korrekt sätt (för den nyfikne görs det med extern).

    Gör alltihop så långt som möjligt själv utan att titta i lösningen för det är en mycket nyttig övning om du är ovan vid C och structs. Räkna med att det tar tid. När du fått koden att kompilera, använd debuggern för att rätta eventuella återstående fel. Använd CodeLite's Help-menu och skriv t ex debug eller breakpoint för att få hjälp. Kompilera och testa att allt fungerar som förut.

... #include "vecmath.h" #include "gameobject.h" GameObject ship, background; ... int main( int argc, char* args[] ) { ... initRenderer(800, 600); // Create an object ship.gfxObj = createGfxObject( "../ship.png" ); ship.gfxObj.outputWidth = 200; ship.gfxObj.outputHeight = 200; ship.pos.x = 400.0f; ship.pos.y = 300.0f; ship.speed = 3; ship.angle = 0; // unused ship.angleSpeed = 90.0/360.0; // unused ship.scale = 1.0f; ship.scaleSpeed = 0; // unused // Create the background object background.gfxObj = createGfxObject( "../background.jpg" ); background.gfxObj.outputWidth = 800; background.gfxObj.outputHeight = 600; background.pos.x = 400.0f; background.pos.y = 300.0f; background.speed = 0; // unused background.angle = 0; //(Note: 1/100 are integer numbers and division becomes equal to 0) background.scale = 1.8f; background.scaleSpeed = 1.0/100.0; ... while(true) // The real-time loop { ... // Update our object(s) shake(&ship.pos); background.angle = fmod(background.angle+0.02, 360); background.scale += 1.0/2500.0; if (state[SDL_SCANCODE_RIGHT]) ship.pos.x = (ship.pos.x+ship.speed >= 799) ? 799 : ship.pos.x+ship.speed; if (state[SDL_SCANCODE_LEFT]) ship.pos.x = (ship.pos.x-ship.speed <= 0) ? 0 : ship.pos.x-ship.speed; if (state[SDL_SCANCODE_DOWN]) ship.pos.y = (ship.pos.y+ship.speed >= 599) ? 599 : ship.pos.y+ship.speed; if (state[SDL_SCANCODE_UP]) ship.pos.y = (ship.pos.y-ship.speed <= 0) ? 0 : ship.pos.y-ship.speed; if (state[SDL_SCANCODE_W]) background.scale = background.scale + background.scaleSpeed; if (state[SDL_SCANCODE_S]) background.scale = (background.scale - background.scaleSpeed <= 0) ? 0 : background.scale - background.scaleSpeed; if (state[SDL_SCANCODE_A]) ship.angle = fmod(ship.angle - ship.angleSpeed, 360.0); if (state[SDL_SCANCODE_D]) ship.angle = fmod(ship.angle + ship.angleSpeed, 360.0); // Render our object(s)... renderGfxObject(&background.gfxObj, background.pos.x, background.pos.y, background.angle, background.scale); renderGfxObject(&ship.gfxObj, ship.pos.x, ship.pos.y, ship.angle, ship.scale); ... } ... } ... void close() { ... freeGfxObject(&ship.gfxObj); freeGfxObject(&background.gfxObj); closeRenderer(); //Free resources and close SDL }
#include "renderer.h" #include <stdio.h> #include <math.h> #include <stdlib.h> // för rand() #include "vecmath.h" #include "gameobject.h" GameObject ship, background; void close(); void vandStrang(); int t = 0; bool bShake = false; int shakeStop = 0; void shake(vec2f* pos) { // om bShake == false, initiera shake med sannolikhet 1/60. // Sätt bShake = true; // Sätt shakeStop till t + (30 till 50) frames // om (bShake && t < shakeStop) // addera omväxlande -1 resp 1 varannan frame till x,y // om (bShake && t >= shakeStop) // sätt bShake = false; if( bShake == false && ((rand() % 60)==1) ) { bShake = true; shakeStop = t + (rand() % 20) + 30; } if( bShake && t < shakeStop) { pos->x += 2*((t % 3) - 1); pos->y += ((rand() % 3) - 1); } if( bShake && (t >= shakeStop) ) { bShake = false; } t++; } int main( int argc, char* args[] ) { // If you want the program to not launch the terminal, then go to // Project->Settings->General->"This program is a GUI application" and uncheck // that flag. // Start up SDL and create window of width=800, height = 600 initRenderer(800, 600); // Create an object ship.gfxObj = createGfxObject( "../ship.png" ); ship.gfxObj.outputWidth = 200; ship.gfxObj.outputHeight = 200; ship.pos.x = 400.0f; ship.pos.y = 300.0f; ship.speed = 3; //(Note: 90/360, without .0, are integer numbers and division then becomes // equal to 0) ship.angle = 0; // unused ship.angleSpeed = 90.0/360.0; // unused ship.scale = 1.0f; ship.scaleSpeed = 0; // unused // Create the background object background.gfxObj = createGfxObject( "../background.jpg" ); background.gfxObj.outputWidth = 800; background.gfxObj.outputHeight = 600; background.pos.x = 400.0f; background.pos.y = 300.0f; background.speed = 0; // unused background.angle = 0; //(Note: 1/100 are integer numbers and division becomes equal to 0) background.scale = 1.8f; background.scaleSpeed = 1.0/100.0; const Uint8 *state = SDL_GetKeyboardState(NULL); // get pointer to key states char strang[] = "Hello World!"; int loopIter = 0; while(true) // The real-time loop { // Handle events on the inqueue (e.g., mouse events) SDL_Event e; //Event handler while( SDL_PollEvent( &e ) != 0 ) { if( e.type == SDL_QUIT ) { //User requests quit close(); exit(0); } } // Clear screen with a grey background color. SDL_SetRenderDrawColor( gRenderer, 0x33, 0x33, 0x33, 0xFF ); SDL_RenderClear( gRenderer ); // Update our object(s) shake(&ship.pos); background.angle = fmod(background.angle+0.02, 360); background.scale += 1.0/2500.0; if (state[SDL_SCANCODE_RIGHT]) ship.pos.x = (ship.pos.x+ship.speed >= 799) ? 799 : ship.pos.x+ship.speed; if (state[SDL_SCANCODE_LEFT]) ship.pos.x = (ship.pos.x-ship.speed <= 0) ? 0 : ship.pos.x-ship.speed; if (state[SDL_SCANCODE_DOWN]) ship.pos.y = (ship.pos.y+ship.speed >= 599) ? 599 : ship.pos.y+ship.speed; if (state[SDL_SCANCODE_UP]) ship.pos.y = (ship.pos.y-ship.speed <= 0) ? 0 : ship.pos.y-ship.speed; if (state[SDL_SCANCODE_W]) background.scale = background.scale + background.scaleSpeed; if (state[SDL_SCANCODE_S]) background.scale = (background.scale - background.scaleSpeed <= 0) ? 0 : background.scale - background.scaleSpeed; if (state[SDL_SCANCODE_A]) ship.angle = fmod(ship.angle - ship.angleSpeed, 360.0); if (state[SDL_SCANCODE_D]) ship.angle = fmod(ship.angle + ship.angleSpeed, 360.0); // Render our object(s) - background objects first, and then forward objects. renderGfxObject(&background.gfxObj, background.pos.x, background.pos.y, background.angle, background.scale); renderGfxObject(&ship.gfxObj, ship.pos.x, ship.pos.y, ship.angle, ship.scale); if( (loopIter % 100) == 99 ) vandStrang(strang); renderText(strang, 300, 150); SDL_RenderPresent( gRenderer ); loopIter++; } close(); //Free allocated resources return 0; } void vandStrang(char str[]) { int l = strlen(str); int half_len = l/2; for(int i=0; i < half_len; i++) { // swap two elements char t = str[i]; str[i] = str[l - i - 1]; str[l - i - 1] = t; } } void close() { // Preferably, you should free all your GfxObjects, by calls to // freeGfxObject(GfxObject* obj), but you don't have to. freeGfxObject(&ship.gfxObj); freeGfxObject(&background.gfxObj); closeRenderer(); //Free resources and close SDL }
Uppg. 2
Funktionspekare
  1. Skapa en funktionspekare som heter print och tar inparametrarna (const char* str, int x, int y) med returtyp void. Skapa även två funktioner:

    void printToWindow(char* str, int x, int y) { ... } void printToConsole(char* str, int x, int y) { ... }
    där printToWindow(...) anropar renderText(...) och printToConsole(...) anropar printf(...).

    Inför med hjälp av funktionspekaren så att man kan toggla, med lämplig tangent, så att din utskrift av "Hello World!" (el vad du använt) skrivs ut i consolen eller i fönstret.

    Ändra anropet renderText(strang, 300, 150); (inuti realtidsloopen i main.c) till att använda din funktionspekare: print(strang, 300, 150); och sätt funktionspekaren omväxlande till printToWindow(...) respektive renderText(...) när du trycker på lämplig tangent.

    Skapa och tilldela värde för en funktionspekare kan du t ex göra på följande sätt:
    void (*print) (char* str, int x, int y) = printToWindow;
    respektive
    print = printToConsole;

    (För lösning, se nästa uppgift.)
  2. Troligen löste du tangentbordschecken med hjälp av state[SDL_SCANCODE_...]. Isåfall märkte du säkert att det är svårt att hålla nere en tangent endast en frame. Här passar det bättre att istället reagera på events som KEY_DOWN för din tangent.

    Dvs här finns en viktig poäng som gäller för all tangentbordsinmatning, inklusive för MD407:an. Ibland vill man kunna känna av en tangents status och utföra logik baserat på om tangenten är nedtryckt eller inte. Ibland vill man kunna reagera just när en tangent trycks ned eller släpps upp. Det första passar t ex realtidsspel där man styr en spelare. Det andra passar t ex ordbehandlare.

    Antag att du vill lyssna på KEY_DOWN för 'c'-tangenten (dvs SDL_SCANCODE_C). Du löser det med SDL på följande sätt:
    SDL_Event e; //Event handler while( SDL_PollEvent( &e ) != 0 ) { ... if( (e.type == SDL_KEYDOWN) && (e.key.keysym.scancode == SDL_SCANCODE_C) ) { ... } }
    Utför denna modifiering.
... void printToWindow(char* str, int x, int y) { renderText(str, x, y); } void printToConsole(char* str, int x, int y) { printf("%s\n", str); } ... void (*print) (char* str, int x, int y) = printToWindow; bool toggle = true; ... while(true) // The real-time loop { // Handle events on the inqueue (e.g., mouse events) SDL_Event e; //Event handler while( SDL_PollEvent( &e ) != 0 ) { if( e.type == SDL_QUIT ) { //User requests quit close(); exit(0); } if( (e.type == SDL_KEYDOWN) && (e.key.keysym.scancode == SDL_SCANCODE_C) ) { toggle = !toggle; if(!toggle) print = printToConsole; else print = printToWindow; } } ... print(strang, 300, 150); ...

Objektorientering med C.

I nästa uppgift skall vi öva på structs med funktionspekare vilket är ett sätt att efterlikna objektorientering med klasser och metoder. Vi vet att några av er ännu inte har läst objektorientering eller Java, men gör ert bästa, förklaringar följer och fråga gärna på föreläsningen. Ni kommer även att använda följande konstruktioner på labbarna.

En Klass är en datatyp och är i princip samma sak som structs men klasser kan dessutom innehålla så kallade metoder. En Metod är en funktion som tillhör klassen. Detta är ett behändigt sätt att strukturera sin kod tillsammans med data. Men för oss är främsta fördelen att vi kan skapa en lista av olika spelobjekt och anropa deras individuella update()- och render()-funktioner i en enkel loop över listan. De olika sorters spelobjekten behöver alltså inte ha samma update()- och render()-funktioner utan kan ha helt olika, vilket är praktiskt eftersom olika spelobjekt troligen behöver bete sig på helt olika sätt.

Objektorienterad stil med klasser och metoder är trevligt. C är dock inte förberett med enkla stöd för detta, men det går att åstadkomma. Det finns många olika sätt att i C efterlikna klasser med metoder och arv. Här följer ett förhållandevis enkelt sätt. Observera att nedanstående inte använder någon form av ny syntax eller funktionalitet hos C. Vi använder istället våra välkända structs och funktionspekare tillsammans för att efterlikna klasser.
Att efterlikna klasser med metoder via structs + funktionspekare:
minklass.h
typedef struct tMinKlass{ int x, y; // eller vilken data du vill void (*metod1) (struct tMinKlass* this, int a, float b); // Metod ... } MinKlass;
Ovanstående fungerar som en klass (MinKlass) med två datamedlemmar (x,y) samt en metod (metod1). "metod1" är en funktionspekare. Metoder implementeras här alltså genom att structen innehåller funktionspekare.
Exempel: Om vi skapar en variabel "MinKlass a;" så skall vi manuellt sätta funktionspekaren a.metod1 att peka på en lämplig global funktion som vi själva måste skapa och som (bl a) tar argument this, där this ska peka på a's data - dvs a's startadress i minnet. Därmed kan vi, inuti den globala funktionen, via this, accessa a's medlemmar (this->x, this->y, this->metod1). (Typen tMinKlass behövs för argumentet *this hos metod1 ovan, eftersom MinKlass där ännu inte är fullt definierad.) Den globala funktionen kan heta vad som helst, t ex "Metod1()", dvs samma eller här nästan samma som a's funktionspekare.
Här är ett exempel på en definition för den globala funktionen Metod1():
minklass.c
void Metod1(struct MinKlass* this, int a, float b) { ... this->x = ...; // access av medlemmar ... }
Vi sätter funktionspekaren metod1 att peka på den globala funktionen Metod1(): "a.metod1 = Metod1;"

Här är ett sammanfattande exempel för att skapa en variabel, tst, av typ MinKlass, initiera den samt anropa metoden:
MinKlass tst; // Vi skapar en variabel (dvs instans) av typen MinKlass // Vi måste sätta funktionspekaren att peka på avsedd funktion: tst.metod1 = Metod1; // Troligen vill vi även initiera tst's datamedlemmar till något tst.x = 5; tst.y = 2; ... // Nu kan vi anropa metod1() för tst på följande sätt: tst.metod1(&tst, 5, 2.8f); // Metodanrop. This pekar på tst



ÖVERKURS: För den intresserade eleven visar vi här även hur man kan åstadkomma arv, dvs hur en klass (Klass2) kan ärva en annan klass (Klass1).
Arv:

typedef struct tKlass1{ int medlem1; int (*metod1) (struct tKlass1* this, ...); ... } Klass1; typedef struct { Klass1 class1; // "ärvd klass" int nästaMedlem; ... } Klass2;
Den ärvda klassens medlemmar och metoder kan kommas åt på följande sätt:
Klass2 tst; tst.class1.medlem1 = ...; tst.class1.metod1 = ...; tst.class1.metod1(&tst.class1, ...);
Uppg. 3
Structs med funktionspekare
Vi skall nu ytterligare snygga upp och generalisera vår kod, bl a så att det senare blir lätt att lägga till fler objekt. Koden för att uppdatera skeppet och bakgrunden ligger just nu direkt i realtidsloopen i main-filen. Har man många objekt och mycket logik där så blir det snabbt oöverskådligt. Vi ska nu göra så att man helt enkelt i realtidsloopen endast loopar över alla spelobjekt och kallar på deras update()- resp render()-funktioner. T ex något i stil med detta:
for(int i=0; i < nGameObjects; i++) gameObjects[i]->update(gameObjects[i]); for(int i=0; i < nGameObjects; i++) gameObjects[i]->render(gameObjects[i]);

Som du kan se vill vi ha en array, gameObjects[], av alla våra spelobjekt (just nu har vi endast skeppet och bakgrunden men vi ska snart lägga till fler). Vår struct GameObject, som du införde i uppgift 1, skall alltså även innehålla metoderna update() och render().

Att skapa en array av rätt typ är trivialt om alla objekten är av samma typ (dvs samma struct). Vi kan ha en array av objekten eller dess pekare. Men... olika spelobjekt kanske senare behöver fundamentalt olika data lagrade i sina structs. Hur gör man då? Det finns åtminstone två olika lösningar:

  1. Vi kan låta vår struct GameObject innehålla ett superset av alla olika medlemmar som olika objekt behöver. Det är inte snyggt, men lätt och helt OK. Nackdelen är att det tar onödigt mycket minne, men för färre än några tusen instansierade objekt har det knappast praktisk betydelse.
  2. Eller så kan vi använda mer avancerad objektorientering, men det är överkurs...
    Vi kan lösa problemet objektorienterat genom att låta arrayen vara en array av typen pekare till en bas-struct (dvs basklass) som alla spelobjekt ärver ifrån. T ex:

    typedef struct { GameObject gameObj; int apa; ... } Player;typedef struct { GameObject gameObj; float bepa; ... } Background;
    Player player; // skapa en instans Background background; // skapa en instans int nGameObjects = 2; GameObject* gameObjects[] = // lägg adresserna i arrayen {&player.gameObj, &background.gameObj}; player.gameObj.update = updatePlayer; ... gameObjects[0].update(gameObject[0]);

    Men hur gör vi nu för att inuti respektive objekts render()- och update()-funktion få tag på den klassdata som inte tillhör basklassen GameObjekt (dvs apa respektive bepa)? Lösningen är väldigt "C-aktig" - dvs inte så vacker men fungerar:

      i Player.c:
    void updatePlayer(GameObject* gameobj) { Player *this = (Player*)((void*)gameobj - (void*)&((Player*)0)->gameObj); // Player-medlemmar kan nu accessas med: this->apa ... // dvs: this->medlem }
    Vad vi gör är att skapa en pekare, this, som pekar på starten av vår struct Player. Vi gör detta genom att beräkna this som "adressen till gameobj (som pekar på vår basklass) minus Player-structens offset till basklassen". (Eftersom vi i det här exemplet lagt gameObj som första medlem i Player och Background är offseten = 0, så man hade här kunnat nöja sig med this = (Player*)&gameobj;.)

Vi väljer alternativ 1 då det är lättast. Gör detta. Behöver du hjälp följer här de steg du behöver göra:

  • Lägg till metoderna update(...) och render(...) i din struct GameObject.
  • För enkelhets skull, lägg din övriga nya kod för stegen nedan direkt i main.c-filen. Vi kommer nästa vecka till hur man lämpligen kan lägga de nya delarna i andra filer (vi behöver använda oss av C:s keyword "extern").
  • Skapa en global array gameObjects[] som innehåller adresserna till background och ship. Skapa även en global variabel för arrayens längd, dvs nGameObjects = 2.
  • Just nu ligger uppdateringen av dina objekt direkt i realtidsloopen i main(). Flytta ut all uppdatering av skeppets data till en funktion updateShip(...). För att kunna göra detta måste du även flytta din variabel const Uint8 *state och lägga den som en global variabel. Likaså, flytta uppdateringen av datan för background till en funktion updateBackground(...). :
    const Uint8 *state; void updateShip(GameObject* this) { ... } void updateBackground(GameObject* this) { ... }
  • Skapa en funktion void render(GameObject* this) som anropar renderGfxObject(...).
  • I main() där du initierar ship, lägg även till:
    ship.update = updateShip; ship.render = render;
    Gör motsvarande för background.
  • Slutligen, i realtidsloopen där du tidigare uppdaterade och renderade skeppet och background, anropar du nu istället metoderna:
    // Update our object(s) for(int i=0; i < nGameObjects; i++) gameObjects[i]->update(gameObjects[i]); // Render our object(s) - background objects first... for(int i=0; i < nGameObjects; i++) gameObjects[i]->render(gameObjects[i]);
GameObject.h: ... typedef struct tGameObject{ GfxObject gfxObj; vec2f pos; float speed; double angle, angleSpeed; float scale, scaleSpeed; void (*update) (struct tGameObject* gameobj); void (*render) (struct tGameObject* gameobj); } GameObject; main.c: ... const Uint8 *state; GameObject ship, background; // ordningen är viktig ty annars ritar bakgrunden över skeppet: GameObject* gameObjects[] = {&background, &ship}; int nGameObjects = 2; void updateShip(GameObject* this) { if (state[SDL_SCANCODE_RIGHT]) this->pos.x = (this->pos.x+this->speed >= 799) ? 799 : this->pos.x+this->speed; if (state[SDL_SCANCODE_LEFT]) this->pos.x = (this->pos.x-this->speed <= 0) ? 0 : this->pos.x-this->speed; if (state[SDL_SCANCODE_DOWN]) this->pos.y = (this->pos.y+this->speed >= 599) ? 599 : this->pos.y+this->speed; if (state[SDL_SCANCODE_UP]) this->pos.y = (this->pos.y-this->speed <= 0) ? 0 : this->pos.y-this->speed; if (state[SDL_SCANCODE_A]) this->angle = fmod(this->angle - this->angleSpeed, 360.0); if (state[SDL_SCANCODE_D]) this->angle = fmod(this->angle + this->angleSpeed, 360.0); } void updateBackground(GameObject* this) { this->angle = fmod(this->angle+0.02, 360); this->scale += 1.0/2500.0; if (state[SDL_SCANCODE_W]) this->scale = this->scale + this->scaleSpeed; if (state[SDL_SCANCODE_S]) this->scale = (this->scale - this->scaleSpeed <= 0) ? 0 : this->scale - this->scaleSpeed; } void render(GameObject* this) { renderGfxObject(&this->gfxObj, this->pos.x, this->pos.y, this->angle, this->scale); } int main( int argc, char* args[] ) { ... ship.update = updateShip; ship.render = render; ... background.update = updateBackground; background.render = render; state = SDL_GetKeyboardState(NULL); // get pointer to key states ... while(true) // The real-time loop { ... // Update our object(s) for(int i=0; i < nGameObjects; i++) gameObjects[i]->update(gameObjects[i]); // Render our object(s) - background objects first, and then forward objects for(int i=0; i < nGameObjects; i++) gameObjects[i]->render(gameObjects[i]); ...
Uppg. 4
Egentligen räcker detta för denna vecka, men lägg gärna till några fler spelobjekt.