Last active
April 30, 2025 01:43
-
-
Save ShaneBrumback/bd9ee6bcf41d9911ff6f18fe2d713ffb to your computer and use it in GitHub Desktop.
Threejs Examples First Person Shooter Game Tommy Gun Starter Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!--//////////////////////////////////////////////////////////////////////////////////////// | |
/// /// | |
/// 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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment