Maskinorienterad Programmering / Programmering av Inbyggda System

Hemuppgifter C-programmering

C - Föreläsning 2.

I förra föreläsningens hemuppgifter övade ni på att skriva ut till konsolen. För mer avancerad output är detta dock begränsande - dels p g a att vi är hänvisade till ascii-tecken, och dels pga att på t ex MacOS är fonten default satt till en font med varierande teckenvidd för olika bokstäver, vilket gör det krångligare att beräkna var i x-led våra utskrifter hamnar.
      Därför skall vi nu istället rita ut till en grafisk display. På labdatorn MD407 kommer ni att kommunicera direkt med styrenheten för grafikdisplayen via portar på fixa minnesadresser. På operativsystem tillåts detta typiskt inte (t ex Windows, MacOS, Linux). Dessutom vill vi typiskt rita ut till ett fönster som hanteras av operativsystemet. För detta måste man accessa specifika funktioner i operativsystemet. Funktionerna varierar mellan operativsystem. På Mac kan man t ex använda Cocoa och på Windows t ex Win32. I dessa hemuppgifter vill vi ha så operativsystemsoberoende lösningar som möjligt och det är även ett vanligt önskemål i verkligheten. Därför kommunicerar man typiskt hellre med API:er (som abstraherar bort OS-beroendet) som kommunicerar med operativsystemet, som kommunicerar med drivrutiner som kommunicerar med hårdvaran. Man kan använda färdiga libraries som SDL eller wxWidgets som båda har C-interface. Vi har valt SDL som API. SDL stödjer Windows, Mac OS X, Linux, iOS och Android. SDL är populärt och används av ett flertal ledande spelföretag, t ex Valve. (SDL är även för oss ett pedagogiskt bättre val då det inte abstraherar bort realtidsloopen och tillåter ett mycket lättviktigt användande i form av ett fåtal anrop.)
      För spel behöver vi även kunna känna av user-input, som mus och tangentbord. På labdatorn MD407 kommunicerar ni direkt med en numerisk keypad via portar. Via standardbiblioteket <stdio.h> så har C inbyggt stöd för att ta emot tecken (getchar()) och även hela strängar (scanf()) som matas in via tangentbordet. Detta bygger på att C-kompilatorn vet vilket operativsystem det skall kompilera mot. Eftersom MD407 saknar operativsystem, och typiskt även tangentbord, så fungerar inte det inbyggda stödet. Med lite pill och tillägg av egna funktioner kan man dock få getchar() och scanf() (och även printf()) att fungera - förutsatt att det finns ett inkopplat tangentbord eller motsvarande.
      I spel vill man vanligen kunna känna av vilka tangenter som är nedtryckta vid en viss tidpunkt (ibland är flera nedtryckta samtidigt). C har inget inbyggt stöd via standardbiblioteken för att känna av en tangents status. På Windows kan vi använda GetAsyncKeyState() som inkluderas via Windows.h. På Mac kan man använda Cocoa eller det gamla Carbon men båda är en aning bökigare. SDL har dock operativsystemsoberoende stöd, via funktionen SDL_GetKeyboardState(), vilket vi kommer använda oss av.

Uppg. 1
Tanka ner rätt startup-projekt nedan. Kompilera och testkör.
På Window:
Startup-projekt för CodeLite på Windows. (Du behöver inte installera SDL).

På Linux:
Som Linux-användare är du van att lösa problem på egen hand. Se till att installera SDL 2.0, SDL_ttf 2.0 samt SDL_image 2.0. Kopiera källkoden från startupprojektet för Windows eller Mac.

På Mac:
Tanka ned Startup-projekt för CodeLite på Mac. Innan du testkompilerar projektet så måste vi installera SDL2, SDL2_image och SDL2_tiff. Det gör du såhär:
Kopiera (inte flytta) filerna SDL2.framework, SDL2_image.framework och SDL2_ttf.framework som ligger i katalogen SDLProject/ i startupprojektet, till /Library/Frameworks/ (obs, ej /System/Library/Frameworks och ej heller "user"/Library/Frameworks). Katalogen /Library/Frameworks ligger alltså i roten på din Mac's filsystem. Med finder hittar du /Library/Frameworks/ så här:


SDL

  • Länk till SDL dokumentation: https://wiki.libsdl.org/
  • Tutorials för SDL för den intresserade.
  • Uppg. 2
    Vi skall mjukstarta...
    1. Lägg till en bakgrundsbild.
      Det finns en fil som heter background.jpg som du kan använda. Gör som för ship och kom ihåg att även sätta dess outputWidth och outputHeight. Lägg till utritningen av bakgrunden alldeles före utritninten av skeppet.
      ... GfxObject ship, background; ... // Create an object ... // Create the background object background = createGfxObject( "../background.jpg"); background.outputWidth = 800; background.outputHeight = 600; ... // Render our object(s) - background objects first, and then forward objects (like a painter) renderGfxObject(&background, 400, 300, 0, 1.0f); renderGfxObject(&ship, 400, 300, 0, 1.0f); renderText("Hello World!", 300, 150); ...
    2. Lägg till så att bakgrunden sakta roterar.
      (Övningsmoment: Förstå hur du skapar nytt oberoende förlopp. Förstå var du lämpligen skapar din nya variabel.)
      Använd en variabel angle som är en double (med en int kan du inte rotera tillräckligt långsamt). Öka den med ett lågt värde, t ex 0.02, varje frame (dvs loop). Med fmod() kan du göra modulo 360 på en double så att värdet wrappar runt. Skicka in din variabel angle i renderGfxObject(). Sista parametern som är en skalfaktor ändrar du lämpligen från 1.0 till c:a 1.8.
      ... // Create the background object background = createGfxObject( "../background.jpg" ); background.outputWidth = 800; background.outputHeight = 600; double angle = 0; while(true) // The real-time loop { ... // Render our object(s) - background objects first, and then forward objects (like a painter) angle = fmod(angle+0.02, 360); renderGfxObject(&background, 400, 300, angle, 1.8f); renderGfxObject(&ship, 400, 300, 0, 1.0f); renderText("Hello World!", 300, 150); ...
    3. Lägg till så att bakgrunden även sakta zoomas.
      ... // Create the background object background = createGfxObject( "../background.jpg" ); background.outputWidth = 800; background.outputHeight = 600; double angle = 0; float scale = 1.8f; while(true) // The real-time loop { ... // Render our object(s) - background objects first, and then forward objects (like a painter) angle = fmod(angle+0.02, 360); scale += 1.0/2500.0; renderGfxObject(&background, 400, 300, angle, scale); renderGfxObject(&ship, 400, 300, 0, 1.0f); renderText("Hello World!", 300, 150); ...
    4. Lägg till så att du kan styra skeppet upp/ner/vänster/höger med datorns piltangenter (eller hur du själv föredrar att styra). För att göra detta behöver man känna till hur man läser av tangenters status i SDL. Man gör det enligt följande. Vid början av programmet (efter initiering av SDL och före realtidsloopen) hämtar man en pekare till en vektor med ett state per tangent:
      // get pointer to key states const Uint8 *state = SDL_GetKeyboardState(NULL);
      Därefter kan du närsomhelst (t ex inne i realtidsloopen) läsa av key states med:
      if (state[SDL_SCANCODE_RIGHT]) { // Right-Arrow key is pressed. }
      Piltangenterna heter:
      SDL_SCANCODE_RIGHT, SDL_SCANCODE_LEFT, SDL_SCANCODE_UP, samt SDL_SCANCODE_DOWN. Se här: https://wiki.libsdl.org/SDL_Scancode
      (Övningsmoment: Pekare. Array. Förståelse för inmatningsenhet som tangentbord. Principen att man läser av states gäller även MD407:ans numeriska tangentbord.)
    ... // Create an object ship = createGfxObject( "../ship.png" ); ship.outputWidth = 200; ship.outputHeight = 200; float x = 400, y = 300, speed = 3; while(true) // The real-time loop { ... SDL_SetRenderDrawColor( gRenderer, 0x33, 0x33, 0x33, 0xFF ); SDL_RenderClear( gRenderer ); // Update our object(s) angle = fmod(angle+0.02, 360); scale += 1.0/2500.0; if (state[SDL_SCANCODE_RIGHT]) x = (x+speed >= 799) ? 799 : x+speed; if (state[SDL_SCANCODE_LEFT]) x = (x-speed <= 0) ? 0 : x-speed; if (state[SDL_SCANCODE_DOWN]) y = (y+speed >= 599) ? 599 : y+speed; if (state[SDL_SCANCODE_UP]) y = (y-speed <= 0) ? 0 : y-speed; // Render our object(s) - background objects first, and then forward objects (like a painter) renderGfxObject(&background, 400, 300, angle, scale); renderGfxObject(&ship, x, y, 0, 1.0f); renderText("Hello World!", 300, 150); ...
    #include "renderer.h" #include <stdio.h> #include <math.h> GfxObject ship, background; void close(); int main( int argc, char* args[] ) { // Start up SDL and create window of width=800, height = 600 initRenderer(800, 600); // Create an object ship = createGfxObject( "../ship.png" ); ship.outputWidth = 200; ship.outputHeight = 200; int x = 400, y = 300, speed = 3; // Create the background object background = createGfxObject( "../background.jpg" ); background.outputWidth = 800; background.outputHeight = 600; double angle = 0; float scale = 1.8f; const Uint8 *state = SDL_GetKeyboardState(NULL); // get pointer to key states 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, red=0x33, blue=0x33, green=0x33, alpha=0xff. 0=minimum, 0xff=maximum. // Alpha is transparency: 0 = fully transparent, 0xFF = fully opaque. However, actual use of transparency requires further settings. SDL_SetRenderDrawColor( gRenderer, 0x33, 0x33, 0x33, 0xFF ); SDL_RenderClear( gRenderer ); // Update our object(s) angle = fmod(angle+0.02, 360); scale += 1.0/2500.0; if (state[SDL_SCANCODE_RIGHT]) x = (x+speed >= 799) ? 799 : x+speed; if (state[SDL_SCANCODE_LEFT]) x = (x-speed <= 0) ? 0 : x-speed; if (state[SDL_SCANCODE_DOWN]) y = (y+speed >= 599) ? 599 : y+speed; if (state[SDL_SCANCODE_UP]) y = (y-speed <= 0) ? 0 : y-speed; // Render our object(s) - background objects first, and then forward objects (like a painter) renderGfxObject(&background, 400, 300, angle, scale); renderGfxObject(&ship, x, y, 0, 1.0f); renderText("Hello World!", 300, 150); // This function updates the screen and also sleeps ~16 ms or so (based on the screen's refresh rate), // because we used the flag SDL_RENDERER_PRESENTVSYNC in function initRenderer() SDL_RenderPresent( gRenderer ); } close(); //Free allocated resources return 0; } void close() { //Preferably, you should free all your GfxObjects, by calls to freeGfxObject(GfxObject* obj), but you don't have to. freeGfxObject(&ship); closeRenderer(); //Free resources and close SDL }

                       
    Om du känner för det, byt därefter ut skeppet och bakgrunden enligt dina preferenser. Skepp och bakgrundsbild är hämtad härifrån:
    Skepp: http://pics-about-space.com/space-ship-sprite?p=4#
    Bakgrund: http://wallpapershome.com
    (Personligt, icke-kommersiellt bruk är normalt alltid tillåtet i Sverige.)


    Uppg. 3
    Pekare, portar och arrayer
    (För den avancerade studenten... mycket av det vi gör nedanför är orealistiskt och bör hellre göras mha structs. Men lugn... vi kommer till det nästa vecka.)
    1. I förra uppgiften skapade du en variabel för x-positionen och en för y-positionen som du uppdaterar när piltangenterna trycks. Modifiera nu din kod så att du istället skapar varsin pekare till x- resp. y-variabeln en gång i början (innan realtidsloopen börjar). Ändra nu så att tangentbordsuppdateringen använder pekarna istället för x- och y-variablerna direkt.
      int *px = &x; ... *px = ... *px + speed ...;
      ... ship = createGfxObject( "../ship.png" ); ship.outputWidth = 200; ship.outputHeight = 200; int x = 400, y = 300, speed = 3; int *px = &x, *py = &y; ... while(true) // The real-time loop { ... if (state[SDL_SCANCODE_RIGHT]) *px = (*px+speed >= 799) ? 799 : *px+speed; if (state[SDL_SCANCODE_LEFT]) *px = (*px-speed <= 0) ? 0 : *px-speed; if (state[SDL_SCANCODE_DOWN]) *py = (*py+speed >= 599) ? 599 : *py+speed; if (state[SDL_SCANCODE_UP]) *py = (*py-speed <= 0) ? 0 : *py-speed; ...
    2. Inför nu så att även skeppets rotation och bakgrundens scale kan modifieras via tangentbordet och via pekare som för x- och y-positionen. Använd de tangenter du tycker passar bäst.
      Knapparna W och S kan t ex läsas av med SDL_SCANCODE_W samt SDL_SCANCODE_S (du fattar principen för SDL Scan codes). Lämpligt är även att införa variabler för rotationshastighet och scale-hastighet så att du lätt kan modifiera dessa till lagom inställningar.
    ... // Create an object ship = createGfxObject( "../ship.png" ); ship.outputWidth = 200; ship.outputHeight = 200; int x = 400, y = 300, speed = 3; double shipAngle = 0, shipAngleSpeed = 90.0/360.0; // Note: Without ".0" // this is integer division = 0. int *px = &x, *py = &y; double *pShipAngle = &shipAngle; // Create the background object background = createGfxObject( "../background.jpg" ); background.outputWidth = 800; background.outputHeight = 600; double angle = 0; float scale = 1.8f, scaleSpeed = 1.0/100.0; float *pScale = &scale; ... while(true) // The real-time loop { ... // Update our object(s) ... if (state[SDL_SCANCODE_W]) *pScale = *pScale + scaleSpeed; if (state[SDL_SCANCODE_S]) *pScale = (*pScale - scaleSpeed <= 0) ? 0 : *pScale-scaleSpeed; if (state[SDL_SCANCODE_A]) *pShipAngle = fmod(*pShipAngle - shipAngleSpeed, 360.0); if (state[SDL_SCANCODE_D]) *pShipAngle = fmod(*pShipAngle + shipAngleSpeed, 360.0); // Render our object(s) - background objects first, and then forward objects renderGfxObject(&background, 400, 300, angle, scale); renderGfxObject(&ship, x, y, shipAngle, 1.0f); ...

    Uppg. 3

    Att använda pekare på det här sättet var egentligen onödigt. Vi kan såklart uppdatera x-, y-, rotation- och scale-variablerna direkt istället för via pekare. Men vi gjorde det för övnings skull. Rensa upp din kod genom att ta bort dina införda pekare och uppdatera variablerna direkt istället.

    1. Absolutadressering: Portar ligger på fixa minnesadresser. Operativsystem tillåter normalt inte skrivning/läsning till minnet var som helst, så vi ska skapa två låtsasportar för övnings skull som ligger på samma adresser som två av våra variabler (till vilka vi såklart har tillåtelse att skriva och läsa).
          Definiera en in- och utport INUTPORT_X och INUTPORT_Y som läser från och skriver till skeppets x- och y-variabler. Gör en funktion som anropas i varje iteration och uppdaterar skeppets position. Förslagsvis kan du låta funktionen skaka skeppet som om det träffas av "gravitationsvågor" från gasmolnet med slumpmässiga intervall och längder.

      För att kunna definiera portarna till skeppets x- och y-adresser så måste du lägga skeppets x- och y-variabler som globala variabler, dvs utanför main()-funktionen och ovanför din skak-funktion, precis som ship och background. Anledningen är att annars är x- och y-variablerna inte kända inuti skak-funktionen och alltså heller inte deras adresser.

      Från föreläsningen kan du se principen för hur en port lämpligen definieras. (OBS - även om det verkar omständigt så är det så vi vill att ni gör på tentan, för att det generellt sett blir lättare att modifiera koden i efterhand.) Dvs:
      Skapa typen port32ptr mha typedef och volatile. Definiera INUTPORT_X_ADDR till adressen för x-variabeln (ej pekarvariabeln som du skapade i (a)-uppgiften). Definiera slutligen INUTPORT_X. Gör även allt motsvarande för INUTPORT_Y.

      Slumpmässighet kan du åstadkomma med rand() som du får inkludera via stdlib.h.

      Det finns givetvis många olika lösningar. Här är först ett enkelt exempel och sedan ett lite intressantare exempel:

    ... #include <math.h> #include <stdlib.h> // för rand() typedef volatile int* port32ptr; #define INUTPORT_X_ADDR &x #define INUTPORT_X *((port32ptr)INUTPORT_X_ADDR) #define INUTPORT_Y_ADDR &y #define INUTPORT_Y *((port32ptr)INUTPORT_Y_ADDR) GfxObject ship, background; void close(); int x = 400, y = 300, speed = 3; // Flytta x,y hit (speed spelar ingen roll) int t = 0; bool bShake = false; int shakeStop = 0; void writePorts() { // 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) { INUTPORT_X += 2* ((t % 3) - 1); INUTPORT_Y += ((rand() % 3) - 1); } if( bShake && (t >= shakeStop) ) { bShake = false; } t++; } ... while(true) // The real-time loop { ... // Update our object(s) writePorts(); ...
    ... int t = 0; bool bShake = false; int shakeStop = 0, shakeStart = 0; void writePorts() { // om bShake == false, initiera shake med sannolikhet 1 per 60 frames. // Sätt bShake = true; // Sätt shakeStop till t + (30 till 50) frames // om (bShake && t < shakeStop) // Låt amplituden minska linjärt från shakeStart till shakeStop // Addera omväxlande positivt resp negativt varannan frame till x // Addera med en en 4:e-del av amplituden till y, // men slumpmässigt positivt resp negativt. // om (bShake && t >= shakeStop) // sätt bShake = false; if( bShake == false && ((rand() % 60)==1) ) { bShake = true; shakeStart = t; shakeStop = t + (rand() % 20) + 30; } if( bShake && t < shakeStop) { int amplitude = 6 - 5 * ((float)t-shakeStart) / (float)(shakeStop - shakeStart); INUTPORT_X += amplitude * ((t % 3) - 1); INUTPORT_Y += (amplitude/4) * ((rand() % 3) - 1); } if( bShake && (t >= shakeStop) ) { bShake = false; } t++; }

    Uppg. 3
    1. Pekare som inputparameter: Föregående uppgift var som sagt ett låtsasexempel på portar. Tag bort portarna och återställ gärna skeppets x- och y-variabler till lokala variabler. Modifiera nu din skak-funktion så att den tar in skeppets x- och y-variabler som parametrar (på något sätt) och uppdaterar värdet av dessa. Detta är ett betydligt mer realistiskt sätt att modifiera variabler mha en funktion.

      Kika i början av slidsen för C-föreläsning 2. Där hittar du ett exempel: inc(&var1, &var2);

      ... void shake(int *x, int *y) { ... *x += ... *y += ... ... } ... while(true) // The real-time loop { ... // Update our object(s) shake(&x, &y); ...
    2. Arrayer: Skapa en funktion som tar in en sträng och kastar om elementen så att den blir baklänges.
      (För att testa din funktion, skriv ut strängen med renderText(...) på skärmen och låt texten flippa ordning t ex varannan sekund.)

      Om du skapar en sträng på det här sättet:

      char *str = "Hello World";

      så kommer den att ligga i skrivskyddat strängliteralminne för de flesta operativsystem och du får alltså inte kasta om dess element. En array som vi skapar själva ligger dock alltid i skrivbart minne:

      char str[] = "Hello World";

    För att slippa allokera något temporärt minne (på heapen eller stacken) så väljer vi att modifiera strängen in-place. Enklast är då att swappa 1:a och sista elementet, därefter 2:a och näst sista, osv.
    ... 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; } } ... char strang[] = "Hello World!"; int loopIter = 0; while(true) // The real-time loop { ... if( (loopIter % 100) == 99 ) vandStrang(strang); renderText(strang, 300, 150); ...