赞
踩
首先优化下之前的代码,把game/deck/p1.vue中修改卡组方法和渲染卡组文字方法提到公共方法中,此时utils/common.ts完整代码如下:
-
- import { nextTick } from 'vue';
- import * as THREE from 'three';
- import * as TWEEN from '@tweenjs/tween.js'
- import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
-
- // 坐标归一化
- const transPos = (x: any, y: any) => {
- let pointer = new THREE.Vector2()
-
- pointer.x = ( x / window.innerWidth ) * 2 - 1;
- pointer.y = - ( y / window.innerHeight ) * 2 + 1;
-
- return pointer
- }
- export { transPos }
-
- // 修改卡组
- const editDeckCard = (group:any, mesh: any, type: any) => {
- return new Promise((resolve, reject) => {
- 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)
- })
- resolve(true)
- })
- }
- export { editDeckCard }
-
- // 渲染卡组文字
- const renderText = (group: any, text: any, font: any, position: any) => {
- return new Promise((resolve, reject) => {
- const geometry = new TextGeometry( `${text}`, {
- 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"),
- alphaHash: true
- })
- const mesh = new THREE.Mesh( geometry, material ) ;
- mesh.position.set(position.x, position.y + 0.01, position.z)
- // 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"),
- alphaHash: true
- });
- let shadowMesh = new THREE.Mesh(shadowGeometry, shadowMaterial);
- shadowMesh.position.set(position.x, position.y, position.z)
- // shadowMesh.position.set(0, 0.005 * commonStore.$state.p1Deck.length, 0)
- shadowMesh.rotateX(-90 * (Math.PI / 180)) // 弧度
- shadowMesh.name = "卡组数量阴影"
-
- group.add(mesh)
- group.add(shadowMesh)
-
- resolve(true)
- })
- }
- export { renderText }

同步修改game/index.vue中对修改卡组方法的使用:
-
- import { transPos, editDeckCard, renderText } from "@/utils/common.ts"
-
- // 初始化手牌
- const initHand = () => {
- 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)
- }
- _number++
- }, 200)
-
- }

之后我们思考下抽卡逻辑:
1.点击卡组,将卡组平移到页面中心位置(0, 0, 0)(y轴可以适当调高一点,这里点击事件只是为了测试抽卡,之后可以删掉)
2.然后卡组上展示“点击抽卡”的文字(文字用的是div+css的形式)
3.点击抽卡,执行抽卡动画
4.抽卡结束后,卡组移回到原位
首先我们在game/index.vue中添加点击事件,用raycaster射线进行鼠标拾取,当点击在卡组上时,执行onP1DeckEvent,这里由于是移动端所以只监听了touch事件,pc端则需要额外监听click事件:
- // 鼠标按下
- window.addEventListener('touchstart', onMousedown)
-
- // 鼠标按下事件
- 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
- // pointer.x = ( clientX / window.innerWidth ) * 2 - 1;
- // pointer.y = - ( clientY / window.innerHeight ) * 2 + 1;
-
- 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)
- }
- }

然后我们在src下新建一个抽卡组件:
这个组件其实是用div+css画了一个透明的蒙层,卡组移到中央后,展示蒙层和“点击抽卡”字样,然后卡组动画我们分为两步,第一步是卡组飞到页面中心,用户点击抽卡后,执行之前的修改卡组和添加手牌方法,然后隐藏蒙层,执行第二步卡组移回原文动画,components/DrawCard.vue完整代码如下:
- <!-- 抽卡 -->
- <template>
- <div v-if="state.visible" ref="maskRef" class="mask">
- <div class="box" @click="onDrawCard">
- <p>点击抽卡</p>
- </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 { transPos, editDeckCard, renderText } from "@/utils/common.ts"
-
- const props: any = defineProps({
- handRef: {},
- deckRef: {}
- })
-
- // 引入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()
- const maskRef = ref()
-
- const state = reactive({
- visible: false
- })
-
- // 卡组抽卡动画1-卡组移到页面中央
- const drawCardAnimate1 = (deckGroup: any, startPos: any, endPos: any) => {
- // 隐藏卡组数量和卡组数量阴影几何体
- let textMesh = deckGroup.children.find((v: any) => v.name === "卡组数量")
- let textShadowMesh = deckGroup.children.find((v: any) => v.name === "卡组数量阴影")
-
- const tw = new TWEEN.Tween({
- x: startPos.x,
- y: startPos.y,
- z: startPos.z,
- opacity: 1.0,
- group: deckGroup
- })
- tw.to({
- x: endPos.x,
- y: endPos.y,
- z: endPos.z,
- opacity: 0.0,
- }, 200)
- tw.easing(TWEEN.Easing.Quadratic.Out)
- tw.onUpdate((obj: any) => {
- obj.group.position.set(obj.x, obj.y, obj.z)
- textMesh.material.opacity = obj.opacity
- textShadowMesh.material.opacity = obj.opacity
- })
- tw.onComplete(function() {
- state.visible = true
- TWEEN.remove(tw)
- })
- tw.start();
- }
-
- // 卡组抽卡动画2-卡组回到原位
- const drawCardAnimate2 = (deckGroup: any, startPos: any, endPos: any) => {
- // 隐藏卡组数量和卡组数量阴影几何体
- let textMesh = deckGroup.children.find((v: any) => v.name === "卡组数量")
- let textShadowMesh = deckGroup.children.find((v: any) => v.name === "卡组数量阴影")
- // textMesh.material.opacity = 1
- // textShadowMesh.material.opacity = 1
-
- const tw = new TWEEN.Tween({
- x: startPos.x,
- y: startPos.y,
- z: startPos.z,
- opacity: 0,
- group: deckGroup
- })
- tw.to({
- x: endPos.x,
- y: endPos.y,
- z: endPos.z,
- opacity: 1,
- }, 200)
- tw.easing(TWEEN.Easing.Quadratic.Out)
- tw.onUpdate((obj: any) => {
- obj.group.position.set(obj.x, obj.y, obj.z)
- textMesh.material.opacity = obj.opacity
- textShadowMesh.material.opacity = obj.opacity
- })
- tw.onComplete(function() {
- TWEEN.remove(tw)
- })
- tw.start();
- }
-
- // 抽卡
- const onDrawCard = async () => {
- let p1_deckGroup = scene.getObjectByName("p1_deckGroup")
- let p1Deck = JSON.parse(JSON.stringify(commonStore.$state.p1Deck))
- let position = new THREE.Vector3(0, 0.005 * p1Deck.length, 0)
-
- let obj = p1Deck[p1Deck.length - 1]
- p1Deck.splice(p1Deck.length-1, 1)
- commonStore.updateP1Deck(p1Deck)
- // 修改卡组
- await editDeckCard(p1_deckGroup, obj, "remove")
- await renderText(p1_deckGroup, `${commonStore.$state.p1Deck.length}`, commonStore.$state._font, position)
- // 手牌区添加手牌
- props.handRef.addHandCard(obj, p1_deckGroup)
- state.visible = false
- setTimeout(() => {
- nextTick(() => {
- drawCardAnimate2(p1_deckGroup, p1_deckGroup.position, p1_deckGroup.userData.position)
- })
- }, 500)
- }
-
-
- defineExpose({
- drawCardAnimate1,
- drawCardAnimate2
- })
- </script>
-
- <style lang="scss" scoped>
- .mask {
- position: fixed;
- top: 0;
- left: 0;
- z-index: 10;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 100%;
- height: 100vh;
- // background-color: white;
- .box {
- width: 50vh;
- // height: 100px;
- text-align: center;
- font-size: 20px;
- color: #fff;
- }
- }
- </style>

注意:由于点击事件会穿透蒙层,所以我们在onMousedown方法中添加了ev.target instanceof HTMLCanvasElement进行判断是否点击到了canvas上。
最后还优化了下卡组,给卡组添加了线框,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, 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()
-
- // 卡组group
- const deckGroup = new THREE.Group()
- deckGroup.name = "p1_deckGroup"
- scene.add(deckGroup)
-
- const init = () => {
- setDeckPos()
- addDeckWireframe()
- 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 );
- }
- })
-
- let position = new THREE.Vector3(0, 0.005 * commonStore.$state.p1Deck.length, 0)
- renderText(deckGroup, `${commonStore.$state.p1Deck.length}`, commonStore.$state._font, position)
-
- }
-
- // 设置卡组位置
- 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)
- // 记录卡组位置
- let position = new THREE.Vector3(point.x - 0.5, point.y, point.z - 0.7)
- deckGroup.userData["position"] = position
- }
- })
- }
-
- // 绘制卡组区域线框
- const addDeckWireframe = () => {
- nextTick(() => {
- const edges = new THREE.EdgesGeometry( deckGroup.children[0].geometry );
- const line = new THREE.LineSegments( edges, new THREE.LineBasicMaterial( { color: 0xffffff } ) );
- deckGroup.add(line);
- })
- }
-
- defineExpose({
- init,
- })
- </script>
-
- <style lang="scss" scoped>
- </style>

最后看下抽卡效果:
至此基本完成了抽卡逻辑和简单动效的开发。
附录:
game/index.vue完整代码如下:
- <template>
- <div ref="sceneRef" class="scene"></div>
- <!-- 手牌 -->
- <Hand ref="handRef"/>
- <!-- 卡组 -->
- <Deck ref="deckRef"/>
- <!-- 抽卡逻辑 -->
- <DrawCard ref="drawCardRef" :handRef="handRef" :deckRef="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 { 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 DrawCard from "@/components/DrawCard.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 handRef = ref()
- const deckRef = ref()
- const drawCardRef = 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('touchstart', onMousedown)
- // 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 () => {
- // 初始化卡组
- 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 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)
- }
- _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
- // pointer.x = ( clientX / window.innerWidth ) * 2 - 1;
- // pointer.y = - ( clientY / window.innerHeight ) * 2 + 1;
-
- 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)
- }
- }
- </script>
-
- <style lang="scss" scoped>
- .scene {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100vh;
- }
- </style>

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。