情報系の大学に入ったので春休み中にReact+TSでマインスイーパーとテトリス作ってみた
はじめまして。大学2年のYoshiと申します。
今回が初めての記事で、マインスイーパーとテトリスの解説だけでなく自己紹介やゲームを作った経緯なども合わせて載せています。
これから優れたエンジニアになるために学んでいく様々なことをZennに記録として残していこうと思いますので、どうぞよろしくお願いします!
自己紹介
東洋大学情報連携学部2年のYoshiと言います。
プログラミングは大学に入ってから真剣に学び始めました。
子供の時から何か面白いものを作りたいという欲があり、プログラミングを学ぶことを決意しました。
大学の講義で学んだこと
講義では主にPythonやJavaScript、アルゴリズムの基礎的なことを学びました。また、Djangoを使ってWebアプリケーションを作ったりしました。
課題やテストは問題なくこなしています。
React+TSとの出会い
大学でTypeScriptを学ぶ非公式サークルができ、大学の講義の内容だけでは成長をあまり感じることができず、もっといろいろなプログラミングを学びたいと思い参加し、学び始めました。
なぜゲームを作ったのか
もともと学ぼうと思ったのは、web系のプログラミングでした。しかし、そんな中でゲーム作りを選んだ理由はフレームワークの使い方とアルゴリズムを同時に習得するのに良く、デザインがほとんど不要でコーティングに注力できると思ったからです。そしてその中でも特に、ユーザーの操作で状態が著しく変化するプログラムを書く練習にもなると考えました。
マインスイーパーの解説
まず最初にマインスイーパーというゲームを製作しました。
ぜひ遊んでみてください!
いろいろな方法で試みましたが上手くいかず苦労しましたが、最終的には連鎖できるマスをリストに追加し続けFor文でゲームボードを書き換えることで上手くいきました。
もっと具体的にはCountBombs
で周りにある地雷を数える関数やListofAround
で周りの座標をリスト化する関数を使うことで白マスを連鎖させることができました。
// クリックした座標に地雷がないとき
if (!existBomb) {
let NumBombs = 0
// 周りの地雷を数える
NumBombs = CountBombs(x, y, NewBombs)
newBoard[y][x] = NumBombs
setRestBlock((c) => c + 1)
// 白マス連鎖(周りの地雷が0個だったら)
if (NumBombs === 0) {
let NewNumBombs = 0
// クリックした周りの座礁をリスト化する
const Coordinate = ListofAround(x, y)
for (const c of Coordinate) {
NewNumBombs = CountBombs(c.x, c.y, NewBombs)
// まだ開いていないマスだったらNewNumBombsの数字で開く
if (newBoard[c.y][c.x] === 9) {
newBoard[c.y][c.x] = NewNumBombs
setRestBlock((c) => c + 1)
}
// 地雷が0個だったらその周りの座標をリストに追加する
if (NewNumBombs === 0) {
for (const nc of ListofAround(c.x, c.y))
if (!Coordinate.some((nb) => nb.x === nc.x && nb.y === nc.y)) {
Coordinate.push({ x: nc.x, y: nc.y })
}
}
}
}
}
また、React hooksであるuseState
やuseEffect
の使い方を学び、JavaScriptの書き方も上手くなりました。
useState
ではゲームクリアやゲームオーバーの状況管理やゲームボードの状態管理などを行いました。
//ゲームクリアの状態管理
const [gameClear, setGameClear] = useState(false)
useEffect
は時間の計測に使いました。setInterval
を使うことで毎秒count
が増えるようになっています。また、ゲームの状況に応じてカウントするかを判定するようにしています。
// 時間の計測
const [count, setCount] = useState(0)
useEffect(() => {
// ゲームの状況に応じてカウントするかを判定する
if (gameStart && !gameOver && !gameClear) {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}
}, [gameStart, gameOver, gameClear])
テトリスの解説
次に作ったのがテトリスです。
ぜひ遊んでみてください!
useMemo
やuseCallback
などの新しいことを学んだり、uesEffect
での時間に応じたプログラムを書くことなどの練習になりました。
難しかったのはテトリミノの扱いです。画面外にいかないようにしたり、重ならないようにするプログラムを思いつくのが大変でした。
引数にXY座標、テトリミノの状態を与えることで、その位置が画面内かどうかと重なっているかを判定して、重なっていたらtrue
,そうでなければfalse
を返す仕組みになっています。
// 動かせるかを判定する関数
const checkCordinate = (cx: number, cy: number, tetromino: number[][]): boolean => {
for (let y = 0; y < tetromino.length; y++) {
for (let x = 0; x < tetromino[y].length; x++) {
if (tetromino[y][x] > 0 && board[y + cy][x + cx] > 0) {
return true
}
}
}
return false
}
また、回転時の処理が、周りに障害物があると上手くいかず、最後まで課題として残りましたが、無事にプログラムを書けました。↓
テトリミノの回転状況によって処理が変わるため、三項演算子でプログラムが複雑っぽくなっています。しかし、回転した時に周りにそのテトリミノが存在できる空間があるかを、For文でmoveX
, moveY
を一定まで増やして判定し、空間があった場合にそのmoveX
, moveY
分動いてから、その回転の処理を行うというシンプルなプログラムです。
//回転させる関数
const changeRotate = (checkRight:boolean): void => {
for (let moveY = 0; moveY < 3; moveY++) {
for (let moveX = 0; moveX < tetromino[0].length - 1; moveX++) {
if (
!checkCordinate(x + moveX, y - moveY, tetromino[checkRight ? (rotateNumber < 3 ? rotateNumber + 1 : 0) : (rotateNumber > 0 ? rotateNumber - 1 : 3)])
) {
X((c) => c + moveX)
Y((c) => c - moveY)
setRotateNumber(checkRight ? ((c) => (c < 3 ? c + 1 : (c = 0))) : ((c) => (c > 0 ? c - 1 : (c = 3))))
return
}
}
for (let moveX = -1; moveX > -(tetromino[0].length - 1); moveX--) {
if (
!checkCordinate(x + moveX, y - moveY, tetromino[checkRight ? (rotateNumber < 3 ? rotateNumber + 1 : 0) : (rotateNumber > 0 ? rotateNumber - 1 : 3)])
) {
X((c) => c + moveX)
Y((c) => c - moveY)
setRotateNumber(checkRight ? (c) => (c < 3 ? c + 1 : (c = 0)) : ((c) => (c > 0 ? c - 1 : (c = 3))))
return
}
}
}
}
このほかにはuseMemo
を使ってゲームボードの状態を管理しました。
changeBoard
でゲームボードの状態を持ってきて、slice
で画面に映る上下の範囲を指定し、filter
で両端の1列をなくすという処理をしています。(両端に9を置くことでテトリミノが画面外に行かないようにプログラムしています)
// 画面に映すゲームボードの状態に更新する
const completeBoard = useMemo(
() =>
changeBoard()
.slice(3, 23)
.map((e) => e.filter((num) => num !== 9)),
[x, y, rotateNumber]
)
useCallback
とKeyboardEvent.code
を使うことでキーボード操作を可能にしました。
// キーボード操作
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
switch (e.code) {
case 'ArrowLeft':
moveLeft() // 左に動く関数
break
~省略~
case 'ShiftRight':
holdfunc() // ホールドを行う関数
break
}
},
[x, y, tetromino, rotateNumber]
)
全体の反省点としてはコードがとても読みづらいことです。特にテトリミノをリセットする関数やホールドを行う関数は変数の更新が多くて、私目線すごく汚く見えました。
Udemyで学んだこと
マインスイーパーとテトリスを作るまではReact, TypeScriptの具体的なことは学ばず,必要最低限で来ました。そのため、ゲームのソースコードもコンポーネント分割をしておらず、見づらくなっていたと思います。
そのことから、React,TypeScriptについてもっと学ぼうとUdemyの講座を受けました。
受けた講座は以下になります
その中でAtomic DesignやReact Router、カスタムフックなどを学びました。
まだ学びはインプットをした状態で、自分で新しいプロジェクトを作ってアウトプットはあまりできていないため、たくさんコードを書いて自分のものにしたいです。
やっていないこと
ステート管理(Udemyで少し触れた程度)や環境構築、Dockerのあたりはやっていません。
やっていきたこと(バイトもしたい)
2年生の講義でC言語を学んでいくので、その技術とReactなどを組み合わせて、何か作っていきたいと考えています。
その中、IT企業でバイトをしてみたいと思っています。大学の講義があるので8月からの夏休みまでは沢山の時間はできないのですが、一般的な大学よりも時間はあると思うので連絡お待ちしております。バイト内容としてはリモートでReact+TSでできて、業務系SaaSの管理画面開発みたいな分野が理想的だと考えています。
連絡手段
ここまで読んでいただきありがとうございます!
連絡の際はTwitterにDMを飛ばしていただけると幸いです。
Discussion
「白マスが連鎖するプログラム」を書くのに苦労したとのことですが、このような「繋がっている部分を検出する」のには "union-find" というアルゴリズムが便利です。ググればたくさん情報が出てくるので、調べてみると幸せになれるかもしれません。
コメントありがとうございます!!
"union-find"調べてみます!