Godot | Open Dungeon | Часть beta

Больше функционала для минималистичного прототипа игры: объекты уровня, враги, апгрейд управления, глобальный скрипт и статичные выстрелы.

Godot | Open Dungeon | Часть beta
что получится в итоге

Архив с готовыми файлами проекта можно взять на странице: https://thenonsense.itch.io/opendungeon

Open Dungeonthenonsense.itch.io

В этой статье мы пошагово обновим прототип alfa до состояния beta:

Запускаем Godot. Открываем проект Open Dungeon, который мы создали ранее в части alpha . Открывается сцена MainScene.

Уровень

Добавим к Main (корневой узел сцены) новый узел Spatial. Перетащим в него все копии Pol и Pillar, затем переименуем этот узел в Level_A1.

далее выбрать выбрать в списке узел Spatial
далее выбрать выбрать в списке узел Spatial
Перетащить выделенные объекты в Spatial
Перетащить выделенные объекты в Spatial
Переименовать Spatial в Level_A1
Переименовать Spatial в Level_A1

Щёлкаем по узлу правой кнопкой, выбирая Save Branch as Scene. Делаем двойной щелчок на папке Prefabs (входя в неё) затем нажимаем сверху справа Create Folder. Пишем имя папки — Levels. Ок. Сохраняем префаб уровня.

Теперь на основной сцене остался только узел level_A1, все вложенные элементы сжались внутрь него. В Godot можно включить возможность редактирования содержания сцены-префаба, не открывая его полностью (в меня по правой кнопке нужно отметить пункт Editable Children), но мы не будем этого делать и для редактирования содержимого добавленных сцен-префабов будем открывать их в отдельном окне.

Откроем Level_A1 отдельной сценой, щёлкнув по значку рядом с глазом.

Выделим узлы Pol_A и Pol_A2, скопируем (Ctrl + D). Справа на панели инспектора в разделе Transform в поле Translation допишем к 0 в ячейке Z ещё минус 10 и нажмём ввод (Enter). Каждый скопированный кусочек сдвинется на минус 10 пунктов, и мы получим уже 4 примыкающих друг к другу куска пола.

Когда нужно изменить числовые параметры в инспекторе (прибавить отнять определённое число), то вместо рассчёта и написания нового результата можно дописывать в поле к текущему параметру + число или — число, чтобы Godot вычислил новый результат автоматически. Если выделены несколько объектов, то изменение параметров полей в инспекторе будет касаться сразу их всех (если, конечно, объекты однотипные, или когда конкретный редактируемый параметр есть у всех выделенных объектов).

Также скопируем три узла Pillar и отодвинем их дальше за синюю стрелку по оси Z.

Сундуки

Выделим отдельно последний узел Pillar_A6 и разберём его обратно, на составляющее, достав из связанного состояния. Для этого нажмём на узел правой кнопкой и выберем пункт Make Loсal.

Растребушив префаб удаляем нижний приплюснутый кубик (MeshInstance3), колонну уменьшаем до куба и сдвигаем вниз, верxний сплющенный куб опускаем сверху, сохраняя на небольшом расстоянии.

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

Также нужно нажать на узел CollisionShape и тоже выбрать опцию Make Unique в выпадающем списке для его Shape. Если этого не сделать, то вы сами можете заметить, что коллизия связанна с коллизиями колонн — потянув за точку куба и увидев, что полупрозрачные кубики колонн тоже тянутся. После применения Make Unique эта коллизия станет уникальной, не связанной с прочими.

Вариант, когда Make Unique не был выполнен - коллайдеры в колоннах тянутся вслед за редактируемым коллайдером объекта.
Вариант, когда Make Unique не был выполнен — коллайдеры в колоннах тянутся вслед за редактируемым коллайдером объекта.

Переименовываем узел Pillar_A6 в Chest_A. В поле Translation в инспекторе нажимаем значок сброса кооординат, чтобы Chest_A оказался в начале координат (0 по всем трём осям).

Находим его. И превратим в префаб (правая кнопка, Save Branch as Scene).

Сейчас у нас открыта папка Levels внутри Prefabs, поднимемся выше и сохраняем Chest_A сюда. Также сохраним текущую сцену Level_A1 (Верхняя панель редактора — Scene — Save Scene).

Зайдём теперь внутрь Chest_A, щелкнув по соответствующему значку.

Выделим узел Area, и в инспекторе в ячейках Layer у Collision оставим включенной только ячейку 3, таким образом коллизия сундуков будет отличаться от коллизии колонн, располагаясь в другом слое.

Выделим корневой узел Chest_A и добавим новый узел Spatial. Переместим его немного вверх и вправо по оси Z, чтобы он оказался на боковой стороне нашего квадратного сундука, в щели между основанием и «крышкой».

Теперь перетащим «крышку» (MeshInstance4) внутрь Spatial.

Выделим узел Spatial и повернём его по оси X потянув за красный круг (как бы приоткрывая «крышку»). Или же можно вбить угол поворота в редакторе (набираем в инспекторе для Rotation Degrees -25 в ячейке X).

Сохраним текущую сцену. Закроем её вкладку, нажав на крестик рядом с названием.

Оказываемся снова в сцене уровня. Сделаем пару копий Chest_A (Сtrl + D) сдвигая каждую немного по Z. Затем выделим все и переместим правее по оси X, в сторону от колонн.

Запустим игру. Персонаж проходит сквозь сундуки, так как мы перевели их в слой, на который не реагирует рейкаст персонажа. Так пока и оставим.

Заготовка для врага

Сделаем копию последнего сундука Chest_A3. Выделим получившийся Chest_A4 и сбросим его Translation в инспекторе.

Щелкаем по Chest_A4 правой кнопкой и разбираем его на составляющие (Make Local).

Переименуем узел в Enemy_red.

У обоих кубиков (MeshInstance2 и MeshInstance4) в поле Shape инспектора применим Make Unique.

А вот в узле CollisionShape выберем другую форму коллизии вместо текущей кубической — SphereShape. Радиус сферы установим в 1.5.

Теперь нажимаем правой кнопкой на узле Enemy_red и сжимаем его в сцену-префаб (правая кнопка, Save Branch as Scene). Сохраняем в ту же текущую папку (Prerfabs) под предлагаемым именем.

Сохраняем текущую сцену уровня. Заходим внутрь сцены Enemy_red. Выделим первый кубик (MeshInstance2), откроем вкладку Material в инспекторе. Щелкнув на пустом поле создадим новый материал, выбрав New SpatialMaterial.

Щёлкаем по белой сфере и в открывшихся подробных настройках нового материала открываем вкладку Albelo. Устанавливаем красный цвет в поле Color.

Сохраняем материал, нажав на иконку дискетки и выбрав Save As…

Щёлкаем два раза по папке Materials, меняем имя на emeny_red_mat и жмём сохранить.

Выделим ещё раз узел MeshInstance2, чтобы появились его настройки в инспекторе. Кликаем по выпадающему списку на красной сфере в его поле material и выбираем опцию Copy.

Теперь выбираем следующий кубик (MeshInstance4), открываем его вкладку Material и в выпадающем списке у поля empty (либо по правой кнопке) выбираем самый нижний вариант — Paste. Красный материал копируется, и теперь оба кубика красные. Сохраним пока сцену.

Настало время настроить слои коллизий, чтобы они стали более информативными. Для этого идём в ProjectProject Settings…

Откроется окошко с кучей настроек.

Проматываем левую колонку вниз и в Layer Names кликаем на строку 3D Physics. В полях справа дописываем свои названия для некоторых слоёв. Layer 2 — solid object, Layer 3 — chest, Layer 4 — enemy, Layer 5 — player. Закрываем настройки, нажав внизу кнопку close.

Выделим теперь узел Area, откроем в инспекторе вкладку Collision, наведём на отмеченную ячейку — там горит подсказка, что слой 3 это chest (сундук). Но в данном случае мы делаем префаб врага, поэтому выключим слой 3 и включим 4 — enemy.

Теперь добавим врагу управляющий скрипт. Выделим корневой узел Enemy_red и нажмём на свиток с зелёным плюсиком правее и выше.

Жмём на иконку папки, поднимаемся из папки Prefabs на уровень выше и заходим в папку scripts, где уже лежит скрипт основной сцены — Main.gd.

Правим название на theAI_red и нажимаем Open. Теперь можно нажать Create.

Попадаем в новый созданный скрипт. Внизу можно нажать на стрелочку, чтобы показать/скрыть панельку с перечнем текущих используемых скриптов.

Видим там, что у нас сейчас выбран скрипт theAI_red.gd. Удаляем всё, что ниже первой строчки и пишем строки:

func _physics_process(delta):
	self.translation.x += delta

Сохраняем текущий выбранный скрипт, щёлкнув по FileSave. Запускаем игру (Кнопка Play, горячая клавиша F5).

Видим, что враг появляется в той же точке, что и игрок, и всё время убегает вправо.

Глобальный скрипт и враг-преследователь

Теперь заведём глобальный одиночный скрипт, который будет загружаться вне сцен и хранить какие-то данные, которые можно будет увидеть из прочих скриптов, где бы они не находились. Для этого щёлкнем на строке res:// в небольшом левом окошке с иерархией ресурсов. Нажимаем кнопку New Script.

Далее щёлкаем на значок папки. Меняем предложенное название скрипта на букву G (G.gd) и нажимаем Open. Создаём скрипт, нажав на Create.

Теперь откроем его, сделав двойной клик по G.gd в списке ресурсов.

Уберём все закомментированные строки и выше _ready() напишем строку var player_place = Vector3.ZERO . Сохраним скрипт. Таким образом сейчас у нас есть переменная player_place для хранения координат, доступная в остальных скриптах по имени G.player_place.

Заходим в ProjectProject Settings…

Переключаемся там на вкладку AutoLoad. Нажимаем на значок папки. Выбираем G.gd. Open.

Далее нажимаем самую правую кнопку Add. Появляется строчка с параметрами. Закрываем окно, нажав внизу на Close. Теперь скрипт G стал доступен из других скриптов по своему имени.

Переключимся в скрипт основной сцены theMain.gd.

Допишем ниже строки func _process() пару строк с новой функцией, реагирующей на события ввода:

func _unhandled_input(event):
	G.player_place = main_hero.global_translation

Если пользователь что-то нажимает, то мы будем отправлять координаты героя (main_hero.global_translation) в переменную глобального скрипта (player_place).

Сохраняем скрипт. Переключаемся в theAI_red.gd.

Заменим весь его код следующим:

extends Spatial

var speed = 1.0

func _physics_process(delta):
	self.look_at(Vector3(G.player_place.x,self.translation.y,G.player_place.z), Vector3.UP)

	self.translate(-Vector3.BACK * speed * delta)

Запускаем игру. Сейчас враг стал преследовать игрока по его координатам, вращаясь вокруг своей оси, если подошёл совсем близко.

Движется враг «спиной», поэтому немного подправим его сцену. Переключаемся в режим 3D, оказавшись на сцене Enemy_red. Выделим корневой узел (Enemy_red) и создадим новый Spatial (автоматически переименуется в Spatial2, так как выше есть узел с таким именем)

Переименуем созданный узел в Visual и сбросим в него все прочие узлы, которые были внутри корневого.

Теперь выделим узел Visual и во вкладке Rotation Degrees инспектора поставим поворот на 180 по Y.

Запускаем игру, враг теперь поворачивается правильной стороной.

Немного упростим запись предустановленных векторов. Переключимся на скрипты и допишем в G.gd после первой строки упрощённые записи единичных векторов по основным осям:

const _X = Vector3.RIGHT
const _Y = Vector3.UP
const _Z = Vector3.BACK

Сохраним скрипт и вернёмся в theAI_red.gd, переписав его следующим образом (ничего не меняя в логике, просто немного упростив запись):

extends Spatial

var speed = 1.0

func _physics_process(delta):
	self.look_at(Vector3(G.player_place.x,self.translation.y,G.player_place.z), G._Y)
	
	self.translate(-G._Z * speed * delta)

То есть, например, вместо записи Vector3.RIGHT мы теперь используем G._Y

Сохраняем скрипт. Закрываем сцены Enemy_red и Level_A1 (щелкая по крестикам). Оказываемся в главной сцене, переключаемся на 3D.

Добавим источник света, чтобы картина стала повеселее. Щёлкаем на узел Hero, добавляем ему новый узел OmniLight.

Поднимем источник света выше (примерно на 3 по y). В настройках источника света в инспекторе увеличим Range до 7 и включим галочку Enabled во вкладке Shadows (чтобы источник света отбрасывал тени, а не только добавлял освещённости).

Запускаем игру. Теперь всё смотрится бодрее.

Улучшаем управление

Донастроим управление. Перемещение стрелками хотелось бы дублировать кнопками WASD. Откроем ProjectProject Settings… Переключимся на вкладку Input Map.

Находим здесь строчку ui_left. Этот ключ уже используется в нашем управлении персонажем, теперь осталось добавить в него дополнительную кнопку. Нажимаем на плюсик сбоку, выбирая Key.

Появится окошко с предложением нажать кнопку. Нажимаем A и жмём ок.

Внутри группы записей под ui_left появилась новая строчка.

Аналогичным образом добавляем D в ui_right, W в ui_up и S в ui_down.

Добавим также пару своих новых ключей, связанных с кнопками мыши. Для этого в верхней строчке Action пишем имя нового ключа mouse_L и нажимаем справа кнопку Add.

Находим строчку mouse_L, нажимаем на плюс справа и на этот раз выбираем пункт Mouse Button.

Здесь сразу выставлена левая кнопка, поэтому сразу подтверждаем нажав на Add.

Аналогичным образом заведём ключ для правой кнопки мыши — mouse_R.

На этом закрываем окошко, нажав на Close. Если запустить игру сейчас, то персонаж будет управляться и клавиатурными стрелками и кнопками WASD.

Повесим теперь на кнопки мыши, например, операции включения и выключения источника света. Для этого откроем скрипт theMain.gd и допишем сверху, где-нибудь после строки onready var hero_visual = $Hero/Visual строчку onready var light = $Hero/OmniLight — чтобы завести ссылку на источник света.

Теперь сместимся по скрипту ниже и выше function _process() добавим следующие строки:

func _physics_process(delta):
	if Input.is_action_pressed("mouse_L"):
		light.show()
	
	if Input.is_action_pressed("mouse_R"):
		light.hide()

Как видно, в коде сейчас идут друг за другом несколько функций: _unhandled_input, _physics_process и _process. Для простоты можно считать, что они расположены в порядке от самой медленно срабатывающей функции, до самой быстрой. На самом деле всё немного сложнее (первая происходит лишь в моменты ввода, вторая крутится в цикле с фиксированной частотой, третья крутится в цикле с максимальной частотой), но что касается проверок ввода вроде if Input.is_action_pressed — если расположить их в _unhandled_input (хотя она предназначена скорее для обработки events), то скорость срабатывания может быть недостаточной (эффект происходит когда кнопка чётко нажата, с фиксацией на ней). В то время как внутри _physics_process скорость срабатывания скорее всего уже оптимальная, не говоря уже о _process. Проверки разового нажатия (if Input.is_action_just_pressed вместо длительного action_pressed) так и вовсе не сработают внутри _unhandled_input.

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

Можно запустить игру и убедиться, что при нажатии на кнопки мыши источник света включается и отключается. Также можно ради интереса временно перетащить проверки ввода из _physics_process в _unhandled_input, для того чтобы выяснить насколько медленнее будут срабатывать кнопки мыши. Кстати, стоит помнить, что отжатие (как и нажатие) какой-то кнопки тоже вызывает срабатывание функции _unhandled_input.

Теперь можно упростить код ранее созданного движения/вращения персонажа, сократив количество проверок и перебросив его в _physics_process. Для этого выполним сначала предварительные действия — выделим всё, что было внутри функции _prоcess и нажмём Ctrl + K, чтобы строки закомментировались и были исключены из выполнения.

Вверху скрипта удалим строчки var horizontal_joy = 0 и var vertikal_joy = 0.

Затем добавляем сюда строчку var move_direction = Vector3.ZERO

Спустимся к функции _unhanlled_input и заменим её код на такой:

func _unhandled_input(event):
	if game_mode == 1:
		G.player_place = main_hero.global_translation
		
		move_direction.x = Input.get_axis("ui_left","ui_right")
		move_direction.z = Input.get_axis("ui_up","ui_down")
		print(move_direction)
		
		if move_direction != Vector3.ZERO:
			hero_visual.look_at(hero_visual.global_transform.origin + move_direction, G._Y)

Далее переписываем функцию _physics_process таким образом:

func _physics_process(delta):
	if game_mode == 1:
		if Input.is_action_pressed("mouse_L"):
			light.show()
		
		if Input.is_action_pressed("mouse_R"):
			light.hide()
			
		if rcast.is_colliding() == false:
			if move_direction != Vector3.ZERO:
				main_hero.translation += move_direction * hero_speed * delta
		else:
			HealthDamage(1)
			if health < 0:
				health = 0
				game_mode = 0
				G.player_place = Vector3.ZERO
				move_direction = Vector3.ZERO
				ui_startscreen.show()

Запускаем игру.

В логе теперь выводится общий вектор move_direction, получаемый при вводе от пользователя в _unhandled_input. Если вектор ненулевой (то есть нужные ключи направления были нажаты), то персонаж поворачивается. Во время _physics_process персонаж, пока вектор направления остаётся куда-то повёрнут (то есть пока кнопки движения не отжаты) - движется. Если отжать кнопки движения, то в момент отжатия _unhandled_input вычислит, что move_direction снова стал нулевым вектором и, соответственно, _physics_process перестанет двигать персонажа. Правда следует учитывать, что событие отжатия может остаться необработаным функцией _unhandled_input, если пользователь в процессе куда-то переключился из окна игры - тогда персонаж залипнет в движении, до тех пор пока в контексте не окажется игровое окно и снова не поступит ввод от пользователя.

Теперь закомментированную функцию _process можно удалить, так как пока она не понадобится.

Выстрелы и прочее

Сделаем теперь простенький выстрел. Переключимся на 3D в сцену MainScene. Выделим узел Main и добавим новый узел Area.

Добавим к Area узел CollisionShape, выберем ему форму сферы (New SphereShape) в поле Shape в инспекторе.

Откроем более подробные настройки SphereShape (щёлкнув по cfvjq надписи SphereShape) и поставим радиус 0.5.

Переименуем узел Area в MagicStrike. Сохраним как префаб (правая кнопка, Save Brunch as Scene).

Щёлкнем по папке Prefabs. Создадим внутри неё папку Spawned, куда и сохраним наш префаб MagicStrike.

Заходим внутрь префаба.

Выделим корневой узел и добавим к нему (Ctrl + A) новый узел MeshInstance, чтобы сделать выстрелу какую-то визуальную форму. Заходим в новом узле в его поле Mesh в инспекторе и выбираем New PrismMesh.

В поле Scale поставим размеры 0.3 по всем осям.

Выделим корневой узел (Magic Strike) и нажмём на свиток с зелёным плюсом, чтобы добавить скрипт.

Нажимаем на иконку папки. Поднимаемся пару раз вверх.

Щелкаем два раза на папке Scripts.

Меняем название на theMagicStrike и жмём Open.

Нажимаем Create.

Открылся скрипт выстрела. Меняем его код на следующий:

extends Area

var timer = 0.0
var lifetime = 2.0

func _physics_process(delta):
	if timer > -1.0:
		timer += delta
	if timer > lifetime:
		timer = -1.0
		queue_free()

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

Переключимся на 3D, закроем сцену MagicStrike, оказавшись в MainScene.

Удалим теперь сам узел MagicStrike с главной сцены.

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

Заходим в скрипты (Scripts), открываем скрипт theMain. Пишем ниже первой строки новую: var magic_strike = preload("res://Prefabs/Spawned/MagicStrike.tscn")

Таким образом мы предзагрузим префаб-выстрел, чтобы создавать его в процессе игры.

Теперь спускаемся ниже, в функцию physics_process и пописываем в условие if Input.is_action_pressed("mouse_L"): , ниже light.show() новые строчки:

			var cloned_pref = magic_strike.instance()
			cloned_pref.transform = main_hero.transform
			cloned_pref.translation.y = 0.5
			self.add_child(cloned_pref)

Запустим игру, видим, что если нажимать правую кнопку мыши, то за персонажем образуется целая "змейка" из треугольных выстрелов. Это происходит потому, что мы поместили создание выстрела в условие if Input.is_action_pressed("mouse_L"), где is_action_pressed означает, что событие происходит всё время, когда нажата кнопка..

Поправим это, дописав выше только что написанного куска кода отдельное условие, которое будет происходить разово, только в момент нажатия кнопки. Вот, как нужно переписать эту часть:

		if Input.is_action_just_pressed("mouse_L"):
			var cloned_pref = magic_strike.instance()
			cloned_pref.transform = main_hero.transform
			cloned_pref.translation.y = 0.5
			self.add_child(cloned_pref)

Теперь на каждое нажатие левой кнопки мыши создаётся один треугольный выстрел.

Переключимся теперь в 3D в основную сцену. Зайдём в узел уровня (Level_A1).

Выделим узел врага (Enemy_red) и сделаем ещё 4 копии (Ctrl + D).

Теперь растащим их примерно по углам уровня. И одного в центре.

Запустим игру. Теперь врагов стало много.

Разве что они все сжимаются в одного, когда проходит некоторое время и во время перезапуска. Немного подправим этот момент. Открываем скрипты, заходим в theAI_red.gd и напишем временное частичное решение, переписав код таким образом:

extends Spatial

var speed = 1.0

var my_point = Vector3.ZERO

func _ready():
	my_point = self.translation

func _physics_process(delta):
	self.look_at(Vector3(G.player_place.x,self.translation.y,G.player_place.z), G._Y)
	
	self.translate(-G._Z * speed * delta)
	
	if G.player_place == Vector3.ZERO:
		self.translation = my_point

Теперь каждый враг запоминает свою начальную позицию, а также смотрит - не оказался ли внезапно игрок в нулевых координатах (что случается при рестарте уровня). Таким образом слепившихся вместе врагов можно сбросить на их начальные позиции, потеряв всё здоровье от соприкосновения с колоннами.

На этом пока и остановимся.

 

Источник

Читайте также