使用 Three.js 进行第一人称射击游戏控制

问题描述 投票:0回答:2

我对 Three.js 和 3D 完全陌生。我正在尝试制作一个非常简单的第一人称射击游戏。我找到了很多例子,但它们看起来都很复杂。我想在使用代码之前先了解它。我遇到的问题是相机旋转。其他一切都很好。我的方法不太管用。尽管我将其设置为 0,但它似乎在 z 轴上旋转。这是我的所有代码(125 行)

var width    = window.innerWidth, height = window.innerHeight,
    scene    = new THREE.Scene(),
    camera   = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000),
    renderer = new THREE.WebGLRenderer(),
    mouse = {
      x: 0,
      y: 0,
      movedThisFrame: false
    };
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);

var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({
  color: 0x00ff00
});
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
var floorgeometry = new THREE.BoxGeometry(100,0.1,100);
var floormaterial = new THREE.MeshBasicMaterial({
  color: 0xff0000
});
var floor = new THREE.Mesh(floorgeometry, floormaterial);
floor.position.y = -1;
scene.add(floor);

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

camera.position.z = 5;

function radDeg(radians) {
  return radians * 180 / Math.PI;
}

function degRad(degrees) {
  return degrees * Math.PI / 180;
}

function rotateCam() {
  if (!mouse.movedThisFrame) {
    mouse.x = 0;
    mouse.y = 0;
  }
  /*

  What am I doing wrong here?

  */
  camera.rotation.x -= mouse.y * 0.001;
  camera.rotation.y -= mouse.x * 0.001;
  camera.rotation.z = 0;

  mouse.movedThisFrame = false;
}

function moveCam() {
  var rotation = camera.rotation.y % (Math.PI * 2), motion = [0,0];
  if (keys.w) {
    motion[0] += 0.1 * Math.cos(rotation);
    motion[1] += 0.1 * Math.sin(rotation);
  }
  if (keys.a) {
    motion[0] += 0.1 * Math.cos(rotation + degRad(90));
    motion[1] += 0.1 * Math.sin(rotation + degRad(90));
  }
  if (keys.s) {
    motion[0] += 0.1 * Math.cos(rotation - degRad(180));
    motion[1] += 0.1 * Math.sin(rotation - degRad(180));
  }
  if (keys.d) {
    motion[0] += 0.1 * Math.cos(rotation - degRad(90));
    motion[1] += 0.1 * Math.sin(rotation - degRad(90));
  }

  camera.position.z -= motion[0];
  camera.position.x -= motion[1];
}

window.onload = function() {
  renderer.domElement.onclick = function() {
    console.log('requested pointer lock');
    renderer.domElement.requestPointerLock();
  };

  renderer.domElement.onmousemove = function(e) {
    if (!mouse.movedThisFrame) {
      mouse.x = e.movementX;
      mouse.y = e.movementY;
      mouse.movedThisFrame = true;
    }
  };

  document.onkeydown = function(e) {
    var char = String.fromCharCode(e.keyCode);
    if (char == 'W')
      keys.w = true;
    else if (char == 'A')
      keys.a = true;
    else if (char == 'S')
      keys.s = true;
    else if (char == 'D')
      keys.d = true;
  };

  document.onkeyup = function(e) {
    var char = String.fromCharCode(e.keyCode);
    if (char == 'W')
      keys.w = false;
    else if (char == 'A')
      keys.a = false;
    else if (char == 'S')
      keys.s = false;
    else if (char == 'D')
      keys.d = false;
  };

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

问题出在

rotateCam
函数上。它不太有效,我真的不知道为什么。

我也尝试使用这个问题上的代码,但它不起作用。

javascript 3d three.js
2个回答
5
投票

第一人称控制比您想象的要复杂。即使您计算出角度数学,当指针未锁定时,鼠标会碰到窗口边缘并停止转动。

我建议你从指针锁定示例开始(http://thirdjs.org/examples/#misc_controls_pointerlock),这是 3js 的第一人称控件的示例。


0
投票

这是我的 Three.js 第一人称射击游戏演示解决方案。它使用 PointerLockControls 类。

<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js Examples - First Person Shooter Game Starter</title>
    <style>
        @font-face {
            font-family: 'Robus-BWqOd';
            src: url('https://www.shanebrumback.com/fonts/Robus-BWqOd.otf') format('opentype');
        }

        body {
            margin: 0;
        }

        canvas {
            display: block;
        }

        #blocker {
            position: fixed;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
        }

        #instructions {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
        }

        #crosshair {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 100px;
            height: 100px;
            display: none; /* Hide the crosshair by default */
        }

        #playButton {
            font-family: 'Robus-BWqOd';
            font-size: 5vw;
            color: white;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.75);
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            white-space: nowrap;
        }

        p {
            font-family: Arial;
            font-size: medium;
            text-align: center;
        }

        @media (max-width: 900px) {
            /* Styles for mobile devices with a maximum width of 767px */
            #playButton {
                font-family: 'Robus-BWqOd';
                font-size: 15vw; /* Adjust the font size as per your preference */
            }
            p {
                font-size: 4vw;
            }
        }

    </style>
</head>
<body>
    <div id="blocker">
        <div id="instructions">
            <div id="playButton">
                Play Now
                <p>
                    ESC - Menu
                    <br />
                    WASF ARROWS - Move
                    <br />
                    LEFT MOUSE  - Fire
                    <br />
                    SPACEBAR  - Fire
                    <br />
                    M - Play / Pause Music
                </p>
            </div>
        </div>
    </div>
    <img id="crosshair" src="https://www.shanebrumback.com/images/reticle.png" alt="Crosshair">

    <script src="https://cdn.jsdelivr.net/npm/three@latest/build/three.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/controls/OrbitControls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/controls/PointerLockControls.js"></script>
    
    <script type="module">


        // Set up the scene
        var scene = new THREE.Scene();
        var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        camera.position.set(9, 0.3, 3); // Set camera position 0.1 units above the grid

        // Create the renderer
        var renderer = new THREE.WebGLRenderer({ alpha: true, depth: true });
        // Configure renderer settings
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.toneMapping = THREE.ReinhardToneMapping;
        renderer.setClearColor(0x000000, 1); // Set background color to black
        renderer.domElement.style.position = 'fixed';
        renderer.domElement.id = 'renderer';
        renderer.domElement.style.zIndex = '-1';
        renderer.domElement.style.left = '0';
        renderer.domElement.style.top = '0';
        document.body.appendChild(renderer.domElement);

        var raycaster = new THREE.Raycaster();
        var mouse = new THREE.Vector2();
        var particles = [];
        var triangles = [];
        let cubes = []

        var hasCubeMoved = false; // Flag to track if the cube has already been moved

        // Gravity effect variables
        var gravity = new THREE.Vector3(0, -0.01, 0); // Adjust the gravity strength as needed
        var maxGravityDistance = 2; // Adjust the maximum distance affected by gravity as needed

        // Add PointerLockControls
        var controls = new THREE.PointerLockControls(camera, document.body);

        // Create a grid
        var gridHelper = new THREE.GridHelper(20, 20);

        // Set the color of the grid lines to white
        gridHelper.material.color.set(0xffffff);

        scene.add(gridHelper);

        // Create a plane geometry with the same size as the grid
        var planeGeometry = new THREE.PlaneGeometry(20, 20);

        // Create a blue material
        var blueMaterial = new THREE.MeshBasicMaterial({
            color: 0x0000ff,
            side: THREE.DoubleSide
        });

        // Create a plane mesh with the geometry and material
        var planeMesh = new THREE.Mesh(planeGeometry, blueMaterial);
        // Rotate the grid by 90 degrees
        planeMesh.rotation.x = Math.PI / 2;
        // Set the position of the plane to align with the grid
        planeMesh.position.copy(gridHelper.position);
        //scene.add(planeMesh);


        // Create a cube
        var geometry = new THREE.BoxGeometry(1, 1, 1);
        var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

        for (var i = 0; i < 5; i++) {
            var cube = new THREE.Mesh(geometry, material);
            cube.position.set(0, 0.5, 0); // Set cube position 0.5 units above the grid
            scene.add(cube);
            cubes.push(cube);
        }

        // Set camera to face cube position
        camera.lookAt(cube.position)

        // Set up pointer lock controls
        var blocker = document.getElementById('blocker');
        var instructions = document.getElementById('instructions');
        var playButton = document.getElementById('playButton');

        playButton.addEventListener('click', function () {
            controls.lock();
        });

        controls.addEventListener('lock', function () {
            instructions.style.display = 'none';
            blocker.style.display = 'none';
            document.getElementById('crosshair').style.display = 'block'; // Show the crosshair when screen is locked
        });

        controls.addEventListener('unlock', function () {
            blocker.style.display = 'block';
            instructions.style.display = '';
            document.getElementById('crosshair').style.display = 'none'; // Hide the crosshair when screen is unlocked
        });

        scene.add(controls.getObject());

        // Keyboard controls
        var moveForward = false;
        var moveBackward = false;
        var moveLeft = false;
        var moveRight = false;

        var onKeyDown = function (event) {
            switch (event.keyCode) {
                case 38: // up arrow
                case 87: // W key
                    moveForward = true;
                    break;
                case 37: // left arrow
                case 65: // A key
                    moveLeft = true;
                    break;
                case 40: // down arrow
                case 83: // S key
                    moveBackward = true;
                    break;
                case 39: // right arrow
                case 68: // D key
                    moveRight = true;
                    break;
            }
        };

        var onKeyUp = function (event) {
            switch (event.keyCode) {
                case 38: // up arrow
                case 87: // W key
                    moveForward = false;
                    break;
                case 37: // left arrow
                case 65: // A key
                    moveLeft = false;
                    break;
                case 40: // down arrow
                case 83: // S key
                    moveBackward = false;
                    break;
                case 39: // right arrow
                case 68: // D key
                    moveRight = false;
                    break;
            }
        };

        document.addEventListener('keydown', onKeyDown);
        document.addEventListener('keyup', onKeyUp);

        // Check collision with the grid
        function checkCollision(position) {
            var gridSize = 20;
            var halfGridSize = gridSize / 2;
            var margin = 0.1;

            if (
                position.x < -halfGridSize + margin ||
                position.x > halfGridSize - margin ||
                position.z < -halfGridSize + margin ||
                position.z > halfGridSize - margin
            ) {
                return true; // Collision detected
            }

            return false; // No collision
        }

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

            updateParticles();

            checkParticleCollision();

            if (controls.isLocked) {
                var delta = 0.03;

                if (moveForward) {
                    controls.moveForward(delta);
                    if (checkCollision(controls.getObject().position)) {
                        controls.moveForward(-delta); // Move back to the previous position
                    }
                }

                if (moveBackward) {
                    controls.moveForward(-delta);
                    if (checkCollision(controls.getObject().position)) {
                        controls.moveForward(delta); // Move back to the previous position
                    }
                }

                if (moveLeft) {
                    controls.moveRight(-delta);
                    if (checkCollision(controls.getObject().position)) {
                        controls.moveRight(delta); // Move back to the previous position
                    }
                }

                if (moveRight) {
                    controls.moveRight(delta);
                    if (checkCollision(controls.getObject().position)) {
                        controls.moveRight(-delta); // Move back to the previous position
                    }
                }
            }

            updateTriangles()

            renderer.render(scene, camera);
        }

        animate();

        function removeParticle(particle) {
            scene.remove(particle);
            particles.splice(particles.indexOf(particle), 1);
        }

        function createParticle() {
            playLaserSound();
            var geometry = new THREE.SphereGeometry(0.05, 16, 16);
            var material = new THREE.MeshBasicMaterial({ color: 0xADD8E6 });
            var particle = new THREE.Mesh(geometry, material);
            particle.position.copy(camera.position);
            particle.initialDirection = camera.getWorldDirection(new THREE.Vector3());
            particle.velocity = particle.initialDirection.clone().multiplyScalar(0.25);
            scene.add(particle);
            particles.push(particle);
        }

        function updateParticles() {
            var distanceThreshold = 20;

            for (var i = particles.length - 1; i >= 0; i--) {
                var particle = particles[i];
                particle.position.add(particle.velocity);

                var distance = particle.position.distanceTo(camera.position);
                if (distance > distanceThreshold) {
                    removeParticle(particle);
                }
            }
        }

        function onMouseDown(event) {
            event.preventDefault();

            if (controls.isLocked) {
                // Particle creation is allowed only when controls are locked
                if (event.button === 0) {
                    createParticle();
                }
            }
        }

        function onMouseMove(event) {
            event.preventDefault();

            mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
            mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

            raycaster.setFromCamera(mouse, camera);
        }

        // Mouse click event listener
        document.addEventListener('mousedown', onMouseDown);
        document.addEventListener('mousemove', onMouseMove, false);

        // Declare a variable to count collided particles
        var collidedParticles = 0;

        var hasCubeMoved = false; // Flag to track if the cube has already been moved

        // Check collision between particles and cubes
        function checkParticleCollision() {
            for (var j = 0; j < cubes.length; j++) {
                var cube = cubes[j];
                var isColliding = false;

                if (cube.visible) {
                    for (var i = 0; i < particles.length; i++) {
                        var particle = particles[i];
                        var particlePosition = particle.position;
                        var particleEdge = particlePosition
                            .clone()
                            .add(particle.velocity.clone().normalize().multiplyScalar(0.1));

                        raycaster.set(particlePosition, particleEdge.sub(particlePosition).normalize());
                        var intersects = raycaster.intersectObject(cube);

                        if (intersects.length === 1) {
                            // Particle collided with the cube
                            isColliding = true;
                            break;
                        }
                    }
                }

                // Set cube color and visibility based on collision status
                if (isColliding) {
                    // Cube is red during collision
                    cube.material.color.set(0xff0000);
                    explosion(cube);
                    moveCubeRandomly(cube);
                    hasCubeMoved = false; // Reset the flag when the cube is hidden
                } else {
                    // Cube is green when there is no collision
                    cube.material.color.set(0x00ff00);

                    // Check if all particles have been removed and the cube has not moved
                    if (collidedParticles === particles.length && !hasCubeMoved) {
                        collidedParticles = 0; // Reset the collided particles counter
                        hasCubeMoved = true; // Set the flag to indicate that the cube has been moved
                    }
                }
            }
        }

        // Move the cube to a random location on the grid
        function moveCubeRandomly(cube) {
            var gridSize = 20; // Adjust the grid size as desired
            var randomX = Math.floor(Math.random() * gridSize) - gridSize / 2;
            var randomZ = Math.floor(Math.random() * gridSize) - gridSize / 2;

            cube.position.x = randomX;
            cube.position.z = randomZ;
        }


        // Create an explosion of small triangles
        function explosion(cube) {

            playExplosionSound();

            var explosionCount = 50;

            for (var i = 0; i < explosionCount; i++) {
                var triangle = createTriangle(cube);
                scene.add(triangle);
                triangles.push(triangle); // Add the triangle to the triangles array

                triangle.userData = {
                    direction: new THREE.Vector3(
                        Math.random() * 2 - 1,
                        Math.random() * 2 - 1,
                        Math.random() * 2 - 1
                    ).normalize(),
                    speed: Math.random() * 0.05 + 0.01, // Random speed
                    rotationAxis: new THREE.Vector3(
                        Math.random(),
                        Math.random(),
                        Math.random()
                    ).normalize(),
                    rotationSpeed: Math.random() * 0.1 + 0.005, // Random rotation speed
                    distance: 0, // Distance traveled by the triangle
                    remove: false, // Flag to mark if the triangle should be removed
                    parentCube: cube, // Reference to the collided cube
                };
            }
        }


        // Create a small triangle
        function createTriangle(cube) {
            var geometry = new THREE.BufferGeometry();
            var vertices = new Float32Array([
                -0.1, 0, 0,
                0.1, 0, 0,
                0, 0.1, 0
            ]);
            var indices = new Uint16Array([0, 1, 2]);

            geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
            geometry.setIndex(new THREE.BufferAttribute(indices, 1));

            var material = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide });

            var triangle = new THREE.Mesh(geometry, material);

            // Set initial position at the center of the collided cube
            triangle.position.copy(cube.position);

            // Set the rotation to face the camera
            triangle.lookAt(camera.position);

            // Set random scale
            var scale = Math.random() * 1 + 0.5; // Adjust the scale range as desired
            triangle.scale.set(scale, scale, scale);

            return triangle;
        }


        // Update the triangles' positions, rotations, and remove them if necessary
        function updateTriangles() {
            for (var i = 0; i < triangles.length; i++) {
                var triangle = triangles[i];
                var userData = triangle.userData;

                // Move the triangle in its direction at a random speed
                var speed = userData.speed;
                triangle.position.add(userData.direction.clone().multiplyScalar(speed));

                // Rotate the triangle around its rotation axis at a random speed
                var rotationSpeed = userData.rotationSpeed;
                triangle.rotateOnWorldAxis(userData.rotationAxis, rotationSpeed);

                // Update the distance traveled by the triangle
                userData.distance += speed;

                // If the triangle has traveled a certain distance, mark it for removal
                if (userData.distance >= 2) {
                    userData.remove = true;
                }
            }

            // Remove triangles that are marked for removal
            for (var i = triangles.length - 1; i >= 0; i--) {
                if (triangles[i].userData.remove) {
                    scene.remove(triangles[i]);
                    triangles.splice(i, 1);
                }
            }


            // Resize renderer when window size changes
            window.addEventListener('resize', function () {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            });


        }


        // Create an AudioContext
        var audioContext = null;
        var musicBuffer = null;
        var laserSoundBuffer = null;
        var explosionSoundBuffer = null;
        var isMusicPlaying = false;
        var musicSource = null;

        // Function to load audio files
        function loadAudioFile(url, callback) {
            var request = new XMLHttpRequest();
            request.open('GET', url, true);
            request.responseType = 'arraybuffer';

            request.onload = function () {
                audioContext.decodeAudioData(request.response, function (buffer) {
                    callback(buffer);
                });
            };

            request.send();
        }

        // Function to play the music
        function playMusic() {
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
            }

            if (!musicBuffer) {
                loadAudioFile('https://www.shanebrumback.com/sounds/first-person-shooter-music.wav', function (buffer) {
                    musicBuffer = buffer;
                    playLoopedSound(buffer, .35);
                    isMusicPlaying = true;
                });
            } else {
                if (isMusicPlaying) {
                    pauseSound();
                    isMusicPlaying = false;
                } else {
                    resumeSound();
                    isMusicPlaying = true;
                }
            }
        }

        // Function to play a sound in a loop with a specific volume
        function playLoopedSound(buffer, volume) {
            musicSource = audioContext.createBufferSource();
            musicSource.buffer = buffer;
            musicSource.loop = true; // Enable looping
            var gainNode = audioContext.createGain();
            gainNode.gain.setValueAtTime(0, audioContext.currentTime); // Set initial volume to 0
            gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 2); // Gradually increase volume to desired level (adjust time as needed)
            musicSource.connect(gainNode);
            gainNode.connect(audioContext.destination);

            // Delay the start of the audio source
            musicSource.start(audioContext.currentTime + 0.1); // Adjust the delay as needed

            // Note: You can adjust the delay time and volume ramping to find the appropriate values that work best for your audio files.
        }

        // Function to pause the music
        function pauseSound() {
            if (musicSource) {
                musicSource.stop();
                musicSource.disconnect();
                musicSource = null;
            }
        }

        // Function to resume the music
        function resumeSound() {
            if (musicBuffer) {
                playLoopedSound(musicBuffer, .35);
            }
        }

        // Function to play the laser sound
        function playLaserSound() {
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
            }

            if (!laserSoundBuffer) {
                loadAudioFile('https://www.shanebrumback.com/sounds/laser.wav', function (buffer) {
                    laserSoundBuffer = buffer;
                    playSound(buffer, 1);
                });
            } else {
                playSound(laserSoundBuffer, 1);
            }
        }

        // Function to play the explosion sound
        function playExplosionSound() {
            if (!audioContext) {
                audioContext = new (window.AudioContext || window.webkitAudioContext)();
            }

            if (!explosionSoundBuffer) {
                loadAudioFile('https://www.shanebrumback.com/sounds/explosion.wav', function (buffer) {
                    explosionSoundBuffer = buffer;
                    playSound(buffer, 0.25); // Adjust the volume here (0.5 = 50% volume)
                });
            } else {
                playSound(explosionSoundBuffer, 0.25); // Adjust the volume here (0.5 = 50% volume)
            }
        }

        // Function to play a sound with a specific volume
        function playSound(buffer, volume) {
            var source = audioContext.createBufferSource();
            var gainNode = audioContext.createGain();
            gainNode.gain.value = volume;

            source.buffer = buffer;
            source.connect(gainNode);
            gainNode.connect(audioContext.destination);
            source.start(0);
        }


        // Event listener for key press
        document.addEventListener('keydown', function (event) {

            if (event.key === 'm' || event.key === 'M') {
                playMusic();
            } else if (event.key === ' ') {
                if (controls.isLocked) {
                    event.preventDefault(); // Prevent default action of spacebar
                    createParticle();
                    playLaserSound();
                }
            } else if (event.key === 'e' || event.key === 'E') {
                playExplosionSound();
            }

        });


    </script>

</body>
</html>
© www.soinside.com 2019 - 2024. All rights reserved.