Skip to content

Instantly share code, notes, and snippets.

@artemuzz
Created August 20, 2019 02:16
Show Gist options
  • Save artemuzz/93ec9f181a57366cdd6e51964fd68ebc to your computer and use it in GitHub Desktop.
Save artemuzz/93ec9f181a57366cdd6e51964fd68ebc to your computer and use it in GitHub Desktop.
iCraft

iCraft

An attempt to create a dumbed-down clone of Minecraft programmed to the standards of the very first version (known as “Cave Game”). Build in a small world of classic Apple! Blocks you place are random iMac models, but silicon will always mark the main ground layer.

🕹 Controls

Action Input
Forward W
Left A
Back S
Right D
Spacebar Jump
Place Block Left Click/Tap
Destroy Block Right Click/Hold
Respawn R

A Pen by Jon Kantner on CodePen.

License.

window.addEventListener("DOMContentLoaded",app);
function app() {
var 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,
// 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 => {
let center = {
x: window.innerWidth/2,
y: window.innerHeight/2
};
if (e) {
cursor.x = e.pageX;
cursor.y = e.pageY;
}
this.pitch = -Math.atan((cursor.y - center.y) / center.y) * 2;
this.yaw = -Math.atan((cursor.x - center.x) / center.x) * 8;
};
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);
},
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();
});
// 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);
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
<script src="https://codepen.io/jkantner/pen/QeXbrq"></script>
body {
margin: 0;
overflow: hidden;
position: fixed;
}
canvas {
cursor: none;
-webkit-tap-highlight-color: transparent;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment