Laden...

Memory - Spiel (Klassenentwurf) - mit Quellcode

Erstellt von peterchen72 vor 17 Jahren Letzter Beitrag vor 17 Jahren 14.292 Views
P
peterchen72 Themenstarter:in
66 Beiträge seit 2006
vor 17 Jahren
Memory - Spiel (Klassenentwurf) - mit Quellcode

Dies ist mein erstes C#-Programm (nach dem "hello-world" natürlich). Bisher habe ich nur mit C und ohne Klassen gearbeitet.

Als Start-Projekt habe ich mir ein Memory-Spiel überlegt. Ich habe 24 Panel-Elemente am Bildschirm plaziert und zeichne für die verschiedenen Muster einfach verschiedenfarbige Striche in das Panel. Um auch den Umgang mit Klassen zu lernen, habe ich eine Klasse RsMemoryCard geschrieben (als Ableitung der Klasse Panel). Herausgekommen ist eine Klasse mit Klassenvariablen (quasi globale Variablen) und Membervariablen (bei jeder Instanz verschieden). Den Typ der 24 Panel-Elemente habe ich von Panel nach RsMemoryCard direkt im Quellcode geändert. Die Methoden der Basisklasse OnPaint und OnMouseClick habe ich überschrieben. Vorgenommen habe ich mir, den ganzen Code aus der Klasse Form1 (Startbild) rauszunehmen und in der Klasse RsMemoryCard zu verpacken.

Ich hätte einige allgemeine Fragen dazu:

  1. Schlüsselwort "this"
    Schreibt man das Schlüsselwort immer davor oder eher nicht? Also z. B. "this.BackColor = Color.White" oder "BackColor = Color.White".

  2. Eigenschaften der Form
    Ist es allgemein besser, die Eigenschaften der Form im Eigenschaftsfenster zu ändern oder im Quellcode mit z. B. "this.MinimizeBox = false", damit die Schaltfläche minimieren nicht angezeigt wird? Ich habe hier die meisten Dinge direkt im Quellcode gemacht.

  3. "///summary"
    Ich habe "///summary" verwendet, nur was kann ich damit machen? Dienen die nur um den Qullcode zu dokumentieren, oder kann man mehr damit machen?

  4. Klassenentwurf
    Es gibt nur eine Klasse mit den Methoden ShowMemoryCardFront() und ShowMemoryCardBack(), die aufgerufen werden, wenn ich die Memory-Karte anklicke. Nun wollte ich zum Schluss noch einen Button "Neues Spiel" einfügen und bin an die Grenzen gestossen, da ich ja in einer Instanz der Klasse nicht auch für die anderen Instanzen die Methode ShowMemoryCardBack aufrufen kann. Wie hättet ihr das Problem gelöst? Evtl. mit einer Zwischenklasse, die ein Array mit den 24-Memory-Karten hat?

Danke im Voraus für Eure Mithilfe.

PS: Ist das Memory-Spiel mit den Strichen evtl. zu schwierig für einen kurzweiligen Zeitvertreib?

P
peterchen72 Themenstarter:in
66 Beiträge seit 2006
vor 17 Jahren
Memory - Spiel - nur EXE-Datei

Falls jemand nur das Spiel ohne Quellcode ausprobieren möchte, hier ist nur die EXE-Datei.

Viel Spass.

20 Beiträge seit 2006
vor 17 Jahren

Hallo peterchen72,

ich finde das Spiel an sich nicht schlecht, habe es nur schnell ausprobiert, denn Quelltext noch nicht angeschaut, aber auf Dauer wird das Piepsen über den Lautsprecher nervig, tut mir Leid, aber ich empfinde das so. Ich würde mich da nach einer anderen Lösung umsehen.

Ömil

1.373 Beiträge seit 2004
vor 17 Jahren

Zunächst mal: ein schönes Projekt, weil es ein realistisches Ziel ist und natürlich dem Anwender mehr zu bieten hat als ein "Hello, world!".

Original von peterchen72
Dies ist mein erstes C#-Programm (nach dem "hello-world" natürlich). Bisher habe ich nur mit C und ohne Klassen gearbeitet.

Das merkt man, ist aber ganz natürlich. Keine Sorge, das kommt mit der Zeit. Je länger du objektorientiert programmierst, umso natürlicher wird es.

  1. Schlüsselwort "this"
    Schreibt man das Schlüsselwort immer davor oder eher nicht? Also z. B. "this.BackColor = Color.White" oder "BackColor = Color.White".

Nicht ganz so leicht die Frage. Manchmal muss man natürlich this verwenden, um Mehrdeutigkeiten aufzulösen. Bei anderen Fällen scheiden sich die Geister. Es einige Programmierer, die konsequent this verwenden, wenn auf Member zugegriffen wird. Das ist sicherlich keine schlechte Angewohnheit, vor allem wenn keine Präfixe für Klassen/Instanzvariablen verwendet. Ein konsequenter Umgang mit this ist aber mMn wichtiger als die Frage, ob man es verwendet. Finde also deinen eigenen Stil und halte dich daran.

  1. Eigenschaften der Form
    Ist es allgemein besser, die Eigenschaften der Form im Eigenschaftsfenster zu ändern oder im Quellcode mit z. B. "this.MinimizeBox = false", damit die Schaltfläche minimieren nicht angezeigt wird? Ich habe hier die meisten Dinge direkt im Quellcode gemacht.

Wenn man mit visuellen Tools wie Visual Studio arbeitet, finde ich es ganz nützlich, eben diese auch zu verweden, also derartige Einstellungen auch mit dem Eigenschaftenfester zu setzen. Zudem wandern diese Einstellungen ja in die .designer.cs Datei, sodass die "normale" Code-Datei frei bleibt für den "echten" Code, d.h. Ereignisbehandlungsmethoden usw.
Auch hier gibt es aber kein "falsch" oder "richtig".

  1. "///summary"
    Ich habe "///summary" verwendet, nur was kann ich damit machen? Dienen die nur um den Qullcode zu dokumentieren, oder kann man mehr damit machen?

<summary> ist ein bestimmter tag für die XML-Quellcodedokumentation. Tatsächlich handelt es sich hierbei "nur" um Dokumentation. Man kann allerdings diese Dokumentation aus dem Quellcode extrahieren und daraus eigenständige API-Dokumentationen erstellen, etwa vergleichbar mit der .NET Frameworkreferenz. Wenn man bedenkt, dass Quellcode viel häufiger gelesen als geschrieben wird, ist Dokumentation, sofern sie angebracht, richtig und aktuell ist, vorteilhaft.

  1. Klassenentwurf
    Es gibt nur eine Klasse mit den Methoden ShowMemoryCardFront() und ShowMemoryCardBack(), die aufgerufen werden, wenn ich die Memory-Karte anklicke. Nun wollte ich zum Schluss noch einen Button "Neues Spiel" einfügen und bin an die Grenzen gestossen, da ich ja in einer Instanz der Klasse nicht auch für die anderen Instanzen die Methode ShowMemoryCardBack aufrufen kann. Wie hättet ihr das Problem gelöst? Evtl. mit einer Zwischenklasse, die ein Array mit den 24-Memory-Karten hat?

Deine Idee mit der "Zwischenklasse" ist vielleicht nicht ganz verkehrt. Wenn ich das Programm schreiben würde, würde ich vermutlich folgende Klassen erstellen:

Card - enthält Kartenspezifische Eigenschaften: Motiv, Index, umgedreht ja/nein
Game - enthält die allgemeinen Spieldaten (bei dir zurzeit die statischen Variablen in RsMemoryCard) und Referenzen zu (beispielsweise) 24 Card-Instanzen, die die einzelnen Kartenzustände repräsentieren (z.B. ein Array). Zudem sollten hier die Spielregeln geprüft werden.
CardPanel - gibt eine Karte auf der Form wieder. [Die Trennung zwischen Daten (Card) und ihrer grafischen Wiedergabe (CardPanel) ist sinnvoll!] Um das zu können, enthält ein CardPanel eine Referenz zu der Card, die es wiedergeben soll.
MainForm - Hauptform, die die verschiedenen CardPanels enthält. Enthält zudem eine Referenz auf das Game-Objekt.

Dies nur als grober Überblick über eine mögliche Klasseneinteilung.

Grüße,
Andre

P
peterchen72 Themenstarter:in
66 Beiträge seit 2006
vor 17 Jahren

@VizeOne: Danke für deine ausführlichen Schilderungen, sie haben mir sehr geholfen.

Ich habe mal versucht mit dem Projekt neu anzufangen.

-> Klasse MemoryCard

Eine Memory-Karte (im normalen Spiel) besteht aus folgenden Dingen:

  • einem Motiv
  • einer Vorder- und Rückseite
  • einem Index bzw. einer Position (x, y) auf dem Spielfeld
  • und noch der Eigenschaft, dass ein Pärchen gefunden wurde

Daraus ergibt sich schon mal die erste Klasse MemoryCard


class MemoryCard
{
	private UInt32 motiv;
	private UInt32 index;
	private bool sichtbar;
	private bool treffer;

	...
}

Anmerkung: Es wird nur der Index und nicht eine X-Y-Position verwendet.

-> Klasse Game

Nun benötige ich eine Klasse, die ein Array von 24 Memory-Karten hat, die Karten mischen kann (zufällig Motive zuordnen) und die ein Array von 24 Panel hat, damit ich die Motive auch zeichnen kann. Zusätzlich noch eine Funktion mit der ich auf das Umdrehen einer Karte reagieren kann. Ausserdem noch eine Funktion Refresh, mit der ich die Karten neu zeichnen kann.

Daraus ergibt sich nun die Klasse Game


class Game
{
	static public UInt32 maxTrefferKarten = 2;
	static public UInt32 maxMemoryKarten = 24;
	static private MemoryCard[] memoryCard;
	static private MemoryCardPanel[] memoryCardPanel;
	static private bool sound;
	static private UInt32 versuche;

	public Game()
	{
		memoryCard = new MemoryCard[maxMemoryKarten];
		for (int i = 0; i < maxMemoryKarten; i++)
		{
			memoryCard[i] = new MemoryCard();
		}

		memoryCardPanel = new MemoryCardPanel[maxMemoryKarten];

		sound = true;
	}

	public void Start()
	{
		// Jedes Muster soll x vorkommen (bei insgesamt 24 Memomory-Karten)
		// Karten mischen
		...

		// Motive den Memory-Karten zuordnen
		for (int i = 0; i < maxMemoryKarten; i++)
		{
			memoryCard[i].Motiv = (UInt32)(int)arList[i];
			memoryCard[i].Sichtbar = false;
			memoryCard[i].Treffer = false;
		}

		// Alle Karten neu zeichnen
		for (int i = 0; i < maxMemoryKarten; i++)
		{
			memoryCardPanel[i].ShowBack();
		}

		versuche = 0;
	}

	static public void RefreshMemoryCard(UInt32 index, out UInt32 motiv, out bool sichtbar)
	{
		sichtbar = memoryCard[index].Sichtbar;
		if (memoryCard[index].Treffer)
			motiv = 99; // Motiv 99 = Karte nicht mehr zeichnen
		else
			motiv = memoryCard[index].Motiv;
	}

	static public void ClickMemoryCard(UInt32 index)
	{
		// Anzahl der Versuche erhöhen
		versuche++;

		// Anzahl der aufgedeckten Karten bestimmen
		...

		// Kontrolle, ob Memory-Karte schon gelöst
		...

		// Kontrolle, ob Memory-Karte sichtbar ist -> Memory-Karte umdrehen
		...

		// Kontrolle, ob nicht schon 2 Karten ausgewählt wurden
		...
		MessageBox.Show("Es können nur 2 Karten ausgewählt werden.\nBitte zuerst wieder eine Karte umdrehen!");

		// Kontrolle, ob Muster stimmen
		...
		memoryCard[index].Treffer = true;
		memoryCardPanel[index].ShowFront(99); // Karte nicht mehr zeichnen

		// Kontrolle, ob alles gelöst
		...
		MessageBox.Show("Glückwunsch. Sie haben " + Game.versuche.ToString() + " Versuche benötigt!");
		if (Game.sound)
			Console.Beep();
	}

	...
}

-> Klasse MemoryCardPanel

In der MainForm habe ich 24 Panel-Objekte plaziert. Um jetzt komfortabel auf Ereignisse (insbesondere auf das Klick-Ereignis) zu reagieren, habe ich diese 24 Objekte in einem MemoryCardPanel-Array in der Klasse Game gebündelt. Die Klasse MemoryCardPanel ist eine Ableitung der Klasse Panel. Im Quellcode von der MainForm (MainForm.Designer.cs) habe ich den Typ Panel nach MemoryCardPanel manuell geändert.

Daraus ergibt sich nun die Klasse MemoryCardPanel


class MemoryCardPanel : Panel
{
	private UInt32 index;

	public MemoryCardPanel() 
		: this( 0 )
	{
	}
	public MemoryCardPanel(UInt32 index)
		: base()	// Konstruktor der Basisklasse aufrufen
	{
		this.index = index;
	}

	public void ShowBack()
	{
		// Rückseite der MemoryCard ausgeben
	}

	public void ShowFront(UInt32 motiv)
	{
		// Vorderseite in Abhängigkeit des Motivs ausgeben
	}

	protected override void OnPaint(PaintEventArgs pe)
	{
		UInt32 motiv;
		bool sichtbar;
		Game.RefreshMemoryCard(index, out motiv, out sichtbar);
		if (sichtbar)
			ShowFront(motiv);
		else
			ShowBack();

		// OnPaint der Basisklasse aufrufen
		base.OnPaint(pe);
	}

	protected override void OnMouseClick(MouseEventArgs e)
	{
		Game.ClickMemoryCard(index);

		// OnMouseClick der Basisklasse aufrufen
		base.OnMouseClick(e);
	}

	...
}

-> Klasse MainForm

Die Form enthält 24 Panel-Objekte und einen Start-Button. Bei einem Klick auf ein Panel-Objekt soll ein Ereignis ausgelöst werden. Da ich ja nicht möchte, dass ich bei jedem Panel das Ereignis Paint und Click abfangen möchte, werden die Panels in einem Array gebündelt. Es enthält eine variable game. Der Start-Button wird mit Klick auf game.Start verbunden.

Daraus ergibt sich nun die Klasse MainForm


public partial class MainForm : Form
{
	private Game game;

	public MainForm()
	{
		InitializeComponent();

		game = new Game();
		int pos = 0;
		game.MemoryCardPanel[pos++] = panel1;
		game.MemoryCardPanel[pos++] = panel2;
		game.MemoryCardPanel[pos++] = panel3;
		game.MemoryCardPanel[pos++] = panel4;
		game.MemoryCardPanel[pos++] = panel5;
		game.MemoryCardPanel[pos++] = panel6;
		game.MemoryCardPanel[pos++] = panel7;
		game.MemoryCardPanel[pos++] = panel8;
		game.MemoryCardPanel[pos++] = panel9;
		game.MemoryCardPanel[pos++] = panel10;
		game.MemoryCardPanel[pos++] = panel11;
		game.MemoryCardPanel[pos++] = panel12;
		game.MemoryCardPanel[pos++] = panel13;
		game.MemoryCardPanel[pos++] = panel14;
		game.MemoryCardPanel[pos++] = panel15;
		game.MemoryCardPanel[pos++] = panel16;
		game.MemoryCardPanel[pos++] = panel17;
		game.MemoryCardPanel[pos++] = panel18;
		game.MemoryCardPanel[pos++] = panel19;
		game.MemoryCardPanel[pos++] = panel20;
		game.MemoryCardPanel[pos++] = panel21;
		game.MemoryCardPanel[pos++] = panel22;
		game.MemoryCardPanel[pos++] = panel23;
		game.MemoryCardPanel[pos++] = panel24;

		for (UInt32 i = 0; i < Game.maxMemoryKarten; i++)
		{
			game.MemoryCardPanel[i].Index = i;
		}

		game.Start();
		game.Sound = true;
		Game.maxTrefferKarten = 2;

	}

	private void menuStarten_Click(object sender, EventArgs e)
	{
		game.Start();
	}

	...
}

Wie schon erwähnt, ist dies mein erstes richtiges Klassenprojekt.

Einige Fragen hätte ich noch.

  1. Gibt es eine schönere Methode, als in MainForm die 24 Zeilen untereinander zu schreiben, um die Panels dem Array zuzuordnen?

  2. Ist das Klassendesign so empfehlenswert? Ich habe bewusst MemoryCard und MemoryCardPanel nicht zusammengelegt, sondern 2 Array in der Klasse Game, da ich die Daten und die Visualisierung strikt trennen wollte. Besonders die Methoden-Aufrufe RefreshMemoryCard und ClickMemoryCard in der Klasse MemoryCardPanel.

  3. Bei der Klasse Game musste ich alles statisch definieren, da es anders nicht ging. Gibt es hier evtl. eine andere Lösung?

  4. Mit der Initialisierung scheint irgendwas nicht ganz zu stimmen. Ich glaube, wenn das Paint-Ereignis des MemoryCardPanel in der Form zu früh kommt, obwohl das MemoryCardPanel-Array in der Klasse Game noch nicht angelegt wurde. Kann dies sein?

Besten Dank im Voraus.

P
peterchen72 Themenstarter:in
66 Beiträge seit 2006
vor 17 Jahren

Achja, hier noch das überarbeitete Spiel. Man kann jetzt auch wählen, ob man 2, 3 oder immer 4 (sehr schwer) richtige Karten finden möchte.

Spiel zum Downloaden

49.485 Beiträge seit 2005
vor 17 Jahren

Hallo peterchen72,

Teilantwort:

irgendwie ist es doof, dass die Dikussion zu Programmierfragen, jetzt hier unter Projekte und so verzahlt mit projektmäßigem wie dem Download läuft. Leider kann man als Moderator zwar Themen teilen aber leider keine Beiträge, sonst hätte ich das neulich schon mit deinem Startbeitrag gemacht. Tja, jetzt haben wir den Salat. Anyway:

zu 1. Gibt es eine schönere Methode, als in MainForm die 24 Zeilen untereinander zu schreiben, um die Panels dem Array zuzuordnen?

Ja, die Panel gar nicht erst mit dem Designer sondern geleich im Code in einer Schleife zu erstellen.

  1. Bei der Klasse Game musste ich alles statisch definieren, da es anders nicht ging. Gibt es hier evtl. eine andere Lösung?

Gehen tut das schon anders. Man kann auch alles instanzmäßig machen und dann ein Game-Objekt erzeugen. Wenn man aber die Game-Klasse ohnehin als Singleton realisieren würde, dann kann man auch erwägen stattdessen Game als statische Klasse zu implementieren.

  1. Mit der Initialisierung scheint irgendwas nicht ganz zu stimmen. Ich glaube, wenn das Paint-Ereignis des MemoryCardPanel in der Form zu früh kommt, obwohl das MemoryCardPanel-Array in der Klasse Game noch nicht angelegt wurde. Kann dies sein?

Sein kann sowas schon. Die Reihenfolge der Initialisierung könnte man mit einem Singleton ggf. einfacher steuern.

herbivore

P
peterchen72 Themenstarter:in
66 Beiträge seit 2006
vor 17 Jahren

Hallo herbivore,

danke für Deine Ausführungen.

Du hast natürlich Recht, die Programmierfragen gehören eigentlich nicht hierher. Ich habe mir nur gedacht, dass es einfacher ist, wenn ich Fragen zu den Klassen gleich an einem konkreten Projekt (Spiel) stelle und das Spiel mit Quellcode auch zur Verfügung stelle. Ich werde bei Bedarf dann ein neues Thema unter einer anderen Rubrik aufmachen.