Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Простой Minecraft-клон на Babylon.js</title> | |
| <style> | |
| html, body { | |
| overflow: hidden; | |
| width: 100%; | |
| height: 100%; | |
| margin: 0; | |
| padding: 0; | |
| font-family: sans-serif; /* Добавим шрифт для UI */ | |
| } | |
| #renderCanvas { | |
| width: 100%; | |
| height: 100%; | |
| touch-action: none; /* Для мобильных устройств */ | |
| } | |
| /* Простой интерфейс для прицела */ | |
| #crosshair { | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 10px; | |
| height: 10px; | |
| border: 1px solid white; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; /* Чтобы не мешал кликам */ | |
| } | |
| /* Инструкция */ | |
| #instructions { | |
| position: absolute; | |
| bottom: 10px; | |
| left: 10px; | |
| color: white; | |
| background-color: rgba(0,0,0,0.5); | |
| padding: 5px; | |
| border-radius: 3px; | |
| font-size: 12px; | |
| } | |
| </style> | |
| <!-- Подключение Babylon.js --> | |
| <script src="https://cdn.babylonjs.com/babylon.js"></script> | |
| <!-- Опционально: загрузчики моделей/текстур, если понадобятся --> | |
| <!-- <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script> --> | |
| </head> | |
| <body> | |
| <canvas id="renderCanvas"></canvas> | |
| <div id="crosshair"></div> | |
| <div id="instructions"> | |
| WASD/Стрелки: Движение | Пробел: Вверх | Shift: Вниз <br> | |
| Левый клик: Удалить блок | Правый клик: Поставить блок | |
| </div> | |
| <script> | |
| const canvas = document.getElementById('renderCanvas'); | |
| const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true }); | |
| const BLOCK_SIZE = 1; // Размер одного блока | |
| const WORLD_WIDTH = 16; // Ширина мира в блоках | |
| const WORLD_DEPTH = 16; // Глубина мира в блоках | |
| const WORLD_HEIGHT = 8; // Максимальная высота мира (для генерации) | |
| // --- Создание сцены --- | |
| const createScene = () => { | |
| const scene = new BABYLON.Scene(engine); | |
| scene.clearColor = new BABYLON.Color3(0.5, 0.8, 1.0); // Цвет неба | |
| scene.gravity = new BABYLON.Vector3(0, -0.9, 0); // Гравитация | |
| scene.collisionsEnabled = true; // Включаем обработку столкновений | |
| // --- Камера --- | |
| const camera = new BABYLON.UniversalCamera("playerCamera", new BABYLON.Vector3(WORLD_WIDTH / 2, WORLD_HEIGHT + 2, WORLD_DEPTH / 2), scene); | |
| camera.setTarget(BABYLON.Vector3.Zero()); // Смотрим в центр | |
| camera.attachControl(canvas, true); // Подключаем управление | |
| camera.speed = 0.2; // Скорость движения | |
| camera.angularSensibility = 4000; // Чувствительность мыши | |
| // Включаем столкновения для камеры | |
| camera.checkCollisions = true; | |
| camera.applyGravity = true; | |
| // Задаем "эллипсоид" - физическую модель игрока для столкновений | |
| camera.ellipsoid = new BABYLON.Vector3(0.5, 0.9, 0.5); // Ширина, Высота, Глубина | |
| // Управление с клавиатуры (WASD + пробел/shift) | |
| camera.keysUp.push(87); // W | |
| camera.keysDown.push(83); // S | |
| camera.keysLeft.push(65); // A | |
| camera.keysRight.push(68); // D | |
| // Добавляем управление вверх/вниз (не стандартное для UniversalCamera) | |
| scene.onBeforeRenderObservable.add(() => { | |
| if (camera.inputs.attached.keyboard) { | |
| const keyboard = camera.inputs.attached.keyboard; | |
| if (keyboard.directInput[' ']) { // Пробел | |
| camera.cameraDirection.y += camera.speed / 10; // Двигаемся вверх | |
| } | |
| if (keyboard.directInput[16]) { // Shift | |
| camera.cameraDirection.y -= camera.speed / 10; // Двигаемся вниз | |
| } | |
| } | |
| }); | |
| // --- Освещение --- | |
| const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene); | |
| light.intensity = 0.8; | |
| // --- Материалы блоков --- | |
| const grassMaterial = new BABYLON.StandardMaterial("grassMat", scene); | |
| grassMaterial.diffuseColor = new BABYLON.Color3(0.4, 0.8, 0.4); // Зеленый | |
| grassMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); // Меньше бликов | |
| const dirtMaterial = new BABYLON.StandardMaterial("dirtMat", scene); | |
| dirtMaterial.diffuseColor = new BABYLON.Color3(0.6, 0.4, 0.2); // Коричневый | |
| dirtMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); | |
| const stoneMaterial = new BABYLON.StandardMaterial("stoneMat", scene); | |
| stoneMaterial.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5); // Серый | |
| stoneMaterial.specularColor = new BABYLON.Color3(0.1, 0.1, 0.1); | |
| // --- Мир блоков --- | |
| // Используем Map для хранения блоков { "x,y,z": mesh } | |
| const blocks = new Map(); | |
| // Прототип блока (для клонирования) | |
| const blockPrototype = BABYLON.MeshBuilder.CreateBox("blockProto", { size: BLOCK_SIZE }, scene); | |
| blockPrototype.isVisible = false; // Сам прототип не видим | |
| blockPrototype.checkCollisions = true; // Блоки должны иметь коллизию | |
| // Генерация простого ландшафта | |
| for (let x = 0; x < WORLD_WIDTH; x++) { | |
| for (let z = 0; z < WORLD_DEPTH; z++) { | |
| // Простая высота на основе шума (очень примитивно) | |
| const height = Math.floor(WORLD_HEIGHT / 2 + Math.sin(x * 0.3) * 2 + Math.cos(z * 0.2) * 2); | |
| for (let y = 0; y < height; y++) { | |
| const block = blockPrototype.clone(`block_${x}_${y}_${z}`); | |
| block.position = new BABYLON.Vector3( | |
| x * BLOCK_SIZE + BLOCK_SIZE / 2, | |
| y * BLOCK_SIZE + BLOCK_SIZE / 2, | |
| z * BLOCK_SIZE + BLOCK_SIZE / 2 | |
| ); | |
| block.isVisible = true; | |
| // Назначаем материал в зависимости от высоты | |
| if (y === height - 1) { | |
| block.material = grassMaterial; // Верхний слой - трава | |
| } else if (y > height - 4) { | |
| block.material = dirtMaterial; // Под травой - земля | |
| } else { | |
| block.material = stoneMaterial; // Глубже - камень | |
| } | |
| const blockKey = `${block.position.x - BLOCK_SIZE/2},${block.position.y - BLOCK_SIZE/2},${block.position.z - BLOCK_SIZE/2}`; // Ключ по нижнему углу | |
| blocks.set(blockKey, block); | |
| block.isBlock = true; // Флаг, что это наш блок | |
| } | |
| } | |
| } | |
| // --- Взаимодействие с блоками --- | |
| scene.onPointerDown = (evt, pickResult) => { | |
| // evt.button: 0 = левый клик, 1 = средний, 2 = правый клик | |
| // --- Удаление блока (левый клик) --- | |
| if (evt.button === 0) { | |
| if (pickResult.hit && pickResult.pickedMesh && pickResult.pickedMesh.isBlock) { | |
| const blockToRemove = pickResult.pickedMesh; | |
| const blockKey = `${blockToRemove.position.x - BLOCK_SIZE/2},${blockToRemove.position.y - BLOCK_SIZE/2},${blockToRemove.position.z - BLOCK_SIZE/2}`; | |
| blocks.delete(blockKey); | |
| blockToRemove.dispose(); | |
| } | |
| } | |
| // --- Размещение блока (правый клик) --- | |
| if (evt.button === 2) { | |
| if (pickResult.hit && pickResult.pickedMesh) { // Проверяем, что попали во что-то | |
| // Определяем позицию для нового блока | |
| // Она должна быть смещена от точки попадания по нормали к поверхности | |
| const normal = pickResult.getNormal(true); // Получаем нормаль к грани | |
| const hitPoint = pickResult.pickedPoint; | |
| // Вычисляем центр потенциального нового блока | |
| const potentialPos = hitPoint.add(normal.scale(BLOCK_SIZE / 2)); | |
| // Округляем (снапим) к сетке блоков | |
| // Важно: Вычитаем/добавляем половину размера блока для центрирования | |
| const newBlockX = Math.floor(potentialPos.x / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2; | |
| const newBlockY = Math.floor(potentialPos.y / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2; | |
| const newBlockZ = Math.floor(potentialPos.z / BLOCK_SIZE) * BLOCK_SIZE + BLOCK_SIZE / 2; | |
| const newBlockKey = `${newBlockX - BLOCK_SIZE/2},${newBlockY - BLOCK_SIZE/2},${newBlockZ - BLOCK_SIZE/2}`; | |
| // Проверяем, не занято ли это место | |
| if (!blocks.has(newBlockKey)) { | |
| // Проверяем, не ставим ли блок "внутрь" игрока | |
| const playerBox = new BABYLON.BoundingBox( | |
| camera.position.subtract(camera.ellipsoid), | |
| camera.position.add(camera.ellipsoid) | |
| ); | |
| const newBlockBox = new BABYLON.BoundingBox( | |
| new BABYLON.Vector3(newBlockX - BLOCK_SIZE/2, newBlockY - BLOCK_SIZE/2, newBlockZ - BLOCK_SIZE/2), | |
| new BABYLON.Vector3(newBlockX + BLOCK_SIZE/2, newBlockY + BLOCK_SIZE/2, newBlockZ + BLOCK_SIZE/2) | |
| ); | |
| if (!playerBox.intersects(newBlockBox)) { | |
| const newBlock = blockPrototype.clone(`block_${newBlockKey.replace(/,/g, '_')}`); | |
| newBlock.position = new BABYLON.Vector3(newBlockX, newBlockY, newBlockZ); | |
| newBlock.material = dirtMaterial; // По умолчанию ставим землю | |
| newBlock.isVisible = true; | |
| newBlock.isBlock = true; | |
| blocks.set(newBlockKey, newBlock); | |
| } else { | |
| console.log("Нельзя ставить блок внутри себя!"); | |
| } | |
| } else { | |
| console.log("Место занято!"); | |
| } | |
| } | |
| } | |
| }; | |
| // Отключаем контекстное меню по правому клику на канвасе | |
| canvas.addEventListener("contextmenu", (evt) => { | |
| evt.preventDefault(); | |
| }); | |
| // --- Оптимизация (очень базовая) --- | |
| // Можно добавить Octree для ускорения рендеринга и коллизий | |
| // scene.createOrUpdateSelectionOctree(); | |
| return scene; | |
| }; | |
| // --- Запуск --- | |
| const scene = createScene(); | |
| engine.runRenderLoop(() => { | |
| scene.render(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| engine.resize(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |