В первой части мы разобрали основы 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}
]
}))
Как это работает:
NPC сначала проверяет, близко ли игрок (distance < 3).
Если да — преследует (chase_player).
Если нет — патрулирует (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. Что дальше?
Добавьте путьfinding через ursina.pathfinding для обхода препятствий.
Создайте диалоговую систему — NPC могут реагировать на слова игрока.
Экспериментируйте с нейросетями (подключите 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()