Exploring First-Person Shooter Rotation with THREE.JS

I recently attempted to create a rotation system for a First Person Shooter game using THREE.JS.

However, I encountered a strange issue where the camera would pause momentarily before returning, especially at certain rotations. When checking the rotation value with console.log(this.rotation.y), it was approximately 1/-1 and sometimes switched directions unexpectedly.

Below is the code snippet from my game.js file:

// Importing necessary libraries
import * as engine from './three/src/Three.js'
// Importing loaders
import {GLTFLoader} from './three/examples/jsm/loaders/GLTFLoader.js'
import {OBJLoader} from './three/examples/jsm/loaders/OBJLoader.js'
import {MTLLoader} from './three/examples/jsm/loaders/MTLLoader.js'

// Importing Player class
import {Player} from './src/player.js'

// Creating a new player instance
const player = new Player(0.1)

// Setting up pause variable
var isPaused = false;

// Creating camera
const camera = new engine.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
player.position.z = 3; // Changing camera position

// Creating scene
const scene = new engine.Scene();
scene.background = new engine.Color("skyblue"); // Setting background color

// Creating renderer
const renderer = new engine.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setAnimationLoop(animate); // Animation loop function
renderer.domElement.style.width = "100%";
renderer.domElement.style.height = "100%";
renderer.domElement.className = "main";
renderer.domElement.id = "main";

// Adding ambient light to the scene
const ambientLight = new engine.AmbientLight(0xFFFFFF);
ambientLight.intensity = 4;
scene.add(ambientLight);

// Game logic

// Camera interaction
renderer.domElement.addEventListener("click", () => {
    renderer.domElement.requestPointerLock();
});

renderer.domElement.addEventListener("mousemove", player.handleMouseMove);

document.addEventListener("pointerlockchange", () => {
    isPaused = document.pointerLockElement !== renderer.domElement;
});

setInterval(() => {
    if (isPaused) {
        player.needToHandleMouseMove = false;
        player.needToHandleKeyboard = false;
        document.getElementById("pauseMenu").style.display = "";
    } else {
        player.needToHandleMouseMove = true;
        player.needToHandleKeyboard = true;
        document.getElementById("pauseMenu").style.display = "none";
    }
}, 20);
isPaused = true;

// Movement controls
document.addEventListener("keydown", player.handleKeyDown);
document.addEventListener("keyup", player.handleKeyUp);
document.addEventListener('wheel', (event) => { event.preventDefault(); }, {passive: false});

document.body.appendChild(renderer.domElement); // Appending renderer element to the body

// Load assets
const glftLoader = new GLTFLoader();
const objLoader = new OBJLoader();
const mtlLoader = new MTLLoader();

glftLoader.load("./res/TestMap.gltf", (glftScene) => {
    var obj = glftScene.scene.children[0];
    obj.position.set(0, 0, 0);
    scene.add(obj);
});

function animate(time) {

    player.checkForMovement();

    camera.rotation.copy(player.rotation);
    camera.position.copy(player.position);

    renderer.render(scene, camera);
}

// HTML interactions
document.getElementById("play").addEventListener("click", () => { isPaused = !isPaused; renderer.domElement.requestPointerLock(); });
document.getElementById("menuquit").addEventListener("click", () => { window.location.href = "../../menus/index/index.html"; });
document.getElementById("osquit").addEventListener("click", () => { require("electron").ipcRenderer.sendSync("closed", "close"); });

Additionally, here is my Player.js file:

import * as THREE from '../three/src/Three.js'

class Player {
    constructor(moveSpeed, keys = { W: false, A: false, S: false, D: false, ' ': false, SHIFT: false }) {
        this.moveSpeed = moveSpeed;
        this.keys = keys;
        this.position = new THREE.Vector3();
        this.rotation = new THREE.Quaternion();
        this.needToHandleMouseMove = false;
        this.needToHandleKeyboard = false;

        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleKeyUp = this.handleKeyUp.bind(this);
        this.handleMouseMove = this.handleMouseMove.bind(this);
    }

