实现步骤
1.将Ammo导入 webpack使用Ammo.js - 在react中使用Ammo.js
2. 加载球架和场地模型
3. 针对模型中的每个物体的每个面进行Ammo物理构建
4. 控制相机移动
5. 创建篮球 设置篮球的球体Ammo数据
6. 按住键盘蓄力,抬起键盘投篮
代码仓库
难点:
Ammo.js对不规则形状模型添加检测
体验地址
代码:
/*
* @Author: hongbin
* @Date: 2023-03-08 10:59:25
* @LastEditors: hongbin
* @LastEditTime: 2023-03-10 20:00:30
* @Description:篮球游戏
*/
import Layout from "@/src/components/Three/Layout";
import { ThreeHelper } from "@/src/ThreeHelper";
import { KeyBoardListener } from "@/src/ThreeHelper/utils/KeyBoardListener";
import { createRef, FC, Fragment, useImperativeHandle, useState } from "react";
import styled from "styled-components";
import * as THREE from "three";
import { Object3D } from "three";
import Ammo from "../../../../src/ThreeHelper/physics/ammo.wasm";
import { AmmoPhysics } from "../../../../src/ThreeHelper/physics/AmmoPhysics";
interface IProps {}
const percentRef = createRef<{ setPercent: (p: number) => void }>();
const Physics: FC<IProps> = () => {
const [percent, setPercent] = useState(0);
useImperativeHandle(
percentRef,
() => ({
setPercent: (percent: number) => {
setPercent(percent);
},
}),
[]
);
return (
<Fragment>
<Layout
title={"篮球游戏 物理引擎 Ammo"}
init={init}
desc="使用three的Ammo助手-客户端渲染"
/>
<Bar percent={percent} />
</Fragment>
);
};
export default Physics;
const Bar = styled.div<{ percent: number }>`
width: 40vw;
height: 1vw;
position: fixed;
top: 80vh;
border: 1px solid ${(props) => (!!props.percent ? "#fff" : "transparent")};
left: 30vw;
transition: 0.3s;
::after {
content: "";
background: #df4b02;
height: 100%;
width: ${(props) => props.percent + "%"};
position: absolute;
}
`;
const init = (helper: ThreeHelper) => {
helper.addAxis();
helper.addStats();
helper.camera.position.set(0, 1, 2);
helper.controls.target.y += 1;
helper.frameByFrame();
helper.addGUI();
Ammo().then(async (Ammo: any) => {
const ammoPhysics = AmmoPhysics(Ammo);
const floor = createFloor();
helper.add(floor);
ammoPhysics.addMesh(floor, 0, { restitution: 0.1 });
await loadArea(helper, Ammo, ammoPhysics);
const [keyBoardControl, person] = controlledCamera(helper, ammoPhysics);
basketball(helper, Ammo, ammoPhysics);
const diff = new THREE.Vector3();
helper.animation(() => {
diff.copy(person.position);
const deltaTime = helper.clock.getDelta();
keyBoardControl.update();
ammoPhysics.stepSimulation(deltaTime, 10);
diff.sub(person.position);
helper.camera.position.sub(diff);
helper.controls.target.sub(diff);
});
});
};
class KeyBoardControl {
private vector = new THREE.Vector3();
private _call?: (v: Vector3) => void;
private scaled = 0.1;
private readonly moveCodeEvent = {
KeyW: (vec: Vector3) => {
vec.z = -this.scaled;
},
KeyS: (vec: Vector3) => {
vec.z = this.scaled;
},
KeyA: (vec: Vector3) => {
vec.x = -this.scaled;
},
KeyD: (vec: Vector3) => {
vec.x = this.scaled;
},
Space: (vec: Vector3) => {
vec.y = this.scaled;
},
};
private moveCode = Object.keys(this.moveCodeEvent) as Array<
keyof typeof this.moveCodeEvent
>;
private KeyBoardListener = new KeyBoardListener();
call(back: (v: Vector3) => void) {
this._call = back;
this.moveCode.forEach((code) => {
this.KeyBoardListener.listenKey(code, () => {});
});
this.KeyBoardListener.keyBoardListen();
}
update() {
if (this._call) {
this.vector.set(0, 0, 0);
this.moveCode.forEach((code) => {
if (this.KeyBoardListener.listenPool[code].isPress) {
this.moveCodeEvent[code](this.vector);
}
});
// if (this.vector.x || this.vector.y || this.vector.z) {
this._call(this.vector);
// }
}
}
}
function createFloor(): Mesh {
const width = 50;
const height = 2;
const depth = 50;
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(width, height, depth),
new THREE.MeshPhysicalMaterial({
color: new THREE.Color("#2f029f"),
metalness: 0.5,
roughness: 1,
transparent: true,
opacity: 0.4,
})
);
mesh.position.y = -2;
return mesh;
}
/**
* 控制相机移动
*/
function controlledCamera(
helper: ThreeHelper,
ammoPhysics: ReturnType<typeof AmmoPhysics>
): [KeyBoardControl, Mesh] {
helper.add(helper.camera);
const keyBoardControl = new KeyBoardControl();
const person = helper.generateRect({ width: 0.5, height: 1, depth: 0.2 });
// helper.add(person);
ammoPhysics.addMesh(person, 10, {
needMove: true,
});
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
keyBoardControl.call((v) => {
if (v.x || v.y || v.z) {
position.copy(person.position);
const angle = helper.controls.getAzimuthalAngle();
v.applyAxisAngle(Object3D.DefaultUp, angle);
position.add(v);
ammoPhysics.setMeshPosition(person, position);
ammoPhysics.setMeshQuaternion(person, quaternion);
}
});
return [keyBoardControl, person];
}
/**
* 加载场地
*/
async function loadArea(
helper: ThreeHelper,
ammo: any,
ammoPhysics: ReturnType<typeof AmmoPhysics>
) {
const gltf = await helper.loadGltf("/models/boll.glb");
helper.add(gltf.scene);
gltf.scene.traverse((obj) => {
//@ts-ignore
if (obj.isMesh) {
ammoPhysics.addMeshByTriangle(
obj as Mesh,
0,
{
restitution: 0.1,
friction: 1,
},
obj.name.includes("篮筐")
);
}
});
}
/**
* 篮球
*/
function basketball(
helper: ThreeHelper,
ammo: any,
ammoPhysics: ReturnType<typeof AmmoPhysics>
) {
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.1, 12, 12),
new THREE.MeshPhysicalMaterial({ color: 0x4411ff })
);
sphere.position.copy(helper.camera.position);
sphere.position.z -= 1;
sphere.position.y += 1;
ammoPhysics.addMesh(sphere, 1, { restitution: 1, needMove: true });
const dir = new THREE.Vector3();
let start = 0;
let duration = 0;
/**
* 按下Q键 根据时间决定力度
*/
const press = () => {
start == 0 && (start = performance.now());
duration = Math.min(100, (performance.now() - start) / 10);
percentRef.current?.setPercent(duration);
};
/**
* 抬起Q键
*/
const up = () => {
start = 0;
percentRef.current?.setPercent(0);
const p = helper.camera.position.clone();
ammoPhysics.setMeshPosition(sphere, p);
helper.camera.getWorldDirection(dir);
const body = sphere.userData.body;
dir.multiplyScalar(duration / 4);
body.setLinearVelocity(new ammo.btVector3(dir.x, dir.y * 1.4, dir.z));
};
helper.listenKey("KeyQ", press, up);
helper.add(sphere);
return sphere;
}