vue3+threejs新手从零开发卡牌游戏(十一):添加简单的手牌上场逻辑

首先优化之前的战域相关代码,主要是将战域分成两部分,上方是对方战域,下方是己方战域,然后修改了战域中格子的名称,方便后续不同手牌上场的逻辑区分:

game/site/index.vue完整代码如下:

<!-- 场地 -->
<template>
  <P1 ref="p1Ref"/>
  <!-- <P2 ref="p2Ref"/> -->
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { transPos } from "@/utils/common.ts"
import P1 from "./p1.vue"
// import P2 from "./p2.vue"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const raycaster = new THREE.Raycaster();

const p1Ref = ref()
// const p2Ref = ref()

onMounted(() => {
  // init()
})

const init = async () => {
  await addSitePlane()
  // setSite()
  p1Ref.value.init()
  // p2Ref.value.init()
}

// 添加战域plane
const addSitePlane = () => {
  return new Promise((resolve, reject) => {
    // 设置战域大小和位置
    let plane = scene.getObjectByName("地面")
    let point = transPos(0, 0) // 战域起始位置的屏幕坐标
    // 
    raycaster.setFromCamera( point, camera );
    const intersects = raycaster.intersectObject( plane );
    if (intersects.length > 0) {
      let point = intersects[0].point
      // 进行裁剪
      // let x = Math.abs(point.x) * 2 - 2.4
      // let y = Math.abs(point.z) * 2 - 4
      let x = Math.abs(point.x) * 2
      let y = Math.abs(point.z) - 2

      // 添加p1战域
      const p1SitePlane = new THREE.Group()
      p1SitePlane.name = "己方战域Plane"
      // p1SitePlane.position.set(0, 0, 0)
      p1SitePlane.rotateX(-90 * (Math.PI / 180)) // 弧度
      p1SitePlane.userData["width"] = x
      p1SitePlane.userData["height"] = y
      scene.add(p1SitePlane)
      resolve(true)
    }
  })
}



defineExpose({
  init,
})
</script>

<style lang="scss" scoped>
</style>

game/site/p1.vue完整代码如下:

<template>
  <div></div>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { useCommonStore } from "@/stores/common.ts"
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos, renderText } from "@/utils/common.ts"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

const commonStore = useCommonStore()

// 初始化己方战域
const init = () => {
  let sitePlane = scene.getObjectByName("己方战域Plane")
  let planeWidth = sitePlane.userData.width
  let spaceX = (planeWidth - 5) / 4 // 设计一行有5个格子
  // console.log(123, spaceX)
  for (let i=0; i<5; i++) {
    let mesh = createWireframe()
    if (i == 0) {
      mesh.name = "场地"
    } else if (i == 1) {
      mesh.name = "己方怪兽区1"
      mesh.userData.empty = true
    } else if (i == 2) {
      mesh.name = "己方怪兽区2"
      mesh.userData.empty = true
    } else if (i == 3) {
      mesh.name = "己方怪兽区3"
      mesh.userData.empty = true
    } else if (i == 4) {
      mesh.name = "墓地"
    }
    if (i == 0 || i == 4) {
      mesh.position.set(-planeWidth/2 + 0.5, -0.65, 0)
    } else {
      mesh.position.set(-planeWidth/2 + 0.5, -0.7, 0)
    }
    mesh.translateX(spaceX * i + i)
    sitePlane.add(mesh)
  }
  for (let i=0; i<5; i++) {
    if (i > 0 && i < 4) {
      let mesh = createWireframe()
      if (i == 1) {
        mesh.name = "己方战术1"
      } else if (i == 2) {
        mesh.name = "己方战术2"
      } else if (i == 3) {
        mesh.name = "己方战术3"
      }
      mesh.position.set(-planeWidth/2 + 0.5, -2.2, 0)
      mesh.translateX(spaceX * i + i)
      sitePlane.add(mesh)
    }
  }
}

// 创建线框网格
const createWireframe = () => {
  
  const geometry = new THREE.BoxGeometry( 1, 0, 1.4 );
  const edges = new THREE.EdgesGeometry( geometry );
  const line = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0xffffff } ) );
  line.rotateX(-90 * (Math.PI / 180)) // 弧度
  line.scale.set(0.8, 0.8, 0.8) // 缩放

  return line
}


defineExpose({
  init,
})
</script>

<style lang="scss" scoped>
</style>

页面效果如下:

然后思考下手牌如何进入战域,这里我参考了游戏王决斗链接的方式,手牌拖曳到己方战域时,弹出操作框,是否执行上场操作,点击是卡牌按序出现在战域的对应位置:

1.首先把之前game/hand/p1.vue中的DragControls移到game/index.vue中

2.在components文件夹下新建Dialog.vue,作为弹出操作层

3.执行上场操作时,clone要上场的手牌,然后先把卡牌从手牌区移除,然后将clone的卡牌加入scene场景中,同时将卡牌移动到对应的战域位置

由于代码较多,这里直接放上完整代码:

components/Dialog.vue:

<template>
  <el-dialog
    v-model="dialogVisible"
    title="Tips"
    width="75%"
    :before-close="handleClose"
  >
    <span>{{ state.message }}</span>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="handleClose">取消</el-button>
        <el-button type="primary" @click="onConfirm">
          确定
        </el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'

const emits = defineEmits(["handToSite"])

const dialogVisible = ref(false)

const state = reactive({
  type: "",
  message: "",
  obj: ""
})

const init = (data: any) => {
  state.type = data.type
  state.message = data.message
  state.obj = data.obj
  nextTick(() => {
    dialogVisible.value = true
  })
}

const onConfirm = () => {
  dialogVisible.value = false
  if (state.type === "handToSite") {
    emits("handToSite", state.obj)
  }
}

const handleClose = () => {
  dialogVisible.value = false
}
defineExpose({
  init
})
</script>

<style lang="scss" scoped>
</style>

game/index.vue:

<template>
  <div ref="sceneRef" class="scene"></div>
  <!-- 手牌 -->
  <Hand ref="handRef"/>
  <!-- 卡组 -->
  <Deck ref="deckRef"/>
  <!-- 战域 -->
  <Site ref="siteRef"/>
  <!-- 抽卡逻辑 -->
  <DrawCard ref="drawCardRef" :handRef="handRef"/>
  <!-- 对话框 -->
  <Dialog ref="dialogRef" @handToSite="handToSite"/>
</template>

<script setup lang="ts">
import { reactive, ref, onMounted, onBeforeUnmount, watch, defineComponent, getCurrentInstance, nextTick } from 'vue'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 轨道控制器
import { DragControls } from 'three/addons/controls/DragControls.js';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { useCommonStore } from "@/stores/common.ts"
import { transPos, editDeckCard, renderText } from "@/utils/common.ts"
import { Card } from "./Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import Hand from "./hand/index.vue"
import Deck from "./deck/index.vue"
import Site from "./site/index.vue"
import DrawCard from "@/components/DrawCard.vue"
import Dialog from "@/components/Dialog.vue"

// 引入threejs变量
const {proxy} = getCurrentInstance()
const THREE = proxy['THREE']
const scene = proxy['scene']
const camera = proxy['camera']
const renderer = proxy['renderer']
const TWEEN = proxy['TWEEN']

const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();

const commonStore = useCommonStore()


// 场景ref
const sceneRef = ref()
const siteRef = ref()
const handRef = ref()
const deckRef = ref()
const drawCardRef = ref()
const dialogRef = ref()

// 坐标轴辅助
const axesHelper = new THREE.AxesHelper(5);
// 创建轨道控制器
// const orbitControls = new OrbitControls( camera, renderer.domElement );
// 字体加载器
const fontLoader = new FontLoader();

onMounted(async () => {
  await initResource()
  initScene()
  initGame()

  // 鼠标按下
  window.addEventListener('touchstart', onMousedown)
  window.addEventListener('touchmove', onMousemove)
  // window.addEventListener('click', onMousedown)

  // 监听浏览器窗口变化进行场景自适应
  window.addEventListener('resize', onWindowResize, false);
})

// 资源加载
const initResource = () => {
  // 字体加载
  return new Promise((resolve, reject) => {
    fontLoader.load('fonts/helvetiker_regular.typeface.json', (font: any) => {
      commonStore.loadFont(font)
      resolve(true)
    });
  })
}