    handleKeyDown(event) {
        if (this.needToHandleKeyboard) {
            const key = event.key.toUpperCase();
            console.log("Key: " + key)
            if (this.keys.hasOwnProperty(key)) {
                this.keys[key] = true;
                if (key === ' ') {
                    event.preventDefault();
                }
            }
        }
    }

    handleKeyUp(event) {
        if (this.needToHandleKeyboard) {
            const key = event.key.toUpperCase();
            console.log("KeyUp: " + key)
            if (this.keys.hasOwnProperty(key)) {
                this.keys[key] = false;
            }
        }
    }

    handleMouseMove(event) {
        if (this.needToHandleMouseMove) {
            let movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
            let quaternionY = new THREE.Quaternion();
            quaternionY.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -movementX * 0.002);
        
            this.rotation.multiply(quaternionY);
        }
    }

    checkForMovement() {
        let moveSpeedX = this.moveSpeed * Math.sin(this.rotation.y);
        let moveSpeedZ = this.moveSpeed * Math.cos(this.rotation.y);

        if (this.keys.W) {
            this.position.x -= moveSpeedX;
            this.position.z -= moveSpeedZ;
        }
        if (this.keys.S) {
            this.position.x += moveSpeedX;
            this.position.z += moveSpeedZ;
        }
        if (this.keys.D) {
            this.position.x += moveSpeedZ;
            this.position.z -= moveSpeedX;
        }
        if (this.keys.A) {
            this.position.x -= moveSpeedZ;
            this.position.z += moveSpeedX;
        }
        if (this.keys[' ']) this.position.y += this.moveSpeed;
        if (this.keys.SHIFT) this.position.y -= this.moveSpeed;
    }
}
export { Player };

Answer №1

The topic you are referring to is the commonly known issue of Gimbal Lock.

Avoid working with Euclidean angles (which is represented by the .rotation property).

It is recommended to use .quaternion instead, as it will properly update the object's state even when using .rotation

UPDATE:

To be more accurate, I believe the problem lies in this code snippet:

    camera.rotation.x = player.rotation.x;
    camera.rotation.y = player.rotation.y;
    camera.rotation.z = player.rotation.z;

You should replace it with either

camera.quaternion.copy(player.quaternion)
or
camera.quaternion.setFromEuler(player.rotation)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Set up a custom JavaScript function to automatically refresh a webpage

I am in the process of setting up a JavaScript function to refresh on a specific webpage. The website utilizes an overarching JS and CSS framework, but I desire this particular function to load in a unique manner. The JS function within the framework dyna ...

Building an array from scratch in Angular

Below is the URL to access the code: https://stackblitz.com/edit/ng-zorro-antd-start-xz4c93 Inquiring about creating a new array. For example, upon clicking the submit button, the desired output should resemble the following structure: "tasks": [ { ...

Unable to find the Popper component in Material UI

Material-UI version "@material-ui/core": "^3.7.0" I have a requirement to display Popper on hover over an element, but unfortunately, the Popper is not visible. This section serves as a container for the Popper component. import PropTypes from &apos ...

Is there a way to retrieve the values of a checkbox from a location outside the form where it is initially defined?

After successfully developing a script that deletes rows from a table when a checkbox is selected, I encountered an issue where the values of the checkboxes were not accessible outside of the form action they were placed in. The checkboxes and correspondin ...

Is there a way to showcase the present weather conditions using simpleweather.js in plain text format?

I have integrated the weather.js library into my HTML file by adding the necessary imports: <script src="js/current.js"></script> <script src="js/sugar.min.js"></script> <script src="js/weather.js"></script> <script ...

Send an array of data from the view to the Controller in CodeIgniter

I have been searching for a solution to this question. However, for some reason, my code is not functioning properly. Here is what I want to achieve: Data Array -> ajax post -> codeigniter controller(save data in DB and redirect to another page) ...

Determine which JavaScript script to include based on whether the code is being executed within a Chrome extension

I am in the process of developing a Chrome extension as well as a web JavaScript application. I currently have an HTML container. I need the container.html file to include <script src="extension.js"> when it is running in the Chrome extension, and ...

How important is a neglected parameter in the world of JavaScript?

Curious about the value of an ignored parameter in JS? Imagine a function that requires 2 values as parameters, but you only provide one during the call. What happens to the other parameter? You might think it's undefined, but the code below only show ...

Alter attribute with an impact

I am looking for a solution to switch the image source using attr, while also incorporating a fade effect in the process. I have attempted to implement one of the suggestions from another post, but it is not producing the desired outcome. Current Appearan ...

Is it possible to terminate an active server process triggered by an HTTP post request in Node.js prior to returning a response?

I developed an application where I utilized Ajax to make calls to a Node server. The issue is that even if the user navigates to another page, the server persists in processing the initial request made by the Ajax call. It then proceeds to process the new ...

I'm having trouble getting this demo to run on my computer using three.js and cannon.js on the sandbox platform

Check out this demo link, even though I have all the dependencies, it still shows that some modules are missing: https://tympanus.net/codrops/2020/02/11/how-to-create-a-physics-based-3d-cloth-with-cannon-js-and-three-js/ Does anyone know how to code this ...

The translation of popups using ngx-translate in Javascript is not functioning properly

When I click on my request, the content "Are you sure?" is not changing to the required language. This issue is located in list.component.html <button type="button" (click)="myrequest(item.Id)">Request View</button> The fu ...

Sending handlebars variable to the client-side JavaScript file

I am currently working on an application using node.js along with express and handlebars, and I'm trying to find a way to transfer handlebars data from the server to client-side JavaScript files. Here is an example: //server.js var person = { na ...

Sequential invocations to console.log yield varying outcomes

So, I'm feeling a bit perplexed by this situation. (and maybe I'm missing something obvious but...) I have 2 consecutive calls to console.log. There is nothing else between them console.log($state); console.log($state.current); and here's ...

Mastering the art of maximizing efficiency with the Jquery Chosen Plugin

Currently, I'm facing an issue with the jQuery chosen plugin due to my large datasets causing the select box to hang and slow down. Below is my implementation of the plugin: var request = $.ajax({ method: "POST", url: "ajaxRequest.php", d ...

When running the command "npx create-next-app@latest --ts," be aware that there are 3 high severity vulnerabilities present. One of the vulnerabilities is that "node-fetch

Just set up a fresh project with Next.js and TypeScript by following the documentation using npx create-next-app@latest --ts. Encountering multiple high severity vulnerabilities despite running npm audit fix --force, which actually adds more vulnerabiliti ...

The inefficiency of invoking Jquery on elements that do not exist for the purpose of caching

What is the impact if JQuery attempts to access elements that do not exist? Will there be any significant CPU overhead other than script loading? Does it matter if this is done for a class or an id? For optimization purposes and to minimize simultaneous c ...

How can one eliminate the white space surrounding the data that Vue is binding?

Working with Vue, I've noticed a gap on the screen following the data binding. Instead of using Vanilla JavaScript's trim() function, is there a way to remove leading and trailing whitespace? Let me provide you with the code for the data binding ...

`Can you provide instructions on modifying CSS using JavaScript once the window size reaches a specified threshold?`

Is there a way to use JavaScript to automatically adjust the font size when the screen reaches 1050px? ...

What is the best way to ensure that any modifications made to an item in a table are appropriately synced

Utilizing xeditable.js, I am able to dynamically update the content of a cell within a table. My goal is to capture these changes and send them via an HTTP request (PUT) to the backend in order to update the database. Below is the table that can be edited ...