How do you make a fps/parkour movement script in cannon.js and three.js?

19 views Asked by At

I've been trying to make a good movement script for a few months now but the one I have now isn't very good. (I'm still a beginner).

This is my code:

import * as THREE from 'three'
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; // Import the GLTFLoader

const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
)
camera.position.set(0, 0, 0)

const cameraDirection = new THREE.Vector3();

window.onload = function() {
renderer.domElement.requestPointerLock();

renderer.domElement.onclick = function() {
renderer.domElement.requestPointerLock();
}
};
const renderer = new THREE.WebGLRenderer({
  alpha: true,
  antialias: true
})
renderer.shadowMap.enabled = true
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
  const width = window.innerWidth
  const height = window.innerHeight
  camera.aspect = width / height
  camera.updateProjectionMatrix()
  renderer.setSize(width, height)
  })

const skyboxPaths = [
'assets/textures/skymap/Daylight Box_Right.png', // Positive X
'assets/textures/skymap/Daylight Box_Left.png', // Negative X
'assets/textures/skymap/Daylight Box_Top.png', // Positive Y
'assets/textures/skymap/Daylight Box_Bottom.png', // Negative Y
'assets/textures/skymap/Daylight Box_Front.png', // Positive Z
'assets/textures/skymap/Daylight Box_Back.png', // Negative Z
];

const cubeTextureLoader = new THREE.CubeTextureLoader();
const skyboxTexture = cubeTextureLoader.load(skyboxPaths);
scene.background = skyboxTexture;

const controls = new PointerLockControls(camera, renderer.domElement)
controls.sensitivity = 0.1

const world = new CANNON.World();
world.gravity.set(0, -5, 0);

// Define the vertex shader
var vertexShader = `
  varying vec2 vUv;
  void main() {
    vUv = uv;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`;

// Define the fragment shader
var fragmentShader = `
  varying vec2 vUv;
  float noise(vec2 pos) {
    return fract(sin(dot(pos, vec2(12.9898, 78.233))) * 43758.5453);
  }
  void main() {
    float n = noise(vUv * 10.0);
    vec3 color = mix(vec3(0.2, 0.1, 0.0), vec3(0.6, 0.4, 0.2), n);
    float brightness = 0.5 + noise(vUv * 30.0) * 0.5;
    color *= brightness;
    gl_FragColor = vec4(color, 1.0);
  }
`;

const textureloader = new THREE.TextureLoader();

// Load the texture
const texture = textureloader.load('assets/textures/materials/stone.png');

const greenMaterial = new THREE.MeshBasicMaterial({color: new THREE.Color(0x00ff00)})
const stoneMaterial = new THREE.MeshBasicMaterial({
  map: texture
})
// Create Shader Material
var shaderMaterial = new THREE.ShaderMaterial({
  uniforms: {
      zoom: { value: 1.0 },
      offset: { value: new THREE.Vector2() }
  },
  vertexShader: vertexShader,
  fragmentShader: fragmentShader
});

        const floorGeometry = new THREE.BoxGeometry(5, 0.5, 5)
        const floorMesh = new THREE.Mesh(floorGeometry, stoneMaterial)
        floorMesh.position.y = -1.75
        floorMesh.castShadow = true
        scene.add(floorMesh)
        const floorShape = new CANNON.Box(new CANNON.Vec3(2.5, 0.25, 2.5))
        const floorBody = new CANNON.Body({ mass: 0 })
        floorBody.addShape(floorShape)
        floorBody.position.x = floorMesh.position.x
        floorBody.position.y = floorMesh.position.y
        floorBody.position.z = floorMesh.position.z
        world.addBody(floorBody)

        const plat1Geometry = new THREE.BoxGeometry(1, 0.5, 1)
        const plat1Mesh = new THREE.Mesh(plat1Geometry, greenMaterial)
        plat1Mesh.position.z = -5
        plat1Mesh.position.y = -1.75
        plat1Mesh.castShadow = true
        scene.add(plat1Mesh)
        const plat1Shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.25, 0.5))
        const plat1Body = new CANNON.Body({ mass: 0 })
        plat1Body.addShape(plat1Shape)
        plat1Body.position.x = plat1Mesh.position.x
        plat1Body.position.y = plat1Mesh.position.y
        plat1Body.position.z = plat1Mesh.position.z
        world.addBody(plat1Body)

        const plat2Geometry = new THREE.BoxGeometry(1, 0.5, 1)
        const plat2Mesh = new THREE.Mesh(plat2Geometry, greenMaterial)
        plat2Mesh.position.z = -8
        plat2Mesh.position.y = -1.75
        plat2Mesh.castShadow = true
        scene.add(plat2Mesh)
        const plat2Shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.25, 0.5))
        const plat2Body = new CANNON.Body({ mass: 0 })
        plat2Body.addShape(plat2Shape)
        plat2Body.position.x = plat2Mesh.position.x
        plat2Body.position.y = plat2Mesh.position.y
        plat2Body.position.z = plat2Mesh.position.z
        world.addBody(plat2Body)

        const plat3Geometry = new THREE.BoxGeometry(1, 0.5, 1)
        const plat3Mesh = new THREE.Mesh(plat3Geometry, greenMaterial)
        plat3Mesh.position.z = -11
        plat3Mesh.position.y = -1.75
        plat3Mesh.castShadow = true
        scene.add(plat3Mesh)
        const plat3Shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.25, 0.5))
        const plat3Body = new CANNON.Body({ mass: 0 })
        plat3Body.addShape(plat3Shape)
        plat3Body.position.x = plat3Mesh.position.x
        plat3Body.position.y = plat3Mesh.position.y
        plat3Body.position.z = plat3Mesh.position.z
        world.addBody(plat3Body)

        const plat4Geometry = new THREE.BoxGeometry(1, 0.5, 12)
        const plat4Mesh = new THREE.Mesh(plat4Geometry, greenMaterial)
        plat4Mesh.position.z = -20
        plat4Mesh.position.y = -1.75
        plat4Mesh.castShadow = true
        scene.add(plat4Mesh)
        const plat4Shape = new CANNON.Box(new CANNON.Vec3(0.5, 0.25, 6))
        const plat4Body = new CANNON.Body({ mass: 0 })
        plat4Body.addShape(plat4Shape)
        plat4Body.position.x = plat4Mesh.position.x
        plat4Body.position.y = plat4Mesh.position.y
        plat4Body.position.z = plat4Mesh.position.z
        world.addBody(plat4Body)

        const plat5Geometry = new THREE.BoxGeometry(5, 0.5, 5)
        const plat5Mesh = new THREE.Mesh(plat5Geometry, greenMaterial)
        plat5Mesh.position.z = -30
        plat5Mesh.position.y = -1.75
        plat5Mesh.castShadow = true
        scene.add(plat5Mesh)
        const plat5Shape = new CANNON.Box(new CANNON.Vec3(2.5, 0.25, 2.5))
        const plat5Body = new CANNON.Body({ mass: 0 })
        plat5Body.addShape(plat5Shape)
        plat5Body.position.x = plat5Mesh.position.x
        plat5Body.position.y = plat5Mesh.position.y
        plat5Body.position.z = plat5Mesh.position.z
        world.addBody(plat5Body)

        const wall1Geometry = new THREE.BoxGeometry(1, 3, 4)
        const wall1Mesh = new THREE.Mesh(wall1Geometry, greenMaterial)
        wall1Mesh.position.z = -14
        wall1Mesh.position.x = 1.5
        wall1Mesh.castShadow = true
        scene.add(wall1Mesh)
        const wall1Shape = new CANNON.Box(new CANNON.Vec3(0.5, 1.5, 2.5))
        const wall1Body = new CANNON.Body({ mass: 0 })
        wall1Body.addShape(wall1Shape)
        wall1Body.position.x = wall1Mesh.position.x
        wall1Body.position.y = wall1Mesh.position.y
        wall1Body.position.z = wall1Mesh.position.z
        world.addBody(wall1Body)

        const wall2Geometry = new THREE.BoxGeometry(1, 3, 4)
        const wall2Mesh = new THREE.Mesh(wall2Geometry, greenMaterial)
        wall2Mesh.position.z = -20
        wall2Mesh.position.x = -1.5
        wall2Mesh.castShadow = true
        scene.add(wall2Mesh)
        const wall2Shape = new CANNON.Box(new CANNON.Vec3(0.5, 1.5, 2.5))
        const wall2Body = new CANNON.Body({ mass: 0 })
        wall2Body.addShape(wall2Shape)
        wall2Body.position.x = wall2Mesh.position.x
        wall2Body.position.y = wall2Mesh.position.y
        wall2Body.position.z = wall2Mesh.position.z
        world.addBody(wall2Body)

        const buttonGeometry = new THREE.BoxGeometry(0.1, 1, 0.1)
        const buttonMesh = new THREE.Mesh()
        buttonMesh.position.z = -30
        buttonMesh.position.y = -1
        buttonMesh.castShadow = true
        scene.add(buttonMesh)
        const buttonShape = new CANNON.Box(new CANNON.Vec3(0.05, 0.5, 0.05))
        const buttonBody = new CANNON.Body({ mass: 0 })
        buttonBody.addShape(buttonShape)
        buttonBody.position.x = buttonMesh.position.x
        buttonBody.position.y = buttonMesh.position.y
        buttonBody.position.z = buttonMesh.position.z
        world.addBody(buttonBody)

        const playerGeometry = new THREE.SphereGeometry(0.25)
        const playerMesh = new THREE.Mesh()
        playerMesh.position.x = 0
        playerMesh.position.y = -1
        playerMesh.castShadow = true
        scene.add(playerMesh)
        const playerShape = new CANNON.Sphere(0.25)
        const playerBody = new CANNON.Body({ mass: 1, restitution: 0 })
        playerBody.addShape(playerShape)
        playerBody.position.x = playerMesh.position.x
        playerBody.position.y = playerMesh.position.y
        playerBody.position.z = playerMesh.position.z
        world.addBody(playerBody)

        const loader = new GLTFLoader();
loader.load('assets/models/button.glb', (gltf) => {
  const levelModel = gltf.scene;
  const scaleFactor = 0.05; // Adjust this value as needed
levelModel.scale.set(scaleFactor, scaleFactor, scaleFactor);
  levelModel.position.set(0, -1.4, -30);
  scene.add(levelModel);
});

const clock = new THREE.Clock()
        let delta

const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.y = 3
light.position.z = 1
light.castShadow = true
scene.add(light)

scene.add(new THREE.AmbientLight(0xffffff, 0.5))

camera.position.z = 5

const keys = {
  a: {
    pressed: false
  },
  d: {
    pressed: false
  },
  s: {
    pressed: false
  },
  w: {
    pressed: false
  },
  space: {
    pressed: false
  },
  e: {
    pressed: false
  }
}
window.addEventListener('keydown', (event) => {
  switch (event.code) {
    case 'KeyA':
      keys.a.pressed = true
      break
    case 'KeyD':
      keys.d.pressed = true
      break
    case 'KeyS':
      keys.s.pressed = true
      break
    case 'KeyW':
      keys.w.pressed = true
      break
    case 'Space':
    keys.space.pressed = true
      break
    case 'KeyE':
    keys.e.pressed = true
      break
  }
})

window.addEventListener('keyup', (event) => {
  switch (event.code) {
    case 'KeyA':
      keys.a.pressed = false
      break
    case 'KeyD':
      keys.d.pressed = false
      break
    case 'KeyS':
      keys.s.pressed = false
      break
    case 'KeyW':
      keys.w.pressed = false
      break
      case 'Space':
      keys.space.pressed = false
      break
    case 'KeyE':
      keys.e.pressed = false
      break
  }
})

let frames = 0

var canJump = false;

var canWallRun = false;

var contactNormal = new CANNON.Vec3(); // Normal in the contact, pointing *out* of whatever the player touched
var upAxis = new CANNON.Vec3(0,1,0);
var posAxis = new CANNON.Vec3(1,0,1);
var negAxis = new CANNON.Vec3(-1,0,-1);

playerBody.addEventListener("collide",function(e){
    var contact = e.contact;

    if(contact.bi.id == playerBody.id)
        contact.ni.negate(contactNormal);
    else
        contactNormal.copy(contact.ni);

    if(contactNormal.dot(upAxis) > 0.5)
        canJump = true;
        if(contactNormal.dot(posAxis) > 0.5)
        canWallRun = true;

      if(contactNormal.dot(negAxis) > 0.5)
        canWallRun = true;
      
      if(contactNormal.dot(posAxis) > 0.5 | contactNormal.dot(negAxis) > 0.5){
      }
      else{
        canWallRun = false;
      }
});

let bobbingclock = new THREE.Clock();
let bobbingSpeed = 10; // Adjust to your liking
let bobbingAmount = 0.025;


let footsteps = new Audio('assets/SFX/Footsteps.wav');
footsteps.loop = true;
let walked = false;

function animate() {
  const animationId = requestAnimationFrame(animate)
  
   // render the main scene
   renderer.render( scene, camera );

  delta = Math.min(clock.getDelta(), 0.1)
  world.step(delta);

    let time = bobbingclock.getElapsedTime();
    let bobbing = Math.sin(time * bobbingSpeed) * bobbingAmount;
if(canJump == true & (keys.w.pressed | keys.a.pressed | keys.s.pressed | keys.d.pressed)){
  camera.position.set(playerMesh.position.x, playerMesh.position.y += bobbing + 0.1, playerMesh.position.z)
}
else{
  camera.position.set(playerMesh.position.x, playerMesh.position.y + 0.1, playerMesh.position.z)
}

  camera.getWorldDirection(cameraDirection);

      // Copy coordinates from Cannon to Three.js
            playerMesh.position.set(playerBody.position.x, playerBody.position.y, playerBody.position.z)
            playerMesh.quaternion.set(
                playerBody.quaternion.x,
                playerBody.quaternion.y,
                playerBody.quaternion.z,
                playerBody.quaternion.w
            )

var slideForce = 2; // Adjust this value to control the sliding force

const force = new CANNON.Vec3();
if(nomenu == true){
  playerBody.velocity.set(0, playerBody.velocity.y, 0)
  
  // Create a new direction vector that doesn't change when you look up or down
  var horizontalDirection = new THREE.Vector3();
  horizontalDirection.copy(cameraDirection);
  horizontalDirection.y = 0;
  horizontalDirection.normalize();

  if (keys.d.pressed & !keys.a.pressed & !keys.s.pressed & !keys.w.pressed) {
    playerBody.velocity.x -= horizontalDirection.z * slideForce;
    playerBody.velocity.z += horizontalDirection.x * slideForce;
    if(walked == false){
      footsteps.play()
      walked = true
    }
  }
  if (keys.a.pressed & !keys.d.pressed & !keys.s.pressed & !keys.w.pressed) {
    playerBody.velocity.x += horizontalDirection.z * slideForce;
    playerBody.velocity.z -= horizontalDirection.x * slideForce;
    if(walked == false){
      footsteps.play()
      walked = true
    }
  }
  if (keys.s.pressed & !keys.a.pressed & !keys.d.pressed & !keys.w.pressed) {
    playerBody.velocity.x -= horizontalDirection.x * slideForce;
    playerBody.velocity.z -= horizontalDirection.z * slideForce;
    if(walked == false){
      footsteps.play()
      walked = true
    }
  }
  if (keys.w.pressed & !keys.a.pressed & !keys.s.pressed & !keys.d.pressed) {
    playerBody.velocity.x += horizontalDirection.x * slideForce;
    playerBody.velocity.z += horizontalDirection.z * slideForce;
    if(walked == false){
      footsteps.play()
      walked = true
    }
  }

if (keys.w.pressed & keys.d.pressed) {
  playerBody.velocity.x += horizontalDirection.x * (slideForce * 0.7);
  playerBody.velocity.z += horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.x -= horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.z += horizontalDirection.x * (slideForce * 0.7);
  if(walked == false){
    footsteps.play()
    walked = true
    }
  }
  if (keys.w.pressed & keys.a.pressed) {
  playerBody.velocity.x += horizontalDirection.x * (slideForce * 0.7);
  playerBody.velocity.z += horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.x += horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.z -= horizontalDirection.x * (slideForce * 0.7);
  if(walked == false){
    footsteps.play()
    walked = true
    }
  }
  if (keys.a.pressed & keys.s.pressed) {
  playerBody.velocity.x += horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.z -= horizontalDirection.x * (slideForce * 0.7);
  playerBody.velocity.x -= horizontalDirection.x * (slideForce * 0.7);
  playerBody.velocity.z -= horizontalDirection.z * (slideForce * 0.7);
  if(walked == false){
    footsteps.play()
    walked = true
    }
  }
  if (keys.s.pressed & keys.d.pressed) {
  playerBody.velocity.x -= horizontalDirection.x * (slideForce * 0.7);
  playerBody.velocity.z -= horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.x -= horizontalDirection.z * (slideForce * 0.7);
  playerBody.velocity.z += horizontalDirection.x * (slideForce * 0.7);
  if(walked == false){
    footsteps.play()
    walked = true
    }
  }
if (keys.space.pressed) {
if (canJump == true){
force.y += 3;
canJump = false
  }
}
if(!keys.w.pressed & !keys.a.pressed & !keys.s.pressed & !keys.d.pressed){
    footsteps.pause()
    footsteps.currentTime = 0;
    walked = false
}
if(canJump == false){
  footsteps.pause()
  footsteps.currentTime = 0;
  walked = false
}
if (keys.e.pressed) {
button()
  }
}

if(canWallRun == true){
  world.gravity.set(0, -1, 0)
  slideForce = 5
}
if(canWallRun == false){
  world.gravity.set(0, -5, 0)
  slideForce = 2
}

function button(){
  if(playerBody.position.x > 0 - 0.3){
    if(playerBody.position.x < 0 + 0.3){
      if(playerBody.position.z < -30 + 0.3){
        if(playerBody.position.z > -30 - 0.3){
          playerBody.position.set(playerBody.position.x, 1000, playerBody.position.z)
        }
      }
    }
  }
}

playerBody.applyImpulse(force, playerBody.position);

if(playerBody.position.y <= -20){
  playerBody.position.x = 0
  playerBody.position.y = -1.2
  playerBody.position.z = 0
  playerBody.velocity.set(0, 0, 0)
}

  frames++
}
animate()

Html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Turn around you idiot</title>
  <link rel="icon" type="gif" href="assets/icon/logo.png">
  <link rel="stylesheet" href="Turn around you idiot.css">
</head>
<body>

  <script src="https://cdn.jsdelivr.net/npm/[email protected]/build/cannon.min.js"></script>

  <style>
    body {
      margin: 0;
      background: white;
    }
  </style>

  <img hidden id="img" src="assets/icon/cursor.png" style="width: 0.35vw;height: 0.35vw;position:absolute;left:50%;top:50%;">
  <p hidden id="actionbar" style="position: absolute;left:50%;top: 90%;">You died!</p>

  <script>
    imgPositionx = window.innerWidth/2 - 0.125
    imgPositiony = window.innerHeight/2 - 0.125

    document.getElementById('img').style.left = imgPositionx
    document.getElementById('img').style.top = imgPositiony

    actionbarPositionx = window.innerWidth/2 - 100
    actionbarPositiony = window.innerHeight/2 - 0.125

    document.getElementById('actionbar').style.left = actionbarPositionx
    document.getElementById('actionbar').style.top = actionbarPositiony
  </script>

  <div id="menu" style="width: 100%;height: 100%;background: none;outline: none;border: none;position: absolute;">
    <h2 style="text-align: center;font-size: 5vw;user-select: none;">Turn around you Idiot!</h2>
    <button id="newgame" type="button" style="height: 5vw;width: 20vw;font-size: x-large;background: none;position: absolute;top: 15vw;left: 20%;font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;outline: none;border: none;user-select: none;" onclick="newgame()">New Game</button>
    <button id="joingame" type="button" style="height: 5vw;width: 20vw;font-size: x-large;background: none;position: absolute;top: 20vw;left: 20%;font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;outline: none;border: none;user-select: none;" onclick="joingame()">Join Game</button>
    <button id="options" type="button" style="height: 5vw;width: 20vw;font-size: x-large;background: none;position: absolute;top: 25vw;left: 20%;font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;outline: none;border: none;user-select: none;" onclick="options()">Options</button>
    <button id="quit" type="button" style="height: 5vw;width: 20vw;font-size: x-large;background: none;position: absolute;top: 30vw;left: 20%;font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;outline: none;border: none;user-select: none;" onclick="quit()">Quit</button>
  </div>

  <dialog hidden id="joinmenu" style="width: 80vw;height: 80vh;background: white;outline: none;border: none;position: absolute;top: 5vh;border-radius: 5vw;">
    <p style="font-size: 10vw;text-align: center;font-family: monospace;user-select: none;">Join Code:</p>
    <input placeholder="IE: 0000" style="font-family: 'Courier New', Courier, monospace;height: 10vw;width: 35vw;position: absolute;left: 25vw;border-radius: 1vw;font-size: 5vw;outline: none;">
    <button id="mainmenu" type="button" style="height: 5vw;width: 20vw;font-size: 3vw;background: none;position: absolute;top: 40vw;left: 3%;font-family:'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;outline: none;border: none;user-select: none;user-select: none;"onclick="mainmenu()">Back</button>
 </dialog>

  <script>
    let nomenu;
    function newgame(){
      nomenu = true
      document.getElementById('menu').style.display = "none"
      document.getElementById('img').style.display = "block"
    }
    function joingame(){
      document.getElementById('joinmenu').style.display = "block"
    }
    function options(){
      
    }
    function quit(){
      
    }
    function mainmenu(){
      document.getElementById("joinmenu").hidden = "true"
      document.getElementById('menu').style.display = "block"
    }
  </script>
  
  <script
    async
    src="https://unpkg.com/[email protected]/dist/es-module-shims.js"
  ></script>

  <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" src="Turn around you idiot.js"></script>
</body>
</html>

I tried searching for some online, but apparently no-one has made a parkour/fps movement script in cannon.js before, and the examples that I found online didn't really help that much.

0

There are 0 answers