Building the Memory Game graphical elements with Bloc
In this chapter, we will build the visual appearance of the cards step by step. In Bloc, visual objects are called elements, which are usually subclasses of BlElement
, the inheritance tree root.
The Card Element
Our graphic element, called CardElement
, representing a card will subclass BlElement
and have a reference to a card model:
BlElement subclass: #CardElement instanceVariableNames: 'card' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. CardElement class instanceVariableNames: ''
We then define accessor and initialize methods, defining a widget background color and geometry:
"protocol: #accessing" CardElement >> card ^ card "protocol: #accessing" CardElement >> card: aCard card := aCard "protocol: #'visual properties'" CardElement >> cardSize ^ 80 @ 80 "protocol: #'visual properties'" CardElement >> backgroundColor ^ Color lightBlue "protocol: #initialization" CardElement >> initialize super initialize. self size: self cardSize. self background: self backgroundColor. self geometry: (BlRoundedRectangleGeometry cornerRadius: 12).
The CardElement>>#initialize
method defines the element size, background color, and rounded-rectangle geometry.
You can observe the result of the current implementation executing the following snippet:
CardElement new
Drawing a Card
Our card has two sides, the back side and the face side, that need to be implemented. We implement them by composing two BlElement
objects, one for the face, and the other the back.
Back Side
Let's start with the back side, which will be represented by two diagonal lines, across the CardElement
card. To do so, we introduce a new back
instance variable, that will be initialized in the CardElement>>#initialize
:
BlElement subclass: #CardElement instanceVariableNames: 'card back' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. CardElement class instanceVariableNames: '' "protocol: #initialization" CardElement >> initializeBack back := BlElement new constraintsDo: [ :c | c horizontal matchParent. c vertical matchParent ]. back addChild: (BlLineElement new border: (BlBorder paint: self foregroundColor width: 1); fromAnchor: (BlElementTopLeftAnchor new referenceElement: back); toAnchor: (BlElementBottomRightAnchor new referenceElement: back); yourself); addChild: (BlLineElement new border: (BlBorder paint: self foregroundColor width: 1); fromAnchor: (BlElementTopRightAnchor new referenceElement: back); toAnchor: (BlElementBottomLeftAnchor new referenceElement: back); yourself). "protocol: #'visual properties'" CardElement >> foregroundColor ^ Color gray "protocol: #initialization" CardElement >> initialize super initialize. self size: self cardSize. self background: self backgroundColor. self geometry: (BlRoundedRectangleGeometry cornerRadius: 12). self initializeBack. self addChild: back.
Let's look closer at the CardElement>>#initializeBack
method.
We use BlElement
which includes two BlLineElement
objects. Each line element uses two BlAnchorRelativeToElementBounds
subclass objects defining line start and end points.
By creating an element instance, we should be able to observe a back side card that looks like this:
To do so, execute the following snippet:
CardElement new
Face Side
Let's implement the face side which displays a character (a symbol). We introduce a new face
instance variable, that will be initialized in the CardElement>>#initialize
:
BlElement subclass: #CardElement instanceVariableNames: 'card face back' classVariableNames: '' poolDictionaries: '' category: 'Tutorial-MemoryGame-UI'. CardElement class instanceVariableNames: '' "protocol: #initialization" CardElement >> initializeFace face := BlTextElement new text: '?' asRopedText. face constraintsDo: [ :c | c frame horizontal alignCenter. c frame vertical alignCenter ]. face visibility: BlVisibility hidden. "protocol: #initialization" CardElement >> initialize super initialize. self size: self cardSize. self background: self backgroundColor. self geometry: (BlRoundedRectangleGeometry cornerRadius: 12). self initializeFace. self addChild: face. self initializeBack. self addChild: back.
A face-side element, defined in CardElement>>#initializeFace
is a text element, by default hidden and with a ?
character.
It means, that if you execute the following snippet, you will see only the back side as we implemented previously:
CardElement new
To be able to see face side, we can implement a method that toggles the face- and back- side visibility:
"protocol: #hooks" CardElement >> onFlippedFace face visibility: BlVisibility visible. back visibility: BlVisibility hidden.
The CardElement>>#onFlippedFace
shows that every BlElement
defines its visibility using BlVisibility
object.
The following snippet should then display the face side, and you will notice that the symbol is not displayed at an expected place yet:
CardElement new onFlippedFace
Looking back at the CardElement>>#initializeFace
method (alignCenter
messages), you can that we defined that the symbol should be in the middle.
To fix it we also need to change CardElement
's layout. At this moment it is a BlBasicLayout
as you can check by executing the following snippet:
CardElement new layout
BlFrameLayout
layout has an ability to center CardElement
object's children:
"protocol: #initialization" CardElement >> initialize super initialize. self layout: BlFrameLayout new. self size: self cardSize. self background: self backgroundColor. self geometry: (BlRoundedRectangleGeometry cornerRadius: 12). self initializeFace. self addChild: face. self initializeBack. self addChild: back.
Now, the face symbol should be centered, and look like this:
CardElement new onFlippedFace
Updating Element on Card Changes
Tu summarize our current progress, we have the game model, including Card
, Game
, and announcements CardFlipped
and CardDisappeared
. The UI element CardElement
can display back and face card sides. The UI element does not yet reflect the model changes as we can check by executing the following snippet:
aCard := Card new. aCard flip. CardElement new card: aCard
It should display the card's face side. The reason is that the graphical widget does not subscribe to the card model changes in the CardElement>>#card:
method.
The following code subscribes to the card's CardFlipped
changes and updates the UI widget accordingly:
"protocol: #accessing" CardElement >> card: aDMGCard card ifNotNil: [ :anOldCard | anOldCard announcer unsubscribe: self ]. card := aDMGCard. card announcer when: CardFlipped send: #onFlipped to: self. self onUpdated. "protocol: #hooks" CardElement >> onUpdated self card ifNil: [ ^ self ]. face text: (self card symbol asString asRopedText fontSize: self symbolFontSize; foreground: self foregroundColor). self onFlipped. "protocol: #hooks" CardElement >> onFlipped self card isFlipped ifTrue: [ self onFlippedFace ] ifFalse: [ self onFlippedBack ] "protocol: #hooks" CardElement >> onFlippedBack face visibility: BlVisibility hidden. back visibility: BlVisibility visible. "protocol: #'visual properties'" CardElement >> symbolFontSize ^ 50
The CardElement
now subscribes to Card
changes in the CardElement>>#card:
method:
It also triggers an CardElement>>#onUpdated
hook that updates a displayed symbol and a corresponding card side:
When CardFlipped
is triggered, a CardElement>>#onFlipped
method is called, which is responsible for displaying the corresponding side:
The mechanism to display the back side is implemented in CardElement>>#onFlippedBack
, similarly as we did for the face side in CardElement>>#onFlippedFace
:
To test the existing code, you can execute the following snippet that initializes a card and its element:
aCardOne := Card new symbol: $A. aCardOneElement := CardElement new card: aCardOne
To switch card side, execute several times the following snippet (NB: Evaluate
, not Inspect
) and observe the changes in the adjacent pane:
aCardOne flip
Disappearing Card Element
To make a card disappear from a game board, we will subscribe to the CardDisappeared
announcement and set the card element's BlVisibility
as we do for the card flipping:
"protocol: #accessing" CardElement >> card: aDMGCard card ifNotNil: [ :anOldCard | anOldCard announcer unsubscribe: self ]. card := aDMGCard. card announcer when: CardFlipped send: #onFlipped to: self. card announcer when: CardDisappeared send: #onDisappear to: self. self onUpdated. "protocol: #hooks" CardElement >> onDisappear self visibility: BlVisibility hidden.
CardElement
now subscribes to CardDisappeared
announcements at CardElement>>#card:
:
The CardElement>>#onDisappear
is implemented by changing the element's visibility:
By displaying the card (Inspect
):
aCardTwo := Card new symbol: $B. aCardTwoElement := CardElement new card: aCardTwo
we can make it disappear by executing (Evaluate
) the following snippet:
aCardTwo disappear
Notice that we do not have an appear
action as the game does not require it.
Ready
In the next part, we will implement the game board widget: Playing the Memory Game
Links to Pages containing missing references - allowed failures to allow references to missing classes and methods in the page.