Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            Вклады
                Лемур GUI
                    Lemur Джемы # 1 — Сопоставления ввода, базовые движения камеры.

Lemur Джемы # 1 — Сопоставления ввода, базовые движения камеры.

Опубликованно: 26.08.2017, 13:20
Последняя редакция, Andry: 16.10.2017 12:31

Итак, как некоторые из вас могут знать, Lemur — это легкая GUI библиотека, которая позволяет создавать метки, кнопки и другие полезные мелочи в 2D или 3D пользовательских интерфейсах.

По сути своей дизайн Lemur было созданием нескольких модульных наборов инструментов, которые вместе образуют полноценную GUI библиотеку. Следовательно, некоторые из этих наборов инструментов имеют большую полезность, даже если вы решите не использовать сами элементы GUI. Этот джем охватывает одну из следующих вещей: InputMapper

Весь проект можно найти здесь: LemurGems

InputMapper

InputMapper обертывает обычный InputManager JME для обеспечения более надежного ввода. Некоторые из его основных особенностей:

  • Полное разделение сопоставлений ввода и обработки ввода
  • Возможность сопоставлять дополнительные вводы с существующим сопоставлениями.
  • Возможность сопоставления комбинаций ввода с существующими сопоставлениями(shift+w, ctrl+колесо мыши, и.т.д.)
  • Возможность обрабатывать «дискретный» стиль состояний вводов как аналоговые оси. (Так что w и s могут сопоставляться с одной аналоговой «осью».)
  • Группы функций для переключения наборов вводов на включено и выключено.

Чтобы продемонстрировать эти функции, я предоставил пример стиля fly-cam, с использованием обработки ввода InputMapper.

Концепция клавиш

Логическое сопоставление из набора вводов в набор слушателей осуществляется через FunctionId. Так, с одной стороны, код может сопоставлять ось X мыши в FunctionId(«Mouse Look X»), а с другой стороны, некоторый другой код может слушать события для FunctionId(«Mouse Look X»). Регистрация одного или другого может быть выполнена в любом порядке, и дополнительные сопоставления ввода в FunctionId(«Mouse Look X») можно добавить в любое время.

FunctionId содержит необязательное обозначение группы. FunctionId(«Movement», «Mouse Look X») эти группы могут быть активированы или деактивированы одновременно.

Хотя в вашем приложение можно выбрать определение FunctionId, вводов и слушателей в одном месте, я предпочитаю разбивать FunctionId и стандартные сопоставления на отдельные классы, чтобы сделать разделение более четким. Я сделал это здесь. Другим преимуществом является то, что он облегчает для другого кода добавление слушателей к тем же функциям, если им выбирают … или несколько состояний приложения могут сопоставляться с теми же ID функциями, которые распознают, что одновременно будет использоваться только одно состояние включенным и.т.д. ,

Для меня сохранение логических ID является хорошей практикой, но это отнюдь не требуется.

В этом примере все Id функции движения камеры содержатся в классе под названием CameraMovementFunctions. Полностью класс можно увидеть здесь: CameraMovementFunctions.java

Я приведу несколько выдержек ниже:

public class CameraMovementFunctions {

    public static final String GROUP_MOVEMENT = "Movement";

public static final FunctionId F_Y_LOOK = new FunctionId(GROUP_MOVEMENT, "Y Look");
public static final FunctionId F_X_LOOK = new FunctionId(GROUP_MOVEMENT, "X Look");

public static final FunctionId F_MOVE = new FunctionId(GROUP_MOVEMENT, "Move");
public static final FunctionId F_STRAFE = new FunctionId(GROUP_MOVEMENT, "Strafe");

....вырезано....

public static void initializeDefaultMappings( InputMapper inputMapper )
{
    // Ось джойстика Y обращена назад на game pads... вперед
    // отрицательно. Поэтому мы перевернем его в сопоставлении.
    inputMapper.map( F_MOVE, InputState.Negative, Axis.JOYSTICK_LEFT_Y );
    
    // Здесь аналогичный подход используется для сопоставления клавиш W и S с
    // 'S' являющейся отрицательным. Таким образом, W и S теперь действуют как 
    // оси джойстика.
    inputMapper.map( F_MOVE, KeyInput.KEY_W );
    inputMapper.map( F_MOVE, InputState.Negative, KeyInput.KEY_S );
    
    // Strafing настроен аналогично перемещению.
    inputMapper.map( F_STRAFE, Axis.JOYSTICK_LEFT_X );
    inputMapper.map( F_STRAFE, KeyInput.KEY_D );
    inputMapper.map( F_STRAFE, InputState.Negative, KeyInput.KEY_A );

    ...вырезано....
    inputMapper.map( F_RUN, KeyInput.KEY_LSHIFT );
    ...вырезано....
}
}

Стоит перейти по ссылке, чтобы просмотреть весь файл, так как есть куча закомментированного текста и некоторые дополнительные сопоставления, которых нет в выше указанной выдержке.

Первичной целью этого класса является определение некоторых логических FunctionIds. Вторичной целью является предоставление некоторых сопоставлений ввода по умолчанию. Вызывающий код может выбрать вызвать этот метод initializeDefaultMappings() или нет, основываясь на необходимости. Возможно, нужно вместо этого определить свои собственные вводы. Функции остаются неизменными.

Это похоже на имена триггеров JME, но немного более гибкое.

Слушатели

InputMapper принимает либо AnalogFunctionListener, либо StateFunctionListener. Они похожи на слушателей InputManager в JME, но с некоторыми отличиями. Во-первых, ни одно из значений не умножается на tpf. Это решать вам.

Во-вторых, вместо состояния «boolean» теперь имеется перечисление InputState. Метод StateFunctionListener выглядит так:

public void valueChanged( FunctionId func, InputState value, double tpf )

InputState имеет три возможных значения: Positive, Off, и Negative.

Все аналоговые или дискретные вводы будут запускать оба типа ввода. Таким образом, оси джойстика могут запускать StateFunctionListener, и вы увидите Positive, Off или Negative в зависимости от положения джойстика. Аналогично, «дискретный» ввод, такой как клавиши, будут запускать аналоговые события в течение всего времени, пока они отключены. Подобно обычным JME, эти значения будут выглядеть как 1 или -1.

Итак, вот CameraMovementState. CameraMovementState.java

public class CameraMovementState extends BaseAppState
                                 implements AnalogFunctionListener, StateFunctionListener {

    private InputMapper inputMapper;
private Camera camera;
private double turnSpeed = 2.5;  // половина полного оборота за 2,5 секунды
private double yaw = FastMath.PI;
private double pitch;
private double maxPitch = FastMath.HALF_PI;
private double minPitch = -FastMath.HALF_PI;
private Quaternion cameraFacing = new Quaternion().fromAngles((float)pitch, (float)yaw, 0);
private double forward;
private double side;
private double elevation;
private double speed = 3.0;

public CameraMovementState( boolean enabled ) {
    setEnabled(enabled);
}

public void setPitch( double pitch ) {
    this.pitch = pitch;
    updateFacing();
}

public double getPitch() {
    return pitch;
}

public void setYaw( double yaw ) {
    this.yaw = yaw;
    updateFacing();
}

public double getYaw() {
    return yaw;
}

public void setRotation( Quaternion rotation ) {
    // Сделаем наше наилучшее
    float[] angle = rotation.toAngles(null);
    this.pitch = angle[0];
    this.yaw = angle[1];
    updateFacing();
}

public Quaternion getRotation() {
    return camera.getRotation();
}

@Override
protected void initialize(Application app) {
    this.camera = app.getCamera();
    
    if( inputMapper == null )
        inputMapper = GuiGlobals.getInstance().getInputMapper();
    
    // Большинство функций движения рассматриваются как аналоговые.        
    inputMapper.addAnalogListener(this,
                                  CameraMovementFunctions.F_Y_LOOK,
                                  CameraMovementFunctions.F_X_LOOK,
                                  CameraMovementFunctions.F_MOVE,
                                  CameraMovementFunctions.F_ELEVATE,
                                  CameraMovementFunctions.F_STRAFE);

    // Только режим запуска рассматривается как «state» или трехзначное значение.
    // (Positive, Off, Negative) и в этом случае мы только заботимся о
    // Positive и Off. См. CameraMovementFunctions для описания 
    // альтернативных способов, которыми это могло быть сделано.
    inputMapper.addStateListener(this,
                                 CameraMovementFunctions.F_RUN);
}

@Override
protected void cleanup(Application app) {

    inputMapper.removeAnalogListener( this,
                                      CameraMovementFunctions.F_Y_LOOK,
                                      CameraMovementFunctions.F_X_LOOK,
                                      CameraMovementFunctions.F_MOVE,
                                      CameraMovementFunctions.F_ELEVATE,
                                      CameraMovementFunctions.F_STRAFE);
    inputMapper.removeStateListener( this,
                                     CameraMovementFunctions.F_RUN);
}

@Override
protected void enable() {
    // Убедитесь, что наша группа ввода включена
    inputMapper.activateGroup( CameraMovementFunctions.GROUP_MOVEMENT );
    
    // И убьём курсор
    GuiGlobals.getInstance().setCursorEventsEnabled(false);
    
    // 'ошибка' в Lemur заставляет его пропустить поворот курсора, если
    // мы включим до инициализации MouseAppState.
    getApplication().getInputManager().setCursorVisible(false);        
}

@Override
protected void disable() {
    inputMapper.deactivateGroup( CameraMovementFunctions.GROUP_MOVEMENT );
    GuiGlobals.getInstance().setCursorEventsEnabled(true);        
}

@Override
public void update( float tpf ) {

    // 'интегрировать' положение камеры, основанное на текущих перемещениях, стрельбе
    // и быстрых подъемах.
    if( forward != 0 || side != 0 || elevation != 0 ) {
        Vector3f loc = camera.getLocation();
        
        Quaternion rot = camera.getRotation();
        Vector3f move = rot.mult(Vector3f.UNIT_Z).multLocal((float)(forward * speed * tpf)); 
        Vector3f strafe = rot.mult(Vector3f.UNIT_X).multLocal((float)(side * speed * tpf));
        
        // Примечание: эта камера перемещается «по высоте» по текущему
        // вектору камеры, потому что я нахожу это более интуитивным в свободном полете.
        Vector3f elev = rot.mult(Vector3f.UNIT_Y).multLocal((float)(elevation * speed * tpf));
                    
        loc = loc.add(move).add(strafe).add(elev);
        camera.setLocation(loc); 
    }
}
 
    /**
     *  Реализация интерфейса StateFunctionListener.
     */
    @Override
    public void valueChanged( FunctionId func, InputState value, double tpf ) {
 
        // Изменение скорости в зависимости от текущего режима работы
        // Другим вариантом было бы использовать значение
        // напрямую:
        //    speed = 3 + value.asNumber() * 5
        //...но я чувствовал, что это было бы немного менее ясно здесь.
        boolean b = value == InputState.Positive;
        if( func == CameraMovementFunctions.F_RUN ) {
            if( b ) {
                speed = 10;
            } else {
                speed = 3;
            }
        }
    }

    /**
 *  Реализация интерфейса AnalogFunctionListener.
 */
@Override
public void valueActive( FunctionId func, double value, double tpf ) {
 
        // Установите скорости вращения и скорости движения, основанное на
        // текущих состояниях осей.    
        if( func == CameraMovementFunctions.F_Y_LOOK ) {
            pitch += -value * tpf * turnSpeed;
            if( pitch < minPitch )
                pitch = minPitch;
            if( pitch > maxPitch )
                pitch = maxPitch;
        } else if( func == CameraMovementFunctions.F_X_LOOK ) {
            yaw += -value * tpf * turnSpeed;
            if( yaw < 0 )
                yaw += Math.PI * 2;
            if( yaw > Math.PI * 2 )
                yaw -= Math.PI * 2;
        } else if( func == CameraMovementFunctions.F_MOVE ) {
            this.forward = value;
            return;
        } else if( func == CameraMovementFunctions.F_STRAFE ) {
            this.side = -value;
            return;
        } else if( func == CameraMovementFunctions.F_ELEVATE ) {
            this.elevation = value;
            return;
        } else {
            return;
        }
        updateFacing();        
    }

    protected void updateFacing() {
    cameraFacing.fromAngles( (float)pitch, (float)yaw, 0 );
    camera.setRotation(cameraFacing);
}
}

Простой main класс, включенный в проект, показывает, как это можно использовать: Main.java

Вот самые важные полезные мелочи:

....вырезано...
    public Main() {
        super(new StatsAppState(), new CameraMovementState(true));
    }

    @Override
public void simpleInitApp() {

    // Пусть GuyGlobals Лемура инициализируют много положительных героев для нас.
    // Это также настроит статический InputMapper, с которого мы можем ссылаться.
    // Альтернативой было бы создание и управление нашим собственным экземпляром 
    // InputMapper,  ... но последующие учебники будут опираться на 
    // другие части Lemur, поэтому мы могли бы также инициализировать его сейчас.
    GuiGlobals.initialize(this);
    
    // Сопоставления ввода камеры и обработка вводных данных камеры были 
    // разделены для ясности, но это означает, что по умолчанию
    // CameraMovementState не будет предоставлен какой-либо ввод.
    // Нам нужно сопоставить некоторые вводы с его функциями:
    CameraMovementFunctions.initializeDefaultMappings(GuiGlobals.getInstance().getInputMapper());
....вырезано...

По существу:

  1. Прикрепить app state
  2. Убедитесь, что InputMapper инициализирован (в этом случае это делается автоматически GuiGlobals.initialize())
  3. Настройте некоторые сопоставления

Некоторые вещи, которые я здесь не рассматривал, оставлены как упражнения для нетерпеливого читателя:

  • Комбинации клавиш
  • Масштабирование сопоставлений ввода для обеспечения инвертирования или регулировки чувствительности. (Взгляд мышью y-вращение, чувствительность мыши и.т.д.)
  • Возможны, некоторые другие вещи, о которых я забыл

Смотрите так же:


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.