製作3D小汽車遊戲(下)

郭先生的部落格發表於2020-12-24

書接上回,這一節我們分模組說一說怎麼寫一個這樣的遊戲

1. 初始化場景、相機和渲染器

這幾乎是繪製three必須做的事情,我們有兩套場景和相機,一個是主場景和相機,另一個是小地圖的場景和相機(用來俯視建築和小汽車),渲染器設定一級曝光,輸出編碼設定為sRGBEncoding,程式碼如下。

scene = new THREE.Scene();
scene.background = new THREE.Color(0x8FBCD4);
scene.fog = new THREE.Fog(0x8FBCD4, 3000, 4000);

camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 10000);
camera.position.set(10,10,10);
            
scene2 = new THREE.Scene();
scene2.background = new THREE.Color(0xffffff);

camera2 = new THREE.OrthographicCamera(-400, 400, 400, -400, 1, 1000);
camera2.position.set(0, 1000, 0);
camera2.lookAt(0,0,0);

renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
this.$refs.box.appendChild(renderer.domElement);

2. 設定地面和建築

地面很簡單,就是一個plane

initGround() {
    const ground_geom = new THREE.PlaneBufferGeometry(8000, 8000);
    const ground_mate = new THREE.MeshLambertMaterial({color: 0xBCD48F, side: THREE.DoubleSide});
    const ground_mesh = new THREE.Mesh(ground_geom, ground_mate);
    ground_mesh.rotation.x = - Math.PI / 2;
    scene.add(ground_mesh);
},

設定建築,我們需要給每一個建築設定長寬高、顏色、位置,並把它們放到一個組裡,然後然需要給每一個建築初始化一個OBB,並把這些OBB資訊新增到一個陣列中,便於我們日後做碰撞檢測

initBuild(num) {
    let color = new THREE.Color();
    let build = new THREE.Group();
    for(let i=0; i<num; i++) {
        let w = Math.random() * 50 + 50;
        let h = Math.random() * 100 + 100;
        let d = Math.random() * 50 + 50;
        let x = Math.random() * 8000 - 4000;
        let z = Math.random() * 8000 - 4000;
        if((x * x + z * z) < Math.pow(140, 2)) {
            //40為車半長的估計值
            x = Math.pow(140, 2) / x;
            z = Math.pow(140, 2) / z;
        }
        let geometry = new THREE.BoxBufferGeometry(w, h, d);
        let material = new THREE.MeshStandardMaterial({color: new THREE.Color().setHSL(Math.random(), 1.0, 0.6)});
        let mesh = new THREE.Mesh(geometry, material);
        mesh.position.set(x, h / 2, z);
        build.add(mesh);
        let obb = new OBB();
        buildObbArray.push(obb.set(new THREE.Vector3(x, h / 2, z), new THREE.Vector3(w/2, h/2, d/2), new THREE.Matrix3()));
    }
    scene.add(build);
    scene2.add(build.clone());
},

3. 初始化小汽車

這裡我們要下載好一個小汽車的模型,首先把模型設定成我們想要的大小,這裡車高設定成10,其他維度等比例改變,然後找到方向盤,輪子等部分,新增到全域性的組中,便於我們控制。

initCar() {
    const shadowTexture = new THREE.TextureLoader().load('/static/gltf/super_car/super_car_ao.png');
    const loader = new GLTFLoader();
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath('/static/gltf/');
    loader.setDRACOLoader(dracoLoader);
    loader.load('/static/gltf/super_car/super_car.glb', gltf => {
        const model = gltf.scene.children[0];
        model.rotation.y = -Math.PI / 2;
        steering_wheel = model.getObjectByName('steering_wheel');[]
        
        const shadow = new THREE.Mesh(
            new THREE.PlaneBufferGeometry( 0.655 * 4, 1.3 * 4 ),
            new THREE.MeshBasicMaterial( {
                map: shadowTexture, blending: THREE.MultiplyBlending, toneMapped: false, transparent: true
            } )
        );
        shadow.position.y = 0.1;
        shadow.rotation.x = - Math.PI / 2;
        model.add(shadow);

        const size = new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3());
        model.scale.copy(new THREE.Vector3().addScalar(carHeight / size.y));
        carHalfSize = new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3()).multiplyScalar(0.4);
        
        car.add(model);
        tyreArray.push(car.getObjectByName('wheel_fl'),car.getObjectByName('wheel_fr'),car.getObjectByName('wheel_rl'),car.getObjectByName('wheel_rr'));
        car.userData.obb = new OBB(new THREE.Vector3(0,5,0), carHalfSize, new THREE.Matrix3());
        scene.add(car);
        orthoCar = new THREE.Mesh(new THREE.SphereBufferGeometry(20, 20), new THREE.MeshBasicMaterial({color: 0xff0000, side: THREE.DoubleSide}));
        orthoCar.rotation.x = - Math.PI / 2;
        scene2.add(orthoCar);
    } );
},

4. 新增事件、轉彎、增減速和切換視角

