Construct2 Memory-Tutorial
Der folgende Text basiert auf dem Text Creating a Memory Match Game , der ursprünglich von Kittiwan veröffentlicht wurde.
Eine spielbare Version ist unter http://www.scirra.com/arcade/addicting-example-games/956/memory-match-tutorial zu finden.
Ich gehe in dem Text davon aus, dass die Free-Version von Construct2 installiert ist, und zwar in Release 119. Der Hinweis auf das Release kann wichtig sein, da Construct2 sehr intensiv weiterentwickelt wird.
Die für das Projekt benötigten Grafiken sind unter http://dl.dropbox.com/u/57899112/MemMatchTut.zip zu bekommen.
Neues Projekt
Im ersten Schritt legen wir unter ein neues Projekt an (New -> New empty project) und benennen es Memory Match. Gleich danach sollte man sicherheitshalber speichern, Save As single file...
Für den Arbeitsbereich wollen wir eine Hintergrundfarbe setzen. Dazu klicken wir unter Layers auf Layer0 und ändern in dessen Properties die Hintergrundfarbe BackgroundColor auf z.B. einen Grünton.
Im nächsten Schritt werden die Abbildungen eingebunden. Dazu macht man einen Doppelklick in das Layout-Fenster, es erscheint das Auswahlfenster für Insert New Object, hier wählt man durch einen Doppelklick das Sprite-Objekt aus. Der Cursor verändert sich zu einem dünnen Kreuz und man klickt in das Layout-Fenster.
Es öffnet sich das Fenster Edit Image, dort öffnet man über einen Klick auf Load in image from a file den Dateidialog. Über diesen Dialog wählt man die Grafik für die Karten-Rückseite aus.
In dem kleinen Fenster Animations neben dem Grafikfenster kann man den Namen der zugehörigen Animation ändern zu CardBack (rechte Maustaste auf den Namen und dann Rename).
Danach klicken wir gleich wieder mit der rechten Maustaste in das Fenster Animations und wählen Add Animation.
Die neue Animation nennen wir CardFace. Für die Vorderseite benötigen wir mehrere Grafiken, die wir auf einen Rutsch laden können. Wir klicken dazu auf CardFace im Animations Fenster und klicken dann mit der rechten Maustaste ins Fenster Animation frames. Dort wählen wir Import frames.
In dem Dateidialog klicken wir einmal das erste Bild an und dann mit gedrückter Shift-Taste das letzte Bild. Es sollten alle Bilder markiert sein. Mit einem Klick auf öffnen im Datei-Dialog werden alle Bilder geladen.
In dem Fenster Animation frames befindet sich jetzt noch ein überflüssiges Bild unter der Nummer 0, welches durch gelöscht werden muss. Es wurde beim Anlegen des Frames automatisch erzeugt. Nun müssen die verbliebenen zwölf Grafiken verdoppelt werden. Dies ist der später folgenden Logik geschuldet, die es aber ermöglicht dann auch ein Bild und z.B. eine Karte mit dem passenden Text zu matchen.
Man klickt nun jedes der Bilder einmal mit der rechten Maustaste an und wählt aus dem Kontextmenü Duplicate. Am Ende ergibt sich folgende Situation.
Wir haben 24 Bilder nummeriert von 0 bis 23 und immer zwei gleiche Bilder nebeneinander.
Nun wollen wir noch den Bezugspunkt für die Grafiken verändern. Dazu klicken wir auf CardBack im Animations Fenster und dann im Image Editor auf den Button Set origin and image points.
Wir klicken mit der rechten Maustaste auf Origin im Image points Fenster und wählen Quick assign aus. Hier wählen wir dann Top left als neue Position für den Bezugspunkt.
Die entsprechende Änderung nehmen wir noch für CardFace vor. Nur hier muss die Änderung anschließend noch mit Apply to whole animation auf alle Karten übertragen werden.
Danach können wir den Image-Editor schließen. Wir sollten dann unter den Properties für die Karte noch den Namen von Sprite auf Card verändern.
Aktion
Damit Mausereignisse berücksichtigt werden können müssen wir zunächst das Maus-Objekt in das Layout einbinden. Doppelklick auf Layout -> Insert New Object -> Mouse.
Nun können wir Ereignisbehandlaung einfügen.
Condition: Mouse -> On an object klicked -> Card Action: Card -> Set animation -> "CardFace"
Probiert man das Spiel jetzt aus (run), dann funktioniert das Umdrehen der Karte schon, es wechseln aber alle Frontseiten durch. Um das zu ändern klick man CardFace an und kann dann unter Properties die Eigenschaft Speed auf den Wert 0 stellen (ist voreingestellt auf 5). Nun folgt keine weiterer Front-Kartenwechsel mehr.
Nun können wir den Grafik-Editor schließen.
Für das folgende Spiel muss sich jede Karte nun merken könnnen, welches Symbol auf ihrer Vorderseite abgebildet ist, dazu können wir wieder eine Instanzen-Variable einsetzen. Wichtig ist, dass das Object Card aktiviert ist. Über Add/Edit Instance variable und einen Klick auf das Pluszeichen fügen wir eine Variable ein. Sie bekommt den Namen CardFaceFrame, den Typ Number und den Wert 0.
Wir fügen gleich noch eine Instanzenvariable hinzu und zwar mit dem Namen FaceUp, dem Type Boolean und dem Wert false.
Nun muss noch erreicht werden, dass bei einem Mausklick auf die Karte auch immer die über die Variable CardFrameFace angegebene Abbildung angezeigt wird. Dazu gehen wir in das Event sheet und fügen eine Action zur vorhandenen Condition hinzu.
Action: Card -> Set frame -> Card.CardFaceFrame
Lässt man das programm jetzt laufen, so erkennt man noch keine Veränderungen, weil die Vorgabe für die Instanzenvariable 0 ist, also das erste Bild. Erst wenn man hier einen anderen Wert einsetzt kann man bemerken, dass eine andere Vorderseite geladen wird.
Positionierung der Karten
Die Anwendung soll relativ flexibel die Anordnung der Karten erlauben. Dazu wird eine Reihe von globalen Variablen benötigt, über die das Layout vorgegeben wird.
Man kann die globalen Variablen in einem zusätzlichen Event Sheet unterbringen. Dazu klickt man unter Projects mit der rechten Maustaste auf Event sheet wählt Add event sheet und benennt es Global.
Hier werden jetzt folgende Variablen angelegt.
- gNumberCards, Number, 24
- gNumberRows, Number, 4
- gNumberColumns, Number, 6
- gCardHeight, Number, 0
- gCardWidth, Number, 0
- gSpaceBetweenCards, Number, 5
- gMarginTop, Number,0
- gMarginLeft, Number, 0
Nun müssen wir die fehlenden Daten berechnen. Dazu gehen wir wider auf das Event sheet und fügen hinzu (möglichst mit Copy & Paste).
Condition: System -> On start of layout Action: System -> Set value -> gCardHeight: int((WindowHeight-((gNumberRows + 1)*gSpaceBetweenCards))/gNumberRows) Action: System -> Set value -> gCardWidth: int(gCardHeight * (Card.Width/Card.Height)) Action: System -> Set value -> gMarginLeft: int((WindowWidth - (gCardWidth + gSpaceBetweenCards) * gNumberColumns) / 2) Action: System -> Set value -> gMarginTop: int((WindowHeight-(gCardHeight + gSpaceBetweenCards)*gNumberRows)/2)
Nun wechseln wir wieder zum Event sheet 1 und klicken dort mit der rechten Maustaste auf den gründen Pfeil vor dem System-Event.
Hier wählen wir
System -> Repeat -> gNumberCards
Nun brauchen wir lokale Variable innerhalb der eben definierten Schleife. Dazu klicken wir mit der rechten Maustaste auf auf den Kreis vor dem Schleifenereignis und wählen Add -> Add local variable.
Wir fügen also eine Variable namens CurrentTableauCard mit dem Anfangswert -1 hinzu, dann eine Variable CurrentColumn, ebenfalls mit -1 und zuletzt CurrentRow, ebenfalls mit -1.
Der Event-Bereich sollte jetzt folgendermaßen aussehen.
Nun kommen die Actions, die die Position der einzelnen Karten berechnen.
Actions: System -> Add to -> CurrentTableauCard , 1 Actions: System -> SetValue -> CurrentColumn -> CurrentTableauCard%gNumberColumns
Nun folgt ein Sub-Event zur Schleife. Dazu klicken wir mit der rechten Maustaste auf den Kreis vor dem letzten System und wählen Add -> Add sub event. Dann
Condition: System -> Compare variable -> Current Column, 0 Action: System -> Add to -> CurrentRow, 1
Nun fügen wir zu der Schleife noch ein leeres Event hinzu, indem wir mit der rechten Maustaste auf (den ersten) Kreis klicken und auswählen Add -> Add blank sub-event.
Wir wollen hier eine Action haben, ohne irgendwelche vorgeschalteten Bedingungen. Hier kommt also nur eine Action.
Action: System -> create object -> Card -> Layer: 0, X: gMarginLeft + (CurrentColumn*(gCardWidth + gSpaceBetweenCards)), Y: gMarginTop + (CurrentRow*(gCardHeight + gSpaceBetweenCards)) Action: Card -> Set size -> width: gCardWidth, height: gCardHeight Action: Card -> set value -> CardFaceFrame -> CurrentTableauCard
Wenn man jetzt die Anwendung im Browser testet, dann fällt auf, dass die 24 Karten richtig angeordnet werden, die Ausgangskarte liegt aber noch groß dahinter.Das lässt sich lösen, indem man die Karte im Lauout-Tab außerhalb des sichtbaren Bereichs verschiebt.
Wenn man jetzt genauer testet und die Karten anklickt, so stellt man fest, dass sie sauber der Reihe nach angeordnet sind und sich auch nicht wieder zurückdrehen.
Mischen der Karten
Zum Mischen der Karten wollen wir ein Array benutzen. Dazu müssen wir in das Layout ein Array-Objekt integrieren. Also ein Doppelklick auf einen freien Bereich in Layout1 und im Fenster Insert New Object ganz oben Array auswählen.
Das Objekt einfach mit einem Doppelklick einfügen, wir ändern unter Properties seinen Namen auf Deck, mehr ist nicht einzustellen. Das Objekt taucht jetzt rechts unter Objects mit auf.
Weiter geht es nun im Tab Event sheet1.
Wir fügen jetzt bei der
Condition: System -> On start of layout
eine weitere Action hinwzu.
Action: Deck -> Set size -> Width: gNumberCards, Height: 1, Depth: 1
Nun brauchen wir eine Schleife, um das Array mit Inhalt zu füllen. Dazu klicken wir wieder mit der rechten Maustaste auf den Kreis vor System und wählen aus dem Kontextmenü Insert new event above.
Für das Event geben wir an:
System -> Repeat gNumberCards
Nun sieht der Bereich folgendermaßen aus.
Das eben eingefügte Schleifenkonstrukt fassen wir mit der Maus am grünen Kreis und schieben es mit gedrückter Maustaste vor die lokalen Variablen.
Die Variable CurrentDeckCard brauchen wir nun noch für die neue Schleife. Dazu können wir in dem Bereich die Variable neu einfügen, oder wir klicken auf eine vorhandene Variable z.B. CurrentTableauCard mit der rechten Maustaste und wählen nacheinander Copy und dann Paste. Construct2 fügt nun eine Variable CurrentTableauCard2 ein, die wir nach oben schieben und zu CurrentDeckCard umbenennen.
Nun fügen wir dem neuen Schleifen-Event eine Action hinzu.
Action: System -> AddTo -> CurrentDeckCard, 1
und weiter
Action: Deck -> Set at X -> x: CurrentDeckCard, Value: CurrentDeckCard
Damit setzen wir eine fortlaufende Zahlenfolge in das Array. Jede Zelle hat ihre Nummer als Inhalt. Nun müssen wir die Zelleninhalte mischen. Am einfachsten Fall (einfach, aber nicht unbedingt effektiv) vertauschen wir dazu oft genug zwei zufällig ausgewählte Zelleninhalte.
Dazu fügen wir eine neue Schleifenstruktur unterhalb der eben benutzten ein. Rechte Maustaste auf den grünen Kreis, Insert new event below.
Event: System -> Repeat, 10
Nun brauchen wir davor noch eine Reihe von lokalen Variablen.
Für die Positionen
Slot1, Number, 0 Slot2, Number, 0
Für die Inhalte
Card1, Number, 0 Card2, Number, 0
Nun müssen wir die Werte für Slot1 und Slot2, also die zu vertauschenden Positionen zufällig bestimmen, das läuft wider über Actions der Schleife.
Action: System -> Set Value -> Slot1: int(random(0, gNumberCards)) Action: System -> Set value -> Slot2: int(random(0, gNumberCards))
Nun müssen die Inhalte ausgelsen und vertauscht werden.
Action: System -> Set value -> Card1: Deck.At(Slot1) Action: System -> Set value -> Card2: Deck.At(Slot2)
Nun schreiben wir die Inhalte über Kreuz zurück.
Action: Deck -> Set at X -> X: Slot1, Value: Card2 Action: Deck -> Set at X -> X: Slot2, Value: Card1
Damit ist das Array sortiert und wir müssen nur noch erreichen, dass das Array bei der Ausgabe der Karten berücksichtigt wird.
Dazu muss die allerunterste Action verändert werden. Momenten ist das
Action: Card -> SetFaceFrame to CurrentTableauCard
Das wird geändert zu
Action: Card -> SetFaceFrame to Deck.At(CurrentTableauCard)
Nun sollte wirklich ein zufällig angeordnetes Tableau mit den Karten erzeugt werden. Falls einem das nicht zufällig genug erscheint sollte man die 10 bei der Wiederholung erhöhen.
Man kann auch einen zusätzliche globale Variable einfügen, gNumberCardShuffles: 50. Dann muss man noch im Eventsheet die 10 durch diese Variable ersetzen.
Momentan ist es nur so, dass man alle Karten umdrehen kann, aber kein Zurückdrehen erfolgt.
Ein Spielzug
Wir brauchen wieder ein paar globale Variablen.
gCountCardsPicked, Number, 0
Dann gehen wir wieder zurück in das EventSheet 1. Dort haben wir auf der obersten Ebene zwei Events. Nun müssen wir an das MouseEvent ganz oben heran. Es darf sich nicht bei jedem Mausklick eine Karte umdrehen, sondern nur zweimal.
Dazu fügen wir wieder ein Sub Event ein (rechte Maustaste auf den grünen Pfeil -> Add -> Add sub-event).
Event: System -> Compare variable -> gCountCardsPicked <Less then 2 Action: System -> gCountCardsPicked, 1 Action: Card -> Set Boolean -> FaceUp, True
Nun müssen wir die beiden "alten" Mouse-Actions in das neue Sub-Event verschieben, hinter die beiden dort vorhandenen Actions. Dazu einfach mit Mouseklick und Shift+Mouseklick markieren und dann mit gedrückter Maustaste herunterziehen.
Nun fügen wir noch zwei globale Variablen ein.
gFirstCardFrame, Number, -1 gScondCardFrame, Number, -1
Zurück im Event-Sheet fügen wir ein neues Sub-Event unterhalb des zuletzt bearbeiteten Events ein (rechte Maustaste auf den leeren Bereich vor dem Zahnrad -> Insert new event below) .
Event: System -> Compare variable -> gCountCardsPicked =Equal to 1 Action: System -> Set value -> Variable: gFirstCardFrame, Value: Card.CardFaceFrame
Nun das Ganze auch für die zweite Karte. Wir fügen ein neues Sub-Event unterhalb des zuletzt bearbeiteten Events ein (rechte Maustaste auf den leeren Bereich vor dem Zahnrad -> Insert new event below) .
Event: System -> Compare variable -> gCountCardsPicked =Equal to 2 Action: System -> Set value -> Variable: gSecondCardFrame, Value: Card.CardFaceFrame
zwei gleiche Karten
Für den Fall, dass wir zwei Karten umgedreht haben wollen wir vergleichen. Dazu brauchen wir ein Sub-Event für das letzte Event (rechte Maustaste auf den leeren Bereich vor dem Zahnrad -> Add -> Add sub-event).
Event: System -> Compare two values -> gFirstCardFrame-(gFirstCardFrame%2) =Equal gSecondCardFrame-(gSecondCardFrame%2) Action: Card -> Destroy
Damit wird aber leider nur die zweite Karte gelöscht, da diese ja aktiv war.
Und noch ein Sub-Event vom letzten Event.
Event: System -> Pick all -> Card
Und noch ein Sub-Event.
Event: Card -> Is boolean instance variable set -> FaceUp Action: Card -> Destroy (Hier konnen wir einfach die Action von oben herunterziehen)
So, nun werden Karten gelöscht, wenn Sie gleich sind. Wir brauchen jetzt noch den Alternativ-Fall, also wenn die beiden Karten nicht gleich sind.
zwei verschiedene Karten
Wir kopieren (rechte Maustaste Copy) einfach den kompletten Bereich für zwei gleiche Karten (beginnt mit dem sehr langen Vergleich, auch die Sub-Events müssen markiert sein) und fügen ihn dann direkt darunter wieder ein (rechte Maustaste Paste).
Nun müssen wir den Bereich noch etwas anpassen. Den Vergleich (=Equal) drehen wir um, indem wir aus dem Kontextmenü (rechte Maustaste) Invert wählen, der Kasten wird dann deutlich mit einem roten Kreuz markiert.
Im letzten Sub-Event löschen wir jetzt die Zeile mit dem Destroy und ergänzen dann.
Action: Card -> Set animation -> "CardBack" Action: Card -> set boolean -> FaceUp, false
Wenn man jetzt nacheinander zwei (verschiedene) Karten umdreht, dann erfolgt das zurückdrehen schneller, als man das Umdrehen der zweiten Karte beobachten kann. Wir brauchen also eine Verzögerung vor dem Zurückdrehen, damit wir auch die zweite Karte betrachten können.
Action: System -> Wait -> 2
Die neue Action müssen wir aber vor die beiden bisherigen schieben, sonst kommt das Warten erst nach dem Zurückdrehen.
Nun kann man die Karten wirklich betrachten. Es bleibt aber das Problem, dass wir nur einmal ein Kartenpaar umdrehen können. Das hängt damit zusammen, das der Inhalt der Variablen gCountCardsPicked noch auf 2 steht. Den müssen wir auf 0 zurücksetzen.
Wir gehen zu der Zeile mit dem Event: System -> gCountCardsPicked = 2 und fügen dort (rchte Maustaste auf das Feld vor dem Zahnrad) ein leeres Sub-event ein.
Das leere Sub-Event wird am Ende des Mausbereiches angefügt.
Action: System -> Set value -> gCountCardPicked, 0 Action: System -> Set value -> gFirstCardFrame, -1 Action: System -> Set value -> gSecondCardFrame, -1
Nun funktioniert das Spiel weitestgehend, es gibt nur den kleinen Schönheitsfehler, dass immer dann, wenn zwei gleiche Karten aufgedeckt wurden, diese sehr schnell verschwinden. Auch hier muss eine Verzögerung eingebaut werden.
Wir suchen also die Zeile, wo die Karten zerstört werden (destroy). Hier setzen wir vor die zerstörerische Aktion die Action.
Action: System -> Wait -> 2
Nun steht an zwei Stellen die Konstante 2 für die Verzögerung. Das ist natürlich unschön, wenn man später einmal Veränderungen an der Wartezeit vornehmen will. Wer möchte kann hier wieder mit einer globalen Variablen arbeiten, die dann statt der Zahl eingesetzt wird.
Einen blöden kleinen Fehler hat unser Spiel noch. Wenn man zweimal die gleiche Karte anklickt, dann verschwindet sie, weil die Karten von den beiden Klicks eben identisch sind. Das müssen wir noch verhindern.
Die Stelle an der wir ansetzen müssen ist die mit der Bedingung gCountCardsPickes <2 .Hier drehen wir nämlich dann die zweite Karte um. Wir müssen jetzt nur untersuchen, ob die aktuelle Karte nicht schon umgedreht ist.
Wir klicken mit der rechten Maustaste vor das Zahnrad in dieser Zeile und wählen Add -> Add another condition.
Condition: Card -> Is boolean instance variable set -> FaceUp
Diese Kondition müssen wir anschließend invertieren (ergibt wieder ein rotes Kreuz im Feld).
Jetzt gibt es leider noch einen kleinen Schönheitsfehler. Man kann, wenn man schnell klickt, drei Karten aufgedeckt haben, nämlich schon die erste Karte der nächsten Runde, bevor das aktuelle Paar zurück gedreht wurde.
Wir müssen also vor den drei Aktionen zum Zurücksetzen noch eine Wartezeit von ebenfalls 2 Sekunden einfügen.
Ein Neustart am Ende des Spieles
Das Spiel funktioniert nun so wie geplant. Wir wollen im nächsten Schritt aber mitverfolgen, wieviele Kartenpaare zur Deckung gebracht wurden. Damit kann man einen Spielstand einführen und das Spiel auch neu starten, wenn alle Paare gefunden wurden.
Es geht wieder los mit einer weiteren globalen Variablen.
gMatches, Number, 0
Nun muss diese Variable noch hochgezählt werden. Der Ort dafür ist die Stelle wo wir festgestellt haben, dass beide Karten gleich sind. Dort ist bisher noch keine Action untergebracht.
Action: System -> add to -> gMatches, 1
Nun müssen wir nur noch feststellen, ob alle Paare gefunden sind.
Dazu Hängen wir ein neues Event an.
Event: System -> Compare variable -> gMatches >=greater or equal gNumberCards/2 Action: System -> Goto layout -> layout1
Nun gibt es wieder das Problem der fehlenden Verzögerung bevor das neue Spiel startet. Dazu fügen wir wieder ein Delay vor die letzte Action. In diesem Fall kann die Verzögerung größer sein, z.B. 4 Sekunden.
Nun gibt es wieder das Problem, dass wir die Variable gMatches nicht zurück gesetzt haben. Das bewirkt, dass das Spiel nach einem Durchlauf immer wieder neu startet.
Das könnten wir jetzt ändern, indem wir die Variable zurücksetzen, nachdem wir festgestellt haben, dass alle Paare aufgedeckt wurden und bevor wir das Layout neu initialisieren. Man kann aber auch einfach am Anfang des Layout (on start of layout) vor die vorhandenen Actions einen Action zum Initialisieren aller Variablen setzen.
Action: System -> Reset global variable to default