Topic 6 of 6

Course materials are currently available in Russian only.

Мини-игра от первого лица

Игрок от первого лица

Соберём всё вместе. Основа — готовый FirstPersonController: ходьба на WASD, обзор мышью, прыжок на пробел, гравитация.

from ursina import *
from ursina.prefabs.first_person_controller import FirstPersonController

app = Ursina()
ground = Entity(model="plane", scale=40, texture="grass", collider="box")
wall = Entity(model="cube", texture="brick", position=(3, 1, 5), scale=(4, 2, 1), collider="box")
player = FirstPersonController(speed=5)
app.run()

Контроллеру нужны коллайдеры: пол — чтобы стоять, стены — чтобы в них упираться. Без collider= игрок провалится сквозь пол.

Предметы

Кристаллы — обычные Entity в списке. Каждый кадр проверяем расстояние до игрока:

for c in list(crystals):                       # копия: из списка будем удалять
    if distance(player.position + Vec3(0, 1, 0), c.position) < 1.5:
        crystals.remove(c)
        destroy(c)
        score += 1
  • destroy(entity) убирает Entity из сцены;
  • из своего списка объект нужно убрать отдельно — и делать это при обходе копии списка;
  • player.position — это ноги игрока; кристаллы висят на высоте, поэтому прибавляем рост.

Текст и время

label = Text("Кристаллы: 0", position=(-0.85, 0.45))    # координаты экрана, а не мира
label.text = f"Кристаллы: {score}"

Обратный отсчёт округляют вверх (math.ceil): пока остаётся хоть доля секунды, таймер показывает 0:01. Минуты и секунды — f"{s // 60}:{s % 60:02d}".

Отложить событие: invoke(функция, delay=2) — вызвать через 2 секунды.

Лабиринт из карты

Как и в 2D, уровень удобно задать строками. Клетка (row, col) → положение (col, 1, row):

for row, line in enumerate(MAZE):
    for col, ch in enumerate(line):
        if ch == "#":
            Entity(model="cube", texture="brick", position=(col, 1, row), scale=(1, 2, 1), collider="box")

Поиск пути

Чтобы найти кратчайший путь по клеткам с учётом стен, используют поиск в ширину:

from collections import deque

dist = {start: 0}
queue = deque([start])
while queue:
    r, c = queue.popleft()                  # клетки выходят в порядке удалённости
    for nr, nc in ((r + 1, c), (r - 1, c), (r, c + 1), (r, c - 1)):
        if клетка в карте, не стена и ещё не посещена:
            dist[(nr, nc)] = dist[(r, c)] + 1
            queue.append((nr, nc))

Очередь гарантирует, что клетки обрабатываются по возрастанию расстояния, — поэтому первая найденная цель и есть ближайшая. Так же ищут путь враги в играх: от игрока «волной» по клеткам.

Итог

  • FirstPersonController и коллайдеры у пола и стен;
  • сбор: расстояние, destroy, удаление из своего списка по копии;
  • Text, таймер с math.ceil и :02d, invoke;
  • лабиринт из строк, кратчайший путь — поиск в ширину с deque.

Вы собрали мини-игру от первого лица: сцена, движение, камера, столкновения и цель на время. Попробуйте добавить в неё врага из темы про камеру — с полем зрения, который идёт к игроку по пути из поиска в ширину.