406 lines
17 KiB
HTML
406 lines
17 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="ru">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Тест системы коллизий интерьеров</title>
|
|||
|
|
<style>
|
|||
|
|
body {
|
|||
|
|
margin: 0;
|
|||
|
|
padding: 0;
|
|||
|
|
background: #000;
|
|||
|
|
color: #fff;
|
|||
|
|
font-family: Arial, sans-serif;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#gameContainer {
|
|||
|
|
width: 100vw;
|
|||
|
|
height: 100vh;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#debugInfo {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 10px;
|
|||
|
|
left: 10px;
|
|||
|
|
background: rgba(0, 0, 0, 0.7);
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
z-index: 1000;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#instructions {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 10px;
|
|||
|
|
right: 10px;
|
|||
|
|
background: rgba(0, 0, 0, 0.7);
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
font-size: 12px;
|
|||
|
|
z-index: 1000;
|
|||
|
|
max-width: 300px;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="gameContainer"></div>
|
|||
|
|
|
|||
|
|
<div id="debugInfo">
|
|||
|
|
<div>Статус: <span id="status">Загрузка...</span></div>
|
|||
|
|
<div>Коллайдеры: <span id="colliders">0</span></div>
|
|||
|
|
<div>Позиция игрока: <span id="position">0, 0, 0</span></div>
|
|||
|
|
<div>В интерьере: <span id="inInterior">false</span></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div id="instructions">
|
|||
|
|
<h3>Управление:</h3>
|
|||
|
|
<p><strong>WASD</strong> - движение</p>
|
|||
|
|
<p><strong>Мышь</strong> - поворот камеры (в интерьере)</p>
|
|||
|
|
<p><strong>Клик по объекту</strong> - вход в интерьер</p>
|
|||
|
|
<p><strong>Escape</strong> - выход из интерьера</p>
|
|||
|
|
<br>
|
|||
|
|
<p><strong>Тест коллизий:</strong></p>
|
|||
|
|
<p>В интерьере игрок не должен проходить сквозь стены и объекты</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script type="module">
|
|||
|
|
import * as THREE from 'https://unpkg.com/three@0.158.0/build/three.module.js';
|
|||
|
|
|
|||
|
|
// Простая система коллизий для тестирования
|
|||
|
|
class SimpleCollisionTest {
|
|||
|
|
constructor() {
|
|||
|
|
this.scene = new THREE.Scene();
|
|||
|
|
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|||
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
|||
|
|
this.player = null;
|
|||
|
|
this.isInInterior = false;
|
|||
|
|
this.interiorColliders = [];
|
|||
|
|
this.moveInput = { forward: false, backward: false, left: false, right: false };
|
|||
|
|
|
|||
|
|
this.init();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
init() {
|
|||
|
|
// Настройка рендерера
|
|||
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|||
|
|
this.renderer.setClearColor(0x87CEEB);
|
|||
|
|
this.renderer.shadowMap.enabled = true;
|
|||
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|||
|
|
|
|||
|
|
document.getElementById('gameContainer').appendChild(this.renderer.domElement);
|
|||
|
|
|
|||
|
|
// Создаем простую сцену
|
|||
|
|
this.createScene();
|
|||
|
|
|
|||
|
|
// Создаем игрока
|
|||
|
|
this.createPlayer();
|
|||
|
|
|
|||
|
|
// Настраиваем обработчики событий
|
|||
|
|
this.setupEventListeners();
|
|||
|
|
|
|||
|
|
// Запускаем игровой цикл
|
|||
|
|
this.animate();
|
|||
|
|
|
|||
|
|
this.updateDebugInfo();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createScene() {
|
|||
|
|
// Освещение
|
|||
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|||
|
|
this.scene.add(ambientLight);
|
|||
|
|
|
|||
|
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|||
|
|
directionalLight.position.set(10, 10, 5);
|
|||
|
|
directionalLight.castShadow = true;
|
|||
|
|
this.scene.add(directionalLight);
|
|||
|
|
|
|||
|
|
// Создаем простой интерьер для тестирования
|
|||
|
|
this.createTestInterior();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createTestInterior() {
|
|||
|
|
const interiorGroup = new THREE.Group();
|
|||
|
|
|
|||
|
|
// Стены
|
|||
|
|
const wallGeometry = new THREE.BoxGeometry(0.2, 3, 10);
|
|||
|
|
const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
|
|||
|
|
|
|||
|
|
// Левая стена
|
|||
|
|
const leftWall = new THREE.Mesh(wallGeometry, wallMaterial);
|
|||
|
|
leftWall.position.set(-5, 1.5, 0);
|
|||
|
|
leftWall.castShadow = true;
|
|||
|
|
interiorGroup.add(leftWall);
|
|||
|
|
|
|||
|
|
// Правая стена
|
|||
|
|
const rightWall = new THREE.Mesh(wallGeometry, wallMaterial);
|
|||
|
|
rightWall.position.set(5, 1.5, 0);
|
|||
|
|
rightWall.castShadow = true;
|
|||
|
|
interiorGroup.add(rightWall);
|
|||
|
|
|
|||
|
|
// Задняя стена
|
|||
|
|
const backWallGeometry = new THREE.BoxGeometry(10, 3, 0.2);
|
|||
|
|
const backWall = new THREE.Mesh(backWallGeometry, wallMaterial);
|
|||
|
|
backWall.position.set(0, 1.5, -5);
|
|||
|
|
backWall.castShadow = true;
|
|||
|
|
interiorGroup.add(backWall);
|
|||
|
|
|
|||
|
|
// Передняя стена (с проходом)
|
|||
|
|
const frontWall1 = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 0.2), wallMaterial);
|
|||
|
|
frontWall1.position.set(-3, 1.5, 5);
|
|||
|
|
frontWall1.castShadow = true;
|
|||
|
|
interiorGroup.add(frontWall1);
|
|||
|
|
|
|||
|
|
const frontWall2 = new THREE.Mesh(new THREE.BoxGeometry(4, 3, 0.2), wallMaterial);
|
|||
|
|
frontWall2.position.set(3, 1.5, 5);
|
|||
|
|
frontWall2.castShadow = true;
|
|||
|
|
interiorGroup.add(frontWall2);
|
|||
|
|
|
|||
|
|
// Пол
|
|||
|
|
const floorGeometry = new THREE.BoxGeometry(10, 0.1, 10);
|
|||
|
|
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x654321 });
|
|||
|
|
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
|||
|
|
floor.position.set(0, 0, 0);
|
|||
|
|
floor.receiveShadow = true;
|
|||
|
|
interiorGroup.add(floor);
|
|||
|
|
|
|||
|
|
// Потолок
|
|||
|
|
const ceiling = new THREE.Mesh(floorGeometry, new THREE.MeshLambertMaterial({ color: 0xFFFFFF }));
|
|||
|
|
ceiling.position.set(0, 3, 0);
|
|||
|
|
interiorGroup.add(ceiling);
|
|||
|
|
|
|||
|
|
// Объекты в интерьере
|
|||
|
|
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|||
|
|
const boxMaterial = new THREE.MeshLambertMaterial({ color: 0xFF0000 });
|
|||
|
|
|
|||
|
|
const box1 = new THREE.Mesh(boxGeometry, boxMaterial);
|
|||
|
|
box1.position.set(-2, 0.5, -2);
|
|||
|
|
box1.castShadow = true;
|
|||
|
|
interiorGroup.add(box1);
|
|||
|
|
|
|||
|
|
const box2 = new THREE.Mesh(boxGeometry, boxMaterial);
|
|||
|
|
box2.position.set(2, 0.5, 2);
|
|||
|
|
box2.castShadow = true;
|
|||
|
|
interiorGroup.add(box2);
|
|||
|
|
|
|||
|
|
// Собираем коллайдеры
|
|||
|
|
this.interiorColliders = [];
|
|||
|
|
interiorGroup.traverse((child) => {
|
|||
|
|
if (child.isMesh && child.geometry) {
|
|||
|
|
this.interiorColliders.push(child);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.scene.add(interiorGroup);
|
|||
|
|
this.interiorGroup = interiorGroup;
|
|||
|
|
|
|||
|
|
console.log('Создано коллайдеров:', this.interiorColliders.length);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
createPlayer() {
|
|||
|
|
const playerGeometry = new THREE.BoxGeometry(0.6, 1.6, 0.6);
|
|||
|
|
const playerMaterial = new THREE.MeshLambertMaterial({ color: 0x0000FF });
|
|||
|
|
this.player = new THREE.Mesh(playerGeometry, playerMaterial);
|
|||
|
|
this.player.position.set(0, 0.8, 0);
|
|||
|
|
this.scene.add(this.player);
|
|||
|
|
|
|||
|
|
// Устанавливаем камеру на уровне глаз игрока
|
|||
|
|
this.camera.position.set(0, 1.6, 0);
|
|||
|
|
this.camera.lookAt(0, 1.6, -1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setupEventListeners() {
|
|||
|
|
// Клавиатура
|
|||
|
|
document.addEventListener('keydown', (event) => {
|
|||
|
|
switch(event.code) {
|
|||
|
|
case 'KeyW':
|
|||
|
|
this.moveInput.forward = true;
|
|||
|
|
break;
|
|||
|
|
case 'KeyS':
|
|||
|
|
this.moveInput.backward = true;
|
|||
|
|
break;
|
|||
|
|
case 'KeyA':
|
|||
|
|
this.moveInput.left = true;
|
|||
|
|
break;
|
|||
|
|
case 'KeyD':
|
|||
|
|
this.moveInput.right = true;
|
|||
|
|
break;
|
|||
|
|
case 'Escape':
|
|||
|
|
this.exitInterior();
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
document.addEventListener('keyup', (event) => {
|
|||
|
|
switch(event.code) {
|
|||
|
|
case 'KeyW':
|
|||
|
|
this.moveInput.forward = false;
|
|||
|
|
break;
|
|||
|
|
case 'KeyS':
|
|||
|
|
this.moveInput.backward = false;
|
|||
|
|
break;
|
|||
|
|
case 'KeyA':
|
|||
|
|
this.moveInput.left = false;
|
|||
|
|
break;
|
|||
|
|
case 'KeyD':
|
|||
|
|
this.moveInput.right = false;
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Мышь для поворота камеры в интерьере
|
|||
|
|
document.addEventListener('mousemove', (event) => {
|
|||
|
|
if (this.isInInterior) {
|
|||
|
|
this.camera.rotation.y -= event.movementX * 0.002;
|
|||
|
|
this.camera.rotation.x -= event.movementY * 0.002;
|
|||
|
|
this.camera.rotation.x = Math.max(-Math.PI/2, Math.min(Math.PI/2, this.camera.rotation.x));
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Клик для входа в интерьер
|
|||
|
|
this.renderer.domElement.addEventListener('click', () => {
|
|||
|
|
if (!this.isInInterior) {
|
|||
|
|
this.enterInterior();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Изменение размера окна
|
|||
|
|
window.addEventListener('resize', () => {
|
|||
|
|
this.camera.aspect = window.innerWidth / window.innerHeight;
|
|||
|
|
this.camera.updateProjectionMatrix();
|
|||
|
|
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enterInterior() {
|
|||
|
|
console.log('Вход в интерьер');
|
|||
|
|
this.isInInterior = true;
|
|||
|
|
this.player.visible = false;
|
|||
|
|
|
|||
|
|
// Запрашиваем pointer lock
|
|||
|
|
this.renderer.domElement.requestPointerLock();
|
|||
|
|
|
|||
|
|
this.updateDebugInfo();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
exitInterior() {
|
|||
|
|
console.log('Выход из интерьера');
|
|||
|
|
this.isInInterior = false;
|
|||
|
|
this.player.visible = true;
|
|||
|
|
|
|||
|
|
// Выходим из pointer lock
|
|||
|
|
document.exitPointerLock();
|
|||
|
|
|
|||
|
|
this.updateDebugInfo();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
checkCollision(testPosition) {
|
|||
|
|
const playerRadius = 0.3;
|
|||
|
|
const playerHeight = 1.6;
|
|||
|
|
|
|||
|
|
// Создаем AABB для игрока
|
|||
|
|
const playerBox = new THREE.Box3();
|
|||
|
|
const playerMin = new THREE.Vector3(
|
|||
|
|
testPosition.x - playerRadius,
|
|||
|
|
testPosition.y,
|
|||
|
|
testPosition.z - playerRadius
|
|||
|
|
);
|
|||
|
|
const playerMax = new THREE.Vector3(
|
|||
|
|
testPosition.x + playerRadius,
|
|||
|
|
testPosition.y + playerHeight,
|
|||
|
|
testPosition.z + playerRadius
|
|||
|
|
);
|
|||
|
|
playerBox.setFromPoints([playerMin, playerMax]);
|
|||
|
|
|
|||
|
|
// Проверяем столкновения с коллайдерами
|
|||
|
|
for (const collider of this.interiorColliders) {
|
|||
|
|
if (!collider.geometry || !collider.visible) continue;
|
|||
|
|
|
|||
|
|
collider.updateMatrixWorld(true);
|
|||
|
|
const colliderBox = new THREE.Box3();
|
|||
|
|
colliderBox.setFromObject(collider);
|
|||
|
|
|
|||
|
|
if (playerBox.intersectsBox(colliderBox)) {
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updatePlayer(deltaTime) {
|
|||
|
|
if (!this.isInInterior) return;
|
|||
|
|
|
|||
|
|
const speed = 3.0;
|
|||
|
|
const moveDistance = speed * deltaTime;
|
|||
|
|
|
|||
|
|
// Используем простые направления вместо кватернионов
|
|||
|
|
const forward = new THREE.Vector3(0, 0, -1);
|
|||
|
|
const right = new THREE.Vector3(1, 0, 0);
|
|||
|
|
|
|||
|
|
// Поворачиваем направления в соответствии с поворотом камеры
|
|||
|
|
forward.applyEuler(new THREE.Euler(0, this.camera.rotation.y, 0));
|
|||
|
|
right.applyEuler(new THREE.Euler(0, this.camera.rotation.y, 0));
|
|||
|
|
|
|||
|
|
let moveVector = new THREE.Vector3();
|
|||
|
|
|
|||
|
|
if (this.moveInput.forward) moveVector.add(forward);
|
|||
|
|
if (this.moveInput.backward) moveVector.add(forward.clone().multiplyScalar(-1));
|
|||
|
|
if (this.moveInput.left) moveVector.add(right.clone().multiplyScalar(-1));
|
|||
|
|
if (this.moveInput.right) moveVector.add(right);
|
|||
|
|
|
|||
|
|
if (moveVector.length() > 0) {
|
|||
|
|
moveVector.normalize().multiplyScalar(moveDistance);
|
|||
|
|
|
|||
|
|
// Проверяем коллизии по осям отдельно
|
|||
|
|
let safePosition = this.camera.position.clone();
|
|||
|
|
|
|||
|
|
// Проверяем движение по X
|
|||
|
|
if (Math.abs(moveVector.x) > 0.001) {
|
|||
|
|
const xTestPosition = safePosition.clone();
|
|||
|
|
xTestPosition.x += moveVector.x;
|
|||
|
|
if (!this.checkCollision(xTestPosition)) {
|
|||
|
|
safePosition.x = xTestPosition.x;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем движение по Z
|
|||
|
|
if (Math.abs(moveVector.z) > 0.001) {
|
|||
|
|
const zTestPosition = safePosition.clone();
|
|||
|
|
zTestPosition.z += moveVector.z;
|
|||
|
|
if (!this.checkCollision(zTestPosition)) {
|
|||
|
|
safePosition.z = zTestPosition.z;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this.camera.position.copy(safePosition);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
updateDebugInfo() {
|
|||
|
|
document.getElementById('status').textContent = this.isInInterior ? 'В интерьере' : 'Вне интерьера';
|
|||
|
|
document.getElementById('colliders').textContent = this.interiorColliders.length;
|
|||
|
|
document.getElementById('position').textContent =
|
|||
|
|
`${this.camera.position.x.toFixed(2)}, ${this.camera.position.y.toFixed(2)}, ${this.camera.position.z.toFixed(2)}`;
|
|||
|
|
document.getElementById('inInterior').textContent = this.isInInterior;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
animate() {
|
|||
|
|
requestAnimationFrame(() => this.animate());
|
|||
|
|
|
|||
|
|
const deltaTime = 0.016; // Примерно 60 FPS
|
|||
|
|
|
|||
|
|
this.updatePlayer(deltaTime);
|
|||
|
|
this.updateDebugInfo();
|
|||
|
|
|
|||
|
|
this.renderer.render(this.scene, this.camera);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Запускаем тест
|
|||
|
|
const test = new SimpleCollisionTest();
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|