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

news2024/11/13 13:52:27

首先我们优化下之前的代码,先加载游戏资源,然后再初始化场景,由于目前只有一个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.coloradmin.cn/o/1546351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

干货分享之反射笔记

入门级笔记-反射 一、利用反射破泛型集合二、Student类三、获取构造器的演示和使用1.getConstructors只能获取当前运行时类的被public修饰的构造器2.getDeclaredConstructors:获取运行时类的全部修饰符的构造器3.获取指定的构造器3.1得到空构造器3.2得到两个参数的有参构造器&a…

基于java+springboot+vue实现的超市管理系统(文末源码+Lw+ppt)23-354

摘 要 系统根据现有的管理模块进行开发和扩展&#xff0c;采用面向对象的开发的思想和结构化的开发方法对超市管理的现状进行系统调查。采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”…

启扬RK3568核心板助力智慧步道轻装健身,打造全民健康生活新方式

随着物联网、AI智能等新技术的快速发展&#xff0c;智慧步道成为全国各地公园建设和全民健身公共服务设施改造的新主题。智慧步道基于物联网、人脸识别、大数据分析等技术&#xff0c;对人们的运动进行监测和数据采集&#xff0c;显示运动数据&#xff0c;包括里程统计、热量消…

【王道训练营】第6题 输入一个整型数,判断是否是对称数,如果是,输出yes,否则输出no

文章目录 我的代码改正代码其他代码 我的代码 没有完成 #include<stdio.h> int main(){int a;int b;int c0;//位数int d0;//比较几次scanf("%d",&a);while(b!0){bb/10;c;}dc/2;//比较几次int ffor(int i0 ;i<d;i){int ec;//位数fa - a / (((e-i-1)*10…

2024年软件测试,“我“从初级到高级进阶,不再走弯路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 现在2024年&#…

Python算法100例-4.2 列出真分数序列

完整源代码项目地址&#xff0c;关注博主私信源代码后可获取 1.问题描述2.问题分析3.算法设计4.确定程序框架5.完整的程序6.拓展训练 1&#xff0e;问题描述 按递增顺序依次列出所有分母为40、分子小于40的最简分数。 2&#xff0e;问题分析 分子和分母只有公因数1的分数&…

Maven发布开源框架到远程仓库

1.背景 当你写了一个自我感觉良好的开源工具希望给他人分享&#xff0c;如果只是在github等网站进行公布之外&#xff0c;用户使用起来还不是很方便&#xff0c;特别是当你提供是特定领域的基础工具。你还可以把它部署到中央仓库&#xff0c;这样别人使用就会方便很多。接下来…

android_uiautomator元素定位

通过UIAUTOMATOR的text属性定位到元素&#xff0c;并打印文本from appium import webdriver from appium.webdriver.common.appiumby import AppiumBy import time # For W3C actions from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriv…

Kafka总结问题

Kafka Kafka Kafka Kafka的核心概念/ 结构 topoic Topic 被称为主题&#xff0c;在 kafka 中&#xff0c;使用一个类别属性来划分消息的所属类&#xff0c;划分消息的这个类称为 topic。topic 相当于消息的分配标签&#xff0c;是一个逻辑概念。主题好比是数据库的表&#xff0…

AI视频激光综合驱鸟装置:全自动、大范围驱鸟 | 真驱鸟科技

在电力系统中&#xff0c;鸟害事故已成为一个不容忽视的问题&#xff0c;直接威胁到电网的正常运行。但鸟类拥有极强的环境适应能力&#xff0c;它们能够在各种环境中生存和繁衍。这种强大的适应性使得传统的单一功能驱鸟器&#xff0c;在面对鸟类时显得力不从心&#xff0c;无…

Github简单入门教程

文章目录 使用前提查看项目内容查看项目介绍打包下载项目查看项目作者项目搜索复制项目分支项目创建向项目中添加文件对项目进行评论 使用前提 想要使用Github&#xff0c;首先需要学会科学上网&#xff0c;不然用国内的网在正规情况下是无法访问Github滴~ 查看项目内容 打开…

【Pt】马灯贴图绘制过程 01-制作基础色

目录 一、导入模型并烘焙 二、制作基础底漆 &#xff08;1&#xff09;底漆层 &#xff08;2&#xff09;水痕层 &#xff08;3&#xff09;指纹层 一、导入模型并烘焙 1. 导入模型&#xff0c;马灯模型如下所示 2. 在纹理集设置中点击“烘焙模型贴图” 设置输出大小为…

使用 Amazon Bedrock + Claude 3 打造个性化智能编程助手

最近&#xff0c;随着人工智能技术的迅速发展&#xff0c;代码助手已经成为软件开发领域备受关注的工具。像 Amazon CodeWhisperer 和 Github Copilot 这样的工具可以在集成开发环境中帮助用户自动生成代码&#xff0c;极大地提高了开发效率。然而&#xff0c;这些助手通常缺乏…

HarmonyOS 健康系统联系案例 创建项目

上文 HarmonyOS 健康系统联系案例 整体原型图介绍 我们 介绍了健康系统的整体 UI 然后 我们一点一点来 今天先搭个环境 首先 我们打开开发工具首页 创建项目 一个非常令人怀念的步骤啊 我们点击 Create Project 创建一个新的工程 模板 还是选最基础的 Empty Ability 然后 …

【鸿蒙HarmonyOS开发笔记】使用@Preview装饰器预览组件

概述 ArkTS应用/服务支持组件预览&#xff0c;要求compileSdkVersion为8或以上。组件预览支持实时预览&#xff0c;不支持动态图和动态预览。组件预览通过在组件前添加注解Preview实现&#xff0c;在单个源文件中&#xff0c;最多可以使用10个Preview装饰自定义组件。 Preview…

[C++]函数重载(什么是函数重载,函数重载的原理(底层怎么实现))

一、什么是函数重载 函数重载是指在同一作用域内&#xff0c;可以有多个功能类似具有相同函数名&#xff0c;不同参数列表&#xff08;包括参数类型、参数个数、参数顺序&#xff09;的函数。编译器会根据函数调用时提供的参数来决定调用哪一个具体的函数。 注意&#xff1a;只…

洛谷_P1102 A-B 数对_python写法

P1102 A-B 数对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 解法一&#xff1a; n, c map(int,input().split())data list(map(int,input().split())) data.sort() maxx max(data)sum 0 l [0 for _ in range(maxx1)] for i in data:l[i] 1 for b in range(1, maxx1)…

漏洞扫描-让安全弱点无所遁形

随着信息技术的迅猛发展和互联网的广泛普及&#xff0c;网络安全问题日益凸显。在这个数字化的世界里&#xff0c;无论是企业还是个人&#xff0c;都面临着前所未有的安全威胁。安全漏洞&#xff0c;作为这些威胁的源头&#xff0c;常常被忽视或无法及时发现。 而漏洞扫描&…

VBA技术资料MF133:隐藏编辑栏及计算字符串

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

20240319-1-过拟合与欠拟合

过拟合欠拟合面试题 1. 如何理解高方差与低偏差? 模型的预测误差可以分解为三个部分: 偏差(bias)&#xff0c; 方差(variance) 和噪声(noise). 偏差 偏差度量了模型的期望预测与真实结果的偏离程度&#xff0c; 即刻画了学习算法本身的拟合能力。偏差则表现为在特定分布上…