vue3+threejs新手从零开发卡牌游戏(八):关联卡组和手牌区、添加初始化卡组和初始化手牌逻辑

首先我们优化下之前的代码,先加载游戏资源,然后再初始化场景,由于目前只有一个font字体需要加载,所以我们将之前game/deck/p1.vue中的font相关代码迁移到game/index.vue下,同时使用async和await处理异步加载,即当资源加载完后再执行场景的初始化,这里font我存入了store供全局使用,后面有对应的store代码:

import { FontLoader } from 'three/addons/loaders/FontLoader.js';


// 字体加载器
const fontLoader = new FontLoader();

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

  // 监听浏览器窗口变化进行场景自适应
  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)
    });
  })
}

...

然后我们思考下卡组的抽卡逻辑:
1.移除一张卡组顶的卡牌

2.将卡牌从卡组位置移动到手牌区位置(动效)

3.将手牌加入手牌区

首先我们可以自定义一个测试卡组,然后存放到store中:

stores/common.ts代码:

import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCommonStore = defineStore('common', () => {

  const _font = ref() // 字体

  const p1Deck = ref([] as any) // 卡组
  const p2Deck = ref([] as any) // 卡组

  
  // 加载字体
  function loadFont(data: any) {
    _font.value = data
  }

  // 更新己方卡组
  function updateP1Deck(data: any) {
    p1Deck.value = data
  }

  // 更新对方卡组
  function updateP2Deck(data: any) {
    p2Deck.value = data
  }


  return {
    _font,
    p1Deck,
    p2Deck,

    loadFont,
    updateP1Deck,
    updateP2Deck,

  }
}, {
  persist: true
})

然后在game/index.vue初始化卡组:

// 初始化卡组
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)
    })
  })
}

其中我们在newDeck数组中保存了这局游戏中所用卡组中所有卡牌的card_id和name,其中name通过名称+索引的方式让每张卡牌有了唯一的name值,这个步骤是为了处理卡组中的同名卡牌,这样我们可以保证每张卡牌都有唯一标识,并且按照优先级我们应该先初始化卡组,然后才能初始化手牌,所以这里也用了Promise,然后初始化卡组存入store中的p1Deck,修改deck/p1.vue代码,将store中的p1Deck传入进来:

import { useCommonStore } from "@/stores/common.ts"

const commonStore = useCommonStore()

const init = () => {
  setDeckPos()
  commonStore.$state.p1Deck.forEach((v: any, i: any) => {
    let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
    if (obj) {
      let card = new Card(obj)
      let mesh = card.init()
      mesh.position.set(0, 0.005 * i, 0)
      mesh.rotateX(180 * (Math.PI / 180)) // 弧度
      mesh.name = v.name
      deckGroup.add( mesh );
    }
  })

  renderText()

}

// 渲染文字
const renderText = () => {
  const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
    font: commonStore.$state._font,
    size: 0.4,
    height: 0,
    curveSegments: 4,
    bevelEnabled: true,
    bevelThickness: 0,
    bevelSize: 0,
    bevelSegments: 0
  });
  geometry.center()
  const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
  const mesh = new THREE.Mesh( geometry, material ) ;
  mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
  mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  mesh.name = "卡组数量"

  // 阴影
  let shadowGeometry = geometry.clone()
  shadowGeometry.translate(0.02, 0.02, 0);
  let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
  let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
  shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
  shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  shadowMesh.name = "卡组数量阴影"

  deckGroup.add(mesh)
  deckGroup.add(shadowMesh)
}

// 设置卡组位置
const setDeckPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
    // 
    raycaster.setFromCamera( point, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // deckGroup.position.set(point.x, point.y, point.z)
      deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
    }
  })
}

此时页面效果如下:

可以看到右侧卡组的数量和你传入的测试卡组数量保持一致了。

然后我们可以思考下,如何从卡组顶移除一张卡牌

1.找到卡组顶的卡牌

2.将它从卡组group中移除

3.更新卡组数量和厚度

我们继续修改game/deck/p1.vue,这个方法是传入一个obj(这个对象就是p1Deck中要移除的那个卡牌对象,里面只有card_id和name两个字段),然后根据obj中的name找到卡组中对应的卡牌mesh,然后根据type判断是要往卡组里添加还是移除这个mesh,如果是移除的话,那么我们先找到卡组Group,然后删除卡组Group中的这个mesh,并进行更新;同时我们删除了文字mesh和对应的文字阴影mesh,将新的卡组数量文字绘制上去,然后将这个方法暴露出去:

