iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 24
0

前情提要

今天我們要來實作記憶方塊的主畫面區塊,在昨天的努力之下,我們已經把區塊都規劃好了,接下來就是要把我們的方塊畫上去。

由於前兩個遊戲 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,
    ...
});

Array
Array.from()

準備音樂

在 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 個顏色,所以也沒有程式化自動選取的必要,而且自動選取顏色很容易會選到不夠漂亮的顏色,因此這邊就是純手工,以自己的美感和主觀來挑選。

coolors
Material Design

方塊元件化

準備好資料之後,接下來就是要把 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 所跳出的警告訊息:

Over 200 classes were generated for component Component.

以 PureComponent 改善效能

為了解決這個令人困擾的問題,我想要避免元件不必要的重新渲染,所以我希望把方塊元件製作成 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.

React.PureComponent
寫 React Components 該注意的6個地方與技巧

方塊元件的程式碼如下,我們把元件宣告成 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;
            `;
    }}
}

成果展示

下圖就是我們今天的成果!
draw-blocks

參考程式碼 & 遊戲展示

Memory Blocks - Github


上一篇
Day23 - 記憶方塊篇:頁面佈局規劃
下一篇
Day25 - 記憶方塊篇:幫方塊做出炫炮又迷幻的動畫
系列文
以經典小遊戲為主題之ReactJS應用練習30

尚未有邦友留言

立即登入留言