Skip to content

204. 综合实战:开放世界(五)

Published:

上节实现了跳跃:

2025-12-07 22.20.44.gif

2025-12-07 22.22.49.gif

这节我们来做更多的交互。

现实中人走到台阶会自动上台阶,但是现在我们得跳一下才能上去。

如何实现上台阶的效果呢?

最简单的方案就是视觉上是台阶,但物理世界的刚体用斜坡来实现。

我们先画一个台阶和平台:

image.png

function createStairs(x, y, z, stepCount, stepWidth, stepDepth, stepHeight, color, platformDepth = 2) {
    const stairsGroup = new THREE.Group();

    // 创建每一级台阶(实心结构)
    for (let i = 0; i < stepCount; i++) {
        // 当前台阶的总高度(
        const currentHeight = (i + 1) * stepHeight;

        // 创建实心台阶
        const stepGeo = new THREE.BoxGeometry(stepWidth, currentHeight, stepDepth);
        const stepMat = new THREE.MeshPhongMaterial({ color });
        const stepMesh = new THREE.Mesh(stepGeo, stepMat);

        // 台阶位置:高度中心在一半高度,Z轴在对应位置
        stepMesh.position.set(
            0,
            currentHeight / 2,
            i * stepDepth + stepDepth / 2
        );

        stepMesh.castShadow = true;
        stepMesh.receiveShadow = true;

        stairsGroup.add(stepMesh);
    }

    // 创建楼梯顶部的平台
    const totalStairsHeight = stepCount * stepHeight;
    const platformGeo = new THREE.BoxGeometry(stepWidth, totalStairsHeight, platformDepth);
    const platformMat = new THREE.MeshPhongMaterial({ color });
    const platformMesh = new THREE.Mesh(platformGeo, platformMat);

    platformMesh.position.set(
        0,
        totalStairsHeight / 2,
        stepCount * stepDepth + platformDepth / 2
    );

    platformMesh.castShadow = true;
    platformMesh.receiveShadow = true;

    stairsGroup.add(platformMesh);

    stairsGroup.position.set(x, y, z);
    group.add(stairsGroup);
}
  
// 创建楼梯
// createStairs(x, y, z, 台阶数量, 台阶宽度, 台阶深度, 台阶高度, 颜色, 平台深度)
createStairs(10, 0, 10, 10, 4, 0.5, 0.2, 0x8b7355, 10);

先看下效果再说:

2025-12-14 17.33.16.gif

可以看到,有一个台阶,然后上了台阶有个平台。

画这些自然要用 BoxGeometry。

过程比较简单:

循环画每一级台阶,并设置位置:

image.png

然后画上面那个平台:

image.png

现在只有视觉效果,物理世界里是没有的。

所以人现在可以穿过它:

2025-12-14 17.38.02.gif

接下来在物理世界添加台阶和平台的刚体。

我们直接用斜坡来做:

image.png

其实核心就是一个 Box,然后旋转下。

厚度很小,0.1

上面的高度就是台阶的数量 * 台阶高度。

image.png

平台的高度,就是台阶顶部的高度。

这里求斜边长度用到了勾股定理:

斜边² = 竖直边² + 水平边²

也就是这样:

image.png

求角度用反正切:

image.png

之后用这些算一下位置:

image.png

z 比较好理解,中心就是深度的一半。

y 需要计算斜坡长度在 Y 方向的投影的一半:

(rampLength / 2) * Math.sin(angle)

减去斜坡厚度在 Y 方向的投影的一半

(rampThickness / 2) * Math.cos(angle)

最后把这个 Box 画出来,旋转下:

image.png

// 物理效果:创建一个斜坡碰撞体(楼梯部分)
const totalDepth = stepCount * stepDepth;
const rampThickness = 0.1; // 斜坡的厚度

// 斜坡上端应该和平台表面高度一致
const totalHeight = totalStairsHeight; // 斜坡顶端和平台顶面平齐
const rampLength = Math.sqrt(totalHeight * totalHeight + totalDepth * totalDepth);

// 计算倾斜角度(相对于水平面)
const angle = Math.atan2(totalHeight, totalDepth);

// 计算斜坡中心位置
// Y坐标:考虑旋转后底端面要贴在地面
const rampCenterY = y + (rampLength / 2) * Math.sin(angle) - (rampThickness / 2) * Math.cos(angle);
// Z坐标:斜坡从地面起点(z)开始,上端到达平台前边缘(z + totalDepth)
const rampCenterZ = z + totalDepth / 2;

const rampBody = new CANNON.Body({
    mass: 0,
    position: new CANNON.Vec3(x, rampCenterY, rampCenterZ)
});

// 创建一个盒子作为斜坡
const rampShape = new CANNON.Box(new CANNON.Vec3(stepWidth / 2, rampThickness / 2, rampLength / 2));
rampBody.addShape(rampShape);

// 旋转斜坡:绕X轴旋转,使其倾斜
rampBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -angle);

world.addBody(rampBody);

试下效果:

2025-12-14 18.05.44.gif

这样,就能上台阶了。

当然,因为视觉上是台阶,但物理世界里是一个斜坡,所以效果不会特别真实,但是还行。

有同学说,这些三角函数的计算太麻烦了,那怎么办?

其实不用计算也行,还记得我们之前都怎么做么?

用 gui 可视化的调!

你可以把它可视化出来:

image.png

// 可视化物理斜坡(调试用)
const rampVisualGeo = new THREE.BoxGeometry(stepWidth, rampThickness, rampLength);
const rampVisualMat = new THREE.MeshPhongMaterial({
    color: 'green',
    transparent: true,
    opacity: 0.3,
    wireframe: false
});
const rampVisualMesh = new THREE.Mesh(rampVisualGeo, rampVisualMat);
rampVisualMesh.position.set(x, rampCenterY, rampCenterZ);
rampVisualMesh.rotation.x = -angle;
group.add(rampVisualMesh);

image.png 看到那个绿色立方体了么?

直接可视化调数值就行,比如位置、高度、斜坡长度,如果你不会算,就直接调数值!

案例代码上传了小册仓库

总结

这节我们实现了上楼梯的物理效果。

我们是用斜坡的方式来模拟实现的,就是一个 Box,倾斜一下。

整体还是比较真实。

但是数值计算会涉及到比较多三角函数,如果不理解,直接可视化调也可以。

评论