Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            Документация для продвинутых пользователей
                jMonkeyEngine Искусственный Интеллект

jMonkeyEngine Искусственный Интеллект

Опубликованно: 07.07.2017, 15:41
Последняя редакция, Andry: 16.10.2017 22:23

Большинство написанных игр нуждаются в каком-то искусственном интеллекте, чтобы доставить ощущение реализма, волнения или бросить вызов игроку. ИИ может быть простым, иметь NPC (персонажа без игрока) реагирующего на какое-либо действие, предпринятое игроком, или сложным, способным плавное перемещаться по сцене, полной препятствий, без застревания. Это трудоёмкая и сложная задача, разработка таких систем, поэтому гораздо проще использовать уже существующую библиотеку, в которой уже сделана тяжелая работа за вас.

К сожалению, jMonkeyEngine не имеет официальной библиотеки для работы с ИИ. Однако есть библиотека искусственного интеллекта jme3, которая, вероятно, наиболее близка к официальному выпуску. Несмотря на то, что он никогда не попадал в какие-либо официальные выпуски, он был разработан, в частности, основными членами команды. Он состоит из двух отдельных моделей ИИ, библиотеки Навигационные Сетки с использованием поиска путей и простой библиотеки Управления Движением(Steering Behaviours), которая использует отслеживание пути.

Вы можете прочитать введении в библиотеки на форуме: Плагин ИИ теперь с помощью NavMesh pathfinding.


Требования

  • jme3 Библиотеки Искусственного Интеллекта — Библиотеки и javaDocs для jme3AI. Он также позволяет сообщать о проблемах или помогать в обслуживании библиотеки.
  • CritterAI — Stephen Pratts NMGen Студия файлы проекта по генерации navmesh.
  • Что бы получить эти игровые ресурсы (3D модели) используемые в этом примере, добавьте jME3-testdata.jar к вашим библиотекам.
  • Java SDK 8+.

Предлагается прочитать, Стивен Пратт подробно описывает параметры конфигурации CritterAI/Jme3AI в удобном для чтения формате.

Пример использования

Библиотека искусственного интеллекта jme3 содержит:

  • NavMesh — Навигационные Сетки(Navigation Mesh) path-finding AI системы, использующей алгоритм A*. [1]
  • Steering — Содержит основы Автономной системы агентов, которая использует path-following и заставляет персонажа перемещать в окружающей среде. Включает также тестовый пример. [2]
Содержимое этого урока ограничено частью NavMesh библиотеки и оно дополняет уроки, преподаваемые этих уроках. Оно демонстрирует использование некоторых классов и методов, изложенных в средние и продвинутом разделах вики. Исходники кода используемого в этом уроке можно найти в репозитории jMonkeyEngine/docs-examples.

Перемещение персонажа через вашу сцену требует трех вещей.

  • Навигационная сетка.
  • Компонент поиска пути, который использует эту навигационную сетку для вычисления пути.
  • Способ перемещения персонажа.

Создание NavMesh

Первое, что вам нужно для поиска пути, — это навигационная сетка. Существует два способа создания NavMesh, процедурного или jMonkey SDK.

  • SDK имеет встроенную команду, но поставляется с компромиссом, в котором не параметров создания исключений. Это означает, что вы просто будете лететь в слепую, если NavMesh выходит из строя.
  • Если вы выберете процедурный, вы увидите исключения для генерации, но вам придется немного поработать над сохранением, загрузкой и/или отображением NavMesh.

Оба метода создают одинаковый NavMesh, и оба они будут рассмотрены в этом уроке.

Из SDK

  • Откройте свою сцену в Terrain Editor или Scene Explorer, и выберите [ПК Мыши] файл вашей сцены в папке с вашими игровыми ресурсами(assets) и выберите Edit Terrain или Edit in SceneComposer.
  • После открытия [ПК Мыши] выберите корневой узел в SceneExplorer и затем выберите Add Spatial ▸ NavMesh.

Откроется диалоговое окно Create NavMesh с настройками по умолчанию. Вы можете подробно прочитать о каждом параметр, перейдя по ссылку Параметры конфигурации в разделе Требования.

Параметр Insight
Система jme3AI использует CritterAI, основанный на навигации Recast and Detour. Автор Recast излагает несколько конкретных правил для создания NavMesh в этом посте блога, которые логически применимы и к jme3AI. Ниже приведен перевод этого сообщения, поскольку оно относится и к jme3AI.

  • Сначала вы должны определить размер своего персонажа «capsule»(капсулы). Например, если вы используете метры в качестве единиц измерения в вашем игровом мире, то размер человеческого персонажа может быть (р)адиус = 0,4, (в)ысота = 2,0.
  • Затем из этого будет получена вокселизация ячейки размера(яр). Обычно хорошее значение для яр равно р/2 или р/3. В открытых пространствах(на улице) р/2 может быть достаточно, в помещении вам порой может быть нужна большая точность, и вы можете использовать р/3 или меньше.
  • Вокселизация ячейки высота(яв) определяется отдельно, чтобы обеспечить большую точность в тестах высоты. Хорошей отправной точкой для яв является яр/2. Если вы получаете небольшие отверстия, где есть разрывы в высоте(шагов), вы можете уменьшить значение ячейки высоты.
  • Далее определяются значения персонажа. Прежде всего minTraversableHeight, которое определяет высоту агента.
  • maxTraversableStep определяет, как высоко персонаж шагами может подняться.
  • Параметр traversableAreaBorderSize определяет радиус агента. Если это значение больше нуля, navmesh будет сокращен через traversableAreaBorderSize. Если вы хотите, чтобы navmesh плотно подходил, используйте нулевой радиус.
  • Параметр maxTraversableSlope используется перед вокселизацией, чтобы проверить, не слишком ли наклон треугольника высок, и тем полигонам чей наклон слишком сильный будет присвоен флаг не проходимый. Параметр имеет значение в радианах.
  • В некоторых случаях очень длинные внешние ребра могут уменьшать результаты триангуляции. Иногда это можно исправить, просто тесселлируя длинные рёбра. Параметр maxEdgeLength определяет максимальную длину ребра. Хорошее значение для maxEdgeLength, это что-то около traversableAreaBorderSize*8. Хороший способ настроить это значение, это сначала установить его действительно высоким и посмотреть, создают ли ваши данные длинные ребра. Если это так, попробуйте найти как можно большее значение при котором получается создать несколько дополнительных вершин, что бы сделать более лучшую тесселяцию.
  • Когда растрированные области преобразуются обратно в векторизованное представление, edgeMaxDeviation описывает, насколько свободно выполняется упрощение. Хорошие значения между 1.1-1.5 (1.3 обычно дают хорошие результаты). Если значение меньше, то некоторые края лестницы начинают появляться на гранях, а если оно больше, то упрощение начинает срезать некоторые углы.
Краткие описания параметров эффектов включены в комментарии файла NavMeshState.java и пояснения в примерах процедурного кода, которые описаны этом разделе.

Если есть проблемы с настройками параметров, вы узнаете об этом если NavMesh не отобразится под Узлом который вы для неё выбрали, и в области состояния нет выполняемой задачи, расположенном в правом нижнем углу SDK.

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

Наибольшее влияние на ваш NavMesh оказывает размер ячейки. Чем меньше размер ячейки, тем более точным является NavMesh, тем больше времени требуется для генерации. Генерация NavMesh 1024×1024 может занять от 30 секунд до десяти минут, в зависимости от сложности местности. Большие ровные NavMeshes могут занимать много часов.
Выбор узла NavMesh в SceneExplorer покажет NavMesh в редакторе Terrain Editor или рабочей области окна SceneComposer. Если он не отображается, с выбранным узлом NavMesh, измените Cull Hint на Never на панели NavMesh — Properties.

Процедурный метод

Существует много способов создания NavMesh. Если вы посмотрите на конструктор для файла Jme3AI.java, вы увидите, что я использую BaseAppState названый NavMeshState.java, который создает объект генератор и собирает новую версию NavMesh при каждом запуске программы.

Конструктор Jme3AI

public Jme3AI() {
    super(new StatsAppState(), new DebugKeysAppState(), new TerrainState(),
            new NavMeshState(), new PCState(), new KeyboardRunState());
}

Сборка NavMesh может занять от нескольких секунд до нескольких часов, в зависимости от того, насколько она сложна. Поэтому, вы обычно создаете NavMesh или сетки, добавляете их в свою папку Assets и загружаете их при запуске. Классы NavMeshState и NavMeshGenerator являются удобными классами, и не требуются для создания NavMesh. Если вы хотите, чтобы ваша игра минималистична, вы можете задать переменные для CritterAI NavmeshGenerator (обратите внимание на нижний регистр «m» у mesh) вызывая метод напрямую или по переменной и передавая IndexBuffer и VertexBuffer вашей сетки в CritterAI объекта NavmeshGenerator.

NavmeshGenerator nmgen = new NavmeshGenerator(cellSize, cellHeight, minTraversableHeight,
                maxTraversableStep, maxTraversableSlope,
                clipLedges, traversableAreaBorderSize,
                smoothingThreshold, useConservativeExpansion,
                minUnconnectedRegionSize, mergeRegionSize,
                maxEdgeLength, edgeMaxDeviation, maxVertsPerPoly,
                contourSampleDistance, contourMaxDeviation);
...
Получите буферы сетки и задайте IntermediateData
...

//Передайте буферы и IntermediateData для процесса сборки
TriangleMesh triMesh = nmgen.build(positions, indices, intermediateData);

...
Процесс trimesh
...

Давайте рассмотрим, что нужно для создания NavMesh с использованием вспомогательных классов NavMeshState и NavMeshGenerator.

Методы генерации NavMeshState NavMesh

/**
 * создание NavMesh
 */
private void createNavMesh() {
    generator = new NavMeshGenerator();
    //Разрешение ширины и глубины, используемое при выборке исходной геометрии.
    //outdoors = agentRadius/2, indoors = agentRadius/3, cellSize =
    //agentRadius для очень маленьких ячеек.
    //Constraints > 0 , default=1
    generator.setCellSize(.25f);
    //Разрешение по высоте, используемое при опробовании исходной геометрии.
    //minTraversableHeight, maxTraversableStep, и contourMaxDeviation
    //необходимо будет превышать значение cellHeight для правильной работы.
    //maxTraversableStep особенно восприимчив к воздействию от значения
    //cellHeight.
    //cellSize/2
    //Constraints > 0, default=1.5
    generator.setCellHeight(.125f);
    //Представляет минимальную высоту от пола до потолка, которая 
    //позволяет рассматривать пространство пола как всё еще проходимое.
    //minTraversableHeight должен быть как минимум в два раза больше значения
    //cellHeight, для того чтобы получить хорошие результаты. Максимальная 
    //высота spatial.
    //Constraints > 0, default=7.5
    generator.setMinTraversableHeight(2f);
    //Представляет максимальную высоту выступа, который считается все
    //еще проходимым.
    //maxTraversableStep должен быть больше в два раза значения cellHeight.
    //Constraints >= 0, default=1
    generator.setMaxTraversableStep(0.3f);
    //Максимальный наклон, который считается проходимым. (В градусах.)
    //Constraints >= 0, default=48
    generator.setMaxTraversableSlope(50.0f);
    //Указывает, следует ли считать выступы непроходимым.
    //Constraints None, default=false
    generator.setClipLedges(false);
    //Представляет ближайшую часть любой части сетки которая может быть
    //препятствием в исходной геометрии.
    //Значение traversableAreaBorderSize должно быть больше, чем cellSize,
    //чтобы был эффект. Радиус spatial.
    //Constraints >= 0, default=1.2
    generator.setTraversableAreaBorderSize(0.6f);
    //Количество сглаживания, которое должно выполняться при создании
    //distance field, используемого для получения областей.
    //Constraints >= 0, default=2
    generator.setSmoothingThreshold(0);
    //Применяет дополнительные алгоритмы для предотвращения образования
    // некорректных областей.
    //Constraints None, default=true
    generator.setUseConservativeExpansion(true);
    //Минимальный размер области для несвязанных (островных) облостей.
    //Constraints > 0, default=3
    generator.setMinUnconnectedRegionSize(8);
    //Любые области меньшего этого размера, по возможности, будут
    //объединены с более крупными областями.
    //Constraints >= 0, default=10
    generator.setMergeRegionSize(20);
    //Максимальная длина краев полигона, представляющих границу сетки.
    //setTraversableAreaBorderSize*8
    //Constraints >= 0, default=0
    generator.setMaxEdgeLength(4.0f);
    //Максимальное расстояние между краями сетки на которое можно отклоняться 
    //от исходной геометрии.
    //от 1.1 до 1.5 для наилучшего результата.
    //Constraints >= 0 , default=2.4
    generator.setEdgeMaxDeviation(1.3f);
    //Максимальное количество вершин на полигоне для полигонов,
    //генерируемых в процессе преобразования воксела в полигон.
    //Constraints >= 3, default=6
    generator.setMaxVertsPerPoly(6);
    //Устанавливает расстояние выборки для использования при 
    //сочетании деталированной сетки с поверхностью исходной геометрии.
    //Constraints >= 0, default=25
    generator.setContourSampleDistance(5.0f);
    //Максимальное расстояние поверхности деталированной сетки может
    //отклоняться от поверхности исходной геометрии.
    //Constraints >= 0, default=25
    generator.setContourMaxDeviation(5.0f);
    //Время, которое разрешенное выполнять процесса генерации в миллисекундах.
    //default=10000
    generator.setTimeout(40000);

    //Объект данных, который будет использоваться для хранения данных, 
    //связанных со сборкой навигационной сетки.
    IntermediateData data = new IntermediateData();
    generator.setIntermediateData(data);

    Mesh mesh = new Mesh();
    GeometryBatchFactory.mergeGeometries(findGeometries(app.getRootNode(),
            new LinkedList<>(), generator), mesh);

    //раскомментируйте что бы показать сетку
//        Geometry meshGeom = new Geometry("MeshGeometry");
//        meshGeom.setMesh(mesh);
//        showGeometry(meshGeom, ColorRGBA.Yellow);
//        saveNavMesh(meshGeom);

    Mesh optiMesh = generator.optimize(mesh);
    navMesh.loadFromMesh(optiMesh);

    Geometry geom = new Geometry(DataKey.NAVMESH);
    geom.setMesh(optiMesh);
    //отобразим сетку
    showGeometry(geom, ColorRGBA.Green);
    //сохраним navmesh в Scenes/NavMesh для последующих загрузок
    exportNavMesh(geom, DataKey.NAVMESH);
    //сохраним geom в rootNode если есть желание
    saveNavMesh(geom);
}

Сначала мы создаем объект NavMeshGenerator, а затем используем что бы задать параметры для NavMesh.

generator = new NavMeshGenerator();
...
generator.setCellSize(.25f);
...

На следующем шаге мы создаем объект IntermediateData.

//объект данных, который будет использоваться для хранения данных,
//связанных со сборкой навигационной сетки.
IntermediateData data = new IntermediateData();
generator.setIntermediateData(data);

Объект IntermediateData может использоваться для получения информации о процессе сборки NavMesh, например, времени сборки. Вы запрашиваете этот объект после сборки NavMesh. Если вы не хотите видеть данные, задайте для него значение null.

И к этому моменту у вас теперь есть объект generator, который вы используете для создания NavMesh.

В файл NavMeshState.java входит вспомогательный метод findGeometries.

//Собирает все геометрии в поставляемом узле в список List. Использует 
//NavMeshGenerator для объединения найденных Terrain сеток в одну геометрию перед 
//добавлением. Масштабируйте и задавайте перемещение объединенных геометрий.
private List<Geometry> findGeometries(Node node, List<Geometry> geoms,
          NavMeshGenerator generator)

Он используется для сбора всех геометрий, прикрепленных к узлу, в список. Если узел потомок какого-то узла является экземпляром Terrain (который может состоять из многих сеток), он будет использовать объект generator, чтобы объединить их в одну сетку, затем масштаб и заданное перемещение объединенной сетки перед добавлением в список. Затем вы используете GeometryBatchFactory для объединения всех геометрий в списке в один объект mesh.

Mesh mesh = new Mesh();
GeometryBatchFactory.mergeGeometries(findGeometries(app.getRootNode(),
        new LinkedList<>(), generator), mesh);

После выполнения этих методов у вас есть один объект mesh, который теперь готов к оптимизации.

Mesh optiMesh = generator.optimize(mesh);

Здесь параметры, заданные с помощью объекта generator, применяются к поставляемой сетке. Метод optimize возвращает новый объект mesh, который отражает настройки вашего генератора. Теперь, когда любые проблемы с вашими параметрами будут отображаться как предупреждения или исключения. Вы должны продолжать изменять различные параметры, по одному за раз и с небольшими приращениями/сокращениями, пока ваша mesh не будет генерироваться без ошибок. Смотрите примечания к каждому параметру в качестве указания о том, как это сделать.

После генерирования сетки вам необходимо связать все её ячейки вместе, чтобы он мог использоваться как ваш объект NavMesh. Вы делаете это, вызывая loadFromMesh() или loadFromData(), в зависимости от вашей реализации, в вашем объекте optiMesh.

navMesh.loadFromMesh(optiMesh);

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

public NavMesh(Mesh mesh) {
  loadFromMesh(mesh);
}

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

Geometry geom = new Geometry(DataKey.NAVMESH);
geom.setMesh(navMesh);

Теперь, когда у вас есть ваша Сетка, вы должны ее сохранить.

//сохраните navmesh в Scenes/NavMesh для загрузки
exportNavMesh(geom, DataKey.NAVMESH);
//сохраните geom в rootNode если хотите
saveNavMesh(geom);

В этом случае объект экспортируется в папку Assets, поэтому он может загружаться, а не генерироваться каждый раз, когда начинается игра. Это предпочтительный метод. Метод saveNavMesh() просто привязывает геометрию к rootNode. Как и где вы хотите сохранить, зависит от вашей реализации и личных предпочтений.

Pathfinding

Существует много способов реализовать класс NavMeshPathfinder библиотеки jme3AI. Вы можете создать control, создающий экземпляр класса NavMeshPathFinder и запросить вновь созданный объект в потоке. Вы можете использовать один AppState для расчета всех ваших путей. Вы могли бы, как и в этом уроке, расширить класс NavMeshPathFinder в пользовательском control.

Вам также нужен способ передачи изменений Vector3f в NavMeshPathfinder. В этом уроке используется ActionListener и Interface. Вы так же можете просто создать public метод в control и вызвать его из ActionListener, либо сохранить Vector3f в UserData и искать изменения из самого control.

Эти решения по реализации, остаются за вами.

Загрузка NavMesh

В этом примере урока оптимизированная сетка была экспортирована как геометрия с использованием двоичного формата jMonkey .j3o. Это означает, что загрузка вашего NavMeshes выполняется так же, как вы загружаете любую модель, используя AssetManager. Когда вы загружаете .j3o, вы захватываете его Mesh и создаете объект NavMesh, который должен быть передан конструктору NavigationControl. В этом уроке используется BaseAppState для загрузки модели, поэтому встроен доступ к классу Application.

//загрузим геометрию NavMesh сохраненную в папке assets
Geometry navGeom = (Geometry) getApplication().getAssetManager().
        loadModel("Scenes/NavMesh/NavMesh.j3o");
NavigationControl navControl = new NavigationControl(new NavMesh(
        navGeom.getMesh()), getApplication(), true)
charNode.addControl(navControl);
//NavigationControl реализует интерфейс Pickable
picked = navControl;
В этом уроке используется пользовательский control NavigationControl, который расширяет класс NavMeshPathfinder. Поскольку этот урок, некоторые дополнительные переменные используются для отображения, навигационные пути и не нужны. Для конструктора NavMeshPathfinder требуется только passing объекта NavMesh, который сделан для более чистого control.

public NavigationControl(NavMesh navMesh) {
  ...
}

Общение с NavigationControl

В этом уроке используются материал из уроков Hello Picking и Выбор Мышью, поэтому вы должны быть уже хорошо знакомы с методами осуществления выбора и добавлением сопоставлений ввода в вашу игру. Как вы реализуете свой ActionListener зависит от вас.

PCState ActionListener

    private class ClickedListener implements ActionListener {

        @Override
        public void onAction(String name, boolean isPressed, float tpf) {

            if (name.equals(ListenerKey.PICK) && !isPressed) {
                CollisionResults results = new CollisionResults();
                Vector2f click2d = getInputManager().getCursorPosition().clone();
                Vector3f click3d = app.getCamera().getWorldCoordinates(click2d,
                        0f).clone();
                Vector3f dir = app.getCamera().getWorldCoordinates(
                        click2d, 1f).subtractLocal(click3d).normalizeLocal();
                Ray ray = new Ray(click3d, dir);
                app.getRootNode().collideWith(ray, results);

                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.");
                }

                if (results.size() > 0) {
                    // Самое близкое столкновение то, во что действительно попало:
                    CollisionResult closest = results.getClosestCollision();
                    // Давайте взаимодействовать - мы отмечаем попадание красной точкой.
                    mark.setLocalTranslation(closest.getContactPoint());
                    app.getRootNode().attachChild(mark);
                    picked.setTarget(closest.getContactPoint());
                    System.out.println("  Closest Contact " + closest.
                            getContactPoint());
                } else {
                    // Нет попаданий? Затем удалим красную метку.
                    app.getRootNode().detachChild(mark);
                }
            }
        }
    }

Главная строка которая нам интересна здесь,

picked.setTarget(closest.getContactPoint());

где picked ссылочный объект, используемый для передачи наших изменений Vector3f в NavigationControl.

//NavigationControl реализует интерфейс Pickable
picked = navControl;

На этом этапе вы загрузили свой NavMesh, добавили NavigationControl в свою spatial структуру и установили метод для связи с NavMeshPathFinder. Далее мы рассмотрим детали NavigationControl.