// 修改卡组
const editDeckCard = (obj: any, type: any) => {
  let group = scene.getObjectByName("p1_deckGroup")
  let text = group.children.find((v: any) => v.name === "卡组数量")
  let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
  // console.log(22, group.children, commonStore.$state.p1Deck)
  if (type === "remove") { // 删除卡组中的卡牌
    let child = group.children.find((v: any) => v.name === obj.name)
    if (child) {
      group.remove(child)
    }
  }
  group.remove(text)
  group.remove(shadowText)
  group.children.forEach((v: any, i: any) => {
    v.position.set(0, 0.005 * i, 0)
  })
  renderText()
}

defineExpose({
  init,
  editDeckCard
})

因为这里还有一层上级目录,所以需要修改game/deck/index.vue,将p1.vue中的方法暴露出去供game/index.vue使用:

// 修改卡组
const editDeckCard = (obj: any, type: any) => {
  p1Ref.value.editDeckCard(obj, type)
}

defineExpose({
  init,
  editDeckCard
})

在game/index.vue中,我们添加一个初始化手牌的方法:

// 初始化手牌
const initHand = () => {
  let cardNumber = 4
  let _number = 0
  let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
  let deckGroup = scene.getObjectByName("p1_deckGroup")
  let _interval = setInterval(function() {
    // console.log(123, p1Deck)
    if (_number < cardNumber) {
      let obj = p1Deck[p1Deck.length - 1]
      p1Deck.splice(p1Deck.length-1, 1)
      commonStore.updateP1Deck(p1Deck)
      // 修改卡组
      deckRef.value.editDeckCard(obj, "remove")
      // 手牌区添加手牌
      handRef.value.addHandCard(obj, deckGroup)
    } else {
      clearInterval(_interval)
    }
    _number++
  }, 200)

}

这里的逻辑是,比如游戏开始时,要从卡顶抽4张牌,这里我设置每200ms抽一张牌,每抽一张牌就把p1Deck里对应的卡牌删掉,然后调用editDeckCard方法进行移除卡牌操作,然后将抽出来的卡牌加入手牌区,我们先把手牌加入手牌区的方法注释掉,此时刷新页面效果如下:

然后我们在game/hand/p1.vue中编写手牌区添加手牌的逻辑,注意我这里先将卡牌mesh加入场景中(方便在世界坐标系中做卡牌移动动效),然后等卡牌移动到手牌区后再真正的将卡牌加入手牌区Group中:

// 添加手牌
const addHandCard = (obj: any, origin: any) => {
  let position = origin.position
  let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
  if (cardObj) {
    let card = new Card(cardObj)
    let mesh = card.init()
    mesh.position.set(position.x, position.y, position.z)
    mesh.material.forEach((v: any) => {
      v.transparent = true
    })
    scene.add( mesh );
    updateCardPos(mesh)
  }

}

// 更新卡牌位置
const updateCardPos = (mesh: any) => {
  const tw = new TWEEN.Tween({
    x: mesh.position.x,
    y: mesh.position.y,
    z: mesh.position.z,
    opacity: 0.9,
    mesh
  })
  tw.to({
    x: handGroup.position.x,
    y: handGroup.position.y,
    z: handGroup.position.z,
    opacity: 0
  }, 200)
  tw.easing(TWEEN.Easing.Quadratic.Out)
  tw.onUpdate((obj: any) => {
    obj.mesh.position.set(obj.x, obj.y, obj.z)
    obj.mesh.material.forEach((v: any) => {
      v.opacity = obj.opacity
    })
  })
  tw.onComplete(function() {
    //动画结束:关闭允许透明,恢复到模型原来状态
    TWEEN.remove(tw)
    scene.remove( mesh );
    
    mesh.material.forEach((v: any) => {
      v.transparent = false
      v.opacity = 1
    })
    handGroup.add(mesh)
    // 计算叠放间距
    let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
    handGroup.children.forEach((v: any, i: any) => {
      v.position.set(i * space, 0.005 * i, 0)
    })
  })

  tw.start();
}

defineExpose({
  init,
  addHandCard
})

addHandCard方法的参数obj指的是从p1Deck中移除的那张卡牌对象,里面只包含card_id和name两个字段,origin指的是来源对象,比如如果是从卡组移入手牌,那么来源对象就是卡组,如果是从墓地移入手牌那么来源就是墓地,这个来源对象是方便记录起始点的位置(结束点我设置的是手牌区的起始点,用来做卡牌的移动特效),上面的updateCardPos方法就是用TWEEN做的移动效果,需要注意的是,结尾一定要写tw.start()方法,否则动画不会执行,同时需要在game/index.vue的动画循环中加入TWEEN.update(),否则动画不会更新,依然看不到动画效果,动画教程可以参考:tween.js user guide | tween.js。

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

最后别忘了修改hand/index.vue方法,将addHandCard方法暴露出去:

const addHandCard = (obj: any, origin: any) => {
  p1Ref.value.addHandCard(obj, origin)
}

defineExpose({
  init,
  addHandCard
})

最后的效果如下:

附:
game/index.vue完整代码:

<template>
  <div ref="sceneRef" class="scene"></div>
  <!-- 手牌 -->
  <Hand ref="handRef"/>
  <!-- 卡组 -->
  <Deck ref="deckRef"/>
</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 { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { useCommonStore } from "@/stores/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"

// 引入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 commonStore = useCommonStore()

// 场景ref
const sceneRef = ref()
const handRef = ref()
const deckRef = ref()

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

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

  // 监听浏览器窗口变化进行场景自适应
  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 () => {
  // 初始化卡组
  await initDeck()
  // 初始化手牌
  initHand()
}

// 初始化卡组
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 = () => {
  let cardNumber = 4
  let _number = 0
  let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
  let deckGroup = scene.getObjectByName("p1_deckGroup")
  let _interval = setInterval(function() {
    // console.log(123, p1Deck)
    if (_number < cardNumber) {
      let obj = p1Deck[p1Deck.length - 1]
      p1Deck.splice(p1Deck.length-1, 1)
      commonStore.updateP1Deck(p1Deck)
      // 修改卡组
      deckRef.value.editDeckCard(obj, "remove")
      // 手牌区添加手牌
      handRef.value.addHandCard(obj, deckGroup)
    } else {
      clearInterval(_interval)
    }
    _number++
  }, 200)

}
</script>

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

game/deck/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 } 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()

// 卡组group
const deckGroup = new THREE.Group()
deckGroup.name = "p1_deckGroup"
scene.add(deckGroup)

const init = () => {
  setDeckPos()
  commonStore.$state.p1Deck.forEach((v: any, i: any) => {
    let obj = CARD_DICT.find((b: any) => b.card_id === v.card_id)
    if (obj) {
      let card = new Card(obj)
      let mesh = card.init()
      mesh.position.set(0, 0.005 * i, 0)
      mesh.rotateX(180 * (Math.PI / 180)) // 弧度
      mesh.name = v.name
      deckGroup.add( mesh );
    }
  })

  renderText()

}

// 渲染文字
const renderText = () => {
  const geometry = new TextGeometry( `${commonStore.$state.p1Deck.length}`, {
    font: commonStore.$state._font,
    size: 0.4,
    height: 0,
    curveSegments: 4,
    bevelEnabled: true,
    bevelThickness: 0,
    bevelSize: 0,
    bevelSegments: 0
  });
  geometry.center()
  const material = new THREE.MeshBasicMaterial( { color: new THREE.Color("white") } )
  const mesh = new THREE.Mesh( geometry, material ) ;
  mesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length + 0.01, 0) // 弧度
  mesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  mesh.name = "卡组数量"

  // 阴影
  let shadowGeometry = geometry.clone()
  shadowGeometry.translate(0.02, 0.02, 0);
  let shadowMaterial = new THREE.MeshBasicMaterial( { color: new THREE.Color("black") } );
  let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
  shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0) // 弧度
  shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
  shadowMesh.name = "卡组数量阴影"

  deckGroup.add(mesh)
  deckGroup.add(shadowMesh)
}

// 设置卡组位置
const setDeckPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point = transPos(window.innerWidth - 15, window.innerHeight - 15) // 卡组起始位置的屏幕坐标
    // 
    raycaster.setFromCamera( point, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // deckGroup.position.set(point.x, point.y, point.z)
      deckGroup.position.set(point.x - 0.5, point.y, point.z - 0.7)
    }
  })
}

// 修改卡组
const editDeckCard = (mesh: any, type: any) => {
  let group = scene.getObjectByName("p1_deckGroup")
  let text = group.children.find((v: any) => v.name === "卡组数量")
  let shadowText = group.children.find((v: any) => v.name === "卡组数量阴影")
  // console.log(22, group.children, commonStore.$state.p1Deck)
  if (type === "remove") { // 删除卡组中的卡牌
    let child = group.children.find((v: any) => v.name === mesh.name)
    if (child) {
      group.remove(child)
    }
  }
  group.remove(text)
  group.remove(shadowText)
  group.children.forEach((v: any, i: any) => {
    v.position.set(0, 0.005 * i, 0)
  })
  renderText()
}

defineExpose({
  init,
  editDeckCard
})
</script>

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

game/hand/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 { DragControls } from 'three/addons/controls/DragControls.js';
import { Card } from "@/views/game/Card.ts"
import { CARD_DICT } from "@/utils/dict/card.ts"
import { transPos } 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()

// 手牌区group
const handGroup = new THREE.Group()
handGroup.name = "p1_handGroup"
scene.add(handGroup)

const controls = new DragControls( handGroup.children, camera, renderer.domElement );

const _width = ref()

const init = () => {
  setHandPos()
}

// 设置手牌区位置
const setHandPos = () => {
  nextTick(() => {
    let plane = scene.getObjectByName("地面")
    let point1 = transPos(10, window.innerHeight - 10) // 手牌区起始位置的屏幕坐标
    let point2 = transPos(window.innerWidth * 0.65, window.innerHeight - 10) // 手牌区结束位置的屏幕坐标
    let x1 = 0 // 手牌区起始位置的世界x坐标
    let x2 = 0 // 手牌区结束位置的世界x坐标
    // 
    raycaster.setFromCamera( point1, camera );
    const intersects1 = raycaster.intersectObject( plane );
    if (intersects1.length > 0) {
      let point = intersects1[0].point
      // 由于卡牌几何体大小设置的是(1, 0.005, 1.4),所以我们对应进行偏移
      // handGroup.position.set(point.x, point.y, point.z)
      handGroup.position.set(point.x + 0.5, point.y, point.z - 0.7)
      x1 = handGroup.position.x
    }
    // 
    raycaster.setFromCamera( point2, camera );
    const intersects = raycaster.intersectObject( plane );
    if (intersects.length > 0) {
      let point = intersects[0].point
      x2 = point.x + 0.5
    }

    // 用绝对值相加得到手牌区长度
    _width.value = Math.abs(x1) + Math.abs(x2)

  })
}

// 添加手牌
const addHandCard = (obj: any, origin: any) => {
  let position = origin.position
  // console.log(666, deckGroupPos)
  let cardObj = CARD_DICT.find((v: any) => v.card_id === obj.card_id)
  if (cardObj) {
    let card = new Card(cardObj)
    let mesh = card.init()
    mesh.position.set(position.x, position.y, position.z)
    mesh.material.forEach((v: any) => {
      v.transparent = true
    })
    scene.add( mesh );
    updateCardPos(mesh)
  }

}

// 更新卡牌位置
const updateCardPos = (mesh: any) => {
  const tw = new TWEEN.Tween({
    x: mesh.position.x,
    y: mesh.position.y,
    z: mesh.position.z,
    opacity: 0.9,
    mesh
  })
  tw.to({
    x: handGroup.position.x,
    y: handGroup.position.y,
    z: handGroup.position.z,
    opacity: 0
  }, 200)
  tw.easing(TWEEN.Easing.Quadratic.Out)
  tw.onUpdate((obj: any) => {
    obj.mesh.position.set(obj.x, obj.y, obj.z)
    obj.mesh.material.forEach((v: any) => {
      v.opacity = obj.opacity
    })
  })
  tw.onComplete(function() {
    //动画结束:关闭允许透明,恢复到模型原来状态
    TWEEN.remove(tw)
    scene.remove( mesh );
    
    mesh.material.forEach((v: any) => {
      v.transparent = false
      v.opacity = 1
    })
    handGroup.add(mesh)
    // 计算叠放间距
    let space = ((_width.value - 1) / (handGroup.children.length - 1)) <= 1 ? (_width.value - 1) / (handGroup.children.length - 1) : 1
    handGroup.children.forEach((v: any, i: any) => {
      v.position.set(i * space, 0.005 * i, 0)
    })
  })

  tw.start();
}

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

} );

controls.addEventListener( 'drag', function ( event: any ) {
  event.object.position.y += 0.04
  // console.log(event)

} );

controls.addEventListener( 'dragend', function ( event: any ) {
  event.object.position.y -= 0.04
} );

defineExpose({
  init,
  addHandCard
})
</script>

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/486760.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Scapy国内城市空气质量数据采集系统设计与实现

代码和完整的报告在文章最后 城市空气质量数据采集系统设计与实现 &#x1f3d9;️ 研究背景 &#x1f32c;️ 城市化与环境挑战&#xff1a;随着城市化进程的加快&#xff0c;环境污染问题&#xff0c;尤其是空气质量问题&#xff0c;已成为公众关注的焦点。数据监测的重要性…

Windows安装配置国产达梦数据库、配置Python接口

文章目录 前言1.下载安装达梦数据库2.配置达梦环境变量3.安装Microsoft Visual C 14.04.安装达梦Python接口dmpython5.测试验证 总结 前言 达梦数据库&#xff08;Dameng Database&#xff09;是由武汉达梦数据库股份有限公司开发的一款高性能的关系型数据库管理系统。该数据库…

关于短群签名论文阅读

参考文献为2004年发表的Short Group Signatures 什么群签名&#xff1f; 群签名大致就是由一组用户组成一个群&#xff0c;其中用户对某条消息的签名&#xff0c;改签名不会揭示是哪一个用户签署的&#xff0c;签名只能表明该消息确实是来自该群的签名。对于群还有一个群管理者…

蓝桥杯算法 - DP

上一篇&#xff1a;[[蓝桥杯算法-排序、递归、全排列]] 动态规划&#xff08;dp&#xff09; dp即动态规划&#xff0c;常用于&#xff1a;数学&#xff0c;计算机科学&#xff0c;管理学&#xff0c;经济和生物信息学。 dp在生活中也很常见&#xff0c;如&#xff1a;你今天…

【随笔】oh-my-posh(Windows power shell为例)

Oh My Posh 是一个适用于任何 shell 的自定义提示引擎&#xff0c;能够使用函数或变量调整提示字符串。 文章目录 一、安装oh-my-posh二、安装Nerd 字体三、oh-my-posh 初始化四、更换主题 一、安装oh-my-posh GitHub repo&#xff1a;https://github.com/JanDeDobbeleer/oh-m…

情感视频素材怎么来的?(情感语录的视频素材在哪里找)

很多小伙伴觉得情感类型的短视频账号用户多&#xff0c;都想要进入分一杯羹&#xff0c;那么这些创作素材去哪里找呢&#xff0c;下面分享几个非常使用的找情感短视频素材的办法。 1&#xff0c;蛙学网 说到情感视频素材的短视频&#xff0c;作为一个专业的短视频素材网站&am…

2024年云服务器ECS价格表出炉——腾讯云

腾讯云服务器多少钱一年&#xff1f;61元一年起。2024年最新腾讯云服务器优惠价格表&#xff0c;腾讯云轻量2核2G3M服务器61元一年、2核2G4M服务器99元一年可买三年、2核4G5M服务器165元一年、3年756元、轻量4核8M12M服务器646元15个月、4核16G10M配置32元1个月、312元一年、8核…

nodeJs中实现连表查询

nodeJs中实现连表查询 router.post(/getOrder, async function(req, res, next) {let userId req.body.phone;let sql select * from orders where userId?;let orders await new Promise((resolve, reject) > {connection.query(sql, [userId], function(error, resul…

一分钟在Solana链创建代币教程

只需要 1 分钟就可以创建自己的SOLANA代币 1、连接Solana钱包2、填写代币信息创建3、创建成功 Solana 是一个基于区块链技术的高性能、去中心化的智能合约平台&#xff0c;旨在为开发者提供高度可扩展和低成本的区块链基础设施。通过其创新的共识机制和高吞吐量的网络架构&…

注册中国商标的大致流程

在当今竞争激烈的商业环境中&#xff0c;商标作为企业形象和品牌标识的重要载体&#xff0c;其保护和推广至关重要。注册中国商标是拓展中国市场的关键步骤 注册中国商标需要以下基本资料&#xff1a; 商标图样&#xff1a;须清晰、完整地展示商标图案和文字内容&#xff1b;商…

MQ消息队列从入门到精通速成

文章目录 1.初识MQ1.1.同步和异步通讯1.1.1.同步通讯1.1.2.异步通讯 1.2.技术对比&#xff1a; 2.快速入门2.1.安装RabbitMQ2.2.RabbitMQ消息模型2.3.导入Demo工程2.4.入门案例2.4.1.publisher实现2.4.2.consumer实现 2.5.总结 3.SpringAMQP3.1.Basic Queue 简单队列模型3.1.1.…

大模型日报|今日必读的6篇大模型论文

大家好&#xff0c;今日必读的大模型论文来啦&#xff01; 1.英伟达提出LATTE3D&#xff1a;更快、更好的“文生3D”方法 近来&#xff0c;由文本到 3D 生成的方法可以生成令人印象深刻的 3D 效果&#xff0c;但这个过程需要耗时的优化过程&#xff0c;每个提示&#xff08;p…

AI之Suno:Suno V3的简介、安装和使用方法、案例应用之详细攻略

AI之Suno&#xff1a;Suno V3的简介、安装和使用方法、案例应用之详细攻略 目录 Suno AI的简介 1、特点与改进&#xff1a; Suno AI的安装和使用方法 1、第一步&#xff0c;让国产大模型—ChatGLM4帮我写一个提示词 2、第二步&#xff0c;将提示词交给Suno v3&#xff0c;…

TikTok vs Instagram!哪个广告形式更适合你

近几年&#xff0c;TikTok以短视频和创新性吸引不少年轻受众&#xff0c;在广告方面也提供挑战赛、创意滤镜和名人合作等多种方式&#xff0c;自2019年起迅速增长&#xff0c;成为Instagram的强劲对手&#xff0c;连续三年下载量居首。而Instagram则拥有十多年历史和庞大用户基…

人工智能(Educoder)-- 搜索技术 -- 盲目式搜索

第1关&#xff1a;盲目搜索之宽度优先搜索算法 任务描述 本关任务&#xff1a;给定迷宫地图以及在迷宫中的起始位置&#xff0c;利用宽度优先搜索算法求解走出迷宫的最短路径长度&#xff0c;走出迷宫意味着达到迷宫地图的边界&#xff08;所有位置下标0开始&#xff09;。 …

安卓工控一体机主板定制_联发科MTK平台解决方案

新移科技安卓工控一体机方案基于MT8766主芯片&#xff0c;采用四核 Cortex-A53 CPU&#xff0c;搭载Android 12.0系统&#xff0c;主频高达2.0GHz&#xff0c;具有低功耗和高性价比的优势。搭载ARM IMG GE8300 高性能GPU和4G全网通版本的RF&#xff0c;网络连接稳定快速。 可直…

Linux调试器-gdb

一、背景 程序的发布方式有两种&#xff0c;debug模式和release模式 debug模式&#xff1a;编译器形成可执行程序的时候会给可执行程序添加调试信息 程序员调试时使用debug模式&#xff0c;而release模式用于测试 而gcc/g默认编译&#xff0c;采用release模式 用gcc/g使用…

智能建筑:基于IT的集成和融合解决方案

智能建筑( Intelligent Building) 定义: 以建筑为平台,兼备建筑设备、办公自动化及通信网络系统,集结构、系统、服务、管理及它们之间的最优化组合,向人们提供一个安全、高效、舒适、便利的建筑环境。 智能建筑的发展历史: -产生:1984年世界上第一座智能大厦诞生于美国…

基于yolov8安全帽检测的系统

基于yolov8安全帽检测的系统 项目描述&#xff1a; 安全头盔检测&#xff08;计算机视觉&#xff09; 1.自训练数据集1538张数据图片&#xff0c;进行标注&#xff0c;并进行100轮的训练&#xff0c;准确率达0.966 2.使用 Flask 和 Ultralytics YOLOv8 模型开发了一个 Web 应…

【开发环境搭建篇】NodeJS的安装和配置

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过大学刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0…
最新文章