tunnel-racer/game.js

485 lines
12 KiB
JavaScript
Raw Normal View History

2022-01-31 12:26:35 +00:00
// ----------------------
// | setup. of three.js |
// ----------------------
// shaders for sphere outline
// https://stemkoski.github.io/Three.js/Shader-Glow.html
2022-01-30 10:45:49 +00:00
const vertexShaderGlow = `
uniform vec3 viewVector;
uniform float c;
uniform float p;
varying float intensity;
varying vec3 vColor;
vec3 hsv2rgb(vec3 c) {
2022-01-30 11:02:00 +00:00
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
2022-01-30 10:45:49 +00:00
}
void main()
{
vec3 vNormal = normalize( normalMatrix * normal );
vec3 vNormel = normalize( normalMatrix * viewVector );
intensity = pow( c - dot(vNormal, vNormel), p );
2022-01-31 12:26:35 +00:00
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
2022-01-30 10:45:49 +00:00
vColor = hsv2rgb(vec3(fract(gl_Position[2] / 270.0), 0.7, 0.7));
//vColor = vec3((sin(position[2] / 360.0) + 1.0) * 0.5, (sin(position[2] / 1000.0) + 1.0) * 0.5, (sin(position[2] / 70.0) + 1.0) * 0.5);
//vColor = vec3(0.5, 0, 0.5);
}
`;
const fragmentShaderGlow = `
varying vec3 vColor;
varying float intensity;
void main()
{
vec3 glow = vColor * intensity;
2022-01-30 11:02:00 +00:00
gl_FragColor = vec4( glow, 1.0 );
2022-01-30 10:45:49 +00:00
}
`;
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// shaders for tunnel border
2022-01-30 11:02:00 +00:00
const fragmentShader = `
#include <common>
uniform vec3 iResolution;
uniform float iTime;
uniform sampler2D iChannel0;
// By Daedelus: https://www.shadertoy.com/user/Daedelus
// license: Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
#define TIMESCALE 0.25
#define TILES 8
#define COLOR 0.7, 1.6, 2.8
varying vec2 vUv;
varying float z;
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord.xy / iResolution.xy;
uv.x *= iResolution.x / iResolution.y;
vec4 noise = texture2D(iChannel0, floor(uv * float(TILES)) / float(TILES));
float p = 1.0 - mod(noise.r + noise.g + noise.b + (iTime + z) * float(TIMESCALE), 1.0);
p = min(max(p * 3.0 - 1.8, 0.1), 2.0);
vec2 r = mod(uv * float(TILES), 1.0);
r = vec2(pow(r.x - 0.5, 2.0), pow(r.y - 0.5, 2.0));
p *= 1.0 - pow(min(1.0, 12.0 * dot(r, r)), 2.0);
fragColor = vec4(COLOR, 1.0) * p;
}
void main() {
mainImage(gl_FragColor, vUv * iResolution.xy);
}
`;
const vertexShader = `
varying vec2 vUv;
varying float z;
void main() {
vUv = uv;
z = -position[1];
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
2022-01-31 12:26:35 +00:00
// size of the tunnel
const tunnelRadius = 100;
// z interval of new obstacles
2022-01-30 11:24:09 +00:00
const spawnInterval = 40;
2022-01-31 12:26:35 +00:00
// coordinate of newest obstacle
2022-01-30 11:24:09 +00:00
let lastSpawned = 0;
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// gyroscope variables
2022-01-30 11:02:00 +00:00
var gn;
2022-01-31 08:15:16 +00:00
let headSet = false;
let gammaReference = 0.0;
let leftRightMove = 0.0;
let upDownMove = 0.0;
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// current player speed
const initialSpeed = 5.0;
let speed = initialSpeed;
// score points
2022-01-30 11:02:00 +00:00
let score = 0;
2022-01-31 12:26:35 +00:00
// amount of sphere objects removed
2022-01-30 11:02:00 +00:00
let removed = 0;
2022-01-31 12:26:35 +00:00
// player is actively racing
2022-01-30 11:02:00 +00:00
let running = false;
2022-01-31 12:26:35 +00:00
// obstacles
const spheres = [];
// wall segments
const borders = [];
2022-01-30 11:02:00 +00:00
let frameCount = 0;
function logger(text) {
console.log(text);
}
function init_gn() {
2022-01-31 12:26:35 +00:00
const args = {
2022-01-30 11:02:00 +00:00
logger: logger
};
gn = new GyroNorm();
gn.init(args).then(function() {
2022-01-31 12:26:35 +00:00
const isAvailable = gn.isAvailable();
2022-01-30 11:02:00 +00:00
if (!isAvailable.deviceOrientationAvailable) {
console.log({
message: 'Device orientation is not available.'
});
}
if (!isAvailable.accelerationAvailable) {
console.log({
message: 'Device acceleration is not available.'
});
}
if (!isAvailable.accelerationIncludingGravityAvailable) {
console.log({
message: 'Device acceleration incl. gravity is not available.'
});
}
if (!isAvailable.rotationRateAvailable) {
console.log({
message: 'Device rotation rate is not available.'
});
}
start_gn();
}).catch(function(e) {
2022-01-31 17:45:39 +00:00
console.error(e);
document.getElementById("start").disabled = false;
2022-01-30 11:02:00 +00:00
});
document.addEventListener('keydown', e => {
if (e.key === "ArrowLeft") {
leftRightMove = -10.0;
} else if (e.key === "ArrowRight") {
leftRightMove = 10.0;
} else if (e.key === "ArrowDown") {
upDownMove = -10.0;
} else if (e.key === "ArrowUp") {
upDownMove = 10.0;
}
});
document.addEventListener('keyup', e => {
if (e.key === "ArrowLeft") {
leftRightMove = 0;
} else if (e.key === "ArrowRight") {
leftRightMove = 0;
} else if (e.key === "ArrowDown") {
upDownMove = 0;
} else if (e.key === "ArrowUp") {
upDownMove = 0;
}
});
}
function stop_gn() {
gn.stop();
}
function start_gn() {
gn.start(gnCallBack);
}
function gnCallBack(data) {
if (!headSet) {
2022-01-31 08:15:16 +00:00
gammaReference = data.do.gamma;
2022-01-30 11:02:00 +00:00
} else {
2022-01-31 08:15:16 +00:00
// leftRight = data.do.beta > 0 ? "right" : "left";
leftRightMove = data.do.beta;
// upDown = data.do.gamma > gammaReference ? "up" : "down";
upDownMove = data.do.gamma - gammaReference;
2022-01-30 11:02:00 +00:00
}
}
function set_head_gn() {
2022-01-31 17:45:39 +00:00
try {
gn.setHeadDirection();
} catch (e) {
// fails if the sensor is not available
}
2022-01-30 11:02:00 +00:00
headSet = true;
}
init_gn();
2022-01-31 12:26:35 +00:00
// setup three.js: materials, geometries, scene, lighting and camera
2022-01-30 11:02:00 +00:00
let scene, renderer;
2022-01-31 17:45:39 +00:00
scene = new THREE.Scene();
2022-01-30 11:24:09 +00:00
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 10000);
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// obstacle geometry and material
2022-01-30 11:02:00 +00:00
const geometrySphere = new THREE.SphereGeometry(11, 32, 32);
2022-01-31 12:26:35 +00:00
const materialSphere = new THREE.MeshBasicMaterial();
2022-01-30 11:02:00 +00:00
const customMaterial = new THREE.ShaderMaterial({
uniforms: {
"c": {
type: "f",
value: 0.8
},
"p": {
type: "f",
value: 2.4
},
viewVector: {
type: "v3",
value: camera.position
}
},
vertexShader: vertexShaderGlow,
fragmentShader: fragmentShaderGlow,
side: THREE.FrontSide,
blending: THREE.AdditiveBlending,
transparent: true
});
2022-01-31 12:26:35 +00:00
// wall texture
2022-01-30 11:02:00 +00:00
const loader = new THREE.TextureLoader();
2022-01-31 09:33:58 +00:00
const texture = loader.load('./bayer.png');
2022-01-30 11:02:00 +00:00
texture.minFilter = THREE.NearestFilter;
texture.magFilter = THREE.NearestFilter;
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
const uniforms = {
iTime: {
value: 0
},
iResolution: {
value: new THREE.Vector3(1, 1, 1)
},
iChannel0: {
value: texture
},
};
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
});
const borderGeometry = new THREE.PlaneGeometry(1, 1);
2022-01-31 17:45:39 +00:00
2022-01-30 11:02:00 +00:00
init();
animate();
document.getElementById("start").onclick = () => {
2022-01-31 17:45:39 +00:00
set_head_gn();
try {
document.body.requestFullscreen();
} catch (e) {
// browser doesn't support this API, try another hack
window.scrollTo(0,1);
}
2022-01-30 11:02:00 +00:00
document.getElementById("start").style.zIndex = -10;
document.getElementById("start").style.visibility = "hidden";
2022-01-31 12:26:35 +00:00
// reset variables
speed = initialSpeed;
2022-01-30 11:02:00 +00:00
score = 0;
2022-01-31 12:26:35 +00:00
for (let i = 0; i < spheres.length; i++) {
scene.remove(spheres[i]);
2022-01-30 11:02:00 +00:00
}
2022-01-31 12:26:35 +00:00
spheres.length = 0;
2022-01-31 17:45:39 +00:00
for (let i = 0; i < borders.length; i++) {
scene.remove(borders[i]);
}
borders.length = 0;
2022-01-30 11:02:00 +00:00
removed = 0;
2022-01-31 17:45:39 +00:00
lastSpawned = 0;
frameCount = 0;
camera.position.set(0, 10, 25);
camera.lookAt(scene.position);
2022-01-30 11:02:00 +00:00
running = true;
}
function gameOver() {
running = false;
2022-01-31 17:45:39 +00:00
headSet = false;
2022-01-30 11:02:00 +00:00
document.getElementById("score").innerText = document.getElementById("score").innerText + " - Game Over!";
document.getElementById("start").style.zIndex = 0;
document.getElementById("start").style.visibility = "visible";
document.getElementById("start").innerText = "Restart";
}
function init() {
camera.position.set(0, 10, 25);
camera.lookAt(scene.position);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 0.8);
camera.add(pointLight);
scene.add(camera);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize);
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
2022-01-31 12:26:35 +00:00
// main render/update function called once per frame
2022-01-30 11:02:00 +00:00
function animate() {
requestAnimationFrame(animate);
uniforms.iTime.value = frameCount / 60.0;
frameCount++;
if (running) {
2022-01-31 12:26:35 +00:00
if (Math.abs(camera.position.x) > tunnelRadius || Math.abs(camera.position.y) > tunnelRadius) {
2022-01-31 08:15:16 +00:00
// out of bounds
gameOver();
}
2022-01-31 12:26:35 +00:00
// collision checks
for (let i = 0; i < spheres.length; i++) {
if (spheres[i].material !== materialSphere) {
2022-01-30 11:02:00 +00:00
continue;
}
2022-01-31 12:26:35 +00:00
let x1 = spheres[i].position.x;
let y1 = spheres[i].position.y;
let z1 = spheres[i].position.z;
2022-01-30 11:02:00 +00:00
let x2 = camera.position.x;
let y2 = camera.position.y;
let z2 = camera.position.z;
if (z2 > z1) {
// account for very fast speeds
// (prevents clipping through obstacles)
z2 = Math.max(z1, z2 - speed);
2022-01-30 10:45:49 +00:00
}
2022-01-30 11:02:00 +00:00
let dist_squared = (x1 - x2) ** 2 + (y1 - y2) ** 2 + (z1 - z2) ** 2;
2022-01-31 12:26:35 +00:00
if (dist_squared <= (spheres[i].geometry.parameters.radius * spheres[i].scale.x) ** 2) {
2022-01-30 11:02:00 +00:00
gameOver();
break;
2022-01-30 10:45:49 +00:00
}
2022-01-30 11:02:00 +00:00
}
2022-01-31 08:15:16 +00:00
}
if (running) {
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// advance player position, increase speed, handle movement input
2022-01-30 11:02:00 +00:00
camera.position.z -= 0.5 * speed;
speed += 0.01;
2022-01-31 08:15:16 +00:00
if (!Number.isNaN(leftRightMove)) {
camera.position.x += 0.1 * leftRightMove;
}
if (!Number.isNaN(upDownMove)) {
camera.position.y += 0.1 * upDownMove;
}
2022-01-30 11:02:00 +00:00
2022-01-31 12:26:35 +00:00
// create new obstacles as needed
2022-01-30 11:24:09 +00:00
while (camera.position.z < lastSpawned - spawnInterval) {
lastSpawned -= spawnInterval;
// randomly spawn large spheres
let scale = Math.random() < 0.1 ? 5.0 : 1.0;
2022-01-31 12:26:35 +00:00
const meshCube = new THREE.Mesh(geometrySphere, materialSphere);
2022-01-30 11:24:09 +00:00
meshCube.position.z = camera.position.z - speed * 210;
2022-01-31 12:26:35 +00:00
meshCube.position.x = (2 * Math.random() - 1.0) * tunnelRadius;
meshCube.position.y = (2 * Math.random() - 1.0) * 0.9 * tunnelRadius;
2022-01-30 11:24:09 +00:00
meshCube.scale.multiplyScalar(scale);
2022-01-30 11:02:00 +00:00
const outlineMesh = new THREE.Mesh(geometrySphere, customMaterial);
2022-01-30 11:24:09 +00:00
outlineMesh.scale.multiplyScalar(scale * 1.17);
2022-01-30 11:02:00 +00:00
outlineMesh.position.z = meshCube.position.z;
outlineMesh.position.x = meshCube.position.x;
outlineMesh.position.y = meshCube.position.y;
meshCube.add(outlineMesh);
scene.add(outlineMesh);
2022-01-31 12:26:35 +00:00
spheres.push(outlineMesh);
2022-01-30 11:02:00 +00:00
scene.add(meshCube);
2022-01-31 12:26:35 +00:00
spheres.push(meshCube);
2022-01-30 11:02:00 +00:00
}
}
renderer.render(scene, camera);
2022-01-31 12:26:35 +00:00
// clean up game elements behind the camera
for (let i = 0; i < spheres.length; i++) {
if (spheres[i].position.z > camera.position.z + spheres[i].geometry.parameters.radius * spheres[i].scale.x + 20) {
scene.remove(spheres[i]);
spheres.splice(i, 1);
2022-01-30 11:02:00 +00:00
i--;
removed++;
}
}
if (removed >= 2 || score == 0) {
score += removed / 2;
2022-01-30 11:29:00 +00:00
removed = 0;
2022-01-30 11:02:00 +00:00
document.getElementById("score").innerText = score;
}
2022-01-31 12:26:35 +00:00
// create new wall segments on demand
2022-01-30 11:24:09 +00:00
if (borders.length == 0 || borders[borders.length - 1].position.z - camera.position.z > -1200) {
2022-01-31 12:26:35 +00:00
const newZ = borders.length == 0 ? -200 : borders[borders.length - 1].position.z - tunnelRadius * 2;
2022-01-30 11:02:00 +00:00
let border = new THREE.Mesh(borderGeometry, material);
2022-01-31 12:26:35 +00:00
border.position.y = -tunnelRadius;
2022-01-30 11:02:00 +00:00
border.rotation.x = -Math.PI / 2;
border.position.z = newZ;
2022-01-31 12:26:35 +00:00
border.scale.multiplyScalar(tunnelRadius * 2);
2022-01-30 11:02:00 +00:00
scene.add(border);
borders.push(border);
border = new THREE.Mesh(borderGeometry, material);
2022-01-31 12:26:35 +00:00
border.position.y = tunnelRadius;
2022-01-30 11:02:00 +00:00
border.rotation.x = Math.PI / 2;
border.position.z = newZ;
2022-01-31 12:26:35 +00:00
border.scale.multiplyScalar(tunnelRadius * 2);
2022-01-30 11:02:00 +00:00
scene.add(border);
borders.push(border);
border = new THREE.Mesh(borderGeometry, material);
2022-01-31 12:26:35 +00:00
border.position.x = -tunnelRadius;
2022-01-30 11:02:00 +00:00
border.rotation.y = Math.PI / 2;
border.position.z = newZ;
2022-01-31 12:26:35 +00:00
border.scale.multiplyScalar(tunnelRadius * 2);
2022-01-30 11:02:00 +00:00
scene.add(border);
borders.push(border);
border = new THREE.Mesh(borderGeometry, material);
2022-01-31 12:26:35 +00:00
border.position.x = tunnelRadius;
2022-01-30 11:02:00 +00:00
border.rotation.y = -Math.PI / 2;
border.position.z = newZ;
2022-01-31 12:26:35 +00:00
border.scale.multiplyScalar(tunnelRadius * 2);
2022-01-30 11:02:00 +00:00
scene.add(border);
borders.push(border);
}
2022-01-31 12:26:35 +00:00
// clean up wall segments behind the camera
2022-01-30 11:02:00 +00:00
for (let i = 0; i < borders.length; i++) {
2022-01-31 12:26:35 +00:00
if (borders[i].position.z > camera.position.z + tunnelRadius + 50) {
2022-01-30 11:02:00 +00:00
scene.remove(borders[i]);
borders.splice(i, 1);
i--;
}
}
2022-01-30 11:53:10 +00:00
}