Skip to content

200. 综合实战:开放世界(一)

Published:

不知道你有没有玩过开放世界的游戏。

比如这个夏日午后

2025-03-27 16.25.50.gif

它就是可以控制玩家在这个世界里自由探索,可以跑、跳。

而且玩家碰到障碍物会停下来,比如碰到石头。并且还可以跳到石头上。

那如何实现这种开放世界呢?

前面我们学了如何控制玩家实现漫游效果,而让玩家和障碍物的碰撞检测,一些物理效果,很明显要用 cannon 来做。

这节开始我们就实现一个这样的开放世界。

创建项目:

npx create-vite open-world

image.png

进入项目,安装依赖:

pnpm install
pnpm install --save three
pnpm install --save-dev @types/three

安装 cannon:

pnpm install --save cannon-es

先写下基础代码:

改下 src/main.js

import './style.css';
import * as THREE from 'three';
import {
    OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb);

scene.add(mesh);

scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const sun = new THREE.DirectionalLight(0xffffff, 0.8);
sun.position.set(20, 30, 10);
scene.add(sun);

const width = window.innerWidth;
const height = window.innerHeight;

const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 200);
camera.position.set(0, 1.6, 5);
camera.lookAt(0, 0, 0);

const renderer = new THREE.WebGLRenderer({
  antialias: true
});
renderer.setSize(width, height)

function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

document.body.append(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);

创建 Scene、Light、Camera、Renderer

改下 style.css

body {
    margin: 0;
}

然后创建 mesh.js

import * as THREE from 'three';

const group = new THREE.Group();

const groundSize = 100;
const groundGeo = new THREE.PlaneGeometry(groundSize, groundSize);
const groundMat = new THREE.MeshLambertMaterial({ color: 0x90a955 });
const groundMesh = new THREE.Mesh(groundGeo, groundMat);
groundMesh.rotation.x = -Math.PI / 2;
group.add(groundMesh);

export default group;

先画了下地面

跑一下:

npm run dev

image.png

2025-11-30 20.02.07.gif

然后画一些障碍物,比如玩家可以跳上去的那种。

image.png

function createBox(x, y, z, width, height, depth, color) {
    const geo = new THREE.BoxGeometry(width, height, depth);
    const mat = new THREE.MeshPhongMaterial({ color });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.set(x, y, z);
    group.add(mesh);
}
  
// 创建几个障碍物
createBox(5, 1, 0, 2, 2, 2, 0xff6b6b);
createBox(-5, 1.5, -5, 3, 3, 3, 0x4ecdc4);
createBox(0, 0.5, -10, 4, 1, 4, 0xffe66d);
createBox(8, 1, -8, 2, 2, 2, 0x95e1d3);
createBox(-8, 1.5, -3, 2, 3, 2, 0xf38181);

2025-11-30 20.06.28.gif

这个黄色的方块,玩家就可以跳上去。

然后画一圈围墙,把这个场景围起来,有点像进击的巨人里那种感觉,人走不出这个城墙。

image.png

// 创建墙壁(边界)
const wallHeight = 4;
const wallThickness = 0.5;

// 前墙
createBox(0, wallHeight / 2, groundSize / 2, groundSize, wallHeight, wallThickness, 0xd4c5a9);
// 后墙
createBox(0, wallHeight / 2, -groundSize / 2, groundSize, wallHeight, wallThickness, 0xd4c5a9);
// 左墙
createBox(-groundSize / 2, wallHeight / 2, 0, wallThickness, wallHeight, groundSize, 0xd4c5a9);
// 右墙
createBox(groundSize / 2, wallHeight / 2, 0, wallThickness, wallHeight, groundSize, 0xd4c5a9);

2025-11-30 20.08.53.gif

然后把玩家加载进来:

还是之前那个士兵模型:

https://github.com/mrdoob/three.js/blob/dev/examples/models/gltf/Soldier.glb

image.png

下载下来,放 public 目录:

image.png

然后加载进来播放 idle 动画:

image.png

let characterModel = null;
let mixer = null;
let idleAction = null;
let walkAction = null;

const gltfLoader = new GLTFLoader();
gltfLoader.load('./Soldier.glb', (gltf) => {
  characterModel = gltf.scene;
  characterModel.scale.setScalar(0.8);
  group.add(characterModel);

  mixer = new THREE.AnimationMixer(characterModel);

  idleAction = mixer.clipAction(gltf.animations[0]);
  walkAction = mixer.clipAction(gltf.animations[3]);

  idleAction.play();

  const clock = new THREE.Clock();
  function render() {
      requestAnimationFrame(render);

      const delta = clock.getDelta();
      mixer.update(delta);
  }
  render();
});

这些前面写过很多次,就不赘述了。

2025-11-30 20.15.45.gif

这样,开放世界的基本场景就搭建完成了。

案例代码上传了小册仓库

总结

这节我们搭了下开放世界的场景,包括玩家、立方体、墙壁。

下节开始我们加上控制玩家在开放世界内行走、跳跃。

评论