2. Moment02 - Objektorientering

I detta moment skall vi kika på objektorientering, OOP (object-oriented programming), och hur vi kan använda denna teknik för att bygga tydligare och mer generella applikationer. Vi kommer lära oss att bygga klasser och skapa objekt vilket är grunden inom den objektorienterade programmeringen.

Objektorientering är en viktig del av de objektorienterade programmeringsspråken. Det är ett sätt att försöka anpassa programmeringen mot den riktiga världen där helt andra datatyper finns än just de datatyper som finns inbyggt i programmeringsspråket. När vi skall sätta samman information om någon person eller en bil så räcker det inte att bara förlita sig på bara de enkla variabler som finns i en applikation. Det är då vi vill använda oss av objekt.

Momentet börjar med en teorigenomgång frikopplad från något specifikt programmeringsspråk innan vi kikar på hur OOP fungerar i programmeringsspråket C#. Detta gör vi genom att följa en tutorial och bygga en klass och lär oss använda denna i en applikation. Slutligen så börjar vi med den tutorial som vi skall jobba med i stora delar av kursen. Den tutorial som bygger oss ett spel i C#.

2.1 Teori

Vi börjar med en allmän genomgång av OOP. Denna presentation är bortkopplad från något specifikt programmeringsspråk. Senare i momentet kommer vi kika på hur vi jobbar med objektorientering specifikt i C#.

2.2 OOP & C# genom två tutorials

2.2.1 Objektorienteringens grunder

För att lära dig objektorientering i C# så har jag byggt en tutorial där du skall skapa klassen Car och sedan jobba med denna klass genom objektorienteringens grunder.

Uppgift

Genomför tutorial ovan och redovisa sedan ditt arbete enligt instruktionerna under punkten Redovisning nedan.

2.2.2 Att lagra objekt i en samling

Dags för nästa tutorial, denna gången bygger vi vidare med klassen Car och nu skall vi bygga en applikation kring våra bilar så att de går att lagra i en lista, lägga till, ta bort och tömma listan. Klicka här för att jobba med tutorialen.

Uppgift

Genomför tutorial ovan och redovisa sedan ditt arbete enligt instruktionerna under punkten Redovisning nedan.

2.2.3 Redovisning

Din lärare bestämmer och meddelar om, och isf hur, dessa uppgifter skall redovisas.

2.3 Speltutorial

Vi kommer arbeta med ett läromedel, skriven av Krister Trangius, som har en bra tutorial för hur spel tillverkas. Även om boken är skriven till en tidigare kursplan, före HT-17, så finns det många saker som är bra och som vi kan dra nytta av.

2.3.1 Spelets grundstruktur

Introduktion till spelutveckling och hur våra projekt ser ut finns i boken men jag kommer här gå igenom det viktigaste. Numreringen härifrån kommer följa bokens kapitelnumrering.

Kapitel 5. Komma igång med XNA och MonoGame

Monogame är ett ramverk för att skapa spel för olika plattformar. Det finns flera liknande ramverk som underlättar för dig att skapa spel där, Godot, Unity och Unreal kan nämnas bland de större.

Tidigare fanns XNA, utvecklat av MicroSoft, men sedan 2013 vidareutvecklas inte detta ramverk och främst MonoGame har ersatt. Att MonoGame har ersatt beror på att koden för dessa båda är lika och det skall gå att kompilera gamla spel som utvecklats för XNA i MonoGame.

Om du inte redan har installerat en utvecklingsmiljö i Windows så hittar du instruktioner för detta i kunskapsdokument: Windowsmiljö.

Kapitel 6. Introduktion till spelutveckling

När du skapar ett nytt spelprojekt i MonoGame så kommer du att få lite grundläggande kod som behövs för att spelet skall fungera. I denna koden så finns det några förskapade metoder som vi behöver bekanta oss med.

Kapitel 6.1 Att skapa ett första projekt

I kunskapsdokument: Windowsmiljö finns det en enkel guide för hur ett projekt i MonoGame skall skapas. Gör det gärna på egen hand så kan du följa kommande kommentarer om den kod du ser i projektet.

Kapitel 6.2 Spel-loopen

När vi utvecklar spel så kommer vi jobba med något som kallas händelsebaserad programmering, vilket innebär att det är händelser som styr vad skall ske i spelet. Vi har tidigare sett detta i Python/Turtle då vi kan styra en figur, eller när vi byggde GUI i Python och på det sättet kunde aktivera olika händelser genom att klicka på knappar eller skriva in en text i ett formulär.

Inom spelprogrammering så kan inte spelet endast vänta på att vi skall göra något, då skulle ju spelet stå helt still tills vi väljer att flytta vår figur. Det skulle ju bli tråkiga spel om inget hände under tiden.

Därför så finns det en spel-loop (game loop) som ser till att spelet hela tiden fortsätter. Denna spel-loop har en viss frekvens då den uppdaterar vårt spel. För MonoGame så kommer den att uppdatera spelet 60 gånger per sekund så det som vi egentligen ser är 60 utritade stillbilder per sekund, enheten för detta är fps (frames per second). Hastigheten på dessa bilder gör att vi ser det som ett rörligt spel.

Kapitel 6.3 Metoden Main()

I metoden Main() skapas ett objekt av typen Game1 och sedan anropas metoden Run(). Run är metoden som kommer se till att spelet körs med hastigheten 60 fps och att spelet hela tiden kommer fortgå. Varför objektet game skapas på detta sätt beskrivs kort i boken för den som är intresserad. För kommande arbete är det inte så viktigt.

6.4 Livscykeln för ett spel i MonoGame

Metoden Run() är viktig för spelet men det är samtidigt ingen metod som vi kommer skriva kod i. Metoden har som uppgift att se till att spelet loopas 60 ggr/s, att alla kontroller görs och att spelet ritas ut som det är tänkt. Vi kan se denna metoden som vår spelmotor som sköter allt som vi förväntar oss skall fungera.

Det intressanta med denna metoden är att den i sin tur anropar fem andra metoder som vi kan påverka och som kommer driva spelet framåt.

Kodexempel

Metoderna Initialize() och LoadContent() används för att sätta upp variabler och allt vi behöver förbereda inför spelet.

Update() och Draw() är själva spel-loopen, och dessa metoder kommer köras 60 ggr var per sekund så länge spelet pågår.

UnloadContent() är metoden som rensar upp efter oss när spelet är slut.

Kapitel 6.4.1 Klassen Game1

I klassen Game1 finns det två variabler och datatyper som vi behöver hålla koll på.

GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;

GraphicsDeviceManager är en klass som tar hand om kopplingen mellan spelet och grafikkortet.

SpriteBatch är klassen som vi använder oss av för att skriva ut bilder i spelet.

public Game1()
{
	graphics = new GraphicsDeviceManager(this);
	Content.RootDirectory = "Content";
}

I konstruktorn så kopplas objektet graphics till grafikkortet.
Content.RootDirectory = "Content" talar om vart vi kommer lägga de filer som vi vill använda i vårt spel, tex bilder och ljud.

Kapitel 6.4.2 Initialize()

I metoden Initialize() initierar vi de objekt som vi kommer behöva under spelet. Metoden ligger utanför spel-loopen så denna metod kommer endast köras en gång.

protected override void Initialize()
{
  // TODO: Add your initialization logic here

  base.Initialize();
}

base.Initialize() anropar metoden Initialize() från den ärvda klassen Game. Här skriver vi vår egna kod före anropet till basklassens metod. Mer om arv kommer i nästa moment.

Kapitel 6.4.3 LoadContent()

I metoden LoadContent() så laddas content (innehåll) in, här tänker vi oss att bilder, ljud, fonter och annat innehåll skall läsas in. Denna metod ingår heller inte i spel-loopen så den körs också en gång. Det är lite flytande vad som skall göras i denna metod och vad som skall/kan göras i Initialize().

protected override void LoadContent()
{
	// Create a new SpriteBatch, which can be used to draw textures.
	spriteBatch = new SpriteBatch(GraphicsDevice);

	//TODO: use this.Content to load your game content here
}

Här skapas objektet spriteBatch som använder för att kunna rita ut sprites (bilder) på skärmen.

Kapitel 6.4.4 Update()

Metoden Update() är den första metoden som körs inne i spel-loopen, här kommer alla händelser fångas upp och alla beräkningar att göras. Eftersom denna körs 60 ggr/s så finns det 60 möjligheter per sekund att läsa av en tangent som trycks ner, en musrörelse eller en kollision mellan två objekt i vårt spel. Sedan måste alla beräkningar göras så att objekten flyttar på sig, något händer när två objekt kolliderar som påverkar dessa och andra objekt.

protected override void Update(GameTime gameTime)
{
	// For Mobile devices, this logic will close the Game when the Back button is pressed
	// Exit() is obsolete on iOS
#if !__IOS__ && !__TVOS__
	if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
		Exit();
#endif

	// TODO: Add your update logic here

	base.Update(gameTime);
}

Koden här är ett exempel på hur den metoden kan se ut. Den kod som finns där för tillfället är kod som avbryter spelet på flera olika sätt, beroende på vilken plattform som spelet skall köras på.

Här anropar vi även den ärvda metoden Update() när vi har gjort våra specifika anrop. Detta fungerar på samma sätt som i Initialize().

Kapitel: 6.4.5 Draw()

Metoden Draw() är den andra metoden som körs inne i spel-loopen och här kommer nu alla bilder att ritas ut så att vi ser att det har hänt något i spelet. Varje gång Draw() körs så levereras en stillbild över hur det ser ut precis vid det tillfälle då bilden ritas. Även denna metod kommer köras 60ggr/s vilket ger oss upplevelsen att spelet rör sig.

Det är när spelet inte hinner göra alla beräkningar eller rita ut allt som skall ritas ut innan spel-loopens nästa varv börjar som vi upplever att spelet laggar. Datorn hinner helt enkelt inte med allt som skall göras. Då får vi ge mer hårdvaruresurser, skriva effektivare kod eller helt enkelt ta bort delar av spelets detaljer.

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

	//TODO: Add your drawing code here

	base.Draw(gameTime);
}

base.Draw() anropas här på samma sätt som tidigare.

Kapitel 6.4.6 UnloadContent()

Metoden UnloadContent() ligger utanför spel-loopen och körs först när spelet avslutas. Här rensar vi upp och släpper resurser som inte behöver finnas kvar när spelet avslutats. Oftast löser detta sig själv, objekt ligger inte kvar och skräpar i minnet när spelet är slut. Däremot är det viktigt att stänga filer som vi har kopplingar till, kanske en fil där data sparas och där den fortfarande är “öppen”. Det är också viktigt att stänga alla eventuella anslutningar ifall vi bygger spel som fungerar över nätverk.

protected override void UnloadContent()
{
  // TODO: Unload any non ContentManager content here
}

Denna metod skapas inte alltid automatiskt, men så här ser metoden ut. Skapas den inte automatiskt så behöver du inte skapa den själv om du inte behöver den.

Kapitel 6.5 Att rita ut på skärmen.

Bilder som används inom spelprogrammering kallas för sprite. Det finns en historik kring hur man använde sprite på ett speciellt sätt tidigare då våra datorer hade mindre kapacitet. Då var det väldigt viktigt att sprites var i exakta storlekar och så optimerade i storlek som det var möjligt. Ofta byggdes större bilder upp av flera mindre sprites för att optimera datorns arbete. Idag har datorerna en helt annan kapacitet och då behöver vi inte trixa med bilder på samma sätt. Namnet sprite har dock blivit kvar. Med det sagt så bör vi ändå se till att våra bilder har rätt storlek när den läses in i spelet, vilket är bra att tänka på när/om du kommer skapa egna sprites.

2.3.2 Arbete med speltutorial

Har du ännu inte skapa ett eget spelprojekt så är det hög tid. Information hittar i kunskapsdokument: Windowsmiljö.

Nu är det dags att skapa projektet SpaceShooter och börja med bokens tutorial. När projektet är skapat så börjar du med att gå till kunskapsdokument - hjälp till boken som är stöd till boken. Här kommer saker som inte längre stämmer med hur boken gör det att fångas upp. Den första delen i kunskapsdokumentet, “6.5 Lägg till en sprite” visar hur du måste göra för att få det att fungera. I övrigt börjar du på kapitel 6.5.1 i boken och jobbar dig sedan igen kapitel 6 i boken.

Uppgift

När du är klar med kapitel 6 så har du säkerligen en ganska så rörig kod, gå igenom koden, snygga till, kommentera de saker du har gjort medan du kommer ihåg vad du har gjort.

Spara en kopia som innehåller allt som fungerar så här långt så kan du jobba vidare med en kopia i nästa kapitel. Skulle något bli väldigt fel i nästa del så kan du enkelt börja om från ett fungerande projekt istället för att behöva felsöka allt för länge.