๐ŸŽฎArcadeLab

Open World 3D Prototype

by ChromeBuilder44
1042 lines16.1 KB๐Ÿ› ๏ธ Three.js (3D graphics)
โ–ถ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Open World 3D Prototype</title>

<style>
    * {
        box-sizing: border-box;
    }

    body {
        margin: 0;
        overflow: hidden;
        background: #000;
        font-family: Arial, sans-serif;
    }

    #hud {
        position: fixed;
        top: 20px;
        left: 20px;
        z-index: 10;
        color: white;
        background: rgba(0,0,0,.45);
        padding: 12px 16px;
        border-radius: 10px;
        backdrop-filter: blur(8px);
    }

    #speed {
        font-size: 22px;
        font-weight: bold;
    }

    #help {
        margin-top: 5px;
        font-size: 13px;
        opacity: .8;
    }

    #crosshair {
        position: fixed;
        left: 50%;
        top: 50%;
        width: 6px;
        height: 6px;
        transform: translate(-50%, -50%);
        border: 1px solid white;
        border-radius: 50%;
        z-index: 5;
    }
</style>
</head>

<body>

<div id="hud">
    <div id="speed">0 km/h</div>
    <div id="help">
        W/S = accelerate/brake ยท A/D = steer ยท Space = handbrake
    </div>
</div>

<div id="crosshair"></div>

<script type="module">

import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";

import { OrbitControls } from
"https://cdn.jsdelivr.net/npm/three@0.180.0/examples/jsm/controls/OrbitControls.js";

/* =========================================================
   ENGINE
========================================================= */

const scene = new THREE.Scene();

scene.background = new THREE.Color(0x87b8e6);

scene.fog = new THREE.FogExp2(
    0x87b8e6,
    0.0022
);

const camera = new THREE.PerspectiveCamera(
    70,
    innerWidth / innerHeight,
    0.1,
    2500
);

camera.position.set(0, 8, 15);

const renderer = new THREE.WebGLRenderer({
    antialias: true,
    powerPreference: "high-performance"
});

renderer.setSize(innerWidth, innerHeight);

renderer.setPixelRatio(
    Math.min(devicePixelRatio, 2)
);

renderer.shadowMap.enabled = true;
renderer.shadowMap.type =
    THREE.PCFSoftShadowMap;

renderer.outputColorSpace =
    THREE.SRGBColorSpace;

renderer.toneMapping =
    THREE.ACESFilmicToneMapping;

renderer.toneMappingExposure = 1.15;

document.body.appendChild(renderer.domElement);


/* =========================================================
   LIGHTING
========================================================= */

const sun = new THREE.DirectionalLight(
    0xffffff,
    4
);

sun.position.set(
    200,
    300,
    150
);

sun.castShadow = true;

sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;

sun.shadow.camera.left = -500;
sun.shadow.camera.right = 500;
sun.shadow.camera.top = 500;
sun.shadow.camera.bottom = -500;

scene.add(sun);


const ambient = new THREE.HemisphereLight(
    0xbfdcff,
    0x35402d,
    1.5
);

scene.add(ambient);


/* =========================================================
   GROUND
========================================================= */

const groundGeometry =
    new THREE.PlaneGeometry(
        2000,
        2000
    );

const groundMaterial =
    new THREE.MeshStandardMaterial({
        color: 0x385b35,
        roughness: 1
    });

const ground =
    new THREE.Mesh(
        groundGeometry,
        groundMaterial
    );

ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;

scene.add(ground);


/* =========================================================
   ROAD SYSTEM
========================================================= */

function createRoad(
    x,
    z,
    width,
    depth
) {

    const geometry =
        new THREE.BoxGeometry(
            width,
            0.08,
            depth
        );

    const material =
        new THREE.MeshStandardMaterial({
            color: 0x252525,
            roughness: 0.9
        });

    const road =
        new THREE.Mesh(
            geometry,
            material
        );

    road.position.set(
        x,
        0.04,
        z
    );

    road.receiveShadow = true;

    scene.add(road);
}


/* Main streets */

for (
    let x = -500;
    x <= 500;
    x += 100
) {

    createRoad(
        x,
        0,
        32,
        1000
    );
}


for (
    let z = -500;
    z <= 500;
    z += 100
) {

    createRoad(
        0,
        z,
        1000,
        32
    );
}


/* =========================================================
   BUILDINGS
========================================================= */

function random(min, max) {
    return Math.random() *
        (max - min) + min;
}


function createBuilding(x, z) {

    const width =
        random(20, 55);

    const depth =
        random(20, 55);

    const height =
        random(15, 110);

    const geometry =
        new THREE.BoxGeometry(
            width,
            height,
            depth
        );

    const material =
        new THREE.MeshStandardMaterial({
            color: new THREE.Color(
                random(.15, .5),
                random(.15, .5),
                random(.15, .5)
            ),
            roughness: .72,
            metalness: .05
        });

    const building =
        new THREE.Mesh(
            geometry,
            material
        );

    building.position.set(
        x,
        height / 2,
        z
    );

    building.castShadow = true;
    building.receiveShadow = true;

    scene.add(building);

    /* rooftop */

    const roof =
        new THREE.Mesh(
            new THREE.BoxGeometry(
                width * .85,
                1,
                depth * .85
            ),
            new THREE.MeshStandardMaterial({
                color: 0x333333
            })
        );

    roof.position.set(
        x,
        height + .5,
        z
    );

    roof.castShadow = true;

    scene.add(roof);
}


/* City blocks */

for (
    let x = -450;
    x <= 450;
    x += 70
) {

    for (
        let z = -450;
        z <= 450;
        z += 70
    ) {

        /*
          Leave space around roads.
        */

        if (
            Math.abs(x % 100) > 30 &&
            Math.abs(z % 100) > 30
        ) {
            createBuilding(
                x + random(-15, 15),
                z + random(-15, 15)
            );
        }
    }
}


/* =========================================================
   TREES
========================================================= */

function createTree(x, z) {

    const trunk =
        new THREE.Mesh(
            new THREE.CylinderGeometry(
                1.2,
                1.5,
                8,
                8
            ),
            new THREE.MeshStandardMaterial({
                color: 0x60402a
            })
        );

    trunk.position.set(
        x,
        4,
        z
    );

    trunk.castShadow = true;

    scene.add(trunk);


    const leaves =
        new THREE.Mesh(
            new THREE.SphereGeometry(
                5,
                12,
                10
            ),
            new THREE.MeshStandardMaterial({
                color: 0x246b2a,
                roughness: 1
            })
        );

    leaves.position.set(
        x,
        10,
        z
    );

    leaves.castShadow = true;

    scene.add(leaves);
}


for (
    let i = 0;
    i < 120;
    i++
) {

    const x =
        random(-480, 480);

    const z =
        random(-480, 480);

    /*
      Avoid placing too many trees
      directly on major roads.
    */

    if (
        Math.abs(x % 100) > 20 &&
        Math.abs(z % 100) > 20
    ) {
        createTree(x, z);
    }
}


/* =========================================================
   CAR
========================================================= */

const car =
    new THREE.Group();


/* Body */

const body =
    new THREE.Mesh(
        new THREE.BoxGeometry(
            4.2,
            1.2,
            7
        ),
        new THREE.MeshStandardMaterial({
            color: 0x8b1010,
            metalness: .65,
            roughness: .25
        })
    );

body.position.y = 1.35;

body.castShadow = true;

car.add(body);


/* Cabin */

const cabin =
    new THREE.Mesh(
        new THREE.BoxGeometry(
            3.4,
            1.25,
            3.3
        ),
        new THREE.MeshPhysicalMaterial({
            color: 0x10151b,
            metalness: .1,
            roughness: .05,
            transmission: .05,
            transparent: true,
            opacity: .9
        })
    );

cabin.position.set(
    0,
    2.15,
    -.35
);

cabin.castShadow = true;

car.add(cabin);


/* Wheels */

const wheels = [];

function createWheel(x, z) {

    const wheel =
        new THREE.Mesh(
            new THREE.CylinderGeometry(
                .85,
                .85,
                .55,
                20
            ),
            new THREE.MeshStandardMaterial({
                color: 0x111111,
                roughness: .85
            })
        );

    wheel.rotation.z =
        Math.PI / 2;

    wheel.position.set(
        x,
        .85,
        z
    );

    wheel.castShadow = true;

    car.add(wheel);

    wheels.push(wheel);
}


createWheel(-2.1, -2.3);
createWheel(2.1, -2.3);
createWheel(-2.1, 2.3);
createWheel(2.1, 2.3);


car.position.set(
    0,
    0,
    0
);

scene.add(car);


/* =========================================================
   VEHICLE PHYSICS
========================================================= */

let speed = 0;

let steering = 0;

const keys = {};


addEventListener(
    "keydown",
    e => keys[e.code] = true
);

addEventListener(
    "keyup",
    e => keys[e.code] = false
);


function updateCar(dt) {

    const accelerating =
        keys["KeyW"];

    const braking =
        keys["KeyS"];

    const handbrake =
        keys["Space"];


    /* Acceleration */

    if (accelerating) {

        speed +=
            28 * dt;

    }

    else if (braking) {

        speed -=
            20 * dt;

    }

    else {

        /*
          Natural rolling resistance.
        */

        speed *=
            Math.pow(.985, dt * 60);
    }


    /* Speed limits */

    speed =
        THREE.MathUtils.clamp(
            speed,
            -12,
            70
        );


    /* Steering */

    let targetSteering = 0;

    if (keys["KeyA"])
        targetSteering = 1;

    if (keys["KeyD"])
        targetSteering = -1;


    steering +=
        (
            targetSteering -
            steering
        ) * 8 * dt;


    /*
      Steering becomes more effective
      at higher speeds.
    */

    const steeringStrength =
        Math.min(
            Math.abs(speed) / 20,
            1
        );


    car.rotation.y +=
        steering *
        steeringStrength *
        speed *
        .012 *
        dt *
        60;


    /* Handbrake */

    if (handbrake) {

        speed *=
            Math.pow(.92, dt * 60);
    }


    /* Forward movement */

    const direction =
        new THREE.Vector3(
            0,
            0,
            -1
        );

    direction.applyQuaternion(
        car.quaternion
    );

    car.position.addScaledVector(
        direction,
        speed * dt
    );


    /* Wheel animation */

    wheels.forEach(
        wheel => {
            wheel.rotation.x +=
                speed * dt * 2;
        }
    );


    /*
      Keep player inside world.
    */

    car.position.x =
        THREE.MathUtils.clamp(
            car.position.x,
            -950,
            950
        );

    car.position.z =
        THREE.MathUtils.clamp(
            car.position.z,
            -950,
            950
        );


    document.getElementById(
        "speed"
    ).textContent =
        Math.round(
            Math.abs(speed) * 3.6
        ) + " km/h";
}


/* =========================================================
   THIRD PERSON CAMERA
========================================================= */

const cameraOffset =
    new THREE.Vector3(
        0,
        6,
        13
    );


function updateCamera(dt) {

    const desired =
        cameraOffset.clone();

    desired.applyQuaternion(
        car.quaternion
    );

    desired.add(
        car.position
    );


    camera.position.lerp(
        desired,
        1 - Math.pow(
            .001,
            dt
        )
    );


    const lookAt =
        car.position.clone();

    lookAt.y += 1.5;

    camera.lookAt(
        lookAt
    );
}


/* =========================================================
   SIMPLE NPC SYSTEM
========================================================= */

const pedestrians = [];


function createPedestrian() {

    const person =
        new THREE.Group();


    const body =
        new THREE.Mesh(
            new THREE.CapsuleGeometry(
                .45,
                1.2,
                6,
                12
            ),
            new THREE.MeshStandardMaterial({
                color:
                    new THREE.Color(
                        random(.2, .8),
                        random(.2, .8),
                        random(.2, .8)
                    )
            })
        );

    body.position.y = 1.2;

    body.castShadow = true;

    person.add(body);


    person.position.set(
        random(-450, 450),
        0,
        random(-450, 450)
    );


    person.userData.direction =
        Math.random() *
        Math.PI * 2;

    person.userData.speed =
        random(.8, 2.2);


    scene.add(person);

    pedestrians.push(person);
}


for (
    let i = 0;
    i < 80;
    i++
) {
    createPedestrian();
}


function updatePedestrians(dt) {

    pedestrians.forEach(
        p => {

            const direction =
                new THREE.Vector3(
                    Math.sin(
                        p.userData.direction
                    ),
                    0,
                    Math.cos(
                        p.userData.direction
                    )
                );


            p.position.addScaledVector(
                direction,
                p.userData.speed * dt
            );


            /*
              Occasionally change direction.
            */

            if (
                Math.random() < .003
            ) {

                p.userData.direction +=
                    random(-1, 1);
            }


            /*
              Keep NPCs inside world.
            */

            if (
                Math.abs(p.position.x) > 480 ||
                Math.abs(p.position.z) > 480
            ) {

                p.userData.direction +=
                    Math.PI;
            }
        }
    );
}


/* =========================================================
   DAY/NIGHT SYSTEM
========================================================= */

let worldTime = 12;


function updateTime(dt) {

    worldTime +=
        dt * .15;

    if (worldTime >= 24)
        worldTime = 0;


    const sunAngle =
        (worldTime / 24) *
        Math.PI * 2;


    sun.position.x =
        Math.cos(sunAngle) * 400;

    sun.position.y =
        Math.sin(sunAngle) * 400;


    const daylight =
        Math.max(
            .08,
            Math.sin(sunAngle)
        );


    sun.intensity =
        daylight * 4;


    ambient.intensity =
        .35 + daylight;


    /*
      Slightly darken the sky at night.
    */

    const sky =
        new THREE.Color();

    sky.setHSL(
        .58,
        .55,
        .25 + daylight * .35
    );

    scene.background = sky;

    scene.fog.color.copy(
        sky
    );
}


/* =========================================================
   GAME LOOP
========================================================= */

const clock =
    new THREE.Clock();


function animate() {

    requestAnimationFrame(
        animate
    );


    const dt =
        Math.min(
            clock.getDelta(),
            .05
        );


    updateCar(dt);

    updateCamera(dt);

    updatePedestrians(dt);

    updateTime(dt);


    renderer.render(
        scene,
        camera
    );
}


animate();


/* =========================================================
   RESIZE
========================================================= */

addEventListener(
    "resize",
    () => {

        camera.aspect =
            innerWidth /
            innerHeight;

        camera.updateProjectionMatrix();

        renderer.setSize(
            innerWidth,
            innerHeight
        );
    }
);

</script>

</body>
</html>

Game Source: Open World 3D Prototype

Creator: ChromeBuilder44

Libraries: three

Complexity: complex (1042 lines, 16.1 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: open-world-3d-prototype-chromebuilder44" to link back to the original. Then publish at arcadelab.ai/publish.