// WASD to move //Left click to add a block //Right click/hold to remove the block //Spacebar to jump //R to Respawn window.addEventListener("DOMContentLoaded",app); function app() { var canvas, scene, camera, renderer, raycaster, textureLoader, world, blocks, player, controlConfig = { backwards: 83, forwards: 87, left: 65, right: 68, jump: 32, respawn: 82 }, cursor = { x: 0, y: 0, inWindow: false, // mobile only holding: false, holdTime: 0, triggerHoldTime: 20 }, randomInt = (min,max) => Math.round(Math.random() * (max - min)) + min; class World { constructor() { // Warning: Larger worlds can cause lag! this.size = 12; this.layers = 2; this.skyHeight = 64; this.voidHeight = 64; this.skyColor = 0xf1f1f1; this.lightColor = 0xffffff; this.gravity = 0.008; this.lighting = { ambient: new THREE.AmbientLight(this.lightColor,0.75), directional: new THREE.DirectionalLight(this.lightColor,0.5) }; // config ambient this.lighting.ambient.name = "Ambient Light"; scene.add(this.lighting.ambient); // config directional let sizeHalf = this.size/2; this.lighting.directional.name = "Directional Light"; this.lighting.directional.position.set(0,this.skyHeight,0); this.lighting.directional.castShadow = true; this.lighting.directional.shadow.camera = new THREE.OrthographicCamera( -sizeHalf - 1,sizeHalf + 1,sizeHalf + 1,-sizeHalf,0.5,2e4 ); this.lighting.directional.shadow.mapSize = new THREE.Vector2(1024,1024); scene.add(this.lighting.directional); // blocks this.addBlock = (x,y,z,block,rad = 0) => { let newBlock = block.mesh.clone(), deg = rad * 180/Math.PI, // for multi-texture blocks, angle should be at nearest 90° rotY = block.multiTexture ? Math.round(deg / 90) * 90 : 0; // ensure angles to left are positive, right are negative if (rotY >= 360 || rotY <= -360) rotY %= 360; if (rotY == 270) rotY = -90; else if (rotY == -270) rotY = 90; newBlock.position.set(x,y,z); newBlock.rotation.y = rotY * Math.PI/180; scene.add(newBlock); }; // add them to world let sizeStart = !(this.size % 2) ? -sizeHalf : Math.round(-sizeHalf), layers = this.layers; for (let z = sizeStart; z < sizeHalf; ++z) { for (let y = 0; y < layers; ++y) { for (let x = sizeStart; x < sizeHalf; ++x) { // to ground level if (y == layers - 1) { this.addBlock(x,y,z,blocks[0]); // under ground level } else { let randBlockID = randomInt(1,blocks.length - 1); this.addBlock(x,y,z,blocks[randBlockID]); } } } } } } class Block { constructor(name,multiTexture = false) { var blockProtoGeo = new THREE.BoxBufferGeometry(1,1,1), blockMat, blockColor = 0xffffff, blockSpriteAtlas = name; if (multiTexture === true) { blockMat = []; for (let b in blockSpriteAtlas) { let path = blockSpriteAtlas[b], sprite = new THREE.MeshLambertMaterial({ color: blockColor, map: textureLoader.load(path) }); sprite.map.wrapS = THREE.RepeatWrapping; sprite.map.wrapT = THREE.RepeatWrapping; sprite.map.minFilter = THREE.NearestMipMapNearestFilter; sprite.map.magFilter = THREE.NearestFilter; sprite.map.repeat.set(1,1); blockMat.push(sprite); } } else { let path = blockSpriteAtlas.side, sprite = new THREE.MeshLambertMaterial({ color: blockColor, map: textureLoader.load(path) }); sprite.map.wrapS = THREE.RepeatWrapping; sprite.map.wrapT = THREE.RepeatWrapping; sprite.map.minFilter = THREE.NearestMipMapNearestFilter; sprite.map.magFilter = THREE.NearestFilter; sprite.map.repeat.set(1,1); blockMat = sprite; } this.multiTexture = multiTexture; this.mesh = new THREE.Mesh(blockProtoGeo,blockMat); this.mesh.material.needsUpdate = true; this.mesh.castShadow = true; this.mesh.receiveShadow = true; } } class Player { constructor(name,x = 0,y = 0,z = 0,preserveCam = false) { this.x = x; this.y = y; this.z = z; this.height = 1.7; this.width = 0.25; this.depth = 0.25; this.pitch = 0; this.yaw = 0; this.speed = 0.05; this.velocity = 0; this.jumpSpeed = 0.13; this.jumpVelocity = 0; this.xdir = ""; this.zdir = ""; this.attackRange = 5; this.lookingAt = null; this.jumping = false; this.onGround = false; this.dead = false; // interaction this.blockFaceHL = { mesh: new THREE.Mesh( new THREE.PlaneBufferGeometry(1,1), new THREE.MeshLambertMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }) ), dir: "" }; this.blockFaceHL.mesh.name = "Block Face Highlight"; this.highlightBlock = () => { raycaster.setFromCamera({x: 0, y: 0},camera); let intersects = raycaster.intersectObjects(scene.children), intersected = intersects.filter( child => child.object.type == "Mesh" && child.distance > this.depth && child.distance < this.attackRange && child.object.name != this.blockFaceHL.mesh.name ), firstBlock = intersected[0]; if (intersected.length) { if (this.lookingAt == null) scene.add(this.blockFaceHL.mesh); this.lookingAt = firstBlock; let fbObj = firstBlock.object, fbObjRot = fbObj.rotation.y; this.blockFaceHL.mesh.position.set(fbObj.position.x,fbObj.position.y,fbObj.position.z); this.blockFaceHL.mesh.rotation.set(0,0,0); let face = firstBlock.faceIndex, minOpacity = 0.16, maxOpacity = 0.84, opacityRange = maxOpacity - minOpacity, ms = opacityRange * 1e3, opacityFromMin = new Date() % ms / ms, zFightFix = 1e-3; this.blockFaceHL.mesh.material.opacity = (opacityFromMin > maxOpacity/2 ? maxOpacity - opacityFromMin : opacityFromMin) + minOpacity; // face directions: 0,1—right, 2,3—left, 4,5—top, 6,7—bottom, 8,9—front, 10,11—back if ( (face < 2 && fbObjRot == 0) || (face >= 2 && face < 4 && Math.abs(fbObjRot) == Math.PI) || (face >= 8 && face < 10 && fbObjRot == Math.PI/2) || (face >= 10 && face < 12 && fbObjRot == -Math.PI/2) ) { this.blockFaceHL.mesh.position.x += 0.5 + zFightFix; this.blockFaceHL.mesh.rotation.y += Math.PI/2; this.blockFaceHL.dir = "east"; } else if ( (face < 2 && Math.abs(fbObjRot) == Math.PI) || (face >= 2 && face < 4 && fbObjRot == 0) || (face >= 8 && face < 10 && fbObjRot == -Math.PI/2) || (face >= 10 && face < 12 && fbObjRot == Math.PI/2) ) { this.blockFaceHL.mesh.position.x -= 0.5 + zFightFix; this.blockFaceHL.mesh.rotation.y -= Math.PI/2; this.blockFaceHL.dir = "west"; } else if (face >= 4 && face < 6) { this.blockFaceHL.mesh.position.y += 0.5 + zFightFix; this.blockFaceHL.mesh.rotation.x -= Math.PI/2; this.blockFaceHL.dir = "above"; } else if (face >= 6 && face < 8) { this.blockFaceHL.mesh.position.y -= 0.5 + zFightFix; this.blockFaceHL.mesh.rotation.x += Math.PI/2; this.blockFaceHL.dir = "below"; } else if ( (face < 2 && fbObjRot == -Math.PI/2) || (face >= 2 && face < 4 && fbObjRot == Math.PI/2) || (face >= 8 && face < 10 && fbObjRot == 0) || (face >= 10 && face < 12 && Math.abs(fbObjRot) == Math.PI) ) { this.blockFaceHL.mesh.position.z += 0.5 + zFightFix; this.blockFaceHL.dir = "south"; } else if ( (face < 2 && fbObjRot == Math.PI/2) || (face >= 2 && face < 4 && fbObjRot == -Math.PI/2) || (face >= 8 && face < 10 && Math.abs(fbObjRot) == Math.PI) || (face >= 10 && face < 12 && fbObjRot == 0) ) { this.blockFaceHL.mesh.position.z -= 0.5 + zFightFix; this.blockFaceHL.mesh.rotation.x -= Math.PI; this.blockFaceHL.dir = "north"; } } else if (this.lookingAt != null) { scene.remove(this.blockFaceHL.mesh); this.blockFaceHL.dir = ""; this.lookingAt = null; } }; this.lookAround = e => { if (cursor.inWindow) { let center = { x: window.innerWidth/2, y: window.innerHeight/2 }, maxPitch = Math.PI/2, sensitivity = 4; if (e) { cursor.x += e.movementX; cursor.y += e.movementY; } this.pitch = -Math.atan((cursor.y - center.y) / center.y) * sensitivity; if (this.pitch < -maxPitch) this.pitch = -maxPitch; else if (this.pitch > maxPitch) this.pitch = maxPitch this.yaw = -Math.atan((cursor.x - center.x) / center.x) * sensitivity; } }; this.build = e => { // destroy block if ((e && e.button === 2) || cursor.holdTime == cursor.triggerHoldTime) { if (this.lookingAt != null) scene.remove(this.lookingAt.object); // place block } else if ((e && e.button === 0) || (cursor.holdTime > 0 && cursor.holdTime < cursor.triggerHoldTime)) { let at = this.lookingAt; if (at != null) { let face = at.faceIndex, pos = at.object.position, rot = at.object.rotation, placeX = pos.x, placeY = pos.y, placeZ = pos.z; switch (this.blockFaceHL.dir) { case "east": ++placeX; break; case "west": --placeX; break; case "above": ++placeY; break; case "below": --placeY; break; case "south": ++placeZ; break; case "north": --placeZ; break; default: break; } let xr = Math.round(this.x), yr = Math.round(this.y), zr = Math.round(this.z), pxr = Math.round(placeX), pyr = Math.round(placeY), pzr = Math.round(placeZ); // prevent placement in same position, touching player, and outside vertical boundaries if ( !(xr == pxr && (yr == pyr || yr == pyr - ~~this.height) && zr == pzr) && (pxr >= -world.size/2 && pxr < world.size/2) && (pyr >= 0 && pyr < world.skyHeight) && (pzr >= -world.size/2 && pzr < world.size/2) ) { let layers = world.layers - 1, currentY = Math.floor(placeY); if (currentY == layers) { world.addBlock(placeX,placeY,placeZ,blocks[0],player.yaw); } else { let randBlockID = randomInt(1,blocks.length - 1); world.addBlock(placeX,placeY,placeZ,blocks[randBlockID],player.yaw); } } } } }; this.xmove = e => { if (e.keyCode == controlConfig.left) this.xdir = "left"; else if (e.keyCode == controlConfig.right) this.xdir = "right"; }; this.jump = e => { if (e.keyCode == controlConfig.jump) this.jumping = true; }; this.zmove = e => { if (e.keyCode == controlConfig.backwards) this.zdir = "backwards"; else if (e.keyCode == controlConfig.forwards) this.zdir = "forwards"; }; this.xmoveStop = e => { if (!e || e.keyCode == controlConfig.left || e.keyCode == controlConfig.right) this.xdir = ""; }; this.jumpStop = e => { if (!e || e.keyCode == controlConfig.jump) this.jumping = false; }; this.zmoveStop = e => { if (!e || e.keyCode == controlConfig.backwards || e.keyCode == controlConfig.forwards) this.zdir = ""; }; this.midairMoveStop = () => { if (!this.onGround && this.jumpVelocity > 0) this.velocity = -this.jumpVelocity; }; this.die = e => { scene.remove(this.mesh); scene.remove(this.blockFaceHL.mesh); this.dead = true; return this.dead; }; this.updatePosition = () => { // moving let move = this.velocity < 0 ? 0 : this.velocity, rate = 0.01, yaw = this.yaw; if (this.xdir == "left") { this.x -= move * Math.cos(yaw); this.z += move * Math.sin(yaw); } else if (this.xdir == "right") { this.x += move * Math.cos(yaw); this.z -= move * Math.sin(yaw); } if (this.zdir == "backwards") { this.z += move * Math.cos(yaw); this.x += move * Math.sin(yaw); } else if (this.zdir == "forwards") { this.z -= move * Math.cos(yaw); this.x -= move * Math.sin(yaw); } // accelerate movement if (this.xdir != "" || this.zdir != "") { this.velocity += rate; if (this.velocity > this.speed) this.velocity = this.speed; } else if (this.xdir == "" && this.zdir == "") { this.velocity -= rate; if (this.velocity < 0) this.velocity = 0; } // jumping, falling if (this.jumpVelocity == 0) { if (this.jumping) { this.jumpVelocity = this.jumpSpeed; this.onGround = false; } else { this.onGround = true; } } this.y += this.jumpVelocity; this.jumpVelocity -= world.gravity; // touch blocks scene.traverse(child => { let cpos = child.position, excludedNames = [this.mesh.name, this.blockFaceHL.mesh.name], xFromPlyr = Math.abs(cpos.x - this.x), yFromPlyr = cpos.y - this.y, zFromPlyr = Math.abs(cpos.z - this.z); if (child instanceof THREE.Mesh && child.name != excludedNames[0] && child.name != excludedNames[1] && xFromPlyr <= 1 && (yFromPlyr <= this.height + 1 || yFromPlyr <= 1) && zFromPlyr <= 1) collideWithBlock(this,child); }); if (this.y < -world.voidHeight) this.die(); // camera keep up camera.position.x = this.x; camera.position.y = this.y + (this.height - 0.5); camera.position.z = this.z; if (preserveCam) this.lookAround(); camera.rotation.x = this.pitch; camera.rotation.y = this.yaw; }; // entity in scene this.mesh = new THREE.Object3D(); this.mesh.name = name; this.mesh.position.set(this.x,this.y,this.z); scene.add(this.mesh); } } var init = () => { // setup scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45,window.innerWidth / window.innerHeight,0.1,2e4); renderer = new THREE.WebGLRenderer(); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; raycaster = new THREE.Raycaster(); textureLoader = new THREE.TextureLoader(); // world generation, spawn player let BLOCK = BlockList(); blocks = [ new Block(BLOCK.siliconBlock), new Block(BLOCK.blueberryIMac,true), new Block(BLOCK.bondiIMac,true), new Block(BLOCK.grapeIMac,true), new Block(BLOCK.limeIMac,true), new Block(BLOCK.macintosh128k,true), new Block(BLOCK.strawberryIMac,true), new Block(BLOCK.tangerineIMac,true) ]; world = new World(); player = new Player("Player",0,world.layers,0); // camera camera.position.set(player.x,player.y + (player.height - 0.5),player.z); camera.rotation.order = "YXZ"; camera.rotation.x = player.pitch; camera.rotation.y = player.yaw; camera.zoom = 0.5; camera.updateProjectionMatrix(); // render renderer.setClearColor(new THREE.Color(world.skyColor)); document.body.appendChild(renderer.domElement); // pointer lock canvas = renderer.domElement; canvas.requestPointerLock = canvas.requestPointerLock || canvas.mozRequestPointerLock; document.exitPointerLock = document.exitPointerLock || document.mozExitPointerLock; }, collideWithBlock = (player,block) => { let pX = player.x, pY = player.y, pZ = player.z, pW = player.width, pH = player.height, pD = player.depth, bX = block.position.x, bY = block.position.y, bZ = block.position.z, bS = 1, // block size vectorX = pX - bX, vectorY = (pY + pH/2) - bY, vectorZ = pZ - bZ, hWidths = pW/2 + bS/2, hHeights = pH/2 + bS/2, hDepths = pD/2 + bS/2; if (Math.abs(vectorX) < hWidths && Math.abs(vectorY) < hHeights && Math.abs(vectorZ) < hDepths) { let pushX = hWidths - Math.abs(vectorX), pushY = hHeights - Math.abs(vectorY), pushZ = hDepths - Math.abs(vectorZ), reqXZSpace = 0.1; // to prevent clinging of block sides if (pushZ <= pushX && pushX <= pushY) { // hit south if (vectorZ > 0) { player.z += pushZ; // hit north } else { player.z -= pushZ; } player.midairMoveStop(); } else if (pushX <= pushZ && pushZ <= pushY) { // hit east if (vectorX > 0) { player.x += pushX; // hit west } else { player.x -= pushX; } player.midairMoveStop(); } else if (pushY <= pushX && pushY <= pushZ && pushX > reqXZSpace && pushZ > reqXZSpace) { // land on top if (vectorY > 0) { player.y = bY + bS/2; player.jumpVelocity = 0; // hit bottom } else { player.y -= pushY; player.jumpVelocity -= player.jumpSpeed; } } } }, adjustWindow = () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth,window.innerHeight) }, renderScene = () => { player.updatePosition(); player.highlightBlock(); if (player.dead) player = new Player(player.name,0,world.layers,0,true); if (cursor.holding) { ++cursor.holdTime; if (cursor.holdTime == cursor.triggerHoldTime) { player.build(); } } renderer.render(scene,camera); requestAnimationFrame(renderScene); }, run = () => { let promise = new Promise((resolve,reject) => { init(); resolve("World loaded"); }); promise.then(msg => { renderScene(); console.log(msg); }); }; run(); // game control window.addEventListener("resize",adjustWindow); window.addEventListener("contextmenu",e => { e.preventDefault(); }); canvas.addEventListener("click",() => { canvas.requestPointerLock(); cursor.inWindow = true; }); window.addEventListener("keydown",e => { if (e.keyCode == 27) { document.exitPointerLock(); cursor.inWindow = false; } }); // player control window.addEventListener("touchstart",e => { cursor.holding = true; cursor.holdTime = 0; }); window.addEventListener("touchmove",e => { player.lookAround(e); cursor.holdTime = 0; }); window.addEventListener("touchend",e => { if (cursor.holdTime < cursor.triggerHoldTime) { player.build(); cursor.holding = false; } }); window.addEventListener("mousemove",e => { player.lookAround(e) }); window.addEventListener("mousedown",e => {player.build(e) }); window.addEventListener("keydown",e => { player.xmove(e) }); window.addEventListener("keydown",e => { player.jump(e) }); window.addEventListener("keydown",e => { player.zmove(e) }); window.addEventListener("keyup",e => { player.xmoveStop(e) }); window.addEventListener("keyup",e => { player.jumpStop(e) }); window.addEventListener("keyup",e => { player.zmoveStop(e) }); window.addEventListener("keydown",e => { if (e.keyCode == controlConfig.respawn && player.die()) player = new Player(player.name,0,world.layers,0,true); }); }