Three.js + Typescript + Ammo.js | A Tiny Shooter Game

按照计划,利用Three.js和Ammo.js制作一个物理小游戏. 通过 学习借鉴以下几个学习资源:

对3D和Three.js形成了一个初步的认识。

然后,基于https://github.com/hvidal/WebGL-Shooter项目,完成了一个类塔防的物理射击游戏。Github地址

QQ图片20220525225341.png


关键概念

两个主体世界

  • 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的方向上继续探索学习。

做难而正确的事情,让正确的事情持续发生。

Did you find this article valuable?

Support Lizhiyu by becoming a sponsor. Any amount is appreciated!