《狂热运输》开发记录 | 2.元素拖拽

100 阅读4分钟

  基于PC版《狂热运输》游戏开发的网页版游戏,初期实现城市搭建项目地址

拖拽功能

  1. 从列表拖拽到画布
  2. 在画布中拖拽

功能实现

拖拽功能实现

  这里使用vue3-dnd文档地址实现元素拖拽效果。比较常用的拖拽是vue-draggable-next,但是它比较专注于列表项的拖拽排序,需要实现的功能是在指定区域内的自由的拖拽。所以考虑使用vue3-dnd
  先需要知道vue3-dnd的两个概念,拖拽源Drag Sources和拖放目标Drop Targets。在项目中被拖拽的元素是拖拽源,再画布再进行拖拽移动的画布是拖拽目标。

image.png   那么需要分别设置拖拽源和拖拽目标。

// drag.vue
<template>
   // 只保留关键代码,其他代码省略
   <DndProvider :backend="HTML5Backend">
       // box组件就是拖拽源
      <Box :src="urlBae+'/static/svg/city/skyscraper-01.svg"  id="city" type="select"/>
  </DndProvider>
</template>
<script setup>
    import {HTML5Backend} from "react-dnd-html5-backend";
    import {DndProvider} from "vue3-dnd";
    import Box from "./BoxSvg.vue";
    const isPro = process.env.NODE_ENV === 'production';
    const urlBae = isPro ? '/rush-traffic' : ''
</script>

  useDrag函数将组件作为拖拽源连接到DnD,给组件添加拖拽源的功能。当一个元素绑定了useDrag返回的结果后,该元素就具备了被用户鼠标拖动的能力,并且可以与其他支持拖拽目标进行交互。

// BoxSvg.vue
<script setup>
import {useDrag} from 'vue3-dnd'
import {toRefs} from "@vueuse/core";

const props = defineProps({
  src: String,
  id: String,
  type: String,
});

/**
 * 返回值:
 *    collect:包含了拖拽过程中需要使用的各种状态属性的对象。
 *    drag:这是一个ref引用,将其绑定到需要成为拖拽源的DOM元素上,这样该元素就可以响应用户的拖拽操作。
 * 配置项:
 *    type:指定拖拽项的类型,以便与目标放置区域匹配。
 *    item:定义拖拽数据,通常包含要传递给放置区域的数据对象
 *    collect:通过collect函数收集拖拽过程中的状态信息
 */
const [collect, drag] = useDrag(() => ({
  type: 'box',
  item: props,
  collect: monitor => ({
    isDragging: monitor.isDragging(),
    handlerId: monitor.getHandlerId(),
  }),
}));
</script>

<template>
  <img :src="src" role="Box" :ref="drag" class="box">
</template>

<style scoped>
.box {
  width: 40px;
  cursor: move;
}
</style>

  设置拖拽目标

// drop.vue
<template>
  <div class="canvas-class">
    // 将拖拽目标层覆盖到cavas绘制的矩阵点层之上
    <div class="svg-class">
      <DndProvider :backend="HTML5Backend">
        <Example/>
      </DndProvider>
    </div>
    <canvas id="canvasScene" style="width: 100%"></canvas>
  </div>
</template>
<script setup>
import {HTML5Backend} from "react-dnd-html5-backend";
import {DndProvider} from "vue3-dnd";
import Example from "@/view/scene/components/Example.vue";
</script>

  这里创建了两层以实现对拖拽元素位置的实时展示。

// Example.vue
<script setup>
import Container from './Container.vue'
import CustomDragLayer from './CustomDragLayer.vue'
</script>

<template>
  <div style="width: 100%;height: 100%" id="drop">
    <Container/>
    <CustomDragLayer/>
  </div>
</template>

  useDrop函数提供了一种将你的组件作为拖拽目标接到DnD的方法,用于创建可放置区域的功能。当组件使用了useDrop时,该组件将能够响应从其他拖拽源发起的拖放操作。
  Container层的主要功能是主要的拖拽源,显示拖拽结束后的真实位置,但是无法实时显示拖拽过程中的位置,所以还需要CustomDragLayer层。

// Container.vue
<script setup>
import {inject, reactive, ref} from 'vue'
import {useDrop} from 'vue3-dnd'
import DraggableBox from './DraggableBox.vue'
import {snapToGrid as doSnapToGrid} from './snapToGrid'

const boxes = reactive({})
const clickId = ref('')
const moveBox = ({id, src, type, size}, left, top) => {
  // 如果是从列表拖拽来的会带有‘type='select'’属性,以此执行新增,其他情况执行修改位置操作
  if (boxes[id] && type != 'select') {
    Object.assign(boxes[id], {left, top})
  } else {
    let length = Object.keys(boxes).length + 1;
    id = id + length;
    boxes[id] = {left: left, top, src, size}
  }
}
// 这接收鼠标滚轮滚动影响画布大小时设置的缩放比例
const scale = inject('scale');

/**
 * 返回值:
 *    drag:这是一个ref引用,将其绑定到需要成为拖拽目标的DOM元素上,这样该元素就可以响应用户的拖拽操作。
 * 配置项:
 *    accept:此放置目标只会对‘指定类型的拖动源’ 做出反应
 *    drop:当拖拽元素放置在目标上时调用
 */
const [, drop] = useDrop(() => ({
  accept: 'box',
  drop(item, monitor) {
    let drop = document.getElementById('drop');
    let offLeft = drop.getBoundingClientRect().left / scale.value;
    let offTop = drop.getBoundingClientRect().top / scale.value;
    let delta = monitor.getDifferenceFromInitialOffset();
    let dx = delta.x / scale.value, dy = delta.y / scale.value
    let left = Math.round((item?.left || 0) + dx);
    let top = Math.round((item?.top || 0) + dy);
    if (item.type == 'select') {
      let currentOffset = monitor.getSourceClientOffset()
      left = Math.round(currentOffset.x / scale.value - offLeft);
      top = Math.round(currentOffset.y / scale.value - offTop);
    }
    [left, top] = doSnapToGrid(left, top);
    // 计算拖拽源放在拖拽目标上的位置进行新增或者修改
    moveBox(item, left, top)
    return undefined
  }
}));
</script>

<template>
  <div :ref="drop" class="container">
    <DraggableBox
        v-for="(value, key) in boxes"
        :id="key"
        :key="key"
        :clickId="clickId"
        v-bind="value"/>
  </div>
</template>

useDragLayer用来实现拖动过程中提供一个元素的实时预览、阴影或者自定义样式等。与直接操作拖拽源源组件不同,useDragLayer是基于全局状态和事件监听来获取并更新拖拽相关的信息

<script setup>
import {inject} from 'vue'
import {useDragLayer} from 'vue3-dnd'
import {snapToGrid} from './snapToGrid'
import {toRefs} from '@vueuse/core'
import Box from './Box.vue'

const scale = inject('scale');

function getItemStyles(initialOffset, currentOffset, item) {
  if (item?.type == 'select' || (!initialOffset || !currentOffset)) {
    return { display: 'none'};
  };
  let drop = document.getElementById('drop');
  let left = drop.getBoundingClientRect().left / scale.value;
  let top = drop.getBoundingClientRect().top / scale.value;
  let {x, y} = currentOffset;
  x = x / scale.value;
  y = y / scale.value;
  let ix = initialOffset.x / scale.value, iy = initialOffset.y / scale.value;
  x -= ix;
  y -= iy;
  [x, y] = snapToGrid(x, y);
  x += ix;
  y += iy;
  const transform = `translate(${Math.round(x - left)}px, ${Math.round(y - top)}px)`return { transform };
}

const collect = useDragLayer(monitor => ({
  // getItem用来获取当前拖动的元素信息
  item: monitor.getItem(),
  itemType: monitor.getItemType(),
  initialOffset: monitor.getInitialSourceClientOffset(),
  currentOffset: monitor.getSourceClientOffset(),
  isDragging: monitor.isDragging(),
}))
const {itemType, isDragging, item, initialOffset, currentOffset} = toRefs(collect);
</script>

<template>
  <div class="layer">
    <div :style="getItemStyles(initialOffset, currentOffset,item)">
      <Box :src=" item?.src" :size="item?.size"/>
    </div>
  </div>
</template>

<style scoped>
.layer {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  pointer-events: none;
}
</style>