こんにちは。あのちっくです。
commmune Advent Calendar 2023 24日目の記事はIntersection Observer APIを使ってブロック崩しを作る話です。
どうぞよろしくお願いします。
導入
WebブラウザにはIntersection Observer APIというAPIがあります。
これは、Element同士の重なり具合が設定した閾値を超えたときにイベントを受け取ることが出来るAPIです。
主に指定の要素が画面に表示された時・画面から外れた時になんらかの処理を行いたいときに使うもののようです。
ですが皆さんは「Intersection」と効くと他に思い浮かべるものがあるかと思います。
そうですね。当たり判定です。
2つの要素の重なりについての情報が取れるAPIならば、きっとゲームプログラミングの「当たり判定」のような使い方もできるはずです。
なので今回は、Intersection Observer APIを使って「ブロック崩し」を作る遊びをしていきたいと思います。
Intersection Observer APIの基本的な使い方
function createObserver() {
let observer;
let options = {
root: parentElement,
threshold: 0.5,
};
observer = new IntersectionObserver(handleIntersect, options);
observer.observe(childElement);
}
Intersection Observer APIの使い方はとても簡単で、options.root
に親要素を指定し、observer.observe
の引数に子要素を与えて実行すると、監視が始まります。
options.threshold
は閾値を表しており、0.5
にすると、子要素が親要素から半分ほどはみ出したときにイベントハンドラ関数handleIntersect
が実行されるというものです。
parent
、child
と表現しましたが、監視対象エレメントに対してrootは祖先要素であれば問題なく動作します。
逆に言うと、兄弟要素など、祖先要素でない場合は反応しません。
この事を知らずに作業を始めてしまい、大変な思いをすることになりました。
ボールを移動させる。
まずはボールを移動させるところからです。
Reactを使いざっくりと実装しました。
const Ball = () => {
const ballElemRef = useRef<HTMLDivElement | null>(null);
const tick = () => {
const ballElem = ballElemRef.current;
if (!ballElem) return;
const vY = Number(ballElem.dataset.vy ?? 0);
const vX = Number(ballElem.dataset.vx ?? 0);
const top = Number(ballElem.style.top?.replace("px", "") ?? 0);
const left = Number(ballElem.style.left?.replace("px", "") ?? 0);
ballElem.style.top = `${top + vY * 2}px`;
ballElem.style.left = `${left + vX * 2}px`;
};
useEffect(() => {
const id = setInterval(tick, 16); // 60fps
return () => clearInterval(id);
}, []);
return (
<div
className="bg-white h-4 w-4 absolute"
style={{
top: '0px',
left: '0px',
}}
data-vx={1}
data-vy={1}
ref={ballElemRef}
/>
);
};
setInterval
をつかって1フレームごとに縦横それぞれ2pxずつ動くようなボールを実装しました。
ちなみにtick
の中身は殆どGitHub Copilotを使って書いています。
ボールが壁に当たったら跳ね返る
次に、ボールが壁に当たったらいい感じに跳ね返るというものを作ってみましょう。
"白いボールが黒いエリアからはみ出た時"というのは簡単に取れそうですが、問題はどの壁にぶつかったかです。
座標を見て判断するのでも良いですが、それでは面白くないので今回は違う方法を考えてみます。
ボールの四方にヒットブロックを置く
画像のように、ボールの子要素として、上下左右に"HitBox"という不可視の要素を作ります。このヒットボックスが完全に(100%)画面から外れたら、その方向の壁に衝突したものとみなし、画面から外れたHitBoxがLeft,Rightであればx方向の速度反転、Top,Bottomであればy方向の速度反転をする。
これで壁にぶつかったときに跳ね返る処理が実現できます。
ブロックとの衝突について考える。
次に、ブロック崩しで崩されるブロックについて考えます。
ボールがブロックと接触した際、ブロックは消滅し、ボールは良い感じに跳ね返る挙動を実装したいです。
ボールとブロックとの衝突についても壁と同様で、跳ね返り方を決める為にはどの方向から衝突したかを判定する必要があります。
せっかくなので壁との衝突判定で使ったHitBoxを使って考えてみましょう。
ボールとブロックが衝突しているかどうかは、
「そのブロックが2つ以上のブロックに少しでも接触している」という判定の仕方をすると、ボールがどの方向で衝突しているかも判断することが出来そうです。
ひとまずこれでやってみましょう。
そして問題が...
冒頭で説明した通り、Intersection Observer APIは 祖先要素との交差状況を監視するAPI なので、ブロックとボールの交差を監視したい場合、 全てのブロックをボールの祖先要素にする 必要があります。
なんだか怪しい気配がしてきましたね。
そのことを踏まえて、とりあえず作ってみましょうか。
再帰だぁ!!!!
const BlockRecursive = ({ x, y }) => {
...
if (endOfBlocks) return <Ball />;
return (
<Block x={x} y={y}>
<BlockRecursive
x={nextX}
y={nextY}
/>
</Block>
);
};
上記のコードは例ですが、このように再帰描画されるコンポーネントを作りました。
末端のBallコンポーネントがHitBoxを持っているので、全てのブロックを対象とした交差監視が可能になります。
コンソールでDOMツリーをみるとこのようになっています。無理矢理感がすごいですね。なんだか嫌になってきました。
(中略)
できました。
下記のページで実際に動作を確認できます。
https://pg.anochick.com/ac/2023/commmune/24
おわり
終わりです。
「あれ?まだ操作できるバーを作ってないじゃん...」って?
終わりったら終わりです。ブロックと同様に操作バーもボールの祖先要素になるように配置したら動作すると思います。
皆さんはWebブラウザ上でブロック崩しを作りたくなったら、その時は普通にcanvasとか使って座標計算するなりして作ることをおすすめします。
とはいえ、 Intersection Observer APIは祖先要素を対象とした交差監視しか出来ない ということを身にしみて理解できたので良かったです。
また、今回作成したもののソースコードは
https://github.com/anoChick/playground/blob/main/app/ac/2023/commmune/24/_components/breakout.tsx
で公開しておきますが、途中で頓挫したプロジェクトであることをご理解の上で参照ください。
それではみなさん Happy holidays!!