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 Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' , the inheritance tree root.

The Card Element

Our graphic element, called CardElement, representing a card will subclass BlElement Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' 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 Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' 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 Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' which includes two BlLineElement BlCurveElement subclass: #BlLineElement instanceVariableNames: '' classVariableNames: '' package: 'BlocPac-Geometry-Curves' objects. Each line element uses two BlAnchorRelativeToElementBounds BlAnchorRelativeToElement subclass: #BlAnchorRelativeToElementBounds instanceVariableNames: '' classVariableNames: '' package: 'BlocPac-Geometry-Anchors' 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 Object subclass: #BlElement uses: TBlTransformable + TBlEventTarget + TBlDebug instanceVariableNames: 'spaceReference parent children bounds measuredBounds boundsCache eventDispatcher constraints layout transformation taskQueue errorHandler userData visuals flags' classVariableNames: '' package: 'Bloc-Basic' defines its visibility using BlVisibility Object subclass: #BlVisibility uses: TBlDebug instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Basic-Properties' 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 BlLayout subclass: #BlBasicLayout instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Layouts-Basic' as you can check by executing the following snippet:

CardElement new layout
  

BlFrameLayout BlNodeBasedLayout subclass: #BlFrameLayout instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Layout-Frame' layout has an ability to center CardElementobject'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 Object subclass: #BlVisibility uses: TBlDebug instanceVariableNames: '' classVariableNames: '' package: 'Bloc-Basic-Properties' 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.