В предыдущей части мы создали болванки сцен: Персонажа, Оружия, Снаряда и врага, сегодня немного доработаем эти болванки, добавим разных пушек, сцену игры и главное меню. Давайте начнём с доработки сцены оружия.
Оружие
Открываем нашу сцену DefaultWeapons, это та, которая болванка для оружия.
Для начала поработаем немного с анимацией стрельбы, сейчас мы имеем, что при удержании кнопки выстрела воспроизводится анимация выстрела, при отпускании она прекращается, то-есть у оружия с долгой перезарядкой анимация выстрела будет проигрываться, даже когда оружие на перезарядке. Или если быстро нажать кнопку выстрела, то пуля полетит, а анимация не воспроизведётся. Поэтому будем воспроизводить анимацию выстрела по условию, если нажата кнопка выстрела и оружие не на перезарядке.
func get_input(): if ((global_position - get_global_mouse_position()).x < 0): _animated_sprite.flip_v = false else: _animated_sprite.flip_v = true look_at(get_global_mouse_position())# направляем взгляд оружия на курсор мыши if (Input.is_action_pressed('fire') && _fire_couldown_timer.is_stopped()):#нажат выстрел и нет перезарядки _animated_sprite.play("Fire")#анимация выстрела fire()#вызываем функцию выстрела
Добавляем сигнал от AnimatedSprite о том, что анимация проигралась до конца(animation_finished())
func _on_AnimatedSprite_animation_finished(): if (_animated_sprite.animation == "Fire"):#если анимация выстрела, то меняем на обычную _animated_sprite.play("Default")
Дальше давайте разберёмся с функцией выстрела. Наше стандартное оружие не обязательно учить стрелять, поэтому можно просто прописать в функции выстрела pass и для каждого отдельного оружия писать механику выстрела именно для этого оружия.
Полный листинг Оружия
extends KinematicBody2D #Добавляем элементы дерева объектов в код onready var _animated_sprite = $AnimatedSprite onready var _fire_couldown_timer = $FireCouldownTimer #Объявляем переменные, которые можно менять извне export (PackedScene) var bullet_scene # это будет сцена нашей пули export var fire_rate = 0.2 # скорость атаки export var damage = 1 # урон #функция обработки нажатий func get_input(): if ((global_position - get_global_mouse_position()).x < 0): _animated_sprite.flip_v = false else: _animated_sprite.flip_v = true look_at(get_global_mouse_position())# направляем взгляд оружия на курсор мыши if (Input.is_action_pressed('fire') && _fire_couldown_timer.is_stopped()):#нажат выстрел и нет перезарядки _animated_sprite.play("Fire")#анимация выстрела fire()#вызываем функцию выстрела func _ready(): _fire_couldown_timer.wait_time = fire_rate # выставляем скорость атаки func spawn_bullet(rot):# передаём параметр дополнительного поворота пули, позже пригодится var b = bullet_scene.instance() var new_position = position var direction = rotation - rot get_parent().add_child(b)# добавляем пулю, как потомка оружия b.start(new_position,direction) b.damage = damage# задаём пуле урон func _on_AnimatedSprite_animation_finished(): if (_animated_sprite.animation == "Fire"):#если анимация выстрела, то меняем на обычную _animated_sprite.play("Default") # функция выстрела func fire(): pass #Это дефолтный класс, ему нет нужны стрелять, для каждого другого оружия #будем определять класс стрельбы заного. func _physics_process(delta): get_input()
Пуля
Тут в принципе особо ничего изменять не надо, только добавить функцию обработки поведения при столкновении, потому-что например ракета должна взрываться и ей потребуется другая обработка, в отличии от обычного выстрела. Вызывать функцию стоит при любом обнаружении столкновений. Для болванки пули, также оставляем функцию пустой.
#Функция обработки поведения при столкновении func collision_action(_collision_object):#_collision_object - объект столкновения pass
Полный листинг Пули
onready var _animated_sprite = $AnimatedSprite var velocity = Vector2() export var damage = 1 export var speed = 750 #функция для задания стартового положения func start(pos, dir): rotation = dir position = pos velocity = Vector2(speed, 0).rotated(rotation) func _physics_process(delta): var collision = move_and_collide(velocity * delta) if collision: collision_action(collision.colider) #Функция обработки сигнала от VisibilityNotifier, Сигнал screen_exited func _on_VisibilityNotifier2D_screen_exited(): queue_free() #Функция обработки поведения при столкновении func collision_action(_collision_object):#_collision_object - объект столкновения pass
С настройками оружия разобрались, давайте перейдём к созданию нового и доработке старого оружия.
Бластер