NavigationControl

NavigationControl — это пользовательский control, который расширяет класс NavMeshPathFinder библиотеки Jme3AI и реализует интерфейс Pickable.

public class NavigationControl extends NavMeshPathfinder implements Control,
        JmeCloneable, Pickable {
}

Интерфейс Pickable прост, и его единственной целью в этой реализации является передача изменений, внесенных в выбираемую цель.

Реализация Интерфейса Pickable

/**
 * @param target задать цель
 */
@Override
public void setTarget(Vector3f target) {
    this.target = target;
}

Сердце вашего control находится в потоке pathfinding, который делает вызовы метода computePath(). Потенциально длительно выполняемые задачи всегда должны выполняться из потока. Ниже приведен конструктор, который вы обычно используете для создания экземпляра вашего control.

public NavigationControl(NavMesh navMesh) {
    super(navMesh); //задать NavMesh для этого control
    executor = Executors.newScheduledThreadPool(1);
    startPathFinder();
}

Во-первых, вы вызываете super(navMesh);, чтобы задать NavMesh для control,, затем настройте ExecutorService и запустите поток pathfinding.

Это реализация пользовательского потока, поэтому вам реализовывать её закрытие. Это делается в методе setSpatial().

if (spatial == null) {
    shutdownAndAwaitTermination(executor);
    ...
} else {
    ...
}

Процесс завершения работы Executor

//стандартный процесс завершения executor
private void shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown(); // Отключение новых заданий из being submitted
    try {
        // Подождите, пока существующие задачи завершатся
        if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // Отмена выполняемых в настоящее время задач
            // Подождать, пока задачи не станут откликаться на отмену
            if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
                LOG.log(Level.SEVERE, "Pool did not terminate {0}", pool);
            }
        }
    } catch (InterruptedException ie) {
        // (Повторно-)Отменить, если текущий поток также прерван
        pool.shutdownNow();
        // Сохранять статус прерывания
        Thread.currentThread().interrupt();
    }
}

Самый простой способ перемещать персонажа с физикой, это использовать класс BetterCharacterControl. В этой реализации это делается в классе PCControl, расширяющий BetterCharacterControl. Поскольку BetterCharacterControl должен присутствовать в spatial для pathfinding, в методе setSpatial() мы выставляем исключение, чтобы оно сообщало нам, если он отсутствует.

if (spatial == null) {
    ...
} else {
    pcControl = spatial.getControl(PCControl.class);
    if (pcControl == null) {
        throw new IllegalStateException(
                "Cannot add NavigationControl to spatial without PCControl!");
    }
}

Поток Pathfinding

Поток pathfinding NavigationControl

//Вычисляет путь с использованием алгоритма A*. Каждые 1/2 секунды проверяет цель 
//для обработки. Прежний путь будет оставаться до тех пор пока не будет сгенерирован новый.
private void startPathFinder() {
    executor.scheduleWithFixedDelay(() -> {
        if (target != null) {
            clearPath();
            setWayPosition(null);
            pathfinding = true;
            //setPosition должен быть задан перед вызовом computePath.
            setPosition(spatial.getWorldTranslation());
            //warpInside(target) всегда перемещает конечную точку внутри navMesh.
            warpInside(target);
            System.out.println("Target " + target);
            boolean success;
            //вычислить путь
            success = computePath(target);
            System.out.println("SUCCESS = " + success);
            if (success) {
                //очистить цель, если она успешна
                target = null;
                ...
            }
            pathfinding = false;
        }
    }, 0, 500, TimeUnit.MILLISECONDS);
}

То как вы настраиваете ваш поток pathfinding, имеет существенное значение.

executor.scheduleWithFixedDelay(() -> {
...
}, 0, 500, TimeUnit.MILLISECONDS);

Этот ExecutorService настроен на немедленное начало (0), с фиксированной задержкой (500) миллисекунд. Это означает, что задача имеет фиксированную задержку в 1/2 секунды между окончанием выполнения и началом следующего выполнения, то есть не учитывает фактическую продолжительность задачи. Если вы будете использовать scheduleAtFixedRate(), вы рискуете, что задача не будет завершена в назначенное время.

Когда вы используете BetterCharacterControl, все, что требуется для перемещения spatial, это ваш setWalkDirection(), и spatial будет непрерывно двигаться в этом направлении. Следующая разбивка кода объясняет, как это использует NavigationControl.

Он начинает с того, что поток pathfinding проверяет переменную target для изменений.

if (target != null) {
    ...
}

Если он найдет цель(target), он вычислит новый путь к этой target и в случае успеха обновит переменную пути NavMeshPathfinder. Цикл update() control непрерывно проверяет эту переменную пути, и если ее значение не равно null, выполняется соответствующее действие.

Прежде чем вы начнете вычислять путь, сначала очистите существующий путь и задайте wayPosition равным null.

if (target != null) {
    clearPath();
    setWayPosition(null);
    pathfinding = true;
    ...
}

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

Затем вы должны вызвать setPosition() перед вызовом метода computePath().

if (target != null) {
  ...
  setPosition(spatial.getWorldTranslation());
  ...
  //вычислить путь
  success = computePath(target);
  ...
}

Есть некоторые вещи, которые вам нужно знать о том, как вычисляется путь.

  • Первой точкой пути(waypoint) на любом пути является та, которую вы установили с помощью setPosition().
  • Последняя точкой пути на любом пути всегда является Vector3f цели.
  • computePath() добавляет одну точку пути к ближайшей от цели ячейки, только если вы не находитесь в goalCell (находитесь в ячейке цели), и если есть ячейка между первой и последней точкой пути, и если это не прямая линии
  • Если внутри goalCell, когда выбрана новая цель, computePath() будет выполнять прямую линию до размещения цели. Это означает, что будут заданы только две точки пути, setPosition() и target.
  • Если target находится вне NavMesh, то и ваша конечная точка будет тоже.

Чтобы гарантировать, что target всегда находится внутри NavMesh, вызовите

if (target != null) {
    ...
    //warpInside(target) всегда перемещает конечную точку внутри navMesh.
    warpInside(target);
    ...
    //вычислить путь
    success = computePath(target);
    ...
}

перед вызовом computePath(), и конечная точка пути будет перемещена в ближайшую ячейку к target, находящейся внутри NavMesh.

Движение персонажа

цикл update() NavigationControl

@Override
public void update(float tpf) {
    if (getWayPosition() != null) {
        Vector3f spatialPosition = spatial.getWorldTranslation();
        Vector2f aiPosition = new Vector2f(spatialPosition.x,
                spatialPosition.z);
        Vector2f waypoint2D = new Vector2f(getWayPosition().x,
                getWayPosition().z);
        float distance = aiPosition.distance(waypoint2D);
        //перемещать персонаж между точками пути(waypoint) до тех пор, пока не будет 
        //достигнута точка пути, а затем задайте null
        if (distance > .25f) {
            Vector2f direction = waypoint2D.subtract(aiPosition);
            direction.mult(tpf);
            pcControl.setViewDirection(new Vector3f(direction.x, 0,
                    direction.y).normalize());
            pcControl.onAction(ListenerKey.MOVE_FORWARD, true, 1);
        } else {
            setWayPosition(null);
        }
    } else if (!isPathfinding() && getNextWaypoint() != null
            && !isAtGoalWaypoint()) {
        if (showPath) {
            showPath();
            showPath = false;
        }
        //перейти к следующей waypoint
        goToNextWaypoint();
        setWayPosition(new Vector3f(getWaypointPosition()));

        //задать spatial с физикой позицию
        if (getPositionType() == EnumPosition.POS_STANDING.position()) {
            setPositionType(EnumPosition.POS_RUNNING.position());
            stopFeetPlaying();
            stopTorsoPlaying();
        }
    } else {
        //waypoint null значит остановить движение и задать spatials с физикой позицию
        if (getPositionType() == EnumPosition.POS_RUNNING.position()) {
            setPositionType(EnumPosition.POS_STANDING.position());
            stopFeetPlaying();
            stopTorsoPlaying();
        }
        pcControl.onAction(ListenerKey.MOVE_FORWARD, false, 1);
    }
}

Если computePath() успешно вычисляет новый путь, то переменная пути для NavMeshPathfinder больше не будет равна null. Цикл обновления NavigationControl проверяет эту переменную пути, каждая итерация, которой wayPosition, равный null, вызывающий методом getNextWaypoint(). Если путь имеет другую точку пути(waypoint), он переместится в следующую позицию в пути и установит в качестве этой позиции переменную wayPosition в NavigationControl.

} else if (!isPathfinding() && getNextWaypoint() != null
        && !isAtGoalWaypoint()) {
    ...
    //перейти к следующему waypoint
    goToNextWaypoint();
    setWayPosition(new Vector3f(getWaypointPosition()));
    ...
}
Помните, что первая точка пути(waypoint) в пути всегда является текущей позицией spatials. Вот почему вы всегда двигаетесь из первой позиции.

На следующей итерации метода update() controls он видит, что wayPosition больше не равен null и вычисляет расстояние от текущей позиции spatials до wayPosition.

if (getWayPosition() != null) {
    Vector3f spatialPosition = spatial.getWorldTranslation();
    Vector2f aiPosition = new Vector2f(spatialPosition.x,
            spatialPosition.z);
    Vector2f waypoint2D = new Vector2f(getWayPosition().x,
            getWayPosition().z);
    float distance = aiPosition.distance(waypoint2D);
    ...
}

Если это больше заданного расстояния, оно будет setViewDirection() в PCControl (которое расширяет BetterCharacterControl), а затем уведомляет PCControl о том, что spatial может перемещаться вызовов напрямую control метода onAction().

if (getWayPosition() != null) {
    ...
        //перемещать персонаж между точками пути(waypoint) до тех пор, пока не будет 
        //достигнута точка пути, а затем задайте null
    if (distance > .25f) {
        Vector2f direction = waypoint2D.subtract(aiPosition);
        direction.mult(tpf);
        pcControl.setViewDirection(new Vector3f(direction.x, 0,
                direction.y).normalize());
        pcControl.onAction(ListenerKey.MOVE_FORWARD, true, 1);
    } else {
        ...
    }
}

Это зависит от NavigationControl который определяет, когда персонаж должен перестать двигаться. Каждый раз, когда spatial достигает точки, которая меньше заданного расстояния, она устанавливает wayPosition равным null.

if (distance > .25f) {
    ...
} else {
    setWayPosition(null);
}

Если позиция пути еще не достигла конца, оно снова будет продвигаться к следующей точке пути в пути и обновлять wayPosition.

} else if (!isPathfinding() && getNextWaypoint() != null
        && !isAtGoalWaypoint()) {
    ...
    //перейти к следующему waypoint
    goToNextWaypoint();
    setWayPosition(new Vector3f(getWaypointPosition()));
    ...
}

Когда достигается последняя точка пути, NavigationControl уведомляет PCControl о том, что spatial больше не может перемещаться.

} else {
    ...
    pcControl.onAction(ListenerKey.MOVE_FORWARD, false, 1);
}

Класс PCControl обрабатывает фактическое перемещение spatial в цикле update(). Он делает это, проверяя переменную forward каждую итерацию. Эта переменная задается при вызове метода onAction() из цикла обновления NavigationControl.

PCControl ActionListener

@Override
public void onAction(String name, boolean isPressed, float tpf) {
    if (name.equals(ListenerKey.MOVE_FORWARD)) {
        forward = isPressed;
    }
}

цикл обновления PCControl

@Override
public void update(float tpf) {
    super.update(tpf);
    this.moveSpeed = 0;
    walkDirection.set(0, 0, 0);
    if (forward) {
        Vector3f modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z);
        position = getPositionType();
        for (EnumPosition pos : EnumPosition.values()) {
            if (pos.position() == position) {
                switch (pos) {
                    case POS_RUNNING:
                        moveSpeed = EnumPosition.POS_RUNNING.speed();
                        break;
                    default:
                        moveSpeed = 0f;
                        break;
                }
            }
        }
        walkDirection.addLocal(modelForwardDir.mult(moveSpeed));
    }
    setWalkDirection(walkDirection);
}

Затем PCControl задаст направление ходьбы, основанное на повороте spatial в мире, и задаст скорость.

Заключение

Целью этого урока было дать вам общее описание того, как работает навигационная система Jme3AI, а также продемонстрировать, насколько гибкой является ее реализация. Весь код в этом уроке является бесплатным для вашего использования и может быть найден в репозитории документации jme3. Конструкция реализации такова, что вы можете легко изменить каждый из параметров, а затем визуально увидеть, как они влияют на NavMesh. Если у вас есть вопросы или предложения по улучшению этого урока, вы можете сделать это на форуме jMonkeyEngine.

Другие опции ИИ

Существуют и другие специальные параметры jME3, о которых вы можете прочитать в вики под темой Искусственный интеллект(ИИ).

Дальнейшее чтение


    1. path-finding(Поиск путей) — означает вычисление кратчайшего маршрута между двумя точками. Обычно лабиринты.

    2. path-following(Следование путём) — берем путь который уже существует и затем следуем по нему.

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

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

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