Files
nl-game/src/games/components/Racing3dGame.vue
李琦 cd1ab90013 初始化
需要注意的是饥荒不够完善:动作、手持工具、细节交互、系统逻辑
2026-08-15 07:24:34 +08:00

242 lines
6.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
// 3D 极速赛车Three.js三车道公路左右变道躲避车流里程即分数
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { Keys, randInt } from '../engine'
const emit = defineEmits(['score', 'end'])
const holder = ref(null)
const W = 640
const H = 560
const LANES = [-2.2, 0, 2.2] // 三车道 X 坐标
let renderer, scene, camera, rafId
let keys
let myCar
let traffic = [] // 敌车 {mesh, lane, z, speed}
let stripes = [] // 车道虚线
let myLane = 1
let dist = 0
let speed = 26
let alive = false
let lastTs = 0
let spawnZ = -40
let shieldOn = false
let invincible = 0
let shieldRing
const CAR_COLORS = ['#ff5d73', '#ffd500', '#4ade80', '#b26bff', '#ff9f45']
// 组装一辆简单小车(车身+车顶)
function makeCar(color) {
const group = new THREE.Group()
const body = new THREE.Mesh(
new THREE.BoxGeometry(1.4, 0.5, 2.6),
new THREE.MeshLambertMaterial({ color })
)
body.position.y = 0.45
const top = new THREE.Mesh(
new THREE.BoxGeometry(1.1, 0.42, 1.3),
new THREE.MeshLambertMaterial({ color: '#dfe6ff' })
)
top.position.set(0, 0.85, 0.1)
group.add(body, top)
// 四个轮子
const wheelGeo = new THREE.CylinderGeometry(0.26, 0.26, 0.24, 14)
const wheelMat = new THREE.MeshLambertMaterial({ color: '#16161e' })
;[[-0.72, 0.9], [0.72, 0.9], [-0.72, -0.9], [0.72, -0.9]].forEach(([x, z]) => {
const wheel = new THREE.Mesh(wheelGeo, wheelMat)
wheel.rotation.z = Math.PI / 2
wheel.position.set(x, 0.26, z)
group.add(wheel)
})
return group
}
function initScene() {
scene = new THREE.Scene()
scene.background = new THREE.Color('#0d0e2b')
scene.fog = new THREE.Fog('#0d0e2b', 22, 60)
camera = new THREE.PerspectiveCamera(65, W / H, 0.1, 120)
camera.position.set(0, 4.6, 7.5)
camera.lookAt(0, 0, -6)
const dir = new THREE.DirectionalLight(0xffffff, 1.4)
dir.position.set(4, 10, 4)
scene.add(dir)
scene.add(new THREE.AmbientLight(0xffffff, 0.6))
// 公路
const road = new THREE.Mesh(
new THREE.PlaneGeometry(8.2, 200),
new THREE.MeshLambertMaterial({ color: '#23253f' })
)
road.rotation.x = -Math.PI / 2
road.position.z = -70
scene.add(road)
// 路肩
const sideMat = new THREE.MeshLambertMaterial({ color: '#141530' })
;[-7, 7].forEach((x) => {
const side = new THREE.Mesh(new THREE.PlaneGeometry(6, 200), sideMat)
side.rotation.x = -Math.PI / 2
side.position.set(x, -0.01, -70)
scene.add(side)
})
// 车道虚线
const stripeMat = new THREE.MeshBasicMaterial({ color: '#8f95c9' })
for (let z = 8; z > -120; z -= 4) {
;[-1.1, 1.1].forEach((x) => {
const s = new THREE.Mesh(new THREE.PlaneGeometry(0.14, 1.6), stripeMat)
s.rotation.x = -Math.PI / 2
s.position.set(x, 0.01, z)
scene.add(s)
stripes.push(s)
})
}
// 我的车
myCar = makeCar('#00e5ff')
myCar.position.set(LANES[1], 0, 3.5)
scene.add(myCar)
// 护盾光环
shieldRing = new THREE.Mesh(
new THREE.TorusGeometry(1.5, 0.06, 10, 40),
new THREE.MeshBasicMaterial({ color: '#00e5ff' })
)
shieldRing.rotation.x = Math.PI / 2
shieldRing.visible = false
myCar.add(shieldRing)
}
function spawnTraffic() {
// 随机 1~2 辆占道(永远至少留一条通路)
const lanes = [0, 1, 2].sort(() => Math.random() - 0.5).slice(0, randInt(1, 2))
lanes.forEach((lane) => {
const car = makeCar(CAR_COLORS[randInt(0, CAR_COLORS.length - 1)])
car.position.set(LANES[lane], 0, spawnZ)
car.rotation.y = Math.PI // 对头方向摆放
scene.add(car)
traffic.push({ mesh: car, lane, z: spawnZ, speed: speed * 0.45 })
})
spawnZ -= randInt(14, 22)
}
function animate(ts) {
rafId = requestAnimationFrame(animate)
const dt = Math.min((ts - lastTs) / 1000 || 0.016, 0.05)
lastTs = ts
if (alive) {
if (invincible > 0) invincible -= dt
// 里程与提速
dist += speed * dt
speed = 26 + Math.min(30, dist * 0.02)
const score = Math.floor(dist)
emit('score', score)
// 我的车向目标车道平滑移动
const targetX = LANES[myLane]
myCar.position.x += (targetX - myCar.position.x) * Math.min(1, 10 * dt)
myCar.rotation.z = (targetX - myCar.position.x) * -0.08
// 车流相对接近
traffic.forEach((t) => {
t.z += (speed - t.speed) * dt
t.mesh.position.z = t.z
})
// 移除超过屏幕的车 & 补新车
traffic = traffic.filter((t) => {
if (t.z > 12) {
scene.remove(t.mesh)
return false
}
return true
})
if (!traffic.length || Math.max(...traffic.map((t) => t.z)) > -22) {
if (spawnZ > -60) spawnTraffic()
}
spawnZ = Math.min(spawnZ + speed * dt, -40)
// 虚线滚动(视觉速度感)
stripes.forEach((s) => {
s.position.z += speed * dt
if (s.position.z > 10) s.position.z -= 128
})
// 碰撞判定
if (invincible <= 0) {
for (const t of traffic) {
if (Math.abs(t.z - myCar.position.z) < 2.4 && Math.abs(LANES[t.lane] - myCar.position.x) < 1.4) {
if (shieldOn) {
shieldOn = false
shieldRing.visible = false
invincible = 1.2
scene.remove(t.mesh)
traffic = traffic.filter((x) => x !== t)
} else {
alive = false
emit('end', { score })
}
break
}
}
}
// 无敌闪烁
myCar.visible = invincible <= 0 || Math.floor(ts / 90) % 2 === 0
}
renderer.render(scene, camera)
}
// ---- 对外接口 ----
function start() {
traffic.forEach((t) => scene.remove(t.mesh))
traffic = []
myLane = 1
dist = 0
speed = 26
spawnZ = -40
shieldOn = false
invincible = 0
alive = true
myCar.position.set(LANES[1], 0, 3.5)
myCar.visible = true
shieldRing.visible = false
emit('score', 0)
}
function stop() {
cancelAnimationFrame(rafId)
keys?.detach()
renderer?.dispose()
}
// 复活:清空车流+2秒无敌护盾挡一次碰撞
function useProp(code) {
if (code === 'revive' && !alive) {
traffic.forEach((t) => scene.remove(t.mesh))
traffic = []
invincible = 2
alive = true
return true
}
if (code === 'shield' && alive && !shieldOn) {
shieldOn = true
shieldRing.visible = true
return true
}
return false
}
defineExpose({ start, stop, useProp })
onMounted(() => {
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(W, H)
holder.value.appendChild(renderer.domElement)
renderer.domElement.style.borderRadius = '10px'
renderer.domElement.style.maxWidth = '100%'
keys = new Keys()
keys.attach()
// 变道输入
keys.onPress((k) => {
if (!alive) return
if ((k === 'ArrowLeft' || k === 'a') && myLane > 0) myLane--
if ((k === 'ArrowRight' || k === 'd') && myLane < 2) myLane++
})
initScene()
animate(0)
})
onBeforeUnmount(stop)
</script>
<template>
<div ref="holder"></div>
</template>