Пошаговая инструкция по созданию минималистичной заготовки 3D-игры на движке Godot.
Архив с готовыми файлами проекта можно взять на странице: https://thenonsense.itch.io/opendungeon
А мы начнём сборку этого прототипа с нуля. Первое, что потребуется сделать - загрузить актуальную версию движка Godot с сайта https://godotengine.org/ в разделе Download.
В стандартной версии код мы будем писать на языке GDScript, внутри самого редактора, не используя сторонние среды написания кода.
Распакуем скачанный файл и запустим Godot engine. В появившемся окне справа нажмём на кнопку New Project.
Новый проект
Пишем имя проекта, затем открываем Browse чтобы выбрать желаемое место в файловой системе (в данном случае папка Godot_35), в которой будут лежать проекты Godot. Потом нажимаем кнопку Create Folder, чтобы в этой папке создалась папка проекта и переключаем рендер на gles2 (он проще в настройке, требует железа послабее, а при желании проект всегда можно перевести на gles3).
Создание 3d сцены, камера
Получаем стартовый экран. Нажмём на создание 3д-сцены.
Меняем имя узла-пустышки Spatial на Main и сохраняем сцену под именем MainScene.
Как видно внизу - сцена появилась в списке ресурсов. Сразу попробуем запустить игру, нажав на кнопку Play вверху.
Godot напишет, что не выбрана основная сцена и предложит варианты - выбираем сделать текущую сцену основной (Select Current).
Игра запускается, видим пустой экран.
Для того, чтобы увидеть 3д-сцену в игре, нужно как минимум добавить туда камеру. Для этого щёлкаем правой кнопкой на Main, выбираем + Add Child Node, напишем в поиске cam и добавим камеру (Camera). Она прикрепится к узлу Main, в те же координаты.
Если теперь запустить игру, то увидим градиент, имитирующий землю и небо. Для порядка отметим флажком на панели инспектора (справа), что камера является основной текущей (Current).
Пока камера выбрана, есть возможность нажать на Preview, чтобы увидеть то, что видит камера прямо в окне редактора.
Отключаем Preview, чтобы снова работать с 3д-сценой. Для вращение в 3д-пространстве зажимаем колесо мыши, для приближения удаления - крутим колёсико. Если нужно отцентровать вид, сфокусировавшись на каком-то объекте сцены - выделяем этот объект и нажимаем F.
Объекты на сцене
Теперь добавим какой-нибудь объект. Снова выбираем узел Main, правая кнопка мыши, + Add Child Node. Вбиваем в поиск mesh и выбираем MeshInstance, для создания полигональной сетки. Узел появляется, но ничего не видно - нужно зайти на панель инспектора справа, и выбрать какую-то из предустановленных простых форм, например цилиндр.
Установив для полигональной сетки форму цилиндра снова выделим камеру, переключимся на Mode Move (горячая кнопка W) и потянем за синюю стрелку, перемещая тем самым камеру по оси Z (справа вверху показаны оси и обозначено, какой цвет какой оси соответствует).
Откроем вкладку Transform на панели инспектора камеры, где собраны базовые свойства для любого 3д-объекта - положение по осям, углы поворота, размер. Видно, что в поле Z стоит какое-то ненулевое значение и рядом круг со стрелкой (нажав на этот значок можно сбросить текущее значение на предустановленное). Поставим в отделе Translation число 3 в поле Y, и 5 в поле Z. В отделе Rotation Degrees установим X на минус 30. Если нажать превью камеры или запустить игру, то видно, что камера теперь смотрит на цилиндр.
Сохраним проект. Можно заметить, что до сохранения у MainScene вверху висела звёздочка, показывая, что изменения не сохранены, подсказывая таким образом, что неплохо бы сохранится при случае. Сцены и прочие изменения также автоматически сохраняются при нажатии на запуск игры.
Теперь сделаем копию цилиндра, и переделаем его в куб. Для этого выделяем цилиндр и нажимаем сочетание Ctrl + D (клонирование). Появляется объект MeshInstance2 (Godot автоматически переименовал новый объект с тем же именем). Передвинем его по оси Z на -3 и в поле Mesh выберем NewCubeMesh, чтобы второй цилиндр стал кубом.
Персонаж и управляющий скрипт
Сделаем заготовку персонажа. Для этого добавляем на сцену (выделив Main и нажав Ctrl + A или правую кнопку мыши, с последующим выбором опции + Add Child Node) новый узел-пустышку Spatial. Перетаскиваем внутрь неё цилиндр и камеру, после чего переименовываем узел Spatial в Hero.
Если нажать на кнопку глаза, справа от Hero, то видим, что цилиндр и камера, находящиеся внутри пустышки скрываются из видимости. Вернём им видимость и переходим к созданию основного скрипта игры.
Выделим корневой узел Main и нажмём на свиток с зелёным плюсом сверху, чтобы добавить новый скрипт. Появится окошко, где предлагается название скрипта MainScene.gd. Нажмём на папку сбоку и в открывшемся окне нажимаем на кнопку сверху Create Folder, называем новую папку Scripts. Теперь меняем имя скрипта, пусть будет theMain, после чего нажимаем на кнопку Open справа. Видим, что путь и имя файла изменились (строчка path), затем нажимаем кнопку Create.
Оказываемся в окне скриптов, где видим строки свежесозданного скрипта theMain.gd.
Удаляем верхние строчки, начинающиеся с символа # (с этого символа начинаются строки с комментариями к коду), и заводим ссылку на нашего героя. Пишем выше строки func _ready(): строчку onready var main_hero = $Hero Ниже, заменим строку pass на такую запись main_hero.translation.x += 10.0 (символ табуляции >| в начале этой строки не трогаем).
Рядом со скриптом горит звёздочка, так как мы вносили изменения, для того чтобы сохранить их нажмём на строчку File и выберем Save.
Теперь запустим игру и видим, что цилиндр сместился влево относительно куба. Это дело рук скрипта theMain, где мы до инициализации сцены получили ссылку на цилиндр с камерой, а затем, при первом появлении в сцене сдвинули цилиндр на 10 по оси X.
Переключимся обратно в сцену, щелкнув на кнопке 3d сверху экрана. Создадим новый дочерний к Main MeshInstance, выбрав в качестве формы New PlaneMesh. Переименуем его в Pol_A и установим его размеры Scale на 5 по X, 1 по Y и 5 по Z.
Заходим в поле Material и выбираем там пункт New SpatialMaterial. Появится изображение белого шара. Щелкаем на него, открываются настройки нового материала. Меняем Color во вкладке Albedo на какой-то другой цвет.
Теперь сохраним этот материал, нажав на иконку дискетки. Создадим папку Materials и переименуем материал в ground_mat.tres
Добавляем интерактив
Вернёмся в скрипты, нажав на свиток справа от Main или щёлкнув на Script вверху экрана.
Расскоментируем строку func _process(delta) (удалив символ # в начале строки) и уберём прочие серые строчки. Пишем ниже func _process() строку main_hero.translation.x -= delta
Таким образом мы создали функцию, которая будет делать что-то в цикле. В данном случае потихоньку сдвигать цилиндр в направлении противоположном начальному сдвигу со скоростью привязанной с скорости выполнения программы. Если убрать deltа и поставить некое число, то движение будет происходить с разной скорость на разных системах. Для того чтобы правильным образом ускорить или замедлить скорость движения нужно в этой строчке умножать delta на нужное число (например, чтобы движение происходило в 2 раза быстрее строчка выглядела бы как main_hero.translation.x -= 2 * delta ). Думаю принцип понятен, а теперь запустим игру и убедимся, что цилиндр движется влево.
Сделаем копию цилиндра в сцене и сдвинем её вперёд и вверх, немного уменьшив. Он будет символизировать голову персонажа, чтобы было видно куда повёрнут герой.
Теперь внутри Hero (выделяем его, Ctrl + A) создадим узел Spatial. Назовём его Visual и перетащим туда оба цилиндра.
Перепишем код в Main следующим образом:
extends Spatial
onready var main_hero = $Hero
onready var hero_visual = $Hero/Visual
var horizontal_joy = 0
var vertikal_joy = 0
func _ready():
main_hero.translation.x += 10.0
func _process(delta):
main_hero.translation.x -= delta
horizontal_joy = 0
vertikal_joy = 0
if Input.is_action_pressed("ui_up"):
vertikal_joy = 1
if Input.is_action_pressed("ui_left"):
horizontal_joy = -1
if Input.is_action_pressed("ui_right"):
horizontal_joy = 1
if Input.is_action_pressed("ui_down"):
vertikal_joy = -1
match vertikal_joy:
0:
match horizontal_joy:
1:
hero_visual.rotation_degrees = Vector3(0.0,-90.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,90.0,0.0)
1:
match horizontal_joy:
0:
hero_visual.rotation_degrees = Vector3(0.0,0.0,0.0)
1:
hero_visual.rotation_degrees = Vector3(0.0,-45.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,45.0,0.0)
-1:
match horizontal_joy:
0:
hero_visual.rotation_degrees = Vector3(0.0,180.0,0.0)
1:
hero_visual.rotation_degrees = Vector3(0.0,-135.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,135.0,0.0)
Обратите внимание, что некоторые строки имеют отступы и начинаются с символов табуляции (ставятся кнопкой Tab), а не пробелов. В языке GDScript эти отступы важны - каждая следующая степень вложенности логики требует добавления ещё одного отступа. Если в определённой области отступов становится слишком много, то есть смысл задуматься о том, чтобы вынести из того места какую-то логику в отдельные функции.
Итак, что мы добавили? Создали ссылку hero_visual на визуальное представление героя (узел с цилиндрами), чтобы поворачивать его отдельно от перемещения контейнера с героем и камерой. Также завели две переменные horizontal_joy и vertikal_joy, чтобы отслеживать, какие оси направления зажаты игроком. Далее в цикле func _process(delta) мы каждый раз обнуляем переменные направления, затем опрашиваем клавиатуру на предмет того, нажал ли пользователь одну из кнопок соотнесённых с ключами ui_up, ui_left, ui_right, ui_down (по умолчанию это стрелки на клавиатуре). Если да, то отмечаем направление по каждой оси в виде отклонений от центра (0) в положительную или отрицательную строну. Завершается всё блоком проверки значений осей, с поворотом визуала героя в соответствующую сторону.
Если запустить игру, то цилиндр будет как обычно ехать влево, но теперь уже "крутить головой" при нажатии стрелок.
Теперь удалим первую строчку в функции_process(delta), в которой цилиндр постояннно едет влево (main_hero.translation.x -= delta на 13-й строке), а выше, до функции _ready() напишем строчку var hero_speed = 2.2 для того, чтобы у персонажа была скорость.
Далее добавим пару строк в самом конце (следим за правильным количеством отступов, чтобы эти строки не оказались внутри проверок по осям):
main_hero.translation.x += horizontal_joy * hero_speed * delta
main_hero.translation.z -= vertikal_joy * hero_speed * delta
Теперь наш герой научился полноценно перемещаться.
Вернёмся на 3д сцену и уменьшим размеры цилиндров, изображающих персонажа, подняв основной так, чтобы он находился немного над полом. Сам узел Visual не трогаем и его размеры не меняем, он должен оставаться единичного размера и с нулевыми координатами по осям. Уже больше похоже на движущегося персонажа.
Для перемещения и изменения размеров вручную пользуемся режимами Move и Scale, включающиеся на панели значков вверху.
Колонны и столкновения
Добавим в Main (через Ctrl+A) новый узел Spatial, назовём его Pillar_A и закинем туда модельку с кубом.
Теперь выделим узел Pillar_A и сделаем из него префаб (отдельную мини-сцену). Для этого щелкаем правой кнопкой мыши и выбираем опцию Save Brunch as Scene.
В открывшемся окне заводим папку для префабов Prefabs и сохраняем сцену под предложенным именем туда.
Как видно, на сцене остался только корневой узел нашей кубической колонны и появился значок справа. Щёлкнем по этому значку и откроется отдельная сцена с внутренностями этого узла.
Выделим куб и нажмём на сброс координат во вкладке Transform у поля Translation.
Куб перепрыгнет в начало локальных координат этой конкретной сцены.
Уменьшим куб в размерах, растянем по оси Y и поднимем, чтобы он расположился выше сетки.
Сохраним изменения в сцене. Переключаемся на вкладку основной сцены MainScene. Видим, что получившаяся колонна заслоняет персонажа.
Это получилось потому, что пустышку для колонны мы создавали в начале координат и когда сбросили смещение куба, находящегося внутри пустышки, то он оказался на том месте, где она расположена в 3д пространстве. Сдвинем Pillar_A по оси Z, расположив перед персонажем.
Теперь сделаем клон узла Pillar_A, выделив его и нажав сочетание Ctrl+D (либо щёлкнув правой кнопкой и выбрав опцию Duplicate). Появится узел Pillar_A2, смещаем его вправо. Затем дублируем его таким же способом, получив Pillar_A3 и смещаем его ещё правее.
Запустим игру, теперь персонаж может побегать между колонн. Естественно, просачиваясь сквозь них, так как мы не сделали колонны "твёрдыми".
Немного подправим колонны. Заходим в любую их них, например Pillar_A3, оказавшись внутри выделяем вытянутый куб, клонируем и делаем два маленьких, приплюснутых, располагая их внизу и вверху. Сохраняем текующую сцену. Все колонны в сцене MainScene теперь поменяются, так как они являются связанными копиями - редактируя одну меняешь и все остальные.
Но продолжим редактирование сцены с колонной. Переключаемся теперь на самый верхний узел сцены (Pillar_A), и добавляем к нему новый узел. Набираем в поиске area, выбираем узел Area и добавляем его.
Видим, треугольный значок предупреждения, в котором написано что у нашей Area нету формы коллизии и нужно её создать.
Для этого щёлкаем на Area правой кнопкой и выбираем добавить новый узел. На этот раз ищем CollisionShape.
У этого нового узла снова видим предупреждение о том, что нужно указать ресурс, который определяет форму.
Щелкнем на панели инспектора справа, в поле Shape, чтобы выбрать одну из предустановленных форм - New BoxShape.
Теперь значков предупреждений нет и у нас на сцене появился коллайдер в форме куба. Уменьшим его размеры и немного приподнимем.
Переключимся снова на Area и снимем галочку со строки Monitoring, чтобы эта зона не отслеживала соприкосновения с прочими коллайдерами.
Ниже, во вкладке CollisionObject, в поле Collision снимаем отметки с единичек и отмечаем только 2 ячейку layer.
Таким образом эта зона столкновения будет "видима" только для объектов, которые мониторят второй слой коллизий. Сохраняем сцену, переключаемся на MainScene.
Выделяем узел Visual и добавляем ему новый узел RayCast - это луч, отслеживающий столкновения.
В инспекторе справа ставим галочку напротив Enabled, чтобы луч был активным. Выставляем параметры Cast To в 0 (по x) 0.2 (по y) и -1 (по z). Теперь можно заметить, что луч выглядит как синий отрезок, выступающий из персонажа.
Ниже Cast To, в ячейках Colision Mask убираем 1 и отмечаем 2, чтобы луч мониторил второй слой коллизий. Ещё ниже во вкладке Collide With отмечаем пункт Areas, чтобы луч взаимодействовал с зонами (Area).
Откроем скрипт theMain и допишем выше _ready() получение ссылки на наш визуальный рейкаст - onready var rcast = $Hero/Visual/RayCast
В самом низу скрипта добавим следующую пару строк (не забываем про отступы-табуляции):
if rcast.is_colliding():
print("столкновение")
Первая строка это условие "если рейкаст с чем-то столкнулся", а ниже идёт инструкция, что делать, когда это произошло. В данном случае это вывод сообщения. Запускаем игру и видим, что когда персонаж подходит к колонне, то вне игры, в окошке редактора выводится сообщение "столкновение", пока рейкаст соприкасается с препятствием (колонной).
Вырежем две строчки выше этой последней записи и перенесём их ниже строки print. Нужно добавить по одному Tab в начало этих строк, чтобы они находились со строкой print на одном уровне. Таим образом движение персонажа теперь будет происходить только при условии столкновения. Если запустить игру сейчас, то персонаж окажется словно приклеенным к месту.
Немного изменим условие, переписав строку следующим образом:
if rcast.is_colliding() == false:
main_hero.translation.x += horizontal_joy * hero_speed * delta
main_hero.translation.z -= vertikal_joy * hero_speed * delta
else:
print("столкновение")
Теперь персонаж будет двигаться тогда, когда его рейкаст ни с чем не сталкивается, а в противном случае (else) будет выводится сообщение, а движения не будет. Запустим игру и убедимся в том, что теперь действительно колонны стали непреодолимым препятствием для героя.
Вернёмся в MainScene. Выделим узел Pol_A, жмём правой кнопкой и превращаем его в префаб (через опцию Save Brunch as Scene). Сохраняем под этим же именем в ранее созданную папку Prefabs, где уже должен лежать префаб Pillar_A.
Дублируем получившийся узел Pol_A (Ctrl+D), получив копию Pol_A2. Сдвинем копию куска пола ровно вправо, введя 10 в поле X у параметра Translation в инспекторе. Теперь пол расширился и герой будет появляться не на пустом месте.
Стартовый экран
Осталось добавить некоторую игровую задачу и обернуть всё получившееся в минимальный интерфейс. Выделим корневой узел Main и добавим к нему узел Control.
Мы увидим, что 3d сцена пропала. Это происходит потому, что только что был добавлен узел, относящийся к двумерным элементам.
Переименуем узел Control в StartScreen. Чтобы увидеть 3д-сцену снова можно выделять прочие узлы (которые созданы в 3д-пространстве), а пока выделим узел StartScreen. Нажмём сверху справа на Layout, выбрав вариант Center.
Таким образом мы привязали будущее стартовое окошко к центру экрана.
Добавим к StartScreen новый узел - ColorRect.
Появился белый квадратик.
Растянем его так, чтобы он перекрывал синюю рамку, обозначающую границы экрана. А также поменяем ему цвет в инспекторе, вкладка Color.
Если сейчас запустить игру, то увидим поле выбранного цвета, которое загораживает 3D сцену, хотя персонажем можно управлять, но просто ничего не видно. Если нажать на значок глаза справа от StartScreen и снова запустить игру, то 3д-сцену снова будет видно.
Включим видимость у StartScreen обратно (если нажимали на глаз) и добавим к нему новый узел - Button (кнопка).
Приблизим видимость колёсиком мыши и растянем кнопку немного в стороны. Переименуем этот узел в StartButton.
Взглянем на панель инспектора и увидим вверху, рядом с надписью Inspector другую вкладку, которая называется Node.
Переключимся на неё - откроется список сигналов, которые может излучать кнопка. Нас интересует сигнал pressed() , испускаемый при нажатии этой кнопки в игре. Щёлкнем на него правой кнопкой и выберем Connect...
Откроется диалог - куда мы хотим подключить сигнал о том, что кнопка нажата. Нам нужно подключить сигнал к скрипту Main, так как он уже выделен, то остаётся лишь нажать кнопку Connect.
Мы автоматически оказываемся внутри скрипта Main и видим, что добавились пара новых строчек. Это функция нажатия на кнопку старта игры, вместо pass мы можем подставить какие-то инструкции, которые нужно сделать в ответ на это действие.
Поднимемся по коду вверх и сразу после extends Spatial допишем строку onready var ui_startscreen = $StartScreen , так мы получаем ссылку на стартовое окошко, заслоняющее 3д экран, на котором располагается кнопка запуска игры. Возвращаемся в самый низ и вместо pass пишем ui_startscreen.hide()
Запускаем игру. Видим кнопку по центру, если её нажать, то цветной экран пропадает и персонаж с колоннами становится виден.
Здоровье и игровой цикл
Переместимся снова вверх по коду и допишем перед func _ready(): пару строк
var game_mode = 0
и
var health = 5000
Таким образом мы завели переменную game_mode, которая будет показывать состояние игры. При 0 - игра неактивна, при 1 - игра активна. Ну а health - это здоровье персонажа, которое мы будем понижать, если он касается препятствия.
Найдём строчку, где мы отправляли сообщение о столкновении, допишем несколько строчек выше и изменим сообщение на другое, добавив вывод текущего значения health:
health -= 1
if health < 0: game_mode = 0
print("health ",health)
При запуске игры увидим, что в лог теперь выводится утекающее здоровье персонажа, если он уткнулся в колонну.
Остались финальные штрихи. После условия if health < 0: добавим пару строк (обнуление параметра health и показ стартового экрана ui_startscreen):
if health < 0:
health = 0
game_mode = 0
ui_startscreen.show()
В функцию нажатия кнопки добавим строчку перевода игры в активный режим - вернёмся к закомментированной первой строчке ниже func _process(delta) (main_hero.translation.x -= delta), удалим её и вместо этого напишем условие, при котором будет происходить весь этот цикл
if game_mode == 1:
Строчка ниже подсветится красным, показывая ошибку, но мы сейчас всё исправим.
Выделяем весь код, начиная с красной строчки, до строки с print включительно и один раз нажимаем Tab.
Все эти строки сдвинутся вправо на шаг (чтобы, наоборот, удалить один шаг, сдвинув влево, нужно нажимать Shift + Tab), и теперь корректно попадают внутрь написанного нами ранее условия.
Поднимемся вверх, изменим health на число поменьше, 200. Запустим игру. Теперь здоровье улетает очень быстро, а когда обнуляется, то показывается стартовый экран и кнопка старта перестаёт работать. Вернее она работает, но персонаж уже сдвинут к препятствию, а здоровье осталось нулевым, поэтому условие конца игры тут же срабатывает снова, опять показывая стартовый экран.
Поправим это, допишем в конце кода в функцию нажатия кнопки строчку, которая установит героя в новые координаты main_hero.translation = Vector3(0.0,0.0,0.0) и строчку, устанавливающую начальное здоровье health = 200
Запускаем игру, теперь герой сразу же появится в начале координат, без сдвига. Когда здоровье заканчивается, показывается стартовый экран и при нажатии на кнопку игра нормально перезапускается заново.
Добавим ещё вывод текущего состояния здоровья прямо в игру, чтобы видеть его изменения, когда игровой файл будет запускаться отдельно от редактора. Добавим к узлу Main новый узел Control, он появится внизу списка узлов сцены. Переименуем его в Interface. Видим на экране зелёное перекрестье, появившееся слева вверху. Поменяем эту привязку на центр, нажав на Layout выше окна редактора и выбрав Center.
Теперь перекрестье сместилось в середину экрана. Далее расширяем область влияния этого узла на весь экран, щелкнув в Layout по кнопке Full Rect.
Теперь добавим к Interface новый узел Control. Переименуем его в HealthGauge. Щёлкнем на Layout, выбрав Bottom Left чтобы сместить привязку этого узла в левый нижний угол.
К HealthGauge добавляем узел Color Rect. Переименуем его в Background. Включаем режим Move Mode и немного оттащим белый квадратика от угла экрана.
Слева переключаемся во вкладку Inspector, открываем свойство Rect и ставим в поле size размер 200 по x. Также меняем цвет квадратика на тёмно-серый.
Клонируем теперь узел Background, меняем ему название на Bar и ставим красный цвет.
Теперь зацепим мышью узел Interface и перетащим его выше узла StartScreen. Таким образом стартовый экран будет загораживать экран интерфейса со шкалой, пока мы не запустим игру.
Переключаемся в режим написания кода. Вверху, ниже строки onready var ui_startscreen = $StartScreen допишем ссылку на полосу здоровья:
onready var healthbar = $Interface/HealthGauge/Bar
Внизу, выше функции func _on_StartButton_pressed(): заведём свою функцию HealthDamage, которая будет наносить повреждения персонажу и вносить изменения в размер красной полосы, символизирующей здоровье.
func HealthDamage(amount):
health -= amount
if health > 0:
healthbar.rect_size.x = health
Выше по коду, после строчки else заменим строку health -= 1 на вызов нашей функции с параметром 1: HealthDamage(1) . Таким образом в этом месте кода будет происходить обращение к функции HealthDamage, с переменной amount равной 1, далее от здоровья отнимается amount и полоса здоровья меняет свой размер по х на новый, равный новой величине здоровья (до тех пор, пока здоровье выше 0, в ином случае полоса не перерисовывается).
В конце кода, в функции нажатия кнопки также допишем строку HealthDamage(0) . Таким образом мы здесь тоже вызовем нашу функцию, на этот раз не нанося никаких повреждений, зато сработает перерисовка полосы здоровья на новый размер. Что нам и нужно, так как перед вызовом функции HealthDamage с параметром 0 мы вручную установили здоровье на 200 и просто хотим перерисовать полоску, чтобы её размер по x стал равен 200 на начале игры.
Запускаем игру - полоска здоровья работает и устанавливается заново с каждым новым перезапуском игрового цикла.
На этом всё,
вот результирующий код скрипта Main.gd
extends Spatial
onready var ui_startscreen = $StartScreen
onready var healthbar = $Interface/HealthGauge/Bar
onready var main_hero = $Hero
onready var hero_visual = $Hero/Visual
onready var rcast = $Hero/Visual/RayCast
var horizontal_joy = 0
var vertikal_joy = 0
var hero_speed = 2.2
var game_mode = 0
var health = 200
func _ready():
main_hero.translation.x += 10.0
func _process(delta):
if game_mode == 1:
horizontal_joy = 0
vertikal_joy = 0
if Input.is_action_pressed("ui_up"):
vertikal_joy = 1
if Input.is_action_pressed("ui_left"):
horizontal_joy = -1
if Input.is_action_pressed("ui_right"):
horizontal_joy = 1
if Input.is_action_pressed("ui_down"):
vertikal_joy = -1
match vertikal_joy:
0:
match horizontal_joy:
1:
hero_visual.rotation_degrees = Vector3(0.0,-90.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,90.0,0.0)
1:
match horizontal_joy:
0:
hero_visual.rotation_degrees = Vector3(0.0,0.0,0.0)
1:
hero_visual.rotation_degrees = Vector3(0.0,-45.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,45.0,0.0)
-1:
match horizontal_joy:
0:
hero_visual.rotation_degrees = Vector3(0.0,180.0,0.0)
1:
hero_visual.rotation_degrees = Vector3(0.0,-135.0,0.0)
-1:
hero_visual.rotation_degrees = Vector3(0.0,135.0,0.0)
if rcast.is_colliding() == false:
main_hero.translation.x += horizontal_joy * hero_speed * delta
main_hero.translation.z -= vertikal_joy * hero_speed * delta
else:
HealthDamage(1)
if health < 0:
health = 0
game_mode = 0
ui_startscreen.show()
print("health ",health)
func HealthDamage(amount):
health -= amount
if health > 0:
healthbar.rect_size.x = health
func _on_StartButton_pressed():
ui_startscreen.hide()
game_mode = 1
main_hero.translation = Vector3(0.0,0.0,0.0)
health = 200
HealthDamage(0)
Ах да, чтобы запускать игру отдельно от редактора, вам нужно собрать игровой билд под определённую операционную систему/технологию.
Сборка запускаемого билда
Для этого идём в параметры Project, выбирая опцию Export...
Откроется пустое окно экспорта, нажимаем там кнопку Add... и выбираем желаемый пункт, например Windows Desktop.
Появятся красные строчки, щелкаем на появившийся там же подчёркнутый пункт Manage Export Templates и в открывшемся окошке нажимаем кнопку Download and Install, чтобы нужные шаблоны сборки автоматически загрузились и установились.
Ожидаем, пока файлы загрузятся.
В итоге получим такое окошко, с надписью вверху о том, что всё установлено. Закрываем это окно, нажав на Close.
Заходим в Project - Export снова, видим что у нас выбран вариант Windows Desktop, а красных строчек уже нет. Теперь нажмём на кнопку Export Project.
Видим предлагаемое для игрового билда имя файла OpenDungeon.exe , а место выбрано прямо внутри файлов проекта. Исправим это, поднявшись в папку выше проекта и создадим там новую попку NewGame.
Далее убираем галочку с Export With Debug и нажимаем Save.
Всё, игровой файл создан. В моём случае движок пишет о том, что не сконфигурирован инструмент Rcedit и предлагает пути решения этого вопроса (достаточно просто отключить пункт Modify Resources в настройках экспорта, чтобы сообщение не вылезало - включим обратно, если когда-нибудь захотим поменять иконку приложения на кастомную).
Однако, и так всё работает - нужно лишь найти и открыть папку NewGame, запустив OpenDungeon.exe
Начинающим в Godot также полезно будет прочитать статью про общие полезные возможности движка:
Godot 100 мелочей
а также материал о создании игры на джем, за короткое время:
Как собрать махолёт, или Jam, Godot и Blender
FloorZ
Хватит мыслить типичными шаблонами.
_process - это не игровой цикл. Это игровой кадр, при том кадр отрисовки. Не надо забивать в него все что можно. Для ввода есть _input* асинхронные функции.
_physics_process - это кадр физики, и завязан на физическом интервале, используй его для движения и логики, если асинхронно это сделать не возможно.
Не надо задавать имя Main. Годот асинхронен и каждый объект есть автономная подсцена. Не надо пытаться выделить God Object который будет отвечать за все.
Контроль управления написан ужасно и используется старая практика, которой придерживались новички еще со времён godot 2
У игры есть методы
Input.get_action_strength()
Он возвращает числе в диапазоне 0.0-1.0 в зависимости от силы нажатия. (работает как со стиками, так и с кнопками)
А есть так же
Input.get_vector
Который возвращает 2Д вектор. В него ты передаешь силы нажатия left, right, forward, back.
В итоге в место лютой писанины, весь твой код уместится в 5-8 строк.
А не бессмысленная писанина match конструкций и if else блоков.
Вот решение большей части твоего кода:
В офф документации говорится, что не надо вращать объекты в ручную в 3Д. Есть методы look_at, всегда можно использовать вспомогательный узел и что-то еще, чем полотно векторов вращения, которые работают по принципу эйлера. rotation и rotation_degrees нужны в основном для работы в инспекторе, логику же писать через базисы и transform лучше.
Так же весь интерфейс написан не правильно.
Твой интерфейс не учитывает адаптивность мониторов. Ты не использовал якоря, хотя они есть в любом объекте типа Control. UI лучше вынести в отдельный CavansLayer узел, а не вставлять их в 3Д узел как дочерний элемент.
Логика персонажа в идеале должна быть внутри отдельного объекта, а лучше в KinematicBody. Реализовывается логика в 10-20 строчек, полностью кинематическое тело с физикой, столкновениями и т.д.
Я это веду все к тому, что вы своим примером подаете ОЧЕНЬ ПЛОХУЮ ПРАКТИКУ!
А потом я каждый день объясняю людям, что так делать не надо. Каждый день люди совершают одни и те же ошибки, не придерживаются хорошим практикам, которые описаны в официальной документации. Смотрят устаревшие гайды от людей, которые не разобрались в инструменте и учат этому других.
Советую ознакомится с этим каналом. И видео которое откроет глаза на более лучшие практики.
(Гайд писался под более старую версию движка, так что там может не быть get_vector)
Архитектором игровых механик если честно гайд и не пахнет. Зато тонна антипаттернов. Вы лучше пишите гайды связанные с дизайном игровых механик и то, с чем связана ваша профессия. А кодинг, оптимизацию оставьте специалистам.
Вы не представляете сколько раз я людям объяснял, что так делать не надо. Но каждый день в дискорд и другие группы влетают люди, что начитались и насмотрелись гайдов от соочесетвенников, особенно те что перелезли с Unity и тянут от туда даже стиль написания кода.