iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 29
0
自我挑戰組

React初心者30天的探索之路系列 第 29

[Day 29] 用React 來寫ooxx 小遊戲

  • 分享至 

  • xImage
  •  

ooxx也可以說是很熱門的練習題,所以今天就用React 來寫一個ooxx的小遊戲吧!

初始化先設定一個長度為9的陣列來記錄玩家下的位置,這邊用了array.from({length}*9) 就會製造出一個長度為9的陣列,但因為我沒有另外寫callback function去設定 ,所以實際上這個陣列就會是 [undefined,undefined,…undefined]

const [record, setRecord] = useState(Array.from({ length: 9 }))

樣式的部分就寫在module.scss, 托flex的福 ,可以很快的解決九宮格css的部分,切完版之後,我需要抓到玩家點擊的是第幾個格子,所以我這樣寫,看玩家點擊哪一個格子就把那個格子index帶入,看起來一切都很合理…

<div onClick={handclick(index)} ></div>

然後畫面就掛掉了。

紅字錯誤顯示無限迴圈 ,我沒料到這樣寫handclick(index)就會馬上執行 ,因為vue這樣寫都沒事(喂,因為handclick function 改變了record的陣列,然後我的九宮格格子是依靠record陣列繪製出來的,再次render變成無窮迴圈,所以這邊記得要改成arrow function,就可以正常運作

接著列出贏家的畫線有幾種可能, 這邊用九宮格的位置來記錄,第一格index為0…最後一格為8

const arr = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]
];

設定玩家a為圈圈,用1來代表,玩家b則為叉叉,用-1來代表,由玩家a先開始,所以初始值設定為1,玩家a下完之後,如果沒有勝負,則輪到玩家b

const [player, setPlayer] = useState(1)

每當玩家點擊格子, 該格子就會依照是哪位玩家 ,就放入對應的數字 1 or -1 ,並且馬上比對剛剛列出八種勝出方式的陣列,並將格子內的ox轉換成對應的數字加總並且取絕對值 ,如果為3就是有人勝出了, 中斷迴圈開始準備畫線
ex 如果record 陣列這三個index (0,1,2)的數字加起來為3 or -3 就代表有人勝出了!(可以畫出最上排的水平線)

const caculation = () => {
    let length = arr.length
    for (let i = 0; i < length; i++) {
        let total = 0
        for (let k = 0; k < 3; k++) {
            total += record[arr[i][k]]
        }
        if (Math.abs(total) === 3) {
            getPos(arr[i])
            break
        }
    }
}

用canvas來畫線,在一開始就已經建立好畫布,只是先隱藏起來,這邊用偷懶的方式, 設定一個x、y的陣列,大概抓個格子的中線位置,所以畫的線不會很精準,先取出獲勝的組合假設是1、4、7好了,抓陣列第一個數字為起點,陣列最後一個數字為終點,再利用除法和取餘數,拿到這兩個格子是位在第幾列的第幾行

const getPos = (arr) => {
    let start = arr[0]
    let end = arr[2]
    let position = [50, 150, 250]
    let obj = {
        start: {
            x: position[start % 3],
            y: position[Math.floor(start / 3)],
        },
        end: {
            x: position[end % 3],
            y: position[Math.floor(end / 3)],
        }
    }
    drawLine(obj)
}

 const drawLine = (obj) => {
    const canvas = canvasRef.current;
    ctx = canvas.getContext('2d');
    ctx.beginPath();
    ctx.moveTo(obj.start.x, obj.start.y);
    ctx.lineTo(obj.end.x, obj.end.y);
    ctx.lineWidth = 5;
    ctx.stroke();
    console.log(player + 'player winner')

}

但是,這時又碰到一個小小的問題

利用useref來抓dom裡的canvas,殊不知一直抓不到 !! 一直是undefined,這時候推斷是雖然我即時將canvas更換為顯示的狀態,但是因為非同步的關係,下一行的canvasRef.current此時還是undefined狀態

一開始用了一個比較蠢的解法是 setTimeout(() => {const canvas = canvasRef.current;...})的確可以運作,但這是這樣寫用膝蓋想也知道不合理,後來查到比較正確的方式是在useEffect階段來取得useRef,不過也失敗了,為什麼?因為我的canvas預設是隱藏,所以useRef抓到的就會是null,問題好像越變越複雜,仔細想想,其實canvas也沒必要隱藏,只要可以讓使用者點擊到被canvas蓋住的格子就可以了,所以就在canvas身上設定css穿透屬性:pointer-events: none;就解決了!

完整程式碼

import React, { useState, useEffect, useRef } from 'react';
import style from './style/game.module.scss'
export default () => {
    const arr = [
        [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], 
        [2, 4, 6]
    ];
    
           
    const [record, setRecord] = useState(Array.from({ length: 9 }))  

    const [player, setPlayer] = useState(1)
    const canvasRef = useRef()


    const caculation = () => {
        let length = arr.length
        for (let i = 0; i < length; i++) {
            let total = 0
            for (let k = 0; k < 3; k++) {
                total += record[arr[i][k]]
            }
            if (Math.abs(total) === 3) {
                getPos(arr[i])
                break
            }
        }
    }


    const getPos = (arr) => {
        let start = arr[0]
        let end = arr[2]
        let position = [50, 150, 250]
        let obj = {
            start: {
                x: position[start % 3],
                y: position[Math.floor(start / 3)],
            },
            end: {
                x: position[end % 3],
                y: position[Math.floor(end / 3)],
            }
        }
        drawLine(obj)
    }

    const drawLine = (obj) => {
        const canvas = canvasRef.current;
        ctx = canvas.getContext('2d');
        ctx.beginPath();
        ctx.moveTo(obj.start.x, obj.start.y);
        ctx.lineTo(obj.end.x, obj.end.y);
        ctx.lineWidth = 5;
        ctx.stroke();
        console.log(player + 'player winner')
     
    }


    const handleClick = (event, index) => {
        record[index] = player
        setRecord(record)
        caculation()
        setPlayer(player * -1)
    }

    const resetGame = () => {
        setRecord(Array.from({ length: 9 }))
        setPlayer(1)
    }

    return (
        <div className={style.container}>
          <canvas width="300" height="300" className={style.myCanvas} ref={canvasRef} /> 
            {
                record.map((vo, k) => {
                    return (
                        <div className={style.item} 
                        key={k} 
                        onClick={(event) => handleClick(event, k)}>
                            <span className={style.content}>
                                {vo > 0 ? 'o' : vo < 0 ? 'x' : ''}
                            </span>
                        </div>
                    )
                })

            }
            <button onClick={resetGame}>resetGame</button>
        </div>
    )
}

最終成品!


上一篇
[Day 28] 用React 來做一個todolist吧!
下一篇
[Day 30] React 完賽心得
系列文
React初心者30天的探索之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言