Aleksmorshen commited on
Commit
758ebd2
·
verified ·
1 Parent(s): 324e9b3

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +318 -485
index.html CHANGED
@@ -2,26 +2,41 @@
2
  <html lang="ru">
3
  <head>
4
  <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
- <title>MiniCraft 3D Demo</title>
7
- <script src="https://cdn.babylonjs.com/babylon.js"></script>
8
- <!-- Для работы Pointer событий на всех устройствах (важно для тачскрина) -->
9
- <script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
10
  <style>
 
11
  html, body {
 
12
  width: 100%;
13
  height: 100%;
14
  margin: 0;
15
  padding: 0;
16
- overflow: hidden;
17
- background-color: #87CEEB; /* Цвет неба по умолчанию */
18
  font-family: sans-serif;
 
 
19
  }
 
20
  #renderCanvas {
21
  width: 100%;
22
  height: 100%;
23
- touch-action: none; /* Отключаем стандартные жесты браузера на канвасе */
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
 
 
25
  #crosshair {
26
  position: absolute;
27
  top: 50%;
@@ -29,554 +44,372 @@
29
  width: 10px;
30
  height: 10px;
31
  border: 1px solid white;
32
- background-color: rgba(0, 0, 0, 0.5);
33
  transform: translate(-50%, -50%);
34
- pointer-events: none; /* Не мешает кликам по канвасу */
35
- z-index: 10;
36
- }
37
- #controls {
38
- position: absolute;
39
- bottom: 10px;
40
- left: 10px;
41
- right: 10px;
42
- display: flex;
43
- justify-content: space-between;
44
- align-items: flex-end;
45
- pointer-events: none; /* Сам контейнер не ловит события */
46
- z-index: 5;
47
- }
48
- .joystick-area {
49
- width: 120px;
50
- height: 120px;
51
- background-color: rgba(200, 200, 200, 0.4);
52
- border-radius: 50%;
53
- position: relative;
54
- pointer-events: auto; /* Область джойстика ловит события */
55
  }
56
- #joystick {
57
- width: 50px;
58
- height: 50px;
59
- background-color: rgba(100, 100, 100, 0.7);
60
- border-radius: 50%;
61
  position: absolute;
62
- top: 50%;
63
- left: 50%;
64
- transform: translate(-50%, -50%);
65
- cursor: grab;
66
- }
67
- #actionButtons {
68
- display: flex;
69
  gap: 15px;
70
- pointer-events: auto; /* Кнопки ловят события */
71
- }
72
- .action-button {
 
 
 
73
  width: 60px;
74
  height: 60px;
75
- border: 2px solid rgba(50, 50, 50, 0.7);
76
- background-color: rgba(200, 200, 200, 0.5);
77
- border-radius: 10px;
78
- font-size: 12px;
79
  font-weight: bold;
80
  display: flex;
81
  justify-content: center;
82
  align-items: center;
83
- text-align: center;
84
  cursor: pointer;
85
  user-select: none; /* Запретить выделение текста на кнопке */
86
  }
87
- .action-button:active {
88
- background-color: rgba(150, 150, 150, 0.7);
89
- }
90
- /* Скрываем мобильные контролы на десктопе (ширина > 800px) */
91
- /* @media (min-width: 801px) {
92
- #controls {
93
- display: none;
 
94
  }
95
- } */ /* Пока оставим видимыми для теста */
96
-
97
- #blockSelector {
98
- position: absolute;
99
- top: 10px;
100
- left: 50%;
101
- transform: translateX(-50%);
102
- background-color: rgba(0,0,0,0.3);
103
- padding: 5px;
104
- border-radius: 5px;
105
- display: flex;
106
- gap: 5px;
107
- z-index: 15;
108
- }
109
- .block-option {
110
- width: 40px;
111
- height: 40px;
112
- border: 2px solid #555;
113
- background-size: cover;
114
- cursor: pointer;
115
- }
116
- .block-option.selected {
117
- border-color: yellow;
118
- box-shadow: 0 0 5px yellow;
119
  }
 
120
  </style>
 
 
 
 
121
  </head>
122
  <body>
123
- <canvas id="renderCanvas"></canvas>
124
- <div id="crosshair"></div>
125
-
126
- <!-- Элементы управления для мобильных -->
127
- <div id="controls">
128
- <div class="joystick-area" id="joystickArea">
129
- <div id="joystick"></div>
130
- </div>
131
- <div id="actionButtons">
132
- <div class="action-button" id="jumpButton">JUMP</div>
133
- <div class="action-button" id="breakButton">BREAK</div>
134
- <div class="action-button" id="placeButton">PLACE</div>
135
- </div>
136
  </div>
 
137
 
138
- <!-- Выбор блока -->
139
- <div id="blockSelector">
140
- </div>
141
-
 
142
 
143
  <script>
144
  const canvas = document.getElementById('renderCanvas');
145
- const engine = new BABYLON.Engine(canvas, true, { stencil: true, preserveDrawingBuffer: true }, true);
146
-
147
- // --- Переменные для управления ---
148
- let moveForward = 0;
149
- let moveRight = 0;
150
- let wantsToJump = false;
151
- let currentBlockType = 'grass'; // Блок по умолчанию для установки
152
-
153
- // --- Текстуры (используем простые URL для примера) ---
154
- // На реальном проекте лучше использовать свои или CC0 текстуры
155
- const textureUrls = {
156
- grassTop: "https://assets.babylonjs.com/environments/grass.png",
157
- grassSide: "https://assets.babylonjs.com/environments/grass_side.png", // Предположим, что есть такая
158
- dirt: "https://assets.babylonjs.com/environments/dirt.jpg",
159
- stone: "https://assets.babylonjs.com/environments/rock.png", // Похоже на камень
160
- wood: "https://assets.babylonjs.com/environments/wood.png" // Похоже на дерево
161
- };
162
- // Простая замена для grassSide, если её нет
163
- if (!textureUrls.grassSide) textureUrls.grassSide = textureUrls.dirt;
164
 
 
 
 
 
 
 
 
165
 
