Programming Keyboard Inputs for Three.js First Person Games

Learn to Program Keyboard Inputs for First Person Games with Three.js

Play Now

ESC - Menu
C - Example Code
WASD ARROWS - Move
LEFT MOUSE - Fire
SPACEBAR - Fire
M - Play / Pause Music


Interactive 3D games have become increasingly popular on the web. With technologies like Three.js, you can create stunning 3D worlds that users can explore. To make your 3D game even more engaging, you can implement keyboard inputs, allowing players to control the game with their keyboards.

Why Use Keyboard Inputs in 3D Games?

Keyboard inputs provide a user-friendly way to interact with your 3D game. They allow players to navigate through the virtual world, control characters, and perform various actions. Whether it's moving a character, firing a weapon, or triggering special abilities, keyboard inputs add an extra layer of interactivity to your game.

Getting Started

Before you can implement keyboard inputs, you should have a basic understanding of Three.js and have a 3D scene set up. Make sure to include the Three.js library in your project.



Handling Keyboard Inputs

The key to adding keyboard inputs to your 3D game is event handling. You'll want to listen for specific keypress events and respond accordingly. Most modern web browsers provide the ability to capture keyboard events, such as keydown and keyup.

Common Keyboard Inputs

Here are some common keyboard inputs you might consider for your 3D game:

  • W, A, S, D keys: These are often used for character movement in games.
  • Spacebar: Useful for actions like jumping or shooting.
  • Arrow keys: Great for camera or vehicle controls.
  • Custom keys: You can define custom inputs for special actions or abilities in your game.

Mapping Keyboard Inputs to Game Actions

Once you've captured keyboard events, you can map them to specific in-game actions. For example, when the 'W' key is pressed, you can move the player character forward. Similarly, pressing the spacebar can trigger a jump action.

Consider User Experience

When implementing keyboard inputs, it's crucial to consider the user experience. Make sure to provide clear instructions or tutorials for players on how to use the keyboard inputs. Additionally, allow players to customize their keybindings to suit their preferences.

Testing and Debugging

Regularly test your keyboard inputs to ensure they work as intended. Debug any issues and make adjustments to improve the gaming experience. Keep in mind that different browsers may handle keyboard events differently, so cross-browser testing is essential.

Conclusion

Adding keyboard inputs to your Three.js 3D game can greatly enhance user interaction and immersion. By providing players with the ability to control the game using their keyboards, you create a more engaging and interactive gaming experience. Experiment with different inputs and actions to make your 3D world come to life.

Explore More Three.js Resources

Recent Blog Posts & Updates

Load This Gist Code For An Instant Demo

<!--////////////////////////////////////////////////////////////////////////////////////////
/// ///
/// Example Using Three.js Library, HTML, CSS & JavaScript ///
// 3D Interactive Web Apps & Games 2021-2024 ///
/// Contact Shane Brumback https://www.shanebrumback.com ///
/// Send a message if you have questions about this code ///
/// I am a freelance developer. I develop any and all web. ///
/// Apps Websites 3D 2D CMS Systems etc. Contact me anytime :) ///
/// ///
////////////////////////////////////////////////////////////////////////////////////////////-->
<!DOCTYPE html>
<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 Tommy Gun Starter Code</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
</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 src="https://cdn.jsdelivr.net/npm/three@latest/examples/js/loaders/GLTFLoader.js"></script>
<script type="module">
// Set up the scene
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, .1, 1000);
camera.position.set(0, 1, 0); // Set camera position 0.1 units above the grid
// Adjust the camera's near clipping plane value
camera.near = .015; // Set a smaller value, like 0.1
camera.updateProjectionMatrix();
// Setup Gun Object
var tommyGun;
// 3D Abandoned Building MOdel
var abandonedBuilding;
//Array for bullet hole meshes
let bulletHoles = [];
//Gun Firing Variable to track when gun is firing
let isFiring = false
// Counter variable to keep track of the number of bullets
var bulletCount = 0;
// Create the renderer
var renderer = new THREE.WebGLRenderer({});
renderer.physicallyCorrectLights
// Configure renderer settings
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
//Create ray caster instance
var raycaster = new THREE.Raycaster();
//Create mouse instance
var mouse = new THREE.Vector2();
//Create array to store bullets
var bullets = [];
// Variables for tracking time and adding bullet hole meshes
let lastMeshAdditionTime = 0;
const meshAdditionInterval = 100; // Interval duration in milliseconds
// Keyboard controls
var moveForward = false;
var moveBackward = false;
var moveLeft = false;
var moveRight = false;
///flashing light // Create a point light
const tommyGunLight = new THREE.PointLight(0xffffff, 100, 100); // Adjust the light color and intensity as needed
tommyGunLight.position.set(0, 0, 0); // Set the light position
tommyGunLight.visible = false
// Add the light to the scene initially
scene.add(tommyGunLight);
// 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);
scene.add(gridHelper);
// 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
});
// Resize renderer when window size changes
window.addEventListener('resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
scene.add(controls.getObject());
// Create an ambient light with brightness
var ambientLight = new THREE.AmbientLight(0xffffff, 2); // Adjust the color as needed
scene.add(ambientLight);
// Load GLTF model
var loader = new THREE.GLTFLoader();
loader.load('https://www.shanebrumback.com/models/glb/tommy_gun.glb', function (gltf) {
gltf.scene.scale.set(.25, .25, .25)
// Set the cube's position to be equal to the camera's position
gltf.scene.position.set(camera.position.x, camera.position.y, camera.position.z);
tommyGun = gltf.scene
scene.add(gltf.scene)
// Add a point light to the gun
var tommyGunLight = new THREE.PointLight(0xffffff, 1);
tommyGunLight.position.set(.025, -.15, 0); // Adjust the position of the light relative to the gun
tommyGun.add(tommyGunLight);
});
//Load building model
loader.load(
'https://www.shanebrumback.com/models/glb/low_poly_abandoned_brick_room.glb',
function (gltf) {
abandonedBuilding = gltf.scene;
abandonedBuilding.position.y = .008
scene.add(abandonedBuilding);
});
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
}
function animate() {
requestAnimationFrame(animate);
// Update bullets
updateBullets();
//ramp up player movement speed and direction
if (controls.isLocked) {
var acceleration = 0.003; // Speed increment per frame
var maxSpeed = 0.10; // Maximum speed
if (moveForward) {
controls.speed = Math.min(controls.speed + acceleration, maxSpeed);
controls.moveForward(controls.speed);
if (checkCollision(controls.getObject().position)) {
controls.moveForward(-controls.speed); // Move back to the previous position
}
} else if (moveBackward) {
controls.speed = Math.min(controls.speed + acceleration, maxSpeed);
controls.moveForward(-controls.speed);
if (checkCollision(controls.getObject().position)) {
controls.moveForward(controls.speed); // Move back to the previous position
}
} else if (moveLeft) {
controls.speed = Math.min(controls.speed + acceleration, maxSpeed);
controls.moveRight(-controls.speed);
if (checkCollision(controls.getObject().position)) {
controls.moveRight(controls.speed); // Move back to the previous position
}
} else if (moveRight) {
controls.speed = Math.min(controls.speed + acceleration, maxSpeed);
controls.moveRight(controls.speed);
if (checkCollision(controls.getObject().position)) {
controls.moveRight(-controls.speed); // Move back to the previous position
}
} else {
controls.speed = 0; // Reset speed when no movement controls are active
}
}
// Set the position and rotation of the tommy gun based on the camera
if (tommyGun) {
// Match tommy gun to player camera position
tommyGun.position.copy(camera.position);
tommyGun.rotation.copy(camera.rotation);
tommyGun.updateMatrix();
tommyGun.translateZ(-.05);
tommyGun.translateY(-.05);
tommyGun.translateX(-.025);
tommyGun.rotateY(Math.PI / 2); // Rotate the model by 180 degrees
}
if (isFiring) {
const currentTime = performance.now();
// Check if the specified interval has passed since the last mesh addition
if (currentTime - lastMeshAdditionTime >= meshAdditionInterval) {
lastMeshAdditionTime = currentTime; // Update the last mesh addition time
// Get the direction of the ray at the time of creation
const direction = raycaster.ray.direction.clone();
// Search for the "barrel_low" mesh within the "tommyGun" object
//use it as bullet particle start point
let finLowObject = null;
tommyGun.traverse(function (object) {
if (object.name === 'barrel_low') {
console.log(object.name);
finLowObject = object;
}
});
const worldPosition = new THREE.Vector3();
finLowObject.getWorldPosition(worldPosition);
createBullet(worldPosition, direction);
updateGunMuzzleFlash(worldPosition);
}
//check bullet collision
checkBulletCollision();
}
//face bullet holes
faceBulletHolesToCamera()
renderer.render(scene, camera);
}
animate();
// Add event listeners for the mouse down and mouse up events
window.addEventListener('mousedown', onMouseDown, false);
window.addEventListener('mouseup', onMouseUp, false);
function onMouseDown(event) {
// Check if the left mouse button is pressed (button code 0)
if (controls.isLocked && event.button === 0 && event.target.id !== 'playButton') {
// Set isFiring to true
isFiring = true;
}
}
function onMouseUp(event) {
// Check if the left mouse button is released (button code 0)
if (event.button === 0) {
// Set isFiring to false
isFiring = false;
}
}
function onMouseMove(event) {
event.preventDefault();
// Get the image element
const imageElement = document.getElementById('crosshair');
// Get the position of the image element on the screen
const imageRect = imageElement.getBoundingClientRect();
const imageCenterX = imageRect.left + imageRect.width / 2;
const imageCenterY = imageRect.top + imageRect.height / 2;
// Calculate the normalized device coordinates (-1 to 1) from the image center
const mouse = new THREE.Vector2();
mouse.x = (imageCenterX / window.innerWidth) * 2 - 1;
mouse.y = -(imageCenterY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
}
// Mouse click event listener
document.addEventListener('mousemove', onMouseMove, false);
function faceBulletHolesToCamera() {
bulletHoles.forEach(function (bulletHole) {
// Calculate the direction from the bullet hole to the camera
var direction = camera.position.clone().sub(bulletHole.position).normalize();
// Calculate the rotation quaternion that faces the camera
var quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), direction);
// Apply the rotation to the bullet hole
bulletHole.setRotationFromQuaternion(quaternion);
});
}
function checkBulletCollision() {
bullets.forEach(function (bullet) {
var bulletPosition = bullet.position;
var bulletDirection = bullet.direction; // Assuming each bullet has a direction property
// Create a raycaster for the current bullet
var raycaster = new THREE.Raycaster(bulletPosition, bulletDirection);
// Find intersections between the ray and the abandonedBuilding object
var intersects = raycaster.intersectObject(abandonedBuilding, true);
if (intersects.length > 0) {
// Play the bullet ricochet sound every 5 bullets
if (bulletCount % 15 === 0) {
playBulletRicochetSound();
}
bulletCount++;
var intersect = intersects[0];
var point = intersect.point;
var faceNormal = intersect.face.normal;
// Create and position the mesh at the intersection point
var offset = new THREE.Vector3(0, 0, 0.01); // Increase the offset value to avoid z-fighting
var insertionOffset = new THREE.Vector3(0, 0.01, 0); // Adjust the insertion offset as needed
var loader = new THREE.TextureLoader();
var material = new THREE.MeshBasicMaterial({
map: loader.load('https://www.shanebrumback.com/images/bullet-hole.png'),
side: THREE.DoubleSide,
transparent: true,
depthWrite: true,
});
var geometry = new THREE.PlaneGeometry(0.08, 0.08);
var bulletHoleMesh = new THREE.Mesh(geometry, material);
var insertionPoint = new THREE.Vector3().copy(point).add(offset).add(insertionOffset);
bulletHoleMesh.position.copy(insertionPoint);
scene.add(bulletHoleMesh);
bulletHoles.push(bulletHoleMesh);
// Fade out the mesh gradually over time
var opacity = 1.0;
var fadeOutDuration = 5000; // 5 seconds
var fadeOutInterval = 50; // Update every 50 milliseconds
var fadeOutTimer = setInterval(function () {
opacity -= fadeOutInterval / fadeOutDuration;
if (opacity <= 0) {
opacity = 0;
clearInterval(fadeOutTimer);
scene.remove(bulletHoleMesh);
bulletHoles.splice(bulletHoles.indexOf(bulletHoleMesh), 1);
}
bulletHoleMesh.material.opacity = opacity;
}, fadeOutInterval);
}
});
}
// Function to toggle the light on or off based on the isFiring variable
function toggleLight(isFiring) {
if (isFiring) {
tommyGunLight.visible = !tommyGunLight.visible; // Toggle the light visibility
} else {
tommyGunLight.visible = false; // Ensure the light is off when not firing
}
}
// Call the function whenever the value of isFiring changes
function updateGunMuzzleFlash(position) {
toggleLight(isFiring);
tommyGunLight.position.copy(camera.position)
}
// Function to create a bullets
function createBullet(position, direction) {
//play machine gun sound bite
playMachineGunSound();
const bulletGeometry = new THREE.SphereGeometry(0.01, 8, 8);
const bulletMaterial = new THREE.MeshBasicMaterial({
color: 0xFFFFFF,
transparent: true,
opacity: 0.5
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
bullet.position.copy(position);
bullet.direction = direction.clone().normalize();
bullet.distanceTraveled = 0;
// Add a point light to the bullet
const pointLight = new THREE.PointLight(0xFFFFFF, 10, 100);
pointLight.position.copy(position);
bullet.add(pointLight);
scene.add(bullet);
bullets.push(bullet);
}
// Function to update bullets
function updateBullets() {
const maxDistance = 5; // Maximum distance a bullet can travel before removal
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.position.addScaledVector(bullet.direction, .75); // Adjust the speed of the bullet here
bullet.distanceTraveled += 0.4;
if (bullet.distanceTraveled >= maxDistance) {
scene.remove(bullet);
bullets.splice(i, 1);
}
}
}
// Variables for audio context and machine gun sound
let audioContext;
let machineGunSoundBuffer;
let bulletRicochetSoundBuffer;
// Function to load an audio file
function loadAudioFile(url, callback) {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function () {
audioContext.decodeAudioData(request.response, function (buffer) {
if (typeof callback === 'function') {
callback(buffer);
}
});
};
request.send();
}
// Function to play a sound from a buffer
function playSound(buffer, volume, loop = false) {
const source = audioContext.createBufferSource();
const gainNode = audioContext.createGain();
// Connect the audio nodes
source.connect(gainNode);
gainNode.connect(audioContext.destination);
// Set the buffer, volume, and loop
source.buffer = buffer;
gainNode.gain.value = volume;
// Start playing the sound
source.start();
}
// Function to play the machine gun sound
function playMachineGunSound() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (!machineGunSoundBuffer) {
loadAudioFile('https://www.shanebrumback.com/sounds/tommy-gun-single-bullet.mp3', function (buffer) {
machineGunSoundBuffer = buffer;
playSound(buffer, 1, isFiring); // Pass the isFiring value to control continuous playback
});
} else {
playSound(machineGunSoundBuffer, 1, isFiring); // Pass the isFiring value to control continuous playback
}
}
// Function to play the bullet ricochet sound
function playBulletRicochetSound() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (!bulletRicochetSoundBuffer) {
loadAudioFile('https://www.shanebrumback.com/sounds/bullet-ricochet.mp3', function (buffer) {
bulletRicochetSoundBuffer = buffer;
playSound(buffer, 1, false); // Play the sound once, not continuous playback
});
} else {
playSound(bulletRicochetSoundBuffer, 1, false); // Play the sound once, not continuous playback
}
}
// Event listener for mouse down event
document.addEventListener('mousedown', function (event) {
// Check if the left mouse button is pressed (button code 0)
if (controls.isLocked && event.button === 0 && event.target.id !== 'playButton') {
playMachineGunSound();
}
});
// Event listener for mouse up event
document.addEventListener('mouseup', function (event) {
// Check if the left mouse button is released (button code 0)
if (event.button === 0) {
tommyGunLight.visible = false;
isFiring = false;
}
});
</script>
</body>
</html>

Three.js Example Code and Projects

Crosshair
Interactive 3D Object Drag Model Rotate Mouse Wheel Double Click to Load Mobile Controller or Hyperlink to another page.
Interactive 3D Object Drag Model Rotate Mouse Wheel Double Click to Load Mobile Controller or Hyperlink to another page.
Interactive 3D Object Drag Model Rotate Mouse Wheel Double Click to Load Mobile Controller or Hyperlink to another page.