ツクログネット

【React】ドラッグ&ドロップで遊べるスライドパズルを作る その2「ピースをドラッグ&ドロップで動かす」

eye catch

前回の記事では、ページ上にスライドパズルを表示しましたが、今の状態ではただ表示されているだけであり、何も動きはありません。

そこで本章では、表示されているスライドパズルのピースがドラッグ&ドロップで動くようにします。

連載目次

本稿の「ドラッグ&ドロップで遊べるスライドパズルを作る」は連載記事であり、以下の章に分けています。

  1. ページ上にスライドパズルを表示する
  2. ピースをドラッグ&ドロップで動かす(この記事)
  3. ピースの移動範囲を制限する
  4. ピースと空白を入れ替える
  5. ピースをシャッフルする
  6. クリア判定を行う
  7. 制限時間を設ける
  8. 難易度を設ける
  9. 好きな画像を選べるようにする
  10. レスポンシブ対応

本章では、2.の「ピースをドラッグ&ドロップで動かす」について説明します。

ディレクトリ構成

現在のディレクトリ構成は下記の通りです。

react-slide-puzzle
 └── src
     ├── components
     |   ├── Blank.js
     |   ├── Piece.js
     |   └── SlidePuzzle.js
     ├── hooks
     |   └── useSlidePuzzle.js
     ├── App.css
     ├── App.js
     ├── App.test.js
     ├── index.css
     ├── index.js
     ├── logo.svg
     ├── serviceWorker.js
     └── utils.js

ピースにIDを与える

puzzlePiecesステート内からドラッグしたピースに紐づくpieceオブジェクトを識別するために、各ピースの要素にIDを与えます。

components/Piece.js
// 省略

const Piece = ({ className, piece, handleDown }) => {
    return (
        <StyledPiece
            // 省略

            id={`piece-${piece.baseId}`}
        >        
          {/* 省略 */}
        </StyledPiece>
    );
};

export default Piece

StyledPieceコンポーネントのid属性にpiece.baseIdを利用したpiece-${piece.baseId}という名前のIDを指定しています。

ピースに座標を与える

ピースが動くように要素に座標を与えます。

はじめに、puzzlePiecesステート内の各pieceオブジェクトに、新たにピースの座標(移動量)となるx, yプロパティを追加します。

hooks/useSlidePuzzle.js
// 省略

import useDraggableElements from './useDraggableElements'

const useSlidePuzzle = (quantity, bg) => {// quantity=ピースの枚数

  // 省略

  const initializePieces = useCallback(async () => {
		// 省略

		pieceImages.then((images) => {
			const pieces = [];

			for (let i = 0; i < quantity; i++) {
				const piece = {
					// 省略

					x: 0,
					y: 0
				};

				pieces[i] = piece;
			}

			setPuzzlePieces(pieces);
		});
	});

  // 省略

  return [
        slidePuzzleRef,
        puzzlePieces
    ]
}

export default useSlidePuzzle

x, yプロパティの初期値は共に0です。

次に、pieceオブジェクトに追加したx, yプロパティをStyledPieceコンポーネントとBlankコンポーネントのtransform: translate3d()のx, y値に指定します。ピースはCSSで移動させるのです。

components/Piece.js
// 省略

const StyledPiece = styled.div`

	// 省略

	transform: translate3d(
		${({ piece }) => piece.x}px,
		${({ piece }) => piece.y}px,
		0
	);
`;

// 省略

export default Piece
components/Blank.js
// 省略

const Blank = styled.div`
    // 省略

	transform: translate3d(
		${({ piece }) => piece.x}px,
		${({ piece }) => piece.y}px,
		0
	);
`;

export default Blank

これで、ピースに座標を与えることができました。

因みに、ピースが移動するタイミングは以下の通りです。

  • ピースをドラッグしているとき
  • ピースと空白を入れ替えるとき
  • ピースがシャッフル中であるとき

また何故、空白はドラッグしないのにBlankコンポーネントにpiece.x, piece.yを指定する必要があるのかというと、シャッフル中やドロップ時にピースと位置を入れ替える際に移動が生じるためです。

ピースの座標をドラッグ量と連動させる

ピースがドラッグされたときに、ピースの座標がドラッグした距離と同じだけ進むようにpiece.x, piece.y=ドラッグ量となるようにします。

hooks/useSlidePuzzle.js
// 省略

import useDraggableElements from './useDraggableElements'

const useSlidePuzzle = (quantity, bg) => {// quantity=ピースの枚数
  const [
        draggingElement,
        draggingDirection,
        translation,
        setTranslation,
        handleDown,
        isDragging,
        restrictDragRangeX,
        restrictDragRangeY
    ] = useDraggableElements();

  // 省略

  const initializePieces = useCallback(async () => {
		// 省略
	});

  // 省略

	// ドラッグでピースを移動させる
	useEffect(() => {

    // ドラッグしていなければここで終了する
		if (!isDragging) return;

		setPuzzlePieces((current) =>
			current.map((piece, i) => {

				if (`piece-${piece.baseId}` === draggingElement.id) {
					return {
						...piece,
						isDragging: true,
						x: translation.x,
						y: translation.y
					};
				} else {
					return piece;
				}
			})
		);
	}, [translation]);

  return [
        slidePuzzleRef,
        puzzlePieces
    ]
}

export default useSlidePuzzle

先頭で新たにuseDraggableElementsフックを呼び出していますが、このフックは、要素をドラッグ&ドロップ可能にする際に使用するカスタムフックです。仕組みや使い方の詳細はこちらの記事を参照ください。

上記のコードでは、ピースをドラッグしたときにドラッグしたピースに紐づくpieceオブジェクトのx, yプロパティの値がドラッグ量と連動するようにしています。

この処理はuseEffectフック用いて行います。

まず、第二引数の配列内にドラッグした距離translationを入れ、ピースがドラッグされたときに処理が行われるようにします。

useEffect(() => {
    // something
}, [translation])

そして、ループ処理でpuzzlePiecesステート内のpieceオブジェクトを一つずつ参照し、ドラッグした要素のID名draggingElement.idと、要素のID名piece-${baseId}を比較し、一致したpieceオブジェクトがドラッグしたピースに紐づいているということであるため、そのpieceオブジェクトのx, yプロパティの値にtranslation.x, translation.yをそれぞれ指定します。

if (`piece-${piece.baseId}` === draggingElement.id) {
					return {
						...piece,
						isDragging: true,
						x: translation.x,
						y: translation.y
					};
				}

これでピースの座標がドラッグした距離と連動するようになりました。

ドラッグでピースが動くようにする

では、ピースがドラッグで進むようにします。

はじめに、useSlidePuzzleが返す配列内にhandleDown関数を追加します。

hooks/useSlidePuzzle.js
// 省略

const useSlidePuzzle = (quantity, bg) => {

  // 省略

  return [
        // 省略

        handleDown
    ]
}

export default useSlidePuzzle

次に、Piece、SlidePuzzleコンポーネントをそれぞれ以下のように書き換えます。

components/Piece.js
// 省略

const Piece = ({ className, piece, handleDown }) => {
	return (
		<StyledPiece
			// 省略
			onMouseDown={handleDown}
		>

			{/* 省略 */}

		</StyledPiece>
	);
};

export default Piece
components/SlidePuzzle.js
// 省略

const SlidePuzzle = forwardRef(
    ({ className, pieces, boardColor, size, handleDown }, ref) => (
        <StyledSlidePuzzle
            // 省略
        >
            {pieces.map((piece, i) => !piece.blank ? (
                <Piece
                    className="draggable"
                    piece={piece}
                    key={i}
                    handleDown={handleDown}
                />
            ) : <Blank piece={piece} />)}
        </StyledSlidePuzzle>
    )
);

export default SlidePuzzle

Pieceコンポーネントは、propsとしてhandleDown関数を受け取るようにしたあと、受け取ったhandleDown関数をStyledPieceコンポーネントのonMouseDown属性に指定するように変更しています。

SlidePuzzleコンポーネントは、propsとしてhandleDown関数を受け取るようにしたあと、受け取ったhandleDown関数をそのままPieceコンポーネントのhandleDown属性に指定しています。

また、PieceコンポーネントのclassName属性に.draggableクラスを指定していますが、このクラスはuseDraggableElementsフック側で用意されたものであり、ドラッグしたい要素に追加する必要があります。

最後に、Appコンポーネントを下記のように書き換えます。

App.js
// 省略

const App = () => {
    const [
        // 省略

        handleDown
    ] = useSlidePuzzle(9, BG);
    return (
        <>
            <GlobalStyle />
            <StyledApp>
                <SlidePuzzle
                       // 省略

                     handleDown={handleDown}
                />
            </StyledApp>
        </>
    );
};

export default App

SlidePuzzleコンポーネントのhandleDown属性にhandleDown関数を指定しています。

これらの変更により、ページ上に表示されているスライドパズルのピース部分は、下記のようにドラッグ&ドロップで動くようになりました。 slide-puzzle-gif1

ここまでのコード

これまでに書いたコードや実際の動作は下記のCodePenで確認できます。

次回

本章は以上で終わります。次回はピースの移動範囲を制限します。