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

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

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

Предыдущий: 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("Дракон",          -2f,  0f,  1f));
    shootables.attachChild(makeCube("консервная банка", 1f, -2f,  0f));
    shootables.attachChild(makeCube("Шериф",            0f,  1f, -2f));
    shootables.attachChild(makeCube("Заместитель",      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. Соберём пересечения между Ray и Shootables в списке результатов.
        // НЕ проверяйте столкновения с корневым узлом, потому что все столкновения будут попадать в skybox! 
        //Всегда создавайте отдельный узел для объектов, с которыми вы хотите реализовать столкновения.
        shootables.collideWith(ray, results);
        // 4. Распечатаем результат.
        System.out.println("----- Столкновения? " + 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("* Столкновение #" + i);
          System.out.println("  Вы стреляли в " + hit + " в " + pt + ", на расстояние " + dist + " wu.");
        }
        // 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("Пол", 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 сцене или нет. Эта линия называется лучом(ray).

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

  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("Дракон",          -2f,  0f,  1f));
    shootables.attachChild(makeCube("консервная банка", 1f, -2f,  0f));
    shootables.attachChild(makeCube("Шериф",            0f,  1f, -2f));
    shootables.attachChild(makeCube("Заместитель",      1f,  0f, -4f));
    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. Соберём пересечения между Ray и Shootables в списке результатов.
        shootables.collideWith(ray, results);
        // 4. Распечатаем результат.
        System.out.println("----- Столкновения? " + 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("* Столкновение #" + i);
          System.out.println("  Вы стреляли в " + hit + " в " + pt + ", на расстояние " + dist + " wu.");
        }
        // 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, Andry

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

    Содержание

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