Playing the Memory Game
The Game Element
In the previous section, we implemented the card element that is able to display back- and face- card sides, to flip, and to disappear. In this section, we implement the board with 4x4 cards as we defined previously in Game>>#gridSize
.
To this purpose, we will define GameElement
:
BlElement subclass: #GameElement instanceVariableNames: 'game' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. GameElement class instanceVariableNames: ''
We then define accessors and initialize methods, defining a widget layout, background color and size:
"protocol: #accessing" GameElement >> game ^ game "protocol: #accessing" GameElement >> game: aGame game := aGame. self onUpdated "protocol: #initialize" GameElement >> initialize super initialize. self layout: BlGridLayout horizontal. self layout cellSpacing: 7. self background: Color gray. self constraintsDo: [ :c | c horizontal fitContent. c vertical fitContent ]. "protocol: #hooks" GameElement >> onUpdated self game ifNil: [ ^ self ]. self removeChildren. self layout columnCount: self game gridSize. self game availableCards do: [ :aCard | | aCardElement | aCardElement := self newCardElement card: aCard. self addChild: aCardElement ]
We need to implement the GameElement>>#newCardElement
:
"protocol: #'instance creation'" GameElement >> newCardElement ^ CardElement new
GameElement>>#initialize
defines the board visual aspects, including BlGridLayout
, background color, and constraints to occupy the minimum horizontal and vertical size to display all cards:
GameElement>>#game:
set the game model Game
and call an update hook.
The GameElement>>#onUpdated
method then defines BlGridLayout
layout dimensions calling BlGridLayout>>#columnCount:
and adds all CardElement
instances representing cards.
You can check the current implementation executing the following snippet:
aGame := Game numbers. GameElement new game: aGame
Handling Mouse Clicks
If you try to click on cards above, you will notice that they do not yet flip as is expected in this game. The reason is that we have not implemented mouse click events yet. To handle the mouse click events, we will subclass BlEventListener
:
BlEventListener subclass: #CardEventListener instanceVariableNames: 'game' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. CardEventListener class instanceVariableNames: ''
We then implement accessors and the mouse click event handler:
"protocol: #accessing" CardEventListener >> game ^ game "protocol: #accessing" CardEventListener >> game: anObject game := anObject "protocol: #'mouse handlers'" CardEventListener >> clickEvent: anEvent self game ifNil: [ ^ self ]. self game chooseCard: anEvent currentTarget card
The CardEventListener>>#clickEvent:
calls the Game>>#chooseCard:
method when a user clicks on a card:
The CardEventListener
has to be added to each CardElement
object when a game view is initialized. The initialization happens in the GameElement>>#onUpdated
method.
Here, we need to add one line to subscribe the listener:
"protocol: #hooks" GameElement >> onUpdated self game ifNil: [ ^ self ]. self removeChildren. self layout columnCount: self game gridSize. self game availableCards do: [ :aCard | | aCardElement | aCardElement := self newCardElement card: aCard. aCardElement addEventHandler: (CardEventListener new game: self game). self addChild: aCardElement ]
We just finished the last piece of code to be able to play a game with numbers:
| aGame | aGame := Game numbers. GameElement new game: aGame
Visual Animations
Bloc offers element animations using BlBaseAnimation
subclasses.
You can for example translate an element:
elementWithLinearTranslationAnimation <gtExample> | container element animation | animation := self linearTranslationAnimation. element := self animationElement. element relocate: 5@5. container := self containerElement. container addChild: element. element addAnimation: animation. ^ container
You can also translate with more complex logic:
elementWithLinearTranslationAnimationWithDurationAndEasing <gtExample> | container animation | animation := self linearTranslationAnimationWithDurationAndEasing. container := self containerWithOneElement. container children first addAnimation: animation. ^ container
In this tutorial we will use BlTransformAnimation
to scale a CardElement
when a mouse is over the card.
Enlarging Card
In the following part, we will learn how to enlarge a card when we move a mouse over it. To do so, we add a method to the CardEventListener
:
"protocol: #'mouse handlers'" CardEventListener >> mouseEnterEvent: anEvent | anAnimation | anAnimation := (BlTransformAnimation scale: 1.1 @ 1.1) absolute; duration: 0.5 seconds. anEvent currentTarget addAnimation: anAnimation
You can now test it playing the game:
aGame := Game numbers. GameElement new game: aGame
Shrinking Card
As you noticed, enlarging works well, but we also need to shrink a card as the mouse leaves the card. To do so, we will use the same animation:
"protocol: #'mouse handlers'" CardEventListener >> mouseLeaveEvent: anEvent | anAnimation | anAnimation := (BlTransformAnimation scale: 1.0 @ 1.0) absolute; duration: 0.5 seconds. anEvent currentTarget addAnimation: anAnimation
You can now test it again here:
aGame := Game numbers. GameElement new game: aGame
Improving the Animation
The just written code has a potential issue. If you move your mouse fast enough, enlarging and shrinking animations will be in a queue at a same time, using BlElement>>#addAnimation:
and executed in parallel. It means, that the CardEventListener>>#mouseLeaveEvent:
animation is performed while the CardEventListener>>#mouseEnterEvent:
animation is not over yet.
The same issue arises if the CardEventListener>>#mouseLeaveEvent:
animation is not over yet and we add the CardEventListener>>#mouseEnterEvent:
animation.
We can fix it by remembering what animation is in progress and stop it by calling BlBaseAnimation>>#stop
before adding a new animation:
BlEventListener subclass: #CardEventListener instanceVariableNames: 'game animation' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. CardEventListener class instanceVariableNames: '' "protocol: #private" CardEventListener >> stopAnimation animation ifNotNil: [ :anAnimation | anAnimation isRunning ifTrue: #stop ]. "protocol: #'mouse handlers'" CardEventListener >> mouseEnterEvent: anEvent self stopAnimation. animation := (BlTransformAnimation scale: 1.1 @ 1.1) absolute; duration: 0.5 seconds. anEvent currentTarget addAnimation: animation "protocol: #'mouse handlers'" CardEventListener >> mouseLeaveEvent: anEvent self stopAnimation. animation := (BlTransformAnimation scale: 1.0 @ 1.0) absolute; duration: 0.5 seconds. anEvent currentTarget addAnimation: animation
Let's try it again:
aGame := Game numbers. GameElement new game: aGame
Ready to Play
You just finished the tutorial. You can play the game executing the code snippet above.
Links to Pages containing missing references - allowed failures to allow references to missing classes and methods in the page.