package alternativa.engine3d.controllers { import alternativa.engine3d.*; import alternativa.engine3d.core.Camera3D; import alternativa.engine3d.physics.SphereCollider; import alternativa.types.Map; import alternativa.types.Matrix3D; import alternativa.types.Point3D; import alternativa.utils.KeyboardUtils; import alternativa.utils.MathUtils; import flash.display.DisplayObject; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.geom.Point; import flash.utils.getTimer; use namespace alternativa3d; /** * Контроллер камеры. Контроллер обеспечивает управление движением и поворотами камеры с использованием * клавиатуры и мыши, а также реализует простую проверку столкновений камеры с объектами сцены. */ public class CameraController { /** * Имя действия для привязки клавиш движения вперёд. */ public static const ACTION_FORWARD:String = "ACTION_FORWARD"; /** * Имя действия для привязки клавиш движения назад. */ public static const ACTION_BACK:String = "ACTION_BACK"; /** * Имя действия для привязки клавиш движения влево. */ public static const ACTION_LEFT:String = "ACTION_LEFT"; /** * Имя действия для привязки клавиш движения вправо. */ public static const ACTION_RIGHT:String = "ACTION_RIGHT"; /** * Имя действия для привязки клавиш движения вверх. */ public static const ACTION_UP:String = "ACTION_UP"; /** * Имя действия для привязки клавиш движения вниз. */ public static const ACTION_DOWN:String = "ACTION_DOWN"; // public static const ACTION_ROLL_LEFT:String = "ACTION_ROLL_LEFT"; // public static const ACTION_ROLL_RIGHT:String = "ACTION_ROLL_RIGHT"; /** * Имя действия для привязки клавиш увеличения угла тангажа. */ public static const ACTION_PITCH_UP:String = "ACTION_PITCH_UP"; /** * Имя действия для привязки клавиш уменьшения угла тангажа. */ public static const ACTION_PITCH_DOWN:String = "ACTION_PITCH_DOWN"; /** * Имя действия для привязки клавиш уменьшения угла рысканья (поворота налево). */ public static const ACTION_YAW_LEFT:String = "ACTION_YAW_LEFT"; /** * Имя действия для привязки клавиш увеличения угла рысканья (поворота направо). */ public static const ACTION_YAW_RIGHT:String = "ACTION_YAW_RIGHT"; // флаги действий private var _forward:Boolean; private var _back:Boolean; private var _left:Boolean; private var _right:Boolean; private var _up:Boolean; private var _down:Boolean; private var _pitchUp:Boolean; private var _pitchDown:Boolean; private var _yawLeft:Boolean; private var _yawRight:Boolean; private var _moveLocal:Boolean = true; // Флаг включения управления камерой private var _controlsEnabled:Boolean = false; // Значение таймера в начале прошлого кадра private var lastFrameTime:uint; // Чувствительность обзора мышью. Коэффициент умножения базовых коэффициентов поворотов. private var _mouseSensitivity:Number = 1; // Коэффициент поворота камеры по тангажу. Значение угла в радианах на один пиксель перемещения мыши по вертикали. private var _mousePitch:Number = Math.PI / 360; // Результирующий коэффициент поворота камеры мышью по тангажу private var _mousePitchCoeff:Number = _mouseSensitivity * _mousePitch; // Коэффициент поворота камеры по рысканью. Значение угла в радианах на один пиксель перемещения мыши по горизонтали. private var _mouseYaw:Number = Math.PI / 360; // Результирующий коэффициент поворота камеры мышью по расканью private var _mouseYawCoeff:Number = _mouseSensitivity * _mouseYaw; // Вспомогательные переменные для обзора мышью private var mouseLookActive:Boolean; private var startDragCoords:Point = new Point(); private var currentDragCoords:Point = new Point(); private var prevDragCoords:Point = new Point(); private var startRotX:Number; private var startRotZ:Number; // Скорость изменения тангажа в радианах за секунду при управлении с клавиатуры private var _pitchSpeed:Number = 1; // Скорость изменения рысканья в радианах за секунду при управлении с клавиатуры private var _yawSpeed:Number = 1; // Скорость изменения крена в радианах за секунду при управлении с клавиатуры // public var bankSpeed:Number = 2; // private var bankMatrix:Matrix3D = new Matrix3D(); // Скорость поступательного движения в единицах за секунду private var _speed:Number = 100; private var velocity:Point3D = new Point3D(); private var destination:Point3D = new Point3D(); private var _fovStep:Number = Math.PI / 180; private var _zoomMultiplier:Number = 0.1; // Привязка клавиш к действиям private var keyBindings:Map = new Map(); // Привязка действий к обработчикам private var actionBindings:Map = new Map(); // Источник событий клавиатуры и мыши private var _eventsSource:DisplayObject; // Управляемая камера private var _camera:Camera3D; // Класс реализации определния столкновений private var _collider:SphereCollider; // Флаг необходимости проверки столкновений private var _checkCollisions:Boolean; // Радиус сферы для определения столкновений private var _collisionRadius:Number = 0; /** * Создание экземпляра класса. * * @param eventsSourceObject объект, используемый для получения событий мыши и клавиатуры */ public function CameraController(eventsSourceObject:DisplayObject) { if (eventsSourceObject == null) { throw new ArgumentError("CameraController: eventsSource is null"); } _eventsSource = eventsSourceObject; actionBindings[ACTION_FORWARD] = setForwardFlag; actionBindings[ACTION_BACK] = setBackFlag; actionBindings[ACTION_LEFT] = setLeftFlag; actionBindings[ACTION_RIGHT] = setRightFlag; actionBindings[ACTION_UP] = setUpFlag; actionBindings[ACTION_DOWN] = setDownFlag; actionBindings[ACTION_PITCH_UP] = setPitchUpFlag; actionBindings[ACTION_PITCH_DOWN] = setPitchDownFlag; actionBindings[ACTION_YAW_LEFT] = setYawLeftFlag; actionBindings[ACTION_YAW_RIGHT] = setYawRightFlag; } /** * Установка привязки клавиш по умолчанию. */ public function setDefaultBindings():void { unbindAll(); bindKey(KeyboardUtils.W, ACTION_FORWARD); bindKey(KeyboardUtils.S, ACTION_BACK); bindKey(KeyboardUtils.A, ACTION_LEFT); bindKey(KeyboardUtils.D, ACTION_RIGHT); bindKey(KeyboardUtils.SPACE, ACTION_UP); bindKey(KeyboardUtils.SHIFT, ACTION_DOWN); bindKey(KeyboardUtils.UP, ACTION_PITCH_UP); bindKey(KeyboardUtils.DOWN, ACTION_PITCH_DOWN); bindKey(KeyboardUtils.LEFT, ACTION_YAW_LEFT); bindKey(KeyboardUtils.RIGHT, ACTION_YAW_RIGHT); } /** * Направление камеры на точку. * * @param point координаты точки направления камеры */ public function lookAt(point:Point3D):void { var dx:Number = point.x - _camera.x; var dy:Number = point.y - _camera.y; var dz:Number = point.z - _camera.z; _camera.rotationZ = -Math.atan2(dx, dy); _camera.rotationX = Math.atan2(dz, Math.sqrt(dx * dx + dy * dy)) - MathUtils.DEG90; } /** * Источник событий клавиатуры и мыши. */ public function get eventsSource():DisplayObject { return _eventsSource; } /** * @private */ public function set eventsSource(value:DisplayObject):void { if (value == null) { throw new ArgumentError("CameraController: eventsSource is null"); } if (_eventsSource != value) { if (_controlsEnabled) { unregisterEventsListeners(); } _eventsSource = value; if (_controlsEnabled) { registerEventListeners(); } } } /** * Текущая управляемая камера. */ public function get camera():Camera3D { return _camera; } /** * @private */ public function set camera(value:Camera3D):void { if (value == null) { controlsEnabled = false; } _camera = value; } /** * Режим движения камеры. Если значение равно true, то перемещения камеры происходят относительно * локальной системы координат, иначе относительно глобальной. * * @default true */ public function get moveLocal():Boolean { return _moveLocal; } /** * @private */ public function set moveLocal(value:Boolean):void { _moveLocal = value; } /** * Включение режима проверки столкновений. Установка значения невозможна, пока камера не добавлена на сцену. */ public function get checkCollisions():Boolean { return _checkCollisions; } /** * @private */ public function set checkCollisions(value:Boolean):void { if (_camera.scene == null) { return; } if (_checkCollisions != value) { _checkCollisions = value; if (_checkCollisions && _collider == null) { _collider = new SphereCollider(_camera.scene, _collisionRadius); _collider.offsetThreshold = 0.01; } } } /** * Радиус сферы для определения столкновений. * * @default 0 */ public function get collisionRadius():Number { return _collisionRadius; } /** * @private */ public function set collisionRadius(value:Number):void { _collisionRadius = value; if (_collider != null) { _collider.sphereRadius = _collisionRadius; } } /** * Связывание клавиши с действием. * * @param keyCode код клавиши * @param action наименование действия */ public function bindKey(keyCode:uint, action:String):void { var method:Function = actionBindings[action]; if (method != null) { keyBindings[keyCode] = method; } } /** * Очистка привязки клавиши. * * @param keyCode код клавиши */ public function unbindKey(keyCode:uint):void { keyBindings.remove(keyCode); } /** * Очистка привязки всех клавиш. */ public function unbindAll():void { keyBindings.clear(); } /** * @private * @param value */ private function setForwardFlag(value:Boolean):void { _forward = value; } /** * @private * @param value */ private function setBackFlag(value:Boolean):void { _back = value; } /** * @private * @param value */ private function setLeftFlag(value:Boolean):void { _left = value; } /** * @private * @param value */ private function setRightFlag(value:Boolean):void { _right = value; } /** * @private * @param value */ private function setUpFlag(value:Boolean):void { _up = value; } /** * @private * @param value */ private function setDownFlag(value:Boolean):void { _down = value; } /** * @private * @param value */ private function setPitchUpFlag(value:Boolean):void { _pitchUp = value; } /** * @private * @param value */ private function setPitchDownFlag(value:Boolean):void { _pitchDown = value; } /** * @private * @param value */ private function setYawLeftFlag(value:Boolean):void { _yawLeft = value; } /** * @private * @param value */ private function setYawRightFlag(value:Boolean):void { _yawRight = value; } /** * Чувствительность мыши. * * @default 1 */ public function get mouseSensitivity():Number { return _mouseSensitivity; } /** * @private */ public function set mouseSensitivity(sensitivity:Number):void { _mouseSensitivity = sensitivity; _mousePitchCoeff = _mouseSensitivity * _mousePitch; _mouseYawCoeff = _mouseSensitivity * _mouseYaw; } /** * Скорость изменения тангажа при управлении мышью (радианы на пиксель). * * @default Math.PI / 360 */ public function get mousePitch():Number { return _mousePitch; } /** * @private */ public function set mousePitch(pitch:Number):void { _mousePitch = pitch; _mousePitchCoeff = _mouseSensitivity * _mousePitch; } /** * Скорость изменения рысканья при управлении мышью (радианы на пиксель). * * @default Math.PI / 360 */ public function get mouseYaw():Number { return _mouseYaw; } /** * @private */ public function set mouseYaw(yaw:Number):void { _mouseYaw = yaw; _mouseYawCoeff = _mouseSensitivity * _mouseYaw; } /** * Угловая скорость по тангажу при управлении с клавиатуры (радианы в секунду). * * @default 1 */ public function get pitchSpeed():Number { return _pitchSpeed; } /** * @private */ public function set pitchSpeed(spd:Number):void { _pitchSpeed = spd; } /** * Угловая скорость по рысканью при управлении с клавиатуры (радианы в секунду). * * @default 1 */ public function get yawSpeed():Number { return _yawSpeed; } /** * @private */ public function set yawSpeed(spd:Number):void { _yawSpeed = spd; } /** * Скорость поступательного движения (единицы в секунду). */ public function get speed():Number { return _speed; } /** * @private */ public function set speed(spd:Number):void { _speed = spd; } /** * Активность управления камеры. * * @default false */ public function get controlsEnabled():Boolean { return _controlsEnabled; } /** * @private */ public function set controlsEnabled(state:Boolean):void { if (_camera == null || _controlsEnabled == state) return; if (state) { lastFrameTime = getTimer(); registerEventListeners(); } else { unregisterEventsListeners(); } _controlsEnabled = state; } /** * Базовый шаг изменения угла зрения в радианах. Реальный шаг получаеся умножением этого значения на величину * MouseEvent.delta. * * @default Math.PI / 180 */ public function get fovStep():Number { return _fovStep; } /** * @private */ public function set fovStep(value:Number):void { _fovStep = value; } /** * Коэффициент изменения коэффициента увеличения. * * @default 0.1 */ public function get zoomMultiplier():Number { return _zoomMultiplier; } /** * @private */ public function set zoomMultiplier(value:Number):void { _zoomMultiplier = value; } /** * @private */ private function registerEventListeners():void { _eventsSource.addEventListener(KeyboardEvent.KEY_DOWN, onKey); _eventsSource.addEventListener(KeyboardEvent.KEY_UP, onKey); _eventsSource.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); _eventsSource.addEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel); } /** * @private */ private function unregisterEventsListeners():void { _eventsSource.removeEventListener(KeyboardEvent.KEY_DOWN, onKey); _eventsSource.removeEventListener(KeyboardEvent.KEY_UP, onKey); _eventsSource.removeEventListener(MouseEvent.MOUSE_DOWN, onMouseDown); _eventsSource.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); _eventsSource.removeEventListener(MouseEvent.MOUSE_WHEEL, onMouseWheel); } /** * @private * @param e */ private function onKey(e:KeyboardEvent):void { var method:Function = keyBindings[e.keyCode]; if (method != null) { method.call(this, e.type == KeyboardEvent.KEY_DOWN); } } /** * @private * @param e */ private function onMouseDown(e:MouseEvent):void { mouseLookActive = true; currentDragCoords.x = startDragCoords.x = _eventsSource.stage.mouseX; currentDragCoords.y = startDragCoords.y = _eventsSource.stage.mouseY; startRotX = _camera.rotationX; startRotZ = _camera.rotationZ; _eventsSource.stage.addEventListener(MouseEvent.MOUSE_UP, onMouseUp); } /** * @private * @param e */ private function onMouseUp(e:MouseEvent):void { mouseLookActive = false; _eventsSource.stage.removeEventListener(MouseEvent.MOUSE_UP, onMouseUp); } /** * @private * @param e */ private function onMouseWheel(e:MouseEvent):void { if (_camera.orthographic) { _camera.zoom = _camera.zoom * (1 + e.delta * _zoomMultiplier); } else { _camera.fov -= _fovStep * e.delta; } } /** * Обработка управляющих воздействий. * Метод должен вызываться каждый кадр перед вызовом Scene3D.calculate(). * * @see alternativa.engine3d.core.Scene3D#calculate() */ public function processInput(): void { if (!_controlsEnabled || _camera == null) return; // Время в секундах от начала предыдущего кадра var frameTime:Number = getTimer() - lastFrameTime; lastFrameTime += frameTime; frameTime /= 1000; // Обработка mouselook if (mouseLookActive) { prevDragCoords.x = currentDragCoords.x; prevDragCoords.y = currentDragCoords.y; currentDragCoords.x = _eventsSource.stage.mouseX; currentDragCoords.y = _eventsSource.stage.mouseY; if (!prevDragCoords.equals(currentDragCoords)) { _camera.rotationZ = startRotZ + (startDragCoords.x - currentDragCoords.x) * _mouseYawCoeff; var rotX:Number = startRotX + (startDragCoords.y - currentDragCoords.y) * _mousePitchCoeff; _camera.rotationX = (rotX > 0) ? 0 : (rotX < -MathUtils.DEG180) ? -MathUtils.DEG180 : rotX; } } // Поворот относительно вертикальной оси (рысканье, yaw) if (_yawLeft) { _camera.rotationZ += _yawSpeed * frameTime; } else if (_yawRight) { _camera.rotationZ -= _yawSpeed * frameTime; } // Поворот относительно поперечной оси (тангаж, pitch) if (_pitchUp) { _camera.rotationX += _pitchSpeed * frameTime; } else if (_pitchDown) { _camera.rotationX -= _pitchSpeed * frameTime; } // TODO: Поворот относительно продольной оси (крен, roll) var frameDistance:Number = _speed * frameTime; velocity.x = 0; velocity.y = 0; velocity.z = 0; var transformation:Matrix3D = _camera.transformation; if (_moveLocal) { // Режим относительных пермещений // Движение вперед-назад if (_forward) { velocity.x += frameDistance * transformation.c; velocity.y += frameDistance * transformation.g; velocity.z += frameDistance * transformation.k; } else if (_back) { velocity.x -= frameDistance * transformation.c; velocity.y -= frameDistance * transformation.g; velocity.z -= frameDistance * transformation.k; } // Движение влево-вправо if (_left) { velocity.x -= frameDistance * transformation.a; velocity.y -= frameDistance * transformation.e; velocity.z -= frameDistance * transformation.i; } else if (_right) { velocity.x += frameDistance * transformation.a; velocity.y += frameDistance * transformation.e; velocity.z += frameDistance * transformation.i; } // Движение вверх-вниз if (_up) { velocity.x -= frameDistance * transformation.b; velocity.y -= frameDistance * transformation.f; velocity.z -= frameDistance * transformation.j; } else if (_down) { velocity.x += frameDistance * transformation.b; velocity.y += frameDistance * transformation.f; velocity.z += frameDistance * transformation.j; } } else { // Режим глобальных перемещений var cosZ:Number = Math.cos(_camera.rotationZ); var sinZ:Number = Math.sin(_camera.rotationZ); // Движение вперед-назад if (_forward) { velocity.x -= frameDistance * sinZ; velocity.y += frameDistance * cosZ; } else if (_back) { velocity.x += frameDistance * sinZ; velocity.y -= frameDistance * cosZ; } // Движение влево-вправо if (_left) { velocity.x -= frameDistance * cosZ; velocity.y -= frameDistance * sinZ; } else if (_right) { velocity.x += frameDistance * cosZ; velocity.y += frameDistance * sinZ; } // Движение вверх-вниз if (_up) { velocity.z += frameDistance; } else if (_down) { velocity.z -= frameDistance; } } // Коррекция модуля вектора скорости if (velocity.x != 0 || velocity.y != 0 || velocity.z != 0) { velocity.length = frameDistance; } if (_checkCollisions) { _collider.calculateDestination(_camera.coords, velocity, destination); _camera.x = destination.x; _camera.y = destination.y; _camera.z = destination.z; } else { _camera.x += velocity.x; _camera.y += velocity.y; _camera.z += velocity.z; } } } }