An interactive tech demo inspired by Crossy Road using three.js where I put together a scene with an orthographic camera that follows the player, a few simplified 3D objects with some texture, lights and shadows, and a basic game logic that randomly generates a level that you can walk through.
Created
January 26, 2021 06:23
-
-
Save vineeth-pappu/fe2af2414a7565c9fb29eea1c200ce66 to your computer and use it in GitHub Desktop.
Crossy Road with three.js
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
| <div id="counter">0</div> | |
| <div id="controlls"> | |
| <div> | |
| <button id="forward"> | |
| <svg width="30" height="30" viewBox="0 0 10 10"> | |
| <g transform="rotate(0, 5,5)"> | |
| <path d="M5,4 L7,6 L3,6 L5,4" /> | |
| </g> | |
| </svg> | |
| </button> | |
| <button id="left"> | |
| <svg width="30" height="30" viewBox="0 0 10 10"> | |
| <g transform="rotate(-90, 5,5)"> | |
| <path d="M5,4 L7,6 L3,6 L5,4" /> | |
| </g> | |
| </svg> | |
| </button> | |
| <button id="backward"> | |
| <svg width="30" height="30" viewBox="0 0 10 10"> | |
| <g transform="rotate(180, 5,5)"> | |
| <path d="M5,4 L7,6 L3,6 L5,4" /> | |
| </g> | |
| </svg> | |
| </button> | |
| <button id="right"> | |
| <svg width="30" height="30" viewBox="0 0 10 10"> | |
| <g transform="rotate(90, 5,5)"> | |
| <path d="M5,4 L7,6 L3,6 L5,4" /> | |
| </g> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| <div id="end"> | |
| <button id="retry">Retry</button> | |
| </div> |
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
| const counterDOM = document.getElementById('counter'); | |
| const endDOM = document.getElementById('end'); | |
| const scene = new THREE.Scene(); | |
| const distance = 500; | |
| const camera = new THREE.OrthographicCamera( window.innerWidth/-2, window.innerWidth/2, window.innerHeight / 2, window.innerHeight / -2, 0.1, 10000 ); | |
| camera.rotation.x = 50*Math.PI/180; | |
| camera.rotation.y = 20*Math.PI/180; | |
| camera.rotation.z = 10*Math.PI/180; | |
| const initialCameraPositionY = -Math.tan(camera.rotation.x)*distance; | |
| const initialCameraPositionX = Math.tan(camera.rotation.y)*Math.sqrt(distance**2 + initialCameraPositionY**2); | |
| camera.position.y = initialCameraPositionY; | |
| camera.position.x = initialCameraPositionX; | |
| camera.position.z = distance; | |
| const zoom = 2; | |
| const chickenSize = 15; | |
| const positionWidth = 42; | |
| const columns = 17; | |
| const boardWidth = positionWidth*columns; | |
| const stepTime = 200; // Miliseconds it takes for the chicken to take a step forward, backward, left or right | |
| let lanes; | |
| let currentLane; | |
| let currentColumn; | |
| let previousTimestamp; | |
| let startMoving; | |
| let moves; | |
| let stepStartTimestamp; | |
| const carFrontTexture = new Texture(40,80,[{x: 0, y: 10, w: 30, h: 60 }]); | |
| const carBackTexture = new Texture(40,80,[{x: 10, y: 10, w: 30, h: 60 }]); | |
| const carRightSideTexture = new Texture(110,40,[{x: 10, y: 0, w: 50, h: 30 }, {x: 70, y: 0, w: 30, h: 30 }]); | |
| const carLeftSideTexture = new Texture(110,40,[{x: 10, y: 10, w: 50, h: 30 }, {x: 70, y: 10, w: 30, h: 30 }]); | |
| const truckFrontTexture = new Texture(30,30,[{x: 15, y: 0, w: 10, h: 30 }]); | |
| const truckRightSideTexture = new Texture(25,30,[{x: 0, y: 15, w: 10, h: 10 }]); | |
| const truckLeftSideTexture = new Texture(25,30,[{x: 0, y: 5, w: 10, h: 10 }]); | |
| const generateLanes = () => [-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9].map((index) => { | |
| const lane = new Lane(index); | |
| lane.mesh.position.y = index*positionWidth*zoom; | |
| scene.add( lane.mesh ); | |
| return lane; | |
| }).filter((lane) => lane.index >= 0); | |
| const addLane = () => { | |
| const index = lanes.length; | |
| const lane = new Lane(index); | |
| lane.mesh.position.y = index*positionWidth*zoom; | |
| scene.add(lane.mesh); | |
| lanes.push(lane); | |
| } | |
| const chicken = new Chicken(); | |
| scene.add( chicken ); | |
| hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6); | |
| scene.add(hemiLight) | |
| const initialDirLightPositionX = -100; | |
| const initialDirLightPositionY = -100; | |
| dirLight = new THREE.DirectionalLight(0xffffff, 0.6); | |
| dirLight.position.set(initialDirLightPositionX, initialDirLightPositionY, 200); | |
| dirLight.castShadow = true; | |
| dirLight.target = chicken; | |
| scene.add(dirLight); | |
| dirLight.shadow.mapSize.width = 2048; | |
| dirLight.shadow.mapSize.height = 2048; | |
| var d = 500; | |
| dirLight.shadow.camera.left = - d; | |
| dirLight.shadow.camera.right = d; | |
| dirLight.shadow.camera.top = d; | |
| dirLight.shadow.camera.bottom = - d; | |
| // var helper = new THREE.CameraHelper( dirLight.shadow.camera ); | |
| // var helper = new THREE.CameraHelper( camera ); | |
| // scene.add(helper) | |
| backLight = new THREE.DirectionalLight(0x000000, .4); | |
| backLight.position.set(200, 200, 50); | |
| backLight.castShadow = true; | |
| scene.add(backLight) | |
| const laneTypes = ['car', 'truck', 'forest']; | |
| const laneSpeeds = [2, 2.5, 3]; | |
| const vechicleColors = [0xa52523, 0xbdb638, 0x78b14b]; | |
| const threeHeights = [20,45,60]; | |
| const initaliseValues = () => { | |
| lanes = generateLanes() | |
| currentLane = 0; | |
| currentColumn = Math.floor(columns/2); | |
| previousTimestamp = null; | |
| startMoving = false; | |
| moves = []; | |
| stepStartTimestamp; | |
| chicken.position.x = 0; | |
| chicken.position.y = 0; | |
| camera.position.y = initialCameraPositionY; | |
| camera.position.x = initialCameraPositionX; | |
| dirLight.position.x = initialDirLightPositionX; | |
| dirLight.position.y = initialDirLightPositionY; | |
| } | |
| initaliseValues(); | |
| const renderer = new THREE.WebGLRenderer({ | |
| alpha: true, | |
| antialias: true | |
| }); | |
| renderer.shadowMap.enabled = true; | |
| renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
| renderer.setSize( window.innerWidth, window.innerHeight ); | |
| document.body.appendChild( renderer.domElement ); | |
| function Texture(width, height, rects) { | |
| const canvas = document.createElement( "canvas" ); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const context = canvas.getContext( "2d" ); | |
| context.fillStyle = "#ffffff"; | |
| context.fillRect( 0, 0, width, height ); | |
| context.fillStyle = "rgba(0,0,0,0.6)"; | |
| rects.forEach(rect => { | |
| context.fillRect(rect.x, rect.y, rect.w, rect.h); | |
| }); | |
| return new THREE.CanvasTexture(canvas); | |
| } | |
| function Wheel() { | |
| const wheel = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 12*zoom, 33*zoom, 12*zoom ), | |
| new THREE.MeshLambertMaterial( { color: 0x333333, flatShading: true } ) | |
| ); | |
| wheel.position.z = 6*zoom; | |
| return wheel; | |
| } | |
| function Car() { | |
| const car = new THREE.Group(); | |
| const color = vechicleColors[Math.floor(Math.random() * vechicleColors.length)]; | |
| const main = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 60*zoom, 30*zoom, 15*zoom ), | |
| new THREE.MeshPhongMaterial( { color, flatShading: true } ) | |
| ); | |
| main.position.z = 12*zoom; | |
| main.castShadow = true; | |
| main.receiveShadow = true; | |
| car.add(main) | |
| const cabin = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 33*zoom, 24*zoom, 12*zoom ), | |
| [ | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carBackTexture } ), | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carFrontTexture } ), | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carRightSideTexture } ), | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true, map: carLeftSideTexture } ), | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true } ), // top | |
| new THREE.MeshPhongMaterial( { color: 0xcccccc, flatShading: true } ) // bottom | |
| ] | |
| ); | |
| cabin.position.x = 6*zoom; | |
| cabin.position.z = 25.5*zoom; | |
| cabin.castShadow = true; | |
| cabin.receiveShadow = true; | |
| car.add( cabin ); | |
| const frontWheel = new Wheel(); | |
| frontWheel.position.x = -18*zoom; | |
| car.add( frontWheel ); | |
| const backWheel = new Wheel(); | |
| backWheel.position.x = 18*zoom; | |
| car.add( backWheel ); | |
| car.castShadow = true; | |
| car.receiveShadow = false; | |
| return car; | |
| } | |
| function Truck() { | |
| const truck = new THREE.Group(); | |
| const color = vechicleColors[Math.floor(Math.random() * vechicleColors.length)]; | |
| const base = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 100*zoom, 25*zoom, 5*zoom ), | |
| new THREE.MeshLambertMaterial( { color: 0xb4c6fc, flatShading: true } ) | |
| ); | |
| base.position.z = 10*zoom; | |
| truck.add(base) | |
| const cargo = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 75*zoom, 35*zoom, 40*zoom ), | |
| new THREE.MeshPhongMaterial( { color: 0xb4c6fc, flatShading: true } ) | |
| ); | |
| cargo.position.x = 15*zoom; | |
| cargo.position.z = 30*zoom; | |
| cargo.castShadow = true; | |
| cargo.receiveShadow = true; | |
| truck.add(cargo) | |
| const cabin = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 25*zoom, 30*zoom, 30*zoom ), | |
| [ | |
| new THREE.MeshPhongMaterial( { color, flatShading: true } ), // back | |
| new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckFrontTexture } ), | |
| new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckRightSideTexture } ), | |
| new THREE.MeshPhongMaterial( { color, flatShading: true, map: truckLeftSideTexture } ), | |
| new THREE.MeshPhongMaterial( { color, flatShading: true } ), // top | |
| new THREE.MeshPhongMaterial( { color, flatShading: true } ) // bottom | |
| ] | |
| ); | |
| cabin.position.x = -40*zoom; | |
| cabin.position.z = 20*zoom; | |
| cabin.castShadow = true; | |
| cabin.receiveShadow = true; | |
| truck.add( cabin ); | |
| const frontWheel = new Wheel(); | |
| frontWheel.position.x = -38*zoom; | |
| truck.add( frontWheel ); | |
| const middleWheel = new Wheel(); | |
| middleWheel.position.x = -10*zoom; | |
| truck.add( middleWheel ); | |
| const backWheel = new Wheel(); | |
| backWheel.position.x = 30*zoom; | |
| truck.add( backWheel ); | |
| return truck; | |
| } | |
| function Three() { | |
| const three = new THREE.Group(); | |
| const trunk = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 15*zoom, 15*zoom, 20*zoom ), | |
| new THREE.MeshPhongMaterial( { color: 0x4d2926, flatShading: true } ) | |
| ); | |
| trunk.position.z = 10*zoom; | |
| trunk.castShadow = true; | |
| trunk.receiveShadow = true; | |
| three.add(trunk); | |
| height = threeHeights[Math.floor(Math.random()*threeHeights.length)]; | |
| const crown = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 30*zoom, 30*zoom, height*zoom ), | |
| new THREE.MeshLambertMaterial( { color: 0x7aa21d, flatShading: true } ) | |
| ); | |
| crown.position.z = (height/2+20)*zoom; | |
| crown.castShadow = true; | |
| crown.receiveShadow = false; | |
| three.add(crown); | |
| return three; | |
| } | |
| function Chicken() { | |
| const chicken = new THREE.Group(); | |
| const body = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( chickenSize*zoom, chickenSize*zoom, 20*zoom ), | |
| new THREE.MeshPhongMaterial( { color: 0xffffff, flatShading: true } ) | |
| ); | |
| body.position.z = 10*zoom; | |
| body.castShadow = true; | |
| body.receiveShadow = true; | |
| chicken.add(body); | |
| const rowel = new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( 2*zoom, 4*zoom, 2*zoom ), | |
| new THREE.MeshLambertMaterial( { color: 0xF0619A, flatShading: true } ) | |
| ); | |
| rowel.position.z = 21*zoom; | |
| rowel.castShadow = true; | |
| rowel.receiveShadow = false; | |
| chicken.add(rowel); | |
| return chicken; | |
| } | |
| function Road() { | |
| const road = new THREE.Group(); | |
| const createSection = color => new THREE.Mesh( | |
| new THREE.PlaneBufferGeometry( boardWidth*zoom, positionWidth*zoom ), | |
| new THREE.MeshPhongMaterial( { color } ) | |
| ); | |
| const middle = createSection(0x454A59); | |
| middle.receiveShadow = true; | |
| road.add(middle); | |
| const left = createSection(0x393D49); | |
| left.position.x = - boardWidth*zoom; | |
| road.add(left); | |
| const right = createSection(0x393D49); | |
| right.position.x = boardWidth*zoom; | |
| road.add(right); | |
| return road; | |
| } | |
| function Grass() { | |
| const grass = new THREE.Group(); | |
| const createSection = color => new THREE.Mesh( | |
| new THREE.BoxBufferGeometry( boardWidth*zoom, positionWidth*zoom, 3*zoom ), | |
| new THREE.MeshPhongMaterial( { color } ) | |
| ); | |
| const middle = createSection(0xbaf455); | |
| middle.receiveShadow = true; | |
| grass.add(middle); | |
| const left = createSection(0x99C846); | |
| left.position.x = - boardWidth*zoom; | |
| grass.add(left); | |
| const right = createSection(0x99C846); | |
| right.position.x = boardWidth*zoom; | |
| grass.add(right); | |
| grass.position.z = 1.5*zoom; | |
| return grass; | |
| } | |
| function Lane(index) { | |
| this.index = index; | |
| this.type = index <= 0 ? 'field' : laneTypes[Math.floor(Math.random()*laneTypes.length)]; | |
| switch(this.type) { | |
| case 'field': { | |
| this.type = 'field'; | |
| this.mesh = new Grass(); | |
| break; | |
| } | |
| case 'forest': { | |
| this.mesh = new Grass(); | |
| this.occupiedPositions = new Set(); | |
| this.threes = [1,2,3,4].map(() => { | |
| const three = new Three(); | |
| let position; | |
| do { | |
| position = Math.floor(Math.random()*columns); | |
| }while(this.occupiedPositions.has(position)) | |
| this.occupiedPositions.add(position); | |
| three.position.x = (position*positionWidth+positionWidth/2)*zoom-boardWidth*zoom/2; | |
| this.mesh.add( three ); | |
| return three; | |
| }) | |
| break; | |
| } | |
| case 'car' : { | |
| this.mesh = new Road(); | |
| this.direction = Math.random() >= 0.5; | |
| const occupiedPositions = new Set(); | |
| this.vechicles = [1,2,3].map(() => { | |
| const vechicle = new Car(); | |
| let position; | |
| do { | |
| position = Math.floor(Math.random()*columns/2); | |
| }while(occupiedPositions.has(position)) | |
| occupiedPositions.add(position); | |
| vechicle.position.x = (position*positionWidth*2+positionWidth/2)*zoom-boardWidth*zoom/2; | |
| if(!this.direction) vechicle.rotation.z = Math.PI; | |
| this.mesh.add( vechicle ); | |
| return vechicle; | |
| }) | |
| this.speed = laneSpeeds[Math.floor(Math.random()*laneSpeeds.length)]; | |
| break; | |
| } | |
| case 'truck' : { | |
| this.mesh = new Road(); | |
| this.direction = Math.random() >= 0.5; | |
| const occupiedPositions = new Set(); | |
| this.vechicles = [1,2].map(() => { | |
| const vechicle = new Truck(); | |
| let position; | |
| do { | |
| position = Math.floor(Math.random()*columns/3); | |
| }while(occupiedPositions.has(position)) | |
| occupiedPositions.add(position); | |
| vechicle.position.x = (position*positionWidth*3+positionWidth/2)*zoom-boardWidth*zoom/2; | |
| if(!this.direction) vechicle.rotation.z = Math.PI; | |
| this.mesh.add( vechicle ); | |
| return vechicle; | |
| }) | |
| this.speed = laneSpeeds[Math.floor(Math.random()*laneSpeeds.length)]; | |
| break; | |
| } | |
| } | |
| } | |
| document.querySelector("#retry").addEventListener("click", () => { | |
| lanes.forEach(lane => scene.remove( lane.mesh )); | |
| initaliseValues(); | |
| endDOM.style.visibility = 'hidden'; | |
| }); | |
| document.getElementById('forward').addEventListener("click", () => move('forward')); | |
| document.getElementById('backward').addEventListener("click", () => move('backward')); | |
| document.getElementById('left').addEventListener("click", () => move('left')); | |
| document.getElementById('right').addEventListener("click", () => move('right')); | |
| window.addEventListener("keydown", event => { | |
| if (event.keyCode == '38') { | |
| // up arrow | |
| move('forward'); | |
| } | |
| else if (event.keyCode == '40') { | |
| // down arrow | |
| move('backward'); | |
| } | |
| else if (event.keyCode == '37') { | |
| // left arrow | |
| move('left'); | |
| } | |
| else if (event.keyCode == '39') { | |
| // right arrow | |
| move('right'); | |
| } | |
| }); | |
| function move(direction) { | |
| const finalPositions = moves.reduce((position,move) => { | |
| if(move === 'forward') return {lane: position.lane+1, column: position.column}; | |
| if(move === 'backward') return {lane: position.lane-1, column: position.column}; | |
| if(move === 'left') return {lane: position.lane, column: position.column-1}; | |
| if(move === 'right') return {lane: position.lane, column: position.column+1}; | |
| }, {lane: currentLane, column: currentColumn}) | |
| if (direction === 'forward') { | |
| if(lanes[finalPositions.lane+1].type === 'forest' && lanes[finalPositions.lane+1].occupiedPositions.has(finalPositions.column)) return; | |
| if(!stepStartTimestamp) startMoving = true; | |
| addLane(); | |
| } | |
| else if (direction === 'backward') { | |
| if(finalPositions.lane === 0) return; | |
| if(lanes[finalPositions.lane-1].type === 'forest' && lanes[finalPositions.lane-1].occupiedPositions.has(finalPositions.column)) return; | |
| if(!stepStartTimestamp) startMoving = true; | |
| } | |
| else if (direction === 'left') { | |
| if(finalPositions.column === 0) return; | |
| if(lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column-1)) return; | |
| if(!stepStartTimestamp) startMoving = true; | |
| } | |
| else if (direction === 'right') { | |
| if(finalPositions.column === columns - 1 ) return; | |
| if(lanes[finalPositions.lane].type === 'forest' && lanes[finalPositions.lane].occupiedPositions.has(finalPositions.column+1)) return; | |
| if(!stepStartTimestamp) startMoving = true; | |
| } | |
| moves.push(direction); | |
| } | |
| function animate(timestamp) { | |
| requestAnimationFrame( animate ); | |
| if(!previousTimestamp) previousTimestamp = timestamp; | |
| const delta = timestamp - previousTimestamp; | |
| previousTimestamp = timestamp; | |
| // Animate cars and trucks moving on the lane | |
| lanes.forEach(lane => { | |
| if(lane.type === 'car' || lane.type === 'truck') { | |
| const aBitBeforeTheBeginingOfLane = -boardWidth*zoom/2 - positionWidth*2*zoom; | |
| const aBitAfterTheEndOFLane = boardWidth*zoom/2 + positionWidth*2*zoom; | |
| lane.vechicles.forEach(vechicle => { | |
| if(lane.direction) { | |
| vechicle.position.x = vechicle.position.x < aBitBeforeTheBeginingOfLane ? aBitAfterTheEndOFLane : vechicle.position.x -= lane.speed/16*delta; | |
| }else{ | |
| vechicle.position.x = vechicle.position.x > aBitAfterTheEndOFLane ? aBitBeforeTheBeginingOfLane : vechicle.position.x += lane.speed/16*delta; | |
| } | |
| }); | |
| } | |
| }); | |
| if(startMoving) { | |
| stepStartTimestamp = timestamp; | |
| startMoving = false; | |
| } | |
| if(stepStartTimestamp) { | |
| const moveDeltaTime = timestamp - stepStartTimestamp; | |
| const moveDeltaDistance = Math.min(moveDeltaTime/stepTime,1)*positionWidth*zoom; | |
| const jumpDeltaDistance = Math.sin(Math.min(moveDeltaTime/stepTime,1)*Math.PI)*8*zoom; | |
| switch(moves[0]) { | |
| case 'forward': { | |
| const positionY = currentLane*positionWidth*zoom + moveDeltaDistance; | |
| camera.position.y = initialCameraPositionY + positionY; | |
| dirLight.position.y = initialDirLightPositionY + positionY; | |
| chicken.position.y = positionY; // initial chicken position is 0 | |
| chicken.position.z = jumpDeltaDistance; | |
| break; | |
| } | |
| case 'backward': { | |
| positionY = currentLane*positionWidth*zoom - moveDeltaDistance | |
| camera.position.y = initialCameraPositionY + positionY; | |
| dirLight.position.y = initialDirLightPositionY + positionY; | |
| chicken.position.y = positionY; | |
| chicken.position.z = jumpDeltaDistance; | |
| break; | |
| } | |
| case 'left': { | |
| const positionX = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 - moveDeltaDistance; | |
| camera.position.x = initialCameraPositionX + positionX; | |
| dirLight.position.x = initialDirLightPositionX + positionX; | |
| chicken.position.x = positionX; // initial chicken position is 0 | |
| chicken.position.z = jumpDeltaDistance; | |
| break; | |
| } | |
| case 'right': { | |
| const positionX = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 + moveDeltaDistance; | |
| camera.position.x = initialCameraPositionX + positionX; | |
| dirLight.position.x = initialDirLightPositionX + positionX; | |
| chicken.position.x = positionX; | |
| chicken.position.z = jumpDeltaDistance; | |
| break; | |
| } | |
| } | |
| // Once a step has ended | |
| if(moveDeltaTime > stepTime) { | |
| switch(moves[0]) { | |
| case 'forward': { | |
| currentLane++; | |
| counterDOM.innerHTML = currentLane; | |
| break; | |
| } | |
| case 'backward': { | |
| currentLane--; | |
| counterDOM.innerHTML = currentLane; | |
| break; | |
| } | |
| case 'left': { | |
| currentColumn--; | |
| break; | |
| } | |
| case 'right': { | |
| currentColumn++; | |
| break; | |
| } | |
| } | |
| moves.shift(); | |
| // If more steps are to be taken then restart counter otherwise stop stepping | |
| stepStartTimestamp = moves.length === 0 ? null : timestamp; | |
| } | |
| } | |
| // Hit test | |
| if(lanes[currentLane].type === 'car' || lanes[currentLane].type === 'truck') { | |
| const chickenMinX = chicken.position.x - chickenSize*zoom/2; | |
| const chickenMaxX = chicken.position.x + chickenSize*zoom/2; | |
| const vechicleLength = { car: 60, truck: 105}[lanes[currentLane].type]; | |
| lanes[currentLane].vechicles.forEach(vechicle => { | |
| const carMinX = vechicle.position.x - vechicleLength*zoom/2; | |
| const carMaxX = vechicle.position.x + vechicleLength*zoom/2; | |
| if(chickenMaxX > carMinX && chickenMinX < carMaxX) { | |
| endDOM.style.visibility = 'visible'; | |
| } | |
| }); | |
| } | |
| renderer.render( scene, camera ); | |
| } | |
| requestAnimationFrame( animate ); |
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
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js"></script> |
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
| @import url('https://fonts.googleapis.com/css?family=Press+Start+2P'); | |
| body { | |
| margin: 0; | |
| font-family: 'Press Start 2P', cursive; | |
| font-size: 2em; | |
| color: white; | |
| } | |
| button { | |
| outline: none; | |
| cursor: pointer; | |
| border: none; | |
| box-shadow: 3px 5px 0px 0px rgba(0,0,0,0.75); | |
| } | |
| #counter { | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| } | |
| #end { | |
| position: absolute; | |
| min-width: 100%; | |
| min-height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| visibility: hidden; | |
| } | |
| #end button { | |
| background-color: red; | |
| padding: 20px 50px 20px 50px; | |
| font-family: inherit; | |
| font-size: inherit; | |
| } | |
| #controlls { | |
| position: absolute; | |
| min-width: 100%; | |
| min-height: 100%; | |
| display: flex; | |
| align-items: flex-end; | |
| justify-content: center; | |
| } | |
| #controlls div { | |
| display: grid; | |
| grid-template-columns: 50px 50px 50px; | |
| grid-template-rows: auto auto; | |
| grid-column-gap: 10px; | |
| grid-row-gap: 10px; | |
| margin-bottom: 20px; | |
| } | |
| #controlls button { | |
| width: 100%; | |
| background-color: white; | |
| border: 1px solid lightgray; | |
| } | |
| #controlls button:first-of-type { | |
| grid-column: 1/-1; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment