Files
gta6/three-player-controller/example/shooting/shooting.js
T
knsoandClaude Opus 5 fb38652e7f chore(controller): three-player-controller als Ordner statt Submodul
Das Submodul zeigte auf GitHub, der eingetragene Stand 028c652 lag aber nur
lokal vor: GitHub-Master ac39682 plus Showcase mit Figuren und launch.json.
Frische Klone konnten den Controller deshalb nicht laden. Jetzt liegt dieser
Stand als normaler Ordner im Repo, ohne Verweis auf GitHub.

dist/ und node_modules/ im Controller bleiben ignoriert und werden wie bisher
lokal gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 21:27:03 +02:00

295 lines
10 KiB
JavaScript

import * as THREE from "three";
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { KTX2Loader } from "three/examples/jsm/loaders/KTX2Loader.js";
import Stats from "three/examples/jsm/libs/stats.module.js";
import { createSceneSetup } from "./core/SceneSetup.js";
import { LocalPlayer } from "./player/LocalPlayer.js";
import { DecalSystem } from "./weapon/DecalSystem.js";
import { WeaponController, MODE } from "./weapon/WeaponController.js";
import { ShootingEffects } from "./weapon/effects.js";
import { HUD } from "./ui/HUD.js";
import { ZombieManager } from "./zombie/ZombieManager.js";
const base = import.meta.env.BASE_URL;
const initPos = new THREE.Vector3(33.21, 4.6, 2.6);
const NORMAL_MAX_CAM = 220;
const MOUSE_SENSITIVITY = 5;
const PLAYER_MODEL_URL = base + "./glb/swat.glb";
const FIRST_PERSON_PITCH_OFFSET_BY_MODEL = {
[base + "./glb/swat.glb"]: Math.PI * (16 / 180),
};
async function init() {
const container = document.querySelector("#container");
let tiles, localPlayer, weapon, zombieManager;
// ==================== 1. 场景基础 ====================
const { scene, renderer, camera, controls, bakeProbes } = createSceneSetup({ container });
// 添加音频监听器
const listener = new THREE.AudioListener();
camera.add(listener);
camera.position.copy(initPos);
camera.lookAt(initPos.x, initPos.y, initPos.z + 1);
controls.target.set(initPos.x, initPos.y, initPos.z + 1);
const stats = new Stats();
Object.assign(stats.dom.style, {
position: "fixed",
bottom: "0px",
left: "0px",
top: "auto",
zIndex: "9998",
display: "block",
});
document.body.appendChild(stats.dom);
const timer = new THREE.Timer();
let _prevGunEngaged = false;
renderer.setAnimationLoop(() => {
timer.update();
const elapsed = timer.getElapsed();
const dt = timer.getDelta();
if (localPlayer && weapon && zombieManager) {
const spineIK = localPlayer.spineIK;
const gunEngaged = weapon.isGunEngaged();
// 开火/瞄准时切 mouseMode 5(锁定朝向跟相机),其余情况用 1
if (gunEngaged !== _prevGunEngaged) {
localPlayer.setThirdMouseMode(gunEngaged ? 5 : 1);
_prevGunEngaged = gunEngaged;
}
// 恢复脊椎骨骼到上一帧的干净动画姿态
if (gunEngaged) spineIK.restoreBones();
// 驱动动画与物理(传入 dt,使上半身 mixer 与主 mixer 同步)
localPlayer ? localPlayer.update(dt) : controls.update();
// 应用脊椎 IK 与朝向(在 player.update() 后、weapon.update() 前)
if (gunEngaged) {
if (localPlayer.getIsFirstPerson()) {
localPlayer.applyHipsCorrection();
spineIK.applyAim1P(camera, localPlayer.pitchTarget1P);
} else {
// console.log('应用 3P IK');
localPlayer.applyHipsCorrection();
spineIK.applyAim3P(camera, true);
}
}
// 更新武器状态(特效、射线命中、连射节拍)——骨骼此时已含 IK
weapon.update(elapsed, dt);
// 丧尸 AI
zombieManager.update(dt, localPlayer.getPosition());
} else {
controls?.update();
}
renderer.render(scene, camera);
stats.update();
});
// ==================== 2. 资源加载器 ====================
const gltfLoader = new GLTFLoader();
const draco = new DRACOLoader();
draco.setDecoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/draco/");
gltfLoader.setDRACOLoader(draco);
const ktx2 = new KTX2Loader();
ktx2.setTranscoderPath("https://unpkg.com/three@0.180.0/examples/jsm/libs/basis/");
ktx2.detectSupport(renderer);
gltfLoader.setKTX2Loader(ktx2);
// ==================== 3. 场景模型 ====================
const { scene: mapScene } = await gltfLoader.loadAsync(
base + "./glb/horror_corridor.glb"
);
mapScene.name = "sceneGLB";
mapScene.scale.set(0.09, 0.09, 0.09);
scene.add(mapScene);
scene.updateMatrixWorld(true);
bakeProbes();
document.addEventListener("contextmenu", (e) => e.preventDefault());
const playerGltf = await gltfLoader.loadAsync(PLAYER_MODEL_URL);
// ==================== 4. 本地玩家 ====================
localPlayer = new LocalPlayer({ scene, camera, controls });
await localPlayer.init({
playerModelConfig: {
model: playerGltf.scene,
animations: playerGltf.animations,
scale: 0.01,
idleAnim: "idle1",
walkAnim: "walk",
runAnim: "run",
jumpAnim: "jump",
flyAnim: "flying",
flyIdleAnim: "flyidle",
headBoneName: "mixamorigHead",
firstPersonPitchOffset: FIRST_PERSON_PITCH_OFFSET_BY_MODEL[PLAYER_MODEL_URL] ?? 0,
rotateY: - Math.PI / 2,
},
initPos: initPos,
minCamDistance: 10,
maxCamDistance: NORMAL_MAX_CAM,
enableOverShoulderView: true,
mouseSensitivity: MOUSE_SENSITIVITY,
camLookAtHeightRatio: 0.9,
keyMap: { toggleFly: null },
colliders: [
{ motion: "static", shape: { kind: "mesh", source: mapScene } },
],
});
localPlayer.getPlayerModel()?.traverse((c) => {
if (c.isMesh) {
c.frustumCulled = false;
c.castShadow = true;
c.receiveShadow = true;
}
});
// ==================== 5. HUD ====================
const weaponSlots = [
{ key: "1", mode: "primary", label: "Rifle" },
{ key: "4", mode: "normal", label: "Fists" },
];
const hud = new HUD(weaponSlots);
hud.build();
// ==================== 6. 特效 ====================
let effects = new ShootingEffects(scene, {
listener,
flashScale: 0.15,
smokeSize: 0.8
});
await effects.load(
base + "./img/muzzle_flash.png",
base + "./img/smoke.png",
base + "./audio/gun_shot.mp3",
base + "./audio/reload.mp3",
);
// ==================== 7. 弹痕系统 ====================
const decalSystem = new DecalSystem(scene, 60, 0.25);
await decalSystem.loadMaterials(["img/bullet_hole.png"], base);
// ==================== 8. 丧尸管理器 ====================
zombieManager = new ZombieManager(scene, {
loader: gltfLoader,
colliders: localPlayer.getColliderMeshes(),
modelUrl: base + "./glb/zombie.glb",
scale: 0.01,
walkAnim: "walking",
idleAnim: "Idle",
punchAnim: "punching",
runAnim: "running",
rotateY: Math.PI,
speed: 120,
});
// ==================== 9. 武器控制器 ====================
weapon = new WeaponController({
scene,
camera,
localPlayer,
decalSystem,
effects,
hud,
zombieManager,
});
await weapon.load(gltfLoader, base);
weapon.setupAnimations();
weapon.bindInput();
weapon.switchMode(MODE.PRIMARY);
// 让 LocalPlayer 的 1P 俯仰驱动能感知武器状态
localPlayer.setGunEngagedGetter(() => weapon.isGunEngaged());
// 初始刷新武器槽高亮
hud.update(weapon.getMode());
// ==================== 10. 鼠标拾取 ====================
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const getMousePickingPoint = (event) => {
const rect = container.getBoundingClientRect();
// 将鼠标位置归一化为设备坐标 (NDC)
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 检测场景中所有的物体(包含 3D Tiles 和玩家模型)
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
return intersects[0].point;
}
return null;
};
container.addEventListener("mousedown", (e) => {
// 只有当鼠标没有被锁定(非第一人称控制状态)时才执行点击拾取
if (document.pointerLockElement !== renderer.domElement) {
const point = getMousePickingPoint(e);
if (point) {
console.log("鼠标点击的 3D 坐标:", point);
// 你可以在这里添加逻辑,比如在点击位置生成丧尸:
// zombieManager.spawnZombie(point);
}
}
});
// ==================== 丧尸波次 ====================
await Promise.all([
zombieManager.startWave({ origin: new THREE.Vector3(2.012, 4.6, -0.78), count: 5, radius: 1 }),
zombieManager.startWave({ origin: new THREE.Vector3(-3.731, 4.6, -1.824), count: 2, radius: 1 }),
zombieManager.startWave({ origin: new THREE.Vector3(11.911, 4.6, -4.010), count: 2, radius: 1 }),
zombieManager.startWave({ origin: new THREE.Vector3(13.567, 4.6, 7.001), count: 2, radius: 1 }),
]);
window.hideLoader?.();
// ==================== Start Screen ====================
await new Promise((resolve) => {
const screen = document.getElementById("start-screen");
screen.addEventListener("mousedown", (e) => e.stopPropagation());
screen.addEventListener("mouseup", (e) => e.stopPropagation());
screen.addEventListener("click", (e) => e.stopPropagation());
screen.style.display = "flex";
document.getElementById("start-btn").addEventListener("click", () => {
screen.remove();
document.body.style.cursor = "none";
document.body.requestPointerLock?.();
resolve();
}, { once: true });
});
// ==================== 窗口缩放 ====================
window.addEventListener("resize", () => {
const cont = document.querySelector("#container");
if (!cont) return;
camera.aspect = cont.clientWidth / cont.clientHeight;
renderer.setSize(cont.clientWidth, cont.clientHeight);
camera.updateProjectionMatrix();
});
}
init();