Maskinorienterad Programmering / Programmering av Inbyggda System

Hemuppgifter C-programmering

C - Föreläsning 4.

Denna vecka skall vi öva oss på bl a: extern, static, enum, och separata .c-filer.

Deklara-tion vs Definition
En deklaration talar endast om för kompilatorn vad en variabel eller funktion har för typ. En definition beskriver variabeln/funktionen i dess helhet, dvs en funktionsdefinition innehåller även funktionskroppen, medan en variabeldefinition instansierar variabeln:
void func(); // deklaration av funktion void func() // definition av funktion { ... } int a; // Definition och instansiering av variabel.
För ren deklaration av variabel - se extern nedan.
Globala
vs lokala variabler
Global vs lokal variabel: En global variabel är definierad utanför en funktion och hamnar typiskt i datasegmentet, dvs inte på stacken, i programmets adressrymd. Därmed existerar en global variabel under hela programmets exekveringstid. En lokal variabel är definierad inuti en funktion och hamnar på stacken. Därmed är den flyktig, dvs upphör att existera när funktionen returnerat.
char str1[] = "hej"; // global variabel void func() { char str2[] = "då"; // lokal variabel }

static
Man kan definiera en variabel som static. Det betyder att den bara är synlig inom det scope där den skapas (inklusive underliggande scopes) men adressen för variabeln hamnar i datasegmentet precis som hos globala variabler. Effekten blir följande:
Lokal variabel definierad som static: Variabeln behåller sitt värde mellan funktionsanrop, precis som om det vore en global variabel, men variabeln syns bara inom funktionen.
void func() { static int nTimes; // static-variabel med lokal synlighet. nTimes++; // ökar värdet varje gång funktionen anropas. }

Global variabel definierad som static: Variabeln syns bara inom c-filen. Vid länkningen slipper man namnkonflikter med globala variabler i andra .c-filer som heter samma sak.

fil1.c
... static int tmp;
fil2.c
... static int tmp;

Funktion definierad som static: Funktioner ligger alltid i kodsegmentet i programmets adressrymd (som även typiskt är skrivskyddat av operativsystemet). Med static blir synligheten hos funktionsnamnet begränsat till .c-filen. Dvs andra .c-filer kan innehålla en funktion med samma namn utan konflikt vid länkningen.

player.c
... static void update(...);
background.c
... static void update(...);

Deklaration med static: Om man gör en deklaration av en funktion som är definierad med static skall deklarationen också använda static.

exempel.c
... static void update(); ... static void update(...) { ... }


extern
Extern talar om att man endast gör en deklaration (ej definition) av en funktion eller variabel och att definitionen (instansen) finns någon annanstans - senare i samma fil eller i annan fil. Extern är främst användbart för variabeldeklarationer vilket innebär att man inte skapar en ny variabel utan endast talar om för kompilatorn vad den har för typ. Variabelns adress kommer att bli känd vid länkningen eller eller senare i samma fil. (Det går inte att använda extern för att göra en lokal variabel synlig utanför sin funktion, eftersom variabeln ligger på stacken och dess adress är flyktig och inte känd vid länkning.)
Extern för variabel:
extern const Uint8 *state;

Extern för funktion: Rena deklarationer är lätt att göra för funktioner, så extern behövs egentligen ej för funktionsdeklarationer:

extern void update(); // är samma sak som: void update();


Separata .h- och .c-
filer för objekt
Med include guards inkluderas en .h-fil max en gång per .c-fil som kompileras. Detta löser t ex att structs som definieras i .h-filen inte blir definierade mer än en gång.
      Men... .h-filen blir alltså inkluderad en gång per .c-fil som använder den, och därför är det viktigt att inte lägga definitioner av globala variabler och funktioner i .h-filer. Annars skapas variabeln/funktionen en gång per .c-fil som inkluderar .h-filen. Vid länkningen skulle detta medföra att länkaren kommer att klaga på "duplicate symbol" av en och samma variabel/funktion, eftersom den blivit instansierad för varje motsvarande .c-fil.
      Istället lägger man endast deklarationerna i .h-filen - och definitionerna i motsvarande .c-fil. Endast deklaration (utan definition) för variabler gör man som sagt med extern, vilket talar om att definitionen kommer senare (typiskt i en annan .c-fil).

Deklarationer av en och samma variabel eller funktion får däremot förekomma hur många gånger som helst, så länge deklarationen inte ändras.

exempel.h
#ifndef EXEMPEL_H #define EXEMPEL_H typedef struct { // typ- int x, y; // definition } Position; // extern char str1[]; // deklaration void func(); // deklaration #endif // EXEMPEL_H
exempel.c
#include exempel.h // för typer // etc. char str1[] = "hej"; // definition void func() // definition { char str2[] = "då"; }
hehheh
Uppg. 1
Flera .c-filer. Extern.

Här är det meningen att du skall lära dig hur man lägger funktionalitet i en .c-fil och gör motsvarande deklaration i .h-filen och inkluderar .h-filen i den/de .c-filer där du behöver använda funktionen, structen eller variabeln.

Vi fortsätter med förra veckans projekt och där vi slutade. Det är lämpligt att separera ut kod för skeppet och bakgrunden (och övriga spelobjekt som du kanske skapat) från main.c till separata filer. Du gör det med hjälp av följande steg:
  1. Flytta updateShip()- och updateBackground()-funktionerna till player.h/.c samt background.h/.c.
  2. Tangentbordsavkänningen, som används för bakgrunden och skeppet, använder sig av variabeln state som ligger i main()-funktionen. Om du inte redan gjort det, gör denna variabel global i main.c men låt tilldelningen ligga kvar i början av main()-funktionen, före realtidsloopen. Nu kan man få åtkomst till variabeln i player.c och background.c genom att deklarera den med extern någonstans var som helst ovanför där du behöver använda den - förslagsvis högst upp i respektive .c-fil efter alla #include...
  3. Eftersom render()-funktionen (som ligger i main.c) just nu är gemensam för bakgrunden och skeppet kan du flytta den funktionen till gameobjects.c (med deklaration i gameobjects.h så klart).
  4. För övnings skull, flytta din instansiering av GameObject ship, background; till respektive .h- och .c-filer.

    Dvs du lägger instansieringen GameObject ship; i player.c och en ren deklaration extern GameObject ship; i player.h (se bilden ovan för exempel.h/c). Genom att inkludera player.h i en .c-fil (t ex main.c) får du åtkomst till ship där. Likadant gör du för background.
  5. Kompilera och testa att allt fungerar som förut.
gameobject.h: #ifndef GAMEOBJECT_H #define GAMEOBJECT_H #include "renderer.h" #include "vecmath.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; void render(GameObject* this); #endif // GAMEOBJECT_H
gameobject.c: #include "gameobject.h" #include "renderer.h" void render(GameObject* this) { renderGfxObject(&this->gfxObj, this->pos.x, this->pos.y, this->angle, this->scale); }
player.h: #ifndef PLAYER_H #define PLAYER_H #include "gameobject.h" extern GameObject ship; void updateShip(GameObject* this); #endif // PLAYER_H
player.c: #include "player.h" #include // för fmod() extern const Uint8 *state; GameObject ship; 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); }
background.h: #ifndef BACKGROUND_H #define BACKGROUND_H #include "gameobject.h" extern GameObject background; void updateBackground(GameObject* this); #endif // BACKGROUND_H
background.c: #include "background.h" extern const Uint8 *state; GameObject background; 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; }
main.c ... #include "vecmath.h" #include "gameobject.h" #include "player.h" #include "background.h" const Uint8 *state; GameObject* gameObjects[] = {&background, &ship}; int nGameObjects = 2; ... int main( int argc, char* args[] ) { ... state = SDL_GetKeyboardState(NULL); // get pointer to key states ... while(true) // The real-time loop { ...

Uppg. 2
Som en sista kontroll av att du förstått hur man kan flytta kod till olika filer med dess konsekvenser, flytta arrayen gameObjects samt int nGameObjects från main.c till gameobjects.c resp. gameobjects.h. Initiera de båda i main()-funktionen innan realtidsloopen. Gör eventuell övrig modifiering som krävs för att koden ska fungera. Kompilera och testa att programmet fungerar som förut. Försök först själv. Är du ovan kan det vara lite lurigt och krävas att du tänker efter.
  • Arrayen gameObjects ska initieras med adresserna till background och ship (i rätt ordning så inte bakgrunden ritar över skeppet) i main() innan realtidsloopen.
  • Definiera (dvs skapa) arrayen gameObjects i gameobjects.c. Här måste du tala om hur många element som arrayen ska ha plats för. Ta gärna till lite i överkant så du kan lägga till objekt senare. Flytta även nGameObjects = 2; hit.
  • Deklarera de två variablerna i gameobjects.h

  • Programstruktur och Objektallokering

    Förra veckan lärde vi oss att gruppera variabler, som på något sätt tillhör ett och samma objekt, i structs. Vi har nu lärt oss att man lägger struct-definitionen i .h-filen, medan definitionerna av metoder och eventuellt andra funktioner som man tycker hör samman med sin struct läggs i .c-filen. Motsvarande deklarationer lägger man i .h-filen.

    Inom objektorientering lägger man gärna större structs i filer med samma namn som sin struct. Detta är t ex mycket tydligt i Java. Små structs (som kanske t om endast används av den större struct:en) kan gott och väl ligga i samma filer som den större struct:en. Inom så kallad komponentbaserad programmering till skillnad från objektorientering grupperar man gärna sin programkod baserad på funktionalitet. Det viktiga är att man själv skapar en kod- och filstruktur som man tycker är naturlig och begriplig. Ofta lägger man mycket tankeverksamhet just på att strukturera sin programkod och var man instansierar sina objekt.


    Objektallokering

    Vi ska här gå igenom olika sätt hur man kan allokera minne för sina instanser av objekt.
    int tmp[10]; // allokera i datasegmentet med global synlighet void func() { int tmp2[10]; // allokera på stacken static int tmp3[10]; // allokera i datasegmentet - med lokal synlighet int *pTmp = malloc(sizeof(int) * 10); // allokera på heapen ... free(pTmp); // deallokera från heapen }

    Allokera globala variabler i datasegmentet
    När vi skapar globala variabler läggs de i datasegmentet för programmets adressrymd. Att ha massa globala variabler utspridda i sin programkod anses dock synnerligen osnyggt. Man vill främja någon form av struktur. Det normala är att man skapar instanser av sina klasser antingen som lokala variabler (med livslängd tills dess att funktionen returnerar) eller genom att allokera dem på heapen.

    Allokera på stacken Att allokera sina objektinstanser på stacken är oftast snyggast och att föredra om man kan. Det är då tydligt vilken funktion som instanserna tillhör samt vad de har för livslängd. Det är dock inte alltid det är praktiskt möjligt (t ex om instanserna ska fortsätta leva efter funktionen returnerat) och dessutom har stacken ofta mer begränsad storlek än heapen och datasegmentet. Stora minnesutrymmen allokerar man typiskt på heapen.

    Allokera på heapen
    I t ex Java, C++, och C# allokerar man på heapen med hjälp av new. I C använder vi malloc(). På MD407:an har vi dock inte automatiskt tillgång till malloc(), då både malloc() och new är operativsystemsberoende funktioner. När malloc allokerar nytt minne från heapen så behöver den då och då be operativsystemet att tillgängliggöra mer virtuellt adressutrymme. Istället skall vi i dessa övningsuppgifter visa att man för många problem ofta klarar sig utmärkt utan att allokera på heapen. Maskinnära programmering innebär ofta programmering av realtidssystem där man emellanåt måste ha noggrann timing av skrivningar/läsningar eller har andra krav att funktioner måste exekvera klart inom en mycket kort tid. Allokering på heapen som kräver operativsystemsanrop tar ofta tämligen lång tid och är inget man vill göra tusentals gånger inuti en realtidsloop. I vissa fall kan ett anrop till malloc/new ta miljontals klockcykler. Istället kan man t ex allokera i datasegmentet (genom globala variabler eller variabler definierade som static).


    Så var bör vi instansiera våra spelobjekt?

    Var bör vi lägga våra instanser av de två globala variablerna ship och background, samt eventuellt ytterligare spelobjekt? Ska de ligga som globala variabler i respektive .h- och .c-fil. Eller i main.c? Eller i en fil globals.h/.c? Våra objekt är visserligen tillräckligt små för att de ska få plats på stacken som lokala variabler (t ex i main()), men låt oss anta det mer generella fallet att vi kan ha både ordentligt många instanser och minnesmässigt stora objekt.

    För våra spelobjekt finns åtminstone tre olika naturliga sätt att placera dessa: i filen som använder objektet (t ex main.c), i .c-filen för sin struct eller i en särskild .c- och .h-fil för globala variabler. I det sistnämnda fallet kan man dessutom skapa en övergripande game-struct.

    I .c-filen som använder objektet: dvs main.c i vårt fall, så som vi började innan vi flyttade ship och background till sina respektive .c/.h-filer. En fördel är att detta alternativ är mycket enkelt och ofta tydligt vem som äger objektinstansen. En nackdel är att instansen även kan behövas användas av andra objekt i andra .c-filer. Att exportera det med extern kan försvåra en snygg överskådlig struktur. Man kan skicka med instansen som inparameter till andra objekts metoder. Detta är ofta snyggt och i linje med objektorientering. Men det kan också lätt bli bökigare än att ha globala variabler som synliggörs med extern.

    I .c-filen för sin struct: dvs player.c resp. background.c (etc). Fördelen är att man då lättare kan lägga över kontrollen av allokeringen till objektets metoder. Man kan t om ofta med fördel skapa en funktion som är kopplad till klassen/structen som heter Create(...) och returnerar adressen för ett instansierat och färdiginitierat objekt. Men man har, precis som i fallet ovan, fortfarande kvar problemet vad man gör om många andra objekt behöver åtkomst till instansen. Man skulle som sagt kunna lösa det genom att skicka med instansen som inparameter till andra objekts metoder.

    Alla globala variabler i en särskild fil globals.h/globals.c Fördelen är att man faktiskt ofta lätt och överskådligt kan organisera alla sina globala variabler i en enda stor struct, t ex struct game som i sin tur innehåller structs levels, etc. En nackdel är att det kan bli otydligt vem som äger vilka instanser och när de skapas respektive när de försvinner. Alla objekt har åtkomst till allt, vilket är både för- och nackdel.

    Sammanfattning
    Det finns inte endast ett sätt som är korrekt. Valet av struktur är ofta ett avvägande mellan många parametrar, som storlek av projekt och enkelhet att implementera, att kunna göra tillägg, att förstå för sig själv och/eller andra, att få korrekt kod m.m. Välj ett sätt som passar dig bäst beroende på situationen. I föregående uppgift använde vi mittenalternativet - dvs lägger instanserna i .c-filen för sin struct - och våra lösningsförslag kommer att fortsätta med det. Om ett antal uppgifter kommer vi att lägga till en form av semi-dynamisk minnesallokering där instanserna också ligger i i .c-filen för motsvarande datastruktur.



    Uppg. 3
    (Övningsmoment: static, programmering på egen hand, )
    Skapa en ny typ av spelobjekt (just nu har du background och ship), dvs som använder en annan bild. T ex kan du ta bilden bredvid. Låt spelobjektet röra sig på skärmen på något sätt. Lägg implementeringen i en ny fil som du kallar aliens.c med tillhörande aliens.h.

    Låt dina aliens-objekt vara av typen GameObject. Behöver du nya struct-medlemmar för att röra objekten så inför dem. Vi låter fortfarande struct GameObject vara ett superset av alla medlemmar som kan behövas av våra olika spelobjekt.

      För att din programkod ska bli kompatibel med de tillägg vi ska göra senare, gör även följande:
    • Gör en funktion void createAliens() i aliens.c/.h som skapar t ex tre aliens och även adderar deras adresser till array gameObjects[]. Anropa createAliens() i main-funktionen innan realtidsloopen startar.
    • På samma ställe som anropet till createAliens(), anropa funktioner void createShip() och void createBackground() som initierar skeppet och bakgrunden. Dvs, flytta koden för att skapa skeppet och bakgrunden från main()-funktionen till createShip() i player.c/.h respektive createBackground() i background.c/h.
      OBS. Objekten ritas ut i ordningen som de läggs i listan gameObjects[]. Därför, anropa createBackground() först och därefter de andra två.
      I aliens.c implementerar du följande funktioner:
    • update()-funktionen som kontinuerligt förflyttar skeppet på något sätt inom skärmen.
      Tips: eftersom din update()-funktion inte behöver synas utanför aliens.c-filen kan du lämpligen definiera den som static. Dvs static void update() { ... }.
    • createAliens() som skapar t ex tre stycken aliens och lägger till dem till gameObjects[]. Att lägga till ett objekt sist i arrayen är lätt och kan göras med: gameObjects[nGameObjects++] = ....
      Eftersom alla tre instanser använder samma bild (=textur) för utritningen så kan du skapa ett och endast ett GfxObject som används av alla tre.
    Exempel:
    static GameObject aliens[3]; static void update(GameObject* this) { this->pos.x += ... ... } void createAliens() { static GfxObject gfx; // ligger i datasegmentet men har static bool bFirstTime = true; // endast lokal synlighet // It is unnecesary to load the image for every instance. // Just do it once: if(bFirstTime) { bFirstTime = false; gfx = createGfxObject( "../alienship.png" ); } aliens[0].gfxObj = gfx; aliens[0].update = update; // defined in this file aliens[0].render = render; // defined in gameobjects.h // initiera resten av medlemmarna i struct GameObject ... // Addera instansen till arrayen gameObjects gameObjects[nGameObjects++] = &aliens[0]; // gör nu samma sak för aliens[1] och aliens[2]... aliens[1].gfxObj = gfx; ... osv ... }
    aliens.h: #ifndef ALIENS_H #define ALIENS_H void createAliens(); #endif // ALIENS_H
    aliens.c #include "aliens.h" #include "gameobject.h" #include "renderer.h" static GameObject aliens[3]; static void update(GameObject* this) { // update positions this->pos.x += this->moveDir.x * this->speed; this->pos.y += this->moveDir.y * this->speed; // Hämta fönsterstorleken: int screenWidth, screenHeight; SDL_GetWindowSize(gWindow, &screenWidth, &screenHeight); // Låt objektet studsa vid skärmkanterna if(this->pos.x <= this->gfxObj.outputWidth/2 && this->moveDir.x < 0) { int overflow = (this->gfxObj.outputWidth/2) - this->pos.x; this->pos.x += 2*overflow; this->moveDir.x = -this->moveDir.x; } if(this->pos.x >= screenWidth - this->gfxObj.outputWidth/2 && this->moveDir.x > 0) { int overflow = this->pos.x - (screenWidth - this->gfxObj.outputWidth/2); this->pos.x -= 2*overflow; this->moveDir.x = -this->moveDir.x; } if(this->pos.y <= this->gfxObj.outputHeight/2 && this->moveDir.y < 0) { int overflow = (this->gfxObj.outputHeight/2) - this->pos.y; this->pos.y += 2*overflow; this->moveDir.y = -this->moveDir.y; } if(this->pos.y >= screenHeight - this->gfxObj.outputHeight/2 && this->moveDir.y > 0) { int overflow = this->pos.y - (screenHeight - this->gfxObj.outputHeight/2); this->pos.y -= 2*overflow; this->moveDir.y = -this->moveDir.y; } } static void init(GameObject* pObj, GfxObject* pGfx, int w, int h, vec2f pos, vec2f moveDir, float speed) { pObj->gfxObj = *pGfx; pObj->gfxObj.outputWidth = w; pObj->gfxObj.outputHeight = h; pObj->pos = pos; pObj->moveDir = moveDir; pObj->speed = speed; pObj->angle = 0; // unused pObj->angleSpeed = 0; // unused pObj->scale = 1.0f; pObj->scaleSpeed = 0; // unused pObj->update = update; // Our own update in this file pObj->render = render; // From gameobjects.h } void createAliens() { static GfxObject gfx; static bool bFirstTime = true; // It is unnecesary to load the image for every instance. Just do it once: if(bFirstTime) { bFirstTime = false; gfx = createGfxObject( "../alienship.png" ); } float speed = 5.0f; int w = 100, h = 100; init(&aliens[0], &gfx, w, h, (vec2f){100, -50}, (vec2f){1, 1}, speed); init(&aliens[1], &gfx, w, h, (vec2f){200, -50}, (vec2f){0.66f, 1}, speed); init(&aliens[2], &gfx, w, h, (vec2f){300, -50}, (vec2f){0.33f, 1}, speed); gameObjects[nGameObjects++] = &aliens[0]; gameObjects[nGameObjects++] = &aliens[1]; gameObjects[nGameObjects++] = &aliens[2]; }
    main.c ... #include "gameobject.h" #include "player.h" #include "background.h" #include "aliens.h" ... int main( int argc, char* args[] ) { // Start up SDL and create window of width=800, height = 600 initRenderer(800, 600); // Create objects createBackground(); createShip(); createAliens(); ...
    background.h ... void createBackground(); ...
    background.c ... void createBackground() { 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; background.update = updateBackground; background.render = render; gameObjects[nGameObjects++] = &background; }
    player.h ... void createShip(); ...
    player.c #include "player.h" #include // för fmod() ... void createShip() { 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 ship.update = updateShip; ship.render = render; gameObjects[nGameObjects++] = &ship; }
    gameobject.h #include "renderer.h" // to get screenWidth/screenHeight #include "vecmath.h" typedef struct tGameObject{ GfxObject gfxObj; vec2f pos; vec2f moveDir; // I added this my for aliens->update() float speed; double angle, angleSpeed; float scale, scaleSpeed; // methods: void (*update) (struct tGameObject* gameobj); void (*render) (struct tGameObject* gameobj); } GameObject; void render(GameObject* this); // metod extern GameObject* gameObjects[]; extern int nGameObjects; #endif // GAMEOBJECT_H
    Uppg. 3

    Avstamp inför fortsättningen

    Övningsuppgifterna fram till och med hit har handlat om att ni skall lära er behärska programspråket C och att era allmänna programmeringskunskaper skall vara tillräckliga för den svårighetsgrad som vi kräver i kursen och även testar på tentan. Det som följer nedan är alltså egentligen överkurs för den här kursen (och kommer inte på tentan). En del tas upp i andra fortsättningskurser och en del gör dig helt enkelt redan här och nu till en bättre programmerare av avancerade förlopp, vilket ofta är viktigt för styrsystem, robotprogrammering, inbäddade system, spelprogrammering, m.m. I synnerhet underlättar det för dig när det gäller att lägga till ytterligare funktionalitet.
      De resterande uppgifterna nedan är till för att:
    • hjälpa er att bli bättre programmerare av oberoende och beroende händelseförlopp.
    • hjälpa er med att kvickt bygga upp ett ramverk som typiskt behövs för många sorters större projekt - t ex inför lab 5 där ni implementerar ett eget spel (eller liknande).
      Det är alltså ett antal punkter kvar vi behöver gå igenom innan vi tryggt släpper er för att implementera resten av spelet på egen hand:
    • Dynamiskt kunna lägga till nya instanser av objekt.
    • Dynamiskt kunna terminera instanser.
    • Låta objekten interagera med varandra.

    Vi vet att förkunskaperna hos er elever i den här kursen varierar oerhört, från de som programmerat mycket och som löser punkterna ovan själva galant, till de som har mycket lite tidigare erfarenheter. Vi skulle kunna ge er ovanstående punkter som labuppgifter under ett par veckor tillsammans med mycket handledning för många elever. Men just dessa uppgifter är mer typiska för en kurs inom objektorientering och dels vill vi raskt komma framåt.

    Den avancerade eleven är mycket välkommen att implementera helt egna lösningar istället för de följande uppgifternas lösningsförslag. Övriga håller vi i handen en stund till.

    Uppg. 4
    Animerade spelobjekt

    (Övningsmoment: låta ett objekts update()-funktion hantera två oberoende förlopp - här förflyttning och animering.)

    Nu ska vi unna oss att göra applikationen intressantare genom att stödja animerade objekt.
    Uppgift: Lägg till några nya animerade spelobjekt (t ex dessa två till höger) som även rör sig runt på skärmen på något sätt.

    För att skapa animerade bilder: Enklast är att skapa en så kallad sprite sheet, d v s en textur (=bild) som innehåller samtliga frames i animeringen, och växla vilken del som skall visas på skärmen under vilken tidpunkt.
    •   Här finns en sådan förskapad för hjärtat (10 st frames):

    •   Här hittar du en sådan förskapad för myntet (40 st frames):

    •   Här hittar du en sådan för ett äpple (25 st frames):

    Alternativt kan du finna dina egna gif-animeringar på nätet och konvertera dem till sprite sheets. Här är en sådan online-site som funkar någorlunda men du kan säkert hitta andra:
      https://sites.google.com/site/stevedunns/convertanimatedgifstospritemaps.

      För att lägga till animering i ditt projekt, välj mellan att:
    • kika på SDL_RenderCopyEx och funktionen renderGfxObject() i filen renderer.c och lista ut själv hur du gör för att lägga till animeringsstöd.
    • Eller...
    • använd en ny förberedd version av filerna renderer.c samt renderer.h. Dessa innehåller en ny funktion:
        void renderGfxSubFrame(GfxObject* gfx, int x, int y, double angle, float scale, SDL_Rect srcRect ); som väljer vilken del av bilden som skall ritas ut. Valet styrs av sista parametern: srcRect.
      SDL_Rect är definierad som:
      typedef struct { int x, y, w, h; } SDL_Rect;
      I render()-metoden för ditt spelobjekt styr du enkelt vilken del av din bild som skall visas på följande sätt. Antag att:
      •   this->numFrames är totalt antal frames i animeringen (10 för hjärtat och 40 för myntet).
      •   this->frame är numret för nuvarande frame som skall visas.
      void render(GameObject* this) { //Render a sub frame to screen. Here, we know that the //image is a sprite sheet with the frames outlined next //to each other in the x-direction. Thus, the //x-coordinate for the current frame is easy to compute: int w = this->gfxObj.textureWidth / this->numFrames; int x = w * (int)(this->frame); SDL_Rect srcRect = { x, 0, w, this->gfxObj.textureHeight }; renderGfxSubFrame(&this->gfxObj, this->pos.x, this->pos.y, this->angle, this->scale, srcRect ); }
      Update() är enkel. Allt du behöver göra är att inkrementera this->frame varje frame och wrappa runt när värdet når this->numFrames.
    main.c: ... #include "heart.h" #include "coin.h" ... int main( int argc, char* args[] ) { initRenderer(800, 600); // Create objects createBackground(); createShip(); createAliens(); createHearts(); createCoins(); ...
    gameobjects.h: typedef struct tGameObject{ ... // new members for animated images: float frame; float frameSpeed; int numFrames; ... } GameObject;
    gameobjects.c // OBS - vi valde här att byta ut default-render()-funktionen i // gameobject.c och även låta background, player och aliens använda den. // Givetvis måste man då även i create()-funktionerna för dessa objekt sätta: // "obj".frame = 0; // "obj".frameSpeed = 1; // "obj".numFrames = 1; void render(GameObject* this) { // Render a sub frame to screen // Here, we know that the heart image is a sprite sheet with 10 frames // simply outlined next to each other in the x-direction. // Thus, the x-coordinate for the current frame is easy to compute: int w = this->gfxObj.textureWidth / this->numFrames; int x = w * (int)(this->frame); SDL_Rect srcRect = { x, 0, w, this->gfxObj.textureHeight }; // select the sub image renderGfxSubFrame(&this->gfxObj, this->pos.x, this->pos.y, this->angle, this->scale, srcRect ); }
    heart.h #ifndef HEART_H #define HEART_H void createHearts(); #endif // HEART_H
    heart.c #include "heart.h" #include "gameobject.h" #include // för fmod() GameObject heart[100]; static void update(GameObject* this) { // update which frame in texture to show this->frame = fmod( (this->frame+this->frameSpeed), this->numFrames ); // update position this->pos.x += ... } static void init(GameObject* pObj, GfxObject* pGfx, vec2f pos, float startFrame) { pObj->gfxObj = *pGfx; pObj->gfxObj.outputWidth = 100; pObj->gfxObj.outputHeight = 100; pObj->pos = pos; pObj->moveDir = (vec2f){0,0}; // unused pObj->speed = 0; pObj->angle = 0; // unused pObj->angleSpeed = 0; // unused pObj->scale = 1.0f; pObj->scaleSpeed = 0; // unused pObj->update = update; // our own update in this file pObj->render = render; // From gameobjects.h pObj->frame = startFrame; pObj->frameSpeed = 0.5f; pObj->numFrames = 10; } void createHearts() { static GfxObject gfx; static bool bFirstTime = true; // It is unnecesary to load the heart image for every instance. Just do it once: if(bFirstTime) { bFirstTime = false; // Create a GfxObject for the heart image gfx = createGfxObject( "../fireheart_new.png" ); } init(&heart[0], &gfx, (vec2f){100, 100}, 0); init(&heart[1], &gfx, (vec2f){200, 100}, 3); init(&heart[2], &gfx, (vec2f){300, 100}, 6); gameObjects[nGameObjects++] = &heart[0]; gameObjects[nGameObjects++] = &heart[1]; gameObjects[nGameObjects++] = &heart[2]; }
    hehheh

    Att plocka bort instanser från listan av aktiva spelobjekt

    Vi vill också kunna terminera aktiva spelobjekt och ta bort dem från vår lista gameObjects[] som håller våra aktiva instanser.

    Att ta bort ett aktivt spelobjekt: Vi kan låta borttagning av en instans från arrayen gameObjects[] styras av objektets update()-metod. När vi i realtidsloopen i main() loopar över alla aktiva instanser i gameObjects[] kan vi låta update()-metoderna returnera false om de skall tas bort ur gameObjects[] och true om de skall finnas kvar. På så sätt kan anropande loopen ansvara för borttagningen.

    Det finns många andra sätt som man kan lösa problemet. Vi skulle kunna låta update()-metoden själv plocka bort sig ur gameObjects[]. Update()-metoden känner inte till sitt index i gameObjects[], vilket behövs för att ta bort rätt element. Men vi hade kunnat låta update()-metoden loopa igenom listan tills dess att det hittar elementet som pekar på samma adress som aktuell instans som skall tas bort. För hundratals eller tusentals instanser riskerar detta dock att ta signifikant lång tid om många spelobjekt skall ta bort sig samtidigt. Vår valda lösning tar nästan ingen tid oberoende av hur många instanser som skall tas bort.

    Uppg. 5
    Gör så att update()-funktionerna för dina nya skapade spelobjekt (dvs ej ship eller background) efter ett tag avslutar instansen genom att returnera false. Låt den anropande loopen i main()-funktionen ansvara för att spelobjektet tas bort ur gameObjects[].

    (Övningsmoment: programmering, pekare, medlemmar, metoder, svårighetsgrad: typisk deluppgift eller lättare uppgift på tenta.)

    Du kan införa structmedlemmarna int age; och int lifeTime; i struct GameObjects, där age beskriver hur många frames objektet varit aktivt och lifeTime beskriver efter hur många frames det skall plockas bort. Du räknar räknar upp age varje frame i update()-metoden. När age == lifeTime låter du update() returnera false (annars true).

    gameobjects.h: typedef struct tGameObject{ ... int age; int lifeTime; ... // methods: bool (*update) (struct tGameObject* gameobj); ... } GameObject; aliens.c / coin.c / heart.c: static bool update(GameObject* this) { ... return (this->age++ < this->lifeTime); } I create-funktionerna: ... pObj->age = 0; pObj->lifeTime = 6*60; // ~6 seconds ... player.c och background.c bool update...(...) { ... return true; } main.c i realtidsloopen där vi anropar update()-metoderna: ... // Update our object(s) int j=0; for(int i=0; i < nGameObjects; i++) { if( gameObjects[i]->update(gameObjects[i]) ) gameObjects[j++] = gameObjects[i]; // lägg tillbaks objektet i arrayen } nGameObjects = j; // uppdaterar arrayens storlek ...

    Att skapa en datastruktur för att initiera händelser vid givna tidpunkter

    Uppg. 6
    Här kommer vi att visa ett (av många) exempel på hur du kan skapa en datastruktur som styr när händelser skall starta. Detta är också ytterligare en övning på att använda structs med funktionspekare.

    Uppgift:

    Olika händelser kan lämpligen initieras med hjälp av olika funktioner. Man kan enkelt kontrollera när dessa olika funktioner skall anropas genom att ha en lista av par av {tidpunkt, funktionspekare}. I realtidsloopen kollar man om det är dags att anropa nästa händelse i listan, och isåfall gör detta och avancerar listiteratorn till nästa element, osv. T ex:
    { ... { time, createHearts }, { time, createCoins }, { time, createAliens }, { time, createHearst }, ... } // en lista av par {tidpunkt, funktion}

    Implementera detta. Testa att det fungerar för några spelobjekt vid ett antal givna tider. Givetvis är det lämpligt att du väljer starttiderna så att objekten hinner självterminera innan de startas på nytt.

      Vi har ännu inte använt klockan men du kan lika gärna använda en variabel frameID (eller loopIter) som du inkrementerar varje frame och jämför mot.

      Listan kan vi enklast implementera som en array av en struct {int time, "funtionspekare"}. För enkelhetens skull är det lämpligt att alla funktionspekare är av samma typ, t ex void (*function) (void), vilket är samma typ som dina create...()-funktioner.

      T ex:
      typedef struct { int time; void (*function) (void); } GameEvent; GameEvent gameEvents[] = { { 60, createHearts }, { 120, createCoins } }; int nextEvent = 0; // Låt t ex kompilatorn beräkna arrayens längd: int nGameEvents = sizeof(gameEvents) / sizeof(GameEvent);

      Inuti realtidsloopen i main()-funktionen räcker det med att lägga till 3-4 rader kod: Checka loopIter mot tidpunkten för nästa element i arrayen. Om det är dags, anropa funktionen och inkrementera nextEvent.
            Om man vill att flera events skall kunna utföras på samma tidpunkt kan du helt enkelt använda en while-loop.

      Följande utförs inuti realtidsloopen någonstans innan uppdatering och rendering av alla objekt, t ex direkt efter hanteringen av keyboard-events:
      // Handle start of game events at the specified times while(loopIter >= gameEvents[nextEvent].time && nextEvent < nGameEvents) { gameEvents[nextEvent].function(); nextEvent++; }
    main.c: ... // Definiera en struct GameEvent typedef struct { int time; void (*function) (void); } GameEvent; // Skapa en array av events GameEvent gameEvents[] = { { 0, createHearts }, { 1*60, createCoins }, { 3*60, createAliens } }; int nextEvent = 0; // Låt t ex kompilatorn beräkna arrayens längd: int nGameEvents = sizeof(gameEvents) / sizeof(GameEvent); ... int main( int argc, char* args[] ) { // Start up SDL and create window of width=800, height = 600 initRenderer(800, 600); // Create objects createBackground(); createShip(); // ta bort anropen createHeart(), createCoins(), createAliens() // för vi använder vår array gameEvents istället ... while(true) // The real-time loop { ... while( SDL_PollEvent( &e ) != 0 ) { ... } ... // Handle initiation of game events while(loopIter >= gameEvents[nextEvent].time && nextEvent < nGameEvents) { gameEvents[nextEvent].function(); nextEvent++; } ...
    hehheh

    Kommentar Det är vanligt att man vill att händelser ska initieras beroende på var spelaren (eller annat objekt) befinner sig. Det kan man lätt lösa genom att ha en tvådimensionell eller tredimensionell datastruktur som checkas baserat på spelarens position - t ex en 2D- eller 3D-array av funktionspekare, istället för vår 1D-array.

    Dynamisk minnesallokering av objekt utan malloc/new

    När vi för närvarande anropar våra create...()-funktioner är det hela tiden samma instanser som (åter-)initieras gång på gång. Våra nuvarande create-funktioner skapar t ex tre hjärtan, fyra mynt respektive tre aliens. Om vi anropar create-funktionen för aliens två gånger i rad får vi inte sex aliens, vilket skulle vara praktiskt. Ofta har man behov av att dynamiskt kunna skapa helt nya instanser, dvs kunna använda dynamisk minnesallokering.

    Med malloc(), som ju är till för dynamisk minnesallokering, kan man helt enkelt allokera nytt minnesutrymme på heapen för nya objekt och delallokera utrymmet med free() när instanserna terminerar. T ex:
    GameObject* pObj = (GameObject*) malloc( sizeof(GameObject) ); ... free(pObj);
    Men om vi inte har möjlighet att använda malloc (som för MD407) så får vi utnyttja datasegmentet istället och här följer ett exempel på hur man kan göra.

    Dynamisk allokering Om vi vet ett övre max för antalet instanser som ska kunna existera samtidigt kan vi enkelt lösa problemet genom att ha en array (allokerad i datasegmentet) med plats för motsvarande antal element, och för varje allokering av en ny instans leta upp ett ledigt element, markera det upptaget och använda dess minnesutrymme. För att hålla reda på vilka element som är lediga respektive upptagna kan vi ha en array av booleans av samma längd för att flagga elementen (true = upptaget, false = ledigt). Vi kan förpacka funktionaliteten i en funktion allocElement() som returnerar en pekare till det lediga minnesutrymmet.

    Deallokering löser vi genom att skapa en funktion freeElement(...) som helt enkelt flaggar elementet som ledigt.

    Vår egen motsvarighet till att använda malloc() respektive free() blir alltså:
    GameObject* pObj = allocElement(); ... freeElement(pObj);


    Implementation

    Låt oss nu implementera alla nödvändiga funktioner för dynamisk minnesallokering utan malloc. Detta är egentligen en alldeles utmärkt övning för er att implementera själva, men är man ovan kan det ta tid, så om du vill finns här ett färdigt förslag:
    Dynamisk minnesallokering i datasegmentet
    dynamicalloc.h // This file implements semi-dynamic allocations of type GameObjects #ifndef DYNAMIC_ALLOC_H #define DYNAMIC_ALLOC_H #include "gameobject.h" GameObject* allocElement(); // Allokerar dynamiskt minnesutrymme för ett objekt void freeElement(GameObject* pObj); // Deallokerar minnesutrymmet för ett objekt #endif // DYNAMIC_ALLOC_H
    dynamicalloc.c #include "dynamicalloc.h" #define FREE 0 // The definitions are only visible in this .c-file #define BUSY 1 typedef struct { GameObject* pObjects; // pekare till en stor statisk array bool* pSlots; // pekare till en array med en bool per element int nElements; // Antal element i arrayen int currentElement; // senaste lediga plats + 1 (för att snabba // upp sökning efter ny ledig plats } AllocData; #define SIZE 1000 // Max antal samtidigt existerande instanser static GameObject objects[SIZE]; static bool slots[SIZE]; static AllocData allocData = {objects, slots, SIZE, 0}; // Allokerar dynamiskt minnesutrymme för en instans GameObject* allocElement() { // Vi vill att alla slots skall vara initierade till FREE vid uppstart. static bool bFirstTime = true; if(bFirstTime) { bFirstTime = false; for (int i=0; i < SIZE; i++) allocData.pSlots[i] = FREE; } // Sök efter första lediga plats, men max nElements försök for(int i=0; i < allocData.nElements; i++) { int index = allocData.currentElement; allocData.currentElement = (allocData.currentElement + 1) % allocData.nElements; if(allocData.pSlots[index] == FREE) { allocData.pSlots[index] = BUSY; return &allocData.pObjects[index]; // returnera den lediga platsen } } // Ingen ledig plats hittades return 0; } // Sätter ett index till ledigt void freeElement(GameObject* pObj) { int index = pObj - &(allocData.pObjects[0]); if (index >= 0 && index < allocData.nElements) { if(allocData.pSlots[index] == BUSY) { allocData.pSlots[index] = FREE; return; } } // Hamnar vi här har något gått fel }

    Uppg. 7
    • Skapa dynamicalloc.c/.h enligt programkoden ovan.
    • Modifiera dina Create()-funktioner (createAliens, createCoins, createHearts etc.) så att de allokerar minne dynamiskt med allocElement().
    • Modifiera din kod i realtidsloopen i main()-funktionen så att minnet deallokeras med freeElement(...) för de element vars update()-metod returnerar false.
    • Testa genom att anropa create-funktionerna så tätt i tiden att många instanser existerar samtidigt. Gör detta genom att låta din datastruktur för händelser vid tidpunkter innehålla många täta anrop. T ex något i stil med:
      GameEvent gameEvents[] = { { 0, createHearts }, { 1*60, createCoins }, { 2.00f*60, createAliens }, { 2.25f*60, createAliens }, { 2.50f*60, createAliens }, { 2.75f*60, createAliens }, { 4.00f*60, createAliens }, { 4.25f*60, createAliens }, { 4.50f*60, createAliens }, { 4.75f*60, createAliens } };
    I create...()-funktionerna: För att dynamiskt allokera minne för en ny instans av ett objekt anropar du:
    GameObject* pObj = allocElement(); if (pObj == 0) // Testa att det gick att allokera. return; // Annars, avbryt create-funktionen. // initiera instansen ...

    I main()-funktionen: i realtidsloopen där vi anropar alla update()-metoder behöver vi deallokera element som meddelar att de skall tas bort, dvs som returnerar false. Dvs:
    // Update our object(s) int j=0; for(int i=0; i < nGameObjects; i++) { if( gameObjects[i]->update(gameObjects[i]) ) { // lägg tillbaks objektet i arrayen gameObjects[j++] = gameObjects[i]; } else { // deallokera elementet freeElement(gameObjects[i]); } }
    aliens.c ... #include "dynamicalloc.h" // static GameObject aliens[3]; // Ta bort den gamla statiska allokeringen void createAliens() { static GfxObject gfx; static bool bFirstTime = true; // It is unnecesary to load the image for every instance. Just do it once: if(bFirstTime) { bFirstTime = false; gfx = createGfxObject( "../alienship.png" ); } for (int i=0; i<3; i++) { // Allokera och initiera en instans GameObject* pObj = allocElement(); // allokera dynamiskt if (pObj == 0) return; init(pObj, &gfx, 100, 100, (vec2f){100, -50}, (vec2f){0.33*(i+1), 1}, 5.0f); gameObjects[nGameObjects++] = pObj; } }
    Uppg. 3

    Objekt och förlopp med sinsemellan interaktion och beroenden

    Engelskans ord "task" betyder uppgift och syftar inom datavetenskap ofta på process eller tråd. Bredden av ordets betydelse är svåröversatt till svenska så därför används begreppet nedan. I vårt sammanhang syftar uppgift (task) här på styrningen av händelseförloppet för ett objekt. Först kommer vi sammanfatta oberoende tasks och därefter gå in på hur man kan lösa beroenden.

    Oberoende tasks

    Vad vi har implementerat hittills är egentligen en form av multitasking, där en realtidsloop exekverar programkod för flera oberoende tasks i tur och ordning (skeppet, bakgrunden och övriga spelobjekt). Detta är mycket likt så kallad non-preemptive scheduling för ett operativsystem.

    Non-preemptive scheduling hos ett operativsystem innebär att varje program exekverar tills dess att de själva släpper tillbaks kontrollen till operativsystemet (kanske efter några millisekunder), som därefter exekverar nästa program en liten stund o s v. I vårt fall innebär liknelsen att realtidsloopen exekverar våra objekts update()- och render()-metoder var och en för sig i tur och ordning, oberoende av varandra och för en frame i taget. Varje enskilt objekt skulle kunna ses som ett godtyckligt enskilt program och metodanropet till update() kan liknas vid att dess programkod exekveras en iteration.

    Preemptive scheduling är motsatsen, dvs att operativsystemet självt avbryter programmen, så att inget program tillåts fullständigt blockera exekverandet av andra program ifall det inte släpper tillbaks kontrollen tillräckligt snabbt. Preemptive scheduling är en stor fördel. Dock finns det tillfällen när man inte vill avbryta ett program, t ex vid timing för kommunikation mot portar. Därav att man ofta kan ha olika prioriteter för kod som exekveras via interrupts.

    Moderna operativsystem använder preemptive scheduling. Preemptive scheduling bygger på interrupts och i labbarna kommer ni att få experimentera med interrupts.


    Beroende tasks

    Förlopp med sinsemellan beroenden
    Du börjar nu få kläm på att hantera flera oberoende förlopp (tasks). I dessa hemuppgifter skall vi nu fortsätta med beroende förlopp, dvs sådana som på något sätt interagerar med varandra. Båda fallen är ofta viktiga i många olika sammanhang, som t ex robotprogrammering och styrsystem (många motorer skall kontrolleras samtidigt och olika sensorer skall läsas av och initiera förlopp samt kontinuerligt styra andra).

    Det finns mycket att säga om hur man kan hantera interaktion och beroenden mellan objekt och deras förlopp. Objektorienterad programmering har utvecklats kontinuerligt sedan 50-talet och komponentbaserad programmering sedan 1968. Vi ger här exempel på hur beroenden kan implementeras (det finns ett otal varianter).

    Objektorientering
    Objektorientering löser ofta beroenden genom att objekten kommunicerar direkt med varandra sinsemellan. Kommunikation mellan instanser sker typiskt genom att objekten anropar metoder hos varandra. Om t ex vårt skepp kolliderar med ett mynt kan objektet för skeppet anropa en metod collect() hos myntet som talar om för myntobjektet att det skall terminera. Vidare kan t ex objektet för skeppet anropa myntets metod getPoints() för att få reda på hur mycket skeppets score ökar. Vem som bör anropa vem beror på programmerarens designval och det är ibland inte uppenbart vilket val som är lämpligast. T ex skulle objektet för myntet istället kunna meddela objektet för skeppet att de har kolliderat. Valet är ditt.

    Component-based programming
    Component-based programming är ett designmönster som vi inte har pratat om hittills och fortfarande sparsmakat lärs ut på universitet. Inom komponentbaserad programmering till skillnad från objektorientering grupperar man hellre sin programkod baserad på funktionalitet än objekttillhörighet. För den variant av komponentbaserad programmering som vi här ska kika på löses ofta beroenden med globala funktioner. Men låt oss först beskriva vad component-based programming är.

    Component-based programming har ökat i popularitet senaste åren (särskilt inom spelprogrammering) som ett svar på att objektorientering inte passar väl för alla problemtyper. Inom objektorientering representerar man olika typer av objekt med olika klasser. Klasser kan ärva andra klasser och därmed dess typ (dvs i någon mening dess egenskaper). Men i spel är det inte ovanligt att instanser av objekt emellanåt fundamentalt skall ändra egenskaper. Ett spelobjekt dör och kan inte längre styra. Ett spelobjekt får en power-up och kan inte längre bli skadat eller får nya kraftfulla egenskaper. Ett spelobjekt är stillastående och ofarligt och förvandlas av någon anledning till ett rörligt monster. I ett styrsystem för robot/bil skall plötsligt ljudet från radion slås av, utritning på instrumentpanelen slås av för vissa element men slås på för andra. Med klasser är det inte enkelt och smidigt att låta en instans av ett objekt sluta ärva från en klass (för att bli av med dess egenskaper) och börja ärva av en annan klass (för att få dess egenskaper), och antal kombinationer av klasser med olika arv kan snabbt bli ohanterligt. Det är inte omöjligt (och ofta inte ens svårt) att lösa problemet med objektorientering, men grundtanken med klasser och arv är ett verktyg som här ofta passar mindre väl genom att komplicera istället för att förenkla problemet.

    Istället delar komponentbaserad programmering upp objekts egenskaper (läs funktionalitet) i separata globala komponenter. Objekt inkluderar därefter de komponenter som de har nytta av för att representera sin sammansatta funktionalitet.

    Komponentbaserad programmering finns på olika abstraktionsnivåer och under lite olika namn, som t ex även: Component-Oriented Programming samt Component-based software engineering. Här ska vi prata om komponentbaserad programmering så som det ofta används inom spelprogrammering, dvs när det appliceras som designmönster inom ett och samma projekt och huvudsakligen för endast en applikation, vilket för spel kanske bäst beskrivs av den här informella sidan. Motsatsen är t ex MicroSofts COM-objekt och ActiveX-komponenter eller portabla komponenter inom t ex webprogrammering, där komponenterna är avsedda att vara just portabla och kunna användas för många olika program och av många olika utvecklare.

    Den kanske allra enklaste formen av komponentbaserad programmering bygger på att varje egenskap, dvs komponent, som finns motsvaras av en boolesk flagga hos objektet och kan slås på eller av. Dessutom tillkommer ofta parametrar, dvs variabler, för respektive egenskap. Varje objekt innehåller en uppsättning av alla flaggor och tillhörande variabler. Av praktiska skäl implementeras flaggorna för egenskaperna ofta som bitar i en variabel som t ex heter properties (eller flera properties-variabler om bitarna inte räcker till). Då kan egenskaper enkelt ändras hos enskilda instanser genom att bara slå på eller av bitar samt modifiera parametrarna.

    En bland många populära lösningar är att låta varje typ av objekt representeras av en och samma klass, just på grund av sin enkelhet - precis som vi har gjort med struct GameObject. Dvs klassen är ett superset av samtliga medlemmar som finns bland alla olika slags objekt.

    Logiken som agerar på egenskaperna implementeras i globala funktioner - till skillnad från i metoder per objekt. Varje funktion loopar typiskt igenom samtliga instanser och utför logiken för de instanser som har motsvarande flagga satt. T ex skulle realtidsloopen i main() kunna anropa en funktion renderAllGameObjects() som ligger i gameobjects.c, som i sin tur loopar igenom varje spelobjekt i arrayen gameObjects[] och anropar dess render()-metod om dess flagga "visible" är satt.

    Beroenden mellan objekt som består av olika komponenter löses följaktligen typiskt genom att globala funktioner ansvarar för hanterandet. En specifik global funktion, t ex för kollisionsdetektering, känner till åtminstone det som behövs hos respektive inblandat spelobjekt och påverkar spelobjekten direkt.

    Både objektorienterad programmering och component-based programming har sina för- och nackdelar och det är fördelaktigt att känna till båda. Om man använder component-based programming är det vanligt att fortfarande utnyttja en stor del objektorientering. Ju fler slags objekt man har som bygger på många olika kombinationer av funktionalitet som ofta är gemensam, desto mer aktuellt blir det att använda komponentbaserad programmering - särskilt om objekten skall kunna ändra egenskaper. Här följer en generalisering.

    ingen eller lite gemensam funktionalitet mycket gemensam funktionalitet
    få slags objekt objektorientering objektorientering med arv /           (komponentbaserad programmering)
    många slags objekt objektorientering komponentbaserad programmering

    Parametrarna för respektive komponent implementeras som sagt med variabler. Graden av generalitet och flexibilitet för komponentens funktionalitet styr valet av parametrarnas typer. Funktionspekare ger en användare av komponenten maximal flexibilitet. Exempel:

    Fåtal alternativ Större definitionsmängd Fullständig flexibilitet
    bool/enum int / float / double / array / sträng / struct / klass ... virtuell metod / funktionspekare

    I nästa uppgift ska vi låta skeppet kunna plocka våra mynt och hjärtan och kasta eller skjuta äpplen på aliens, varpå de träffade objekten avslutas. För träffade aliens, eller om skeppet träffas av aliens, skall avslutningen ske med en explosionsanimering. För sakens skull och för att det passar bra ska vi prova på att implementera kollisionsdetekteringslogiken med en mycket enkel variant inspirerad av komponentbaserad programmering. Komponenten som vi inför kopplar vi till en flagga COLL_CHECK som är 1 för spelobjekt som ska kollisionstestas. Logiken för vad som skall hända vid kollisioner baserar vi på spelobjektens egenskaper som också är representerade som komponenter (PLAYER, ENEMY, COIN, POWER_UP etc.). För att ha fullständig flexibilitet för explosionsanimeringarna använder vi funktionspekare för att välja explosionsanimering samt kunna växla till tillhörande update()-metod. Observera att om man skulle vilja använda komponentbaserad programmering fullt ut behöver man även ändra alla andra objektspecifika metoder (i vårt fall update() och render()) till generella globala metoder.

    Objekt som skapar eller terminerar andra objekt
    Det är vanligt att objekt har tillåtelse att skapa andra objekt (i vårt fall när player-objektet skapar äpplen) och objekt kan få meddelande om när de ska avslutas. Att låta ett objekt skapa andra typer av objekt är ofta oproblematiskt. Ett objekt kan skapa ett nytt objekt helt själv eller anropa en create()-funktion som implementeras av det skapade objektets .c-fil.

    Inom objektorientering använder man normalt en klasspecifik konstruktor, dvs en form av Create-funktion, som automatiskt anropas när objekt allokeras (på stacken eller heapen) för att initiera en instans, samt en klasspecifik destruktor som anropas när instansen deallokeras (från stack eller heap).

    Men... det är ofta lämpligt att objekten själva ansvarar för att faktiskt terminera samt exakt när de skall deallokeras. I vårt fall kanske ett träffat eller plockat objekt gör en speciell avslutningsanimering (t ex explosion eller initierar uppspelning av ett ljud). I ett inbyggt system kanske ett objekt som skall avslutas behöver göra några slutberäkningar, spara ner information och frigöra resurser vilket kan involvera ytterligare kommunikation med andra objekt. Principen är densamma.

    I övningsuppgifterna ovan låter vi instansers update()-metod returnera false när objekten skall deallokeras (dvs tas bort), vilket duger bra för oss här och nu.

    Kollisionsdetektering

    För kollisionsdetektering skapar man typiskt en global funktion CollisionDetection(...) som loopar igenom alla par av objekt och kollisionsdetekterar dem mot varandra.

    I en objektorienterad lösning gör man t ex så att man för varje par av objektinstanser, (A,B), som ska kollisionstestas anropar en metod hos objekt A som tar B som inparameter och vice versa. Dvs:

    // För varje par A, B som skall kollisionstestas pA->collisionDetect(pB); pB->collisionDetect(pA);
    Om t ex A är ett spelarskott och B en fiende kan pA->collisionDetect(pB) anropa pB->getPoints(), addera rätt antal poäng till spelaren och terminera A. pB->collisionDetect(pA) kan kolla att typen för A är ett spelarskott och initiera en avslutande explosionsanimering.

    Man behöver alltså kunna särskilja vilka typer av objekt som är inblandade i en given kollision (t ex player vs heart, player vs coin) för att kunna initiera rätt typ av skeende. I objektorienterade språk som Java, C++, C# m.fl. ger språket stöd för att avgöra typer i runtime, men i C är det sämre ställt. Istället är två exempel på tillvägagångssätt för att identifiera objekttypernna att antingen ha dem i separata listor eller ha en struktmedlem som talar om objektets typ. T ex:

    enum object_type { PLAYER, COIN, ALIEN, HEART, APPLE}; typedef struct tGameObject{ ... object_type type; ... } GameObject;

    Fördelen med objektorientering är flexibilitet. Nackdelen är att varje objekt måste ha en metod collisionDetect(), vilken sannolikt är mer eller mindre identisk för många snarlika typer av objekt. (Man kan visserligen försöka använda arv eller funktionspekare som pekar ut lämplig funktion av ett antal olika varianter för att minska mängden implementation.)

    Filosofin med komponentbaserad programmering är istället att bryta loss egenskaperna eller funktionaliteter (dvs komponenterna) ifrån objekttyperna och i största mån skapa global logik som agerar utefter de olika egenskaperna istället för vilka datatyper objekten är. Därmed kan man lätt hantera en mängd olika typer av objekt med olika kombinationer av egenskaper utan att behöva lägga till implementation för varje unik kombination.

    Nackdelen är att man kan få en eller ett antal stora komplicerade globala funktioner. Men en annan fördel är att gemensam logik är samlad på ett ställe i programkoden, vilket kan vara överskådligare och göra det enklare att införa modifieringar.

    Vilka egenskaper man vill indela i kan förstås variera fullständigt. I vårt spel gäller just nu att ett objekt kan vara: player, enemy, player shot, enemy shot (har vi ännu inte lagt till), poänggivande mynt eller powerup. Än så länge har vi inte implementerat några kombinationer, men man kan enkelt tänka sig objekt som både är enemy och powerup, eller mynt och powerup, samtidigt. Datastrukturen blir då t ex något i stil med:
    // properties implemented as bits typedef enum { COLL_CHECK=2, VISIBLE = 4, PLAYER=8, ENEMY=16, PLAYER_SHOT=32, ENEMY_SHOT=64, COIN=128, POWER_UP=256 } Props; typedef struct tGameObject{ ... // Properties for component-based programming style Props properties; ... } GameObject;

    Interaktion mellan våra objekt via kollisioner

    Vi ska nu låta våra objekt interagera med varandra när de kolliderar. I ett realtidsspel utgör kollisionshanteringen inte sällan en mycket central del i spellogiken. Ofta är det i samband med att objekt kolliderar som objekt och förlopp interagerar med och påverkar varandra. Här ska vi visa ett enkelt men ändå flexibelt sätt att implementera kollisionslogiken.

    Detta är sista programmeringsuppgiften för hemuppgifterna. Hemuppgifter förel. 5 presenterar endast hur man skulle kunna gå tillväga för att konvertera vårt projekt till att exekvera på labbdatorn MD407 (vilket kan vara intressant för er inför labb 5). Du kan med fördel läsa igenom det så fort du är färdig med denna uppgift 8.

    Uppg. 8
    Att låta skeppet kunna plocka upp mynt och hjärtan samt kasta/skjuta äpplen på andra objekt


    Uppg. 8a
    Som en första deluppgift, implementera att skeppet kan kasta/skjuta äpplen. (Vi väntar med kollisionsdetekteringen tills nästa deluppgift.) Du kan implementera det som du vill. Här har du en animering för ett äpple:
    med färdig png-fil att ladda in i ditt program (högerklicka och ladda ned):

    • I player.c, för skeppets update-funktion, läs av om en skjuta-/kasta-knapp är nedtryckt (t ex SDL_SCANCODE_SPACE). Om knappen är nedtryckt, anropa en funktion createPlayerShot(...), med inparametrar som du väljer, som skapar ett skott (äpple) som rör sig t ex uppåt på skärmen och terminerar när det är utanför skärmen. Du kan implementera createPlayerShot i egna filer shots.h/.c precis på liknande sätt som våra andra spelobjekt.
            För att inte äpplena skall skapas för tätt inpå varandra (dvs varje frame som knappen är nedtryckt), lägg till en ny medlem delay; till struct GameObject som initieras till t ex 20 när ett äpple avfyrats och inte tillåter skapandet av ett nytt skott förrän räknaren är 0.
    • För att terminera äpplen när de är utanför skärmen kan du i deras gemensamma update()-funktion checka positionen mot fönsterkanterna, vilka du kan avläsa med:
      // Hämta fönsterstorleken: int screenWidth, screenHeight; SDL_GetWindowSize(gWindow, &screenWidth, &screenHeight);
    gameobject.h typedef struct tGameObject{ ... // new member for shooting hearts int delay; ...
    player.c bool updateShip(GameObject* this) { ... if(this->delay > 0) this->delay--; if(state[SDL_SCANCODE_SPACE]) { if(this->delay == 0) { // Skapa ett skott. // Låt det starta alldeles ovanför skeppet vec2f pos = this->pos; pos.y -= 0.8f * (this->gfxObj.outputHeight/2); vec2f dir = {0.0f, -1.0f}; // skottets riktning - här uppåt createPlayerShot1(pos, dir); this->delay = 20; // vänta minst 20 frames innan nytt skott tillåts. } } ... }
    shots.h #ifndef SHOTS_H #define SHOTS_H #include "gameobject.h" GameObject* createPlayerShot1(vec2f pos, vec2f dir); #endif // SHOTS_H
    shots.c #include "shots.h" #include "gameobject.h" #include // för fmod() #include "dynamicalloc.h" static bool update(GameObject* this) { // update which frame in texture to show this->frame = fmod( (this->frame+this->frameSpeed), this->numFrames ); // update positions this->pos.x += this->moveDir.x * this->speed; this->pos.y += this->moveDir.y * this->speed; // Hämta fönsterstorleken: int screenWidth, screenHeight; SDL_GetWindowSize(gWindow, &screenWidth, &screenHeight); // Terminera objektet om det är helt utanför fönstret bool bOutside = false; bOutside |= this->pos.x <= -this->gfxObj.outputWidth/2; bOutside |= this->pos.x >= screenWidth + this->gfxObj.outputWidth/2; bOutside |= this->pos.y <= -this->gfxObj.outputHeight/2; bOutside |= this->pos.y >= screenHeight + this->gfxObj.outputHeight/2; if(bOutside) return false; // kolla om dags att terminera return ( (this->age++ < this->lifeTime) && !bOutside); } GameObject* createPlayerShot1(vec2f pos, vec2f dir) { static GfxObject gfxApple; static bool bFirstTime = true; if(bFirstTime) { bFirstTime = false; // Create a GfxObject for the apple image gfxApple = createGfxObject( "../apples25st.png" ); } // Allokera och initiera en instans GameObject* pObj = allocElement(); // allokera dynamiskt if (pObj == 0) return 0; pObj->gfxObj = gfxApple; pObj->gfxObj.outputWidth = 50; pObj->gfxObj.outputHeight = 50; pObj->pos = pos; pObj->moveDir = dir; pObj->speed = 5; pObj->angle = 0; // unused pObj->angleSpeed = 0; // unused pObj->scale = 1.0f; pObj->scaleSpeed = 0; // unused pObj->update = update; // our own update in this file pObj->render = render; // From gameobjects.h pObj->frame = 0; pObj->frameSpeed = 1; pObj->numFrames = 25; pObj->age = 0; pObj->lifeTime = 10*60; // 10 seconds gameObjects[nGameObjects++] = pObj; return pObj; }
    Uppg. 8a
    (Vill du hämta fullständig projektlösning, använd den för uppg. 8b nedan.)


    Uppg. 8b
    Kollisionsdetektering

    Vi ska låta aliens explodera om de blir träffade av ett äpple. Vi ska låta skeppet explodera om det kolliderar med en alien. Vi skall också låta skeppet kunna plocka hjärtan och mynt.

    Typiskt för kollisionsdetektering är att man har en nästlad loop (i två nivåer) som loopar igenom alla spelobjekt som potentiellt kan kollidera och testar var och ett parvis mot varandra:
    for(int i=0; i < nGameObjects; i++) { GameObject* pA = gameObjects[i]; for(int j=0; j < nGameObjects; j++) { GameObject* pB = gameObjects[j]; if(collide(pA, pB)) { ...

    Vi behöver också kunna särskilja på kollisioner mellan olika slags spelobjekt för att initiera rätt skeenden - här inte nödvändigtvis baserat på deras datatyper (vi använder just nu ändå bara en och samma datatyp GameObjects för alla objekt), utan snarare på vilka sorts spelobjekt det rör sig om. Vi kan specificera alla olika sorter med en enum och ha en variabel i struct GameObjects som talar om vilken sort objektet tillhör. Dock, för att öka generaliteten kan vi representera varje sort med en bitflagga. Då blir det teoretiskt möjligt att låta ett och samma objekt tillhöra mer än en sort (vilket passar komponentbaserad programmering). Vi ska också införa två extra flaggor: en som markerar om objektet skall kollisionsdetekteras överhuvudtaget (COLL_CHECK) samt en flagga (TERMINATING) som vi använder inuti vår globala kollisionsdetekteringsfunktion.

    Explosioner: Vi vill initiera explosionsanimeringar om vårt skepp blir träffat av en alien eller om ett äpple träffar en alien. För att enkelt kunna bestämma per instans hur dess explosionsanimering ser ut låter vi struct GameObject innehålla en metod setExplosion() som anropas för att initiera rätt explosion. Då har vi full flexibilitet att både dela implementation mellan olika objekt samt ha unika varianter. Funktionen setExplosions() byter objektets animering och även update()-funktion (eftersom objektet t ex inte längre ska kunna flytta sig). Update()-funktionen spelar explosionsanimeringen en gång och avslutar därefter automatiskt objektet.

  • Ladda ned den här explosionsanimeringen och lägg i ditt projekt tillsammans med de övriga animeringarna:
  • Lägg till detta till din fil gameobjects.h:
    gameobject.h // properties implemented as bit booleans typedef enum {TERMINATING=1, COLL_CHECK=2, PLAYER=8, ENEMY=16, PLAYER_SHOT=32, ENEMY_SHOT=64, COIN=128, POWER_UP=256 } Props; typedef struct tGameObject{ ... Props properties; ... // methods: void (*setExplosion)(struct tGameObject* gameobj); ... } GameObject;
    Som ni ser lade vi även till sorten ENEMY_SHOT samt POWER_UP, när vi ändå håller på.

    Att testa spatialt (rumsligt) överlapp mellan objekt: Om man har objekt som kan rotera och är ungefär lika höga som breda är det enklaste att göra ett test som approximerar varje objekt med en cirkel och testar överlapp mellan två cirklar. Så här kan du göra för att beräkna ett objekts cirkelcentrum och radie, där pObj är en pekare till en instans av typen GameObject:

    vec2f center = pObj->pos; float radius = sqrt( pObj->gfxObj.outputHeight*pObj->gfxObj.outputHeight + pObj->gfxObj.outputWidth*pObj->gfxObj.outputWidth)*0.5f;
    För exempel på hur du kan implementera exakt kollisionstest (för objekt som inte är roterade), titta här: http://lazyfoo.net/tutorials/SDL/index.php .

  • Tanken med denna uppgift är att visa ett sätt hur man lätt kan implementera kollisionshanteringen. Därför följer här hela lösningen. Skapa filerna collision.h/c-filer och inför följande i respektive filer:
  • aliens.c #include "collision.h" ... static void init(... ... pObj->setExplosion = setExplosion1; pObj->properties = COLL_CHECK | ENEMY; ...
    coin.c static void init( ... pObj->lifeTime = 5*600000; // so we have time to take it pObj->properties = COLL_CHECK | COIN; pObj->setExplosion = 0; // no explosion animation
    heart.c static void init(... ... pObj->lifeTime = 6*600000; // so we have time to take it pObj->properties = COLL_CHECK | POWER_UP; pObj->setExplosion = 0; // no explosion animation
    player.c #include "collision.h" ... void createShip() { ... ship.setExplosion = setExplosion1; ship.properties = COLL_CHECK | PLAYER;
    shots.c GameObject* createPlayerShot1(... ... pObj->properties = COLL_CHECK | PLAYER_SHOT; pObj->setExplosion = 0;
    main.c #include "collision.h" ... // Anropa CollisionDetection() mellan update- och render-looparna: // Update our object(s) ... CollisionDetection(gameObjects, &nGameObjects); // Render our object(s) - background objects first, and then forward objects for( ...
    collision.h #ifndef COLLISION_H #define COLLISION_H #include "gameobject.h" void CollisionDetection(GameObject* gameObjects[], int *nGameObjects); void setExplosion1(GameObject* gameobj); void setExplosion2(GameObject* gameobj); #endif //COLLISION_H
    collision.c #include "collision.h" #include "vecmath.h" #include "dynamicalloc.h" #include "player.h" static inline float getRadius(GameObject* pObj) { float radius = sqrt( pObj->gfxObj.outputHeight*pObj->gfxObj.outputHeight + pObj->gfxObj.outputWidth*pObj->gfxObj.outputWidth)*0.25f; return radius; } static inline bool collide(GameObject* pA, GameObject* pB) { vec2f centerA = pA->pos; vec2f centerB = pB->pos; float dx = (centerA.x - centerB.x); float dy = (centerA.y - centerB.y); float dist = sqrt(dx*dx + dy*dy); return (dist <= getRadius(pA) + getRadius(pB)); } static bool update(GameObject* this) { // kolla om animeringen är färdig bool bFinished = this->frame+this->frameSpeed >= this->numFrames; // update which frame in texture to show this->frame = fmod( (this->frame+this->frameSpeed), this->numFrames ); // Hack to make player resurrect after dying. if( bFinished && (this->properties & PLAYER) ) createShip(); return !bFinished; } void setExplosion1(GameObject* pObj) { static GfxObject expl; static bool bFirstTime = true; if(bFirstTime) { bFirstTime = false; // Create a GfxObject for the explosion animation expl = createGfxObject( "../explosion1_10.png" ); } int w = pObj->gfxObj.outputWidth; int h = pObj->gfxObj.outputHeight; pObj->gfxObj = expl; pObj->gfxObj.outputWidth = w*1.5f; pObj->gfxObj.outputHeight = h*1.5f; pObj->frame = 0; pObj->frameSpeed = 0.3f; pObj->moveDir = (vec2f){0,0}; pObj->numFrames = 10; pObj->update = update; } void CollisionDetection(GameObject* gameObjects[], int * outNumGameObjects) { int nGameObjects = *outNumGameObjects; for(int i=0; i < nGameObjects; i++) { GameObject* pA = gameObjects[i]; if( !(pA->properties & COLL_CHECK) ) continue; // A is not collidable // We want to test: PLAYER and PLAYER_SHOT against the other types if( !(pA->properties & (PLAYER | PLAYER_SHOT)) ) continue; for(int j=0; j < nGameObjects; j++) { GameObject* pB = gameObjects[j]; if( !(pB->properties & COLL_CHECK) ) continue; // B is not collidable // Testa A och B mot varandra if(collide(pA, pB)) { if(pB->properties & (ENEMY | ENEMY_SHOT) ) { // Player/playershot can hurt several enemies at once, // so don't turn off collision detection yet. pA->properties |= TERMINATING; // flag object as killed. pB->properties |= TERMINATING; // flag other obj as killed // disable further collision check for the enemy. pB->properties &= ~COLL_CHECK; } if( (pA->properties & PLAYER) && (pB->properties & (COIN | POWER_UP)) ) { // Powerup/coin kan bara plockas av en spelare åt gången. // Därför, slå av dess kollisionsdetektering pB->properties &= ~COLL_CHECK; pB->properties |= TERMINATING; // kill coin/powerup // ... add points or power-up capabilities } } } } // Do something with each of the newly terminating objects. // E.g., initiate termination animation or remove them. int j=0; for(int i=0; i < nGameObjects; i++) { GameObject* pObj = gameObjects[i]; gameObjects[j++] = gameObjects[i]; // lägg tillbaks objektet i arrayen if( pObj->properties & TERMINATING ) { // clear the terminating and coll_check flags pObj->properties &= ~(TERMINATING | COLL_CHECK); if( pObj->setExplosion ) pObj->setExplosion(pObj); else { freeElement(pObj); // deallokera objektet j--; // minska antal objekt } } } *outNumGameObjects = j; // uppdaterar arrayens storlek }
    hehheh
  • Kompilera och testa:
  • Uppg. 8c
    Lägg till Powerup-funktionalitet

    Just nu är våra hjärtan flaggade som powerups. Men inget händer när skeppet plockar dem. Anledningen är att vi ännu inte har lagt till någon motsvarande logik i kollisionshanteringen. Här är ett utdrag ur koden i collision.c som visar var du lägger till det:

    collision.c void CollisionDetection(... ... if(collide(pA, pB)) { ... if( (pA->properties & PLAYER) && (pB->properties & (COIN | POWER_UP)) ) { ... // ... add points or power-up capabilities --- HÄR ---
    Lägg till så att följande inträffar om en powerup har plockats av skeppet:
    •   Skeppets hastighet pA->speed multipliceras med 1.6f.
    •   Skottens hastighet ökar med en faktor 1.4.
    •   Tiden mellan skott minskar (t ex med en faktor 0.66).

    Inför dessa två nya medlemsvariabler i struct GameObjects:

    typedef struct tGameObject{ ... // new members for powerups - used by player int delayTime; // önskad tid mellan skott float shotSpeed; // skottens hastighet ...

    I player.c, där skotten skapas, kan du kontrollera hastighet och delay med:
    vec2f dir={0.0f*this->shotSpeed, -1.0f*this->shotSpeed}; createPlayerShot1(pos, dir); this->delay = this->delayTime;

    bool updateShip(GameObject* this) { ... if(state[SDL_SCANCODE_SPACE]) { if(this->delay == 0) { // Skapa ett skott. // Låt det starta alldeles ovanför skeppet vec2f pos = this->pos; pos.y -= 0.8f * (this->gfxObj.outputHeight/2); vec2f dir = {0.0f*this->shotSpeed, -1.0f*this->shotSpeed}; createPlayerShot1(pos, dir); this->delay = this->delayTime; } } return true; } void createShip() { ... ship.delayTime = 20; ship.shotSpeed = 1; ... }
    if(collide(pA, pB)) { ... if( (pA->properties & PLAYER) && (pB->properties & (COIN | POWER_UP)) ) { // Powerup/coin kan bara plockas av en spelare åt gången pB->properties &= ~COLL_CHECK; // Därför, slå av kollisionsdetektering pB->properties |= TERMINATING; // kill coin/powerup // ... add points or power-up capabilities if(pB->properties & POWER_UP) { pA->speed *= 1.6f; pA->delayTime /= 1.5f; pA->shotSpeed *= 1.4f; } } ...