Ursina: Создание умных NPC через поведенческие деревья (Часть 2)

от автора

<— Прошлая статья

В первой части мы разобрали основы Ursina и создали простую 3D-игру. Теперь перейдем к более сложной механике — искусственному интеллекту для NPC с помощью поведенческих деревьев (Behavior Trees).


1. Что такое поведенческие деревья?

Поведенческое дерево — это структура, которая определяет логику действий NPC. В отличие от простых скриптов, оно позволяет:

  • Гибко комбинировать условия и действия

  • Легко масштабировать ИИ-логику

  • Избегать спагетти-кода в сложных сценариях

Пример простого дерева для NPC-стража:

Поведение охранника:   ├─ Если видит игрока → Атаковать   ├─ Если слышит шум → Исследовать   └─ Иначе → Патрулировать  

2. Подключаем AI в Ursina

Ursina включает модуль Behavior для работы с поведенческими деревьями.

Шаг 1: Создаем базового NPC

from ursina import *   from ursina.prefabs.ai import Behavior    app = Ursina()    # NPC - это просто куб с "мозгами"   npc = Entity(model='cube', color=color.red, position=(3, 0, 0))  

Шаг 2: Добавляем поведение

# Определяем действия NPC   def patrol():       npc.x += time.dt * 2  # Движется вправо       if npc.x > 5:           npc.x = -5  # Возвращается на старт    def chase_player():       npc.position += (player.position - npc.position).normalized() * time.dt * 3    # Создаем дерево поведения   npc.add_script(Behavior({       'sequence': [           {'condition': lambda: distance(npc, player) < 3, 'action': chase_player},           {'action': patrol}       ]   }))  

Как это работает:

  1. NPC сначала проверяет, близко ли игрок (distance < 3).

  2. Если да — преследует (chase_player).

  3. Если нет — патрулирует (patrol).


3. Сложные сценарии

Пример: NPC с тремя состояниями

states = {       'calm': {           'action': lambda: setattr(npc, 'color', color.green),           'transition': [               {'condition': lambda: distance(npc, player) < 4, 'next_state': 'alert'},           ]       },       'alert': {           'action': lambda: setattr(npc, 'color', color.yellow),           'transition': [               {'condition': lambda: distance(npc, player) < 2, 'next_state': 'angry'},               {'condition': lambda: distance(npc, player) > 5, 'next_state': 'calm'},           ]       },       'angry': {           'action': chase_player,           'transition': [               {'condition': lambda: distance(npc, player) > 4, 'next_state': 'alert'},           ]       }   }    npc.add_script(Behavior({'state_machine': states}))  

Логика:

  • 🟢 Спокойный (зеленый): игрок далеко → NPC стоит.

  • 🟡 Настороженный (желтый): игрок приближается → NPC поворачивается к нему.

  • 🔴 Агрессивный (красный): игрок слишком близко → погоня!


4. Продвинутые техники

4.1. Работа с памятью NPC

Чтобы NPC «запоминал» игрока даже когда тот скрылся:

npc.memory = {'last_seen_player_pos': None}    def remember_player():       if distance(npc, player) < 3:           npc.memory['last_seen_player_pos'] = player.position    Behavior({       'action': remember_player,       'sequence': [           {'condition': lambda: npc.memory['last_seen_player_pos'] is not None,            'action': lambda: npc.look_at(npc.memory['last_seen_player_pos'])},           {'action': patrol}       ]   })  

4.2. Групповое поведение

Создаем стаю NPC, которая атакует вместе:

enemies = [Entity(model='cube', color=color.red) for _ in range(5)]    for enemy in enemies:       enemy.add_script(Behavior({           'parallel': [               {'condition': lambda e=enemy: distance(e, player) < 4, 'action': chase_player},               {'action': lambda e=enemy: e.look_at_2d(player)}           ]       }))  

5. Оптимизация производительности

  • Используйте distance_squared вместо distance для проверок (избегаем квадратного корня).

  • Ограничивайте частоту проверок через time.dt:

    def update():       if time.time() % 1.0 < time.dt:  # Проверяем раз в секунду           npc.bt.update()  

6. Что дальше?

  1. Добавьте путьfinding через ursina.pathfinding для обхода препятствий.

  2. Создайте диалоговую систему — NPC могут реагировать на слова игрока.

  3. Экспериментируйте с нейросетями (подключите TensorFlow для обучения NPC).

Совет: Для сложных проектов используйте визуальный редактор поведенческих деревьев Behavior Tree Editor.

Итог: С Ursina даже сложный ИИ становится доступным. Начните с простых патрулей, а затем создавайте NPC с характером!

P.S. Попробуйте сделать NPC, который:

  • Прячется за укрытиями

  • Подбирает предметы

  • Общается с другими NPC

Какой вариант реализуете первым? 🚀

пример игры
from ursina import * import random  app = Ursina()  # Настройки игры player_speed = 5 npc_speed = 2.5 collectibles_count = 10 score = 0  # Создаем игрока player = Entity(     model='cube',     color=color.orange,     scale=(1, 1, 1),     collider='box' )  # Создаем землю ground = Entity(     model='plane',     texture='grass',     scale=(20, 1, 20),     collider='box' )  # Создаем NPC npc = Entity(     model='cube',     color=color.red,     position=(5, 0, 5),     collider='box' )  # Создаем собираемые предметы collectibles = [] for i in range(collectibles_count):     collectible = Entity(         model='sphere',         color=color.yellow,         scale=0.5,         position=(random.uniform(-8, 8), 0.5, random.uniform(-8, 8)),         collider='sphere'     )     collectibles.append(collectible)  # UI для счета score_text = Text(text=f'Score: {score}', position=(-0.8, 0.4), scale=2)  # Логика ИИ NPC def update_npc():     if distance(npc, player) < 6:         npc.look_at(player)         npc.position += npc.forward * time.dt * npc_speed     else:         npc.rotation_y += 20 * time.dt         npc.position += npc.forward * time.dt * (npc_speed / 2)  # Камера с фиксированным углом camera.position = (0, 15, -20)  # Высота и отдаление camera.rotation_x = 30          # Наклон камеры вниз  def update():     global score          # Движение игрока     player.x += held_keys['d'] * time.dt * player_speed     player.x -= held_keys['a'] * time.dt * player_speed     player.z += held_keys['w'] * time.dt * player_speed     player.z -= held_keys['s'] * time.dt * player_speed          # Вращение игрока (убрали Q/E, чтобы камера не "срывалась")          # Обновление ИИ NPC     update_npc()          # Камера следует за игроком с плавным смещением     camera.position = (         player.x,          15,                   # Фиксированная высота         player.z - 20          # Отдаление по Z     )     camera.look_at(player)     # Камера всегда направлена на игрока          # Проверка сбора предметов     for collectible in collectibles[:]:         if player.intersects(collectible).hit:             collectibles.remove(collectible)             destroy(collectible)             score += 1             score_text.text = f'Score: {score}'                          if random.random() > 0.3:                 new_collectible = Entity(                     model='sphere',                     color=color.yellow,                     scale=0.5,                     position=(random.uniform(-8, 8), 0.5, random.uniform(-8, 8)),                     collider='sphere'                 )                 collectibles.append(new_collectible)          if player.intersects(npc).hit:         print("Game Over!")         application.pause()  app.run()


ссылка на оригинал статьи https://habr.com/ru/articles/898360/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *