Skip to content

Instantly share code, notes, and snippets.

@iam-pankaj
Created December 28, 2018 07:40
Show Gist options
  • Save iam-pankaj/5bc6206e7e791c4634d1fce988a2be64 to your computer and use it in GitHub Desktop.
Save iam-pankaj/5bc6206e7e791c4634d1fce988a2be64 to your computer and use it in GitHub Desktop.
Crossy Road with three.js

Crossy Road with three.js

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.

A Pen by Hunor Marton Borbely on CodePen.

License.

<div id="counter">0</div>
<div id="controlls">
<div>
<button id="forward">↑</button>
<button id="left">←</button>
<button id="backward">↓</button>
<button id="right">→</button>
</div>
</div>
<div id="end">
<button id="retry">Retry</button>
</div>
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 );
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;
}
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 );
hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.6);
scene.add(hemiLight)
dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(-100, -100, 200);
dirLight.castShadow = true;
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)
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': {
camera.position.y = initialCameraPositionY + currentLane*positionWidth*zoom + moveDeltaDistance;
chicken.position.y = currentLane*positionWidth*zoom + moveDeltaDistance; // initial chicken position is 0
chicken.position.z = jumpDeltaDistance;
break;
}
case 'backward': {
camera.position.y = initialCameraPositionY + currentLane*positionWidth*zoom - moveDeltaDistance;
chicken.position.y = currentLane*positionWidth*zoom - moveDeltaDistance;
chicken.position.z = jumpDeltaDistance;
break;
}
case 'left': {
camera.position.x = initialCameraPositionX + (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 - moveDeltaDistance;
chicken.position.x = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 - moveDeltaDistance; // initial chicken position is 0
chicken.position.z = jumpDeltaDistance;
break;
}
case 'right': {
camera.position.x = initialCameraPositionX + (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 + moveDeltaDistance;
chicken.position.x = (currentColumn*positionWidth+positionWidth/2)*zoom -boardWidth*zoom/2 + moveDeltaDistance;
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 );
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js"></script>
@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;
}
#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 {
margin-bottom: 20px;
font-size: 0;
max-width: 180px;
}
#controlls button {
width: 50px;
font-family: inherit;
font-size: 30px;
border: 3px solid white;
color: white;
background-color: transparent;
margin: 5px;
}
#controlls button:first-of-type {
width: 170px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment