В этом руководстве мы разберёмся, как в игровом движке Ursina определять расстояние между объектами. Это очень важно для создания интерактивных игр: чтобы враги замечали игрока, предметы можно было подбирать, а интерфейс реагировал на действия пользователя.

Основные функции для определения расстояния

В Ursina есть три основные функции для вычисления расстояния. Они различаются тем, какие координаты учитывают.

1. distance() — полное 3D расстояние

Эта функция вычисляет расстояние между центрами двух объектов в трёхмерном пространстве (по осям X, Y и Z).

from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
app = Ursina()
# Создаем землю для бега
ground = Entity(model='plane',texture='grass',scale=20,collider='box')

# Создаем главного героя (FirstPersonController) над землей
player = FirstPersonController(position=(0, 2, 0))

# Создаем сферу (синяя сфера)
sphere = Entity(position=(3, 4, 0), model='sphere', color=color.blue)

# Добавляем небо для лучшей атмосферы
Sky()

def update():
    # Измеряем расстояние от главного героя до сферы
    dist = distance(player, sphere)
    print(f"Расстояние от героя до сферы: {dist:.2f}")

app.run()

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

Функция берёт мировые координаты объектов и считает расстояние по формуле:

sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2 + (z_2 - z_1)^2}

Это классическое евклидово расстояние в 3D.

2. distance_2d() — 2D расстояние (игнорирует Z)

Эта функция считает расстояние только по осям X и Y, как на плоскости. Координата Z (глубина) не учитывается.

from ursina import * 
from ursina.prefabs.first_person_controller import FirstPersonController 
app = Ursina() 
# Создаем землю для бега 
ground = Entity( 
    model='plane', 
    texture='grass', 
    scale=20, 
    collider='box' 
) 
# Создаем главного героя (FirstPersonController) над землей 
player = FirstPersonController(position=(0, 2, 0)) 

# Создаем сферу (синяя сфера) 
sphere = Entity(position=(3, 4, 0), model='sphere', color=color.blue) 

# Добавляем небо для лучшей атмосферы 
Sky() 

def update(): 
    # Измеряем 2D расстояние от главного героя до сферы (только X и Y) 
    dist = distance_2d(player, sphere) 
    print(f"2D расстояние от героя до сферы: {dist:.2f}") 

app.run()

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

Игнорируется высота по оси Z. Это удобно для интерфейсов (UI) или когда объекты всегда находятся на одной плоскости.

3. distance_xz() — горизонтальное расстояние (игнорирует Y)

Эта функция считает расстояние только по осям X и Z, то есть по «земле». Высота (ось Y) не учитывается.

from ursina import * 
from ursina.prefabs.first_person_controller import FirstPersonController 
app = Ursina() 
# Создаем землю для бега 
ground = Entity( 
    model='plane', 
    texture='grass', 
    scale=20, 
    collider='box' 
) 

# Создаем главного героя (FirstPersonController) над землей 
player = FirstPersonController(position=(0, 2, 0)) 

# Создаем сферу (синяя сфера) 
sphere = Entity(position=(3, 4, 0), model='sphere', color=color.blue) 

# Добавляем небо для лучшей атмосферы 
Sky() 

def update(): 
    # Измеряем горизонтальное расстояние от героя до сферы (только X и Z) 
    dist = distance_xz(player, sphere) 
    print(f"Горизонтальное расстояние от героя до сферы: {dist:.2f}") 

app.run()

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

Игнорируется высота (Y). Это полезно для наземных персонажей: например, чтобы враг «увидел» игрока, даже если один стоит на холме, а другой внизу.

4. Как работает intersects()

Функция intersects() в Ursina предназначена для проверки столкновений между объектами, у которых есть коллайдеры (collider). Это основной способ узнать, соприкоснулись ли два объекта в игре.

Как это работает внутри

Проверка коллайдера: Сначала функция убеждается, что у объекта действительно есть коллайдер (например, box, sphere, mesh) и что свойство collision установлено в значение True. Если коллайдера нет, проверка не проводится.

Система столкновений: Для обнаружения пересечений Ursina использует мощную систему Panda3D's CollisionTraverser. Она «просматривает» сцену и ищет пересечения между физическими моделями объектов.

Фильтрация: Система автоматически исключает из проверки объекты, которые указаны в списке игнорирования (например, чтобы пуля не сталкивалась с тем, кто её выпустил), а также объекты без коллайдеров.

Результат: Если столкновение обнаружено, функция возвращает объект HitInfo. Это специальный контейнер с подробной информацией о контакте:hit: True/False — было ли вообще столкновение.

entity — ссылка на объект, с которым произошло столкновение.

point — точные координаты точки столкновения в пространстве.

distance — расстояние от центра вашего объекта до точки столкновения.

Примеры использования intersects()

Вот несколько практических примеров с подробными комментариями для начинающих.

Пример 1: Простое обнаружение стены

Допустим, у нас есть игрок и стена. Мы хотим узнать, врезался ли игрок в стену.

from ursina import *
app = Ursina()
# Создаем игрока (куб) и стену (плоскость)
player = Entity(model='cube', color=color.orange, collider='box')
wall = Entity(model='cube', position=(5,0,0), scale=(1,5,5), collider='box', color=color.red)

def update():
    # Двигаем игрока вперед
    player.x += 0.1 * time.dt
    # Проверяем столкновение
    hit_info = player.intersects()
    # Если hit_info.hit равно True, значит игрок во что-то врезался
    if hit_info.hit:
        print("Столкновение!")
        print(f"С кем: {hit_info.entity}")
        print(f"Точка удара: {hit_info.point}")
        print(f"Расстояние до точки удара: {hit_info.distance:.2f}")

app.run()

Комментарий:

Здесь мы каждую секунду проверяем, не пересекся ли куб игрока с кубом стены. Как только это происходит, в консоль выводится информация о том, с чем именно произошло столкновение и где именно.

Пример 2: Подбор предмета (альтернатива distance)

Вместо того чтобы измерять расстояние до предмета, можно использовать коллайдер для более точного определения «касания».

from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
app = Ursina()
ground = Entity(model='plane', texture='grass', scale=10, collider='box')
player = FirstPersonController(model='cube', origin_y=-.5)
pickup = Entity(model='sphere', position=(1,.5,3), collider='box')

def update():
    # Проверяем столкновение игрока с предметом
    hit_info = player.intersects()
    # Если мы коснулись предмета и это именно наш pickup...
    if hit_info.hit and hit_info.entity == pickup:
        print('Предмет подобран!')
        destroy(pickup) # Удаляем предмет со сцены

app.run()

Комментарий:

Этот метод надежнее простого измерения расстояния (distance()), потому что он учитывает реальную форму объекта. Если предмет имеет сложную форму (например, mesh), intersects() сработает точнее.

ВАЖНО: intersects() требует чтобы у обоих объектов были коллайдеры.

Пример 3: Пуля и враг

Классический пример для шутеров: пуля должна наносить урон врагу только при попадании.

import random
from ursina import *
app = Ursina()

class Bullet(Entity):
    def __init__(self, position, direction):
        super().__init__(
            model='sphere',
            scale=.1,
            speed=15,
            collider='sphere',
            position=position,
            color=color.yellow,
            name='bullet'
        )
        self.direction = direction.normalized()

    def update(self):
        self.position += self.direction * time.dt * self.speed
        hit_info = self.intersects(ignore=(self,))
        if hit_info.hit:
            print(f"Пуля попала в {hit_info.entity.name}!")
            destroy(self)

class Enemy(Entity):
    def __init__(self):
        super().__init__(
            model='cube',
            color=color.red,
            scale=1,
            collider='box',
            name='enemy'
        )
        self.x = random.choice([-4, 4])
        self.z = random.choice([-4, 4])

    # Создаем врагов
for i in range(5):
    Enemy()
# Создаем игрока для стрельбы
player = Entity(model='cube', color=color.blue, position=(0, 0, 0))
shoot_timer = 0

def update():
    global shoot_timer
    shoot_timer += time.dt
    # Автоматически стреляем каждую секунду
    if shoot_timer >= 1:
        shoot_timer = 0
        # Генерируем случайное направление
        random_direction = Vec3(
            random.uniform(-1, 1),
            random.uniform(-1, 1),
            random.uniform(-1, 1)
        ).normalized()
        # Создаем пулю
        bullet = Bullet(
            position=player.position,
            direction=random_direction
        )

app.run()

Комментарий:

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

ВАЖНО: В реальной игре лучше использовать raycast() для мгновенного попадания вместо физических пуль

Пример 4. Система подбора предметов

Когда игрок подходит к предмету достаточно близко, его можно подобрать. Расстояние определяет момент взаимодействия.

from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController
app = Ursina()
ground = Entity(model='plane', texture='grass', scale=10, collider='box')
player = FirstPersonController(model='cube', origin_y=-.5, color=color.orange, has_pickup=False)
pickup = Entity(model='sphere', position=(1,.5,3))

def update():
    if not player.has_pickup and distance(player, pickup) < pickup.scale_x / 2:
        print('Предмет подобран!')
        player.has_pickup = True
        pickup.animate_scale(0, duration=.1)
        destroy(pickup, delay=.1)

app.run()

Почему важно: Без проверки расстояния предмет можно было бы подобрать с любого места карты. Расстояние создаёт реалистичную зону взаимодействия.

Пояснение: Для кода выше необходимо проходить над самим предметом, а закрыть программу можно с помощью клавиши на клавиатуре «Пуск» или прописать в коде закрытие по нажатию клавиши

Пример 5. ИИ врага — зона обнаружения

Враги реагируют на игрока только когда он входит в их зону обнаружения.

from ursina import *
app = Ursina()
# Создаем игрока с возможностью движения
player = Entity(model='cube', position=(0, 0, 0), color=color.blue)
# Создаем землю для лучшей визуализации
ground = Entity(model='plane', scale=20, color=color.dark_gray, y=-1)

class Enemy(Entity):
    def __init__(self, **kwargs):
        super().__init__(model='cube', scale_y=2, origin_y=-.5, color=color.light_gray, **kwargs)
        self.original_color = self.color
        self.detection_radius = 10  # Зона обнаружения

    def update(self):
        # Проверяем расстояние до игрока
        dist = distance_xz(player.position, self.position)
        if dist > self.detection_radius:
            # Слишком далеко - враг не реагирует, серый цвет
            self.color = self.original_color
            return

            # Игрок в зоне обнаружения - враг становится красным
        self.color = color.red
        # Смотрим на игрока
        self.look_at_2d(player.position, 'y')
        # Движемся к игроку если не слишком близко
        if dist > 2:
            self.position += self.forward * time.dt * 3

# Создаем врагов на разных позициях
enemies = [Enemy(x=x * 5, z=5) for x in range(-2, 3)]  # -10, -5, 0, 5, 10

# Добавляем управление игроком
def update():
    player.x += held_keys['d'] * time.dt * 5
    player.x -= held_keys['a'] * time.dt * 5
    player.z += held_keys['w'] * time.dt * 5
    player.z -= held_keys['s'] * time.dt * 5

# Инструкция для пользователя
info_text = Text(
    text="Используйте WASD для движения\nВраги краснеют когда вас обнаруживают",
    position=(-0.85, 0.45),
    scale=0.8
)

app.run()

Почему важно: Расстояние определяет, когда враг начинает преследовать игрока.

Пример 6. Столкновения в Pong

Мяч должен отскакивать от ракеток и стен — это основа геймплея.

from ursina import *
app = Ursina()
window.color = color.black
camera.orthographic = True
camera.fov = 1

left_paddle = Entity(scale=(1 / 32, 6 / 32), x=-.75, model='quad', origin_x=.5, collider='box')
right_paddle = duplicate(left_paddle, x=.75)

# Создаем стены для отскока
floor = Entity(model='quad', y=-.5, origin_y=.5, collider='box', scale=(2, 10), visible=False)
ceiling = duplicate(floor, y=.5, rotation_z=180, visible=False)
left_wall = duplicate(floor, x=-.5 * window.aspect_ratio, rotation_z=90, visible=True)
right_wall = duplicate(floor, x=.5 * window.aspect_ratio, rotation_z=-90, visible=True)

# Исправляем мяч - добавляем начальную скорость и вращение
ball = Entity(model='circle', scale=.05, collider='box', speed=10, rotation_z=45)

def update():
    # Двигаем мяч в направлении его "вперед"
    ball.position += ball.right * time.dt * ball.speed

    # Добавляем движение ракеток
    left_paddle.y += (held_keys['w'] - held_keys['s']) * time.dt * 5
    right_paddle.y += (held_keys['up arrow'] - held_keys['down arrow']) * time.dt * 5

    # Проверяем столкновения
    hit_info = ball.intersects()
    if hit_info.hit:
        if hit_info.entity in (left_paddle, right_paddle):
            # Отскакиваем от ракетки
            ball.rotation_z += 180 * (-1 if hit_info.entity == left_paddle else 1)
            # Добавляем эффект угла отскока в зависимости от места удара
            ball.rotation_z -= (hit_info.entity.world_y - ball.y) * 20 * 32 * (
                -1 if hit_info.entity == left_paddle else 1)
            ball.speed *= 1.1  # Ускоряем мяч

        elif hit_info.entity in (floor, ceiling):
            # Отскакиваем от потолка и пола
            ball.rotation_z = -ball.rotation_z

def input(key):
    if key == 'space':
        # Сбрасываем мяч при нажатии пробела
        ball.position = (0, 0, 0)
        ball.rotation = (0, 0, 45)
        ball.speed = 10

app.run()

Что такое UI элементы?

UI (User Interface, пользовательский интерфейс) — это все элементы на экране, с которыми взаимодействует игрок: кнопки, полоски здоровья, инвентарь, надписи.

Примеры UI элементов:

Кнопка «Начать игру».

Полоска здоровья персонажа.

Таблица рекордов.

Меню настроек.

Для таких элементов часто используют distance_2d(), потому что они всегда находятся в одной плоскости экрана.

Советы для начинающих

Используйте distance() для полных 3D взаимодействий.

Для наземных персонажей — distance_xz().

Для интерфейса и плоских объектов — distance_2d().

Для оптимизации в играх с множеством объектов сравнивайте квадраты расстояний (без извлечения корня). Теперь вы знаете, как определять расстояние между объектами в Ursina и зачем это нужно!

Ссылка на мой Телеграм-канал: Нажмите сюда

Давайте создадим сообщество школьников-программистов или начинающих программистов и будем создавать интересные проекты и игры на Python.