Ursina Engine. Игры на Python.
167 subscribers
43 photos
45 videos
21 files
30 links
Добро пожаловать в наш канал, посвященный Ursina Engine — мощному и простому в использовании игровому движку на Python!

Курс на Stepik - https://stepik.org/course/250738

!!!Поддержать канал !!!!
https://www.donationalerts.com/r/ursinaenginepy
Download Telegram
🔁 FSM (Finite State Machine)🔄

FSM — это конечный автомат состояний.
Объект всегда находится в одном состоянии, может переходить в другие, и у каждого состояния есть логика входа, обновления и выхода.

FSM реализован классом :
from direct.fsm.FSM import FSM


🧠 Зачем нужен FSM

FSM идеально подходит для:

🎮 Игры

Состояния игрока: Idle, Walk, Run, Jump, Attack
Анимации персонажей
Управление оружием
Камеры (follow / free / cinematic)

🧩 Логика

Меню (MainMenu → Settings → Game)
Режимы игры

🧩 Базовая концепция FSM
Каждое состояние — это набор методов с именами по шаблону:

🔻enterState() — Вызывается при переходе в состояние
🔻exitState() — Вызывается при выходе из текущего состояния
🔻filterState(request, args) — Разрешить / запретить переход
❗️State — имя состояния

🎮 Управление переходами

Переходы между состояниями запрашиваются с помощью метода:

fsm.request('State')


🏗 Минимальный пример использования FSM + Actor

# =========================
# ИМПОРТЫ
# =========================
from ursina import * # основной движок Ursina
from direct.actor.Actor import Actor # Actor из Panda3D (анимации)
from direct.fsm.FSM import FSM # FSM (машина состояний)
from ursina.shaders import lit_with_shadows_shader


# =========================
# ИНИЦИАЛИЗАЦИЯ ПРИЛОЖЕНИЯ
# =========================
app = Ursina()

# Делаем шейдер с освещением и тенями шейдером по умолчанию
Entity.default_shader = lit_with_shadows_shader

# Источник направленного света (как солнце)
DirectionalLight(color=color.dark_gray, shadows=True)


# =========================
# СЦЕНА / ЗЕМЛЯ
# =========================
plane = Entity(
model="plane",
scale=100,
texture="brick",
collider="box"
)
plane.texture_scale = (10, 10)


# =========================
# РОДИТЕЛЬ ДЛЯ ИГРОКА И КАМЕРЫ
# =========================
# Это позволяет вращать персонажа и камеру вместе
parent_entity = Entity()

camera.parent = parent_entity
camera.position = (0, 2.5, 4.5)
camera.rotation_y = 180


# =========================
# НАСТРОЙКИ МЫШИ
# =========================
mouse.locked = True
mouse_sensitivity = Vec2(6, 40)


# =========================
# ЗАГРУЗКА АКТОРА (ИГРОКА)
# =========================
player = Actor(
"player.glb", # базовая модель
{
"idle": "idle.glb",
"walk": "walking.glb",
"run": "running.glb",
},
)

player.reparent_to(parent_entity)


# =========================
# SUBPARTS (РАЗДЕЛЕНИЕ ТЕЛА)
# =========================
# Ноги — для ходьбы / бега
player.makeSubpart(
"legs",
[
"mixamorig:LeftUpLeg",
"mixamorig:LeftLeg",
"mixamorig:LeftFoot",
"mixamorig:LeftToeBase",
"mixamorig:LeftToe_End",
"mixamorig:RightUpLeg",
"mixamorig:RightLeg",
"mixamorig:RightFoot",
"mixamorig:RightToeBase",
"mixamorig:RightToe_End",
],
)

# Торс — всё, кроме ног
player.makeSubpart(
"torso",
["mixamorig:Hips"], # корневая кость торса
[
"mixamorig:LeftUpLeg",
"mixamorig:LeftLeg",
"mixamorig:LeftFoot",
"mixamorig:LeftToeBase",
"mixamorig:LeftToe_End",
"mixamorig:RightUpLeg",
"mixamorig:RightLeg",
"mixamorig:RightFoot",
"mixamorig:RightToeBase",
"mixamorig:RightToe_End",
],
)


# =========================
# ПРИВЯЗКА ОРУЖИЯ К КОСТИ
# =========================
# Получаем кость позвоночника
ancorSpine = player.exposeJoint(
None,
"modelRoot",
"mixamorig:Spine2"
)

# Создаём оружие
gun = Entity(
model="scifi_gun.obj",
texture="gun_texture.jpg",
color=color.dark_gray,
scale=0.067,
collider="box",
)

# Привязываем оружие к кости
gun.world_parent = ancorSpine
gun.position = (-25, -10, 12)
gun.rotation = (280, -90, 0)
Please open Telegram to view this post
VIEW IN TELEGRAM


# =========================
# ФЛАГИ СОСТОЯНИЯ
# =========================
is_moving = False # игрок движется
is_running = False # игрок бежит
is_aiming = False # прицеливание (пока не используется)
has_gun = False # оружие в руках


# =================================================
# FSM — МАШИНА СОСТОЯНИЙ ИГРОКА
# =================================================
class PlayerFSM(FSM):

def __init__(self, player):
super().__init__("PlayerFSM")
self.player = player

# -------------------------
# IDLE
# -------------------------
def enterIdle(self): # метод enter позволяет добавить логику при входе в какое либо состояния
self.player.loop("idle")

# -------------------------
# WALK
# -------------------------
def enterWalk(self):
self.player.loop("walk")

def exitWalk(self):
pass # метод exit позволяет добавить логику при выходе из какого либо состояния

# -------------------------
# RUN
# -------------------------
def enterRun(self):
self.player.loop("run")


# Создаём FSM
fsm = PlayerFSM(player)

# Стартовое состояние
fsm.request("Idle")


# =========================
# UPDATE — КАЖДЫЙ КАДР
# =========================
def update():
global is_running

# Вращение персонажа мышью
parent_entity.rotation_y += mouse.velocity[0] * mouse_sensitivity[1]

# Скорость зависит от бега
speed = 3 if is_running else 1

# Движение вперёд
if is_moving:
parent_entity.position += parent_entity.back * speed * time.dt

# Фиксированный наклон камеры
camera.rotation_x = 10


# =========================
# INPUT — СОБЫТИЯ КЛАВИАТУРЫ
# =========================
def input(key):
global is_moving, is_running, is_aiming, has_gun

if key == "escape":
application.quit()

# Начало движения
if key == "w":
is_moving = True
fsm.request("Walk")

# Остановка
if key == "w up" and not has_gun:
is_moving = False
fsm.request("Idle")

# Начало бега
if key == "left shift" and is_moving:
is_running = True
fsm.request("Run")

# Конец бега
if key == "left shift up" and is_moving:
is_running = False
fsm.request("Walk")


# =========================
# ЗАПУСК
# =========================
if __name__ == "__main__":
app.run()
1
Правильный код для теней от DirectionalLight.

light = DirectionalLight(color=color.dark_gray, shadows=True, position = (0,10, -50))
light.look_at(Vec3(1,-1, 2))
shadow_bounds_box = Entity(model='wireframe_cube', scale=1000, visible=0)
light.update_bounds(shadow_bounds_box)


🔻Разберём этот кусок по слоям

1️⃣ Создание направленного света
light = DirectionalLight(
color=color.dark_gray,
shadows=True,
position=(0, 10, -50)
)

Что такое DirectionalLight

Это солнце 🌞

Свет идёт параллельными лучами
Не имеет «источника» как лампа

Идеален для:
солнца
луны
общего освещения сцены

Параметры:

color=color.dark_gray тёмно-серый Не слепит сцену, даёт мягкие тени
shadows=True включено Обязательно, иначе теней не будет
position=(0,10,-50) точка в пространстве Используется для расчёта теней

⚠️ Для DirectionalLight позиция не влияет на яркость,
но очень важна для карты теней.


2️⃣ Направление света

light.look_at(Vec3(1, -1, 2))

Что происходит

Свет смотрит в точку (1, -1, 2)
Лучи идут ОТ света → К этой точке

❗️Почему это важно
Тени считаются в направлении света

❗️Если look_at неправильный:
тени исчезают
тени обрезаются
появляются артефакты

3️⃣ Ограничительная рамка для теней

shadow_bounds_box = Entity(
model='wireframe_cube',
scale=1000,
visible=0
)

🔥 Это ключевой момент, который многие не понимают.

Зачем нужен ограничитель теней

DirectionalLight НЕ ЗНАЕТ,
где в мире нужно считать тени.

👉 Ему нужно сказать:
«Вот область мира, где считаем тени»

Что это за объект

wireframe_cube — просто форма

scale=1000 — огромная коробка(размер выбирайте сами)

visible=0 — не отображается

📦 Это воображаемая коробка, внутри которой:

объекты отбрасывают тени
объекты получают тени

4️⃣ Привязка границ теней

light.update_bounds(shadow_bounds_box)

Что делает эта строка
Берёт ограничительная рамку объекта
Использует её как усеченную теневую камеру(👉 объём пространства, который “видит” свет,
и только внутри этого объёма считаются тени.)

Проще:
📸 У света есть своя камера
📦 Эта камера видит только определённую коробку мира
🌑 Всё, что внутри — отбрасывает и получает тени
🚫 Всё, что снаружи — игнорируется

⚠️ Если НЕ делать update_bounds

Тени:
исчезают
обрезаются
мигают при движении камеры
работают только рядом с (0,0,0)

🎯 Итог: что делает весь код
Создаёт солнечный свет
Включает реальные тени
Указывает направление света
Определяет область мира, где тени работают
Делает тени стабильными и предсказуемыми

🧠 Полезные советы
Делай bounds как можно меньше, но чтобы влезала сцена
Большой scale → мыльные тени
Маленький scale → обрезанные тени
Bounds можно двигать за игроком (для открытого мира)
Please open Telegram to view this post
VIEW IN TELEGRAM
И обязательно нужно применить lit_with_shadows_shader!!! Без этого шейдера теней не будет вообще!!!

🔻Чтобы применить его сразу ко всем объектам в начале кода импортируем шейдер и прописываем:
Entity.default_shader = lit_with_shadows_shader


🔻Теперь тени будут у всех объектов, если не переопределить шейдер для какого либо объекта(ов) явно.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Друзья, всех приветствую 👋

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

Работа над курсом также продолжается. Новые темы курса: actor, машина состояний, и совсем скоро появиться AIBehavior и урок по сборке в exe.

Спасибо ,что остаетесь здесь и спасибо за поддержку!!😉
2
🎮 AIBehaviors в Ursina Engine

Разберёмся, что это такое и как это реально работает 👇

--——————————————————————————————-

🧠 Что такое AIBehaviors?

Это система “поведенческого ИИ” .
Не путай с полноценным AI:

🔻 Это НЕ:
— сложная логика
— не поиск пути

👉 Это:
— набор сил, которые “толкают” объект в нужную сторону

--————————————————————————————————-

⚙️ Основные понятия

🔹 AICharacter
Обёртка над объектом, которая даёт ему “физику поведения”
(масса, ускорение, сила)

🔹 AIWorld
Контейнер, где живут все AI-объекты и препятствия
👉 именно здесь обновляется весь AI

🔹 AIBehaviors
Набор действий, которые можно включать:
— движение
— избегание
— блуждание

--——————————————————————————————————-

🚶 Базовые алгоритмы поведения

🔸 seek (преследование)
Двигается прямо к цели
👉 самый простой вариант врага
——
🔸 pursue (умное преследование)
Предсказывает, куда движется цель
👉 лучше работает против игрока
——
🔸 wander (блуждание)
Случайное движение
👉 патрули, “живой” мир
——
🚧 obstacleAvoidance (обход препятствий)
Звучит мощно, но:

👉 AI НЕ строит маршрут
👉 AI просто “отталкивается” от объектов

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

🔻есть направление к цели
🔻 если впереди препятствие → добавляется сила в сторону

--———————————————————————————————————————

AIBehaviors — это:
👉 не интеллект, а математика движения

Поэтому:
— может застревать
— может дёргаться
— не умеет обходить сложные уровни

---

🔥 Когда использовать

Быстрое прототипирование
Простые враги

---

🚀 Как делают нормальный AI в Ursina

Вместо AIBehaviors:

• FSM (машина состояний)
• raycast (зрение)
• векторная логика движения

👉 Это даёт полный контроль
👍2
🤖Пример применения AIBehaviors

Чтобы использовать поведенческие алгоритмы, необходимо сделать два обязательных импорта, кроме основного:
from ursina import *
from panda3d.ai import AIWorld
from panda3d.ai import AICharacter
 
app = Ursina()



Теперь добавим в сцену поверхность и два куба, которые будут в качестве персонажей:

plane = Entity(model="plane", scale=100, texture="brick", collider="box")

plane.texture_scale = (10, 10)

# Игрок
player = Entity(model="cube", color=color.azure, position=(0, 0, 0), collider='box')

# Враг
enemy = Entity(model="cube", color=color.red, position=(15, 0, 15), collider='box')



Создаём AIWorld:
ai_world = AIWorld(app.render)


render — это корневой узел всей 3D-сцены
 
Создаём AICharacter
enemy_char = AICharacter("red_cube", enemy, 50, 1, 3)


Остановимся подробнее:
AICharacter(name, entity, mass, movement_force, max_force)
name - задаём имя, можно любое
entity - передаём объект, который будет ИИ-персонажем
Изменяя следующие параметры, мы можем тонко отрегулировать динамику и характер движения:
mass — масса
movement_force — сила движения
max_force — ограничение ускорения
 
Добавляем нашего персонажа в AIWorld
ai_world.addAiChar(enemy_char)


Получаем алгоритмы поведения для персонажа

enemy_ai = enemy_char.getAiBehaviors()



Задаем поведение:

enemy_ai.pursue(player)


 
В функции update обязательно вызываем обновление AIWorld

def update():

    ai_world.update()
 
EditorCamera()

app.run()




В этом примере враг будет постоянно преследовать игрока👾
👍2
Делюсь скринами из примера новой игры😉
На данный момент есть карта маленького городка и персонаж игрока, добавленного с помощью Actor. Есть пока что только одна механика стрельбы. Управление реализовано через FSM(Finite State Machine)
3🔥2
Поддержите моё творчество
Нравится канал? Поддержите меня, и я смогу создать ещё больше интересного контента. Ваш вклад — это моя мотивация!
🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Нашел способ сделать «зеркало», пока есть нюансы, но я добью до приемлемого результата и всё покажу😉
🔥3
Мини карту тоже можно так будет сделать👍
Ещё у меня есть идея редактора уровней написать, уже есть кое какие наброски.
Я когда-то давно публиковал пост на эту тему с примером, но сейчас я уже придумал интерфейс и удобное управление.
Как только доработаю, обязательно поделюсь 😊
👍4