Reactでブロック崩し作ってみた!!(コード編)
目次
はじめに
メリークリスマス!!現二回生ポテリーです。クリスマスはいかがお過ごしでしょうか。私は今年に爪痕を残すべく今更勉強、作業に注力しています。この回では前回に引き続きReactで作ったブロック崩しのコード解説をしていこうと思います。全ては説明しきれないので主要な部分のみになります…!前回は自己満足の記事だったのでこの記事から見始めて大丈夫です。この回ではHTML、CSS、Javascriptは触ったことがある前提で話を進めていきますのでご注意ください。私も学習中の身ですので、生温かい目で見てください。
コード解説
この節では部位ごとに分けてコードの解説を行っていきます。CSSには追加でtailwindcssを用いているので注意してください。
プレイヤー移動
まず、実現したい動作の確認します。
マウス押下時
プレイヤーの移動開始
ドラッグ中
移動
マウスを離した時
プレイヤーの移動終了
const [playerPositionX, setPlayerPositionX] = useState(175); //プレイヤーの位置
const [isFired, setIsFired] = useState(false); //ボールの発射(プレイ開始)フラグ
const [startX, setStartX] = useState(0); //カーソルの位置
const [isDragging, setIsDragging] = useState(false); //ドラッグ中かを判定するフラグ
const handleMouseDown = (e) => { //押下したときの処理
setIsDragging(true);
setStartX(e.clientX);
if (!isFired) setIsFired(true);
};
const handleMouseMove = useCallback( //動いているときの処理
(e) => {
if (isDragging) {
const deltaX = e.clientX - startX;
const newPosition = Math.max(0, Math.min(playerPositionX + deltaX, 350));
setPlayerPositionX(newPosition);
setStartX(e.clientX);
}
},
[isDragging, startX, playerPositionX]
);
const handleMouseUp = useCallback(() => setIsDragging(false), []); //離したときの処理
useEffect(() => { //イベントリスナーの追加・削除
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
} else {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
handleMouseDown
マウス降下時の処理は、handleMouseDownで記述しています。処理に入った時、isDraggingというフラグをtrueにすることで状態を伝えています。このような時、useStateという変数の状態管理のHooks(Reactで提供されている関数コンポーネント)を使用することでクラスのthisを使わず簡潔に状態管理を行うことが可能です。setIsDragging(true);と記述すれば、isDraggingがtrueに変わります。同様にsetStartXでマウスカーソルの現在の位置を記録。e.clientXにはマウスカーソルのx座標が格納されているのでこれを使用しています。isFiredはボールの打ち出しに関するフラグです。初期状態ではプレイヤーが操作するまで、つまりマウス押下までボールは動かないようになっています。
handleMouseMove
この関数ではe.clientXから、1フレーム前のマウスカーソルの座標であるstartXを引くことにより、移動量を計算、新しいプレイヤーの座標をsetPlayerPositionで設定していきます。また、プレイヤーが画面外に移動することを防ぐために、playerPositionXをmin、Max関数で0から350pxになるように制限しています。(プレイ画面のwidthは500px、プレイヤーは150px)
handleMouseUp
プレイヤーの移動を停止させるためにisDraggingをfalseに変更しています。
EventListenerの登録
上記だけではただ関数を作っただけなので、実際に動作させるための設定をしていきます。useEffectでは、ページ更新(マウント)前後、または第二引数に記述した変数や関数の状態更新時に処理を走らせます。ここではisDraggingが状態変更をしたとき発動します。(マウント時にもreturn前の処理は行われるがif文で制御している。)addEventListenerでmouseup、mousemove時に第二引数に設定されている処理(関数)を実行しています。またhandleMouseDownの実行はhtmlのdivタグ内onMouseDown属性で設定しています。
プレイヤーの位置を伝える
const Game_contents = () => {
return (
<Player positionX={playerPositionX}/> //player.jsx
);
};
子コンポーネントであるplayer.jsxにprops(受け渡し配列)として値を渡しています。これにより、player.jsx内で値を操作することが出来るようになります。ちなみにreturn文の中にhtmlの記述をしていくのがReactの特徴です。
const Player = ({ positionX }) => {
return (
<div
className="player"
style={{
transform: `translateX(${positionX}px)`, //x方向に動く
}}
></div>
);
};
positionXを引数で受け取り、html内のstyle属性でtranslateXの値を操作しています。これにてplayerの移動ができるようになりました。
ボールのアニメーションと壁反射
const [ballX, setBallX] = useState(initialX); //現在のボールの位置x
const [ballY, setBallY] = useState(initialY); //現在のボールの位置y
const [evenX, setEvenX] = useState(false); //反射時の遇奇管理x用
const [evenY, setEvenY] = useState(false); //反射時の遇奇管理y用
let ballSpeedX = (isSpeed ? -10 : -5) * Math.pow(-1, evenX + 2);
let ballSpeedY = (isSpeed ? -10 : -5) * Math.pow(-1, evenY + 2);
const vectorXRef = useRef(ballSpeedX); //ベクトルxを定義
const vectorYRef = useRef(ballSpeedY); //ベクトルyを定義
const animationRef = useRef(); //requestAnimationFrameのIDを格納
const updateBallPosition = useCallback(() => {
vectorXRef.current = ballSpeedX;
vectorYRef.current = ballSpeedY;
let nextX = ballX + vectorXRef.current; //1フレーム分x方向に進める
let nextY = ballY + vectorYRef.current; //1フレーム分y方向に進める
if ((nextX <= 0 || nextX >= boxDimensions.width - 16) && nextY < 300) { //左右の壁への衝突判定
vectorXRef.current *= -1;
setEvenX((even) => !even);
}
if (nextY <= -boxDimensions.height || nextY >= boxDimensions.height) { //上の壁への衝突判定
vectorYRef.current *= -1;
setEvenY((even) => !even);
}
setBallX(nextX);
setBallY(nextY);
animationRef.current = requestAnimationFrame(updateBallPosition); //再帰的に繰り返す
}, [
boxDimensions,
ballX,
ballY,
playerPositionX,
initialX,
initialY,
ballSpeedX,
ballSpeedY,
]);
requestAnimationFrame
ボールの位置更新はupdateBallPositionを再帰的に回すことで実現しています。前記事でも言及した通り、requestAnimationFrameはsetTimeoutや、setIntervalを使用するよりもアニメーションがスムーズになります。これはフレームを更新するタイミングでの関数実行となるからです。
useRef
useRefで定義したanimationRefのcurrentに代入しています。あくまでcurrentに代入しているので、再レンダリングがされないのがuseRefの特徴的な部分です。useStateでは状態が変化するたびにコンポーネントが再レンダリングされるため、アニメーションのパフォーマンスが悪化する可能性があります。また、useRefではHTMLの要素の高さや幅などの情報を取得することもできますが、今回は行っていません。続けて、animationRef.currentにrequestAnimationFrameを代入することによって独自のIDを保持します。これにより、canselAnimationFrameでアニメーションを停止させることが出来ます。
ボールを動かす
次に肝心のボールを動かす箇所を解説します。ballSpeedX、ballSpeedYには、通常、-5か5が代入されます。アイテムを取得するとスピードが上がるように変更を加えているため、evenX、evenYなどの面倒くさい要素が追加されています。evenXがtrue、evenYがfalseの時、ballSpeedX、ballSpeedYにはそれぞれ5、-5が代入されます。このevenX、evenYは衝突判定があった時に更新され、真偽が反転します。ballSpeedX、ballSpeedYはuseRefで定義されたvectorXRef、vectorYRefのcurrentにそれぞれ代入され、衝突判定があった方向に合わせて、-1を掛けることで向きを反転させています。壁の衝突判定には多様の冗長性を持たせています。無駄ではない冗長性です(?)。境界ギリギリに衝突判定を持たせてしまうと思ったように反射しないのです。1px進むにつれて判定を行ってくれれば良いのですが、判定を飛び越えてすり抜けていってしまう場合があるので余裕を持たせています。
ブロック生成
存在定義
const initializeBlocks = () => {
return Array.from({ length: 18 }, () =>
Array.from({ length: 8 }, () => Math.random() > 0.5) //50%の確率でブロック生成
);
};
const [blockStatus, setBlockStatus] = useState(initializeBlocks);
上記の部分において初期状態でブロックを表示するか否かをboolで返しています。random関数は0~1までの値をランダムに生成するので0.5を基準で大小判定することによって1/2の確率を表現できます。ブロックが表示される領域は縦18×横8と決まっているので、その分だけ二次元配列を生成しておき、その要素に真偽値を代入させています。for文とpushを使って代入するのも良いのですが、Array.fromの方が簡潔に記述することが出来ます。またこの二次元配列はその後も逐一更新していくものなのでuseStateで管理していきます。
色定義
const getColor = () => { //4種類の色からランダムに決定
const colors = ['orange', 'aquamarine', 'hotpink', 'chartreuse'];
const index = Math.floor(Math.random() * colors.length);
return colors[index];
};
const initializeBlockColors = useCallback((blockStatus) => {
return blockStatus.map((row) =>
row.map((isVisible) => (isVisible ? getColor() : 'transparent')) //trueなら色を付ける
);
}, []);
const initialBlockColors = useRef(initializeBlockColors(blockStatus)).current;
initializeBlockColors関数にてblockStatusでtrueになっている部分に対してgetColor関数を実行し、返り値として色を受け取ります。falseの部分にはtransparentを設置しています。またfor文ではなくmapメソッドを使って簡潔に記述できるのでとても良いです。まだ私も慣れていないですがとても便利です。ちなみにこのゲームにはfor文は一度も出てきません。そして毎フレーム実行されて色の抽選が再度行われないようにuseRefを使用しています。
上記の色定義とブロック生成は下画像のようになっています。
ブロックの実体
ブロックの定義は終わりましたが、まだ実際に配置をする作業が残っています。
const Game_contents = () => {
return (
{blockStatus.map((row, rowIndex) => (
<Blocks
key={rowIndex} //一意のキーを設定
rowStatus={row} //行の状態を渡す
rowIndex={rowIndex} //行番号を渡す
blockStatus={blockStatus} //ブロック全体の状態を渡す
blockColors={initialBlockColors} //色配列を渡す
/>
))}
);
};
ここではBlocksコンポーネントを配置しています。このBlocksには1行8列のブロック配列が入っているので、それをblockStatusの行数分(18行)繰り返して動的に生成。このとき、Blocksにおいて使う値をpropsとして渡しています。
const Blocks = ({ rowStatus, rowIndex, blockStatus, blockColors}) => {
return (
<div className="flex justify-center">
{rowStatus.map((isVisible, colIndex) => ( //各ブロックにアクセス
<Block
key={`${rowIndex}-${colIndex}`} // 一意のキーを設定
rowIndex={rowIndex} // 行番号
colIndex={colIndex} // 列番号
blockStatus = {blockStatus} //そのまま渡す
blockColors = {blockColors} //そのまま渡す
/>
))}
</div>
);
};
ここでも行っていることはほとんど同じです。子コンポーネントであるBlockは単一のブロックなので、これを1行8列で並べています。また列番号と行番号を送ることで、Blockにおいてその値を使えるようにしています。
const Block = ({ blockStatus, blockColors, rowIndex, colIndex }) => {
const isVisible = blockStatus[rowIndex][colIndex]; //特定の行・列の状態を渡す
const bgColor = blockColors[rowIndex][colIndex]; //特定のブロックの色を渡す
return (
<div
className={`block ${!isVisible ? 'invisible' : ''}`} //表示・非表示
style={{
backgroundColor: bgColor //色設定
}}
></div>
);
};
ここまでGame_contents.jsxからpropsとして送られてきた行番号、列番号、ブロックの色、真理値をここで全部使っていきます。各ブロックを行番号、列番号で特定し、真理値を見ることで表示するか非表示にするか、何色に設定するかをhtmlで設定します。これにて実体が出来ました。
ブロック衝突・アイテム生成・配置
const [score, setScore] = useState(0);
const BlockColliderLeftRight = [ //ブロックの生成座標セット
[5, 68, 129, 190, 251, 312, 373, 434], // left(x座標)
[67, 128, 189, 250, 311, 372, 433, 494], // right(x座標)
];
const BlockColliderUpDown = [
[-596, -562, -528, -494, -460, -426, -392, -358, -324, -290, -256, -222, -188, -154, -120, -86, -52, -18], // up (y座標)
[-563, -529, -495, -461, -427, -393, -359, -325, -291, -257, -223, -189, -155, -121, -87, -53, -19, 15] // down (y座標)
];
const [collisionDirection, setCollisionDirection] = useState({
left: false,
right: false,
up: false,
down: false
});
const checkBlockCollision = (ballPosX, ballPosY, ballRadius) => {
let updatedCollisionDirection = {
left: false,
right: false,
up: false,
down: false,
};
setBlockStatus((prevStatus) =>
prevStatus.map((row, rowIndex) =>
row.map((isVisible, colIndex) => { //個々のブロック状態にアクセス
if (!isVisible) return false;
const left = BlockColliderLeftRight[0][colIndex]; //ブロック左の座標を格納
const right = BlockColliderLeftRight[1][colIndex]; //ブロック右の座標を格納
const up = BlockColliderUpDown[0][rowIndex]; //ブロック上の座標を格納
const down = BlockColliderUpDown[1][rowIndex]; //ブロック下の座標を格納
if ( //ブロックの中に入ったかどうかを見る
ballPosX + ballRadius + 7 >= left &&
ballPosX - ballRadius - 7 <= right &&
ballPosY + ballRadius + 7 >= up &&
ballPosY - ballRadius - 7 <= down
) {
if (ballPosX + ballRadius + 7 > left && ballPosX - ballRadius < left + 10 && ballPosY >= up && ballPosY < down ){ //左からの衝突
updatedCollisionDirection.left = true;
setScore(score + 50) //スコア増加
generateItem(rowIndex, colIndex); //指定の位置にアイテム生成をする関数
return false;
}
if (ballPosX - ballRadius -7 < right && ballPosX + ballRadius > right - 10 && ballPosY > up && ballPosY <= down ) { //右からの衝突
updatedCollisionDirection.right = true;
setScore(score + 50 )
generateItem(rowIndex, colIndex);
return false;
}
if (ballPosY + ballRadius + 7> up && ballPosY - ballRadius < up + 10 && ballPosX >= left && ballPosX < right ) { //上からの衝突
updatedCollisionDirection.up = true;
setScore(score + 50 )
generateItem(rowIndex, colIndex);
return false;
}
if (ballPosY - ballRadius - 7< down && ballPosY + ballRadius > down - 10 && ballPosX > left && ballPosX <= right) { //下からの衝突
updatedCollisionDirection.down = true;
setScore(score + 50 )
generateItem(rowIndex, colIndex);
return false;
}
}
return true;
})
)
);
setCollisionDirection(updatedCollisionDirection); //新しいブロック状態に更新
};
ブロック衝突
どの方向からの衝突かによって反転させるボールのベクトルがx方向なのかy方向なのかが変わってきます。なので、どの方向からの侵入なのかを判別するuseState管理のオブジェクトCollisionDirectionを作成しました。また、どのブロックが表示されているのかの状態を格納しているblockstatusを確認し、true(表示)になっているブロックに対して、衝突判定を常に動かし、入ってきた方向の要素をupdatedCollisionDirection.left = true;のように変更することで方向を検知しています。さらに、blockStatusの衝突されたブロックに対してもfalseを返して非表示にしています。これでぶつかった時にブロックを消すことが出来ます。ここの衝突判定でも範囲に余裕を持たせています。また衝突を検知したと同時にスコアを増加させる処理を追加しています。
アイテム生成
const [items, setItems] = useState([]);
const getColor2 = () => { //アイテムの色を取得する関数
const colors = ['cornflowerblue', 'mediumorchid', 'yellow', 'darksalmon']; //4種類からランダム
const index = Math.floor(Math.random() * colors.length);
return colors[index];
};
const ItemPositionXY = [ //アイテムの生成座標セット
[563,529,495,461,427,393,359,325,291,257,223,189,155,121,87,53,19,-15],//-15*i+34
[16,77,138,199,260,321,382,443]//16+61*i
];
const generateItem = (rowIndex, colIndex) => {
if (Math.random() < 0.03) { // 3%の確率でアイテム生成
const itemX = ItemPositionXY[1][colIndex];
const itemY = ItemPositionXY[0][rowIndex];
const itemColor = getColor2(); //アイテムの色を取得
setItems((prevItems) => [
...prevItems,
{ x: itemX, y: itemY, color: itemColor, id: `${rowIndex}-${colIndex}`} //アイテム配列に追加
]);
}
};
generateItemでアイテムを生成しています。ItemPositionXYにおいて各ブロックの中央の座標を格納しており、ブロックが消失したとき、そのブロック座標に対応した座標に生成するような仕組みです。また、ブロックの色とは違う、アイテムの色を取得するためのgetColor2関数を用いてアイテムの色をランダムに決定しています。このアイテムの色によって、取得したときの効果を区別します(後述)。setItemsに現在のアイテム配列(アイテムは同時に複数存在し得る)に加え、新しいアイテムオブジェクトを追加してきます。ここで使われているのがスプレッド構文(…prevItemsの部分)です。この構文を用いることで、pushのような破壊的なメソッドを用いることなくかつ簡潔に要素を追加するコードを記述することが出来ます。このgenerateItemをブロックの衝突判定が発動するとき(ブロックが消失したとき)に処理します。またこのgenerateItemの内側の処理は3%の確率で通過するように条件分岐を設けています。
アイテム配置
const Game_contents = () => {
return (
{items.map((item) => (
<Item
key={item.id} //一意のキー
itemPosX={item.x} //アイテム生成位置x
itemPosY={item.y} //アイテム生成位置y
itemColor={item.color} //色
playerPositionX={playerPositionX} //プレイヤーの位置
ItemEffect={ItemEffect} //色によってアイテム効果を分別する関数
/>
))}
);
};
Blocksと同じようにmapを用いて動的にItemを生成しています。この時、またpropsでアイテム生成の位置や関数、色などが譲渡されています。
const Item = ({ itemPosX, itemPosY, itemColor, playerPositionX, ItemEffect }) => {
const getIconName = (color) => { //ioniconsのアイコンのnameを色によって分ける
switch (color) {
case 'cornflowerblue':
return 'skull-outline'; //ドクロマーク
case 'mediumorchid':
return 'bowling-ball-outline'; //ボウリングの玉
case 'yellow':
return 'star-outline'; //星
case 'darksalmon':
return 'heart-outline'; //ハート
default:
break;
}
};
return (
<div
className={`w-10 h-10 rounded-full flex items-center justify-center ${isInvisible ? 'invisible' : ''}`}
style={{
position: 'absolute',
left: `${itemPosX}px`,
bottom: `${itemPosition}px`,
backgroundColor: itemColor,
}}
>
<ion-icon name={getIconName(itemColor)}></ion-icon> //色によって分けたnameを配置
</div>
);
};
使っているアイコンはioniconsなのですが、それはion-iconタグのname属性に名前を指定することで区別をするので、switch文を用いてつける名前を変更しています。この時区別するときに使うのがpropsで渡されたitemColorです。htmlでpropsの値を各々代入していき、配置していきます。
アイテム効果
const [isSpeed, setIsSpeed] = useState(false); //ドクロマーク
const [isCollisionNone, setIsCollisionNone] = useState(false); //ボウリングの玉
const [isBarrier,setIsBarrier] = useState(false); //星
const [CountHeal,setCountHeal] = useState(0); //ハート
const ItemEffect = (color) => {
switch (color) {
case 'cornflowerblue': //ドクロマーク
toggleSpeed();
setTimeout(() => {
setIsSpeed(false);
}, 10000); // 10秒
break;
case 'mediumorchid': //ボウリングの玉
toggleCollisionNone();
setTimeout(() => {
setIsCollisionNone(false);
}, 10000); // 10秒
break;
case 'yellow': //星
setIsBarrier(true);
break;
case 'darksalmon': //ハート
setCountHeal((count) => count + 1);
break;
default:
break;
}
}
ここでは色に応じて各アイテムのフラグを管理しています。ドクロマークとボウリングの玉のアイテムは効果が永続することのないように、10秒経ったら自動的にフラグを戻すようにしています。
ドクロマーク(速度二倍)
const Ball = ({isSpeed}) => {
let ballSpeedX = (isSpeed ? -10 : -5) * Math.pow(-1, evenX + 2); //ドクロマークのアイテムを取った時は10,-10
let ballSpeedY = (isSpeed ? -10 : -5) * Math.pow(-1, evenY + 2);
const vectorXRef = useRef(ballSpeedX);
const vectorYRef = useRef(ballSpeedY);
};
前の項でも少し言及しましたが、ここで受け取ったisSpeedの真偽で速度を変わります。trueになっているときは-10、10がballSpeedX、ballSpeedYに代入されます。
ボウリングの玉(貫通弾)
const Ball = ({isCollisionNone}) => {
if ((collisionDirection.left || collisionDirection.right) && !isCollisionNone) { //条件にisColliderNoneを追加
vectorXRef.current *= -1;
setEvenX((even) => !even);
}
if ((collisionDirection.up || collisionDirection.down) && !isCollisionNone) { //条件にisColliderNoneを追加
vectorYRef.current *= -1;
setEvenY((even) => !even);
}
};
親から受け取ったisCollisionNoneの真偽値をブロックコライダーの条件分岐にそのまま適用することによって、isCollisionNoneがtrueになった時、衝突判定を行わなくなります。
星(バリア)
const Player = ({isBarrier}) => {
return (
<div
className={`w-full h-1 bg-orange-300 mt-5 ${!isBarrier ? "invisible" : ""}`} //isBarrierの真偽によって表示・非表示
></div>
);
};
ここでは単純にisBarrierがtrueの時、divタグで作った線を下に表示するようにしています。
const Ball = ({isBarrier}) => {
if(isBarrier && nextY > 175){ //落ちそうになったら反射
vectorYRef.current *= -1;
setEvenY((even) => !even);
setIsBarrier(false);
}
};
受け取ったisBarrierがtrueの時、プレイヤーの位置に関わらず、跳ね返してくれます。
ハート(回復)
const ItemEffect = (color) => {
switch (color) {
case 'darksalmon': //ハート
setCountHeal((count) => count + 1); //ハートカウントを+1
break;
default:
break;
}
}
取得したアイテムの色がdarksalmonだった時、ハートカウンターを+1します。
const [heartNumber, setHeartNumber] = useState(3);
const resetBallPosition = () => { //ボール落下時設定を初期化
setBallX(243);
setBallY(155);
setIsFired(false);
setPlayerPositionX(175);
setIsDragging(false);
setIsSpeed(false);
setIsCollisionNone(false);
setCountHeal(0);
};
const checkBallFalling = (ballPosX, ballPosY) => { //ボール落下時の処理
if (ballPosX < -10 || ballPosY > 300) {
setHeartNumber((prev) => prev - 1); //ハートを-1
resetBallPosition();
}
};
ボールが落下したとき、初期状態で3であるheartNumberを1ずつ減らしていきます。さらに色々なパラメータを初期状態に戻し、再プレイができるようになってます。heartNumberが0になったらゲームオーバーです。
const Score_heart = ({ score, heartNumber, CountHeal, setHeartNumber, setCountHeal }) => {
const [heartWidth, setHeartWidth] = useState(0); //1/3、2/3ハートの横幅を指定する
const [heartPosition, setHeartPosition] = useState(0); //1/3、2/3ハートの位置を調整
useEffect(() => {
if(heartNumber < 5){ //ハートは4つまで
if (CountHeal === 1) { //1/3ハート
setHeartWidth(20);
setHeartPosition(10);
} else if (CountHeal === 2) { //2/3ハート
setHeartWidth(30);
setHeartPosition(4);
} else if (CountHeal === 3) {
setHeartNumber(() => heartNumber + 1); //3/3ハート ⇒ハートカウンターを+1
setHeartWidth(0);
setCountHeal(0);
} else if (CountHeal === 0) {
setHeartWidth(0); //CountHealが0の時は表示しない
}
}
else{
setCountHeal(0);
}
}, [CountHeal, setHeartNumber, setCountHeal, heartNumber]);
return (
<div className="flex mb-3">
<div className="score">Score: {score}</div>
<div className="heart_container">
<img //1/3、2/3ハートの画像
style={{
width: `${heartWidth}px`,
height: `${heartWidth}px`,
marginTop: `${heartPosition}px`
}}
alt="heart-outline"
src={heart}
/>
{Array.from({ length: heartNumber }).map((_, index) => ( //ハートの画像を動的に追加
<img key={index} alt="heart-outline" src={heart} className="heart" />
))}
</div>
</div>
);
};
CountHealが+1されると小さいハートが出現します。
heartPositionやheartWidthを用いて1/3回復、2/3回復時のハートの位置をHTMLで微調整しています。また、1/3回復時、2/3回復時に落下するとこれらの端数のハートは消し飛ぶのに加えて普通に1個ハートも減るので、1回落とすまでにハートアイテムを3回取る必要があります。条件分岐でハートは最大4個までしか増えないようになっています。
おまけ 子の値を親に伝える
親コンポーネントから子コンポーネントに状態を渡すにはpropsを使う方法がありました。では子コンポーネントから親コンポーネントへの伝達はどのように行うのでしょうか。
const Ball = ({
checkBlockCollision, //親から
checkBallFalling, //親から
}) => {
const updateBallPosition = useCallback(() => {
checkBlockCollision(nextX, nextY, 5); //衝突判定の関数にボールの次の座標を入力
checkBallFalling(nextX, nextY); //落下判定の関数にボールの次の座標を入力
});
};
この個所では親コンポーネントであるGame_content.jsxからcheckBlockCollisionとcheckBallFallingを受け取っています。これらはボールの位置情報を親コンポーネントとリンクするために渡されました。ここの引数にボールの位置情報を設定することでブロックの衝突判定やボールの落下判定ができるようになっていたのです。つまり、何が言いたいのかというと、親コンポーネントに状態を伝達するためには、状態を変化させる関数をpropsとして渡し、そこで引数に入れる必要があるということです。またuseStateで宣言したset….をpropsとして渡せば直接状態を親の変数に伝えることが出来るようになります。
最後に
ここまで見ていただきありがとうございました。とても分かりにくかったと思います。ふーんくらいにとどめておいてください。正直、ゲーム用のフレームワークではないので無理やり実装している箇所が多いです。Unityなどでゲームを作るのも良いですが、一度このように一からコードを書いてみると、どのような仕組みで機能が実装されているのかを考えることが出来てとても有意義だと思います。皆さんも是非挑戦してみてください。良いお年を!