Gå till innehållet

Projektuppgift 5 - Träffa ormen

Introduktion till projektuppgift 5

Vår sista projektuppgift kommer att vara ett spel som liknar det klassiska arkadspelet ”Whac-a-mole”. När spelet startar så kommer några bilder av en orm dyka upp på skärmen, det är spelarens mål att trycka på ormarna så att de försvinner. Med jämna mellanrum så kommer det fram nya ormar på skärmen så om spelaren inte är tillräckligt snabb så blir hen snart överrumplad av alla ormar i spelet. Detta spel kommer behöva känna av om vi trycker med musen på en viss bild och till vår hjälp kommer vi ta variabeltypen Point samt metoden Contains som vi ska studera innan vi börjar med projektuppgiften.

Vi kommer göra så att Träffa ormen har en meny som visas när man öppnar programmet som innehåller en knapp för att starta själva spelet. När en spelomgång är över kommer man tillbaka till menyn och kan då välja att spela en gång till.

Spelets bilder

Om du vill använda samma bilder i Träffa Ormen som vi har gjort så hittar du dem här

Variabeltypen Point och metoden Contains

Varje Rectangle har en metod som heter Contains som kan undersöka om en annan Rectangle eller Point befinner sig inuti den. Vi kommer inte att skapa några egna variabler av typen Point men vi kommer att använda oss av en sådan som finns i varje MouseState som heter Position. Detta exempel innehåller en bild som ritas ut på skärmen, när muspekaren är över bilden så ändrar bilden färg.

Här visas de medlemsvariabler som programmet använder.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

Texture2D parrotBild;
Rectangle parrotRect;
Color parrotColor = Color.White;
MouseState mus = Mouse.GetState();

I Initialize så gör vi så att muspekaren är synlig, annars är det svårt att se att muspekaren är över bilden när den ändrar färg.

IsMouseVisible = true;

base.Initialize();

I LoadContent så laddar vi in bilden som ska användas, vilken bild man använder spelar i detta exempel ingen roll. En Rectangle skapas som har samma bredd och höjd som den bild som används.

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

I Updatemetoden så kommer det som faktiskt är nytt i detta exempel: användandet av metoden Contains. Om den returnerar true så ändras bildens färg till röd, annars får den ritas så som den är.

mus = Mouse.GetState();
if (parrotRect.Contains(mus.Position) == true)
{
    parrotColor = Color.Red;
}
else
{
    parrotColor = Color.White;
}

base.Update(gameTime);

Draw-metoden ritar upp vår bild med den Rectangle och den färg som hör ihop med den.

GraphicsDevice.Clear(Color.CornflowerBlue);

spriteBatch.Begin();
spriteBatch.Draw(parrotBild, parrotRect, parrotColor);
spriteBatch.End();
base.Draw(gameTime);

Med hjälp av Contains-metoden så kan man enkelt låta en bild fungera som en knapp. Man kan undersöka om användaren har tryckt på knappen genom att kolla om muspekaren är inuti rektangeln samtidigt som den vänstra musknappen precis har blivit nedtryckt.

Uppgift P5.1

Gör om exempelprogrammet så att den uppritade bilden fungerar som en knapp. När man trycker med musen på bilden så ska den byta färg, första gången ska den bli röd, sedan vanlig, sedan röd o.s.v. När man enbart håller muspekaren över bilden så ska ingenting hända.

Lösningstips P5.1

Lägg till ett nytt MouseState som du kallar för gammalMus.

Lösningsförslag P5.1

Planering och implementering av Träffa ormen

Vi kommer bygga upp detta spel steg för steg och inte planera t.ex. alla medlemsvariabler på en gång i början, vi fyller istället på med mer medlemsvariabler när det behövs. När man gör ett större program så är det omöjligt att planera det i förväg helt och hållet och det är bra att du får se hur ett program kan byggas upp med tiden.

Vi ska ända börja med att lägga till några medlemsvariabler i spelet som vi kommer att använda senare. Man kommer att trycka på ormarna med musen så vi kommer att behöva MouseState för att undersöka den, ormarna som skapas ska placera på slumpade positioner så vi behöver en Random-variabel och vi kommer att behöva skriva text och därmed behövs ett SpriteFont. Programmet ska ha två olika lägen, eller två olika scener, en scen som visar programmets meny och en scen som körs när man spelar själva spelet. Vi skapar en int-variabel som håller koll på vilken scen som är den aktuella scenen, om den har värdet 0 så är programmet i meny-scenen och om den är 1 så är programmet i spel-scenen. Glöm inte bort att lägga till följande using-uttryck för att kunna använda Random.

using System;

Här är koden för de medlemsvariabler vi behöver än så länge.

MouseState mus = Mouse.GetState();
MouseState gammalMus = Mouse.GetState();
SpriteFont arial;

Random slump = new Random();

// 0 för meny, 1 för spel
int scen = 0;

SpriteFont-filen som används är ändrad så att teckenstorleken är 36 och svenska tecken tillåts.

Hålla koll på scenen

I vårt program använder vi en int-variabel som håller koll på vilken scen programmet ska visa. Man hade kunnat använda en string istället och ha scennamnen "Meny" och "Spel" men det är lättare att stava fel till ett helt ord än ett tal, så därför har vi valt en int.

Hade vi lärt oss mer om enum-variabeltyper så hade vi kunnat skapa en egen variabeltyp som enbart hade ett visst antal tillåtna värden, dessa värden hade då kunnat vara våra scennamn.

Eftersom musen ska användas för att trycka på en knapp i huvudmenyn och på ormarna i spelet så är det bra om muspekaren syns när programmet kör, det ändrar vi i programmets Initialize-metod.

IsMouseVisible = true;

base.Initialize();

För att få en bra struktur på vår kod så är det viktigt att dela upp vårt program i metoder och vi kommer göra så att vi separerar koden som hör ihop med de olika scenerna i spelet. Programmets Update-metod ska därför se ut såhär:

gammalMus = mus;
mus = Mouse.GetState();

switch (scen)
{
    case 0:
        UppdateraMeny();
        break;
    case 1:
        UppdateraSpel();
        break;
}

base.Update(gameTime);

Det enda vi gör i Update-metoden är att först updatera våra MouseState-variabler, därefter så använder vi en switch-sats för att undersöka värdet av variabeln scen som håller koll på vilken scen som programmet ska visa just nu. Koden som styr varje scen finns i var sin metod som har returntypen void och saknar parametrar, dessa metoder heter UppdateraMeny och UppdateraSpel. Vi har inte skapat dessa metoder än men vi kommer att göra det snart, du kan själv ta och skapa dem nu utan att skriva någon kod inuti dem så länge.

Koden i programmets Draw-metod ser ut på ett väldigt liknande sätt som i Update-metoden. Du kan passa på att skapa även metoderna RitaMeny och RitaSpel själv nu utan att lägga någon kod i dem, även dessa metoder ska ha returntypen void.

switch (scen)
{
    case 0:
        RitaMeny();
        break;
    case 1:
        RitaSpel();
        break;
}

base.Draw(gameTime);

Med denna struktur färdig för programmet så kan vi börja skriva koden för varje scen separat, vi börjar med menyscenen.

Uppdelning av LoadContent

Vi har inte gjort någon uppdelning av LoadContent-metoden i vårt program i t.ex. LaddaMeny och LaddaSpel, men det hade man kunnat göra om man vill. Metoden LoadContent blir inte så stor i vårt program så vi tyckte inte att det behövdes.

Scenen Meny

Redan i början av kapitlet såg vi hur programmets menyscen ska se ut: den ska ha texten ”Träffa ormen!” samt en knapp som man kan trycka på för att starta spelet. Knappen är gjord av en bild som innehåller text, knapptexten skrivs alltså inte ut med spriteBatch.Draw utan den är en del av bilden.

Träffa Ormen - Knapp

Knappbilden är hämtad från www.kenney.nl som har många gratis bilder som är användbara för spel, texten har sedan lagts till med hjälp av programmet Paint.NET som är ett gratis bildprogram. Notera att Paint.NET inte laddas ner från www.paint.net utan från www.getpaint.net.

Du behöver inte använda en likadan bild för din knapp, du kan själv skapa en bild med texten ”Spela” i med hjälp av valfritt ritprogram. Det spelar ingen roll vilken storlek bilden har eftersom vi kommer att placera den i mitten av fönstret med hjälp av kod ändå.

Vi behöver några nya medlemsvariabler i programmet som hör ihop med menyn. Knappen får en Rectangle som anger var den ska ritas så att vi kan använda metoden Contains när vi undersöker om användaren har tryckt på knappen.

// Menyvariabler
Texture2D knappBild;
Rectangle knappRect;
string välkomstText = "Träffa ormen!";
Vector2 välkomstPosition;

Textens positionsvektor ges inte något värde nu, det ska vi göra först senare. Vi ska använda en metod som du inte har sett hittills för att mäta hur bred strängen välkomstText kommer att vara när den ritas upp, detta kommer vi att använda för att placera texten mitt i spelfönstret.

Metoden LoadContent behöver ny kod för att ladda in knappbilden. Vi skapar direkt därefter rektangeln för bilden. Eftersom spelfönstret är 800 pixlar brett så är x-koordinaten 400 i mitten av spelfönstret. Om man ger en rektangel x-koordinaten 400 så får den sin vänstra kant i mitten av spelfönstret, men genom att subtrahera 400 med halva bildens bredd så kommer bildens mittpunkt istället att hamna mitt i spelfönstret vilker ser snyggt ut. På raden efteråt så görs samma sak för texten ”Träffa ormen!”. Med hjälp av metoden MeasureString så får man en Vector2 som anger storleken på texten när den ritas ut, X-koordinaten för denna Vector2 är bredden av texten.

knappBild = Content.Load<Texture2D>("button");
knappRect = new Rectangle(400 - knappBild.Width / 2, 360, knappBild.Width, knappBild.Height);
välkomstPosition = new Vector2(400 - arial.MeasureString(välkomstText).X / 2, 100);

Vi kan redan nu ta och skriva koden som ska rita ut föremålen i menyscenen, det gör vi i vår egenskapade metod RitaMeny och den ser ut så här.

/// <summary>
/// Kod för att rita scenen Meny
/// </summary>
void RitaMeny()
{
    GraphicsDevice.Clear(Color.CornflowerBlue);

    spriteBatch.Begin();
    spriteBatch.DrawString(arial, välkomstText, välkomstPosition, Color.White);
    spriteBatch.Draw(knappBild, knappRect, Color.White);
    spriteBatch.End();
}

Kvar för menyscenen är koden som i vanliga fall hade stått i programmets Update-metod. Den ska vi skriva i vår egenskapade metod UppdateraMeny. Innan vi skriver koden här så planerar vi den med psuedokod.

/// <summary>
/// Update-kod för scenen Meny
/// </summary>
void UppdateraMeny()
{
    // if (VänsterMusKnapp precis trycktes && muspekaren är över knappbilden)
        // Byt scen till spelscenen
}

Vi vill undersöka om den vänstra musknappen precis har tryckts ned. Detta kommer vi att göra även i spelscenen när man trycker på ormarna och då är det bra att lägga koden som undersöker detta i en egen metod så att vi inte behöver upprepa oss. Detta gör vi i metoden VänsterMusTryckt.

/// <summary>
/// Undersöker om den vänstra musknappen precis blev nedtryckt
/// </summary>
/// <returns></returns>
bool VänsterMusTryckt()
{
    if (mus.LeftButton == ButtonState.Pressed && gammalMus.LeftButton == ButtonState.Released)
    {
        return true;
    }
    else
    {
        return false;
    }
}

Nu skulle vi egentligen kunna skriva koden i UppdateraMeny, men vi ska faktiskt ta och lägga till ytterligare en metod till i programmet. Denna metod ska heta BytScen och den ska anropas varje gång man vill byta från en scen till en annan. Det kan kännas onödigt än så länge i programmet men det är något som du kommer att ha nytta av när du jobbar vidare med det längre fram. När man byter scen i ett spel så behöver man ofta köra viss kod som ser till att scenen är redo att startas upp, och om vi inte lägger denna kod i en egen metod så är det risk att vi behöver upprepa oss på många olika ställen i koden som vill ändra vilken scen som är programmets aktiva scen. Vi kommer att lägga till kod i denna metod när vi implementerar spelscenen om ett litet tag, men än så länge ser denna metod endast ut så här.

/// <summary>
/// Byter scen till det scen-ID som anges
/// </summary>
/// <param name="nyscen"></param>
void BytScen(int nyscen)
{
    scen = nyscen;
}

Nu är vi redo att binda ihop metoderna i koden för UppdateraMeny.

/// <summary>
/// Update-kod för scenen Meny
/// </summary>
void UppdateraMeny()
{
    if (VänsterMusTryckt() == true && knappRect.Contains(mus.Position) == true)
    {
        BytScen(1);
    }
}

Med detta skriver så är koden som kontrollerar spelets meny färdig, vi har en fungerande knapp som man kan trycka på för att komma till spelscenen. Än så länge händer det inte någonting i spelscenen, den ritas inte ens upp, men det ska vi ta och ändra på nu.

Scenen Spel

När man startar spelscenen så ska det finnas ett antal slumpmässigt placerade ormar som spelaren ska trycka på. Med jämna mellanrum ska det dessutom dyka upp nya ormar på skärmen. Vi kommer behöva lite nya medlemsvariabler för denna scen, innan vi skapar dem så måste du lägga till följande using-uttryck för att kunna använda listor i ditt program.

using System.Collections.Generic;

Bilden av ormen som används är hämtad från www.kenney.nl.

// Spelvariabler
List<Rectangle> ormar = new List<Rectangle>();
Texture2D ormBild;
int startAntalOrmar = 5;
int updatesMellanNyaOrmar = 90;
int updatesTillNästaOrm = 90;

Vi använder en lista av rektanglar för att hålla koll på var ormarna i spelet är, då kan vi använda metoden Contains när vi ska undersöka om spelaren har tryckt på en orm. Vi låter även en del av spelscenens inställningar sparas i variabler här så att de blir enkla att ändra på, som hur många ormar det ska finnas från början och hur lång tid det ska ta innan en ny orm dyker upp.

Metoden LoadContent behöver endast en ny kodrad, den som laddar in bilden på ormen.

ormBild = Content.Load<Texture2D>("snake");

Vi kan direkt ta och skriva koden som ritar upp alla ormarna som finns eftersom den inte blir så lång. Detta gör vi i metoden RitaSpel som du redan har skapat. Vi går helt enkelt igenom hela listan med rektanglar och ritar upp ormbilden en gång per rektangel. Notera att vi har valt att ha en annan bakgrundsfärg i denna metod inuti GraphicsDevice.Clear än vad vi hade för menyn, det blir då ännu tydligare att se att spelet har bytt scen.

/// <summary>
/// Kod för att rita scenen Spel
/// </summary>
void RitaSpel()
{
    GraphicsDevice.Clear(Color.Tomato);

    spriteBatch.Begin();
    foreach (Rectangle ormRect in ormar)
    {
        spriteBatch.Draw(ormBild, ormRect, Color.White);
    }
    spriteBatch.End();
}

För att se att ritkoden fungerar som den ska så behöver listan ha några rektanglar i sig så det blir nästa steg att fixa. Vi ska göra det genom att lägga till kod i metoden BytScen, men innan vi gör det så ska vi skapa en annan metod. Vi kommer att behöva lägga till nya ormrektanglar i listan både när vi byter scen och med jämna mellanrum när spelscenen är igång och för att slippa upprepa oss så lägger vi denna kod i en metod som heter LäggTillOrm.

/// <summary>
/// Lägger till en ny orm på en slumpad position
/// </summary>
void LäggTillOrm()
{
    int nyOrmX = slump.Next(0, 800 - ormBild.Width);
    int nyOrmY = slump.Next(0, 480 - ormBild.Height);
    Rectangle nyOrmRect = new Rectangle(nyOrmX, nyOrmY, ormBild.Width, ormBild.Height);
    ormar.Add(nyOrmRect);
}

Ormens X och Y-koordinater slumpas så att ormen inte kan hamna utanför spelfönstret, inte ens delvis. Därefter så läggs ormrektanglen till i listan över alla ormar som finns. Nu är vi redo att ändra koden i metoden BytScen.

/// <summary>
/// Byter scen till det scen-ID som anges
/// </summary>
/// <param name="nyscen"></param>
void BytScen(int nyscen)
{
    scen = nyscen;

    if (nyscen == 1)
    {
        ormar.Clear();
        for (int i = 0; i < startAntalOrmar; i++)
        {
            LäggTillOrm();
        }
        updatesTillNästaOrm = updatesMellanNyaOrmar;
    }
}

Det som är nytt i metoden är if-satsen som undersöker om den nya scenen är spelscenen, d.v.s. scenen som har id 1. Om den är det så kommer denna scen att initialiseras, den görs redo genom att listan med ormar först töms med metoden Clear om ormlistan skulle innehålla några ormar sedan förra gången spelet spelades. Därefter så lägger vi till det antal ormar i listan som är sparat i variabeln startAntalOrmar. Slutligen så ser vi till att updatesTillNästaOrm som håller koll på hur lång tid det är till en ny orm ska skapas har ett korrekt startvärde.

Nyttan med BytScen-metoden

Nu kan vi se nyttan med metoden BytScen: Om vi har någon kod som ska köras vid ett scenbyte så kan vi placera den här. Då är vi säkra på att den alltid körs oavsett var i programmet man byter scen och det blir dessutom lätt för oss att hitta koden som körs vid ett scenbyte. Detta blir ännu mer användbart om vi lägger till ännu fler scener till spelet.

Det enda som återstår i hela spelet är koden i metoden UppdateraSpel. Innan vi skriver den så gör vi en kort beskrivning i pseudokod.

/// <summary>
/// Update-kod för scenen spel
/// </summary>
void UppdateraSpel()
{
    // updatesTillNästaOrm--
    // if (updatesTillNästaOrm <= 0)
    //    Lägg till en ny orm i listan och starta om nedräkningen
    // if (VänsterMusTryckt == true)
    //    Gå igenom listan med ormar
    //    Ta bort den första ormen som innehåller muspekaren
    // if (ormlistan är tom)
    //    Byt scen till menyscenen

}

Variabel updatesTillNästaOrm håller koll på när det är dags att skapa en ny orm. Eftersom den har startvärdet 90 och Update-metoden i MonoGame kör 60 gånger per sekund så kommer det att skapas nya ormar med 1,5 sekunders mellanrum.

När vi går igenom hela listan med ormar så gör vi det med en for-loop istället för en foreach-loop eftersom man inte kan ta bort ett element från en lista inuti en foreach-loop. När man tar bort ett element från en lista så flyttas alla element som ligger senare i listan, d.v.s. alla element med högre index, framåt i listan genom att deras index minskar med 1. Om man går igenom en lista med en vanlig for-loop och tar bort ett element i listan inuti for-loop så kommer man att hoppa över ett av elementen i listan eftersom indexet ändras för alla element som man inte har undersökt än. Genom att gå igenom listan baklänges så slipper man detta problem, när man tar bort ett element i listan så påverkas endast indexen för de element som man redan har undersökt.

/// <summary>
/// Update-kod för scenen spel
/// </summary>
void UppdateraSpel()
{
    updatesTillNästaOrm--;
    if (updatesTillNästaOrm <= 0)
    {
        LäggTillOrm();
        updatesTillNästaOrm = updatesMellanNyaOrmar;
    }

    if (VänsterMusTryckt() == true)
    {
        for (int i = ormar.Count - 1; i >= 0; i--)
        {
            if (ormar[i].Contains(mus.Position))
            {
                ormar.RemoveAt(i);
                break;
            }
        }
    }

    if (ormar.Count == 0)
    {
        BytScen(0);
    }
}

Den sista if-satsen i denna metod gör så att man kommer tillbaka till huvudmenyn när man har lyckats ta bort alla ormar och användaren får då möjlighet att spela igenom genom att trycka på huvudmenyns knapp.

Hela programmet

Följ länken nedan för att se koden till hela programmet.

Hela programmet

Förslag på förändringar och utökningar av Träffa ormen

Här följer förslag på vad du själv kan göra för att bygga ut Träffa ormen. Du väljer själv hur mycket du vill göra och vad du har tid med, men du rekommenderas att åtminstone genomföra delen ”Hjälpscen”.

Hjälpscen

Lägg till en ny scen i spelet. Denna sen ska vara en hjälpscen som kortfattat förklarar vad spelet går ut på. Man ska kunna komma till hjälpscenen via en knapp i huvudmenyn och man ska kunna gå tillbaka till huvudmenyn med en knapp inuti hjälpscenen.

Musik och ljudeffekter

Lägg till passande musik och ljudeffekter till ditt spel, t.ex. när man trycker på en orm.

Förlust

Gör så att det är möjligt att förlora spelet om det finns för många ormar på skärmen samtidigt.

Scen när spelet är slut

När spelet är slut, oavsett om man vann eller förlorade, så ska man komma till en scen som berättar lite statistik om den senaste omgången. Denna scen ska ha en knapp som leder tillbaka till huvudmenyn. Antingen så kan du göra en scen som man kommer till oavsett om man vann eller förlorade, eller så gör du en scen för vinst och en scen för förlust.

Timer och high score

Lägg till en ny scen som man ska kunna komma till med hjälp av en knapp från huvudmenyn. Denna scen ska visa den snabbaste vinsten som spelaren har fått. Lägg till en timer i spelscenen som räknar antalet sekunder som spelscenen har varit igång. Gör även så att det finns en maxgräns på hur lång tid man har på sig.

Olika svårighetsgrad

Gör en ny scen där man kan ändra svårighetsgrad för spelet. Svårighetsgraden ska avgöra hur många ormar som finns på plats från början och hur lång tid det tar innan det skapas en ny orm. Man ska komma åt denna scen via en knapp i huvudmenyn.

Förslag på egen projektuppgift – Space Invaders

Skapa en kopia av det klassiska spelet ”Space Invaders”. I Space Invaders så styr spelaren ett rymdskepp som finns längst ner i spelfönstret som kan flyttas i X-led. Rymdskeppet kan skjuta skott som åker rakt uppåt mot de utomjordingar som rör sig ner på skärmen. Utomjordingarna skjuter då och då skott som rör sig nedåt mot spelarens rymdskepp och dessa måste spelaren akta sig för. Om någon utomjording lyckas ta sig ner till spelarens Y-position så förlorar spelaren spelet, man vinner genom att förstöra alla utomjordingar.

Space Invaders

Om du lyckas få ett fungerande Space Invaders så kan du bygga ut det med en huvudmeny, olika svårighetsgrader och olika utomjordingar som fungerar på olika sätt.