這裡我們主要使用q–切換視角,a,d–轉彎,w,s–加減速。

document.addEventListener('keypress', event => {
    if(event.key == 'd') {
        this.turn(0);
    } else if(event.key == 'a') {
        this.turn(1);
    } else if(event.key == 'w') {
        this.speed(1);
    } else if(event.key == 's') {
        this.speed(0)
    } else if(event.key == 'q') {
        view = view == 0 ? 1 : 0;
    }
})

對於速度的控制,sp代表左右方向

speed(sp) {
    if(sp == 0 && speed > 0) {
        speed -= 2;
    } else if(sp == 0 && speed > -10) {
        speed -= 0.5;
    } else if(sp == 1 && speed < 40) {
        speed += 0.5;
    }
},

對於轉彎的控制,我們用多段控制模擬非線性

turn(direct) {
    //模擬非線性轉向
    if(direct == 0 && rotateTyre > -rotateMax * 0.5) {
        rotateTyre -= 0.02;
    } else if (direct == 0 && rotateTyre > -rotateMax * 0.8) {
        rotateTyre -= 0.04;
    } else if (direct == 0 && rotateTyre > -rotateMax) {
        rotateTyre -= 0.06;
    } else if(direct == 1 && rotateTyre < rotateMax * 0.5) {
        rotateTyre += 0.02;
    } else if(direct == 1 && rotateTyre < rotateMax * 0.8) {
        rotateTyre += 0.04;
    } else if(direct == 1 && rotateTyre < rotateMax) {
        rotateTyre += 0.06;
    }
    tyreArray[0].rotation.y = rotateTyre;
    tyreArray[1].rotation.y = rotateTyre;
    //方向盤
    steering_wheel.rotation.y = - rotateTyre;
},

5. 渲染

因為我們有兩個場景要渲染,這裡就選擇渲染兩次

render() {
    stats.update();
    this.run();
    renderer.setScissor( 0, 0, window.innerWidth, window.innerWidth );
    renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
    renderer.setScissorTest(true);
    renderer.render( scene, camera );
    renderer.setScissor( 0, 0, window.innerHeight/4, window.innerHeight/4 );
    renderer.setViewport( 0, 0, window.innerHeight/4, window.innerHeight/4);
    renderer.setScissorTest(true);
    renderer.render( scene2, camera2 );
    this.globalID = requestAnimationFrame(this.render);
}

run方法裡面控制著車的角度,車子的位置,輪子的傳動,相機的位置,相機的lookAt,以及碰撞檢測,這裡面有我們上一節複習的有向包圍盒OBB和尤拉角的使用

run() {
    let delta = - clock.getDelta();
    //輪胎轉動∝速度
    tyreArray.forEach(d => d.rotation.copy(new THREE.Euler(delta * speed + d.rotation.x, d.rotation.y, d.rotation.z, 'ZYX')));
    //rotateOffset 旋轉偏移量  rotateTyre輪胎偏轉  rotateCorrection偏轉系數  speed車速
    let rotateOffset = Math.sin(rotateTyre) * rotateCorrection * speed;
    //rotateRun 旋轉偏移總量
    rotateRun += rotateOffset;
    //rotateVector 車前進方向向量(不斷乘offset得到)
    rotateVector.applyAxisAngle(new THREE.Vector3(0,1,0), rotateOffset);
    //車x和z方向增加量 ∝車速
    car.position.x += speed * speedCorrection * rotateVector.x;
    car.position.z += speed * speedCorrection * rotateVector.z;
    camera2.position.set(car.position.x, 1000, car.position.z);
    camera2.lookAt(car.position.x, 10, car.position.z);
    orthoCar.position.copy(car.position);
    //車身旋轉 使用 旋轉偏移總量rotateRun
    car.rotation.y = rotateRun;
    //切換視角
    if(view == 0) {
        camera.position.set(car.position.x - 3 * Math.sin(rotateRun), 8, car.position.z - 3 * Math.cos(rotateRun));
        camera.lookAt(camera.position.x + Math.cos(rotateRun), 8, camera.position.z - Math.sin(rotateRun));
    } else {
        camera.position.set(car.position.x + 50 * Math.cos(rotateRun + Math.PI * 0.9), 20, car.position.z - 50 * Math.sin(rotateRun + Math.PI * 0.9));
        camera.lookAt(camera.position.x + Math.cos(rotateRun), 19.9, camera.position.z - Math.sin(rotateRun));
    }
    //判斷是否碰撞
    car.userData.obb.set(car.position, carHalfSize, new THREE.Matrix3().setFromMatrix4(car.matrixWorld));
    const obb = car.userData.obb;
    for(let i=0; i<buildObbArray.length; i++) {
        const obbTest = buildObbArray[i];
        if(obb.intersectsOBB(obbTest) === true) {
            speed = 0;
        }
    }
},

這裡我們直接遍歷建築的OBB陣列然後通過intersectsOBB方法,判斷是否相撞就可以了。

 

轉載請註明地址:郭先生的部落格

相關文章