Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do my Three.js game camera controls glitch / not work

I have made a game, and everything works, except when I move the camera with my mouse, everything duplicates in my view. I see a quickly alternating version alternating between my original camera view and my new camera view.

I made this code, and I have tried to make the camera update and change it instead of duplicating it, but it doesn't work. Here is my broken code so far:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D Platformer</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // Create a scene
        var scene = new THREE.Scene();

        // Create a camera with increased FOV
        var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);

        // Create a renderer
        var renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        // Create a large platform
        var platformGeometry = new THREE.BoxGeometry(20, 1, 20);
        var platformMaterial = new THREE.MeshBasicMaterial({ color: 0xaaaaaa });
        var platform = new THREE.Mesh(platformGeometry, platformMaterial);
        platform.position.y = -1;
        scene.add(platform);

        // Create a player
        var playerGeometry = new THREE.BoxGeometry(1, 1, 1);
        var playerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
        var player = new THREE.Mesh(playerGeometry, playerMaterial);
        scene.add(player);

        // Create additional cubes
        var cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
        var cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        for (var i = 0; i < 5; i++) {
            var cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
            cube.position.set(Math.random() * 18 - 9, 0.5, Math.random() * 18 - 9);
            scene.add(cube);
        }

        // Create an AI that runs around randomly
        var aiGeometry = new THREE.BoxGeometry(1, 1, 1);
        var aiMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
        var ai = new THREE.Mesh(aiGeometry, aiMaterial);
        ai.position.set(0, 0.5, -5); // Initial position
        scene.add(ai);

        // AI movement variables
        var aiSpeed = 0.05;
        var aiDirection = new THREE.Vector3(Math.random() * 2 - 1, 0, Math.random() * 2 - 1).normalize();
        var aiJumpSpeed = 0.2;
        var aiVelocityY = 0;
        var aiOnGround = true;

        // Player controls
        var playerSpeed = 0.1;
        var jumpSpeed = 0.2;
        var velocityY = 0;
        var onGround = true;

        var keys = {
            w: false,
            a: false,
            s: false,
            d: false,
            space: false
        };

        window.addEventListener('keydown', function(event) {
            if (keys.hasOwnProperty(event.key)) keys[event.key] = true;
        });

        window.addEventListener('keyup', function(event) {
            if (keys.hasOwnProperty(event.key)) keys[event.key] = false;
        });

        // Mouse controls
        var mouse = {
            isDragging: false,
            previousX: 0,
            previousY: 0,
            deltaX: 0,
            deltaY: 0
        };

        document.addEventListener('mousedown', function(event) {
            mouse.isDragging = true;
            mouse.previousX = event.clientX;
            mouse.previousY = event.clientY;
        });

        document.addEventListener('mouseup', function() {
            mouse.isDragging = false;
        });

        document.addEventListener('mousemove', function(event) {
            if (mouse.isDragging) {
                mouse.deltaX = event.clientX - mouse.previousX;
                mouse.deltaY = event.clientY - mouse.previousY;

                camera.rotation.y -= mouse.deltaX * 0.002;
                camera.rotation.x -= mouse.deltaY * 0.002;

                camera.rotation.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.rotation.x)); // Prevent flipping

                mouse.previousX = event.clientX;
                mouse.previousY = event.clientY;
            }
        });

        // Animation loop
        function animate() {
            requestAnimationFrame(animate);

            // Player movement
            if (keys.w) player.position.z -= playerSpeed;
            if (keys.a) player.position.x -= playerSpeed;
            if (keys.s) player.position.z += playerSpeed;
            if (keys.d) player.position.x += playerSpeed;

            // Prevent jumping off the edge
            player.position.x = Math.max(-9.5, Math.min(9.5, player.position.x));
            player.position.z = Math.max(-9.5, Math.min(9.5, player.position.z));

            // Player jumping
            if (keys.space && onGround) {
                velocityY = jumpSpeed;
                onGround = false;
            }

            player.position.y += velocityY;
            velocityY -= 0.01; // Gravity effect

            if (player.position.y <= 0) {
                player.position.y = 0;
                onGround = true;
            }

            // AI random movement and jumping
            ai.position.add(aiDirection.clone().multiplyScalar(aiSpeed));
            if (Math.random() < 0.05 && aiOnGround) { // Increased jump frequency
                aiVelocityY = aiJumpSpeed;
                aiOnGround = false;
            }

            ai.position.y += aiVelocityY;
            aiVelocityY -= 0.01; // Gravity effect

            if (ai.position.y <= 0) {
                ai.position.y = 0;
                aiOnGround = true;
            }

            // Avoid getting stuck with AI
            var distance = player.position.distanceTo(ai.position);
            if (distance < 1) {
                var pushDirection = new THREE.Vector
                                Vector3().subVectors(player.position, ai.position).normalize();
                player.position.add(pushDirection.multiplyScalar(0.1));
            }

            // Position camera
            camera.position.set(player.position.x, player.position.y + 1.5, player.position.z);
            camera.lookAt(player.position.x + Math.sin(camera.rotation.y), player.position.y + 1.5, player.position.z - Math.cos(camera.rotation.y));

            renderer.render(scene, camera);
        }
        animate();
    </script>
</body>
</html>

like image 586
Mason Fisher Avatar asked Nov 21 '25 21:11

Mason Fisher


1 Answers

The problem is a conflict or duplication between the manually updated camera rotation and the view calculated by lookAt(). You directly modify camera.rotation.x and camera.rotation.y based on mouse movement. After updating the player's position, you change the camera position using camera.position.set and then call camera.lookAt. Hence the jerking/shaking. Instead of directly modifying the camera rotation via camera.rotation.x and camera.rotation.y in the mouse event, you can separate the control variables (yaw and pitch) from the camera's internal rotation. Using lookAt() based on yaw and pitch, you eliminate the oscillation between two different rotation updates.

This should helpful as well:

https://sbcode.net/threejs/follow-cam/

<script type="importmap">
  {
"imports": {
  "three": "https://unpkg.com/[email protected]/build/three.module.js",
  "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
  }
</script>

<script type="module">
import * as THREE from 'three';

const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
    90,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

const platformGeometry = new THREE.BoxGeometry(20, 1, 20);
const platformMaterial = new THREE.MeshBasicMaterial({
    color: 0xaaaaaa
});
const platform = new THREE.Mesh(platformGeometry, platformMaterial);
platform.position.y = -1;
scene.add(platform);

const playerGeometry = new THREE.BoxGeometry(1, 1, 1);
const playerMaterial = new THREE.MeshBasicMaterial({
    color: 0x00ff00
});
const player = new THREE.Mesh(playerGeometry, playerMaterial);
scene.add(player);

const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({
    color: 0xff0000
});
for (let i = 0; i < 5; i++) {
    const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
    cube.position.set(Math.random() * 18 - 9, 0.5, Math.random() * 18 - 9);
    scene.add(cube);
}

const aiGeometry = new THREE.BoxGeometry(1, 1, 1);
const aiMaterial = new THREE.MeshBasicMaterial({
    color: 0xff0000
});
const ai = new THREE.Mesh(aiGeometry, aiMaterial);
ai.position.set(0, 0.5, -5);
scene.add(ai);

const playerSpeed = 0.1;
let velocityY = 0;
let onGround = true;
const keys = {
    w: false,
    a: false,
    s: false,
    d: false,
    space: false
};

window.addEventListener("keydown", (event) => {
    if (keys.hasOwnProperty(event.key)) keys[event.key] = true;
});
window.addEventListener("keyup", (event) => {
    if (keys.hasOwnProperty(event.key)) keys[event.key] = false;
});

let yaw = 0;
let pitch = 0;
const mouse = {
    isDragging: false,
    previousX: 0,
    previousY: 0,
};

document.addEventListener("mousedown", (event) => {
    mouse.isDragging = true;
    mouse.previousX = event.clientX;
    mouse.previousY = event.clientY;
});
document.addEventListener("mouseup", () => {
    mouse.isDragging = false;
});
document.addEventListener("mousemove", (event) => {
    if (mouse.isDragging) {
        const deltaX = event.clientX - mouse.previousX;
        const deltaY = event.clientY - mouse.previousY;

        yaw -= deltaX * 0.002;
        pitch -= deltaY * 0.002;
        pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
        mouse.previousX = event.clientX;
        mouse.previousY = event.clientY;
    }
});

window.addEventListener("resize", () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

function animate() {
    requestAnimationFrame(animate);
    if (keys.w) player.position.z -= playerSpeed;
    if (keys.a) player.position.x -= playerSpeed;
    if (keys.s) player.position.z += playerSpeed;
    if (keys.d) player.position.x += playerSpeed;

    player.position.x = Math.max(-9.5, Math.min(9.5, player.position.x));
    player.position.z = Math.max(-9.5, Math.min(9.5, player.position.z));

    if (keys.space && onGround) {
        velocityY = 0.2;
        onGround = false;
    }
    player.position.y += velocityY;
    velocityY -= 0.01;
    if (player.position.y <= 0) {
        player.position.y = 0;
        onGround = true;
    }

    camera.position.set(player.position.x, player.position.y + 1.5, player.position.z);

    const target = new THREE.Vector3();
    target.x = player.position.x + Math.sin(yaw) * Math.cos(pitch);
    target.y = player.position.y + 1.5 + Math.sin(pitch);
    target.z = player.position.z - Math.cos(yaw) * Math.cos(pitch);

    camera.lookAt(target);

    renderer.render(scene, camera);
}
animate();
</script>
like image 130
Łukasz Daniel Mastalerz Avatar answered Nov 24 '25 15:11

Łukasz Daniel Mastalerz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!