Сцены оружия и пули для бластера остаются без изменений. Для начала настроим пулю. Нам нужно добавить для пули обработчик столкновения. Пкм на корень сцены -> Расширить скрипт
В созданном скрипте, нужно переопределить функцию collision_action().
func collision_action(_collision_object): if _collision_object.has_method("hit"): #Вызвали метод, если он есть _collision_object.hit(damage) queue_free()#Удалили пулю
Перейдём к настройке оружия, выставляем его характ��ристики, у меня (урон = 1, перезарядка = 0.2) и не забываем прицепить сцену нашей пули.
Дальше расширяем скрипт бластера и переопределяем функцию fire()
func fire(): if (_fire_couldown_timer.is_stopped()): # не на перезарядке spawn_bullet(0) # создаём пулю с 0-м дополнительным поворотом _fire_couldown_timer.start()

Наш бластер готов идём дальше
Дробовик
Для дробовика сцены оставляем стандартными(конечно ставим новые спрайтики и анимации). Расширяем скрипт для пули и переопределяем функцию collision_action(), она будет такая-же, как для бластера.
Расширяем скрипт для дробовика и переопределяем функцию fire()
func fire(): if (_fire_couldown_timer.is_stopped()): spawn_bullet(PI/12)# Поворачиваем пулю на ~15 градусов spawn_bullet(PI/24)# Поворачиваем пулю на ~7,5 градусов spawn_bullet(0)# выпускаем пулю прямо spawn_bullet(-PI/24)# Поворачиваем пулю на ~-7,5 градусов spawn_bullet(-PI/12)# Поворачиваем пулю на ~-15 градусов _fire_couldown_timer.start()# включаем перезарядку
Для дробовика я выставил урон = 1 и скорость атаки = 0.5

Винтовка
Сцены для винтовки никак изменять не надо, только конечно заменить анимации. Скрипт выстрела будет такой-же, как для бластера.
Пуля из винтовки должна будет способна пробить несколько врагов, если враг после попадания умер. Перейдём сразу к расширению скрипта пули. Функция collision_action() примет следующий вид:
export var fly_count = 3 #сколько врагов пробьёт на сквозь var previous_enemy #враг который уже получил урон func collision_action(_collision_object): if (_collision_object.has_method("hit") && previous_enemy != _collision_object): #Вызвали метод, если он есть #и враг не равен предыдущему _collision_object.hit(damage) fly_count -=1 # уменьшили счётчик возможных пробиваний previous_enemy = _collision_object # запомнили врага if (fly_count > 0): # если счётчик не 0, продолжаем движение move_and_slide(velocity) else: queue_free() else: queue_free()
И обязательно не забудьте объявить новые переменные.
Для винтовки я выставил урон 5 и перезарядку 2, функция fire(), ничем не отличается от бластера.

БАЗУУУУКААААА
Какой экшен-шутер может обойтись без красивых взрывов от выстрела из ракетницы, правильно никакой. Тут уже нужно будет немного повозиться с сценой ракеты, поэтому начнём с сцены самой базуки.
Тут ничего добавлять не нужно, только изменить характеристики и добавить спрайты базуки. У меня урон = 3, а перезарядка = 5.
Теперь переходим к интересному, к сцене ракеты.
Тут есть как говорится 2 стула. Можно либо создать отдельную сцену для взрыва, и когда снаряд касается любого игрового объекта через код создавать взрыв и удалять снаряд, либо создать взрыв, как вторую анимацию у ракеты и добавить ещё пару элементов на сцену. Если в вашем проекте предусмотрено несколько объектов, которые должны взрываться( различные гранаты, ракетницы ) , то скорее всего стоит выбрать первый вариант, я же выбрал второй вариант, он как-то попроще, на мой взгляд.

Для начала в дерево объектов следует добавить:
⦁ Area2D(Bum)
⦁ CollisionShape2D(CollisionShapeBum), как дочерний к Area2D
⦁ Timer(BumLiveTime),как дочерний к Area2D
И у CollisionShapeBum, в инспекторе параметр Disabled, включить. Подогнать все элементы. У меня это выглядит следующим образом:

⦁ красным обведено - CollisionShapeBum
⦁ зелёным - CollisionShape2D
Таймер будем задавать маленьким, у меня 0.2 секунды, это время жизни взрыва, он всё-таки не моментально взорвался и всё, у него обязательно сделать Oneshot = true.
Логика проста, когда объект с чем-то коснулся, он становится взрывом CollisionShape2D, выключается, а CollisionShapeBum включается. Добавляем в скрипт сигнал от Area2D body_entered, на вхождение тела в область коллизии и в нём уже наносим урон, если это возможно
Расширяем скрипт и переходим к его редактированию:
onready var _collision_shape = $CollisionShape2D#Фигура столкновений ракеты onready var _collision_shape_bum = $Bum/CollisionShapeBum#Фигура столкновений взрыва onready var _bum_live_time = $Bum/BumLiveTime #Таймер жизни взрыва func collision_action(_collision_object):# обработка столкновения снаряда if(_animated_sprite.animation == "Fly"):# если он был снарядом _animated_sprite.play("Bum")# превращаем в взрыв _collision_shape.disabled = true # выключаем обычную фигуру столкновения _collision_shape_bum.disabled = false # включаем фигуру столкновения взрыва scale = Vector2(10,10) # увеличиваем размер в 10 раз, #у вас может быть в другое количество раз, для моего проекта это в самый раз velocity=Vector2(position.x,position.y) _bum_live_time.start() func _on_Bum_body_entered(body): if(body.has_method("hit")): body.hit(damage) func _on_BumLiveTime_timeout(): queue_free()

Перейдём к модификации сцены персонажа.
Персонаж
К болванке сцены персонажа, нужно привязать камеру.
Сделать что-то вроде рюкзака, в котором будет хранится используемое оружие.
Добавить сигналы о получении персонажем урона и смерти
Начнём с простого. На узел DefaultCharacter добавляем Camera2D и в инспекторе выставляем current = true. На этом всё, камеру добавили.
Что я подразумеваю под рюкзаком, на сцене будет узел содержащий 6 position2D, на этих позициях будет отображаться оружие которое сейчас есть у персонажа. У самого персонажа будет массив с ссылками на сцены оружия, которое он использует. Начнём с добавления узла рюкзак. К персонажу добавляем Node2D(Backpack) и 6 Position2D(Slot1...Slot6), как дочерние у Node2D:


Навешиваем на Backpack скрипт и переходим к его редактированию. Нам нужно добавить простенькую функцию, которая возвращает Вектор позиции данного слота.
extends Node2D #Массив Слотов onready var backpack = [$Slot1,$Slot2,$Slot3,$Slot4,$Slot5,$Slot6] #функция получения текущего положения func get_slot_position(elem): return backpack[elem].position
Дальше переходим к редактированию скрипта персонажа:
В начале скрипта нам нужно объявить новую переменную, backpack_items, на 6 эллементов, в нём будет хранится либо сцена, либо null. Так-же объявить сигнал take_damage(damage) и dead
#Рюкзак персонажа var backpack_items = [preload("res://scenes/Weapons/Shotgun/Shotgun.tscn"),null,null,null,null,null] #Сигнал о получении урона signal take_damage(damage) #Сигнал о смерти signal dead
В функцию получения урона нужно добавить отправку сигналов:
#Функция получения урона func take_damage(dmg): if(_immortal_timer.is_stopped()): #Проверяем не бессмертен ли наш персонаж health -= dmg _animated_sprite.play("TakeDamage") emit_signal("take_damage",dmg) #Отправляем сигнал о получении урона _immortal_timer.start() #Запускаем таймер после получения урона if(health == 0): emit_signal("dead")#Отправляем сигнал о смерти
Теперь перейдём к созданию функций по работе с рюкзаком.
Сначала напишем функцию, которая принимает в качестве аргумента номер слота от 0 до 5, в котором нужно отрисовать оружие:
#функция прикрепления оружия func equip_item(slot):# передаём номер слота в котором нужно отрисовать оружие if (backpack_items[slot] != null):#Если слот объявлен var weapon = backpack_items[slot].instance() weapon.position = _backpack.get_slot_position(slot)#получаем позицую данного слота weapon.name = "WeaponSlot" + String(slot)#Именя оружия WeaponSlot0..5 add_child(weapon) weapon.scale = Vector2(0.5,0.5)# у меня стоит масштабировать оружие, возможно у вас нет
Дальше нам понадобится функция которая будет создавать все элементы рюкзака на сцене и если какой-то на сцене уже есть, то удалять его и рисовать заново:
#одеваем всё доступное оружие func equip_all(): for i in range(6):#Пробегаем по всему массиву backpack_item if(get_node("WeaponSlot"+String(i)) != null): var item =get_node("WeaponSlot"+String(i)) #Ищем узел if (item != null): #Если есть то удаляем его со сцены item.queue_free() equip_item(i)# и рисуем новый
Так-же понадобится функция для удаления оружия у персонажа, которая в качестве аргумента получает номер слота из которого нужно удалить объект:
#удаляем оружие func remove_equip_item(slot):#Передаём номер слота if (slot >= 0 && slot <=5):#Проверяем номер слота var item = get_node("WeaponSlot" + slot) backpack_items[slot] = null#обнуляем значение в рюкзаке item.queue_free()#удаляем объект
И в конце понадобится функция которая будет добавлять предмет в рюкзак, в качестве аргумента будет принимать сцену:
#добавляем оружие func add_equip_item(item): for i in range(6): if (backpack_items[i] == null):#Находим первый пустой элемент массива backpack_items[i] = item#заливаем в него сцену оружия equip_all()#Одеваем всё оружие return
Полный скрипт персонажа
extends KinematicBody2D #Добавляем элементы дерева объектов в код onready var _animated_sprite = $AnimatedSprite onready var _idle_animation_timer = $IdleAnimationTimer onready var _immortal_timer = $ImmortalTimer onready var _backpack = $Backpack #Объявляем переменные, которые можно менять извне export var health = 5 #Жизни export var speed = 200 #Скорость #Объявляем переменные только для этого скрипта var velocity = Vector2.ZERO #Вектор направления var direction = Vector2.ZERO #Вектор движения var backpack_items = [preload("res://scenes/Weapons/Blaster/Blaster.tscn"),preload("res://scenes/Weapons/Blaster/Blaster.tscn"),null,null,null,null] #Сигнал о получении урона signal take_damage(damage) #Сигнал о смерти signal dead #Функция считывания нажатий func get_input(): velocity = Vector2.ZERO if Input.is_action_pressed("left"): velocity.x -= 1 if Input.is_action_pressed("right"): velocity.x += 1 if Input.is_action_pressed("up"): velocity.y -= 1 if Input.is_action_pressed("down"): velocity.y += 1 direction = velocity.normalized() * speed #Функция воспроизведения анимаций func get_anim(): if (_immortal_timer.is_stopped()): #Проверяем не воспроизводится-ли анимация бессмертия if (velocity != Vector2.ZERO): #Если есть направление движения, то идём _animated_sprite.play("Walk") else: if (_animated_sprite.animation != "IdleAnimation"): #Иначе если не брутальная анимация, то просто стоим _animated_sprite.play("Stand") if (_idle_animation_timer.is_stopped()): #Запускаем отчёт до брутальной анимации _idle_animation_timer.start() if (velocity.x > 0): # поворачиваем нашего персонажа в сторону движения _animated_sprite.flip_h = false if (velocity.x < 0): _animated_sprite.flip_h = true #Функция получения урона func take_damage(dmg): if(_immortal_timer.is_stopped()): #Проверяем не бессмертен ли наш персонаж health -= dmg _animated_sprite.play("TakeDamage") emit_signal("take_damage",dmg) #Отправляем сигнал о получении урона _immortal_timer.start() #Запускаем таймер после получения урона if(health == 0): emit_signal("dead")#Отправляем сигнал о смерти func _ready(): _animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять equip_all() func _physics_process(delta): get_input() get_anim() var collider = move_and_collide(direction * delta) # записываем в переменную collider для дальнейшей обработки столкновения func _on_IdleAnimationTimer_timeout(): _animated_sprite.play("IdleAnimation") # Включаем БРУТАЛЬНУЮ анимацию по истечении таймера #функция прикрепления оружия func equip_item(slot):# передаём номер слота в котором нужно отрисовать оружие if (backpack_items[slot] != null):#Если слот объявлен var weapon = backpack_items[slot].instance() weapon.position = _backpack.get_slot_position(slot)#получаем позицую данного слота weapon.name = "WeaponSlot" + String(slot)#Именя оружия WeaponSlot0..5 add_child(weapon) weapon.scale = Vector2(0.5,0.5)# у меня стоит масштабировать оружие, возможно у вас нет #одеваем всё доступное оружие func equip_all(): for i in range(6):#Пробегаем по всему массиву backpack_item if(get_node("WeaponSlot"+String(i)) != null): var item =get_node("WeaponSlot"+String(i)) #Ищем узел if (item != null): #Если есть то удаляем его со сцены item.queue_free() equip_item(i)# и рисуем новый #удаляем оружие func remove_equip_item(slot):#Передаём номер слота if (slot >= 0 && slot <=5):#Проверяем номер слота var item = get_node("WeaponSlot" + slot) backpack_items[slot] = null#обнуляем значение в рюкзаке item.queue_free()#удаляем объект #добавляем оружие func add_equip_item(item): for i in range(6): if (backpack_items[i] == null):#Находим первый пустой элемент массива backpack_items[i] = item#заливаем в него сцену оружия equip_all()#Одеваем всё оружие return
Давайте ещё изменим стандартный курсор, на нарисованный нами
Курсор
Переходим в настройки проекта -> Дисплей -> Курсор мыши. В пользовательском изображении, указываем путь к нашему курсору.
Перейдём к созданию меню и главной сцены, но для начала нам нужно написать небольшой синглтон, который будет отвечать за переключение сцен.
Синглтон SceneLoader
Для начала перейдите на вкладку скрипт и создайте скрипт файл. Обязательно проверьте чтобы он наследовал узел Node. Я сохраним его в отдельную папку scripts

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

Переходим к редактированию этого скрипта:
extends Node # Константа хранящая путь к папке со сценами const MAP_ROOT = "res://scenes/Game/" var current_scene = null # текущая сцена #При первом запуске объявляем текущую сцену func _ready(): var root = get_tree().root current_scene = root.get_child(root.get_child_count() - 1) #подготавливаем полный путь сцены, тоесть сюда будет передаваться только название func build_map_path(map_name): var path = MAP_ROOT.plus_file(map_name + "Scene/"+ map_name + ".tscn") #у меня сцены карт хранятся в папке scenes/Game/Папка с названием сцены/файл с названием сцены.tscn _change_map(path) #функция смены карты func _change_map(path): #безопасно удаляем текущую сцены current_scene.free() # загружаем в в переменную текущей сцены, требуемую сцену current_scene = ResourceLoader.load(path).instance() # добавляем требуемую сцену, как потомка корня дерева get_tree().root.add_child(current_scene) # передаём параметр установленной сцены через API get_tree().current_scene = current_scene
Тут закончили теперь давайте накидаем простенькое главное меню
Главное меню
Будем рассуждать логически у главного меню должно быть что-то похожее на задний фон и кнопочки. В нашем проекте я ещё добавлю анимацию танцующего главного героя на задний фон. Главным узлом сцены выбираем Node2D(MainMenu). Дальше добавляем элементы в дерево объектов:

⦁ Sprite(Background)
⦁ AnimatedSprite
⦁ TextureButton(StartGameBtn)
⦁ Label(StartGameLbl) - подчинённый TextureButton
⦁ TextureButton(SettingBtn)
⦁ Label(SettingLbl) - подчинённый SettingBtn
⦁ TextureButton(ExitGameBtn)
⦁ Label(ExitGameLbl) - подчинённый ExitGameBtn
В инспекторе у надписей нужно написать сам текст кнопки, дальше в инспекторе переходим во вкладку Control-> Theme Overrides -> Fonts и в свойстве Font, выбираем новый DynamicFont, дважды нажимаем на него, открываем свойство Font и указываем путь к нашему шрифту. Дальше из меню Fonts->Font выбираем Settings и указываем желаемый размер шрифта( у меня 36). Шрифт настроили, теперь давайте настроим расположение. В верхней панели нажимаем "Макет" и выбираем в нём расположение "По центру".
Меню примерно должно выглядеть так:

Персонаж на экране телефона как раз таки и есть AnimatedSprite, он будет танцевать. Сцену настроек сделаем в следующей части статьи.
Перейдём к скрипту:
extends Node2D #Переменные onready var _animated_sprite = $AnimatedSprite onready var _start_btn = $StartGameBtn onready var _settings_btn = $SettingsBtn onready var _exit_btn = $ExitGameBtn #При запуске включается анимация func _ready(): _animated_sprite.play("Dance") #Когда анимация закончилась, персонаж поворачивается func _on_AnimatedSprite_animation_finished(): _animated_sprite.flip_h = !_animated_sprite.flip_h #При нажатии кнопки начала игры, вызывается функция нашего Синглтона и переключается на сцену игры func _on_StartGameBtn_pressed(): SceneLoader.build_map_path("GameScene") #При нажатии кнопки выхода, игра закрывается func _on_ExitGameBtn_pressed(): get_tree().quit()
Скрипт короткий и вполне понятный, получаем сигналы с кнопок и обрабатываем их.
Сейчас мы сделаем простенькую сцену игры, враги будут появляться случайно по периметру карты. За каждую прожитую секунду будут начисляться очки.
Для начала создадим сцену с пользовательским интерфейсом:
Пользовательский интерфейс
Главным узлом сцены выбираем Node2D, дочерние к нему элементы:

⦁ ColorRect(CurrentHealthBar)
⦁ TextureRect(HealthBar) - дочерний у ColorRect
⦁ Label(Health) - дочерний у ColorRect
⦁ Label(Score)
⦁ Timer(ScoreAddTimer) - дочерний у Label
Label(Health) - будет отображать текущий и максимальный запас жизней, через слэш.
ColorRect - это просто зелёная полоска жизней, которая будет уменьшаться, как у врагов
TextureRect - это текстура HealthBar
Label(Score) - отображает заработанные очки
Timer - отчёт до начисления очков( 1 сек)

Теперь навешиваем скрипт на Node2D и переходим к его редактированию:
extends Node2D #Элементы дерева onready var _score_add_timer = $Score/ScoreAddTimer onready var _score = $Score onready var _current_health_bar = $CurrentHealthBar onready var health = $CurrentHealthBar/Health var max_health #Максимальное кол-во хп var current_health #Текущее кол-во хп var health_bar_size #Размер на который нужно уменьшать зелёный квадрат # Функция заадния всех переменных func init_health(hp): max_health = hp current_health = hp health.text = String(current_health) + "/" + String(max_health)#записываем в лэйбэл наши хп health_bar_size = _current_health_bar.rect_size.x / max_health# определяем длинну деления # При старте запускаем таймер func _ready(): _score_add_timer.start() # Функция вызываемая при получении урона func take_damage(damage): current_health -= damage health.text = String(current_health) + "/" + String(max_health) _current_health_bar.rect_size.x -= health_bar_size * damage # Сигнал от таймера func _on_ScoreAddTimer_timeout(): _score.text = String(int(_score.text) + 1)
Прокомментировал всё достаточно подробно, не вижу смысла что-то дополнять
Теперь создаём сцену игры и переходим к её редактированию.
Сцена игры
Главным элементом сцены выбираем Node2D, дочерние элементы:

⦁ StaticBody2D, дочерние к нему
⦁ Sprite(background), дочерний к StaticBody2D
⦁ 4 CollisionShape2D, дочерний к StaticBody2D
⦁ Timer(MobSpawnTimer)
⦁ Path2D(MobPath)
⦁ PathFollow2D(MobFollowPath), дочерний к Path2D
⦁ Сцена нашего персонажа
⦁ Сцена UserInterface, дочерняя к нашему персонажу
Для чего-же нам StaticBody с 4-мя CollisionShape, Это будут граница экрана за которые персонаж не может выйти, как примерно стоит всё это расположить:

Для StaticBody2D, Нужно настроить CollisionObject2D, как мы это делали в предыдущей части, теперь нужно дать название 5-му слою в списке(у меня это Map) и присвоить нашему StaticBody2D, Layer 5. Дальше отредактируем столкновение для DefaultCharacter и DefaultEnemy:


Спавнить зомби, будем так-же, как и в первой статье на тестовой сцене, в следующей части это переработаем.
Навешиваем на Node2D скрипт и переходим к его редактированию:
#Сцена Врага export (PackedScene) var zombie_enemy #Элементы дерева onready var _mob_spawn_timer = $MobSpawnTimer onready var player = $BrutalHero onready var _user_interface = $BrutalHero/UserInterface #Функция призыва Зомби func spawn_zombie(): var z = zombie_enemy.instance() var zombie_spawn_location = $MobPath/MobPathFollow zombie_spawn_location.unit_offset = rand_range(0.0,1.0)#Генерируем случайную точку спавна z.position = zombie_spawn_location.position#Настраиваем позицию z.player = player get_parent().add_child(z)# добавляем зомби z.speed = 2# присваеваем ему статы z.health = 5 func _ready(): _mob_spawn_timer.start() randomize()# подключаем генератор случайных чисел _user_interface.init_health(player.health)# инициализируем наш UI func _on_MobSpawnTimer_timeout(): spawn_zombie() spawn_zombie() spawn_zombie() _mob_spawn_timer.start() #Добавляем сигнал, от нашего персонажа на получение урона func _on_BrutalHero_take_damage(damage): _user_interface.take_damage(damage) #Добавляем сигнал, от нашего персонажа о смерти func _on_BrutalHero_dead(): get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
Всё подробно прокомментировал, стоит остановится только на одном моменте, в строке get_tree().call_group("all_enemy", "queue_free") (строка 42), вызываем какую-то группу, что-то с ней делаем, как это так?
Для этого нам нужно перейти на сцену DefaultEnemy, Нажать на главный узел сцены и перейти на вкладку "Узел". На вкладке Узел перейти к Группам, ввести название(all_enemy) и нажать добавить. Теперь когда будет появляться экземпляр сцены DefaultEnemy, он будет добавляться в группу "all_enemy", и при вызове функции queue_free(), мы удаляем всех, кто находится в этой группе.
Вот в принципе и всё. На этом мы можно сказать сделали первую ULTRA-alfa версию нашей игры. В следующей части добавим музыку и звуковые эффекты, а то играть можно, но в тишине скучно. Так-же добавим цель для игры, а то на данном этапе набранные нами очки даже никуда не сохраняются и не отображаются. Добавим меню выбора персонажа и различных персонажей, переработаем сцену игры и добавим большое количество врагов.
Голосуйте в опросе, 3 самых популярных варианта добавим в следующей части.
