Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            jMonkeyEngine3: Привет мир, Обучающая Серия
                jMonkeyEngine 3 урок (8) — Hello Picking

jMonkeyEngine 3 урок (8) — Hello Picking

Опубликованно: 04.04.2017, 23:10
Последняя редакция, Andry: 29.06.2017 18:03

Предыдущий: Hello Animation, Следующий: Hello Collision

Типичные взаимодействия в играх включают в себя стрельбу, подбирание предметов и открывание дверей. С точки зрения реализации, эти совсем разные взаимодействия удивительно похожи: Сначала пользователь выбирает цель на 3D-сцене и прицеливается, а затем запускает действие на неё. Мы называем этот процесс выбором(picking).

Вы можете выбрать что-то, нажав клавишу на клавиатуре, или щелкнув мышью. В любом случае, вы определяете цель, направляя луч -прямую линию- в сцену. Этот метод реализации выбора называется методом бросания лучей или ray casting (не является тем же, что трассировка лучей).

Этот урок основывается на том, что вы узнали в уроке Hello Input. Вы найдете более подробные образцы кода в разделах Выбор Мышью и Столкновения и Пересечения.

beginner-picking

Пример кода

package jme3test.helloworld;
 
import com.jme3.app.SimpleApplication;
import com.jme3.collision.CollisionResult;
import com.jme3.collision.CollisionResults;
import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput;
import com.jme3.input.MouseInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.MouseButtonTrigger;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Ray;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.scene.shape.Sphere;
 
/** Пример 8 - как позволить пользователю собирать(выбрать)  объекты в сцене
  * с помощью мыши или нажатий клавиш. Может использоваться для выстрелов, открытие дверей и.т.д. */
public class HelloPicking extends SimpleApplication {
 
  public static void main(String[] args) {
    HelloPicking app = new HelloPicking();
    app.start();
  }
  private Node shootables;
  private Geometry mark;
 
  @Override
  public void simpleInitApp() {
    initCrossHairs(); // "+" в центре экрана, чтобы помочь прицеливаться
    initKeys();       // загрузка пользовательской сопоставления клавиш
    initMark();       // красный шар, чтобы отметить попадание
 
    /** создать четыре цветных куба и пол, чтобы стрелять по ним: */
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon", -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can", 1f, -2f, 0f));
    shootables.attachChild(makeCube("the Sheriff", 0f, 1f, -2f));
    shootables.attachChild(makeCube("the Deputy", 1f, 0f, -4f));
    shootables.attachChild(makeFloor());
    shootables.attachChild(makeCharacter());
  }
 
  /** Объявить действие "Shoot" и сопоставить его с триггером. */
  private void initKeys() {
    inputManager.addMapping("Shoot",
      new KeyTrigger(KeyInput.KEY_SPACE), // триггер 1: пробел, или
      new MouseButtonTrigger(MouseInput.BUTTON_LEFT)); // триггер 2: левая кнопка мыши
    inputManager.addListener(actionListener, "Shoot");
  }
  /** Определить действие "Shoot": Определить, что было поражено и как реагировать. */
  private ActionListener actionListener = new ActionListener() {
 
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Сбросить список результатов.
        CollisionResults results = new CollisionResults();
        // 2. Направить луч от точки расположения камеры по направлению камеры.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Собрать пересечения между лучом и Shootables в списке результатов.
        // НЕ проверяйте столкновения с корневым узлом, иначе все столкновения попадут в skybox! 
        //Всегда создавайте отдельный узел для объектов, с которыми вы хотите реализовать столкновения.
        shootables.collideWith(ray, results);
        // 4. Распечатать результат.
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // Для каждого удара мы знаем расстояние, точку удара, и название геометрии.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Используем результаты (мы отмечаем объекты в которые были попадания)
        if (results.size() > 0) {
          // Самое близкое столкновения, это то во что действительно попало:
          CollisionResult closest = results.getClosestCollision();
          // Давайте взаимодействовать - мы отмечаем попадание красной точкой.
          mark.setLocalTranslation(closest.getContactPoint());
          rootNode.attachChild(mark);
        } else {
          // Нет попаданий? Тогда удалим красную метку.
          rootNode.detachChild(mark);
        }
      }
    }
  };
 
  /** Объект куб мишень для тренировки */
  protected Geometry makeCube(String name, float x, float y, float z) {
    Box box = new Box(1, 1, 1);
    Geometry cube = new Geometry(name, box);
    cube.setLocalTranslation(x, y, z);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.randomColor());
    cube.setMaterial(mat1);
    return cube;
  }
 
  /** Пол, чтобы показать, что "выстрел" может пройти через несколько объектов. */
  protected Geometry makeFloor() {
    Box box = new Box(15, .2f, 15);
    Geometry floor = new Geometry("the Floor", box);
    floor.setLocalTranslation(0, -4, -5);
    Material mat1 = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mat1.setColor("Color", ColorRGBA.Gray);
    floor.setMaterial(mat1);
    return floor;
  }
 
  /** Красный шар, который отмечает последнее место 'попадания' от 'выстрела'. */
  protected void initMark() {
    Sphere sphere = new Sphere(30, 30, 0.2f);
    mark = new Geometry("BOOM!", sphere);
    Material mark_mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    mark_mat.setColor("Color", ColorRGBA.Red);
    mark.setMaterial(mark_mat);
  }
 
  /** Значёк плюса в центре, чтобы помочь игроку целиться. */
  protected void initCrossHairs() {
    setDisplayStatView(false);
    guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
    BitmapText ch = new BitmapText(guiFont, false);
    ch.setSize(guiFont.getCharSet().getRenderedSize() * 2);
    ch.setText("+"); // перекрестие
    ch.setLocalTranslation( // центр
      settings.getWidth() / 2 - ch.getLineWidth()/2, settings.getHeight() / 2 + ch.getLineHeight()/2, 0);
    guiNode.attachChild(ch);
  }
 
  protected Spatial makeCharacter() {
    // загрузка персонажа из jme3test-test-data
    Spatial golem = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
    golem.scale(0.5f);
    golem.setLocalTranslation(-1.0f, -1.5f, -0.6f);
 
    // Мы должны добавить свет, чтобы сделать модель видимой
    DirectionalLight sun = new DirectionalLight();
    sun.setDirection(new Vector3f(-0.1f, -0.7f, -1.0f));
    golem.addLight(sun);
    return golem;
  }
}

Вы должны увидеть четыре цветных куба ппарящих над серым полом, и перекрестие. Направьте перекрестие и нажмите [ЛК мыши] или нажмите пробел, чтобы стрелять. Место попадания отмечается красной точкой.

Следите за выходным потоком приложения, он даст вам более подробную информацию: имя 3d-модели, которая была подбита, координаты попадания и расстояние.


Понимание вспомогательных методов

Методы makeCube(), makeFloor(), initMark(), и initCrossHairs, являются пользовательскими вспомогательными методами. Мы вызываем их в simpleInitApp() для инициализации графа сцены с образцами контента.

  1. makeCube() создает простые цветные кубы в качестве цели для возможности попрактиковаться в стрельбе.
  2. makeFloor() создает узел с серым полом в качестве цели для возможности попрактиковаться в стрельбе.
  3. initMark() создает красный шар (метку). Мы будем использовать его позже, чтобы отметить место в которое было попадание.
    • Обратите внимание, что метка не прикреплена и поэтому её нет при старте игры!
  4. initCrossHairs() создает простое перекрестие, печатая знак «+» в середине экрана.
    • Обратите внимание, что перекрестие прикреплено к guiNode, а не к RootNode.

В этом примере, мы прикрепили все объекты для стрельбы к одному пользовательскому узел, Shootables. Это оптимизация, позволяющая движку вычисляет только пересечения с объектами, которые нас на действительно интересуют. Узел Shootables прикрепляется к RootNode, как обычно.


Понимание роли бросания луча(Ray Casting) в тестовом выстреле

Наша цель состоит в том, чтобы определить, какой ящик пользователь «подстрелил» (взял). В общем, мы хотим определить, какую сетку (модели) выбрал пользователь, когда навел на неё перекрестие. Математически, мы рисуем линию идущую от камеры в направление её взгляда, чтобы увидеть, пересекается ли она с объектами в 3D сцене. Эта линия называется лучом.

Вот наш простой алгоритм бросания луча для выбора объектов:

  1. Сбросим список результатов.
  2. Получить луч выходящий из точки расположения камеры и идущий в направление камеры.
  3. Соберём все пересечения между лучом и объектами из узла Shoottable в список результатов.
  4. Используем список результатов, чтобы определить, что было поражен:
    1. Для каждого попадания, JME сообщает расстояние от камеры и до точки попадания, а также название сетки (модели).
    2. Отсортируем результаты по расстоянию.
    3. Возьмите самые близкий результат. Это будет сетка, которая была поражена.

Реализация тестового попадания

Загрузка сцены

Сначала инициализируем некоторых Shoottable узлы и прикрепим их к сцене. Вы будете использовать mark(метки) объекты позже.

  Node shootables;
  Geometry mark;
 
  @Override
  public void simpleInitApp() {
    initCrossHairs();
    initKeys();
    initMark();
 
    shootables = new Node("Shootables");
    rootNode.attachChild(shootables);
    shootables.attachChild(makeCube("a Dragon",    -2f, 0f, 1f));
    shootables.attachChild(makeCube("a tin can",    1f,-2f, 0f));
    shootables.attachChild(makeCube("the Sheriff",  0f, 1f,-2f));
    shootables.attachChild(makeCube("the Deputy",   1f, 0f, -4));
    shootables.attachChild(makeFloor());
  }

Настройка слушателя ввода

Затем вы объявляете действие выстрела. Оно будет вызвано кликом [ЛК мыши], и нажатием клавиши пробел. Метод initKeys() вызывается из simpleInitApp(), для настройки этих сопоставлений ввода.

  /** Объявить действие "Shoot" и сопоставить его с триггером. */
  private void initKeys() {
    inputManager.addMapping("Shoot",      // Декларировать...
      new KeyTrigger(KeyInput.KEY_SPACE), // триггер 1: пробел, или
      new MouseButtonTrigger(MouseInput.BUTTON_LEFT));         // триггер 2: левая кнопка мыши
    inputManager.addListener(actionListener, "Shoot"); // ... и добавим.

Действие Выбор при помощи перекрестья

Далее мы реализуем ActionListener, который реагирует на триггер с действием Shoot. Действие следуют алгоритму бросания луча, описанному выше:

  1. При каждом клике [ЛК мыши] или нажатии пробел, запускается действие Shoot.
  2. Действие бросает луч вперед и определяет пересечения с объектами shootable (= ray casting).
  3. Для любой цели, которая была поражена, будет напечатано имя, расстояние и координаты попадания.
  4. И наконец будет прикреплена красная метка к ближайшему объекту попадания, чтобы выделить место, в которое было попадание.
  5. Если нет никаких попаданий, список результатов станет пуст, а красные метки удалятся.

Обратите внимание, как много печатается, чтобы показать вам, какие попадания были зарегистрированы.

  /** Определить действие "Shoot": Определить, что было поражено и как реагировать. */
  private ActionListener actionListener = new ActionListener() {
 
    public void onAction(String name, boolean keyPressed, float tpf) {
      if (name.equals("Shoot") && !keyPressed) {
        // 1. Сбросить список результатов.
        CollisionResults results = new CollisionResults();
        // 2. Направить луч от точки расположения камеры по направлению камеры.
        Ray ray = new Ray(cam.getLocation(), cam.getDirection());
        // 3. Собрать пересечения между лучом и Shootables в списке результатов.
        shootables.collideWith(ray, results);
        // 4. Распечатать результат.
        System.out.println("----- Collisions? " + results.size() + "-----");
        for (int i = 0; i < results.size(); i++) {
          // Для каждого удара мы знаем расстояние, точку удара, и название геометрии.
          float dist = results.getCollision(i).getDistance();
          Vector3f pt = results.getCollision(i).getContactPoint();
          String hit = results.getCollision(i).getGeometry().getName();
          System.out.println("* Collision #" + i);
          System.out.println("  You shot " + hit + " at " + pt + ", " + dist + " wu away.");
        }
        // 5. Используем результаты (мы отмечаем объекты в которые были попадания)
        if (results.size() > 0) {
          // Самое близкое столкновения, это то во что действительно попало:
          CollisionResult closest = results.getClosestCollision();
          // Давайте взаимодействовать - мы отмечаем попадание красной точкой.
          mark.setLocalTranslation(closest.getContactPoint());
          rootNode.attachChild(mark);
        } else {
          // Нет попаданий? Тогда удалим красную метку.
          rootNode.detachChild(mark);
        }
      }
    }
  };
Обратите внимание на то, как вы используете предоставленный метод results.getClosestCollision().getContactPoint(), чтобы определить ближайшее место попадания. Если ваша игра использует «оружие» или «заклинания», которые могут поразить несколько целей одновременно, вы можете перебрать циклом весь список результатов, и взаимодействовать с каждым из них.

Действие Выбор с помощью указателя мыши

Приведенный выше пример предполагает, что игрок наводит перекрестие (прикрепленное к центру экрана) на цель. Но вы можете изменить код выбора, таким образом что бы могли свободно кликнуть на объекты в сцене с помощью видимого курсора мыши. Для того чтобы сделать это, вы должны преобразовать 2D координаты экрана мыши, в координаты 3D мира , чтобы получить начальную точку для луча выбора.

  1. Сбросим список результатов.
  2. Получим 2D координаты мыши.
  3. Преобразуем 2D координаты экрана в их 3D эквивалент.
  4. Направим луч от места клика в 3D место впереди в сцене.
  5. Соберем пересечения между лучом и всеми узлами в списке результатов.
...
CollisionResults results = new CollisionResults();
Vector2f click2d = inputManager.getCursorPosition();
Vector3f click3d = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 0f).clone();
Vector3f dir = cam.getWorldCoordinates(
    new Vector2f(click2d.x, click2d.y), 1f).subtractLocal(click3d).normalizeLocal();
Ray ray = new Ray(click3d, dir);
shootables.collideWith(ray, results);
...

Используйте это вместе с inputManager.setCursorVisible(true), чтобы убедиться, что курсор виден.

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


Упражнения

После того, как попадание было зарегистрировано, ближайший объект идентифицируется в качестве цель в которую попали, и отмечается красной точкой. Измените образец кода, чтобы решить эти упражнения:

Упражнение 1: Волшебное заклинание

Изменение цвета ближайшей выбранной цели!
Вот несколько советов:

  1. Перейдите к строке, в которой идентифицирована ближайшая цель, и добавить свои изменения после неё.
  2. Чтобы изменить цвет объекта, вы должны сначала узнать его Геометрию. Определите узел, указав имя цели.
    • Используйте Geometry g = closest.getGeometry();
  3. Создайте новый цветной материал и установите это Материал этому узла.
    • Просмотр метода makeCube() в качестве примера того, как установить случайные цвета.

Упражнение 2: Стрельба по персонажу

Стрельба по кубам не очень интересна — можете ли вы добавить код, который загружает и размещает модель в сцене, и стрелять в нее?

  • Вы можете использовать Spatial golem = assetManager.loadModel(“Models/Oto/Oto.mesh.xml); из библиотеки jme3-test-data.jar.
  • Модели затенены! Вам нужно немного света!

Упражнение 3: Взять в инвентарь

Измените код следующим образом что бы имитировать игрока собирающего предметы в инвентарь: При нажатии кнопки один раз, ближайшая цель определяется и отделяется от сцены. При нажатии кнопки во второй раз, цель прикрепляется в том месте, которое вы выбрали когда осуществили повторный щелчек. Вот несколько советов:

  1. Создайте узел инвентаря для временного хранения открепленных узлов.
  2. Узел инвентаря не прикрепляется к rootNode.
  3. Вы можете сделать инвентарь видимым путем присоединения узла инвентаря к guiNode (который прикрепляет его к HUD). Обратите внимание на следующие предостережения:
    • Если узлы используют освещаемый материал (не Unshaded.j3m), тогда также добавить свет к guiNode.
    • Единицы размера в HUD это пиксели, поэтому куб размера 2-wu отобразится только в 2 пикселях в HUD. — Так что не забудьте увеличить масштаб!
    • Размещение узлов: Нижний левый угол в HUD (0f, 0f), а верхний правый угол (settings.getWidth(),settings.getHeight()).
Ссылка на решения предлагаемые пользователями: Некоторые предлагаемые решения
Но, сначала, попытайтесь решить все сами!

Вывод

Вы научились использовать метод бросания лучей(ray casting), для решения задачи определения того, какой объект находящийся на экране, выбрал пользователь. Вы узнали, что это может быть использовано для различных взаимодействий, таких как стрельба, открытие дверей, собирание и бросании предметов, нажатие на кнопки или рычаги и.т.д.

Используйте свое воображение здесь:

  • В вашей игре, щелчок мыши может вызвать какие-либо действия по отношению к идентифицированной геометрии: Отделить её от корневого узла и положить в инвентарь, приложите что-то рядом с ней, вызвать анимацию или эффект, открыть дверь или сундук, и.т.д.
  • В вашей игре, вы можете заменить красную метку на излучатель частиц, добавить эффект взрыва, воспроизводить звук, вычислить новый счет после каждого попадания в зависимости от того, во что и куда было попадание и.т.д.

Теперь, было бы хорошо, если бы эти цели и пол были твердыми объектами, и вы могли бы ходить по полу, и только между или рядом с целями, а не через них? Давайте продолжим изучение, чтобы узнать о обнаружении столкновений.


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

  • Hello Input
  • Выбор Мышью
  • Столкновения и пересечения

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

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

    Содержание

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