В предыдущей части мы создали болванки сцен: Персонажа, Оружия, Снаряда и врага, сегодня немного доработаем эти болванки, добавим разных пушек, сцену игры и главное меню. Давайте начнём с доработки сцены оружия.

Оружие

Открываем нашу сцену 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
Пример расположения Backpack

Навешиваем на 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:

DefaultCharacter
DefaultCharacter
DefaultEnemy
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 самых популярных варианта добавим в следующей части.

Комментарии (8)


  1. vedmak3
    05.07.2023 06:00
    -1

    Вопрос к автору, почему выбор пал на Godot, а не на Unity например. И если есть опыт на Unity, ваша оценка на сколько Godot годный инструмент.


    1. IsaacBlog Автор
      05.07.2023 06:00
      +1

      В Godot заинтересовал ООП подход программирования, это пожалуй был самый главный плюс, почему выбрал именно его. О Godot узнал, решил почитать немного и как-то сразу затянуло, с Unity такого не случилось, поэтому выбрал Godot. На Unity опыта нет, но лично мне Godot нравится, для 2D проектом мне он кажется весьма удобным и понятным, в 3D не пробовал.


      1. ildarin
        05.07.2023 06:00

        На юнити тоже ООП, но там шарп луа и жс вроде как. В UE тоже ООП.

        Жаль, что Вы не написали, что за язык в GODOT.


        1. IsaacBlog Автор
          05.07.2023 06:00
          +1

          На сколько я понимаю в Unity ооп есть, но не сказать, что реализован, так-же хорошо, как в Godot. В Godot используется собственный язык программирования GDScript, есть поддержка C#, и C++, еще в 3-й версии был VisualScript.


          1. ildarin
            05.07.2023 06:00

            ооп есть, но не сказать, что реализован, так-же хорошо, как в Godot

            Можно подробнее, как именно там ООП лучше реализован?


        1. ChessMax
          05.07.2023 06:00
          +2

          В Unity lua из коробки никогда не было (насколько мне известно). От поддержки js уже очень давно отказались.


          1. ildarin
            05.07.2023 06:00

            Мог ошибиться. Тогда годот хорош гибкостью выбора языка. Хотя, в шарпах можно делать плюсовые вставки, так что формально плюсы в юнити тоже есть.


  1. Zara6502
    05.07.2023 06:00
    +1

    спасибо за материал. у меня вопрос (я сильно далек от темы) - моя супруга писала анимации в Macromedia Flash (к моменту появления Adobe и ActionScript она уже этим не занималась) и там всё (практически) делалось мышкой. Позднее я пытался поглядеть что же там есть в Юнити и с год назад в Годот и я так и не понял в чем конкретно их плюс - коллективная разработка например или какие-то API? В чем то самое преимущество нежели C#+D2D (или как там сейчас 2д API называется).