Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ShaneBrumback/bd9ee6bcf41d9911ff6f18fe2d713ffb to your computer and use it in GitHub Desktop.
Save ShaneBrumback/bd9ee6bcf41d9911ff6f18fe2d713ffb to your computer and use it in GitHub Desktop.
Threejs Examples First Person Shooter Game Tommy Gun Starter Code
<!--////////////////////////////////////////////////////////////////////////////////////////
/// ///
/// 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