React Drag & Drop: «Игра в бутылки»

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет!

Меня зовут Сергей, я фронтенд-разработчик отдела спецпроектов KTS. Наш отдел занимается разработкой веб-приложений для промокампаний. 

Помните, как в 1-й книге о Гарри Поттере Гермиона разгадывала логическую загадку с бутылочками волшебных зелий? Сегодня расскажу, как мы создавали именно такую игру.

Мы воспользуемся react-dnd, styled-components, mobx и createPortal.

  1. Правила

  2. Разбираемся с пакетами

  3. Создаем конфиг

  4. Хранилище с логикой игры

  5. Создаем игровое поле

  6. Создаем бутылки и ячейки

  7. Настраиваем Drag'n'Drop

  8. Совместимость с тач-устройствами

  9. Расставляем бутылки и играем

Правила

У нас есть 5 бутылок и 2 полки. При старте игры бутылки произвольно устанавливаются на одной полке. Их необходимо установить в правильном порядке на второй полке.

Разбираемся с пакетами

Реализуем Drag'n'Drop с помощью пакета react-dnd. Он создает контекст с провайдером, который отслеживает события drag- и drop-компонентов. Внутри компонентов доступны хуки useDrag и useDrop.

React-dnd не включает в себя браузерные и тач-события, но позволяет использовать дополнительные DnD-бэкенды.

Для работы с HTML Drag and Drop API подключим react-dnd-html5-backend. Браузерный API позволит нам использовать нативный механизм перетягивания, без дополнительных узлов и рендера в процессе перетаскивания. В билде у нас получатся HTML-элементы с атрибутом draggable.

Для совместимости с тач-устройствам выберем Touch events и подключим react-dnd-touch-backend. В этом случае придется создавать узел с перетаскиваемым объектом с помощью createPortal и стилизовать с помощью хука usePreview.

Теперь пакет react-dnd-multi-backend сам выберет, какой DnD-бэкенд лучше использовать на устройстве.

Создаем конфиг

Перечислим наши бутылки, изображения бутылок, правильный порядок и полки:

enum BottlesEnum {
	blue,
	brown,
	green,
	red,
	white,
}

const bottles: Record<BottlesEnum, BottleType> = {
	[BottlesEnum.blue]: {
		id: BottlesEnum.blue,
		image: require('./img/blue.png'),
	},
	/*...*/
}

const correctPositions = [
	BottlesEnum.green,
	BottlesEnum.white,
	BottlesEnum.brown,
	BottlesEnum.red,
	BottlesEnum.blue,
];

enum ShelvesEnum {
	top,
	bottom,
}

Текстом напишем подсказки для правильного порядка на основе изображений:

const rules = [
	'По краям стоят круглые бутылки',
	'Голубая бутылка стоит рядом с красной',
	'В центре стоит бутылка без пробки',
	'Красная бутылка стоит правее зеленой',
	'Зеленая бутылка не стоит рядом с бутылками без пробки',
];

Хранилище с логикой игры

Для хранения состояний и логики будем использовать MobX. Он позволяет создавать локальное хранилище, или стор, и использовать его в нужном компоненте.

Само хранилище — обычный объект. Вся магия в том, что мы помечаем объекты, которые влияют на отображение компонента как наблюдаемые (observable), и делаем наблюдаетелем сам компонент (observer).

Таким образом на перерисовку компонента будет влиять только изменение observable-полей. Подробнее можно прочитать здесь.

В сторе будем хранить содержимое ячеек, начальную позицию перетаскиваемой ячейки, логику стартового перемешивания, обработчики onDrag/onDrop и проверку решения.

import { makeAutoObservable } from 'mobx';
import { createContext } from 'react';

type ShelfItemType = BottlesEnum | null;
type ShelfItemsListType = ShelfItemType[];

class BottlesGameStore {
	draggedPosition: PositionType | null = null; // начальная позиция drag-элемента в момент перетаскивания
	positions: Record<ShelvesEnum, ShelfItemsListType>; // содержимое ячеек на полках

	constructor() {
		makeAutoObservable(this);
		this.shuffle();
	}

	shuffle(): void { // перемешиваем бутылки
		this.positions = {
			[ShelvesEnum.top]: new Array(correctPositions.length).fill(null),
			[ShelvesEnum.bottom]: [...correctPositions].sort(
				() => Math.random() - 0.5
			),
		};

		this.isOneAtBottomCorrect && this.shuffle(); // перемешиваем еще раз, если хотя бы одна стоит на нужной позиции
	}

	get isOneAtBottomCorrect(): boolean {
		return correctPositions.some(
			(bottleId, columnIndex) =>
				bottleId === this.positions[ShelvesEnum.bottom][columnIndex]
		);
	}

	onDrag(position: PositionType): void {
		this.draggedPosition = position;
	}

	onDrop(bottleId: number, position: PositionType): void {
		const itemAtDrop = this.getItem(position); // проверяем бутылку в drop-ячейке
		if (itemAtDrop || !this.draggedPosition || this.isCorrect) {
			return;
		}

		this.setItem(this.draggedPosition, null); // удаляем бутылку из drag-ячейки, в которой она находилась в момент начала перетаскивания
		this.setItem(position, bottleId); // сохраняем бутылку в drop-ячейку
	}

	getItem(position: PositionType): ShelfItemType {
		const [shelfIndex, columnIndex] = position;
		return this.positions[shelfIndex][columnIndex];
	}

	setItem(position: PositionType, item: ShelfItemType): void {
		const [shelfIndex, columnIndex] = position;
		this.positions[shelfIndex][columnIndex] = item;
	}

  get isCorrect(): boolean { // проверяем правильные позиции
	  return (
		  JSON.stringify(correctPositions) ===
		  JSON.stringify(this.positions[ShelvesEnum.top]) // элегантный способ 						
Источник: https://habr.com/ru/company/kts/blog/647241/


Интересные статьи

Интересные статьи

Автор статьи, перевод которой мы сегодня публикуем, говорит, что в компании commercetools приняли на вооружение хуки React в начале 2019 года — в момент их появления в React 16.8.0. С тех пор про...
По мере того, как растёт сложность клиентских приложений, размеры их бандлов становятся всё больше и больше. В этой ситуации сильнее всего страдают люди, вынужденные, по разным причинам, пользова...
Чаще всего при выборе этого языка ожидается, что разработка одного приложения под две платформы займёт в два раза меньше времени, чем разработка двух приложений. Но по итогу оказывается, что разр...
Автор заметки, перевод которой мы сегодня публикуем, говорит, что существует пропасть между использованием React для разработки пользовательских интерфейсов и необходимостью знать о том, как рабо...
Друзья, вот и снова настала весна! Прекрасная летняя погода всё ближе. Продуктивные будни принесли новые результаты, а это значит, что пора снова встретиться! В этом году мы подготовили что-то...