今天我們要來實作記憶方塊的主畫面區塊,在昨天的努力之下,我們已經把區塊都規劃好了,接下來就是要把我們的方塊畫上去。
由於前兩個遊戲 Tic-Tac-Toe 以及 貪吃蛇 是在同一個鐵人賽的作品,因此做法上都會盡量相似,目的是為了要熟悉以及練習,符合我們一開始這個系列的初衷。所以這次畫出記憶方塊的方式也會跟前兩個遊戲作法相同,因此也可以互相參照 Day04 以及 Day14 的做法和說明。
為了要畫出方格,首先我們要先準備好資料,Day22 我們有提到, sideLength 是決定我們的方塊目前是幾乘幾的重要參數,遊戲初始的時候,我們預設是從 2x2 的方塊開始遊戲,因此我們給定預設參數 DEFAULT_SIDE_LENGTH 來產生,這個預設參數的值是 2。
blocks是一個 object of array 的資料,object 裡面存放每一個方塊各別的資訊,我們在 Day05 以及 Day14 我們有提到我們是怎麼產生一個有預設值的陣列,當時是用 lodash 這個工具,這次我們要換個方法來做。
由於我們需要產生一個 2x2 的方塊,因此需要一個長度為 4 的陣列。如下程式碼所示,我們先用 new Array(sideLength * sideLength) 來產生一個長度為 4 陣列,然後再用 Array.from() 方法得到一個新的陣列,而這個新的陣列裡面的值設定為我們想要的值。
containers/MemoryBlocks/reducer.js
const createBlocks = sideLength => Array.from(Array(sideLength * sideLength), (value, index) => ({
id: index,
audio: () => getAudioObject(PIANO_SOUNDS[index]),
}));
const initialState = fromJS({
blocks: createBlocks(DEFAULT_SIDE_LENGTH),
sideLength: DEFAULT_SIDE_LENGTH,
...
});
在 blocks 陣列當中,每一個物件有兩個值,一個是 block 的 id,另外一個是 Audio Object(HTML5 audio 物件)。上面程式中,Audio Object 產生方法如下:
containers/MemoryBlocks/utils.js
export const getAudioObject = (note) => new Audio(PIANO_SOUNDS_URL + note + '.wav');
然後這邊的 PIANO_SOUNDS_URL 是 hahow 動畫設計課中,吳哲宇老師所提供的。
containers/MemoryBlocks/constants.js
export const PIANO_SOUNDS_URL = 'https://awiclass.monoame.com/pianosound/set/';
由於音訊檔的命名是按照音樂的簡譜記譜法
,1 代表 Do,1.5 代表 #Do,2 代表 Re,依此類推。因此我準備一個陣列來存這些檔名,也就是會讓我的每一個 block 透過 id 來對應一個音訊檔,參數如下:
containers/MemoryBlocks/constants.js
export const PIANO_SOUNDS = [1, 1.5, 2, 2.5, 3, 4, 4.5, 5, 5.5, 6, 6.5, 7, 8, 8.5, 9, 9.5, 10, 11, 11.5, 12, 12.5, 13, 13.5, 14, 15];
所以 id 是 0 的 block,我會拿到 PIANO_SOUNDS[0] 的音,也就是 Do。 id 是 1 的 block,我會拿到 PIANO_SOUNDS[1] 的音,也就是 #Do,依此類推。
介绍音频 API
HTMLAudioElement
動畫互動網頁特效入門(JS/CANVAS)
另一方面,由於我們每一個 block 的顏色是獨一無二的,所以跟 PIANO_SOUNDS 一樣,我也會設計一個對應的陣列來存顏色的參數,所以透過 block 的 id ,我們可以拿到對應的顏色
containers/MemoryBlocks/constants.js
export const BLOCK_COLORS = [
'#ff5353',
'#ffc429',
'#5980c1',
'#fbe9b7',
'#FF9F1C',
'#b2ff59',
'#69f0ae',
'#ffff00',
'#b2dfdb',
'#ff6e40',
'#00E5FF',
'#e0e0e0',
'#f06292',
'#ba68c8',
'#8c9eff',
'#8BC34A',
'#E91E63',
'#FFE2D1',
'#FFDF64',
'#00c853',
'#DCABDF',
'#78FFD6',
'#C8553D',
'#3185FC',
'#FFFFFF',
];
顏色的選取我是取自下面這幾個網站,手工挑的,不是自動產生的。因為我的方塊最多到 5x5 ,最多只有 25 個聲音和 25 個顏色,所以也沒有程式化自動選取的必要,而且自動選取顏色很容易會選到不夠漂亮的顏色,因此這邊就是純手工,以自己的美感和主觀來挑選。
準備好資料之後,接下來就是要把 blocks 在面畫面上迭代出來。不過在畫出方塊之前,我還有一件事情想做,就是我想要把方塊另外獨立出來元件化。
這邊來說明一下理由,因為透過 styled-component
,我們會讓每個方塊擁有它獨一無二的顏色,Day04 我們有提到,styled-component 可以將 React 的參數用props的方式傳入來控制樣式,也就是隨著不同的 props ,方塊會有不同的顏色。另外,使用 styled-components 會為我們生成的 React 元件產生隨機的 className ,藉此來解決 className 衝突的問題。
但值得注意的是,當 React 元件每次生成的時候,styled-component 也會為他重新產生一個 class 並且給他新的隨機的 className。所以每次記憶方塊遊戲在進行的時候,只要有事件發生,也就代表傳入 這個元件中的 props 會被改變,所以裡面所有的元件就會需要重新渲染
,擁有 styled-components 的每一個方塊也是如此,會需要重新產生新的 css class 以及 className。這些事件在遊戲進行的時候會非常頻繁
的發生,例如點擊方塊來回答問題的時候,題目播放時,方塊會需要按照題目順序各別亮起來,還有答對及答錯的時候所有的方塊會需要閃爍,任何事件的發生,所有的子元件不管有沒有改變狀態,都需要重新渲染。並且,遊戲難度增加的時候,方塊的數目會從 2x2 變成 3x3,再變成 4x4 ,再變成 5x5 。所以需要重新產生和渲染的元件會越來越多。
所以如果這個問題沒有特別注意到的話,遊戲玩到後來就會覺得越來越頓,而且每個事件發生的時候都需要很嚴重的停頓一下。下面是太頻繁重新渲染,所以 styled-component 太頻繁產生 class 所跳出的警告訊息:
為了解決這個令人困擾的問題,我想要避免元件不必要的重新渲染,所以我希望把方塊元件製作成 PureComponent,在適當的時機下,透過 PureComponent 可以提升效能,這是由於繼承 React.PureComponent 的元件,在生命週期 shouldComponentUpdate 中會對新的 props & state 與舊的 props & state 預設實作 shallow compare ,如果兩者相同就會回傳 false,不會 re-render component。
React.PureComponent is similar to React.Component. The difference between them is that React.Component doesn’t implement shouldComponentUpdate(), but React.PureComponent implements it with a shallow prop and state comparison.
If your React component’s render() function renders the same result given the same props and state, you can use React.PureComponent for a performance boost in some cases.
方塊元件的程式碼如下,我們把元件宣告成 PureComponent。方塊當中我傳入三個參數,一個是 id ,命名為 blockId,用來決定方塊的顏色及音效,再來是 sideLength ,當方塊越來越多的時候,我們要透過這個參數來調整方塊彼此之間的間距,方塊數越多,彼此間的間距應該要越小,這個方法我們在 Day04 調整井字棋方格彼此間的間距有用到,可以參考這篇文章,再來就是 handleOnClick ,也就是方塊被點擊到時需要做的事件處理。
containers/MemoryBlocks/components/Block/index.js
import React from 'react';
import PropTypes from 'prop-types';
import { StyledBlock } from './Styled';
class Block extends React.PureComponent {
static propTypes = {
blockId: PropTypes.number,
sideLength: PropTypes.number,
handleOnClick: PropTypes.func,
}
static defaultProps = {
blockId: 0,
sideLength: 0,
handleOnClick: () => { },
}
render() {
const {
blockId,
sideLength,
handleOnClick,
} = this.props;
return (
<StyledBlock
blockId={blockId}
sideLength={sideLength}
>
<div
id={`block-${blockId}`}
data-id={blockId}
className="block__block-item"
onClick={handleOnClick}
/>
</StyledBlock>
);
}
}
export default Block;
製作完方塊元件之後,我們就可以在主畫面把方塊用迭代的方式畫出來了
containers/MemoryBlocks/index.js
{
blocks.map((block) => (
<Block
key={block.get('id')}
blockId={block.get('id')}
sideLength={sideLength}
handleOnClick={this.handleOnBlockClick}
/>
))
}
為了讓方塊以 2x2 的方式排列,我們跟前面一樣使用到 grid
containers/MemoryBlocks/Styled.js
.memory-blocks__blocks-wrapper {
position: relative;
width: ${GAME_WRAPPER_SIZE}px;
height: ${GAME_WRAPPER_SIZE}px;
@media only screen and (max-width: 600px) {
width: 100vw;
height: 100vw;
}
display: grid; /* 外容器宣告成 grid */
${(props) => {
const sideLength = props.sideLength;
return `
grid-template-columns: repeat(${sideLength}, 1fr); /* 設定縱列 grid */
grid-template-rows: repeat(${sideLength}, 1fr); /* 設定行列 grid */
grid-gap: ${40 / sideLength}px;
`;
}}
}
下圖就是我們今天的成果!