/
RaskolnickOFF
/
3D
Обзор
Документация
Войти
/
RaskolnickOFF
/
3D
Код
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
js/source/systems/render/CameraSystem.js
206 строк
8 KB
RaskolnickOFF
Structure update
06 июн 2026, 03:18
06 июн 2026, 03:18
9bc1c25
Код
Авторство
О чём код?
// js/source/systems/render/CameraSystem.js — система камеры (рендер-слой) import * as THREE from 'three'; import { Components } from '../../core/Components.js'; import { GameEvents } from '../../core/GameEvents.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; export class CameraSystem { /** * @param {import('../core/RenderContext.js').RenderContext} ctx * @param {Object} [offset] — { x, y, z } * @param {Object} [zoomLimits] — { min, max } */ constructor(ctx, offset = { x: 0, y: 10, z: 15 }, zoomLimits = { min: 5, max: 20 }) { this.camera = ctx.camera; this.renderer = ctx.renderer; this.eventBus = ctx.eventBus; this.currentOffset = { ...offset }; this.targetId = null; this.zoomLimits = zoomLimits; // Режимы камеры this._freeMode = false; this._orbitControls = null; this._savedPlayerPos = null; // позиция игрока перед уходом в free mode // 4 стороны обзора, переключение по средней кнопке мыши this._currentSide = 0; this.angle = this._getAngleForSide(this._currentSide); this._lastPlayerX = 0; this._lastPlayerZ = 0; this._isPlayerMoving = false; // --- Callbacks --- this._onMouseDown = (e) => { if (this._freeMode) return; // в свободном режиме OrbitControls сами управляют if (e.button === 1) { e.preventDefault(); if (this._isPlayerMoving) return; this._currentSide = (this._currentSide + 1) % 4; this.angle = this._getAngleForSide(this._currentSide); } }; this._onMouseUp = (e) => { // Не используется }; this._onMouseMove = (e) => { // Не используется — дискретный поворот }; // Zoom колёсиком this._onWheel = (e) => { if (this._freeMode) return; // OrbitControls сами зумируют e.preventDefault(); const zoomSpeed = 0.5; const delta = e.deltaY > 0 ? zoomSpeed : -zoomSpeed; this.currentOffset.y += delta; this.currentOffset.z += delta; const min = this.zoomLimits.min; const max = this.zoomLimits.max; if (this.currentOffset.y < min) { this.currentOffset.y = min; this.currentOffset.z = min; } if (this.currentOffset.y > max) { this.currentOffset.y = max; this.currentOffset.z = max; } }; this._onContextMenu = (e) => { if (e.button === 1) e.preventDefault(); }; window.addEventListener('mousedown', this._onMouseDown); window.addEventListener('mouseup', this._onMouseUp); window.addEventListener('mousemove', this._onMouseMove); window.addEventListener('wheel', this._onWheel, { passive: false }); window.addEventListener('contextmenu', this._onContextMenu); // Подписка на переключение свободной камеры this.eventBus.on(GameEvents.CAMERA_TOGGLE_FREE, () => this.toggleFreeMode()); } _getAngleForSide(side) { const base = Math.PI / 4; // 45° return base + (side % 4) * (Math.PI / 2); } follow(entityId) { this.targetId = entityId; } getAngle() { return this.angle; } /** * Переключить режим: follow ↔ свободная камера */ toggleFreeMode() { this._freeMode = !this._freeMode; if (this._freeMode) { // Сохранить позицию игрока для возврата if (this.targetId !== null) { // Найдём игрока через engine (будет передан позже) this._savedTargetId = this.targetId; } // Создать OrbitControls if (!this._orbitControls) { this._orbitControls = new OrbitControls(this.camera, this.renderer.domElement); this._orbitControls.enableDamping = true; this._orbitControls.dampingFactor = 0.1; } this._orbitControls.enabled = true; } else { // Вернуться к игроку if (this._orbitControls) { this._orbitControls.enabled = false; } this.targetId = this._savedTargetId; // Сбросить last-позиции чтобы не было рывка this._lastPlayerX = 0; this._lastPlayerZ = 0; } } update(engine) { if (this._freeMode) { // OrbitControls сами обновляют камеру if (this._orbitControls) { this._orbitControls.update(); } return; } if (this.targetId === null) return; const entity = engine.getEntity(this.targetId); if (!entity || !entity.has(Components.TRANSFORM)) return; const transform = entity.get(Components.TRANSFORM); // Определяем, движется ли игрок const dx = transform.x - this._lastPlayerX; const dz = transform.z - this._lastPlayerZ; this._isPlayerMoving = Math.sqrt(dx * dx + dz * dz) > 0.02; this._lastPlayerX = transform.x; this._lastPlayerZ = transform.z; // Позиция камеры const sin = Math.sin(this.angle); const cos = Math.cos(this.angle); const offsetX = this.currentOffset.x * cos - this.currentOffset.z * sin; const offsetZ = this.currentOffset.x * sin + this.currentOffset.z * cos; entity.add(Components.CAMERA_OFFSET_X, offsetX); entity.add(Components.CAMERA_OFFSET_Z, offsetZ); const targetX = transform.x + offsetX; const targetY = transform.y + this.currentOffset.y; const targetZ = transform.z + offsetZ; const lerpFactor = 0.1; this.camera.position.x += (targetX - this.camera.position.x) * lerpFactor; this.camera.position.y += (targetY - this.camera.position.y) * lerpFactor; this.camera.position.z += (targetZ - this.camera.position.z) * lerpFactor; // Look-at const lookTarget = new THREE.Vector3(transform.x, transform.y, transform.z); const currentLook = new THREE.Vector3(); this.camera.getWorldDirection(currentLook); currentLook.multiplyScalar(10).add(this.camera.position); const smoothLook = new THREE.Vector3().lerpVectors(currentLook, lookTarget, 0.1); this.camera.lookAt(smoothLook); entity.add(Components.CAMERA_ANGLE, this.angle); } destroy() { window.removeEventListener('mousedown', this._onMouseDown); window.removeEventListener('mouseup', this._onMouseUp); window.removeEventListener('mousemove', this._onMouseMove); window.removeEventListener('wheel', this._onWheel); window.removeEventListener('contextmenu', this._onContextMenu); if (this._orbitControls) { this._orbitControls.dispose(); } } resetPosition(playerX, playerZ) { this._lastPlayerX = playerX; this._lastPlayerZ = playerZ; this.camera.position.set(playerX, playerZ + 10, playerZ + 15); this._isPlayerMoving = false; } }