DirectX - AI - Spil med modspil

AI (Artificial Intelligense)

Kunstig intelligens (Artificial Intelligence) indbygges i de spil, hvor computeren enten skal yde modspil til spilleren, eller hvor computeren f.eks. skal hjælpe spilleren med at færdiggøre en påbegyndt opgave. For at illustrere den første af de nævnte muligheder, vil vi konstruere et spil, som vi vil kalde Vektor Ræs. I dette spil skal spilleren kunne spille mod computeren, som derfor skal have indbygget en algoritme med kunstig intelligens, således at computeren kan foretage fornuftige og i bedste fald vindende træk i spillet mod den menneskelige modstander.

  1. Vektor Ræs (ideen bag spillet)
  2. Level Designer
  3. Alternativ Level Designer
  4. The Engine
  5. Modellen (opbygning af banen)
  6. Håndtering af spillerens input
  7. Introduktion til AI (modspil fra computeren)
  8. Håndtering af mere komplekse situationer
  9. Den sidste finish

1. Vektor Ræs

Vektor Ræs spilles på en spilleplade med et antal prikker (90 X 90) opbygget i et gitter. Herpå anbringes en bane, som konstrueres med en såkaldt Level designer. Ved hjælp af denne kan man bygge flere baner, og på den måde hele tiden udbygge spillet med nye udfordringer.

Selve spillet foregår på den måde, at to (eller flere) spillere anbringer deres brik (racer) på et punkt på mållinjen. Der må ikke på noget tidspunkt placeres to brikker i samme punkt samtidig, hvorimod en spillebrik godt i næste træk må genbruge en placering, som blev brugt af en anden brik i et tidligere træk. Hver gang flytter man sin brik efter følgende regler:

Brikken må flyttes lige så langt i X- og Y-retningerne, som den blev flyttet i sidste træk. Desuden må den nu bevæge sig 1 position længere eller kortere i såvel X- som Y-retningen. Dette giver et maksimum på 9 mulige placeringer i trækket; men antallet af mulige placeringer kan være mindre, fordi man er tæt ved banens kant eller fordi nogle af punkterne er optaget af andre spilleres brikker. Også ved svingene på banen kan der være færre muligheder for at placere brikken, fordi det ikke er tilladt at skære hjørner.

Herunder ses grundplanen for spillet. Det eneste, der er til syne fra starten, er de hvide prikker, som i virkeligheden er Brick objekter med størrelsen 0.2. Aktuelt ser man kun et lille udsnit af de 8100 prikker - det er nødvendigt at zoome ind på brikkerne for at kunne se spillet tilstrækkeligt tydeligt. Projektionsmatricen sættes op i Engine objektet, hvor man kan eksperimentere med forskellige opsætninger.

Positioneringen af kameraet sker dog i Model objektet, således at kameraet hele tiden sættes til at følge den brik, spilleren styrer. Måske kan man gøre det sådan, at det bliver muligt at flytte kameraet til en af de andre brikker, for at se, hvordan den er positioneret. Øverst til venstre i skærmbilledet er der et panel, hvor man kan give spilleren forskellige informationer, som kan være nyttige i forbindelse med konstruktion af trækket. Til sving skal oplyse om afstanden til næste sving, DeltaX og DeltaY oplyser om den relative hastighed i de to retninger.

Selve banen forestiller jeg mig opbygget af et antal standardformer, således som afbildet herunder:

Modul 1 Modul 2 Modul 3 Modul 4 Modul 5 Modul 6 Modul 11

Disse skabeloner anvendes som mønster for opbygningen af selve banen. Med Level Designeren skabes der mulighed for at generere data på en fil, som kan indlæses og omdannes til en bane med de ønskede mønstre. Modul 11 udgør start og mållinie - her placeres spillernes brikker fra start. Som konvention har jeg vedtaget, at man altid starter med at bevæge sig mod højre, når man står på mållinien.

2. Level Designer

Inden vi for alvor går i gang med konstruktion af spillet, laver vi et andet program - en Level Designer.

Ideen med en Level Designer er at give mulighed for at designe nye baner, så man kan lave nye udfordringer. I den forbindelse skal man tage stilling til, hvordan oplysninger om banens opbygning kan udveksles via filer mellem Level Designeren og selve spilprogrammet.

Her har jeg valgt et format bestående af et to-dimensionalt array af heltal. Hvert heltal repræsenterer en bestemt udformning af netop den sektion af banen, som heltallet repræsenterer. Et heltal omsættes altså til et bestemt mønster bestående af 81 brikker i et 9 X 9 gitter. Sammensætningen af de 10 X 10 mønstre bestående af hver 81 brikker giver i alt 8100 brikker.

I dette tilfælde udarbejdes et ganske almindeligt Windows program. Der er mange muligheder for opbygning af skærmbilledet; men jeg har valgt at placere en TableLayoutPanel komponent i skærmbilledet. Heri har jeg defineret et array med 10 rækker og 10 kolonner. I hver celle er der anbragt en TextBox. Brugen af TableLayoutPanel giver en bekvem måde til systematisk behandling af alle de indlejrede TextBox komponenter.

Da vi helst skal kunne indsætte TextBox objekterne i tabellen i en bestemt rækkefølge, har jeg defineret alle TextBox objekternes TabIndex attributter sådan, at de er nummereret fra 0 til 99 rækkevis fra nederste venstre hjørne til øverste højre hjørne. Dette unikke heltal mellem 0 og 99 kan jeg så bruge til at finde den tilsvarende position i et array af heltal.

Koden til Nulstil knappen

	private void Nulstil_Click(object sender, EventArgs e)
	{
		for (int i = 0; i < 10; i++)
			for (int j = 0; j < 10; j++)
				array[i, j] = 0;
		foreach (TextBox t in tableLayoutPanel1.Controls)
		{
			t.Text = array[t.TabIndex % 10, t.TabIndex / 10].ToString();
		}
	}

Tabellen med heltallene nulstilles og derefter flyttes tabellens enkelte værdier ud i TextBox objekterne.

Koden til Gem Som knappen

	private void GemSom_Click(object sender, EventArgs e)
	{
		foreach (TextBox t in tableLayoutPanel1.Controls)
		{
			array[t.TabIndex % 10, t.TabIndex / 10] = int.Parse(t.Text);
		}
		sfd.ShowDialog();
		FileStream fs = new FileStream(sfd.FileName, FileMode.OpenOrCreate, FileAccess.Write);
		BinaryFormatter bf = new BinaryFormatter();
		bf.Serialize(fs, array);
		fs.Close();
	}

Først overføres indholdet af de 100 TextBox objekter til tabellen. Dernæst serialiseres tabellen, hvorefter den streames ud på en fil. Filnavnet findes ved kald af en SaveFileDialog. Koden er meget rudimentær, idet der mangler test for eksistens af filnavne osv.

Koden til Hent knappen

	private void Hent_Click(object sender, EventArgs e)
	{
		ofd.ShowDialog();
		FileStream fs = new FileStream(ofd.FileName, FileMode.Open, FileAccess.Read);
		BinaryFormatter bf = new BinaryFormatter();
		array = (int[,])bf.Deserialize(fs);
		fs.Close();
		foreach (TextBox t in tableLayoutPanel1.Controls)
		{
			t.Text = array[t.TabIndex % 10, t.TabIndex / 10].ToString();
		}
	}

Navnet på den fil, der ønskes indlæst findes ved kald af en OpenFileDialog. Dernæst indlæses og deserialiseres indholdet af filen, som konverteres til indhold i tabellen. Tabellens indhold overføres til slut til de 100 TextBox objekter i skærmbilledet.

Koden til Gem knappen

Denne knap forudsætter, at der forud er hentet data ind ved hjælp af Hent knappen, fordi det filnavn, der blev brugt her, genanvendes nu.

	private void Gem_Click(object sender, EventArgs e)
	{
		foreach (TextBox t in tableLayoutPanel1.Controls)
		{
			array[t.TabIndex % 10, t.TabIndex / 10] = int.Parse(t.Text);
		}
		FileStream fs = new FileStream(ofd.FileName, FileMode.OpenOrCreate, FileAccess.Write);
		BinaryFormatter bf = new BinaryFormatter();
		bf.Serialize(fs, array);
		fs.Close();
	}

Først overføres indholdet af de 100 TextBox objekter til tabellen. Dernæst serialiseres tabellen, hvorefter den streames ud på en fil.

Koden til LevelDesigneren kan hentes her.

3. Alternativ Level Designer

Den i punkt 2 viste Level Designer har ingen særlige finesser indbygget - den er udelukkende konstrueret med det formål for øje, at der hurtigt skal kunne konstrueres en bane, som kan bruges til afprøvning af spillet, når vi senere går i gang med at udvikle det.

En af de studerende i DM052, Torben Laursen, tog udfordringen op, og udviklede en Level Designer med et bedre grafisk interface, så man også visuelt får en fornemmelse af, hvordan banen kommer til at se ud. Torben har givet mig lov til at gøre koden til denne Level Designer tilgængelig her, så man kan se, hvordan den er konstrueret.

Først et skærmdump, som giver et godt indtryk af, hvordan programmet virker. Det er hurtigt og effektivt at opbygge baner med dette program.

Programmet er ganske let og intuitivt at arbejde med. Koden kan hentes via dette link.

4. The Engine

Selve spillet opbygges som sædvanlig omkring en Game Engine - jeg har i dette tilfælde foretrukket genvalg af den Engine, som vi allerede har har brugt et par gange. Dette afsnit er medtaget for at præcisere nogle af de få ændringer - eller man skulle måske snarere sige præciseringer, som er hensigtsmæssige i forhold til det aktuelle spil.

Spilleplanen opbygges af 3D brikker (terninger) med størrelsen 1 i alle dimensioner. Placeringen sker i koordinatsystemets øverste højre kvadrant, således at brikkernes placering følger placeringen i den tabel, som internt i hukommelsen repræsenterer spillepladen.

View transformationen sættes som sædvanlig i forbindelse med opstart af vores Engine:

	View = Matrix.LookAtLH(new Vector3(30, 50, -30), new Vector3(30, 50, 0), new Vector3(0, 1, 0));

View transformationsmatricen ændres i øvrigt i modellen:

Som projektionsmatrice anvendes i dette tilfælde en ortogonal projektion fremfor en perspektiv projektion. Selvom der anvendes 3D objekter, er det kun forsiden af objekterne, der skal kunne ses, så en ortogonal projektion, hvor man ser objekterne lige forfra er helt perfekt til netop dette spil.

	Projection = Matrix.OrthoLH(50, 50, 1.0f, 100.0f);

Styringen af spillet skal ske med tastaturets knapper, så vores Engine forsynes med en metode, der kan fange input fra tastaturet og sende det til videre behandling i modellens metoder.

	private void Keydown(Object sender, System.Windows.Forms.KeyEventArgs e)
	{
		theModel.Keyboard = e.KeyCode;
		if (e.KeyCode == Keys.Escape)
			Application.Exit();
		if (e.KeyCode == Keys.F12)
			theModel.HentBane();
	}

Som det ses behandles knapperne Escape og F12 her - piltasterne og returtasten, der bruges til flytning af spillerens brik, behandles i Moellens UpdateModel metode, der kaldes fra hovedloopet i vores Engine.

5. Modellen

Model klassen indeholder en række funktioner, som er centrale for spillet; men her vil vi første omgang se på metoden HentBane, der kaldes, når en ny bane ønskes indlæst.

I modellen arbejdes med to arrays, theField og theArray:

	private Brick[,] theField;
	private int[,] theArray = new int[10, 10];

Tabellen theField kommer til at indeholde 8100 objekter af typen Brick. Brick objektet har vi arbejdet med i et tidligere spil, og det er den vigtigste grund til at det genanvendes her. Det indeholder en tredimensional terning - i virkeligheden kunne vi arbejde med en simplere 2D model; men fordelen ved at genbruge Brick objektet er, at vi ved, hvordan det opfører sig, og hvordan vi skal manipulere med det.

Tabellen theArray indeholder informationer om banens opbygning. Disse data er repræsenteret ved 10X10 heltal, som er gemt i en fil. Denne fil bliver opbygget med en Level Designer, som vi tidligere har konstrueret (se her).

Opgaven i metoden HentBane går ud på at indlæse data for den ønskede bane fra en fil og lagre disse data i tabellen theArray. Herefter omdannes disse data til Brick objekter i tabellen theField. Hvert tal i theArray omdannes til en blok bestående af 81 Brick objekter i et 9 X 9 gitter.

Indlæsning af filen sker med følgende kode:

	OpenFileDialog ofd = new OpenFileDialog();
	ofd.ShowDialog();
	FileStream fs = new FileStream(ofd.FileName, FileMode.Open, FileAccess.Read);
	BinaryFormatter bf = new BinaryFormatter();
	theArray = (int[,])bf.Deserialize(fs);
	fs.Close();

Opbygningen af banen sker i en switch/case sætning, hvoraf vi her ser starten:

	switch (theArray[i, j])
	{
		case 0:
		for (int x = 0; x < 9; x++)
			for (int y = 0; y < 9; y++)
				theField[i * 9 + x, j * 9 + y].Update(Color.Green, 1.0f, 9);
		break; 

Denne type bruges til opbygning af alle de dele af spillepladen, hvor der ikke er nogen vejbane. Alle brikker gøres grønne.


Oprettet: 21. marts 2007
Sidst opdateret: 09. april 2007

Index