Pull to refresh

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

Level of difficultyEasy
Reading time4 min
Views724

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

В первой части мы разобрали основы 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()

Tags:
Hubs:
Total votes 3: ↑1 and ↓2-1
Comments0

Articles