Gå till innehållet

Kapitel 8 - MonoGame

Introduktion till kapitlet

Vi ska nu göra en ny sorts grafiska program med bilder, ljud och möjlighet att kontrollera programmen med tangentbordet och musen. Till vår hjälp har vi MonoGame, ett verktyg som underlättar när vi ska skapa våra program som mestadels kommer att vara spel.

Vad är MonoGame?

MonoGame är ett ramverk för att skapa spel, både i 2D och 3D. Det innebär att MonoGame har en samling med metoder, variabeltyper och andra tillbehör som gör det lätt för oss att rita 2D-grafik, kolla vilka tangenter på tangentbordet som är nedtryckta, ta reda på var på skärmen musen pekar, spela upp ljud med mera. Trots att MonoGame har stöd för 3D-spel så kommer vi endast att skapa 2D-spel eftersom det är ganska så mycket lättare.

MonoGame är en vidareutveckling av XNA som utvecklades av Microsoft. MonoGame är skapat så att alla program och all kod som skrevs med XNA ska fungera i MonoGame utan några ändringar och därför hänger många namn med orden Microsoft och XNA kvar trots att Microsoft inte har någonting med MonoGame att göra. Spel gjorda i MonoGame kan köras på datorer, mobiltelefoner och spelkonsoler, vi kommer dock att endast göra spel för datorer i denna bok.

Öppen källkod

MonoGame är skapad med öppen källkod, eller open source som det heter på engelska. Detta betyder att vem som helst kan få se hur MonoGame är programmerat och MonoGame låter dessutom alla som vill vara med och hjälpa till för att göra MonoGame ännu bättre.

Om du vill se koden bakom MonoGame så kan du gå in på https://github.com/MonoGame/MonoGame.

En viktig del av MonoGame är dess Content Pipeline. Detta är ett program som konvertar alla innehållsfiler, d.v.s. alla ljud- och bild-filer du vill ha i ditt spel, till ett format med filändelsen xnb. När bilder konverteras till xnb-filer så sparas de på ett sätt som går snabbt för datorns grafikkort att använda när spelet körs.

Installera MonoGame

Du kan ladda ner MonoGame från www.monogame.org om du går till menyalternativet ”Downloads”. Ladda ner MonoGame for Visual Studio så får du den version som är gjord för Windows. Se till att stänga av Visual Studio innan du installerar MonoGame, om du skulle få problem med MonoGame någon gång så kan du testa att installera om det när Visual Studio är avstängt.

Installation av MonoGame

Du behöver inte välja något annat än standardalternativen under denna installation, när installationen är klar kan du öppna Visual Studio för att börja arbeta med MonoGame.

Version av MonoGame

Denna bok skrevs när MonoGame 3.6 var den senaste utgåvan vilket den inte längre är. Det kan finnas några mindre skillnader, framförallt i Content Pipeline-programmet, i de nya utgåvorna. Om det skiljer sig alltför mycket så går det bra att installera MonoGame 3.6 istället för de nyare utgåvorna.

Ett första program med MonoGame - Rita en bild

Vi ska nu göra Monogames motsvarighet till ett Hello World-program; ett program där vi ritar ut en bild i programfönstret. Börja med att skapa ett nytt projekt men välj MonoGame Windows Project istället för Console Application som vi alltid valt hittills.

Hitta de olika typer av MonoGame-program som du kan skapa

Om du väljer MonoGame i listan till vänster så ser du bara de typer av MonoGame-program som du kan skapa. För att se alla typer av C#-program som du kan skapa, inklusive konsolprogrammen som vi gjort hittills i boken, så kan du trycka på Visual C#.

Skapa ett MonoGame-projekt

Du kan direkt testa att kompilera och köra programmet och kommer då att få upp ett fönster med blå bakgrund. Till skillnad från våra tidigare program så får vi två kodfiler i vårt projekt från början, en som heter Game1.cs och en som heter Program.cs. Tidigare har vi skrivit all vår kod i Program-filen, men när vi jobbar med MonoGame så kommer vi inte att skriva någonting där. Koden som redan finns i Program-filen innehåller all kod den behöver för att starta igång vårt MonoGame-program. Vi kommer istället att skriva mycket kod i Game1-filen som kommer förberedd med några metoder. Du kan ändra namn på Game1-filen om du vill genom att högerklicka på filen i Solution Explorer och välja Rename.

Solution Explorer i ett MonoGameprogram

De metoder som från början finns i Game1-filen är:

  • Game1 – En speciell metod som kallas för spelets konstruktor. Denna metod anropas när spelet startas upp. Vi kommer mestadels inte att skriva någon kod här.
  • Initialize – Anropas en gång när programmet startat.
  • LoadContent – Anropas en gång när programmet startat (efter Initialize). I denna metod kommer vi att ladda in Content, d.v.s. bilder, ljud och teckensnitt.
  • UnloadContent – Anropas en gång när programmet stängs av, vi kommer inte att använda denna metod.
  • Update – En loop ser till att denna metod anropas 60 gånger per sekund (denna frekvens kan ändras). I denna metod skriver vi kod som t.ex. undersöker användarens tangentbord och mus, spelar upp ljud eller flyttar runt föremål på skärmen.
  • Draw – Anropas efter varje Update så länge som grafikkortet hinner med det. Här skriver vi all kod som ritar ut bilder på skärmen.

Förutom dessa metoder så kommer MonoGame förladdat med många olika variabler och variabeltyper som vi kommer att se senare.

Målet för vårt första program är att ladda in en bild i en variabel och sedan rita ut den någonstans på skärmen. För att kunna ladda in bilden behöver vi först konvertera den till formatet .xnb med hjälp av MonoGames content pipeline. De steg vi behöver gå igenom för att skapa programmet är följande:

  1. Konvertera bilden till .xnb-format med hjälp av MonoGames Content Pipeline.
  2. Ladda in bilden i metoden LoadContent.
  3. Rita bilden i metoden Draw.

Varje spel du gör med MonoGame har en Content.mgcb-fil som håller reda på vilka bilder och ljudfiler ditt spel ska använda. Du kan se denna fil i Solution Explorer i Visual Studio i mappen Content. För att lägga till en bild till spelet så ska vi öppna Content.mgcb-filen med MonoGames content pipeline.

Öppna MonoGames Content Pipeline Tool

Nedan beskrivs en ändring du behöver göra första gången du öppnar MonoGames Content Pipeline Tool, när du har gjort denna ändring räcker det med att dubbelklicka på Content.mgcb-filen i framtiden!

I vanliga fall så dubbelklickar du bara på Content.mgcb-filen för att öppna den, men eftersom detta är första gången du använder MonoGame så måste vi berätta för Visual Studio att vi alltid vill öppna denna filtyp med just MonoGames content pipeline. För att göra detta så högerklickar du på Content.mgcb-filen och väljer Open With. I listan som dyker upp väljer du MonoGame Pipeline Tool och trycker på Set as default, därefter OK. Filen kommer nu att öppnas i MonoGames content pipeline och i framtiden behöver du bara dubbelklicka på en Content.mgcb-fil för att öppna den.

MonoGames Content Pipeline Tool

Det enklaste sättet att lägga till en bild är att dra och släppa den i den övre vänstra rutan av de tre stora rutorna som finns i programmet, d.v.s. i rutan där det just nu står Content. Det kommer då att komma upp en ruta som frågar vad du vill göra med bilden, välj alternativet Copy the file to the directory. En kopia av bilden du släppte kommer att skapas i den mapp där ditt projekt är sparat på din dator. Välj nu Build -> Build i menyn längst upp och din bild konverteras till en xnb-fil.

I detta exempel så används en bild som heter parrot.png som är hämtad från www.kenney.nl. På www.kenney.nl kan man hitta många bilder som är lämpliga för spel och som är helt gratis att använda. Du kan använda vilken bild du vill, men välj en bild som inte har alltför hög upplösning.

MonoGames Content Pipeline Tool efter konvertering

Nästa steg blir att ladda in bilden i en variabel när programmet körs och sedan rita upp den på skärmen. Vi kommer att använda oss av medlemsvariabler när vi gör våra MonoGame-program, en medlemsvariabel är precis som en klassvariabel förutom att man inte behöver skriva static framför den. När vi gör MonoGame-program behöver vi aldrig skriva static framför en variabel eller metod. Först av allt så deklarerar vi två medlemsvariabler, en variabel som kommer att innehålla bilden vi ska rita och en variabel som innehåller bildens position. I MonoGame så sparas bilder i variabler av typen Texture2D och en position kan sparas med typen Vector2. Det finns redan två medlemsvariabler i Game1, graphics och spriteBatch, och vi deklarerar våra nya medlemsvariabler på samma ställe i koden som dessa.

Medlemsvariabler

De variabler som är utanför alla programmets metoder kallas för medlemsvariabler. När vi arbetade med konsolprogram kallade vi dessa variabler för klassvariabler, skillnaden mellan klassvariabler och medlemsvariabler är att klassvariabler har ordet static först vilket man inte behöver i MonoGame. Betydelsen av ordet static ligger utanför kursen Programmering 1.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Vector2 parrotPosition = new Vector2(100, 200);

Variabeln graphics används för att justera spelets grafikinställningar (t.ex. spelets upplösning), medan spriteBatch har metoder för att rita bild och text på skärmen så vi kommer att använda spriteBatch lite senare. Vår Texture2D-variabel deklarerar vi men ger inte något värde, vi kommer att tilldela variabeln ett värde i metoden LoadContent. Bildens position bestämmer vi här som (100, 200) vilket innebär att bildens övre vänstra hörn kommer att vara 100 pixlar till höger om fönstrets vänsterkant och 200 pixlar nedanför fönstrets överkant. Nästa steg blir att ladda in bilden i metoden LoadContent.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
    parrotBild = Content.Load<Texture2D>("parrot");
}

Bilden laddas in med hjälp av metoden Content.Load. Man skriver typen för det som man vill ladda in mellan < och >. Notera att man inte skriver med filändelsen på den bild man vill ladda in, vi har skrivit parrot och inte parrot.png eller parrot.xnb. Det som fattas nu är att rita upp bilden i Draw-metoden.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin();
    spriteBatch.Draw(parrotBild, parrotPosition, Color.White);

    spriteBatch.End();

    base.Draw(gameTime);
}

Med hjälp av variabeln spriteBatch kan vi rita upp bilden. Innan man börjar rita upp bilder (oavsett om det är en eller flera bilder) så måste man anropa metoden Begin och när man är färdig ska man anropa metoden End. Mellan dessa två anrop kan man anropa metoden Draw hur många gånger man vill. Det finns många olika varianter av metoden Draw, den enklaste att använda tar en Texture2D som första argument, en Vector2 som andra argument och en Color som tredje argument. Det tredje argumentet vi anger är vilken färgning vi vill ge bilden. Om man anger Color.White så får bilden inte någon färgning, men om man istället använder t.ex. Color.Red så blir bilden rödfärgad. Om du kör ditt program nu så kommer bilden du har laddat in att ritas upp på den position som din Vector2-variabel anger.

MonoGame Hello World

Nu har vi lyckats skapa MonoGames motsvarighet till Hello World.

Brant inlärningskurva

De flesta brukar tycka att det är väldigt mycket jobb i MonoGame för att bara rita ut en bild. Precis som med det mesta i livet så blir de steg som du nu har gått igenom enklare desto mer du övar på dem och eftersom du kommer att göra på detta sätt i alla MonoGameprogram som du skapar så blir det mycket övning.

Uppgift 8.1

Gör ett nytt program som ritar ut en valfri bild någonstans i den högra halvan av spelfönstret.

Lösningstips 8.1

Ändra värdet för vektorns X-koordinat för att flytta bilden i sidled, ett X-värde mellan 400 och 800 placerar bilden i den högra halvan.

Lösningsförslag 8.1

Flera bilder samtidigt

När du väl har lärt dig att rita ut en bild på skärmen så är det inte så svårt att lägga till fler. Det är inte heller så svårt att rita ut samma bild på flera ställen, det enda du behöver är fler positionsvariabler. Vi ska nu lägga till ytterliggare en bild i programmet och rita ut den på två ställen. Först så måste du lägga till bilden i MonoGames pipeline tool och därefter konvertera bilderna till xnb-format med Build->Build. Därefter så skapar vi några nya medlemsvariabler för den nya bilden och positionerna.

Den nya bilden som används i exemplet heter zebra.png, även denna bild är hämtad från www.kenney.nl.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild, zebraBild;
Vector2 parrotPosition = new Vector2(100, 200);
Vector2 zebraPosition1 = new Vector2(400, 30);
Vector2 zebraPosition2 = new Vector2(50, 300);

Nästa steg blir att ladda in bilden i LoadContent-metoden. Tänk på att inte skriva med filändelsen på din bild.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
    parrotBild = Content.Load<Texture2D>("parrot");
    zebraBild = Content.Load<Texture2D>("zebra");
}

Slutligen så måste vi lägga till några fler anrop till metoden spriteBatch.Draw i Game1-filens Draw-metod.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.Tomato);

    spriteBatch.Begin();
    spriteBatch.Draw(parrotBild, parrotPosition, Color.White);
    spriteBatch.Draw(zebraBild, zebraPosition1, Color.White);
    spriteBatch.Draw(zebraBild, zebraPosition2, Color.White);
    spriteBatch.End();

    base.Draw(gameTime);
}

Notera att det finns en ändring i den första raden i Draw-metoden. Anropet GraphicsDevice.Clear rensar fönstret från de bilder som ritades upp förra gången Draw-metoden anropades och fyller hela fönstret med en färg, det är alltså den färg som står i parentesen som blir programmets bakgrundsfärg. I exemplet ovan så har färgen ändrats från CornflowerBlue till Tomato, en rödaktig färg. Du kan själv bläddra bland de fördefinierade färger som finns genom att skriva Color följt av en punkt.

Uppgift 8.2

Skapa ett program som ska rita minst 3 olika bilder på skärmen. Varje bild ska ritas upp på minst 2 olika positioner.

Lösningstips 8.2

Kom ihåg att du bara behöver ladda in varje bild i endast en Texture2D oavsett hur många gånger du ska rita upp den.

Lösningsförslag 8.2

Få bilden att röra sig

Ett spel med enbart bilder som inte rör sig är inte mycket till spel, så nu ska vi se hur vi får bilderna att röra sig. Vi fortsätter med vårt program från det förra exemplet som innehåller en Texture2D vid namn parrotBild som ritas upp på positionen parrotPosition som är en Vector2. För att bilden ska röra sig behöver vi bara ändra värdet av parrotPosition. En Vector2 har en X-koordinat och en Y-koordinat som anger var bilden ritas. X- och Y-koordinaterna kan anges som float, alltså decimaltal som tar mindre plats i minnet än double.

I MonoGame så bör man lägga all kod som körs under spelets gång i Update-metoden med undantag av koden som ritar bilder. Vi ändrar därför Update-metoden så att bilden flyttar på sig. Update-metoden körs 60 gånger per sekund så länge som spelet körs och så länge datorn klarar av det. Notera att vi alltså fortsätter ändra samma program som i det tidigare exemplet.

protected override void Update(GameTime gameTime)
{
    parrotPosition.X += 2;
    parrotPosition.Y += 1;
    base.Update(gameTime);
}

Notera att vi har tagit bort den if-sats som fanns i Update-metoden när vi skapade programmet. Denna if-sats gör så att man kan stänga av spelet med escapetangenten eller med en ansluten spelkontroll, men eftersom det går lika bra att stänga av programmet med krysset i programmets övre högra hörn så tar vi bort denna kod för tillfället. Eftersom Update-metoden körs 60 gånger per sekund kommer koden i exemplet göra så att bilden rör sig 120 pixlar per sekund i x-led och 60 pixlar per sekund i y-led.

Frekvensen för Update

Man kan använda andra uppdateringsfrekvenser för Update-metoden än 60 gånger per sekund, men vi håller oss till standardvärdet för enkelhetens skull. Att värdet är just 60 är anpassat till att många datorskärmar uppdaterar sin bild 60 gånger per sekund.

När man flyttar en bild med värden som är skrivna direkt i Update-metoden så blir det svårt om man vill få bilden att ändra hastighet. Då är det bättre att använda en variabel för hastigheten. Vi kan i exemplet byta ut värdena 2 och 1 mot två double-variabler, en för x-hastigheten och en för y-hastigheten, men ännu smidigare är att använda en Vector2 för hastigheten.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild, zebraBild;
Vector2 parrotPosition = new Vector2(100, 200);
Vector2 zebraPosition1 = new Vector2(400, 30);
Vector2 zebraPosition2 = new Vector2(50, 300);

// Denna variabel är ny
Vector2 parrotHastighet = new Vector2(2, 1);

Typen Vector2 har nämligen stöd för flera vanliga räknesätt, man kan bland annat addera två Vector2 -variabler med varandra. Detta gör vi nu i Update-metoden.

protected override void Update(GameTime gameTime)
{
    parrotPosition += parrotHastighet;
    if (parrotPosition.X > 300 || parrotPosition.X < 0)
    {
        parrotHastighet *= -1;
    }
    base.Update(gameTime);
}

Varje Update kommer parrotPosition att öka med parrotHastighet. Detta innebär med de startvärden som finns i exemplet att x-koordinaten för parrotPosition kommer att öka med 2 varje update och y-koordinaten med 1, alltså samma ökning som tidigare. Med i Update-metoden finns nu också en if-sats som kollar om x-koordinaten antingen är större än 300 eller mindre än 0. Om den är det så kommer hastigheten att multipliceras med -1, vilket innebär att hastigheten byter riktning i både x-led och y-led. När du kör programmet kommer därför bilden att röra sig fram och tillbaka på skärmen.

Uppgift 8.3

Skapa ett nytt program som innehåller två bilder. Den ena bilden ska röra sig fram och tillbaka någonstans i den vänstra halvan av spelfönstret, den andra bilden ska röra sig fram och tillbaka någonstans i den högra halvan.

Lösningstips 8.3

Skapa en Vector2 per bild som håller koll på bildens hastighet.

Lösningsförslag 8.3

Uppgift 8.4

Upplösningen, det vill säga bredden och höjden, för ett MonoGame-spel är 800 pixlar brett och 480 pixlar högt om man inte ändrar det. Skapa ett nytt program där en bild rör sig runt i programfönstret och ser ut att studsa mot fönstrets kanter.

Lösningstips 8.4

Ändra bildens hastighet i X-led när den kommer för långt åt höger/vänster med en if-sats. Använd en annan if-sats för att ändra bildens hastighet i Y-led när den kommer för långt upp/ner.

Ta reda på hur stor din bild är mätt i pixlar och använd detta för att få studsarna att se bra ut. Senare i kapitlet kommer du få lära dig hur man kan undersöka storleken av en bild med hjälp av kod.

Lösningsförslag 8.4

Uppgift 8.5

Skapa ett nytt program som innehåller en bild som rör sig snabbt åt höger till en början. Bildens hastighet ska minska tills den till slut börjar röra sig åt vänster istället, därefter ska hastigheten åt vänster fortsätta öka.

Lösningstips 8.5

Skapa en Vector2 som håller koll på bildens acceleration, d.v.s. hur mycket hastigheten ska förändras varje update. Om acceleration i X-led är negativ så kommer man få den önskade effekten.

Lösningsförslag 8.5

Styr bilden med musen

Nu är det dags att se hur man som användare kan påverka det som händer i programmet, vi ska låta bilden ritas på samma position som användaren håller sin mus på. Det krävs inte så mycket kod för att hämta information om musen.

Vi skapar ett nytt MonoGame Windows Project men kommer fortsätta att använda samma bild som tidigare. Först så skapar vi de medlemsvariabler som vi behöver.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Vector2 parrotPosition = new Vector2(100, 200);
MouseState mus;

Typen MouseState sparar information om musen vid något visst tillfälle. Vi kommer i början av varje körning av Update-metoden spara musens nuvarande tillstånd i mus för att sedan kunna kolla vad musens koordinater är. Innan vi gör det så lägger vi till en kodrad i Initialize-metoden.

protected override void Initialize()
{
    IsMouseVisible = true;
    base.Initialize();
}

Denna kodrad gör så att muspekaren syns i spelfönstret, detta gör vi så att du lättare kan se på vilket sätt som bilden följer efter musen. I metoden LoadContent laddar vi in bilden vi ska använda på samma sätt som i tidigare exempel.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
    parrotBild = Content.Load<Texture2D>("parrot");
}

Det som återstår nu är att ändra koden i Update-metoden.

protected override void Update(GameTime gameTime)
{
    mus = Mouse.GetState();
    parrotPosition.X = mus.X;
    parrotPosition.Y = mus.Y;

    base.Update(gameTime);
}

Först så hämtas musens nuvarande tillstånd med metoden Mouse.GetState och lagras i variabeln mus. Typen MouseState har information om bland annat musens X- och Y-koordinater och bildens position sätts så att den har samma koordinater som musen. När du kör programmet kommer du att märka att det är bildens övre vänstra hörn som är vid muspekaren eftersom koordinaterna för en bild alltid anger koordinaterna för just det övre vänstra hörnet. Om du istället skulle vilja rita upp bilden så att bildens mittpunkt är vid muspekaren så är det bara att ändra koden så som följande exempel visar.

protected override void Update(GameTime gameTime)
{
    mus = Mouse.GetState();
    parrotPosition.X = mus.X - (parrotBild.Width / 2);
    parrotPosition.Y = mus.Y - (parrotBild.Height / 2);

    base.Update(gameTime);
}

Bilden flyttas till vänster med halva sin bredd och uppåt med halva sin höjd jämfört med muspekaren. Detta gör så att bildens mittpunkt hamnar precis vid muspekaren. Parenteserna i koden är inte nödvändiga utan endast där för att du tydligare ska se i vilken ordning uträkningarna sker.

Uppgift 8.6

Skapa ett nytt program som innehåller två olika bilder. Den ena bilden ska följa efter muspekaren i X-led men inte i Y-led. Den andra bilden ska följa efter muspekaren i Y-led men inte i X-led.

Lösningsförslag 8.6

Uppgift 8.7

Skapa ett program med en bild som jagar muspekaren. Bildens mittpunkt ska alltså hela tiden röra sig mot muspekaren men det ska ta lite tid för bilden att komma ikapp muspekaren när den har rört sig.

Lösningstips 8.7

Ett sätt att lösa uppgiften är att varje Update räkna ut hur stor skillnad det är mellan musens position och bildens mittpunkt, därefter så flyttar man bilden en viss andel av skillnaden, t.ex. 10 %. Bilden rör sig alltså inte hela vägen till musen utan endast en del av sträckan varje Update.

Lösningsförslag 8.7

Ändra storlek med Rectangle

Som du kanske har märkt så ritas en bild alltid upp i sin riktiga storlek när du ritar den med hjälp av en Vector2. Det betyder att om du har en bild som är väldigt bred och hög så kan vara större än hela programfönstret när du startar ditt MonoGame-spel. Det finns stöd för att rita en bild med valfri bredd och höjd i MonoGame, då anger man en Rectangle som bestämmer hur bilden ska ritas istället för en Vector2. I exemplet som följer ska vi rita upp en bild med hälften av sin ursprungliga bredd och höjd. Programmet ska ha följande medlemsvariabler:

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Rectangle parrotRectangle;

Om man vill rita bilden med t.ex. bredden 60 och höjden 100 så går det bra att skapa rektangeln redan här. I vårt fall vill vi dock rita bilden med hälften av dess urpsrungliga höjd och bredd och för att göra detta måste vi ha tillgång till bildens höjd och bredd. Det har vi inte förrän vi har laddat in bilden i LoadContent-metoden, så därför skapar vi vår Rectangle först efter att vi har laddat in bilden.

protected override void LoadContent()
{
    // Create a new SpriteBatch, which can be used to draw textures.
    spriteBatch = new SpriteBatch(GraphicsDevice);
    parrotBild = Content.Load<Texture2D>("parrot");
    parrotRectangle = new Rectangle(100, 200, parrotBild.Width / 2, parrotBild.Height / 2);
}

Efter att vi har laddat in bilden skapar vi bildens Rectangle med 4 argument: bildens X-position, Y-position, bredd och höjd. Rektangelns bredd och höjd sätts till hälften av bildens bredd och höjd. De värden du anger måste vara heltal och detta är en nackdel med att använda Rectangle istället för Vector2; X- och Y-koordinaterna sparas som heltal och inte decimaltal (Vector2 sparar koordinaterna som float). Detta innebär att du med en Vector2 kan öka X-koordinaterna med 0,1 varje gång Update-metoden körs och då kommer bilden flyttas en pixel till höger vart tionde Update, men du kan inte göra detta med en Rectangle. En annan nackdel är att man inte kan addera två Rectangle-variabler på samma sätt som vi gjorde med två Vector2-variabler tidigare.

Heltal i Rectangle

Glöm inte att du enbart kan använda heltal i en Rectangle och inte decimaltal som man kan göra med Vector2.

Vi behöver nu ändra Draw-metoden så att vi använder vår nya Rectangle istället för en Vector2 för att rita upp bilden. Vi har hittills endast använt en av de olika varianterna av Draw-metoden som finns i variabeltypen SpriteBatch. För att rita en bild med en Rectangle så måste vi använda en annan variant än den vi hittills använt.

protected override void Draw(GameTime gameTime)
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin();
    spriteBatch.Draw(parrotBild, parrotRectangle, Color.White);
    spriteBatch.End();

    base.Draw(gameTime);
}

Istället för att ange en Vector2 så anger vi en Rectangle för att bestämma var bilden ska ritas.

Man kan ändra bredden och höjden för en Rectangle även efter att den har skapats. Det görs genom att ändra dess Width och Height vilket visas nedan.

protected override void Update(GameTime gameTime)
{ 
    parrotRectangle.Width += 2;
    parrotRectangle.Height += 3; 
}

Eftersom Update-metoden körs 60 gånger per sekund i vanliga fall så kommer bildens bredd att öka med 120 pixlar/sekund och höjden med 180 pixlar/sekund.

Uppgift 8.8

Skapa ett program som innehåller en bild som anpassas i storlek efter var användaren håller muspekaren. Sätt bildens övre vänstra hörn i punkten (100, 150). Bildens nedre högra hörn ska hela tiden vara där användaren håller muspekaren.

Lösningstips 8.8

Du kan räkna ut vilken bredd och höjd bilden ska ha genom att jämföra musens position med bildens position.

Lösningsförslag 8.8

Uppgift 8.9

Skapa ett program som innehåller en bild som växlar mellan att öka och minska i storlek när programmet kör. Rita bilden i dess egentliga storlek från början men gör så att den växer när programmet kör. När bilden har växt så mycket att den är dubbelt så bred och hög som den var från början ska den börja krympa istället för att växa. När den har återgått till sin ursprungliga storlek ska den växa igen. Bilden ska inte förvrängas när den växer eller krymper, d.v.s. förhållandet mellan bildens bredd och höjd ska inte ändras.

Lösningstips 8.9

Skapa en bool som håller koll på bildens om bilden växer eller krymper och en float som håller koll på hur stor bilden är just nu (1 = ursprunglig storlek, 2 = dubbelt så stor).

Lösningsförslag 8.9

Skriva text

Förutom att rita bilder på spelfönstret så kan man också skriva text. Vi ska i ett nytt exempel se vilka steg man behöver gå igenom för att skriva text i spelfönstret.

Hur text ser ut när den skrivs i spelfönstret beror på vilket teckensnitt som används. Vanliga exempel på teckensnitt är Times New Roman, Arial och Calibri. MonoGame använder sig av något som kallas för SpriteFont när man ska skriva text. Font är det engelska ordet för teckensnitt och sprite betyder ungefär bild, så SpriteFont kan översättas till bildteckensnitt. Ett SpriteFont är en bild som innehåller de tecken man vill kunna skriva med i en viss storlek från ett visst teckensnitt. Det är bättre för spelens prestanda att använda teckensnitt som är sparade som bilder istället för de sätt som teckensnitt brukar sparas på en dator. Innan man kan skriva text i MonoGame måste man därför skapa ett SpriteFont, en bild med tecken från ett visst teckensnitt, och det gör man med hjälp av MonoGames Content Pipeline Tool.

Skapa ett nytt program och öppna MonoGames Content Pipeline Tool. För att skapa ett SpriteFont väljer du menyn Edit -> Add -> New Item.

Lägg till SpriteFont

I fönstret som dyker upp finns det några förvalda typer av filer du kan lägga till ditt program. Välj att skapa en SpriteFont Description och döp den till vad du vill (i exemplet har vi valt att kalla filen för arial). En SpriteFont Description är inte ett SpriteFont, det är en textfil som innehåller information om vilket teckensnitt ditt SpriteFont ska ha. När du trycker på Build i MonoGames Pipeline Tool så kommer det att skapas ett SpriteFont enligt den beskrivning som finns i .spritefont-filen. I exemplet så använder vi de standardvärden som .spritefont-filen innehåller vilket är teckensnittet Arial med storlek 12, så det räcker med att skapa en .spritefont-fil och därefter trycka på Build i MonoGames Pipeline Tool.

Om du vill ändra på det SpriteFont som skapas i något annat program så kan du öppna .spritefont-filen och ändra värdena som finns där, filen innehåller en beskrivning av var någonstans du ska ändra filen för att det SpriteFont som skapas ska bli som du vill. En sak som är värd att notera är att ett SpriteFont inte skapas med alla tecken som finns, vilka tecken ett SpriteFont innehåller bestäms av .spritefont-filen. De standardvärden som finns i .spritefont-filen gör att ditt SpriteFont inte innehåller de svenska tecknen å, ä eller ö. Om du vill kunna använda å, ä och ö i din text behöver du ändra den del i slutet av .spritefont-filen som just nu är <End>&#126;</End> till <End>&#246;</End>`.

Olika tecken till ditt SpriteFont

Genom att ändra End-värdet i en SpriteFont till 246 får du med våra svenska bokstäver. Om du vill ta reda på vilka End-värden som behövs för andra tecken kan du kolla vilken Unicode-kod tecknen har på https://unicode-table.com/

När du väl har skapat ett SpriteFont med hjälp av MonoGames Pipeline Tool så är det inte så svårt att skriva text. Först så behöver vi ladda in vårt SpriteFont och sedan använda det för att skriva ut en string på valfri position.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont arialFont;
string meddelande = "Hej!";
Vector2 meddelandePosition = new Vector2(200, 100);

Bland medlemsvariablerna finns ett SpriteFont som ska innehålla det SpriteFont som skapats i MonoGames Pipeline Tool. Det finns också en sträng som innehåller texten som ska skrivas ut och en Vector2 som anger var texten ska skrivas.

// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
arialFont = Content.Load<SpriteFont>("arial");

I metoden LoadContent så laddar vi in vårt SpriteFont på ett liknande sätt som när vi laddar in bilder. Strängen som anges som parameter (i exemplet arial) är det namn som du döpte din .spritefont-fil till i MonoGames Pipeline Tool, inte själva teckensnittets namn.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
spriteBatch.DrawString(arialFont, meddelande, meddelandePosition, Color.Red);
spriteBatch.End();

base.Draw(gameTime);

I Draw-metoden så måste vi skriva ett anrop till spriteBatch.Begin och spriteBatch.End som vanligt. Däremellan kan man som tidigare anropa metoden spriteBatch.Draw hur många gånger man vill, och man kan också anropa metoden spriteBatch.DrawString hur många gånger man vill. Den variant av spriteBatch.DrawString som vi har använt tar ett SpriteFont som första parameter, en sträng som andra, en Vector2 som tredje och en färg som fjärde parameter. Du kan bläddra bland de färger som finns fördefinierade om du skriver Color följt av en punkt.

Ett vanligt problem som kan dyka upp är att man behöver göra något extra för att skriva ut tal med hjälp av DrawString. Eftersom DrawString kräver en string och inte t.ex. en int så måste man omvandla ett tal till en string för att kunna skriva ut det. Anta att medlemsvariablerna i Game1 såg ut så här istället.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
SpriteFont arialFont;
int svar = 42;
Vector2 meddelandePosition = new Vector2(200, 100);

Om man nu vill skriva ut värdet av variabeln svar så kan man göra på följande sätt.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
spriteBatch.DrawString(arialFont, svar.ToString(), meddelandePosition, Color.Red);
spriteBatch.End();

base.Draw(gameTime);

Metoden ToString omvandlar int-variabeln svar till en string.

Uppgift 8.10

Skriv ut ditt namn och din ålder i spelfönstret samt ett valfritt ord som innehåller å, ä eller ö. Texten ska skrivas med teckensnittet Times New Roman i storleken 36.

Lösningsförslag 8.10

Uppgift 8.11

Skapa ett nytt program som räknar hur många sekunder det har gått sedan programmet startade och som skriver ut det i programfönstret. Du kan anta att Update-metoden körs 60 gånger per sekund.

Lösningstips 8.11

Skapa en int som du ger värdet 0, öka den med 1 varje Update. När denna int får värdet 60 så har en sekund passerat och du kan då öka en annan int som håller koll på hur många sekunder som gått i programmet. Passa samtidigt på att nollställa räknarvariabeln som ökar med 1 varje Update.

Lösningsförslag 8.11

MonoGame och tangentbordet

Nu ska vi se hur man kan använda tangentbordet för att styra vad som händer i ett MonoGame-spel. Det finns en lista i MonoGame över vilka tangenter som finns på ett tangentbord, denna lista ligger i Keys. För att undersöka om en viss tangent är nedtryckt så behöver man först känna till vad en KeyboardState är.

I MonoGame så innehåller en KeyboardState tillståndet för tangentbordet vid en viss tidpunkt, d.v.s. vilka tangenter som var nedtryckta på tangentbordet vid någon viss tidpunkt. Först av allt så måste man spara tangentbordets nuvarande tillstånd i en KeyboardState-variabel för att sedan undersöka om en viss tangent är nedtryckt med en metod som heter IsKeyDown. Vi gör ett nytt program för att testa hur man kan använda tangentbordet för att styra en bild.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Vector2 parrotPosition = new Vector2(100, 200);
KeyboardState tangentbord = Keyboard.GetState();

I detta program så används en bild som heter parrot.png precis som i ett tidigare exempel. Denna bild är tillagd i MonoGames Pipeline Tool och laddas i metoden LoadContent precis som tidigare. Det som är nytt är medlemsvariabeln tangentbord av typen KeyboardState. Den ges direkt ett värde med med den metoden Keyboard.GetState(). Denna metod hämtar ett KeyboardState som anger vilka tangenter som var nedtryckta när metoden anropades.

// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
parrotBild = Content.Load<Texture2D>("parrot");

I LoadContent-metoden så är det ingen ny sorts kod, bilden parrot.png laddas in i variabeln parrotBild. Nästa metod där vi lägger till egen kod är Update-metoden, det är här som vi skriver koden som sköter förflyttning av bilden med hjälp av tangentbordet.

tangentbord = Keyboard.GetState();

if (tangentbord.IsKeyDown(Keys.Left))
{
    parrotPosition.X -= 3;
}
if (tangentbord.IsKeyDown(Keys.Right))
{
    parrotPosition.X += 3;
}

base.Update(gameTime);

Det första som sker i varje Update är att variabeln tangentbord tilldelas ett nytt KeyboardState, ett KeyboardState som innehåller information om vilka tangenter som är nedtryckta just när Update-metoden anropas. Typen KeyboardState innehåller en metod som heter IsKeyDown och det är den som används i if-satserna som följer. Som parameter kräver IsKeyDown ett värde ur typen Keys. Keys.Left står för den vänsterpilen på tangentbordet och Keys.Right står för högerpilen.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
spriteBatch.Draw(parrotBild, parrotPosition, Color.White);
spriteBatch.End();

base.Draw(gameTime);

Med hjälp av ett KeyboardState, metoden Keyboard.GetState och Keys så kan man alltså kolla vilka tangenter användaren har tryckt ner på tangentbordet.

Fler typer av inmatning

Förutom musen och tangentbordet så kan man även använda spelkontroller med hjälp av GamePadState. När man gör MonoGame-spel till telefoner eller surfplattor har man tillgång till något som kallas för TouchCollection för att hantera imatning på en touchskärm.

Uppgift 8.12

Bygg ut exemplet ovan så att man kan styra bilden i ditt program i Y-led med hjälp av upp- och nerpilarna. Du ska också bygga ut programmet så att tangenterna W, A, S och D också kan användas för att styra bilden (W = uppåt, S = neråt, A = vänster, D = höger).

Lösningsförslag 8.12

Uppgift 8.13

Bygg ut exemplet ovan så att man kan justera vilken hastighet som bilden förflyttas med när man trycker på piltangenterna eller WASD. Om man trycker på tangenten Q ska bildens hastighet ställas in så att den rör sig 120 pixlar per sekund i varje led, om man trycker på E ska den röra sig med hastigheten 300 pixlar per sekund i varje led.

Lösningstips 8.13

Eftersom Update körs 60 gånger per sekund så är 120 pixlar per sekund = 2 pixlar per Update, hastigheten 300 pixlar per sekund = 5 pixlar per Update. Skapa en variabel som håller koll på bildens hastighet.

Lösningsförslag 8.13

Tangenttryckningar och nedhållningar

Som du märkte när du testade ditt program i det föregående exemplet så förflyttas bilden när du håller ner en tangent, man behöver inte trycka ner tangenten om och igen för att bilden ska flytta på sig. Ibland vill man dock att någonting bara ska ske just när man trycker ner en tangent och inte att saken ska fortsätta ske så länge som man håller ner tangenten. Ett exempel på detta är om man gör ett spel där man styr en figur som kan hoppa, i de flesta spel behöver man trycka ner hoppa-tangenten varje gång man vill hoppa, det räcker inte med att man håller hoppa-tangenten nedtryckt för att fortsätta hoppas.

För att kolla om en tangent precis har blivit nedtryckt behöver vi använda två KeyboardState. I det ena ska vi spara information om vilka tangenter som var nedtryckta förra gången som Update-metoden anropades. I detta exemplet används samma bild som i det förra exemplet men koden för att ladda in bilden och rita upp den visas ej.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Vector2 parrotPosition = new Vector2(100, 200);
KeyboardState gammaltTangentbord = Keyboard.GetState();
KeyboardState tangentbord = Keyboard.GetState();

Det finns en ny medlemsvariabel i Game1, ett KeyboardState som kallas för gammaltTangentbord. När spelet startar kommer det att ha samma värde som variabeln tangentbord men detta kommer att ändras när spelet kör.

gammaltTangentbord = tangentbord;
tangentbord = Keyboard.GetState();

if (tangentbord.IsKeyDown(Keys.Space) && gammaltTangentbord.IsKeyUp(Keys.Space))
{
    parrotPosition.X += 30;
}
base.Update(gameTime);

Först av allt i varje Update-metod så sparas värdet av tangentbord i variabeln gammaltTangentbord. Detta gör att variabeln gammaltTangentbord kommer att innehålla information om vilka tangenter som var nedtryckta den förra gången som Update-metoden anropades. Variabeln tangentbord får på nästa rad ett nytt KeyboardState som innehåller information om vilka tangenter som är nedtryckta under det nuvarande Update-anropet. En knapp har precis tryckts ner om den är nedtryckt under detta Update-anrop men inte var det under det förra Update-anropet, och det är detta som if-satsen undersöker.

Notera att metoden IsKeyUp användes för gammaltTangentbord medan IsKeyDown användes för tangentbord. Bilden kommer att flyttas 30 pixlar åt höger varje gång man trycker ner mellanslagstangent, men den flyttas inte mer bara för att man håller mellanslagstangenten nedtryckt.

Uppgift 8.14

Skapa ett program som skriver ut "Nu syns texten!" någonstans på skärmen. När användaren trycker på I-tangenten så ska texten inte synas längre. Nästa gång användaren trycker på I-tangenten så ska texten visas igen, nästa gång döljas o.s.v.

Lösningstips 8.14

Använd en bool för att hålla koll på om texten ska skrivas ut eller inte. Om denna bool t.ex. heter textenSyns så kan du ändra den inuti lämplig if-sats inuti update med hjälp av textenSyns = !textenSyns, då byter den från true till false eller tvärtom.

Lösningsförslag 8.14

Uppgift 8.15

Skapa ett program som innehåller en bild som ritas upp på skärmen. När man trycker på mellanslag ska bilden börja röra på sig åt höger. Nästa gång man trycker på mellanslag ska bilden stanna igen. Gången efter det ska bilden röra sig åt vänster, nästa gång man trycker på mellanslag ska den stanna igen. Därefter ska mönstret upprepas, d.v.s. nästa gång man trycker på mellanslag ska bilden börja röra sig åt höger.

Lösningstips 8.15

Bilden kommer gå igenom 4 olika faser innan rörelsemönstret upprepas, d.v.s. stilla -> höger -> stilla -> vänster. Numrera faserna från t.ex. 0 till 3 och skapa en variabel som håller koll på vilken av faserna bilden är på just nu.

Lösningsförslag 8.15

Musknappar

I det förra kapitlet fick du lära dig hur du kunde kolla var i spelfönstret muspekaren befinner sig med hjälp av typen MouseState. Vi ska nu se hur man kan använda MouseState tillsammans med uppräckningen ButtonState för att kolla om knapparna på musen är nedtryckta eller inte. I detta exemplet används samma bild som i det förra exemplet men koden för att ladda in bilden och rita upp den visas ej.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
Vector2 parrotPosition = new Vector2(100, 200);
MouseState mus = Mouse.GetState();

Bland medlemsvariablerna har vi samma variabler som i en del tidigare exempel tillsammans med en MouseState som vi väljer att kalla för mus. Koden som följer är i spelets Update-metod.

mus = Mouse.GetState();
parrotPosition.X = mus.X;
parrotPosition.Y = mus.Y;

base.Update(gameTime);

I varje Update så hämtas information om musen och sparas i variabeln mus. Bildens övre vänstra hörn placeras där muspekaren är på samma sätt som vi gjorde i det förra kapitlet.

I detta exempel så lägger vi till lite mer kod i Draw-metoden som gör så att bilden endast ritas när man håller den vänstra musknappen nedtryckt.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
if (mus.LeftButton == ButtonState.Pressed)
{
    spriteBatch.Draw(parrotBild, parrotPosition, Color.White);
}
spriteBatch.End();

base.Draw(gameTime);

En MouseState innehåller information om många av musens knappar och informationen sparas med hjälp av en ButtonState som har värdet Pressed eller Released.

Uppgift 8.16

Skapa ett program som innehåller en bild som man flyttar runt med musen. När man håller musens vänstra nedknapp intryckt så ska bilden flyttas till musens position, om man inte håller den vänstra knappen nedtryckt så ska bilden vara stilla.

Lösningsförslag 8.16

Uppgift 8.17

Gör om programmet i den föregående uppgiften så att bilden endast flyttas precis när man trycker på musknappen, inte när man håller nere den. Bildens mittpunkt ska placeras där man trycker ner musknappen.

Lösningstips 8.17

Använd två stycken MouseState på samma sätt som du använda två stycken KeyboardState när du undersökte om användaren tryckte på en tangent.

Lösningsförslag 8.17

Ljud i MonoGame

Vi ska nu se hur man kan lägga till olika typer av ljud i ett MonoGame-spel. För att kunna använda ljud i vårt program så behöver vi lägga till två nya using-uttryck längst upp i programmet.

using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Audio;

Det första using-uttrycket krävs för att kunna använda variabler av typen Song som används för bakgrundsmusik och det andra krävs för att använda variabler av typen SoundEffect som används för ljudeffekter.

Först av allt så ska vi lägga till de filer vi vill använda i MonoGames Content Pipeline Tool. I exemplet så har en mp3-fil och en wav-fil lagts till. Filen musik.mp3 är en låt som är en dryg minut lång medan boom.wav innehåller en kort ljudeffekt på cirka en sekund. Det är ganska vanligt att wav-filer används för ljudeffekter och mp3-filer för musik så därför importeras wav-filen som en SoundEffect som standard och mp3-filen som en Song.

Ljudfiler i Content Pipeline Tool

Det går själv att ändra om en ljudfil ska importeras som en Song eller som en SoundEffect i MonoGames Content Pipeline Tool i alternativet ”Processor” som finns för varje fil. Det går också att ändra vilken ljudkvalitet filen importeras med vilket påverkar vilken ljudets storlek på datorns hårddisk.

Filtyper för ljudfiler

Det är ganska vanligt att ljudfiler som man hittar på nätet inte är i wav-format utan i mp3 eller något annat format. Många av formaten blir som standard till Song, så glöm inte att ändra *Processor" till SoundEffect i Pipeline Tool om du vill att ljudet ska vara just en SoundEffect istället.

Nästa steg blir att ladda in och spela upp ljuden i vårt program. Efter att vi har lagt till de using-uttryck som krävs för att spela upp ljud som vi såg tidigare så lägger vi till följande medlemsvariabler

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Song musik;
SoundEffect boom;
KeyboardState gammaltTangentbord = Keyboard.GetState();
KeyboardState tangentbord = Keyboard.GetState();

Förutom en SoundEffect och en Song så tar vi med två KeyboardState som vi kommer att använda i exemplets Update-metod, ljudeffekten boom ska nämligen spelas upp precis när vi trycker ner mellanslag på tangentbordet.

Vi tar nu och laddar in båda ljudfilerna i de variabler vi har skapat. Vi kommer även att börja spela bakgrundsmusiken i metoden LoadContent eftersom den ska spelas så fort som spelet startar.

// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
boom = Content.Load<SoundEffect>("boom");
musik = Content.Load<Song>("musik");

// Spela bakgrundsmusiken loopad
MediaPlayer.IsRepeating = true;
MediaPlayer.Play(musik);

Bakgrundsmusik som är lagrad i Song-variabler spelas upp med hjälp av metoden MediaPlayer.Play. Innan vi spelar upp vår låt så ändrar vi en inställning som gör så att låten kommer att loopas, d.v.s. börja spelas om från början när den tar slut, eftersom detta passar bra för bakgrundsmusik. När en Song spelas så strömmas musikfilen från datorns hårddisk under tiden som den spelar, programmet hämtar musikfilen bit för bit desto längre in i låten som den kommer. De lagras på ett komprimerat sätt som gör att de inte tar upp så mycket utrymme.

Det som återstår för oss är att spela upp vår SoundEffect. En SoundEffect laddas in i datorns minne när man laddar den i LoadContent-metoden till skillnad från en Song som strömmas från hårddisken. En SoundEffect lagras inte komprimerat på samma sätt som en Song men man kan påverka hur stor plats en SoundEffect tar genom att ändra dess ljudkvalitet i MonoGames Content Pipeline Tool. Nedan så visas hur vi kan göra så att ljudeffekten boom spelas upp när man trycker på mellanslagstangenten på tangentbordet.

gammaltTangentbord = tangentbord;
tangentbord = Keyboard.GetState();

if (tangentbord.IsKeyDown(Keys.Space) && gammaltTangentbord.IsKeyUp(Keys.Space))
{
    boom.Play();
}
base.Update(gameTime);

Om vi hade spelat upp ljudeffekten varje Update som mellanslag var nedtryckt istället för att spela upp den precis när mellanslag trycks ner så hade ljudfilen börjat spelas 60 gånger per sekund, och det skulle inte låta särskilt bra. Du kan själv testa att ändra programmet så att det gör på detta sätt istället för att höra hur det låter.

Uppgift 8.18

Skapa ett program som innehåller två olika låtar. När programmet startar så ska låt 1 börja spelas automatiskt. Användaren ska sedan kunna välja vilken låt som spelas som bakgrundsmusik genom att trycka på A-tangenten för låt 1 eller B-tangenten för låt 2.

Lösningstips 8.18

Glöm inte att skapa två KeyboardState för att hålla koll på när användaren klickar på en tangent och inte bara håller ner den.

Lösningsförslag 8.18

Uppgift 8.19

Skapa ett program som innehåller bakgrundsmusik som startar så fort som programmet startas. Programmet ska innehålla en bild som studsar runt mot fönstrets kanter. Varje gång bilden studsar mot en kant så ska ett ljud spelas upp.

Lösningsförslag 8.19

Listor och MonoGame

En lista går att använda i MonoGame för att lösa samma problem som man kan lösa med hjälp av en array med samma fördelar jämfört med en array vi såg i förra kapitlet. När du skapar ett Monogame-program så måste du lägga till följande using-uttryck längst upp i filen för att kunna använda en List.

using System.Collections.Generic;

Vi ska i nästa exempel skapa ett program som använder en lista av Vector2 för att rita en bild på många olika platser. Börja därför med att lägga till detta using-uttryck i ditt nya MonoGame-program. Härnäst så deklarerar vi några medlemsvariabler som ska användas i spelet. Bilden som används heter parrot.png och är inlagd i MonoGames Content Pipeline Tool.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
Texture2D parrotBild;
List<Vector2> parrotPositioner = new List<Vector2>();

Vi deklarerar här en Lista och en variabel för att spara bilden i. Eftersom vi i detta exempel vill generera de vektorer som ska finnas i listan en gång när spelet startar så blir det lämpligt att göra detta i metoden Initialize.

for (int i = 0; i < 10; i++)
{
    parrotPositioner.Add(new Vector2(i * 70, 200));
}

base.Initialize();

Vektorerna som läggs till i listan har alla samma y-värde men olika x-värden.

Variabeln parrotBild tilldelas ett värde i metoden LoadContent på samma sätt som i alla tidigare program, du får själv skriva koden som gör detta.

För att rita bilden på alla positioner som finns i listan så använder vi en loop, i detta fall en foreach-loop. Det går också bra att använda en for-loop.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
foreach (Vector2 parrotPosition in parrotPositioner)
{
    spriteBatch.Draw(parrotBild, parrotPosition, Color.White);
}
spriteBatch.End();

base.Draw(gameTime);

När man nu kör programmet så kommer samma bild att ritas upp på 10 olika platser.

I det förra kapitlet kunde vi se att det är viktigt att veta hur listor och andra samlingar fungerar tillsammans med värdetyper, och det blir tydligt om vi vill ändra på värdena i vår lista eftersom Vector2 och Rectangle är värdetyper och inte referenstyper.

Så tar du reda på om en variabeltyp är en värdetyp eller referenstyp

Om du håller musen över en variabeltyp i Visual Studio så får man information om variabeltypen i en ruta som dyker upp. Om variabeltypen är en struct eller enum så är den en värdetyp, är den en class så är den en referenstyp. En array är alltid en referenstyp oavsett om arrayen innehåller en värdetyp eller referenstyp.

På detta sätt kan man se att både Vector2 och Rectangle tillhör kategorin struct, alltså är de värdetyper.

Tänk nu att vi vill bygga ut vårat program så att alla bilderna i det förra exemplet rör på sig när programmet körs. Vi kan göra detta med hjälp av en loop i programmets Update-metod. Vi använder i detta fall en for-loop istället för en foreach-loop eftersom man inte kan ändra värdet av den variabel som man loopar igenom med foreach-loopen. Trots detta så kommer inte koden i följande exempelruta att fungera, programmet kan inte kompileras med denna kod.

for (int i = 0; i < parrotPositioner.Count; i++)
{
    parrotPositioner[i].X++;
}

base.Update(gameTime);

Anledningen till att man inte kan använda denna kod har att göra med att Vector2 är en värdetyp och inte en referenstyp. Inuti listan med vektorer så finns det en dold array som sparar alla vektorer i listan. När man vill komma åt parrotPositioner[i] så anropas en särskild metod som List har. Denna metod hämtar en vektor i den dolda arrayen, men eftersom Vector2 är en värdetyp så hämtas en kopia av själva vektorn istället för en kopia av referensen till vektorn. Det viktiga här är alltså att vi får en kopia av vektorn, och när vi skriver att vi vill öka X-koordinaten för vektorn med 1 så är den denna kopia som kommer att få en större X-koordinat, inte den vektor som finns i listan. Om Vector2 hade varit en referenstyp så hade man inte stött på detta problem eftersom man då hade fått en kopia av referensen till vektorn i listans dolda array, och med hjälp av denna referens hade man kunnat ändra vektorn som finns inuti listan.

Hur ska man då göra för att öka X-koordinaterna för alla bilder i listan? Det finns några olika alternativ:

  • Vi kan spara kopian i en ny Vector2-variabel, ändra värdet av kopian och sedan spara kopian på rätt plats i listan.
  • Vi kan använda en array istället för en List. Då förlorar vi alla de fördelar som en List har jämfört med en array men vi har då tillgång till varje Vector2 direkt istället för att bara få kopior av vektorerna som vi får när vi använder en List.

Vi ska se hur man kan skriva kod som löser problemet på det första sättet, du får själv testa att skriva om programmet så att det använder en array i en uppgift.

Vector2 temp;
for (int i = 0; i < parrotPositioner.Count; i++)
{
    temp = parrotPositioner[i];
    temp.X++;
    parrotPositioner[i] = temp;
}

base.Update(gameTime);

Denna Update-metod kommer att åstadkomma det som vi tidigare ville. Vi har deklarerat en variabel som vi kallar för temp, ett namn som man ofta använder för en variabel som behövs tillfälligt eller temporärt. Denna variabel kan med fördel kunnat deklareras som en medlemsvariabel så att den inte behöver skapas på nytt i varje Update. Vår for-loop sparar kopian av vektorn som vi får från listan i variabeln temp. Därefter så ökar vi värdet av temp-variabelns X-koordinat, och slutligen så sparar vi värdet av temp-variabeln på den plats i listan som loopen jobbar med.

Andra sätt att lösa problemen

Det finns andra sätt att lösa problemet med värdetyper och List, men de ingår inte i Programmering 1. Du kan vara lugn med att om du fortsätter läsa programmering så behöver du inte använda temp-variabler med listor så ofta. När du lär dig att skapa egna klasser, som ingår i Programmering 2, så kan man undvika detta problem smidigt.

Uppgift 8.20

Gör ett program som ritar upp samma bild på tio olika platser i spelfönstret. Man ska kunna styra alla bilderna samtidigt med vänster och höger piltangenter. Alla bilderna ska flyttas med samma hastighet när man håller ner någon av dessa tangenter. Detta program ska använda sig av en List, inte en array.

Lösningstips 8.20

Eftersom du ska använda dig av en list i detta program och Vector2 är en värdetyp så måste du använda dig av en temp-variabel när du ska flytta bilderna.

Lösningsförslag 8.20

Blandade uppgifter till kapitel 8

Uppgift 8.21

Skapa ett program som innehåller en bild som rör sig fram och tillbaka i x-led. Först ska bilden röra sig åt höger i en sekund, därefter åt vänster i en sekund, därefter höger i en sekund o.s.v. Du bestämmer själv vilken fart bilden ska ha.

Lösningstips 8.21

Skapa en int som du ger värdet 0, öka den med 1 varje Update. När denna int får värdet 60 så har en sekund passerat och då är det dags att ändra hastighet. Passa samtidigt på att nollställa räknarvariabeln som ökar med 1 varje Update.

Lösningsförslag 8.21

Uppgift 8.22

Skapa ett program där du lägger in en bild av en bil. Man ska kunna styra bilen uppåt, nedåt, framåt och bakåt med hjälp av piltangenterna på tangentbordet. Piltangenterna ska fungera som ”gaspedaler” i de olika riktningarna, när man t.ex. håller ner höger piltangent så ska bilen accelerera åt höger. När man inte håller ner någon piltangent ska bilen fortsätta färdas med samma hastighet som den hade innan man släppte piltangenterna.

Lösningsförslag 8.22

Uppgift 8.23

Skapa ett MonoGameprogram som inte ritar ut några bilder. Varje gång användaren klickar med musen så ska en bild skapas på denna plats och ritas ut där, den ska ritas ut på denna position så länge som programmet körs.

Lösningstips 8.23

Använd två MouseState för att ta reda på om användaren klickat med musen. Spara de positioner som användaren har klickat på i en List<Vector2>.

Lösningsförslag 8.23

Uppgift 8.24

Skapa ett program som innehåller en bild. Bildens nedre högra hörn ska hela tiden vara på samma position som muspekaren, det övre vänstra hörnet ska vara stilla. När användaren klickar på den vänstra musknappen så ska programmet ändras så att det nedre högra hörnet är stilla, det övre vänstra hörnet ska vara där muspekaren är. Varje gång man klickar på den vänstra musknappen så ska det hörn som är stilla och det hörn som rör sig bytas.

Lösningsförslag 8.24

Uppgift 8.25

Skapa ett program som inte har några bilder i fönstret när det startar upp, programmet ska spela upp bakgrundsmusik när det startar. Varje gång användaren klickar på den vänstra musknappen ska en ljudeffekt spelas upp och en bild ska dyka upp på en slumpad position i fönstret. Bildens storlek ska också vara slumpad men hela bilden måste få plats i spelfönstret. Det ska vara samma bild som dyker upp varje gång man klickar på den vänstra musknappen. Använd en array för att spara de rektanglar som behövs för att rita upp bilderna, programmet behöver inte klara av att rita upp mer än 1000 bilder innan det kraschar.

Lösningstips 8.25

Skapa en int som håller koll på hur många bilder du har skapat och använd den i de loopar programmet har som går igenom arrayen med rektanglar.

Lösningsförslag 8.25

Uppgift 8.26

Skapa ett MonoGameprogram som innehåller samma bild på 20 olika platser, bilden bör vara ganska så liten. Bilderna ska åka runt med en hastighet som slumpas när programmet startar och de ska studsa mot fönstrets kanter. När två bilder krockar med varandra ska båda bilderna tas bort från programmet.

Lösningstips 8.26

Använd rektanglar och metoden Intersects för att undersöka om två bilder krockar med varandra.

När du går igenom listan med rektanglar med en loop så är det bra att låta den räkna nedåt istället för uppåt, annars riskerar man att "hoppa över" några av rektanglarna när loopen körs. Detta sker p.g.a. att när man tar bort ett element från en lista så minskar indexen med 1 för alla element som kommer senare i listan.

Lösningsförslag 8.26

Inga fler svåra uppgifter?

Svårigheter i programmering dyker inte alltid upp i små korta övningar likt de uppgifter som finns här. De grafiska programmen med MonoGame som du nu lärt dig göra lämpar sig bra för större, längre program, och du kommer att ha möjlighet att testa på svåra övningar när du utökar de kommande projektuppgifterna.