首先在game目录下创建site文件夹,用来存放战域相关代码:
这里思考下如何创建战域,我的想法是添加一个平面,将己方战域和对方战域都添加进这个平面中,所以首先我们先添加一个战域plane,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
const geometry = new THREE.PlaneGeometry( x, y );
geometry.center()
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color("gray"),
side: THREE.FrontSide,
alphaHash: true,
// alphaTest: 0,
opacity: 0
});
const site = new THREE.Mesh( geometry, material );
site.name = "战域"
site.rotateX(-90 * (Math.PI / 180)) // 弧度
site.userData["width"] = x
site.userData["height"] = y
scene.add(site)
resolve(true)
}
})
}
defineExpose({
init,
})
</script>
<style lang="scss" scoped>
</style>
这里计算出了平面的长和宽,将长宽数据存放进site的userData中,这个长和宽可以用来帮助我们后续绘制格子的时候做位置计算。然后我们在site/p1.vue中开始绘制己方战域:
1.绘制一个线框格子,格子Box的大小和卡牌设置成一样,如果在屏幕中看着比较大也可以通过scale进行适当的缩放:
// 创建线框网格
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
}
2.我们可以按照自己的想法绘制游戏中有几个格子,这里我设计的是前5个格子(包括一个场地格子和一个墓地格子),后三个格子,site/p1.vue相关代码如下:
// 初始化己方战域
const init = () => {
let sitePlane = scene.getObjectByName("战域")
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"
} else if (i == 2) {
mesh.name = "己方场地2"
} else if (i == 3) {
mesh.name = "己方场地3"
} 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 = "己方场地4"
} else if (i == 2) {
mesh.name = "己方场地5"
} else if (i == 3) {
mesh.name = "己方场地6"
}
mesh.position.set(-planeWidth/2 + 0.5, -2.2, 0)
mesh.translateX(spaceX * i + i)
sitePlane.add(mesh)
}
}
}
这里我为每个格子添加了name,方便后续卡牌上场,进墓等操作逻辑。
页面效果如下:
附录:
game/index.vue完整代码:
<template>
<div ref="sceneRef" class="scene"></div>
<!-- 手牌 -->
<Hand ref="handRef"/>
<!-- 卡组 -->
<Deck ref="deckRef"/>
<!-- 战域 -->
<Site ref="siteRef"/>
<!-- 抽卡逻辑 -->
<DrawCard ref="drawCardRef" :handRef="handRef"/>
</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 Site from "./site/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 siteRef = 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 () => {
// 初始化场地
initSite()
// 初始化卡组
await initDeck()
// 初始化手牌
initHand()
}
// 初始化场地
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 = () => {
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>
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("战域")
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"
} else if (i == 2) {
mesh.name = "己方场地2"
} else if (i == 3) {
mesh.name = "己方场地3"
} 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 = "己方场地4"
} else if (i == 2) {
mesh.name = "己方场地5"
} else if (i == 3) {
mesh.name = "己方场地6"
}
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>