植物大战僵尸+React chess game设计(1) : 让棋子移动起来

239 阅读3分钟

首先,我们需要让棋子动起来。做法是直接使用@atlaskit/pragmatic-drag-and-droppragmatic-drag-and-drop库。

正在拖拽僵尸: image.png

1.安装:

npm i @atlaskit/pragmatic-drag-and-drop

参考文档: atlassian.design/components/…

官方的示例很详尽

2.给棋子添加draggable

import { useEffect, useRef, useState } from "react";
import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { getHeight } from "../helper/common";
import { ACTION } from "../constants/default";

const Piece = ({ image, alt, type = 'plant', name, location }) => {
    const ref = useRef(null);
    const [dragging, setDragging] = useState(false);

    useEffect(() => {
        const el = ref.current;

        return draggable({
            element: el,
            onDragStart: () => setDragging(true),
            onDrop: () => setDragging(false),
            getInitialData: () => ({ location, name, action: ACTION.MOVE }),
        });
    }, []);

    return (
        <img
            // css={[dragging && hidePieceStyles, imageStyles]}
            src={image}
            alt={alt}
            ref={ref}
            width={'80px'}
            height={getHeight(type)}
        />
    );
}

export default Piece;

draggable是pragmatic-drag-and-drop/element/adapter的方法,

draggable is an HTMLElement that can be dragged around by a user.

draggable can be located:

  • Outside of any drop targets
  • Inside any amount of levels of nested drop targets
  • So, anywhere!

draggable接受以下参数:

(1)element: HTMLElement 我们要拖拽的元素,这里设置为图片 (2)getInitialData: 拖拽时我们想传递的参数,这里传递了location(位置),name(棋子名称),和action(棋子行为) (3)onDragStart/onDrop: 这里可以设置棋子在拖拽时的样式

3.给棋盘上的格子添加dropTargetForElements

import { dropTargetForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import Piece from './piece';
import '../components/grassland.css'
import { useEffect, useRef, useState } from 'react';
import { canMove, isEqualCoord } from '../helper/common';
import { ACTION, PLANTS_DETAIL } from '../constants/default';

const Tile = ({location, piece, pieces, setPieces}) => {

    const ref = useRef(null);
    const [state, setState] = useState('idle');

    useEffect(() => {
        const el = ref.current;

        return dropTargetForElements({
            element: el,
            getData: () => ({ location }),
            onDragEnter: ({source}) => {
                console.log("source:", source);

                if(source.data.action === ACTION.MOVE){
                    if (canMove(source.data.location, location, source.data.name, pieces)) {
                        setState('validMove');
                    } else {
                        setState('invalidMove');
                    }
                }
            },
            onDragLeave: () => setState('idle'),
            onDrop: () => setState('idle'),
        });
    }, [location, pieces]);

    const getColor = (rIndex, cIndex) => {
        
        if (state === 'validMove') {
            return 'skyblue';
        } else if (state === 'invalidMove') {
            return 'pink';
        }

        if((rIndex + cIndex) % 2 === 0){
            return "white";
        }

        return cIndex < 4 ? 'grass' : 'blood';
    }

    return <div ref={ref} className={`box
        ${
            piece && piece.type === 'zombie' ? 'zombie' : ''
        }
        ${
            getColor(location[0], location[1])
        }`}>
        {piece && <Piece image={piece.image} type={piece.type} name={piece.name} location={piece.location}/>}
    </div>
}

export default Tile;

棋子拖拽后需要drop在棋盘的格子上,所以我们要给格子加上dropTargetForElements, (1)element: HTMLElement 同上,这里是方格 (2)getData:当棋子拖到了方格上,需要传递方格的location,来决定drop的位置 (3)onDragEnter: 当棋子拖到了方格上,根据棋子的传参来判断移动的合法性(这里不block移动,只决定格子的颜色) (4)onDragLeave/onDrop:棋子移开后还原格子颜色

合法性判断方法:

// 判断坐标相等
export const isEqualCoord = (pos1, pos2) => {
	return pos1[0] === pos2[0] && pos1[1] === pos2[1];
}

export const getHeight = (type) => {
    return type === 'plant' ? '80px' :
           type === 'zombie' ? '120px' :
           '60px'
}

export const canMove = (source, target, pieceName, pieces) => {
    const rowDist = Math.abs(source[0] - target[0]);
	const colDist = Math.abs(source[1] - target[1]);

    if (pieces.find((piece) => isEqualCoord(piece.location, target))) {
		return false;
	}

    switch (pieceName) {
		case 'peeshooter':
			return false;
		case 'zombie':
			return [0, 1].includes(rowDist + colDist);
		default:
			return false;
	}
}

豌豆射手不能移动,只能原地攻击,僵尸每次可以移动1格

僵尸移动到不合法位置,格子变为红色

image.png

4.给棋盘添加monitorForElements

Monitors allow you to observe drag and drop interactions from anywhere in your codebase. This allows them to recieve draggable and drop target data and perform operations without needing state to be passed from components.

棋盘代码

import { useState, useEffect } from 'react';
import '../components/grassland.css'
import { ACTION, PLANTS, PLANTS_DETAIL, ZOMBIES_DETAIL } from '../constants/default';
import { canMove, isEqualCoord } from '../helper/common';
import Tile from './tile';
import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';

const Grassland = ({pieces, setPieces}) => {

    const n = 8;
    const m = 8;

    const [chessBoard, setChessBoard] = useState([]);

    useEffect(() => {
        const result = [];
        for (let i = 0; i < n; i++) {

            const row = Array.from({ length: m });
            result.push(row);
        }
        setChessBoard(result);
    }, []);

    const getColor = (cIndex) => {
        return cIndex < 4 ? 'grass' : 'blood';
    }

    useEffect(() => {
        return monitorForElements({
            onDrop({ source, location }) {

                if(source.data.action === ACTION.CREATE){
                    const piece = PLANTS_DETAIL[source.data.name] ?? ZOMBIES_DETAIL[source.data.name];
                    const loc = location.current.dropTargets[0]?.data?.location;
                    if(!loc){
                        return;
                    }
                    console.log("piece:", piece);
                    setPieces([{ type: piece.type, location: loc, image: piece.imgSrc, name: piece.name }, ...pieces]);
                    return;
                }

                const destination = location.current.dropTargets[0];
                if (!destination) {
                    return;
                }
                const destinationLocation = destination.data.location;
                const sourceLocation = source.data.location;
                const piece = pieces.find((p) => isEqualCoord(p.location, sourceLocation));
                const restOfPieces = pieces.filter((p) => p !== piece);

                if (
                    canMove(sourceLocation, destinationLocation, piece.name, pieces) &&
                    piece !== undefined
                ) {
                    setPieces([{ type: piece.type, location: destinationLocation, image: piece.image, name: piece.name }, ...restOfPieces]);
                }
            },
        });
    }, [pieces]);
    

    return <>
        <div className="grassland">
        {chessBoard.length > 0 &&
                chessBoard.map((row, rIndex) => {
                    return (
                        <div className="row" key={rIndex}>
                            {row.map((_, cIndex) => {

                                const tileCoord = [rIndex, cIndex];

                                const piece = pieces.find((piece) => isEqualCoord(piece.location, tileCoord))

                                return (
                                    <Tile location={[rIndex, cIndex]} piece={piece} pieces={pieces} setPieces={setPieces}/>
                                );
                            })}
                        </div>
                    );
                })}
        </div>
    </>

}

export default Grassland;

游戏主文件

import { useState } from "react";
import CardList from "../components/cardlist"
import Grassland from "../components/grassland"
import { PLANTS, ZOMBIES } from "../constants/default";

const GamePage = ({totalSun, plantlist, totalBrain, zombielist}) => {

    const [pieces, setPieces] = useState([
        {
            type: 'plant',
            image: PLANTS.peeshooter,
            location: [3, 2],
            name: 'peeshooter'
        },
        {
            type: 'plant',
            image: PLANTS.peeshooter,
            location: [5, 1],
            name: 'peeshooter'
        },
        {
            type: 'zombie',
            image: ZOMBIES.zombie,
            location: [3, 6],
            name: 'zombie'
        },
        {
            type: 'zombie',
            image: ZOMBIES.zombie,
            location: [4, 6],
            name: 'zombie'
        }
    ]);

    console.log("pieces", pieces);

    return <>
      <CardList total={totalSun} cards={plantlist} type='plant'/>
      <Grassland pieces={pieces} setPieces={setPieces}/>
      <CardList total={totalBrain} cards={zombielist} type='zombie'/>
    </>
}

export default GamePage;

source,location两个参数是前两步传的,因此可以依据它们在onDrop方法里更新棋子位置。

PS:码上掘金貌似只能支持单文件, 也没法连接外部github, live demo得另外找个方式搞🤥️