Meta Description" name="description" />
<!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover" />
<title>Mobil Voxel Demo (Three.js)</title>
<style>
html,body { height:100%; margin:0; background:#000; -webkit-user-select:none; user-select:none; touch-action:none; }
#game { position:fixed; inset:0; display:block; }
/* Joystick */
.joystick {
position:fixed;
left:16px;
bottom:16px;
width:120px;
height:120px;
border-radius:50%;
background: rgba(255,255,255,0.05);
display:flex;
align-items:center;
justify-content:center;
touch-action:none;
z-index:20;
}
.thumb {
width:50px;height:50px;border-radius:50%;
background: rgba(255,255,255,0.14);
transform: translate(0,0);
transition: transform 0s;
}
/* Right controls */
.right-controls {
position:fixed;
right:12px;
bottom:16px;
display:flex;
flex-direction:column;
gap:12px;
z-index:20;
align-items:center;
touch-action:none;
}
.btn {
width:72px;
height:72px;
border-radius:14px;
display:flex;
align-items:center;
justify-content:center;
background: rgba(255,255,255,0.06);
color: #fff;
font-weight:700;
-webkit-tap-highlight-color: transparent;
user-select:none;
}
.btn:active { background: rgba(255,255,255,0.12); }
/* top-right overlay for camera hint */
.hint {
position:fixed;
right:12px;
top:12px;
z-index:20;
color:#fff;
font-size:13px;
background: rgba(0,0,0,0.3);
padding:8px 10px;
border-radius:8px;
}
/* small FPS/Help at top-left */
.status {
position:fixed;
left:12px;
top:12px;
z-index:20;
color:#fff;
font-size:13px;
background: rgba(0,0,0,0.3);
padding:6px 10px;
border-radius:8px;
}
/* Ensure canvas covers the viewport */
canvas { width:100%; height:100%; display:block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<!-- Joystick -->
<div class="joystick" id="joystick" aria-hidden="true">
<div class="thumb" id="thumb"></div>
</div>
<!-- Right side buttons -->
<div class="right-controls" id="rightControls" aria-hidden="true">
<div class="btn" id="jumpBtn">Zıpla</div>
<div class="btn" id="placeBtn">Koy/Kır</div>
</div>
<div class="hint">Sağda sürükle: kamera</div>
<div class="status" id="status">Hazır</div>
<script src="https://unpkg.com/three@0.152.2/build/three.min.js"></script>
<script>
/*
Mobil Voxel Demo
- Sol alt: joystick (touch)
- Sağ swipe: kamera döndürme
- Sağ butonlar: zıplama, blok koy/kır (kısa bas: koy, uzun bas: kır)
- Tek dosyada çalışır
*/
// ---------------------- Basic Three.js setup ----------------------
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight, false);
renderer.outputEncoding = THREE.sRGBEncoding;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // sky
const camera = new THREE.PerspectiveCamera(70, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 4, 8);
camera.lookAt(0,2,0);
// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 0.7);
dir.position.set(5,10,2);
scene.add(dir);
// Resize handling
function onResize(){
renderer.setSize(window.innerWidth, window.innerHeight, false);
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
// ---------------------- Simple voxel world ----------------------
const blockSize = 1;
const blocks = new Map(); // key: "x,y,z" -> mesh
// Materials (simple)
const mat = new THREE.MeshStandardMaterial({ color:0x8B5A2B }); // dirt
const grassMat = new THREE.MeshStandardMaterial({ color:0x3cb043 });
// Create a small flat world (16x1x16 with a few random blocks)
const worldRadius = 8;
for(let x=-8;x<8;x++){
for(let z=-8;z<8;z++){
addBlock(x,0,z, Math.random()<0.02 ? 2 : 1); // sparse taller blocks occasionally
// add a few blocks above as scenery
}
}
for(let i=0;i<40;i++){
const rx = Math.floor((Math.random()*16)-8);
const rz = Math.floor((Math.random()*16)-8);
const ry = 1 + Math.floor(Math.random()*3);
addBlock(rx,ry,rz,1);
}
function key(x,y,z){ return `${x},${y},${z}`; }
function addBlock(x,y,z,type=1){
const k = key(x,y,z);
if(blocks.has(k)) return;
const geom = new THREE.BoxGeometry(blockSize,blockSize,blockSize);
const m = new THREE.Mesh(geom, type===2 ? grassMat : mat);
m.position.set(x*blockSize, y*blockSize + blockSize/2, z*blockSize);
scene.add(m);
blocks.set(k, {mesh:m, type});
}
function removeBlock(x,y,z){
const k = key(x,y,z);
const o = blocks.get(k);
if(!o) return false;
scene.remove(o.mesh);
o.mesh.geometry.dispose();
// material reused so not disposing
blocks.delete(k);
return true;
}
// ---------------------- Player physics & control state ----------------------
const player = {
pos: new THREE.Vector3(0,2,0),
vel: new THREE.Vector3(0,0,0),
speed: 3.0, // m/s
onGround: false,
height: 1.6
};
const GRAVITY = -20;
const JUMP_SPEED = 7;
let lastTime = performance.now()/1000;
// Camera rotation state
let yaw = 0; // horizontal
let pitch = 0; // vertical
const pitchLimit = Math.PI/2 - 0.1;
// ---------------------- Joystick (left) ----------------------
const joystick = document.getElementById('joystick');
const thumb = document.getElementById('thumb');
let joystickActive = false;
let joyId = null;
let joyStart = { x:0, y:0 };
let joyVec = { x:0, y:0 }; // -1..1
function resetThumb(){ thumb.style.transform = `translate(0px,0px)`; }
joystick.addEventListener('touchstart', e=>{
e.preventDefault();
const t = e.changedTouches[0];
joystickActive = true; joyId = t.identifier;
joyStart.x = t.clientX; joyStart.y = t.clientY;
});
window.addEventListener('touchmove', e=>{
if(!joystickActive) return;
for(const t of e.changedTouches){
if(t.identifier!==joyId) continue;
const dx = t.clientX - joyStart.x;
const dy = t.clientY - joyStart.y;
const max = 40; // pixels radius for full speed
const nx = Math.max(-1, Math.min(1, dx/max));
const ny = Math.max(-1, Math.min(1, dy/max));
// Update thumb position (invert y because screen coords)
thumb.style.transform = `translate(${nx*max}px, ${ny*max}px)`;
joyVec.x = nx;
joyVec.y = ny;
}
}, {passive:false});
window.addEventListener('touchend', e=>{
if(!joystickActive) return;
for(const t of e.changedTouches){
if(t.identifier!==joyId) continue;
joystickActive=false; joyId=null;
joyVec.x = 0; joyVec.y = 0;
resetThumb();
}
});
// ---------------------- Right-side camera swipe ----------------------
let rightTouchId = null;
let rightTouchStart = null;
function isOnRightSide(x){
return x > window.innerWidth * 0.45; // right half-ish
}
window.addEventListener('touchstart', e=>{
for(const t of e.changedTouches){
if(isOnRightSide(t.clientX) && rightTouchId===null){
rightTouchId = t.identifier;
rightTouchStart = { x: t.clientX, y: t.clientY };
}
}
}, {passive:true});
window.addEventListener('touchmove', e=>{
for(const t of e.changedTouches){
if(t.identifier!==rightTouchId) continue;
const dx = t.clientX - rightTouchStart.x;
const dy = t.clientY - rightTouchStart.y;
rightTouchStart.x = t.clientX; rightTouchStart.y = t.clientY;
// sensitivity tuned
const sensX = 0.005;
const sensY = 0.005;
yaw -= dx * sensX;
pitch -= dy * sensY;
pitch = Math.max(-pitchLimit, Math.min(pitchLimit, pitch));
}
}, {passive:true});
window.addEventListener('touchend', e=>{
for(const t of e.changedTouches){
if(t.identifier===rightTouchId){
rightTouchId = null;
rightTouchStart = null;
}
}
}, {passive:true});
// ---------------------- Buttons: Jump and Place/Break ----------------------
const jumpBtn = document.getElementById('jumpBtn');
const placeBtn = document.getElementById('placeBtn');
const status = document.getElementById('status');
jumpBtn.addEventListener('touchstart', e=>{
e.preventDefault();
if(player.onGround){
player.vel.y = JUMP_SPEED;
player.onGround = false;
}
}, {passive:false});
// Place/Break: short tap = place block in front, long press > 500ms = break targeted block
let placeTouchTimer = null;
let placeTouchId = null;
placeBtn.addEventListener('touchstart', e=>{
e.preventDefault();
const t = e.changedTouches[0];
placeTouchId = t.identifier;
placeTouchTimer = setTimeout(()=>{ // long-press -> break
placeTouchTimer = null;
doBreak();
}, 500);
}, {passive:false});
placeBtn.addEventListener('touchend', e=>{
e.preventDefault();
for(const t of e.changedTouches){
if(t.identifier!==placeTouchId) continue;
if(placeTouchTimer){
clearTimeout(placeTouchTimer);
placeTouchTimer = null;
doPlace();
}
placeTouchId = null;
}
}, {passive:false});
// ---------------------- Raycast helpers for place/break ----------------------
const raycaster = new THREE.Raycaster();
const tmpVec = new THREE.Vector3();
function worldCoordFromVec3(v){
// convert position to grid coordinate (integer block coords)
const x = Math.round(v.x / blockSize);
const y = Math.round((v.y - blockSize/2) / blockSize);
const z = Math.round(v.z / blockSize);
return {x,y,z};
}
function getTargetBlock(){
// cast a ray from camera forward to detect closest block within 6m
raycaster.set(camera.position, camera.getWorldDirection(tmpVec));
const intersects = raycaster.intersectObjects(Array.from(blocks.values()).map(b=>b.mesh), true);
if(intersects.length>0){
const p = intersects[0].point;
return worldCoordFromVec3(p);
}
return null;
}
function doPlace(){
// find a spot slightly in front of camera and place a block at integer position offset
const dir = camera.getWorldDirection(tmpVec).clone();
const placePos = camera.position.clone().add(dir.multiplyScalar(2.2)); // ~2m ahead
const coords = {
x: Math.round(placePos.x / blockSize),
y: Math.round((placePos.y - blockSize/2) / blockSize),
z: Math.round(placePos.z / blockSize)
};
// simple check: don't place inside player
const dx = coords.x*blockSize - player.pos.x;
const dz = coords.z*blockSize - player.pos.z;
if(Math.abs(dx) < 0.9 && Math.abs(dz) < 0.9 && coords.y <= Math.round(player.pos.y)) {
status.textContent = 'İçine blok koyma!';
setTimeout(()=> status.textContent='Hazır',800);
return;
}
addBlock(coords.x, coords.y, coords.z, 1);
status.textContent = `Koyuldu: ${coords.x},${coords.y},${coords.z}`;
setTimeout(()=> status.textContent='Hazır',700);
}
function doBreak(){
const t = getTargetBlock();
if(!t){ status.textContent='Hedef bulunamadı'; setTimeout(()=> status.textContent='Hazır',700); return; }
const ok = removeBlock(t.x, t.y, t.z);
if(ok){
status.textContent = `Kırıldı: ${t.x},${t.y},${t.z}`;
setTimeout(()=> status.textContent='Hazır',700);
} else {
status.textContent='Kırılamadı';
setTimeout(()=> status.textContent='Hazır',700);
}
}
// ---------------------- Game loop ----------------------
function step(dt){
// Move according to joystick - relative to camera yaw
const forward = new THREE.Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // forward vector
const right = new THREE.Vector3(Math.sin(yaw - Math.PI/2), 0, Math.cos(yaw - Math.PI/2));
const moveVec = new THREE.Vector3();
moveVec.addScaledVector(forward, -joyVec.y); // joystick y maps to forward/back
moveVec.addScaledVector(right, joyVec.x); // joystick x maps to right/left
if(moveVec.lengthSq() > 0.0001){
moveVec.normalize();
moveVec.multiplyScalar(player.speed);
}
// apply to horizontal velocity (simple)
player.vel.x = moveVec.x;
player.vel.z = moveVec.z;
// gravity
player.vel.y += GRAVITY * dt;
// integrate
player.pos.addScaledVector(player.vel, dt);
// simple ground collision (ground at y = 0 blocks exist there)
// Check block under player's feet
const footX = Math.round(player.pos.x / blockSize);
const footZ = Math.round(player.pos.z / blockSize);
// find highest block at that x,z
let highest = -Infinity;
for(const k of blocks.keys()){
const [bx,by,bz] = k.split(',').map(Number);
if(bx===footX && bz===footZ){
highest = Math.max(highest, by);
}
}
const groundY = (highest === -Infinity) ? -1 : highest; // -1 means no block
const groundWorldY = (groundY+1) * blockSize; // top of top block? but simpler use (groundY+1)*blockSize?
// We'll assume player's feet should be at block top + small offset
const minY = (groundY === -Infinity) ? 0 : (groundY+1)*blockSize + 0.0001;
if(player.pos.y <= player.height/2 + minY){
player.pos.y = player.height/2 + minY;
player.vel.y = 0;
player.onGround = true;
} else {
player.onGround = false;
}
// update camera to follow player
const camOffset = new THREE.Vector3(0, player.height-0.2, 0);
const camPos = player.pos.clone().add(camOffset);
// adjust camera behind player based on yaw/pitch
const distance = 6;
const cx = Math.cos(pitch) * Math.sin(yaw);
const cz = Math.cos(pitch) * Math.cos(yaw);
const cy = Math.sin(pitch);
const behind = new THREE.Vector3(-cx, -cy, -cz).multiplyScalar(distance);
camera.position.copy(camPos.clone().add(behind));
// look at player's head
camera.lookAt(player.pos.x, player.pos.y + player.height - 0.2, player.pos.z);
}
// minimal render loop
function animate(){
requestAnimationFrame(animate);
const now = performance.now()/1000;
let dt = Math.min(0.05, now - lastTime);
lastTime = now;
step(dt);
renderer.render(scene, camera);
}
animate();
// ---------------------- Small helpers: tap to teleport / debug (optional) ----------------------
/* Double tap to reset view (for convenience) */
let lastTap = 0;
window.addEventListener('touchend', (e)=>{
const t = performance.now();
if(t - lastTap < 300){
// double tap -> teleport forward (debug)
const dir = camera.getWorldDirection(new THREE.Vector3());
player.pos.add(dir.multiplyScalar(2));
status.textContent='İleri atla (debug)';
setTimeout(()=>status.textContent='Hazır',700);
}
lastTap = t;
}, {passive:true});
// Cleanup when page hidden (save battery)
document.addEventListener('visibilitychange', () => {
if(document.hidden){
// pause rendering? here we just lower pixel ratio
renderer.setPixelRatio(1);
} else {
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
}
});
</script>
</body>
</html><!doctype html>
<html lang="tr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover" />
<title>Mobil Voxel Demo (Three.js)</title>
<style>
html,body { height:100%; margin:0; background:#000; -webkit-user-select:none; user-select:none; touch-action:none; }
#game { position:fixed; inset:0; display:block; }
/* Joystick */
.joystick {
position:fixed;
left:16px;
bottom:16px;
width:120px;
height:120px;
border-radius:50%;
background: rgba(255,255,255,0.05);
display:flex;
align-items:center;
justify-content:center;
touch-action:none;
z-index:20;
}
.thumb {
width:50px;height:50px;border-radius:50%;
background: rgba(255,255,255,0.14);
transform: translate(0,0);
transition: transform 0s;
}
/* Right controls */
.right-controls {
position:fixed;
right:12px;
bottom:16px;
display:flex;
flex-direction:column;
gap:12px;
z-index:20;
align-items:center;
touch-action:none;
}
.btn {
width:72px;
height:72px;
border-radius:14px;
display:flex;
align-items:center;
justify-content:center;
background: rgba(255,255,255,0.06);
color: #fff;
font-weight:700;
-webkit-tap-highlight-color: transparent;
user-select:none;
}
.btn:active { background: rgba(255,255,255,0.12); }
/* top-right overlay for camera hint */
.hint {
position:fixed;
right:12px;
top:12px;
z-index:20;
color:#fff;
font-size:13px;
background: rgba(0,0,0,0.3);
padding:8px 10px;
border-radius:8px;
}
/* small FPS/Help at top-left */
.status {
position:fixed;
left:12px;
top:12px;
z-index:20;
color:#fff;
font-size:13px;
background: rgba(0,0,0,0.3);
padding:6px 10px;
border-radius:8px;
}
/* Ensure canvas covers the viewport */
canvas { width:100%; height:100%; display:block; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<!-- Joystick -->
<div class="joystick" id="joystick" aria-hidden="true">
<div class="thumb" id="thumb"></div>
</div>
<!-- Right side buttons -->
<div class="right-controls" id="rightControls" aria-hidden="true">
<div class="btn" id="jumpBtn">Zıpla</div>
<div class="btn" id="placeBtn">Koy/Kır</div>
</div>
<div class="hint">Sağda sürükle: kamera</div>
<div class="status" id="status">Hazır</div>
<script src="https://unpkg.com/three@0.152.2/build/three.min.js"></script>
<script>
/*
Mobil Voxel Demo
- Sol alt: joystick (touch)
- Sağ swipe: kamera döndürme
- Sağ butonlar: zıplama, blok koy/kır (kısa bas: koy, uzun bas: kır)
- Tek dosyada çalışır
*/
// ---------------------- Basic Three.js setup ----------------------
const canvas = document.getElementById('game');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight, false);
renderer.outputEncoding = THREE.sRGBEncoding;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87ceeb); // sky
const camera = new THREE.PerspectiveCamera(70, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0, 4, 8);
camera.lookAt(0,2,0);
// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dir = new THREE.DirectionalLight(0xffffff, 0.7);
dir.position.set(5,10,2);
scene.add(dir);
// Resize handling
function onResize(){
renderer.setSize(window.innerWidth, window.innerHeight, false);
camera.aspect = window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
}
window.addEventListener('resize', onResize);
// ---------------------- Simple voxel world ----------------------
const blockSize = 1;
const blocks = new Map(); // key: "x,y,z" -> mesh
// Materials (simple)
const mat = new THREE.MeshStandardMaterial({ color:0x8B5A2B }); // dirt
const grassMat = new THREE.MeshStandardMaterial({ color:0x3cb043 });
// Create a small flat world (16x1x16 with a few random blocks)
const worldRadius = 8;
for(let x=-8;x<8;x++){
for(let z=-8;z<8;z++){
addBlock(x,0,z, Math.random()<0.02 ? 2 : 1); // sparse taller blocks occasionally
// add a few blocks above as scenery
}
}
for(let i=0;i<40;i++){
const rx = Math.floor((Math.random()*16)-8);
const rz = Math.floor((Math.random()*16)-8);
const ry = 1 + Math.floor(Math.random()*3);
addBlock(rx,ry,rz,1);
}
function key(x,y,z){ return `${x},${y},${z}`; }
function addBlock(x,y,z,type=1){
const k = key(x,y,z);
if(blocks.has(k)) return;
const geom = new THREE.BoxGeometry(blockSize,blockSize,blockSize);
const m = new THREE.Mesh(geom, type===2 ? grassMat : mat);
m.position.set(x*blockSize, y*blockSize + blockSize/2, z*blockSize);
scene.add(m);
blocks.set(k, {mesh:m, type});
}
function removeBlock(x,y,z){
const k = key(x,y,z);
const o = blocks.get(k);
if(!o) return false;
scene.remove(o.mesh);
o.mesh.geometry.dispose();
// material reused so not disposing
blocks.delete(k);
return true;
}
// ---------------------- Player physics & control state ----------------------
const player = {
pos: new THREE.Vector3(0,2,0),
vel: new THREE.Vector3(0,0,0),
speed: 3.0, // m/s
onGround: false,
height: 1.6
};
const GRAVITY = -20;
const JUMP_SPEED = 7;
let lastTime = performance.now()/1000;
// Camera rotation state
let yaw = 0; // horizontal
let pitch = 0; // vertical
const pitchLimit = Math.PI/2 - 0.1;
// ---------------------- Joystick (left) ----------------------
const joystick = document.getElementById('joystick');
const thumb = document.getElementById('thumb');
let joystickActive = false;
let joyId = null;
let joyStart = { x:0, y:0 };
let joyVec = { x:0, y:0 }; // -1..1
function resetThumb(){ thumb.style.transform = `translate(0px,0px)`; }
joystick.addEventListener('touchstart', e=>{
e.preventDefault();
const t = e.changedTouches[0];
joystickActive = true; joyId = t.identifier;
joyStart.x = t.clientX; joyStart.y = t.clientY;
});
window.addEventListener('touchmove', e=>{
if(!joystickActive) return;
for(const t of e.changedTouches){
if(t.identifier!==joyId) continue;
const dx = t.clientX - joyStart.x;
const dy = t.clientY - joyStart.y;
const max = 40; // pixels radius for full speed
const nx = Math.max(-1, Math.min(1, dx/max));
const ny = Math.max(-1, Math.min(1, dy/max));
// Update thumb position (invert y because screen coords)
thumb.style.transform = `translate(${nx*max}px, ${ny*max}px)`;
joyVec.x = nx;
joyVec.y = ny;
}
}, {passive:false});
window.addEventListener('touchend', e=>{
if(!joystickActive) return;
for(const t of e.changedTouches){
if(t.identifier!==joyId) continue;
joystickActive=false; joyId=null;
joyVec.x = 0; joyVec.y = 0;
resetThumb();
}
});
// ---------------------- Right-side camera swipe ----------------------
let rightTouchId = null;
let rightTouchStart = null;
function isOnRightSide(x){
return x > window.innerWidth * 0.45; // right half-ish
}
window.addEventListener('touchstart', e=>{
for(const t of e.changedTouches){
if(isOnRightSide(t.clientX) && rightTouchId===null){
rightTouchId = t.identifier;
rightTouchStart = { x: t.clientX, y: t.clientY };
}
}
}, {passive:true});
window.addEventListener('touchmove', e=>{
for(const t of e.changedTouches){
if(t.identifier!==rightTouchId) continue;
const dx = t.clientX - rightTouchStart.x;
const dy = t.clientY - rightTouchStart.y;
rightTouchStart.x = t.clientX; rightTouchStart.y = t.clientY;
// sensitivity tuned
const sensX = 0.005;
const sensY = 0.005;
yaw -= dx * sensX;
pitch -= dy * sensY;
pitch = Math.max(-pitchLimit, Math.min(pitchLimit, pitch));
}
}, {passive:true});
window.addEventListener('touchend', e=>{
for(const t of e.changedTouches){
if(t.identifier===rightTouchId){
rightTouchId = null;
rightTouchStart = null;
}
}
}, {passive:true});
// ---------------------- Buttons: Jump and Place/Break ----------------------
const jumpBtn = document.getElementById('jumpBtn');
const placeBtn = document.getElementById('placeBtn');
const status = document.getElementById('status');
jumpBtn.addEventListener('touchstart', e=>{
e.preventDefault();
if(player.onGround){
player.vel.y = JUMP_SPEED;
player.onGround = false;
}
}, {passive:false});
// Place/Break: short tap = place block in front, long press > 500ms = break targeted block
let placeTouchTimer = null;
let placeTouchId = null;
placeBtn.addEventListener('touchstart', e=>{
e.preventDefault();
const t = e.changedTouches[0];
placeTouchId = t.identifier;
placeTouchTimer = setTimeout(()=>{ // long-press -> break
placeTouchTimer = null;
doBreak();
}, 500);
}, {passive:false});
placeBtn.addEventListener('touchend', e=>{
e.preventDefault();
for(const t of e.changedTouches){
if(t.identifier!==placeTouchId) continue;
if(placeTouchTimer){
clearTimeout(placeTouchTimer);
placeTouchTimer = null;
doPlace();
}
placeTouchId = null;
}
}, {passive:false});
// ---------------------- Raycast helpers for place/break ----------------------
const raycaster = new THREE.Raycaster();
const tmpVec = new THREE.Vector3();
function worldCoordFromVec3(v){
// convert position to grid coordinate (integer block coords)
const x = Math.round(v.x / blockSize);
const y = Math.round((v.y - blockSize/2) / blockSize);
const z = Math.round(v.z / blockSize);
return {x,y,z};
}
function getTargetBlock(){
// cast a ray from camera forward to detect closest block within 6m
raycaster.set(camera.position, camera.getWorldDirection(tmpVec));
const intersects = raycaster.intersectObjects(Array.from(blocks.values()).map(b=>b.mesh), true);
if(intersects.length>0){
const p = intersects[0].point;
return worldCoordFromVec3(p);
}
return null;
}
function doPlace(){
// find a spot slightly in front of camera and place a block at integer position offset
const dir = camera.getWorldDirection(tmpVec).clone();
const placePos = camera.position.clone().add(dir.multiplyScalar(2.2)); // ~2m ahead
const coords = {
x: Math.round(placePos.x / blockSize),
y: Math.round((placePos.y - blockSize/2) / blockSize),
z: Math.round(placePos.z / blockSize)
};
// simple check: don't place inside player
const dx = coords.x*blockSize - player.pos.x;
const dz = coords.z*blockSize - player.pos.z;
if(Math.abs(dx) < 0.9 && Math.abs(dz) < 0.9 && coords.y <= Math.round(player.pos.y)) {
status.textContent = 'İçine blok koyma!';
setTimeout(()=> status.textContent='Hazır',800);
return;
}
addBlock(coords.x, coords.y, coords.z, 1);
status.textContent = `Koyuldu: ${coords.x},${coords.y},${coords.z}`;
setTimeout(()=> status.textContent='Hazır',700);
}
function doBreak(){
const t = getTargetBlock();
if(!t){ status.textContent='Hedef bulunamadı'; setTimeout(()=> status.textContent='Hazır',700); return; }
const ok = removeBlock(t.x, t.y, t.z);
if(ok){
status.textContent = `Kırıldı: ${t.x},${t.y},${t.z}`;
setTimeout(()=> status.textContent='Hazır',700);
} else {
status.textContent='Kırılamadı';
setTimeout(()=> status.textContent='Hazır',700);
}
}
// ---------------------- Game loop ----------------------
function step(dt){
// Move according to joystick - relative to camera yaw
const forward = new THREE.Vector3(Math.sin(yaw), 0, Math.cos(yaw)); // forward vector
const right = new THREE.Vector3(Math.sin(yaw - Math.PI/2), 0, Math.cos(yaw - Math.PI/2));
const moveVec = new THREE.Vector3();
moveVec.addScaledVector(forward, -joyVec.y); // joystick y maps to forward/back
moveVec.addScaledVector(right, joyVec.x); // joystick x maps to right/left
if(moveVec.lengthSq() > 0.0001){
moveVec.normalize();
moveVec.multiplyScalar(player.speed);
}
// apply to horizontal velocity (simple)
player.vel.x = moveVec.x;
player.vel.z = moveVec.z;
// gravity
player.vel.y += GRAVITY * dt;
// integrate
player.pos.addScaledVector(player.vel, dt);
// simple ground collision (ground at y = 0 blocks exist there)
// Check block under player's feet
const footX = Math.round(player.pos.x / blockSize);
const footZ = Math.round(player.pos.z / blockSize);
// find highest block at that x,z
let highest = -Infinity;
for(const k of blocks.keys()){
const [bx,by,bz] = k.split(',').map(Number);
if(bx===footX && bz===footZ){
highest = Math.max(highest, by);
}
}
const groundY = (highest === -Infinity) ? -1 : highest; // -1 means no block
const groundWorldY = (groundY+1) * blockSize; // top of top block? but simpler use (groundY+1)*blockSize?
// We'll assume player's feet should be at block top + small offset
const minY = (groundY === -Infinity) ? 0 : (groundY+1)*blockSize + 0.0001;
if(player.pos.y <= player.height/2 + minY){
player.pos.y = player.height/2 + minY;
player.vel.y = 0;
player.onGround = true;
} else {
player.onGround = false;
}
// update camera to follow player
const camOffset = new THREE.Vector3(0, player.height-0.2, 0);
const camPos = player.pos.clone().add(camOffset);
// adjust camera behind player based on yaw/pitch
const distance = 6;
const cx = Math.cos(pitch) * Math.sin(yaw);
const cz = Math.cos(pitch) * Math.cos(yaw);
const cy = Math.sin(pitch);
const behind = new THREE.Vector3(-cx, -cy, -cz).multiplyScalar(distance);
camera.position.copy(camPos.clone().add(behind));
// look at player's head
camera.lookAt(player.pos.x, player.pos.y + player.height - 0.2, player.pos.z);
}
// minimal render loop
function animate(){
requestAnimationFrame(animate);
const now = performance.now()/1000;
let dt = Math.min(0.05, now - lastTime);
lastTime = now;
step(dt);
renderer.render(scene, camera);
}
animate();
// ---------------------- Small helpers: tap to teleport / debug (optional) ----------------------
/* Double tap to reset view (for convenience) */
let lastTap = 0;
window.addEventListener('touchend', (e)=>{
const t = performance.now();
if(t - lastTap < 300){
// double tap -> teleport forward (debug)
const dir = camera.getWorldDirection(new THREE.Vector3());
player.pos.add(dir.multiplyScalar(2));
status.textContent='İleri atla (debug)';
setTimeout(()=>status.textContent='Hazır',700);
}
lastTap = t;
}, {passive:true});
// Cleanup when page hidden (save battery)
document.addEventListener('visibilitychange', () => {
if(document.hidden){
// pause rendering? here we just lower pixel ratio
renderer.setPixelRatio(1);
} else {
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
}
});
</script>
</body>
</html>2
2
185KB
649KB
4,501.0ms
108.0ms
4,501.0ms