// 初始化场景
const initScene = () => {
  renderer.setSize( window.innerWidth, window.innerHeight );
  sceneRef.value.appendChild( renderer.domElement );
  scene.add(axesHelper)

  // camera.position.set( 5, 5, 5 );
  camera.position.set( 0, 6.5, 0 );
  camera.lookAt(0, 0, 0)

  addPlane()

  animate();
}

// scene中添加plane几何体
const addPlane = () => {
  const geometry = new THREE.PlaneGeometry( 20, 20);
  const material = new THREE.MeshBasicMaterial( {
    color: new THREE.Color("gray"), 
    side: THREE.FrontSide, 
    alphaHash: true,
    // alphaTest: 0,
    opacity: 0
  } );
  const plane = new THREE.Mesh( geometry, material );
  plane.rotateX(-90 * (Math.PI / 180)) // 弧度
  plane.name = "地面"
  scene.add( plane );
}

// 用requestAnimationFrame进行渲染循环
const animate = () => {
  requestAnimationFrame( animate );
  TWEEN.update()
  renderer.render( scene, camera );
}

// 场景跟随浏览器窗口大小自适应
const onWindowResize = () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

// 初始化游戏
const initGame = async () => {
  // 初始化场地
  initSite()
  // 初始化卡组
  await initDeck()
  // 初始化手牌
  await initHand()
  // 绑定手牌事件
  onHandEvent()
}

// 初始化场地
const initSite = () => {
  siteRef.value.init()
}

// 初始化卡组
const initDeck = () => {
  return new Promise((resolve, reject) => {
    let p1Deck = [
      "YZ-01",
      "YZ-02",
      "YZ-03",
      "YZ-04",
      "YZ-01",
      "YZ-02",
      // "YZ-03",
      // "YZ-04",
      // "YZ-01",
      // "YZ-02",
      // "YZ-03",
      // "YZ-04",
    ]
    // 洗牌
    p1Deck.sort(() => {
      return Math.random() - 0.5
    })
    let newDeck: any = []
    p1Deck.forEach((v: any, i: any) => {
      let obj = CARD_DICT.find((b: any) => b.card_id === v)
      if (obj) {
        newDeck.push({
          card_id: v,
          name: `${obj.name}_${i}`
        })
      }
    })
    // console.log("p1Deck", newDeck)
    commonStore.updateP1Deck(newDeck)
    
    nextTick(() => {
      handRef.value.init()
      deckRef.value.init()
      resolve(true)
    })
  })
}

// 初始化手牌
const initHand = () => {
  return new Promise((resolve, reject) => {
    let cardNumber = 4
    let _number = 0
    let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
    let deckGroup = scene.getObjectByName("p1_deckGroup")
    let position = new THREE.Vector3(0, 0.005 * p1Deck.length, 0)
    let _interval = setInterval(async() => {
      // console.log(123, p1Deck)
      if (_number < cardNumber) {
        let obj = p1Deck[p1Deck.length - 1]
        p1Deck.splice(p1Deck.length-1, 1)
        commonStore.updateP1Deck(p1Deck)
        // 修改卡组
        await editDeckCard(deckGroup, obj, "remove")
        await renderText(deckGroup, `${commonStore.$state.p1Deck.length}`, commonStore.$state._font, position)
        // 手牌区添加手牌
        handRef.value.addHandCard(obj, deckGroup)
      } else {
        clearInterval(_interval)
        resolve(true)
      }
      _number++
    }, 200)
  })

}

// 鼠标按下事件
const onMousedown = (ev: any) => {
  // console.log(222, ev.target)
  // 判断是否点击到canvas上
  if(!(ev.target instanceof HTMLCanvasElement)){
    return;
  }
  // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
  let clientX = ev.clientX || ev.changedTouches[0].pageX
  let clientY = ev.clientY || ev.changedTouches[0].pageY

  let point = transPos(clientX, clientY)

  // 通过摄像机和鼠标位置更新射线
  raycaster.setFromCamera( point, camera );

  // 点击卡组事件
  // onP1DeckEvent()
}

// 点击卡组事件
const onP1DeckEvent = () => {
  if (commonStore.$state.p1Deck.length <= 0) {
    return
  }
  let p1_deckGroup = scene.getObjectByName("p1_deckGroup")
  let arr = raycaster.intersectObject(p1_deckGroup, true)
  if (arr.length <= 0) {
    return
  }
  let pos1 = p1_deckGroup.userData.position
  let pos2 = new THREE.Vector3(0, 2, 0)
  // console.log(444, pos1, pos2)
  if (p1_deckGroup.position.x !== pos2.x) {
    drawCardRef.value.drawCardAnimate1(p1_deckGroup, pos1, pos2)
  }
}

// 鼠标移动事件
const onMousemove = (ev: any) => {
  // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
  pointer.x = ev.clientX || ev.changedTouches[0].pageX
  pointer.y = ev.clientY || ev.changedTouches[0].pageY
}

// 手牌事件
const onHandEvent = () => {
  let handGroup = scene.getObjectByName("p1_handGroup")
  const dragControls = new DragControls( handGroup.children, camera, renderer.domElement );

  dragControls.addEventListener( 'dragstart', function ( event: any ) {
    event.object.position.y += 0.04
  });

  dragControls.addEventListener( 'drag', function ( event: any ) {
    event.object.position.y += 0.04
    
  });

  dragControls.addEventListener( 'dragend', function ( event: any ) {
    event.object.position.y -= 0.04
    let p1SitePlane = scene.getObjectByName("己方战域Plane")
    let point = transPos(pointer.x, pointer.y)
    // 通过摄像机和鼠标位置更新射线
    raycaster.setFromCamera( point, camera );
    const intersects = raycaster.intersectObjects(p1SitePlane.children);
    if (intersects.length > 0) {
      // console.log(555, intersects[0])
      // intersects.forEach((v: any) => {
      //   console.log(123, v.object.name)
      // })
      dialogRef.value.init({
        type: "handToSite",
        obj: event.object,
        message: "是否上场该卡牌"
      })
    }
    
    // let raycaster = controls.getRaycaster()
    // let point = new THREE.Vector3(0, 0, 0)
    // event.object.getWorldPosition(point)
    // // point = event.object.position
    // raycaster.setFromCamera( point, camera );
    // // console.log(333, event.object.position,point)
    // let p1SitePlane = scene.getObjectByName("己方战域Plane")
    // const intersects = raycaster.intersectObjects(p1SitePlane.children)
    // console.log(444, intersects, p1SitePlane)
    // // console.log(555, p1SitePlane.children[0].position)
    // intersects.forEach((v: any) => {
    //   console.log(123, v.object.name)
    // })
  });
}

// 手牌移入己方战域
const handToSite = (data: any) => {
  let sitePlane = scene.getObjectByName("己方战域Plane")
  let p1_handGroup = scene.getObjectByName("p1_handGroup")
  console.log(p1_handGroup, data)
  let userData = data.userData
  if (userData.type === "怪兽") {
    let meshes = sitePlane.children.filter((v: any) => v.name.indexOf("己方怪兽区") > -1 && v.userData.empty === true)
    if (meshes.length > 0) {
      let position = new THREE.Vector3(0, 0, 0)
      meshes[0].getWorldPosition(position)
      meshes[0].userData.empty === false
      let mesh = p1_handGroup.children.find((v: any) => v.name === data.name)
      let newMesh = mesh.clone()
      newMesh.userData.areaType = meshes[0].name // 用来记录卡牌在哪个区域,怪兽区、墓地、手牌、卡组、场地等
      newMesh.scale.set(0.8, 0.8, 0.8)
      p1_handGroup.remove(mesh)
      scene.add(newMesh)
      newMesh.position.set(position.x, position.y, position.z)
    }
  }
  // console.log(data)
  // // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
  // let clientX = ev.clientX || ev.changedTouches[0].pageX
  // let clientY = ev.clientY || ev.changedTouches[0].pageY
  // let point = transPos(clientX, clientY) // 卡组起始位置的屏幕坐标
  // let p1SitePlane = scene.getObjectByName("己方战域Plane")

  // // 通过摄像机和鼠标位置更新射线
  // raycaster.setFromCamera( point, camera );
  // const intersects = raycaster.intersectObjects(p1SitePlane.children);
  // if (intersects.length > 0) {
  //   console.log(555, intersects[0])
  // }
  // intersects.forEach((v: any) => {
  //   console.log(v.object.name)
  // })
}

</script>

<style lang="scss" scoped>
.scene {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
}
</style>

页面效果如下:

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清岚_lxn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值