Three.js + Typescript + Ammo.js | A Tiny Shooter Game
Published on 23 July 2021
4 min read

Three.js + Typescript + Ammo.js | A Tiny Shooter Game
按照计划,利用Three.js和Ammo.js制作一个物理小游戏. 通过 学习借鉴以下几个学习资源:
对3D和Three.js形成了一个初步的认识。
然后,基于https://github.com/hvidal/WebGL-Shooter项目,完成了一个类塔防的物理射击游戏。Github地址

关键概念
两个主体世界
- Three.js的视图世界
THREE.Scene
- Ammo.js的物理世界
Ammo.btDiscreteDynamicsWorld
通过设置THREE.Object3D 的userData.physicsBody为Ammo.btRigidBody,即使Three.js的物体添加到物理世界。
每帧更新物理世界的坐标,旋转等各项数据到视图世界,完成位移碰撞的及时调整。
for (let i = 0; i < len; i++) {
var objThree = this.rigidBodies[i];
var motionState = objThree.userData.physicsBody.getMotionState();
if (motionState) {
motionState.getWorldTransform(this.tempTransform);
let p = this.tempTransform.getOrigin();
objThree.position.set(p.x(), p.y(), p.z());
let q = this.tempTransform.getRotation();
objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());
}
}
基础内容
-创建Three.js场景
this.renderer = new THREE.WebGLRenderer();
this.renderer.setClearColor(clearColor);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth, window.innerHeight);
element.appendChild(this.renderer.domElement);
this.scene = new THREE.Scene();
- 创建Ammo.js物理世界
const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new Ammo.btAxisSweep3(new Ammo.btVector3(-1000, -1000, -1000), new Ammo.btVector3(1000, 1000, 1000));
const solver = new Ammo.btSequentialImpulseConstraintSolver();
this.physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, overlappingPairCache, solver, collisionConfiguration);
this.physicsWorld.setGravity(new Ammo.btVector3(0, -16, 0));
- 射击逻辑
shoot() {
this.raycaster.setFromCamera(this.screenCenter, this.camera);
this.pos.copy(this.raycaster.ray.direction);
this.pos.add(this.raycaster.ray.origin);
this.pos.setZ(this.pos.z - 10);
const ball = this.factory.createSphere(this.radius, this.mass, this.pos, this.quat, this.ballMaterial);
ball.castShadow = true;
ball.receiveShadow = true;
const body = ball.userData.physicsBody;
this.pos.copy(this.raycaster.ray.direction);
this.pos.multiplyScalar(160);
//调整子弹速度
body.setLinearVelocity(new Ammo.btVector3(this.pos.x, this.pos.y, this.pos.z));
}
增加内容:
- 连击实现
//鼠标按下之后,间隔时间超过0.2秒即射击一次
if (isMouseDowning && duration > 0.2) {
duration = 0;
mouseShooter.shoot();
}
- 计时作为最终的游戏得分
function recordTotalTime() {
if (beginTime == 0) {
beginTime = new Date().getTime();
} else {
let _currentTime = new Date().getTime();
totalTime += (_currentTime - beginTime);
document.getElementById('scoreBar').innerHTML = Math.floor(totalTime / 1000) + "." + totalTime % 1000;
beginTime = _currentTime;
}
}
- 游戏结束判断(物体在斜坡底部掉落)
//斜坡底部的z坐标值计算
var edgeZ = Math.cos(groundRotationX) * groundScaleZ / 2;
//判断斜坡上的物体的z轴坐标是否大于斜坡底部的z坐标值
private checkGameOver(controls, edgeZ): boolean {
const len = this.rigidBodies_slope.length;
for (let i = 0; i < len; i++) {
var objThree = this.rigidBodies_slope[i];
if (objThree.position.z > edgeZ) {
controls.enabled = false;
const message = document.getElementById('message');
const blocker = document.getElementById('blocker');
const gameOver = document.getElementById('gameOver');
blocker.style.display = 'none';
message.style.display = 'none';
gameOver.style.display = 'block';
lockPointer(controls);
document.getElementById('score').innerHTML = document.getElementById('scoreBar').innerHTML;
this.isGameOver = true;
return true;
}
}
}
没想到用three.js和ammo.js做一个小DEMO会花这么多时间去学习,通过这个学习的过程也了解到了3D和物理相关的知识在广度和深度上都是值得去探索的,后面我也会继续在web3D的方向上继续探索学习。
做难而正确的事情,让正确的事情持续发生。