1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

連打ゲーを作った

Posted at

簡単なゲームを作ってみたくなったので、連打ゲーを作ってみました!

ユーザーの操作は

  • 連打する秒数を設定する
  • clickボタンを連打する
    のみです!

クリックされるたびにカウントアップするだけだと面白くないので

  • 設定された秒数の間は、何回クリックしたのはユーザーは確認できない
  • 設定された秒数が経過すると、クリックした回数がわかる
    • ただ回数を出すのは面白くないので、数字が増えていく様子が見えるようにする
      という仕様で作りたいと思います!

完成物

キャプチャ

count-click-demo.gif

コード

import { ChangeEvent, useState } from "react";

function App() {
	const button = document.querySelector("button");

	const [time, setTime] = useState(1);
	const handleChangeTime = (e: ChangeEvent<HTMLInputElement>) => {
		setTime(Number(e.target.value));
	};

	const [count, setCount] = useState(0);
	const handleClick = () => {
		if (time <= 0) {
			alert("秒数は1以上を設定してください");
			return;
		}
		setTimeout(() => {
			setCount((prev) => prev + 1);
			if (button) {
				button.disabled = true;
			}
		}, time * 1000);
	};

	const resetGame = () => {
		setCount(0);
		if (button) {
			button.disabled = false;
		}
	};

	return (
		<div>
			<input type="number" value={time} onChange={handleChangeTime} />
			<span>秒間クリックしまくれ!!</span>

			<h1>{count}</h1>

			<button onClick={handleClick}>click!</button>
			<button className="reset" onClick={resetGame}>
				reset
			</button>
		</div>
	);
}

export default App;

N秒後にstateを更新したい

  • 設定された秒数の間は、何回クリックしたのはユーザーは確認できない
  • 設定された秒数が経過すると、クリックした回数がわかる
    • ただ回数を出すのは面白くないので、数字が増えていく様子が見えるようにする

この仕様を実現するために「ユーザーがクリックしているときにcountを更新を保留し、N秒経過した後に、保留していたcountの更新を行う」という実装を試みました。

失敗例

const [count, setCount] = useState(0);

const handleClick = () => {
	// 一部処理を省略しています
	setTimeout(() => {
		setCount(count + 1);
	}, time * 1000);
};

この例ではsetCountcount + 1を渡しています。これだとうまくいきません。

ユーザーがボタンをクリックしてから(handleClickが発火してから)time秒後にsetCount()が実行され、現在のcountに1が足されるので問題ないように見えますが、この実装だとcountが1以上になることはありません。

これは古いクロージャによる更新を行なってしまっていることが原因です。

setTimeout()に設定した秒数の間にボタンをクリックした時、setTimeout()に渡されているコールバック関数は同じクロージャになります。つまり、1度目のクリックも2度目のクリックも、コールバック関数が() => setCount(0 + 1)と解釈されるということです。

なので、countが1になった後に更新されないのです。

わかりやすい解説
https://speakerdeck.com/recruitengineers/react-yan-xiu-2024?slide=125

成功例

これはsetCount()(prev) => prev + 1というコールバックを渡すことで解決します。

const [count, setCount] = useState(0);

const handleClick = () => {
	// 一部処理を省略しています
	setTimeout(() => {
		setCount((prev) => prev + 1);
	}, time * 1000);
};

prevには(prev) => prev + 1が実行されるときのcountの値が入ります。

なので、クリックした回数分、countが更新されます。

妥協点

レンダリング効率

countが更新されるたびにApp.tsx全体が再レンダリングされるので、本当はcountに関する処理のみを担当するコンポーネントを作成して、count更新による影響範囲を最小限にするべきだと思います。
面倒くさかったのでやりませんでした。

結果発表方法

結果発表時に一定間隔で数値を増やしていきたかったのですが、実装が大変そうだったので妥協しました。
今はクリックしてから4秒後にcountを更新する、という実装になっているため、結果発表時にクリック回数がスムーズに増えていかないことがあります。例えば、一度クリックした後に2秒待ってから再度クリックする、という動作を行うと、結果発表時にもcountが1→2になるのに2秒かかります(タスクキューに入るのに時間差がある)。

とはいえ、ゲームの趣旨的に制限時間いっぱいまでユーザーはボタンを連打し続けてくれだろうと考え、改修は先送りにしました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?