166
- // --- Функция создания сцены ---
167
- const createScene = () => {
 
 
 
168
  const scene = new BABYLON.Scene(engine);
169
- scene.clearColor = new BABYLON.Color3(0.53, 0.81, 0.92); // Цвет неба
170
- scene.gravity = new BABYLON.Vector3(0, -0.9, 0); // Гравитация
171
- scene.collisionsEnabled = true; // Включаем обработку столкновений
172
-
173
- // --- Камера (Игрок) ---
174
- const camera = new BABYLON.UniversalCamera("playerCamera", new BABYLON.Vector3(5, 5, -5), scene);
175
- camera.setTarget(new BABYLON.Vector3(0, 1.6, 0)); // Куда смотрит камера при старте
176
- camera.attachControl(canvas, true); // Управление мышью/клавиатурой
177
- camera.speed = 0.15; // Скорость движения
178
- camera.angularSensibility = 4000; // Чувствительность мыши
179
-
180
- // Настройки для столкновений и гравитации камеры
 
 
 
 
 
 
 
 
 
 
181
  camera.checkCollisions = true;
182
  camera.applyGravity = true;
183
- // Эллипсоид - "хитбокс" игрока (ширина, высота, ширина)
184
- camera.ellipsoid = new BABYLON.Vector3(0.4, 0.9, 0.4);
185
- // Смещение эллипсоида, чтобы "ноги" были ниже камеры
186
- camera.ellipsoidOffset = new BABYLON.Vector3(0, 0.9, 0);
187
-
188
- // Убираем стандартное управление клавиатурой, если используем джойстик
189
- // camera.inputs.removeByType("FreeCameraKeyboardMoveInput");
190
- // camera.inputs.removeByType("FreeCameraMouseInput"); // Оставим мышь для обзора
191
 
192
  // --- Освещение ---
193
  const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
194
  light.intensity = 0.8;
195
 
196
- // --- Материалы блоков ---
197
- const materials = {};
198
-
199
- // Трава (MultiMaterial)
200
- const grassMat = new BABYLON.StandardMaterial("grassMat", scene);
201
- grassMat.diffuseTexture = new BABYLON.Texture(textureUrls.grassTop, scene);
202
- const grassSideMat = new BABYLON.StandardMaterial("grassSideMat", scene);
203
- grassSideMat.diffuseTexture = new BABYLON.Texture(textureUrls.grassSide, scene);
204
- const grassBottomMat = new BABYLON.StandardMaterial("grassBottomMat", scene);
205
- grassBottomMat.diffuseTexture = new BABYLON.Texture(textureUrls.dirt, scene); // Низ травы = земля
206
-
207
- const grassMultiMat = new BABYLON.MultiMaterial("grassMulti", scene);
208
- grassMultiMat.subMaterials.push(grassSideMat); // right, left, front, back (индексы 0, 1, 2, 3)
209
- grassMultiMat.subMaterials.push(grassSideMat);
210
- grassMultiMat.subMaterials.push(grassSideMat);
211
- grassMultiMat.subMaterials.push(grassSideMat);
212
- grassMultiMat.subMaterials.push(grassMat); // top (индекс 4)
213
- grassMultiMat.subMaterials.push(grassBottomMat); // bottom (индекс 5)
214
- materials['grass'] = grassMultiMat;
215
-
216
- // Земля
217
- const dirtMat = new BABYLON.StandardMaterial("dirtMat", scene);
218
- dirtMat.diffuseTexture = new BABYLON.Texture(textureUrls.dirt, scene);
219
- materials['dirt'] = dirtMat;
220
-
221
- // Камень
222
- const stoneMat = new BABYLON.StandardMaterial("stoneMat", scene);
223
- stoneMat.diffuseTexture = new BABYLON.Texture(textureUrls.stone, scene);
224
- materials['stone'] = stoneMat;
225
-
226
- // Дерево
227
- const woodMat = new BABYLON.StandardMaterial("woodMat", scene);
228
- woodMat.diffuseTexture = new BABYLON.Texture(textureUrls.wood, scene);
229
- materials['wood'] = woodMat;
230
-
231
-
232
- // --- Функция создания блока ---
233
- const createBlock = (type, x, y, z) => {
234
- const block = BABYLON.MeshBuilder.CreateBox(`block_${type}_${x}_${y}_${z}`, {size: 1}, scene);
235
- block.position = new BABYLON.Vector3(x + 0.5, y + 0.5, z + 0.5); // Центрируем блок в координате
236
- block.checkCollisions = true; // Включаем столкновения для блока
237
- block.isPickable = true; // Разрешаем "подбирать" лучом
238
-
239
- if (type === 'grass' && materials['grass'] instanceof BABYLON.MultiMaterial) {
240
- block.material = materials['grass'];
241
- // Указываем, какой subMaterial использовать для каждой грани
242
- block.subMeshes = [];
243
- // Индексы вершин куба стандартные
244
- // Каждая грань - 2 треугольника = 6 вершин
245
- const verticesCount = 36;
246
- // 4 боковые грани (0-3), 1 верхняя (4), 1 нижняя (5)
247
- block.subMeshes.push(new BABYLON.SubMesh(0, 0, verticesCount, 0, 6, block)); // right
248
- block.subMeshes.push(new BABYLON.SubMesh(1, 0, verticesCount, 6, 6, block)); // left
249
- block.subMeshes.push(new BABYLON.SubMesh(2, 0, verticesCount, 12, 6, block)); // front
250
- block.subMeshes.push(new BABYLON.SubMesh(3, 0, verticesCount, 18, 6, block)); // back
251
- block.subMeshes.push(new BABYLON.SubMesh(4, 0, verticesCount, 24, 6, block)); // top
252
- block.subMeshes.push(new BABYLON.SubMesh(5, 0, verticesCount, 30, 6, block)); // bottom
253
- } else if (materials[type]) {
254
- block.material = materials[type];
255
- } else {
256
- console.warn(`Material for type "${type}" not found!`);
257
- // Можно назначить материал по умолчанию
258
- block.material = materials['dirt'];
259
- }
260
-
261
- // Оптимизация для статичных блоков (можно раскомментировать, если мир не меняется часто)
262
- // block.freezeWorldMatrix();
263
- return block;
264
  };
265
-
266
-
267
- // --- Создание простого мира ---
268
- const worldSize = 10; // Размер мира (10x10 блоков)
269
- const worldHeight = 3; // Высота слоя земли/травы
270
-
271
- for (let x = 0; x < worldSize; x++) {
272
- for (let z = 0; z < worldSize; z++) {
273
- createBlock('stone', x, 0, z); // Слой камня внизу
274
- createBlock('dirt', x, 1, z); // Слой земли
275
- createBlock('grass', x, 2, z); // Слой травы сверху
 
 
 
 
 
 
 
 
 
276
  }
277
  }
278
- // Добавим пару блоков для теста установки/удаления
279
- createBlock('wood', 5, 3, 5);
280
- createBlock('stone', 6, 3, 5);
281
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
- // --- Механика установки / удаления блоков ---
284
- let lastActionTime = 0;
285
- const actionCooldown = 200; // Задержка между действиями в мс
286
 
287
- const performAction = (isPlace) => {
288
- const currentTime = Date.now();
289
- if (currentTime - lastActionTime < actionCooldown) {
290
- return; // Игнорировать действие, если еще кулдаун
291
- }
292
- lastActionTime = currentTime;
293
-
294
- const ray = camera.getForwardRay(5); // Луч вперед на 5 единиц
295
- const pickInfo = scene.pickWithRay(ray, (mesh) => mesh.name.startsWith("block_"));
296
-
297
- if (pickInfo.hit && pickInfo.pickedMesh) {
298
- const pickedBlock = pickInfo.pickedMesh;
299
-
300
- if (isPlace) {
301
- // --- Установка блока ---
302
- const normal = pickInfo.getNormal(true); // Нормаль к грани, куда попал луч
303
- if (!normal) return;
304
-
305
- // Вычисляем позицию для нового блока, прилегающую к грани старого
306
- // Смещаем на половину размера блока вдоль нормали и округляем до сетки
307
- const placePos = pickedBlock.position.add(normal.scale(0.5));
308
- const targetX = Math.floor(placePos.x);
309
- const targetY = Math.floor(placePos.y);
310
- const targetZ = Math.floor(placePos.z);
311
-
312
- // Проверка, не ставим ли блок внутрь игрока
313
- const playerFeetY = Math.floor(camera.position.y - camera.ellipsoid.y); // Y "ног" игрока
314
- const playerHeadY = Math.floor(camera.position.y + camera.ellipsoid.y*0.5); // Y "головы" игрока
315
- const playerX = Math.floor(camera.position.x);
316
- const playerZ = Math.floor(camera.position.z);
317
-
318
- if (targetX === playerX && targetZ === playerZ && (targetY === playerFeetY || targetY === playerHeadY)) {
319
- console.log("Cannot place block inside player");
320
- return; // Нельзя ставить блок туда, где стоит игрок
321
- }
322
-
323
-
324
- // Проверяем, нет ли уже блока в этом месте
325
- const existingBlock = scene.getMeshByName(`block_${currentBlockType}_${targetX}_${targetY}_${targetZ}`); // Проверяем по полному имени (может быть неточно)
326
- // Более надежная проверка - перебрать все меши или использовать пространственный хэш (сложнее)
327
- let occupied = false;
328
- scene.meshes.forEach(mesh => {
329
- if (mesh.name.startsWith("block_") &&
330
- Math.abs(mesh.position.x - (targetX + 0.5)) < 0.1 &&
331
- Math.abs(mesh.position.y - (targetY + 0.5)) < 0.1 &&
332
- Math.abs(mesh.position.z - (targetZ + 0.5)) < 0.1) {
333
- occupied = true;
334
- }
335
- });
336
-
337
- if (!occupied) {
338
- createBlock(currentBlockType, targetX, targetY, targetZ);
339
- } else {
340
- console.log("Position occupied");
341
- }
342
 
343
- } else {
344
- // --- Удаление блока ---
345
- // Не даем сломать самый нижний слой камня (условное "дно мира")
346
- if (pickedBlock.position.y > 0.51) { // Сравниваем с центром блока (0 + 0.5)
347
- pickedBlock.dispose();
348
- } else {
349
- console.log("Cannot break bedrock layer");
350
- }
351
- }
352
- }
353
- };
354
 
355
- // Обработчики для мыши (десктоп)
356
- scene.onPointerDown = (evt) => {
357
- // evt.button: 0 = левая, 1 = средняя, 2 = правая
358
- if (evt.button === 0) { // Левая кнопка - ломать
359
- performAction(false);
360
- } else if (evt.button === 2) { // Правая кнопка - ставить
361
- performAction(true);
362
  }
 
363
 
364
- // Захват указателя для удобного вращения камерой
365
- if (!engine.isPointerLock) {
366
- // engine.enterPointerlock(); // Не всегда хорошо работает на мобильных, осторожно
 
 
 
 
367
  }
368
- };
 
 
 
369
 
370
- // Выход из захвата указателя по Esc
371
- // document.addEventListener("pointerlockchange", () => {
372
- // if (document.pointerLockElement !== canvas) {
373
- // // Пользователь вышел из режима захвата
374
- // }
375
- // }, false);
376
 
377
- return scene;
378
- }; // --- Конец createScene ---
 
 
 
379
 
 
 
380
 
381
- // --- Создание и запуск сцены ---
382
- const scene = createScene();
383
 
384
- // --- Настройка мобильных контролов ---
385
- const joystickArea = document.getElementById('joystickArea');
386
- const joystick = document.getElementById('joystick');
387
- const jumpButton = document.getElementById('jumpButton');
388
- const breakButton = document.getElementById('breakButton');
389
- const placeButton = document.getElementById('placeButton');
390
- const blockSelectorDiv = document.getElementById('blockSelector');
391
-
392
- let joystickActive = false;
393
- let joystickStartX = 0;
394
- let joystickStartY = 0;
395
- const joystickMaxDist = joystickArea.offsetWidth / 2 - joystick.offsetWidth / 2; // Макс смещение ручки
396
-
397
- // --- Функции для мобильного управления ---
398
- const handleJoystickStart = (x, y) => {
399
- joystickActive = true;
400
- const rect = joystickArea.getBoundingClientRect();
401
- joystickStartX = rect.left + rect.width / 2;
402
- joystickStartY = rect.top + rect.height / 2;
403
- joystick.style.transition = 'none'; // Убрать плавность при перетаскивании
404
- };
405
 
406
- const handleJoystickMove = (x, y) => {
407
- if (!joystickActive) return;
408
 
409
- let deltaX = x - joystickStartX;
410
- let deltaY = y - joystickStartY;
411
- let distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
 
412
 
413
- // Ограничиваем максимальное смещение
414
- if (distance > joystickMaxDist) {
415
- deltaX = (deltaX / distance) * joystickMaxDist;
416
- deltaY = (deltaY / distance) * joystickMaxDist;
417
- distance = joystickMaxDist;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  }
419
 
420
- // Обновляем позицию ручки джойстика
421
- joystick.style.left = `calc(50% + ${deltaX}px)`;
422
- joystick.style.top = `calc(50% + ${deltaY}px)`;
423
 
424
- // Преобразуем смещение в векторы движения (нормализованные)
425
- moveRight = deltaX / joystickMaxDist;
426
- moveForward = -deltaY / joystickMaxDist; // Y инвертирован (вверх = вперед)
427
- };
 
