Voxel Realms - Character World
by ApexPirate861106 lines19.5 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>Voxel Realms - Character World</title>
<style>
html,body{
margin:0;
width:100%;
height:100%;
overflow:hidden;
background:#07111b;
font-family:Arial,sans-serif;
touch-action:none;
user-select:none;
}
canvas{
display:block;
width:100%;
height:100%;
}
#loading{
position:fixed;
inset:0;
display:flex;
align-items:center;
justify-content:center;
background:#07111b;
color:white;
z-index:100;
font-size:24px;
}
#hud{
position:fixed;
inset:0;
pointer-events:none;
color:white;
}
#title{
position:absolute;
top:12px;
left:15px;
font-size:20px;
font-weight:bold;
text-shadow:0 2px 4px #000;
}
#info{
position:absolute;
top:42px;
left:15px;
font-size:13px;
text-shadow:0 2px 4px #000;
}
#health{
position:absolute;
top:12px;
right:15px;
background:#111b;
padding:8px 12px;
border-radius:12px;
}
#joystick{
position:absolute;
left:25px;
bottom:28px;
width:125px;
height:125px;
border-radius:50%;
background:#ffffff22;
border:2px solid #ffffff55;
pointer-events:auto;
}
#stick{
position:absolute;
width:55px;
height:55px;
left:35px;
top:35px;
border-radius:50%;
background:#ffffff66;
border:2px solid #ffffffaa;
}
#lookZone{
position:absolute;
right:0;
top:0;
width:52%;
height:72%;
pointer-events:auto;
}
.action{
position:absolute;
pointer-events:auto;
width:65px;
height:65px;
border-radius:50%;
border:2px solid #ffffff66;
background:#172333bb;
color:white;
font-size:25px;
display:flex;
justify-content:center;
align-items:center;
box-shadow:0 4px 10px #0008;
}
#jump{
right:25px;
bottom:32px;
}
#sprint{
right:105px;
bottom:105px;
width:58px;
height:58px;
font-size:21px;
}
#cameraBtn{
right:25px;
top:85px;
width:52px;
height:52px;
font-size:22px;
}
#start{
position:fixed;
inset:0;
z-index:90;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
background:linear-gradient(#07111bee,#0d2435ee);
color:white;
text-align:center;
}
#start h1{
font-size:42px;
margin:0 0 8px;
}
#start p{
opacity:.8;
max-width:330px;
}
#start button{
margin-top:20px;
padding:15px 35px;
border:0;
border-radius:14px;
background:#42c86b;
color:white;
font-size:18px;
font-weight:bold;
}
</style>
</head>
<body>
<div id="loading">Loading Voxel Realms...</div>
<div id="start">
<h1>🌍 Voxel Realms</h1>
<p>Explore the world, meet characters and discover your home.</p>
<p>Mobile controls included.</p>
<button id="startBtn">PLAY</button>
</div>
<div id="hud">
<div id="title">🌍 Voxel Realms</div>
<div id="info">Explore • Find your home</div>
<div id="health">❤️ 100</div>
<div id="lookZone"></div>
<div id="joystick">
<div id="stick"></div>
</div>
<div class="action" id="jump">⬆️</div>
<div class="action" id="sprint">🏃</div>
<div class="action" id="cameraBtn">📷</div>
</div>
<script type="module">
import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.169.0/build/three.module.js";
/* =========================================================
BASIC SETUP
========================================================= */
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x82cfff);
scene.fog = new THREE.Fog(0x82cfff,45,130);
const camera = new THREE.PerspectiveCamera(
65,
innerWidth/innerHeight,
.1,
250
);
const renderer = new THREE.WebGLRenderer({
antialias:true
});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.5));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
document.body.appendChild(renderer.domElement);
/* =========================================================
LIGHT
========================================================= */
const hemi = new THREE.HemisphereLight(
0xbfe8ff,
0x536344,
2.0
);
scene.add(hemi);
const sun = new THREE.DirectionalLight(
0xffffff,
3
);
sun.position.set(-40,70,30);
sun.castShadow=true;
sun.shadow.mapSize.width=1024;
sun.shadow.mapSize.height=1024;
sun.shadow.camera.left=-70;
sun.shadow.camera.right=70;
sun.shadow.camera.top=70;
sun.shadow.camera.bottom=-70;
scene.add(sun);
/* =========================================================
MATERIALS
========================================================= */
const MAT = {
grass:new THREE.MeshStandardMaterial({color:0x55a83c}),
dirt:new THREE.MeshStandardMaterial({color:0x80552f}),
stone:new THREE.MeshStandardMaterial({color:0x858b91}),
wood:new THREE.MeshStandardMaterial({color:0x8b542d}),
darkWood:new THREE.MeshStandardMaterial({color:0x4d2c19}),
roof:new THREE.MeshStandardMaterial({color:0x9b382f}),
leaf:new THREE.MeshStandardMaterial({color:0x31843b}),
glass:new THREE.MeshStandardMaterial({
color:0x7bd9ff,
transparent:true,
opacity:.55
}),
white:new THREE.MeshStandardMaterial({color:0xffffff}),
black:new THREE.MeshStandardMaterial({color:0x111111}),
skin:new THREE.MeshStandardMaterial({color:0xd28c62}),
shirt:new THREE.MeshStandardMaterial({color:0x2778d0}),
pants:new THREE.MeshStandardMaterial({color:0x25304a}),
shoe:new THREE.MeshStandardMaterial({color:0x202020}),
hair:new THREE.MeshStandardMaterial({color:0x3a2117}),
red:new THREE.MeshStandardMaterial({color:0xc93434}),
yellow:new THREE.MeshStandardMaterial({color:0xe9bf32}),
green:new THREE.MeshStandardMaterial({color:0x31a85b}),
pink:new THREE.MeshStandardMaterial({color:0xe4779a})
};
/* =========================================================
BLOCK FUNCTION
========================================================= */
function block(x,y,z,mat,s=1){
const geo=new THREE.BoxGeometry(s,s,s);
const mesh=new THREE.Mesh(geo,mat);
mesh.position.set(x,y,z);
mesh.castShadow=true;
mesh.receiveShadow=true;
scene.add(mesh);
return mesh;
}
/* =========================================================
WORLD
========================================================= */
const WORLD=48;
for(let x=-WORLD;x<=WORLD;x++){
for(let z=-WORLD;z<=WORLD;z++){
let h=1;
const d=Math.sqrt(x*x+z*z);
if(d>12){
h=1+
Math.floor(
Math.sin(x*.18)*.7+
Math.cos(z*.15)*.6+
Math.sin((x+z)*.08)*.8
);
h=Math.max(1,Math.min(4,h));
}
for(let y=0;y<h;y++){
let mat =
y===h-1
? MAT.grass
: MAT.dirt;
block(x,y,z,mat);
}
}
}
/* =========================================================
WATER
========================================================= */
const waterGeo=new THREE.PlaneGeometry(70,70);
const waterMat=new THREE.MeshStandardMaterial({
color:0x238dd0,
transparent:true,
opacity:.55
});
const water=new THREE.Mesh(waterGeo,waterMat);
water.rotation.x=-Math.PI/2;
water.position.y=.55;
water.position.set(25,.55,-25);
scene.add(water);
/* =========================================================
TREE
========================================================= */
function tree(x,z){
const trunk=block(x,2,z,MAT.wood,1);
trunk.scale.y=3;
for(let dx=-1;dx<=1;dx++){
for(let dz=-1;dz<=1;dz++){
const leaf=block(
x+dx,
4.3,
z+dz,
MAT.leaf,
1.3
);
leaf.scale.y=.8;
}
}
block(x,5.1,z,MAT.leaf,1.3);
}
for(let i=0;i<30;i++){
const x=Math.floor(Math.random()*75)-37;
const z=Math.floor(Math.random()*75)-37;
if(Math.abs(x)<12 && Math.abs(z)<12) continue;
tree(x,z);
}
/* =========================================================
HOUSE
========================================================= */
function createHouse(){
const g=new THREE.Group();
/* floor */
const floor=new THREE.Mesh(
new THREE.BoxGeometry(12,.5,10),
MAT.wood
);
floor.position.y=1.25;
floor.receiveShadow=true;
g.add(floor);
/* walls */
function wall(x,y,z,sx,sy,sz){
const m=new THREE.Mesh(
new THREE.BoxGeometry(sx,sy,sz),
MAT.wood
);
m.position.set(x,y,z);
m.castShadow=true;
m.receiveShadow=true;
g.add(m);
}
wall(-6,4,0,.6,5,10);
wall(6,4,0,.6,5,10);
wall(0,4,-5,12,5,.6);
/* front wall pieces, leaving door */
wall(-4,4,5,.6,5,2.5);
wall(4,4,5,.6,5,2.5);
wall(0,6,5,7,1.5,.6);
/* roof */
const roof1=new THREE.Mesh(
new THREE.BoxGeometry(7,1,12),
MAT.roof
);
roof1.position.set(-3,7,0);
roof1.rotation.z=-.35;
roof1.castShadow=true;
g.add(roof1);
const roof2=roof1.clone();
roof2.position.x=3;
roof2.rotation.z=.35;
g.add(roof2);
/* windows */
function window(x,z){
const w=new THREE.Mesh(
new THREE.BoxGeometry(2,.05,2),
MAT.glass
);
w.position.set(x,4,z);
w.rotation.x=Math.PI/2;
g.add(w);
}
window(-6.3,0);
window(6.3,0);
/* door */
const door=new THREE.Mesh(
new THREE.BoxGeometry(2.2,.25,4),
MAT.darkWood
);
door.position.set(0,3,5.25);
g.add(door);
/* bed */
const bed=new THREE.Mesh(
new THREE.BoxGeometry(3,0.7,2),
MAT.red
);
bed.position.set(-3,2,-2.5);
g.add(bed);
const pillow=new THREE.Mesh(
new THREE.BoxGeometry(1.5,.4,1.7),
MAT.white
);
pillow.position.set(-3,2.5,-3);
g.add(pillow);
/* table */
const table=new THREE.Mesh(
new THREE.BoxGeometry(2,.4,1.5),
MAT.darkWood
);
table.position.set(3,3,-1);
g.add(table);
g.position.set(0,0,0);
scene.add(g);
}
createHouse();
/* =========================================================
CHARACTER BUILDER
========================================================= */
function makeCharacter(options={}){
const c=new THREE.Group();
const skin=options.skin || MAT.skin;
const shirt=options.shirt || MAT.shirt;
const pants=options.pants || MAT.pants;
const hair=options.hair || MAT.hair;
/* body */
const torso=new THREE.Mesh(
new THREE.BoxGeometry(1.5,2,0.8),
shirt
);
torso.position.y=3.5;
torso.castShadow=true;
c.add(torso);
/* neck */
const neck=new THREE.Mesh(
new THREE.BoxGeometry(.45,.45,.45),
skin
);
neck.position.y=4.7;
neck.castShadow=true;
c.add(neck);
/* head */
const head=new THREE.Mesh(
new THREE.BoxGeometry(1.65,1.55,1.35),
skin
);
head.position.y=5.65;
head.castShadow=true;
c.add(head);
/* hair */
const hairTop=new THREE.Mesh(
new THREE.BoxGeometry(1.72,.55,1.42),
hair
);
hairTop.position.set(0,6.35,0);
hairTop.castShadow=true;
c.add(hairTop);
/* =====================================================
FACE
===================================================== */
function facePart(geo,mat,x,y,z){
const m=new THREE.Mesh(geo,mat);
m.position.set(x,y,z);
m.castShadow=true;
c.add(m);
return m;
}
/* eyes */
const eyeGeo=new THREE.SphereGeometry(.16,12,8);
const leftEye=facePart(
eyeGeo,
MAT.white,
-.38,
5.78,
-.68
);
const rightEye=facePart(
eyeGeo,
MAT.white,
.38,
5.78,
-.68
);
/* pupils */
const pupilGeo=new THREE.SphereGeometry(.075,10,6);
facePart(
pupilGeo,
MAT.black,
-.38,
5.78,
-.82
);
facePart(
pupilGeo,
MAT.black,
.38,
5.78,
-.82
);
/* nose */
const nose=new THREE.Mesh(
new THREE.BoxGeometry(.16,.22,.18),
skin
);
nose.position.set(0,5.5,-.76);
nose.castShadow=true;
c.add(nose);
/* mouth */
const mouth=new THREE.Mesh(
new THREE.BoxGeometry(.45,.11,.08),
MAT.red
);
mouth.position.set(0,5.22,-.72);
mouth.castShadow=true;
c.add(mouth);
/* ears */
const earGeo=new THREE.SphereGeometry(.25,10,8);
facePart(
earGeo,
skin,
-.86,
5.62,
0
);
facePart(
earGeo,
skin,
.86,
5.62,
0
);
/* =====================================================
ARMS
===================================================== */
const leftArm=new THREE.Mesh(
new THREE.BoxGeometry(.48,1.9,.5),
shirt
);
leftArm.position.set(-1.05,3.5,0);
leftArm.castShadow=true;
c.add(leftArm);
const rightArm=leftArm.clone();
rightArm.position.x=1.05;
c.add(rightArm);
/* hands */
const handGeo=new THREE.BoxGeometry(.5,.45,.5);
const leftHand=new THREE.Mesh(handGeo,skin);
leftHand.position.set(-1.05,2.4,0);
leftHand.castShadow=true;
c.add(leftHand);
const rightHand=leftHand.clone();
rightHand.position.x=1.05;
c.add(rightHand);
/* =====================================================
LEGS
===================================================== */
const leftLeg=new THREE.Mesh(
new THREE.BoxGeometry(.62,1.8,.65),
pants
);
leftLeg.position.set(-.43,1.75,0);
leftLeg.castShadow=true;
c.add(leftLeg);
const rightLeg=leftLeg.clone();
rightLeg.position.x=.43;
c.add(rightLeg);
/* feet */
const leftFoot=new THREE.Mesh(
new THREE.BoxGeometry(.7,.4,1),
MAT.shoe
);
leftFoot.position.set(-.43,.75,-.18);
leftFoot.castShadow=true;
c.add(leftFoot);
const rightFoot=leftFoot.clone();
rightFoot.position.x=.43;
c.add(rightFoot);
c.userData.parts={
leftArm,
rightArm,
leftLeg,
rightLeg
};
return c;
}
/* =========================================================
PLAYER
========================================================= */
const player=makeCharacter({
skin:MAT.skin,
shirt:MAT.shirt,
pants:MAT.pants,
hair:MAT.hair
});
player.position.set(3,0,10);
scene.add(player);
/* =========================================================
NPCs
========================================================= */
const npc1=makeCharacter({
skin:new THREE.MeshStandardMaterial({color:0x9c603f}),
shirt:MAT.green,
pants:MAT.pants,
hair:new THREE.MeshStandardMaterial({color:0x17110d})
});
npc1.position.set(-8,0,4);
npc1.rotation.y=Math.PI;
scene.add(npc1);
const npc2=makeCharacter({
skin:new THREE.MeshStandardMaterial({color:0xf0b48b}),
shirt:MAT.yellow,
pants:new THREE.MeshStandardMaterial({color:0x68408e}),
hair:new THREE.MeshStandardMaterial({color:0x8a4a20})
});
npc2.position.set(9,0,-5);
scene.add(npc2);
const npc3=makeCharacter({
skin:new THREE.MeshStandardMaterial({color:0x70462f}),
shirt:MAT.red,
pants:new THREE.MeshStandardMaterial({color:0x253c59}),
hair:new THREE.MeshStandardMaterial({color:0x090909})
});
npc3.position.set(-10,0,-9);
scene.add(npc3);
/* =========================================================
ANIMALS
========================================================= */
function cow(x,z){
const g=new THREE.Group();
const body=new THREE.Mesh(
new THREE.BoxGeometry(2.4,1.4,1.3),
MAT.white
);
body.position.y=1.9;
body.castShadow=true;
g.add(body);
const head=new THREE.Mesh(
new THREE.BoxGeometry(1.1,1.1,1),
MAT.white
);
head.position.set(0,2.6,-1.3);
head.castShadow=true;
g.add(head);
/* eyes */
const eyeGeo=new THREE.SphereGeometry(.1,8,6);
const e1=new THREE.Mesh(eyeGeo,MAT.black);
e1.position.set(-.28,2.75,-1.78);
g.add(e1);
const e2=e1.clone();
e2.position.x=.28;
g.add(e2);
/* ears */
const earGeo=new THREE.BoxGeometry(.35,.25,.35);
const ear1=new THREE.Mesh(earGeo,MAT.white);
ear1.position.set(-.55,3,-1.2);
g.add(ear1);
const ear2=ear1.clone();
ear2.position.x=.55;
g.add(ear2);
/* legs */
for(const x2 of [-.75,.75]){
for(const z2 of [-.35,.35]){
const leg=new THREE.Mesh(
new THREE.BoxGeometry(.4,1.5,.4),
MAT.white
);
leg.position.set(x2,1,z2);
leg.castShadow=true;
g.add(leg);
}
}
g.position.set(x,0,z);
scene.add(g);
return g;
}
cow(14,7);
cow(17,4);
/* =========================================================
CONTROLS
========================================================= */
const keys={};
addEventListener("keydown",e=>{
keys[e.key.toLowerCase()]=true;
if(e.code==="Space"){
jumpRequest=true;
}
});
addEventListener("keyup",e=>{
keys[e.key.toLowerCase()]=false;
});
/* joystick */
const joystick=document.getElementById("joystick");
const stick=document.getElementById("stick");
let joyX=0;
let joyY=0;
let joyPointer=null;
function updateJoystick(e){
const r=joystick.getBoundingClientRect();
let x=e.clientX-(r.left+r.width/2);
let y=e.clientY-(r.top+r.height/2);
const max=45;
const len=Math.hypot(x,y);
if(len>max){
x=x/len*max;
y=y/len*max;
}
joyX=x/max;
joyY=y/max;
stick.style.transform=
`translate(${x}px,${y}px)`;
}
joystick.addEventListener("pointerdown",e=>{
joyPointer=e.pointerId;
joystick.setPointerCapture(e.pointerId);
updateJoystick(e);
});
joystick.addEventListener("pointermove",e=>{
if(e.pointerId===joyPointer)
updateJoystick(e);
});
function resetJoystick(){
joyX=0;
joyY=0;
stick.style.transform="translate(0,0)";
joyPointer=null;
}
joystick.addEventListener("pointerup",resetJoystick);
joystick.addEventListener("pointercancel",resetJoystick);
/* =========================================================
CAMERA LOOK
========================================================= */
const lookZone=document.getElementById("lookZone");
let lookPointer=null;
let lastLookX=0;
let lastLookY=0;
let camYaw=0;
let camPitch=.25;
lookZone.addEventListener("pointerdown",e=>{
lookPointer=e.pointerId;
lastLookX=e.clientX;
lastLookY=e.clientY;
lookZone.setPointerCapture(e.pointerId);
});
lookZone.addEventListener("pointermove",e=>{
if(e.pointerId!==lookPointer)return;
const dx=e.clientX-lastLookX;
const dy=e.clientY-lastLookY;
camYaw-=dx*.008;
camPitch-=dy*.006;
camPitch=Math.max(-.15,Math.min(.9,camPitch));
lastLookX=e.clientX;
lastLookY=e.clientY;
});
lookZone.addEventListener("pointerup",()=>{
lookPointer=null;
});
lookZone.addEventListener("pointercancel",()=>{
lookPointer=null;
});
/* =========================================================
JUMP + SPRINT
========================================================= */
let jumpRequest=false;
let verticalVelocity=0;
let onGround=true;
let sprint=false;
const jumpBtn=document.getElementById("jump");
const sprintBtn=document.getElementById("sprint");
jumpBtn.addEventListener("pointerdown",()=>{
jumpRequest=true;
});
sprintBtn.addEventListener("pointerdown",()=>{
sprint=true;
});
sprintBtn.addEventListener("pointerup",()=>{
sprint=false;
});
sprintBtn.addEventListener("pointercancel",()=>{
sprint=false;
});
/* =========================================================
CAMERA ANGLES
========================================================= */
let cameraMode=0;
document.getElementById("cameraBtn").addEventListener("click",()=>{
cameraMode++;
if(cameraMode>2)cameraMode=0;
if(cameraMode===0){
camYaw=0;
}
if(cameraMode===1){
camYaw=Math.PI/2;
}
if(cameraMode===2){
camYaw=Math.PI;
}
});
/* =========================================================
DESKTOP MOUSE CAMERA
========================================================= */
let mouseDown=false;
let mouseX=0;
let mouseY=0;
renderer.domElement.addEventListener("mousedown",e=>{
mouseDown=true;
mouseX=e.clientX;
mouseY=e.clientY;
});
addEventListener("mouseup",()=>{
mouseDown=false;
});
addEventListener("mousemove",e=>{
if(!mouseDown)return;
const dx=e.clientX-mouseX;
const dy=e.clientY-mouseY;
camYaw-=dx*.006;
camPitch-=dy*.005;
camPitch=Math.max(-.15,Math.min(.9,camPitch));
mouseX=e.clientX;
mouseY=e.clientY;
});
/* =========================================================
ZOOM
==========Game Source: Voxel Realms - Character World
Creator: ApexPirate86
Libraries: three
Complexity: complex (1106 lines, 19.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: voxel-realms-character-world-apexpirate86" to link back to the original. Then publish at arcadelab.ai/publish.