428
 
429
- const handleJoystickEnd = () => {
430
- if (!joystickActive) return;
431
- joystickActive = false;
432
- moveForward = 0;
433
- moveRight = 0;
434
- // Возвращаем ручку в центр плавно
435
- joystick.style.transition = 'left 0.1s ease-out, top 0.1s ease-out';
436
- joystick.style.left = '50%';
437
- joystick.style.top = '50%';
438
- };
439
 
440
- // --- Навешивание событий для тачскрина и мыши (через Pointer Events) ---
441
- joystickArea.addEventListener('pointerdown', (e) => {
442
- e.preventDefault(); // Предотвратить скролл страницы
443
- e.target.setPointerCapture(e.pointerId); // Захватить указатель для этого элемента
444
- handleJoystickStart(e.clientX, e.clientY);
445
- });
 
 
 
 
 
 
 
 
446
 
447
- joystickArea.addEventListener('pointermove', (e) => {
448
- e.preventDefault();
449
- handleJoystickMove(e.clientX, e.clientY);
450
- });
451
 
452
- // События на окончание касания/клика
453
- joystickArea.addEventListener('pointerup', handleJoystickEnd);
454
- joystickArea.addEventListener('pointercancel', handleJoystickEnd); // Если палец ушел с экрана
455
- joystickArea.addEventListener('pointerleave', handleJoystickEnd); // Если указатель мыши ушел с элемента
456
-
457
-
458
- // Кнопки действий
459
- jumpButton.addEventListener('pointerdown', () => wantsToJump = true);
460
- // При отпускании кнопки прыжка можно сбросить флаг, но для одиночного прыжка это не нужно
461
- // jumpButton.addEventListener('pointerup', () => wantsToJump = false);
462
-
463
- breakButton.addEventListener('pointerdown', () => scene.onPointerDown({ button: 0 })); // Симулируем левый клик
464
- placeButton.addEventListener('pointerdown', () => scene.onPointerDown({ button: 2 })); // Симулируем правый клик
465
-
466
- // --- Выбор блока ---
467
- const availableBlocks = ['grass', 'dirt', 'stone', 'wood'];
468
- availableBlocks.forEach(type => {
469
- const option = document.createElement('div');
470
- option.classList.add('block-option');
471
- option.dataset.type = type;
472
- // Устанавливаем фон из текстуры (упрощенно, берем первую попавшуюся)
473
- let textureUrl = textureUrls[type] || textureUrls.grassTop || textureUrls.dirt; // Найти подходящую
474
- option.style.backgroundImage = `url(${textureUrl})`;
475
- if (type === currentBlockType) {
476
- option.classList.add('selected');
477
- }
478
 
479
- option.addEventListener('click', () => {
480
- currentBlockType = type;
481
- // Обновляем выделение
482
- document.querySelectorAll('.block-option').forEach(el => el.classList.remove('selected'));
483
- option.classList.add('selected');
484
- console.log("Selected block:", currentBlockType);
485
- });
486
- blockSelectorDiv.appendChild(option);
487
- });
488
 
489
 
490
- // --- Основной цикл рендеринга ---
491
- let playerGrounded = false; // Флаг, стоит ли игрок на земле
492
- const groundCheckDist = 0.1; // Дистанция для проверки земли под ногами
493
-
494
- engine.runRenderLoop(() => {
495
- const camera = scene.activeCamera;
496
- if (!camera) return;
497
-
498
- const deltaTime = engine.getDeltaTime() / 1000.0; // Время кадра в секундах
499
-
500
- // --- Проверка, стоит ли игрок на земле ---
501
- const rayStart = camera.position.clone();
502
- rayStart.y -= camera.ellipsoid.y; // Начало луча у "ног"
503
- const rayDir = new BABYLON.Vector3(0, -1, 0);
504
- const groundRay = new BABYLON.Ray(rayStart, rayDir, camera.ellipsoid.y + groundCheckDist);
505
- const groundPickInfo = scene.pickWithRay(groundRay, (mesh) => mesh.checkCollisions);
506
- playerGrounded = groundPickInfo.hit;
507
-
508
- // --- Прыжок ---
509
- if (wantsToJump && playerGrounded) {
510
- // Простой вариант: напрямую изменить Y-скорость (если бы была физика)
511
- // Или просто подкинуть вверх (может пройти сквозь потолок без физики)
512
- // camera.cameraDirection.y = 0.2; // Не работает с UniversalCamera напрямую
513
- // Используем импульс, если есть физический движок, или просто меняем позицию
514
- camera.position.y += 1.5; // Резкий подъем (величина пры��ка)
515
- wantsToJump = false; // Сбрасываем флаг после прыжка
516
- }
517
- // Сбрасываем флаг, если кнопка отпущена или игрок уже не на земле
518
- // (это нужно, если кнопка удерживается, для одиночного нажатия сброс выше)
519
- // if (!wantsToJump || !playerGrounded) { wantsToJump = false }
520
-
521
-
522
- // --- Перемещение с помощью джойстика/клавиатуры ---
523
- const effectiveSpeed = camera.speed * deltaTime * 60; // Нормализуем скорость к 60 FPS
524
-
525
- // Получаем векторы направления камеры (независимо от ее наклона вверх/вниз)
526
- const forward = new BABYLON.Vector3(
527
- Math.sin(camera.rotation.y),
528
- 0,
529
- Math.cos(camera.rotation.y)
530
- ).normalize();
531
- const right = new BABYLON.Vector3(
532
- Math.sin(camera.rotation.y + Math.PI / 2),
533
- 0,
534
- Math.cos(camera.rotation.y + Math.PI / 2)
535
- ).normalize();
536
-
537
- // Вычисляем вектор движения на основе ввода (джойстик или WASD)
538
- // Клавиатурный ввод обрабатывается UniversalCamera по умолчанию,
539
- // но мы можем его переопределить или дополнить джойстиком.
540
- let moveDirection = BABYLON.Vector3.Zero();
541
-
542
- // Используем ввод с джойстика, если он активен
543
- if (Math.abs(moveForward) > 0.1 || Math.abs(moveRight) > 0.1) {
544
- moveDirection = forward.scale(moveForward).add(right.scale(moveRight));
545
- } else {
546
- // Если джойстик неактивен, проверяем стандартный ввод камеры (WASD)
547
- // Это немного хак, зависит от внутренней реализации камеры
548
- const keyboardInput = camera.inputs.attached.keyboard; // Получаем доступ к клавиатурному вводу
549
- if (keyboardInput) {
550
- const x = keyboardInput.keysLeft.indexOf(keyboardInput.keysLeft[0]) !== -1 ? -1 : keyboardInput.keysRight.indexOf(keyboardInput.keysRight[0]) !== -1 ? 1 : 0;
551
- const z = keyboardInput.keysUp.indexOf(keyboardInput.keysUp[0]) !== -1 ? 1 : keyboardInput.keysDown.indexOf(keyboardInput.keysDown[0]) !== -1 ? -1 : 0;
552
- if (x !== 0 || z !== 0) {
553
- moveDirection = forward.scale(z).add(right.scale(x));
554
- }
555
  }
556
  }
 
 
 
557
 
 
 
558
 
559
- // Нормализуем и применяем скорость
560
- if (moveDirection.lengthSquared() > 0) {
561
- moveDirection.normalize().scaleInPlace(effectiveSpeed);
562
- // Двигаем камеру с учетом столкновений
563
- camera.moveWithCollisions(moveDirection);
564
- }
565
-
566
- // Небольшая коррекция, чтобы игрок не "тонул" в земле при столкновениях
567
- if (playerGrounded && camera.position.y < (groundPickInfo.pickedPoint.y + camera.ellipsoid.y)) {
568
- //camera.position.y = groundPickInfo.pickedPoint.y + camera.ellipsoid.y;
569
- }
570
 
 
 
571
 
572
- scene.render();
 
 
 
573
  });
574
 
575
- // --- Обработка изменения размера окна ---
576
- window.addEventListener('resize', () => {
577
  engine.resize();
578
- // Пересчитать joystickMaxDist при ресайзе
579
- // joystickMaxDist = joystickArea.offsetWidth / 2 - joystick.offsetWidth / 2;
580
  });
581
 
582
  </script>
 
2
  <html lang="ru">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>BabylonCraft Demo</title>
 
 
 
7
  <style>
8
+ /* Базовые стили */
9
  html, body {
10
+ overflow: hidden;
11
  width: 100%;
12
  height: 100%;
13
  margin: 0;
14
  padding: 0;
 
 
15
  font-family: sans-serif;
16
+ color: white;
17
+ background-color: #333; /* Фон для областей вне канваса */
18
  }
19
+
20
  #renderCanvas {
21
  width: 100%;
22
  height: 100%;
23
+ touch-action: none; /* Отключаем стандартные действия браузера при касании (скролл, зум) */
24
+ outline: none; /* Убираем рамку фокуса */
25
+ }
26
+
27
+ /* Стили для оверлея с информацией и управлением */
28
+ #infoOverlay {
29
+ position: absolute;
30
+ top: 10px;
31
+ left: 10px;
32
+ background-color: rgba(0, 0, 0, 0.5);
33
+ padding: 10px;
34
+ border-radius: 5px;
35
+ font-size: 12px;
36
+ max-width: calc(100% - 40px); /* Чтобы не вылезал на мобилках */
37
  }
38
+
39
+ /* Крестик в центре экрана */
40
  #crosshair {
41
  position: absolute;
42
  top: 50%;
 
44
  width: 10px;
45
  height: 10px;
46
  border: 1px solid white;
 
47
  transform: translate(-50%, -50%);
48
+ pointer-events: none; /* Чтобы не мешал кликам */
49
+ mix-blend-mode: difference; /* Чтобы был виден на любом фоне */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
+
52
+ /* Простые кнопки для мобильных действий */
53
+ #mobileControls {
 
 
54
  position: absolute;
55
+ bottom: 20px;
56
+ right: 20px;
57
+ display: none; /* По умолчанию скрыты, показываем через JS */
58
+ flex-direction: column;
 
 
 
59
  gap: 15px;
60
+ }
61
+
62
+ .mobile-button {
63
+ background-color: rgba(255, 255, 255, 0.3);
64
+ border: 1px solid rgba(255, 255, 255, 0.5);
65
+ color: white;
66
  width: 60px;
67
  height: 60px;
68
+ border-radius: 50%;
69
+ font-size: 18px;
 
 
70
  font-weight: bold;
71
  display: flex;
72
  justify-content: center;
73
  align-items: center;
 
74
  cursor: pointer;
75
  user-select: none; /* Запретить выделение текста на кнопке */
76
  }
77
+ .mobile-button:active {
78
+ background-color: rgba(255, 255, 255, 0.5);
79
+ }
80
+
81
+ /* Медиа-запрос для показа мобильных кнопок на тач-устройствах */
82
+ @media (hover: none) and (pointer: coarse) {
83
+ #mobileControls {
84
+ display: flex;
85
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  }
87
+
88
  </style>
89
+ <!-- Подключаем Babylon.js с CDN -->
90
+ <script src="https://cdn.babylonjs.com/babylon.js"></script>
91
+ <!-- (Опционально) PEP для поддержки Pointer Events в старых браузерах -->
92
+ <script src="https://code.jquery.com/pep/0.4.3/pep.js"></script>
93
  </head>
94
  <body>
95
+ <canvas id="renderCanvas" touch-action="none"></canvas>
96
+ <div id="infoOverlay">
97
+ Управление:<br>
98
+ - WASD: Движение<br>
99
+ - Мышь: Осмотреться<br>
100
+ - Левый клик / Кнопка "B": Сломать блок<br>
101
+ - Правый клик / Кнопка "P": Поставить блок (земля)<br>
102
+ - Пробел: Вверх<br>
103
+ - Shift: Вниз<br>
104
+ <br>
105
+ <span id="fpsCounter">FPS: </span><br>
106
+ <span id="blockInfo">Наведите курсор на блок</span>
 
107
  </div>
108
+ <div id="crosshair">+</div>
109
 
110
+ <!-- Кнопки для мобильных -->
111
+ <div id="mobileControls">
112
+ <div id="placeButton" class="mobile-button">P</div>
113
+ <div id="breakButton" class="mobile-button">B</div>
114
+ </div>
115
 
116
  <script>
117
  const canvas = document.getElementById('renderCanvas');
118
+ const engine = new BABYLON.Engine(canvas, true, { stencil: true, preserveDrawingBuffer: true }, true); // Enable stencil/preserveDrawingBuffer if needed later
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ // --- Переменные и константы ---
121
+ const BLOCK_SIZE = 1;
122
+ const PLAYER_HEIGHT = 1.8;
123
+ const GRAVITY = -9.81; // Условная гравитация
124
+ const MOVE_SPEED = 0.15;
125
+ const MOUSE_SENSITIVITY = 0.002;
126
+ const TOUCH_SENSITIVITY = 0.004; // Чувствительность для тач-управления камерой
127
 
128
+ let currentBlockType = 'grass'; // Тип блока для установки
129
+ let blocks = {}; // Объект для хранения координат существующих блоков { "x_y_z": mesh }
130
+
131
+ // --- Создание сцены ---
132
+ const createScene = function () {
133
  const scene = new BABYLON.Scene(engine);
134
+ scene.clearColor = new BABYLON.Color3(0.5, 0.8, 1.0); // Цвет неба
135
+ scene.collisionsEnabled = true; // Включаем проверку столкновений на уровне сцены
136
+ scene.gravity = new BABYLON.Vector3(0, GRAVITY / 60, 0); // Применяем гравитацию (делим на ~FPS)
137
+
138
+ // --- Камера ---
139
+ // FreeCamera подходит для управления в стиле Minecraft
140
+ const camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(5, PLAYER_HEIGHT + 5, 5), scene);
141
+ camera.setTarget(BABYLON.Vector3.Zero());
142
+ camera.attachControl(canvas, true);
143
+
144
+ // Настройки управления камерой
145
+ camera.speed = MOVE_SPEED; // Скорость движения WASD
146
+ camera.inertia = 0.9; // Плавность остановки
147
+ camera.angularSensibility = 1 / MOUSE_SENSITIVITY; // Инвертируем, т.к. в Babylon это делитель
148
+ camera.keysUp = [87]; // W
149
+ camera.keysDown = [83]; // S
150
+ camera.keysLeft = [65]; // A
151
+ camera.keysRight = [68]; // D
152
+ camera.keysUpward = [32]; // Space
153
+ camera.keysDownward = [16]; // Shift
154
+
155
+ // Включаем физику для камеры (чтобы она падала и сталкивалась)
156
  camera.checkCollisions = true;
157
  camera.applyGravity = true;
158
+ // Задаем "тело" для камеры для столкновений (эллипсоид)
159
+ camera.ellipsoid = new BABYLON.Vector3(0.4, PLAYER_HEIGHT / 2, 0.4);
160
+ camera.ellipsoidOffset = new BABYLON.Vector3(0, PLAYER_HEIGHT / 2, 0); // Смещаем центр эллипсоида
 
 
 
 
 
161
 
162
  // --- Освещение ---
163
  const light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
164
  light.intensity = 0.8;
165
 
166
+ // --- Материалы для блоков ---
167
+ const materials = {
168
+ grass: new BABYLON.StandardMaterial("grassMat", scene),
169
+ dirt: new BABYLON.StandardMaterial("dirtMat", scene),
170
+ stone: new BABYLON.StandardMaterial("stoneMat", scene),
171
+ wood: new BABYLON.StandardMaterial("woodMat", scene),
172
+ leaves: new BABYLON.StandardMaterial("leavesMat", scene),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  };
174
+ materials.grass.diffuseColor = new BABYLON.Color3(0.2, 0.8, 0.2); // Зеленый
175
+ materials.dirt.diffuseColor = new BABYLON.Color3(0.6, 0.4, 0.2); // Коричневый
176
+ materials.stone.diffuseColor = new BABYLON.Color3(0.5, 0.5, 0.5); // Серый
177
+ materials.wood.diffuseColor = new BABYLON.Color3(0.7, 0.5, 0.3); // Дерево
178
+ materials.leaves.diffuseColor = new BABYLON.Color3(0.1, 0.6, 0.1); // Листва
179
+ materials.leaves.alpha = 0.9; // Немного прозрачности для листвы (не идеально)
180
+
181
+ // --- Генерация начального мира ---
182
+ const worldSize = 16; // Размер плоского мира
183
+ const groundLevel = 0;
184
+
185
+ for (let x = -worldSize / 2; x < worldSize / 2; x++) {
186
+ for (let z = -worldSize / 2; z < worldSize / 2; z++) {
187
+ // Верхний слой - трава
188
+ createBlock(x, groundLevel, z, 'grass', scene, materials);
189
+ // Несколько слоев земли под травой
190
+ createBlock(x, groundLevel - 1, z, 'dirt', scene, materials);
191
+ createBlock(x, groundLevel - 2, z, 'dirt', scene, materials);
192
+ // Нижний слой - камень
193
+ createBlock(x, groundLevel - 3, z, 'stone', scene, materials);
194
  }
195
  }
 
 
 
196
 
197
+ // Добавим простое "дерево" для примера
198
+ createBlock(5, groundLevel + 1, -5, 'wood', scene, materials);
199
+ createBlock(5, groundLevel + 2, -5, 'wood', scene, materials);
200
+ createBlock(5, groundLevel + 3, -5, 'wood', scene, materials);
201
+ // Листва
202
+ for(let lx = 4; lx <= 6; lx++) {
203
+ for(let ly = 4; ly <= 5; ly++) {
204
+ for(let lz = -6; lz <= -4; lz++) {
205
+ if(lx === 5 && lz === -5 && ly <= 3) continue; // Не ставим листву внутри ствола
206
+ createBlock(lx, groundLevel + ly, lz, 'leaves', scene, materials);
207
+ }
208
+ }
209
+ }
210
 
 
 
 
211
 
212
+ // --- Функция создания блока ---
213
+ function createBlock(x, y, z, type, scene, materials) {
214
+ const blockId = `${x}_${y}_${z}`;
215
+ if (blocks[blockId]) return; // Не создаем блок, если он уже есть
216
+
217
+ const box = BABYLON.MeshBuilder.CreateBox("box_" + blockId, { size: BLOCK_SIZE }, scene);
218
+ box.position = new BABYLON.Vector3(x + BLOCK_SIZE / 2, y + BLOCK_SIZE / 2, z + BLOCK_SIZE / 2);
219
+ box.material = materials[type] || materials.stone; // Используем камень, если тип не найден
220
+ box.checkCollisions = true; // Включаем столкновения для блока
221
+ box.isPickable = true; // Разрешаем блоку быть "выбираемым" лучом
222
+ box.metadata = { type: type, x: x, y: y, z: z }; // Сохраняем тип и координаты
223
+
224
+ blocks[blockId] = box; // Добавляем в наш учет блоков
225
+ return box;
226
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ // --- Логика взаимодействия (ломать/ставить блоки) ---
229
+ let pointerLocked = false;
 
 
 
 
 
 
 
 
 
230
 
231
+ // Захват/освобождение мыши для удобного управления камерой на ПК
232
+ canvas.addEventListener("click", () => {
233
+ if (!pointerLocked && !isTouchDevice()) { // Не захватывать на тач-устройствах
234
+ canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
235
+ if (canvas.requestPointerLock) {
236
+ canvas.requestPointerLock();
237
+ }
238
  }
239
+ });
240
 
241
+ const lockChangeAlert = () => {
242
+ if (document.pointerLockElement === canvas || document.mozPointerLockElement === canvas || document.webkitPointerLockElement === canvas) {
243
+ console.log('Pointer locked');
244
+ pointerLocked = true;
245
+ } else {
246
+ console.log('Pointer unlocked');
247
+ pointerLocked = false;
248
  }
249
+ }
250
+ document.addEventListener('pointerlockchange', lockChangeAlert, false);
251
+ document.addEventListener('mozpointerlockchange', lockChangeAlert, false);
252
+ document.addEventListener('webkitpointerlockchange', lockChangeAlert, false);
253
 
 
 
 
 
 
 
254
 
255
+ // Обработка кликов/касаний для ломания/установки
256
+ scene.onPointerDown = function (evt, pickResult) {
257
+ // evt.button === 0: Левая кнопка мыши / Основное касание
258
+ // evt.button === 1: Средняя кнопка мыши
259
+ // evt.button === 2: Правая кнопка мыши
260
 
261
+ const isBreakAction = (evt.button === 0);
262
+ const isPlaceAction = (evt.button === 2);
263
 
264
+ handleInteraction(isBreakAction, isPlaceAction, pickResult);
265
+ };
266
 
267
+ // Обработка нажатий мобильных кнопок
268
+ const placeButton = document.getElementById('placeButton');
269
+ const breakButton = document.getElementById('breakButton');
270
+
271
+ breakButton.addEventListener('pointerdown', (e) => {
272
+ e.preventDefault(); // Предотвращаем стандартное поведение (например, двойной тап зум)
273
+ const pickResult = scene.pick(scene.pointerX, scene.pointerY, (mesh) => mesh.isPickable);
274
+ handleInteraction(true, false, pickResult); // Имитируем "ломание"
275
+ });
276
+ placeButton.addEventListener('pointerdown', (e) => {
277
+ e.preventDefault();
278
+ const pickResult = scene.pick(scene.pointerX, scene.pointerY, (mesh) => mesh.isPickable);
279
+ handleInteraction(false, true, pickResult); // Имитируем "установку"
280
+ });
 
 
 
 
 
 
 
281
 
 
 
282
 
283
+ function handleInteraction(isBreak, isPlace, pickResult) {
284
+ if (pickResult.hit && pickResult.pickedMesh && blocks[pickResult.pickedMesh.name.replace('box_','')]) {
285
+ const pickedMesh = pickResult.pickedMesh;
286
+ const blockId = pickedMesh.name.replace('box_','');
287
 
288
+ // Ломание блока
289
+ if (isBreak) {
290
+ console.log("Breaking block:", blockId);
291
+ delete blocks[blockId]; // Удаляем из учета
292
+ pickedMesh.dispose(); // Удаляем со сцены
293
+ }
294
+ // Установка блока
295
+ else if (isPlace) {
296
+ // Определяем нормаль грани, на которую попал луч
297
+ const normal = pickResult.getNormal(true); // true = использовать локальные координаты нормали
298
+ const position = pickedMesh.position.clone(); // Позиция блока, на который кликнули
299
+
300
+ // Вычисляем координаты нового блока, примыкающего к грани
301
+ const newBlockPos = position.add(normal.scale(BLOCK_SIZE));
302
+
303
+ // Округляем координаты до ближайшей сетки (важно!)
304
+ // Учитываем смещение центра блока (+BLOCK_SIZE/2)
305
+ const newX = Math.floor(newBlockPos.x);
306
+ const newY = Math.floor(newBlockPos.y);
307
+ const newZ = Math.floor(newBlockPos.z);
308
+ const newBlockId = `${newX}_${newY}_${newZ}`;
309
+
310
+ // Проверяем, не пытаемся ли поставить блок на место игрока
311
+ const playerHeadY = Math.floor(camera.position.y + camera.ellipsoidOffset.y + camera.ellipsoid.y / 2);
312
+ const playerFeetY = Math.floor(camera.position.y + camera.ellipsoidOffset.y - camera.ellipsoid.y / 2);
313
+ const playerX = Math.floor(camera.position.x);
314
+ const playerZ = Math.floor(camera.position.z);
315
+
316
+ const collisionWithPlayer = (newX === playerX && newZ === playerZ && (newY === playerFeetY || newY === playerHeadY));
317
+
318
+ if (!blocks[newBlockId] && !collisionWithPlayer) { // Не ставим, если уже есть блок или там игрок
319
+ console.log("Placing block at:", newX, newY, newZ);
320
+ createBlock(newX, newY, newZ, currentBlockType, scene, materials);
321
+ } else {
322
+ console.log("Cannot place block here:", newBlockId, "Exists?", !!blocks[newBlockId], "Collision?", collisionWithPlayer);
323
+ }
324
+ }
325
+ }
326
  }
327
 
 
 
 
328
 
329
+ // --- Обновление информации на экране ---
330
+ const fpsCounter = document.getElementById('fpsCounter');
331
+ const blockInfo = document.getElementById('blockInfo');
332
+ scene.onBeforeRenderObservable.add(() => {
333
+ fpsCounter.textContent = `FPS: ${engine.getFps().toFixed()}`;
334
 
335
+ // Показываем информацию о блоке под курсором
336
+ const pickResult = scene.pick(canvas.width / 2, canvas.height / 2, (mesh) => mesh.isPickable);
337
+ if (pickResult.hit && pickResult.pickedMesh && pickResult.pickedMesh.metadata) {
338
+ const meta = pickResult.pickedMesh.metadata;
339
+ blockInfo.textContent = `Блок: ${meta.type} (${meta.x}, ${meta.y}, ${meta.z})`;
340
+ } else {
341
+ blockInfo.textContent = 'Наведите курсор на блок';
342
+ }
343
+ });
 
344
 
345
+ // --- Простое тач управление камерой (вращение) ---
346
+ let lastTouchX = null;
347
+ let lastTouchY = null;
348
+ let isTouching = false;
349
+
350
+ canvas.addEventListener('pointerdown', (evt) => {
351
+ if(evt.pointerType === 'touch') {
352
+ isTouching = true;
353
+ lastTouchX = evt.clientX;
354
+ lastTouchY = evt.clientY;
355
+ // Важно: Отключаем стандартное управление мышью на время тача, чтобы избежать конфликтов
356
+ // camera.detachControl(); // Это может быть слишком резко, нужно более тонкое управление
357
+ }
358
+ });
359
 
360
+ canvas.addEventListener('pointermove', (evt) => {
361
+ if(isTouching && evt.pointerType === 'touch') {
362
+ const deltaX = evt.clientX - lastTouchX;
363
+ const deltaY = evt.clientY - lastTouchY;
364
 
365
+ // Вращаем камеру вручную
366
+ // Коэффициенты подбираются экспериментально
367
+ camera.cameraRotation.y += deltaX * TOUCH_SENSITIVITY;
368
+ camera.cameraRotation.x += deltaY * TOUCH_SENSITIVITY;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ // Ограничиваем вертикальное вращение (чтобы не смотреть вверх ногами)
371
+ camera.cameraRotation.x = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, camera.cameraRotation.x));
 
 
 
 
 
 
 
372
 
373
 
374
+ lastTouchX = evt.clientX;
375
+ lastTouchY = evt.clientY;
376
+ }
377
+ });
378
+
379
+ const touchEndHandler = (evt) => {
380
+ if(evt.pointerType === 'touch') {
381
+ isTouching = false;
382
+ lastTouchX = null;
383
+ lastTouchY = null;
384
+ // Возвращаем стандартное управление, если оно было отключено
385
+ // if(!camera._attachedCanvas) {
386
+ // camera.attachControl(canvas, true);
387
+ // }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  }
389
  }
390
+ canvas.addEventListener('pointerup', touchEndHandler);
391
+ canvas.addEventListener('pointerout', touchEndHandler); // Также сбрасываем при выходе за пределы канваса
392
+ canvas.addEventListener('pointercancel', touchEndHandler); // И при отмене касания
393
 
394
+ return scene;
395
+ };
396
 
397
+ // --- Вспомогательная функция для определения тач-устройства ---
398
+ function isTouchDevice() {
399
+ return (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0));
400
+ }
 
 
 
 
 
 
 
401
 
402
+ // --- Запуск ---
403
+ const scene = createScene();
404
 
405
+ engine.runRenderLoop(function () {
406
+ if (scene) {
407
+ scene.render();
408
+ }
409
  });
410
 
411
+ window.addEventListener('resize', function () {
 
412
  engine.resize();
 
 
413
  });
414
 
415
  </script>