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

Дерево талантов

В чём вообще вся задумка. Игрок может в любое время посмотреть своё дерево талантов и если у него есть очки прокачки, то прокачать.

Само по себе дерево будет состоять из ветвей, при нажатии на ветвь появляется окно с описанием и 2 кнопки, прокачать или нет. Если игрок прокачивает, то об этом отправляется сигнал. Если коротко, то примерно так.

Ветвь дерева

Создаём новую сцену и главным узлом сцены выбираем TextureButton, дочерние элементы:

  • Sprite

  • Node2D(DescriptionNode)

  • ColorRect(Description), дочерний у DescriptionNode

  • Label(DescriptionLbl), дочерний у Description

  • TextureButton(DescriptionAccept), дочерний у Description

  • TextureButton(DescriptionCancel), дочерний у Description

У TreeBranch добавляем Texture, и ставим Disabled - true. В Sprite заливаем иконку по умолчанию. У DescriptionNode, выставляем Z Index = 10. В Description, устанавливаем текстурку так-же нужно поставить точку вращения объекта в центр текстуры, для этого выбираем и нажимаем в примерный центр текстуры. В DescriptionLbl добавляем шрифт. В DescriptionAccept и DescriptionCancel, добавляем текстурки. Пока на этом всё, добавляем скрипт на корень и переходим к его редактированию. После всех настроек ставим у DescriptionLbl Visible = false.

Как ветвь дерева выглядит у меня:

DescriptionLbl Visible - false
DescriptionLbl Visible - false
DescriptionLbl Visible - true
DescriptionLbl Visible - true

Варианты дерева прокачек будут следующими: увеличение атаки, увеличение здоровья, увеличение скорости, сделать оружие "СУПЕР". Все данные мы будем задавать через редактор, а именно: описание, предыдущую ветвь, какой параметр улучшаем, на какое процент увеличиваем показатель, какое оружие улучшается, ветви на этом слое и картинка. Так-же потребуется 3 сигнала, один будет срабатывать, когда мы прокачали эту ветвь, второй будет срабатывать при открытии описания и сигнализировать, что другие нужно закрыть, третий будет сигнализировать о том, что нужно прокачать параметр и указывать на сколько.

#Элементы дерева
onready var _description = $DescriptionNode
onready var _description_Lbl = $DescriptionNode/Description/DescriptionLbl
#Описание ветви
export(String, MULTILINE) var description_text
#Предыдущая ветвь
export (NodePath) var previous_branch
#Какой параметр улучшаем
export(int, "Atack", "Speed", "Health", "WeaponUp") var param_up
#на сколько % улучшаем
export var param_scale:float
#какое оружие делаем "Супер", если выбран WeaponUp
export (int, "Blaster", "Shotgun", "Rifle", "Bazooka") var param_weapon_name
#массив ветвей на этом слое
export (Array, NodePath) var this_layer_branch
#картинка, которая заливается в img
export (Texture) var img
#сигнал о прокачке ветви
signal branch_pick
#сигнал о открытии описание этой ветви
signal close_description
#сигнал улучшения навыка(урон, скорость, хп)
signal skill_up(param_name,value)

Теперь напишем функцию _ready(), в ней буду заливаться выбранная нами картинка таланта, и привязываться сигналы, от элементов, которые мы записали в export - переменные

func _ready():
	#Установили режим паузы для дерева
	pause_mode = Node.PAUSE_MODE_PROCESS
	#Залили картинку
	$Sprite.texture = img
	#Развернули картинку
	$Sprite.rotation_degrees -= rect_rotation
	#Развернули описание
	$DescriptionNode/Description.rect_rotation -=rect_rotation
	#Объявили предыдущую ветвь
	var branch = get_node(previous_branch)
	#Если не Null, то добавили сигнал
	if (branch != null):
		branch.connect("branch_pick",self,"_on_branch_pick")
	else:
	#Если у дерева нет предыдущих ветвей, то это корень дерева
		disabled = false
	#Пробегаемся по массиву ветвей на этом слое
	for i in range(this_layer_branch.size()):
		#Обхявили ветвь
		var layer_branch = get_node(this_layer_branch[i])
		#Если не null
		if (layer_branch != null):
			#То привязали 2 сигнала
			layer_branch.connect("branch_pick",self,"_on_branch_pick_this_layer")
			layer_branch.connect("close_description",self,"_on_close_description_this_layer")

Давайте сейчас напишем функцию, которая будет определять, как прокачивать персонажа

#Функция обработки выбора ветви
func skill_up():
	match param_up:
		0,1,2:
			#Если это атака, хп или скорость
			emit_signal("skill_up",param_up,param_scale)
		3:
			#Если прокачали оружие, то вызвали функцию синглтона
			WeaponsName.weapon_level_up(param_weapon_name)

Ещё нам нужно дописать в Синглтон WeaponsName, объявление переменных, которые становятся true, если оружие стало супер и функцию, которая определяет, какое оружие сделать супер и функцию очистку, которая делает все переменные false:

var blaster_up = false
var shotgun_up = false
var rifle_up = false
var bazooka_up = false

func weapon_level_up(weapon):
	match weapon:
		0:
			blaster_up = true
		1:
			shotgun_up = true
		2:
			rifle_up = true
		3:
			bazooka_up = true

func clear_all():
	blaster_up = false
	shotgun_up = false
	rifle_up = false
	bazooka_up = false

Теперь добавим в код сигналы от TreeBranch, DescriptionAccept, DescriptionCancel, и напишем функции обработки сигналов, которые привязали в _ready()

#Обработка сигнала предыдущей ветви,
#тоесть если выбрали предыдущую ветвь,
#То снять disabled
func _on_branch_pick():
	disabled = false
	
#Обработка сигналов, ветви с этого слоя,
#если выбрали с этого слоя,
#то блокируем эту ветвь	
func _on_branch_pick_this_layer():
	disabled = true
	
#Обрабатываем сигнал от других ветвей этого слоя, 
#тоесть если открыли другое описание, то закрыли это
func _on_close_description_this_layer():
	_description_Lbl.text = ""
	_description.visible = false
	
#Прикрепляем сигнал от TreeBranch и обрабатываем
func _on_TreeBranch_pressed():
	#Если у родительской сцены, переменная 
	#level_up_count > 0, показываем описание
	if(get_parent().level_up_count > 0):
		emit_signal("close_description")
		_description_Lbl.text = description_text
		_description.visible = true

#Прикрепляем сигнал от DescriptionCancel и обрабатываем
func _on_DescriptionCancel_pressed():
	_description_Lbl.text = ""
	_description.visible = false

#Прикрепляем сигнал от DescriptionAccept и обрабатываем
func _on_DescriptionAccept_pressed():
	#у меня в текстурке texture_pressed хранится текстура, 
	#как выглядила бы ветвь, после выбора
	texture_disabled = texture_pressed
	_description_Lbl.text = ""
	_description.visible = false
	#Отправили сигнал
	emit_signal("branch_pick")
	#Вызвали функцию
	skill_up()
	#Уменьшили родительский счётчик
	get_parent().level_up_count -=1
	disabled = true

Код прокомментирован подробно, и теперь у нас есть универсальная сцена ветви дерева, которую мы будем добавлять на сцену дерева, настраивать все export переменные и она будет работать.

Сцена дерева талантов

Главным узлом сцены выбираем Node2d, добавляем sprite и дальше собираем наше дерево. Все его ветви можно спокойно вращать.

Примерный вид сцены
Примерный вид сцены
вид в игре
вид в игре

Как расположить элементы в дереве объектов и назвать. Я называл так: Layer№Branch№.

Примеры настроек ветвей:

Эта ветвь будет увеличивать параметр скорости на 10%, является корнем дерева и больше других ветвей нет.

Эта ветвь будет улучшать оружие(винтовку), Так-же на этом-же слое есть 4 ветви и указана предыдущая.

С настройками сцены разобрались, добавляем скрипт и переходим к редактированию:

extends Node2D
#Переменная игрока, будем передавать со сцены игры
var player
#Переменная хранящая количество прокачек
var level_up_count = 0
#Сигнал о прокачке
signal branch_skill_up(param, scale)


func _ready():
	init_tree()
	
#Считываем нажатия, если нажата кнопак меню, то ставим игру на паузу,
#и показываем, выставляем масштаб, и местоположение
func _process(delta):
	if (Input.is_action_just_pressed("menu")):
		get_tree().paused = !get_tree().paused
		visible = !visible
		scale = Vector2(1,1)
		position = player.position
#Привязываем сигнал от всех потомком и прокачке
func init_tree():
	for i in get_child_count():
		get_child(i).connect("skill_up",self,"_on_branch_skill_up_")

#Обрабатываем этот сигнал(отправляя свой) 		
func _on_branch_skill_up_(param, scale):
	emit_signal("branch_skill_up",param,scale)

Так-же не забываем в настройках добавить кнопку "menu", у меня это пробел.

Так-же выставляем, что пауза не останавливаем работу этой сцены. Для этого выбираем корень сцены дерева талантов, в инспекторе находим pause mode и ставим его в Process

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

Оружие

Начнём с доработки оружия. Бластер будет выстреливать по 2 пули, дробовик выстреливать 11 пулями, у винтовки перезарядка уменьшится на 50%, радиус взрыва у базуки увеличится на 50%. Значит нам надо будет отредактировать следующие скрипты:

  • Скрипт бластера

  • Скрипт дробовика

  • Скрипт винтовки

  • Скрипт ракеты для базуки

Скрипт бластера

В дерево объектов нужно добавить Timer(SecondFire), выставить OneShot = true, Wait Timer = 0,1

#Добавили таймер
onready var _second_fire = $SecondFire

func fire():
	if (_fire_couldown_timer.is_stopped()): # не на перезарядке
		#Если оружие Суперское, то запускаем таймер
		if (WeaponsName.blaster_up):
			_second_fire.start()
		spawn_bullet(0) # создаём пулю с 0-м дополнительным поворотом
		_fire_couldown_timer.start()
		_weapon_sound.play()

#Таймер сработал, стреляем
func _on_SecondFire_timeout():
	spawn_bullet(0)

Скрипт дробовика

func fire():
	if (_fire_couldown_timer.is_stopped()):
		if (WeaponsName.shotgun_up):
			spawn_bullet(5*PI/12)# Поворачиваем пулю на ~37,5 градусов
			spawn_bullet(4*PI/12)# Поворачиваем пулю на ~ 30 градусов
			spawn_bullet(3*PI/12)# Поворачиваем пулю на ~ 22,5 градусов
			spawn_bullet(2*PI/12)# Поворачиваем пулю на ~ 15 градусов
			spawn_bullet(PI/12)# Поворачиваем пулю на ~ 7,5 градусов
			spawn_bullet(0)# выпускаем пулю прямо
			spawn_bullet(-PI/12)# Поворачиваем пулю на ~ -7,5 градусов
			spawn_bullet(-2*PI/12)# Поворачиваем пулю на ~ -15 градусов
			spawn_bullet(-3*PI/12)# Поворачиваем пулю на ~ -22,5 градусов
			spawn_bullet(-4*PI/12)# Поворачиваем пулю на ~ -30 градусов
			spawn_bullet(-5*PI/12)# Поворачиваем пулю на ~ -37,5 градусов
		else:
			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()# включаем перезарядку	
		_weapon_sound.play()

Скрипт винтовки

func fire():
	if(_fire_couldown_timer.is_stopped()):
		spawn_bullet(0)
		_fire_couldown_timer.start()
		if (WeaponsName.rifle_up):
			fire_rate *= 0.5 #Если оружие улучшено, умножаем на 0.5
		_fire_couldown_timer.wait_time = fire_rate
		_weapon_sound.play()

Скрипт ракеты для базуки

extends "res://scenes/Weapons/DefaultWeapon/DefaultBullet.gd"

onready var _collision_shape = $CollisionShape2D#Фигура столкновений ракеты
onready var _collision_shape_bum = $Bum/CollisionShapeBum#Фигура столкновений взрыва
onready var _bum_live_time = $Bum/BumLiveTime #Таймер жизни взрыва
onready var _bum_sound = $Bum/BumSound #Звук взрыва

func collision_action(_collision_object):# обработка столкновения снаряда
	if(_animated_sprite.animation == "Fly"):# если он был снарядом
		_animated_sprite.play("Bum")# превращаем в взрыв
		_collision_shape.disabled = true # выключаем обычную фигуру столкновения
		_collision_shape_bum.disabled = false # включаем фигуру столкновения взрыва
		if(WeaponsName.bazooka_up):#если оружие супер
			scale = Vector2(15,15)
		else:
			scale = Vector2(10,10)  # увеличиваем размер в 10 раз, 
		#у вас может быть в другое количество раз, для моего проекта это в самый раз
		velocity=Vector2(position.x,position.y)
		_bum_live_time.start()
		_bum_sound.play()
func _on_Bum_body_entered(body):
	if(body.has_method("hit")):
		body.hit(damage)


func _on_BumLiveTime_timeout():
	queue_free()

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

Болванка персонаж

Для начала нам нужно объявить переменную, в которую будет передавать сцену дерева объектов.

export (NodePath) var skill_tree# дерево навыков

в функции _ready(), нужно привязать сигнал от нашего дерева.

#если есть дерево, то привязываем его сигнал
	var tree = get_node(skill_tree)
	if (tree != null):
		tree.connect("branch_skill_up",self,"_on_SkillTree_branch_skill_up")

и написать функцию обработки сигнала от дерева _on_SkillTree_branch_skill_up, не забывайте добавить 2 параметра для функции, ведь сигнал у нас передаёт 2 переменных.

#обработка сигнала от дерева
func _on_SkillTree_branch_skill_up(param, scale):
	#выбираем параметр
	match param:
		0:#урон
			damage_scale += damage_scale * scale/100# увеличиваем урон
		1:#скорость
			speed += speed * scale/100# увеличиваем скорость
		2:#хп
			health += round(health * scale/100)# увеличивам хп
			_user_interface.init_health(health)#переинициализируем ui
		_:
			pass
Полный скрипт болванки персонажа
extends KinematicBody2D


#Добавляем элементы дерева объектов в код
onready var _animated_sprite = $AnimatedSprite
onready var _idle_animation_timer = $IdleAnimationTimer
onready var _immortal_timer = $ImmortalTimer
onready var _backpack = $Backpack
onready var _user_interface = $Camera2D/UserInterface
#Объявляем переменные, которые можно менять извне 
export var health = 5 #Жизни
export var speed = 200 #Скорость 
export var damage_scale:float = 1#Множитель урона
export (NodePath) var skill_tree# дерево навыков
#Объявляем переменные только для этого скрипта
var velocity = Vector2.ZERO #Вектор направления
var direction = Vector2.ZERO #Вектор движения
var backpack_items = [null,null,null,null,null,null]#рюкзак
var weapon_count = 0 #Переменная хранящая количество оружия
#Сигнал о получении урона
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) #Отправляем сигнал о получении урона
		_user_interface.take_damage(dmg)
		_immortal_timer.start() #Запускаем таймер после получения урона
	if(health <= 0):
		emit_signal("dead")#Отправляем сигнал о смерти


func _ready():
	_animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять
	equip_all()
	#если есть дерево, то привязываем его сигнал
	var tree = get_node(skill_tree)
	if (tree != null):
		tree.connect("branch_skill_up",self,"_on_SkillTree_branch_skill_up")


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)# у меня стоит масштабировать оружие, возможно у вас нет
		weapon.damage = ceil(weapon.damage*damage_scale)#Перемножаем урон с нашим показателем и округляем в большую сторону
		
		
#одеваем всё доступное оружие
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()#удаляем объект
		weapon_count -=1 #уменьшили на 1 переменную количества оружия

#добавляем оружие
func add_equip_item(item):
	for i in range(6):
		if (backpack_items[i] == null):#Находим первый пустой элемент массива
			backpack_items[i] = item#заливаем в него сцену оружия
			weapon_count +=1 #увеличели на 1 переменную количества оружия
			equip_all()#Одеваем всё оружие
			return	
#Можно добавить оружие
func can_add():
	if (weapon_count < 6):
		return true
	else:
		return false
#обработка сигнала от дерева
func _on_SkillTree_branch_skill_up(param, scale):
	#выбираем параметр
	match param:
		0:#урон
			damage_scale += damage_scale * scale/100# увеличиваем урон
		1:#скорость
			speed += speed * scale/100# увеличиваем скорость
		2:#хп
			health += round(health * scale/100)# увеличивам хп
			_user_interface.init_health(health)#переинициализируем ui
		_:
			pass

Теперь перейдём к настройке сцены игры и редактированию скрипта игры.

Сцена игры

Для начала добавим в дерево объектов сцену дерева умений. И перейдём к редактированию скрипта игры.

Для начала объявим переменную дерева в коде.

onready var _skill_tree = $SkillTree

В функции _ready(), добавим передачу нашей переменой игрока в дерево талантов

_skill_tree.player = player#привязываем игрока к дереву

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

func clear_level():
	WeaponsName.clear_all()#убираем улучшения с оружия
	get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены

#Добавляем сигнал, от нашего персонажа о смерти
func _on_Player_dead():
	save_record(player._user_interface.get_score())#записали результаты
	clear_level()
	SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
Полный скрипт сцены игры
extends Node2D


#Сцена Врага
export (PackedScene) var zombie_enemy

#Элементы дерева
onready var _mob_spawn_timer = $MobSpawnTimer
onready var player = $Player
onready var _character_spawn_point = $CharacterSpawnPoint
onready var spawn = $Spawn
onready var _skill_tree = $SkillTree
#Массив всего оружия
var weapon_massiv = [WeaponsName.BLASTER,WeaponsName.RIFLE, WeaponsName.BAZOOKA, WeaponsName.SHOTGUN]
#Сложность игры
var spawn_time = 5 #Время частоты спавна врагов
var zombie1_chance = 40#вероятность для обычного зомби
var smart_chance = 40#вероятность для умного зомби
var shield_chance = 12#вероятность для зомби с щитом
var scary_chance = 4#вероятность для страшного зомби
var fat_chance = 4#вероятность для толстого зомби
var spawn_count = 3#кол-во призываемых зомби
var difficult_tick = 0#кол-во раз, которое увеличилась сложность
var weapon_add_chance = 0#шанс добавления предметов

#Функция призыва точки спавна в качестве аргумента используется сцена с врагом
func spawn_point(enemy):
	var z = EnemyNames.SPAWNPOINT.instance()
	var rect_pos = spawn.rect_global_position
	var rect_size = spawn.rect_size
	#генерируем случайный вектор с местоположение зомби по следующему алгорится
	#для местоположению по х выбираем случайное значение из диапазона:
	#берём глобальное расположение квадрата оп х, как миннимум
	#и глобал местоположения по х + размер по х, как максимум
	#для y тоже самое, только вместо х-y
	z.position = Vector2(rand_range(rect_pos.x,rect_pos.x+rect_size.x),rand_range(rect_pos.y,rect_pos.y+rect_size.y))
	z.z_index = 100#Ставим z_index большим, чтобы точка спавна всегда распологалась поверх других объектов
	z.player = player#Задаём игрока
	z.zombie_enemy = enemy#Задаём врага
	get_parent().add_child(z)# добавляем точку спавна
	
#Функция инициализации
func _init():
	spawn_hero(Vector2(0,0))#Вызываем функцию создания героя
	
#Функция старта(срабатывает после _init)
func _ready():
	_mob_spawn_timer.start()#Включили таймер спавна
	_skill_tree.player = player#привязываем игрока к дереву
	randomize()# подключаем генератор случайных чисел
	player.position = _character_spawn_point.global_position
	#Передали игроку установленное в редакторе местоположение
	player._user_interface.init_health(player.health)# инициализируем наш UI
	player.connect("dead",self,"_on_Player_dead")#Привязываем сигнал о смерти игрока

#Функция срабатывания таймера MobSpawnTimer
func _on_MobSpawnTimer_timeout():
	#Задаём цикл для призыва зомби
	for i in range(spawn_count):
		#Генерируем шанс, делаем остаток от деления на 101 - будут числа в радиусе от (0 до 100)
		var chance = randi() % 101
		if (chance <= zombie1_chance):
			spawn_point(EnemyNames.ZOMBIE1)
		elif (chance <= zombie1_chance + smart_chance):
			spawn_point(EnemyNames.SMARTZOMBIE)
		elif (chance <= zombie1_chance + smart_chance + shield_chance):
			spawn_point(EnemyNames.ZOMBIESHEILD)
		elif (chance <= zombie1_chance + smart_chance + shield_chance + scary_chance):
			spawn_point(EnemyNames.ZOMBIESCARY)
		elif (chance <= zombie1_chance + smart_chance + + shield_chance + scary_chance + fat_chance):
			spawn_point(EnemyNames.FATZOMBIE)
		#Если вдруг что-то пошло не так, то спавним Zombie1
		else:
			spawn_point(EnemyNames.ZOMBIE1)
		#рассмотрим генерацию на примере след. данных
		#zombie1_chance = 40
		#smart_chance = 40
		#shield_chance = 12
		#scary_chance = 4
		#fat_chance = 4
		#Если от 0 до 40, то Zombie1, если от 41 до 80, то Smart_zombie,
		#Если то 81 до 92, то Zombie_shield, если от 93 до 96, то Zombie_scary
		#Если от 97 до 100, то Fat_zombie
	_mob_spawn_timer.wait_time = spawn_time#задали время срабатывания
	_mob_spawn_timer.start()#включили
	


#Добавляем сигнал, от нашего персонажа о смерти
func _on_Player_dead():
	save_record(player._user_interface.get_score())#записали результаты
	clear_level()
	SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
	
#Функция создания героя
func spawn_hero(pos):
	var p
	if(SelectedCharacter.Character != null):#Если герой выбран
		p = SelectedCharacter.Character.instance()
	else:#Если вдруг каким-то образом не выбран
		p = CharacterNames.BRUTALHERO.instance()
	p.name = "Player"#Задаём имя, которое будет в дереве объектов
	p.position = pos
	add_child(p)
	p.skill_tree="../SkillTree"
	player = p
	p.z_index = 2#Задаём z_index - 2, чтоыб герой ходил сверху крови
	

#Усложняем игру
func _on_Difficult_timeout():
	#генерируем шанс на получение оружия
	var weapon_chance = randi() % 100
	#Если чисто меньше, нашего шанса и можно добавить
	if (weapon_chance <= weapon_add_chance && player.can_add()):
		#Добавляем случайное оружия из массива с оружие
		player.add_equip_item(weapon_massiv[randi() % weapon_massiv.size()])
		#Обнуляем шанс на получение
		weapon_add_chance = 0
	else:
		#Если не получили, то увеличиваем шанс
		weapon_add_chance+=5
	#Увеличиваем счётчик усложнения
	difficult_tick += 1
	#Когда счётчик кратен 3, то
	if (difficult_tick % 3 == 0):
		#Добавляем ещё одного зомби
		spawn_count+=1
		#и меняем вероятности
		shield_chance += 4
		if (shield_chance > 20): #ограничиваем вероятность спавна в 20%
			shield_chance = 20
		fat_chance += 2
		if (fat_chance > 20): #ограничиваем вероятность спавна в 20%
			fat_chance = 20
		scary_chance += 2
		if (scary_chance > 20):#ограничиваем вероятность спавна в 20%
			scary_chance = 20
		zombie1_chance -= 4
		if (zombie1_chance < 20):#ограничиваем вероятность спавна в 20%
			zombie1_chance = 20
		smart_chance -= 4	
		if (smart_chance < 20):#ограничиваем вероятность спавна в 20%
			smart_chance = 20
#zombie1 и smart крайте просты, поэтому их вероятность уменьшаем, за счёт этого
#увеличиваем вероятность на появление других зомби
#сумма шанса призыва всех зомби должна быть равна 100.

#функция сохранения в качестве аргумента берёт текущий счёт
func save_record(score):
	#Объявили нвоый файл
	var save_file = File.new()
	#Создали новый "словарь" записали в него лучший показатели на текущий момент
	var save_dict ={
		"BrutalHero": CharacterNames.brutalhero_score,
		"Cowboy":CharacterNames.cowboy_score,
		"Robot":CharacterNames.robot_score,
		"Soldier":CharacterNames.soldier_score,
	}
	#Если данный герой и текущий счёт больше лучшего, то записываем другой
	if(SelectedCharacter.Character == CharacterNames.BRUTALHERO && score > save_dict["BrutalHero"]):
		save_dict["BrutalHero"] = score
	if(SelectedCharacter.Character == CharacterNames.COWBOY && score > save_dict["Cowboy"]):
		save_dict["Cowboy"] = score
	if(SelectedCharacter.Character == CharacterNames.ROBOT && score > save_dict["Robot"]):
		save_dict["Robot"] = score
	if(SelectedCharacter.Character == CharacterNames.SOLDIER && score > save_dict["Soldier"]):
		save_dict["Soldier"] = score
	#Открываем фаил с сохранением
	save_file.open("user://save.save", File.WRITE)
	#Сохраняем
	save_file.store_line(to_json(save_dict))

func clear_level():
	WeaponsName.clear_all()#убираем улучшения с оружия
	get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены

Уровень персонажа

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

Сцена очка опыта

Главным узлом сцены выбираем StaticBody2D(ExpPoint), дочерние элементы:

⦁ Sprite
⦁ CollisionShape

Заливаем спрайт, подгоняем CollisionShape. В настройках добавляем название для нового слоя столкновений( у меня это 7, назвал Pick_up), настройки для CollisionObject2D:

столкновения для очка опыта
столкновения для очка опыта

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

Навешиваем скрипт на ExpPoint, и переходим к его редактированию:

extends StaticBody2D

#объявляем переменную, кол-во даваемого опыта
export var exp_param = 1

#функция, которая сработает, когда игрок подберёт опыт
func pick_up_exp():
	queue_free()

#функция, которая вернут кол-во даваемого опыта
func get_exp_param():
	return exp_param

Сцена врага

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

Добавляем в наш синглтон EnemyNames, константу с сценой опыта

#очко опыта
const EXPPOINT = preload("res://scenes/Game/ExpPoint/ExpPoint.tscn")

В скрипте болванке для врагов, объявляем переменную с сценой, переименовываем функцию спавна крови spawn_blood, в функцию dead(), и расширяем её, потом вызываем функцию dead() в обработчике получения урона, если хп кончились:

var exp_scene = EnemyNames.EXPPOINT

#функция смерти врага
func dead():
	var b = blood_scene.instance()
		
	b.position = position#задали местоположение
	b.rotation_degrees = randi() % 361#сгенерировали случайный угол поворота

	get_parent().add_child(b)#добавили кровь
	
	var e = exp_scene.instance()#объявии сцену с опытом
	
	e.position = position#задали местоположение
	e.rotation_degrees = randi() % 361#сгенерировали случайный угол поворота
	e.z_index = 50#увеличиваем z-index
	
	get_parent().add_child(e)#добавили опыт
	
	queue_free()#удалили зомби

#Функция получения урона
func hit(damage):
	health -= damage
	_red_health.rect_size.x -= health_size * damage
	if (health <= 0): #Если <= 0, то удалился
		dead()
Полный скрипт болванки врага
extends KinematicBody2D

#подгрузили сцену с кровью и опытом
var blood_scene = EnemyNames.ZOMBIEBLOOD
var exp_scene = EnemyNames.EXPPOINT
#Добавляем элементы дерева объектов в код
onready var _animated_sprite = $AnimatedSprite
onready var _red_health = $HealthBar/RedHealth

#Добавляем переменную игрока, позже понадобится
export onready var player
#Характеристики врага
export var health = 5
export var speed:float = 2
export var damage = 1
#Ещё чу-чуть переменных
#Длина на которую нужно уменьшить размер RedHealth, в случае получения 1 ед. урона
onready var health_size = _red_health.rect_size.x / health 

var motion = Vector2.ZERO
var dir = Vector2.ZERO

#Функция по выстраиванию пути к заданной точке
func find_position(pos):
	dir = (pos - position).normalized()
	motion = dir.normalized() * speed
	if(dir.x < 0):
		_animated_sprite.set_flip_h(true)
	else:
		_animated_sprite.set_flip_h(false)


func _ready():
	randomize()#подключили рандомайзер 
	_animated_sprite.playing = true #Включили анимацию

#Функция получения урона
func hit(damage):
	health -= damage
	_red_health.rect_size.x -= health_size * damage
	if (health <= 0): #Если <= 0, то удалился
		dead()

func _physics_process(delta):
	#Если игрока не существует, то некуда идти
	if (player != null):
		find_position(player.position)
		var collision = move_and_collide(motion)
		if collision:#Если столкнулся
			if collision.collider.has_method("take_damage"):#И есть метод take_damage
				collision.collider.take_damage(damage)#нанёс урон

#функция смерти врага
func dead():
	var b = blood_scene.instance()
		
	b.position = position#задали местоположение
	b.rotation_degrees = randi() % 361#сгенерировали случайный угол поворота

	get_parent().add_child(b)#добавили кровь
	
	var e = exp_scene.instance()#объявии сцену с опытом
	
	e.position = position#задали местоположение
	e.rotation_degrees = randi() % 361#сгенерировали случайный угол поворота
	e.z_index = 50#увеличиваем z-index
	
	get_parent().add_child(e)#добавили опыт
	
	queue_free()#удалили зомби

func get_health():
	return health

Сцена пользовательского интерфейса

На сцену нужно добавить ещё одну полоску, которая будет отображать опыт, как мы это делали во второй статье

Переходим к редактированию скрипта:

Нужно добавить переменные отвечающие за опыт, функцию инициализации полоски с опытом и функцию добавления опыта

#Элементы дерева
onready var _score_add_timer = $Score/ScoreAddTimer
onready var _score = $Score
onready var _current_health_bar = $CurrentHealthBar

var max_exp #Максимальное кол-во опыта
var current_exp #Текущее кол-во опыта
var exp_bar_size #Размер на который нужно уменьшать жёлтый квадрат

# Функция задания всех переменных опыта
func init_exp(add_exp):
	max_exp = add_exp 
	current_exp = 0 
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	exp_bar_size = _current_exp_bar.rect_size.x / max_exp# определяем длинну деления
	_current_exp_bar.rect_size.x = 0 #обнуляем полоску опыта до 0

# Функция вызываемая при получении опыта
func take_exp(add_exp):
	current_exp += add_exp #добавили опыт
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	_current_exp_bar.rect_size.x += exp_bar_size * add_exp#увеличиваем длину квадрата
Полный скрипт пользовательского интерфейса
extends Node2D

#Элементы дерева
onready var _score_add_timer = $Score/ScoreAddTimer
onready var _score = $Score
onready var _current_health_bar = $CurrentHealthBar
onready var _health = $CurrentHealthBar/Health
onready var _current_exp_bar = $CurrentExpBar
onready var _exp = $CurrentExpBar/Exp


var max_health #Максимальное кол-во хп
var current_health #Текущее кол-во хп
var health_bar_size #Размер на который нужно уменьшать зелёный квадрат

var max_exp #Максимальное кол-во опыта
var current_exp #Текущее кол-во опыта
var exp_bar_size #Размер на который нужно уменьшать жёлтый квадрат

# Функция задания всех переменных опыта
func init_exp(add_exp):
	max_exp = add_exp 
	current_exp = 0 
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	exp_bar_size = _current_exp_bar.rect_size.x / max_exp# определяем длинну деления
	_current_exp_bar.rect_size.x = 0 #обнуляем полоску опыта до 0

# Функция задания всех переменных хп
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 take_exp(add_exp):
	current_exp += add_exp #добавили опыт
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	_current_exp_bar.rect_size.x += exp_bar_size * add_exp#увеличиваем длину квадрата

# Сигнал от таймера
func _on_ScoreAddTimer_timeout():
	_score.text = String(int(_score.text) + 1)

#Функция передачи счёта
func get_score():
	return int(_score.text)

Сцена персонажа

Нужно объявить переменные опыта(требуемы для повышения опыт, параметр во сколько раз увеличится опыт при повышении, текущий опыт), добавить сигнал о повышении уровня, вызов функцию init_exp(), в функции _ready(), добавить обработку столкновений для столкновения с очком опыта, добавить функцию повышения уровня level_up()

export var max_exp = 20#сколько опыта надо для повышения уровня
export var scale_exp = 2# во сколько раз увеличится требуемы опыт после повышения
var current_exp = 0 #Переменная хранящая кол-во опыта

#Сигна о повышении уроня
signal lvl_up

func _ready():
	_animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять
	equip_all()
	_user_interface.init_exp(max_exp)# инициализая опыта, для интерфейса
	_user_interface.init_health(health)# инициализация опыта, для интерфейса
	#если есть дерево, то привязываем его сигнал
	var tree = get_node(skill_tree)
	if (tree != null):
		tree.connect("branch_skill_up",self,"_on_SkillTree_branch_skill_up")

func _physics_process(delta):
	get_input()
	get_anim()
	var collision= move_and_collide(direction * delta) 
	#записываем в переменную collision для дальнейшей обработки столкновения
	if collision:#если столкновение
		if collision.collider.has_method("pick_up_exp"):#И есть метод take_damage
				collision.collider.pick_up_exp()#поднимаем опыт
				current_exp += collision.collider.get_exp_param()#увеличиваем значение
				level_up()#вызываем функцию повышение опыта
				_user_interface.take_exp(collision.collider.get_exp_param())#увеличиваем опыт

#функция поднятия опыта
func level_up():
	if (current_exp == max_exp):#Если достигли опыта для лвлапа
		emit_signal("lvl_up")#отправляем сигнал
		max_exp *= scale_exp#увеличиваем требуемый опыт для след уровня
		current_exp = 0#обнуляем текущий опыт
		_user_interface.init_exp()#обновляем интерфейс
Полный скрипт болванки персонажа
extends KinematicBody2D


#Добавляем элементы дерева объектов в код
onready var _animated_sprite = $AnimatedSprite
onready var _idle_animation_timer = $IdleAnimationTimer
onready var _immortal_timer = $ImmortalTimer
onready var _backpack = $Backpack
onready var _user_interface = $Camera2D/UserInterface
#Объявляем переменные, которые можно менять извне 
export var health = 5 #Жизни
export var speed = 200 #Скорость 
export var damage_scale:float = 1#Множитель урона
export var max_exp = 20#сколько опыта надо для повышения уровня
export var scale_exp = 2# во сколько раз увеличится требуемы опыт после повышения
export (NodePath) var skill_tree# дерево навыков
#Объявляем переменные только для этого скрипта
var velocity = Vector2.ZERO #Вектор направления
var direction = Vector2.ZERO #Вектор движения
var backpack_items = [null,null,null,null,null,null]#рюкзак
var weapon_count = 0 #Переменная хранящая кол-во оружия
var current_exp = 0 #Переменная хранящая кол-во опыта
#Сигнал о получении урона
signal take_damage(damage)
#Сигнал о смерти
signal dead
#Сигна о повышении уроня
signal lvl_up
#Функция считывания нажатий
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) #Отправляем сигнал о получении урона
		_user_interface.take_damage(dmg)
		_immortal_timer.start() #Запускаем таймер после получения урона
	if(health <= 0):
		emit_signal("dead")#Отправляем сигнал о смерти


func _ready():
	_animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять
	equip_all()
	_user_interface.init_exp(max_exp)# инициализая опыта, для интерфейса
	_user_interface.init_health(health)# инициализация опыта, для интерфейса
	#если есть дерево, то привязываем его сигнал
	var tree = get_node(skill_tree)
	if (tree != null):
		tree.connect("branch_skill_up",self,"_on_SkillTree_branch_skill_up")


func _physics_process(delta):
	get_input()
	get_anim()
	var collision= move_and_collide(direction * delta) 
	#записываем в переменную collision для дальнейшей обработки столкновения
	if collision:#если столкновение
		if collision.collider.has_method("pick_up_exp"):#И есть метод take_damage
				collision.collider.pick_up_exp()#поднимаем опыт
				current_exp += collision.collider.get_exp_param()#увеличиваем значение
				level_up()#вызываем функцию повышение опыта
				_user_interface.take_exp(collision.collider.get_exp_param())#увеличиваем опыт
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)# у меня стоит масштабировать оружие, возможно у вас нет
		weapon.damage = ceil(weapon.damage*damage_scale)#Перемножаем урон с нашим показателем и округляем в большую сторону
		
		
#одеваем всё доступное оружие
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()#удаляем объект
		weapon_count -=1 #уменьшили на 1 переменную количества оружия

#добавляем оружие
func add_equip_item(item):
	for i in range(6):
		if (backpack_items[i] == null):#Находим первый пустой элемент массива
			backpack_items[i] = item#заливаем в него сцену оружия
			weapon_count +=1 #увеличели на 1 переменную количества оружия
			equip_all()#Одеваем всё оружие
			return	
#Можно добавить оружие
func can_add():
	if (weapon_count < 6):
		return true
	else:
		return false
#обработка сигнала от дерева
func _on_SkillTree_branch_skill_up(param, scale):
	#выбираем параметр
	match param:
		0:#урон
			damage_scale += damage_scale * scale/100# увеличиваем урон
		1:#скорость
			speed += speed * scale/100# увеличиваем скорость
		2:#хп
			health += round(health * scale/100)# увеличивам хп
			_user_interface.init_health(health)#переинициализируем ui
		_:
			pass

#функция поднятия опыта
func level_up():
	if (current_exp == max_exp):#Если достигли опыта для лвлапа
		emit_signal("lvl_up")#отправляем сигнал
		max_exp *= scale_exp#увеличиваем требуемый опыт для след уровня
		current_exp = 0#обнуляем текущий опыт
		_user_interface.init_exp()#обновляем интерфейс

Добавление построек

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

Болванка для всех построек

Создаём новую сцену, главным узлом выбираем KinematicBody2D(DefaultBuilding),дочерние к нему:

Добавляем сцену в новую группу all_buildings, чтобы удалять их в функции очистки сцены игры. На это болванка постройки готова.

Болванка защитной постройки

Выбираем главным узлом сцены Болванку для постройки и добавляем ещё следующие элементы:

Выстраиваем 2 ColorRect, которые будут полоской жизни, так-же, как делали это для болванки врага. Навешиваем скрипт и переходим к его редактированию:

extends KinematicBody2D

#Объявляем переменные дерева
onready var _health_bar = $HealthBar
onready var _red_health_bar = $HealthBar/RedHealthBar
onready var _immortal_timer = $ImmortalTimer

#Объявляем переменную хп
export var health = 5

#Объявляем переменную длины красной полоски
var health_bar_size

#Присваиваем размер красной полоски
func _ready():
	health_bar_size = round(_red_health_bar.rect_size.x / health)

#Функция получения урона	
func take_damage(dmg):
	#Если можно ударить
	if(_immortal_timer.is_stopped()):
		health -= dmg#наносим урон
		_immortal_timer.start()#запустили таймер
		#Уменьшаем красную полоску
		_red_health_bar.rect_size.x -= health_bar_size * dmg
		#Если хп кончились, то вызываем функцию смерти
		if(health <= 0):
			dead()
#Функция смерти
func dead():
	#Будет переопределять для каждой постройки
	pass

Теперь нужно задать слои столкновений, в настройках именуем новый слой столкновений( у меня 8, назвал Protect_building) присваиваем защитной постройке этот уровень и в маске указываем, что сталкивается только с врагами

Обычная баррикада

Выбираем главным узлом сцены болванку защитной постройки, указываем хп в редакторе, и расширяем скрипт, нам нужно переопределить функцию смерти:

extends "res://scenes/Buildings/ProtectBuilding/DefaultProtectBuilding.gd"

#Функция смерти
func dead():
	#Это обычная барикада, она просто будет удаляться
	queue_free()

У меня стоит 10 жизней

Взрывающаяся баррикада

Главным узлом сцены выбираем болванку защитной постройки. Добавляем следующие элементы:

Когда у баррикады закончатся жизни, она взорвётся. Добавляем анимацию взрыва. Расширяем скрипт и переходим к его редактированию:

extends "res://scenes/Buildings/ProtectBuilding/DefaultProtectBuilding.gd"

onready var _animated_sprite = $AnimatedSprite
onready var _collision_shape = $CollisionShape2D
onready var _collision_shape_bum = $Bum/CollisionShapeBum#Радиус вызрыва
onready var _bum_live_time = $Bum/BumLiveTime #Таймер жизни взрыва
onready var _bum_sound = $Bum/BumSound #Звук взрыва

export var damage = 3#Урон взрыва

#Переопределяем функцию смерти
func dead():
	_animated_sprite.play("Bum")# превращаем в взрыв
	_collision_shape.disabled = true # выключаем обычную фигуру столкновения
	_collision_shape_bum.disabled = false # включаем фигуру столкновения взрыва
	_bum_live_time.start()#Вклюаем таймер
	_bum_sound.play()#проигрываем звук
		
#Если кто-то в взрыве
func _on_Bum_body_entered(body):
	#и есть метод hit
	if(body.has_method("hit")):
		#наносим урон
		body.hit(damage)
		
#таймер кончился, удаляем взрыв
func _on_BumLiveTime_timeout():
	queue_free()

У меня стоит 5 жизней и урон 2

Болванка атакующей постройки

Выбираем главным узлом сцены, болванку постройки и добавляем в дерево объектов Timer(CouldownTimer):

Навешиваем скрипт и переходим к его редактированию:

extends KinematicBody2D
#объявляем элементы дерева
onready var _couldown_timer = $CouldownTimer
#задаём переменные
export (PackedScene) var bullet_scene#пуля
export var damage = 1#урон
export var fire_rate = 2#скорость атаки
#присвоили скорость атаки таймеру
func _ready():
	_couldown_timer.wait_time = fire_rate
#когда таймер сработал, стреляем
func _on_CouldownTimer_timeout():
	fire()
#функцию выстрела будет переопределять
func fire():
	pass
#функция создания пули
func spawn_bullet(rot):
# передаём параметр дополнительного поворота пули, позже пригодится 
	var b = bullet_scene.instance()
	#задали местоположение и поворот
	var new_position = position
	var direction = rotation - rot
	get_parent().add_child(b)# добавляем пулю, как потомка оружия
	b.scale = Vector2(0.5,0.5)
	b.start(new_position,direction)
	b.damage = damage# задаём пуле урон

С атакующими постройками не будем сталкиваться не мы, не враги.

Турель стреляющая во круг

Главным узлом сцены выбираем болванку атакующей постройки. Расширяем скрипт и переходим к редактированию:

extends "res://scenes/Buildings/AtackBuilding/DefaultAtackBuilding.gd"


func fire():
	#создаём пули с поворотами
	spawn_bullet(0)
	spawn_bullet(PI/4)
	spawn_bullet(2*PI/4)
	spawn_bullet(3*PI/4)
	spawn_bullet(4*PI/4)
	spawn_bullet(5*PI/4)
	spawn_bullet(6*PI/4)
	spawn_bullet(7*PI/4)
	#запускаем перезарядку
	_couldown_timer.start()

У меня стоит 1 урона ,перезарядка 2, сцена пули дробовика

Турель стреляющая ракетами

Главным узлом сцены выбираем болванку атакующей постройки. Турель будет стрелять ракетами в случайном направлении. Расширяем скрипт и переходим к редактированию:

func fire():
	spawn_bullet(randi() % 361)
	_couldown_timer.start()

Главное в сцену пули, залейте сцену снаряда базуки. У меня стоит 2 урона и перезарядка 5.

Хорошо, какие-то постройки создали, теперь нужно создать для них меню.

Сцена болванки кнопки постройки

Создаём новую сцены. Главным узлом сцены выбираем TextureButton(BuildingsButton), дочерние элементы:

В BuildingsButton заливаем спрайт, в label будет писаться цена постройки, в спрайте рисоваться картинка постройки. Навешиваем на корень скрипт и переходим к его редактированию:

extends TextureButton

#объявляем переменные
#Картинка отображаемая в Sprite
export (Texture) var img
#Какую постройку нужно построить при покупке
export (PackedScene) var building_scene
#Цена
export var building_price: int
#Сигнал о покупке
signal building_buy(building_scene, building_price)

# В функции _ready() отрисовываем иконку кнопки, текст стоимости 
#и присоединяем сигнал нажатия кнопки
func _ready():
	$Sprite.texture = img
	$Label.text = String(building_price) + " Exp"
	connect("pressed",self,"_on_BuildingsButton_pressed")

func _on_BuildingsButton_pressed():
	emit_signal("building_buy",building_scene,building_price)

Меню выбора постройки

Главным узлом сцены выбираем Node2D(BuildingMenu), дочерними выбираем Sprite и добавляем наши созданные ранее кнопки, не забывая их настроить.

Навешиваем скрипт на BuildingMenu и переходим к редактированию:

extends Node2D
#Сигнал о постройке
signal building_stand(building,building_price)
#Переменная можно-ли показать меню
var can_show = true
#Переменная игрока
var player

#присоединяем кнопки
func _ready():
	connect_button()
	
#функция присоединения сигнала кнопок
func connect_button():
	#Пробегаемся по все элементам дерева
	for i in range(get_child_count()):
		#Если есть этот метод
		if (get_child(i).has_method("_on_BuildingsButton_pressed")):
			#То присоединяем от этого элемента сигнал
			var btn = get_child(i).connect("building_buy",self,"_on_building_buy")
#Обработка сигнала
func _on_building_buy(scene,price):
	emit_signal("building_stand",scene,price)#Отправляем сигнал дальше

#Считываем нажатие на открытие меню
func _process(delta):
	#Если нажата кнопка, у меня это Z
	if(Input.is_action_just_pressed("Open_build_menu")):
		#Если нужно показать
		if(can_show):
			show()#Показываем
			can_show = false#меняем переменную
			position = player.position#Выствляем местоположение
			position.x += 225
			get_tree().paused = true#включаем паузу
		else:#Иначе
			hide()#Прячем
			can_show = true#меняем переменную
			get_tree().paused = false#выключаем паузу

Так-же выставляем, что пауза не останавливаем работу этой сцены. Для этого выбираем корень сцены меню построек, в инспекторе находим pause mode и ставим его в Process.

Пример как это выглядит:

Пользовательский интерфейс

В пользовательском интерфейсе, нужно добавить функцию уменьшающую опыт:

# функция списывания опыта
func remove_exp(minus_exp):
	current_exp -= minus_exp#Вычли опыт
	_current_exp_bar.rect_size.x -= exp_bar_size * minus_exp#уменьшили длину квадрата
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
Полный скрипт пользовательского интерфейса
extends Node2D

#Элементы дерева
onready var _score_add_timer = $Score/ScoreAddTimer
onready var _score = $Score
onready var _current_health_bar = $CurrentHealthBar
onready var _health = $CurrentHealthBar/Health
onready var _current_exp_bar = $CurrentExpBar
onready var _exp = $CurrentExpBar/Exp
onready var _buildings_menu = $BuildingsMenu

var max_health #Максимальное кол-во хп
var current_health #Текущее кол-во хп
var health_bar_size #Размер на который нужно уменьшать зелёный квадрат

var max_exp #Максимальное кол-во опыта
var current_exp #Текущее кол-во опыта
var exp_bar_size #Размер на который нужно уменьшать жёлтый квадрат

# Функция задания всех переменных опыта
func init_exp(add_exp):
	max_exp = add_exp 
	current_exp = 0 
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	exp_bar_size = _current_exp_bar.rect_size.x / max_exp# определяем длинну деления
	_current_exp_bar.rect_size.x = 0 #обнуляем полоску опыта до 0

# Функция задания всех переменных хп
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 take_exp(add_exp):
	current_exp += add_exp #добавили опыт
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт
	_current_exp_bar.rect_size.x += exp_bar_size * add_exp#увеличиваем длину квадрата
	
# функция списывания опыта
func remove_exp(minus_exp):
	current_exp -= minus_exp#Вычли опыт
	_current_exp_bar.rect_size.x -= exp_bar_size * minus_exp#уменьшили длину квадрата
	_exp.text = String(current_exp) + "/" + String(max_exp)#записываем в лэйбэл наш опыт

# Сигнал от таймера
func _on_ScoreAddTimer_timeout():
	_score.text = String(int(_score.text) + 1)

#Функция передачи счёта
func get_score():
	return int(_score.text)

Болванка персонажа

К персонажу нам нужно добавить подключение сигнала от меню и его обработку.

Добавляем переменную нашего меню:

export (NodePath) var build_menu# дерево постройки

В функции _ready(), привязываем сигнал:

#если есть меню, то привязываем его сигнал
var menu = get_node(build_menu)
if (menu != null):
	menu.connect("building_stand",self,"build")

Функция обработки сигнала:

#Функция обработки сигнала постройки
func build(build_scene, build_price):
	#Если опыта >=, чем стоит постройка
	if (current_exp >= build_price):
		#вычитаем стоимость
		current_exp -= build_price
		#обновляем UI
		_user_interface.remove_exp(build_price)
		#добавляем постройку
		var b = build_scene.instance()
		
		b.position = position
		
		get_parent().add_child(b)
Полный скрипт персонажа
extends KinematicBody2D


#Добавляем элементы дерева объектов в код
onready var _animated_sprite = $AnimatedSprite
onready var _idle_animation_timer = $IdleAnimationTimer
onready var _immortal_timer = $ImmortalTimer
onready var _backpack = $Backpack
onready var _user_interface = $Camera2D/UserInterface
#Объявляем переменные, которые можно менять извне 
export var health = 5 #Жизни
export var speed = 200 #Скорость 
export var damage_scale:float = 1#Множитель урона
export var max_exp = 20#сколько опыта надо для повышения уровня
export var scale_exp = 2# во сколько раз увеличится требуемы опыт после повышения
export (NodePath) var skill_tree# дерево навыков
export (NodePath) var build_menu# дерево постройки
#Объявляем переменные только для этого скрипта
var velocity = Vector2.ZERO #Вектор направления
var direction = Vector2.ZERO #Вектор движения
var backpack_items = [null,null,null,null,null,null]#рюкзак
var weapon_count = 0 #Переменная хранящая кол-во оружия
var current_exp = 0 #Переменная хранящая кол-во опыта
#Сигнал о получении урона
signal take_damage(damage)
#Сигнал о смерти
signal dead
#Сигна о повышении уроня
signal lvl_up
#Функция считывания нажатий
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) #Отправляем сигнал о получении урона
		_user_interface.take_damage(dmg)
		_immortal_timer.start() #Запускаем таймер после получения урона
	if(health <= 0):
		emit_signal("dead")#Отправляем сигнал о смерти


func _ready():
	_animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять
	equip_all()
	_user_interface.init_exp(max_exp)# инициализая опыта, для интерфейса
	_user_interface.init_health(health)# инициализация опыта, для интерфейса
	#если есть дерево, то привязываем его сигнал
	var tree = get_node(skill_tree)
	if (tree != null):
		tree.connect("branch_skill_up",self,"_on_SkillTree_branch_skill_up")
	#если есть меню, то привязываем его сигнал
	var menu = get_node(build_menu)
	if (menu != null):
		menu.connect("building_stand",self,"build")


func _physics_process(delta):
	get_input()
	get_anim()
	var collision= move_and_collide(direction * delta) 
	#записываем в переменную collision для дальнейшей обработки столкновения
	if collision:#если столкновение
		if collision.collider.has_method("pick_up_exp"):#И есть метод take_damage
				collision.collider.pick_up_exp()#поднимаем опыт
				current_exp += collision.collider.get_exp_param()#увеличиваем значение
				level_up()#вызываем функцию повышение опыта
				_user_interface.take_exp(collision.collider.get_exp_param())#увеличиваем опыт
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)# у меня стоит масштабировать оружие, возможно у вас нет
		weapon.damage = ceil(weapon.damage*damage_scale)#Перемножаем урон с нашим показателем и округляем в большую сторону
		
		
#одеваем всё доступное оружие
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()#удаляем объект
		weapon_count -=1 #уменьшили на 1 переменную количества оружия

#добавляем оружие
func add_equip_item(item):
	for i in range(6):
		if (backpack_items[i] == null):#Находим первый пустой элемент массива
			backpack_items[i] = item#заливаем в него сцену оружия
			weapon_count +=1 #увеличели на 1 переменную количества оружия
			equip_all()#Одеваем всё оружие
			return	
#Можно добавить оружие
func can_add():
	if (weapon_count < 6):
		return true
	else:
		return false
#обработка сигнала от дерева
func _on_SkillTree_branch_skill_up(param, scale):
	#выбираем параметр
	match param:
		0:#урон
			damage_scale += damage_scale * scale/100# увеличиваем урон
		1:#скорость
			speed += speed * scale/100# увеличиваем скорость
		2:#хп
			health += round(health * scale/100)# увеличивам хп
			_user_interface.init_health(health)#переинициализируем ui
		_:
			pass

#функция поднятия опыта
func level_up():
	if (current_exp == max_exp):#Если достигли опыта для лвлапа
		emit_signal("lvl_up")#отправляем сигнал
		max_exp *= scale_exp#увеличиваем требуемый опыт для след уровня
		current_exp = 0#обнуляем текущий опыт
		_user_interface.init_exp(max_exp)#обновляем интерфейс

#Функция обработки сигнала постройки
func build(build_scene, build_price):
	#Если опыта >=, чем стоит постройка
	if (current_exp >= build_price):
		#вычитаем стоимость
		current_exp -= build_price
		#обновляем UI
		_user_interface.remove_exp(build_price)
		#добавляем постройку
		var b = build_scene.instance()
		
		b.position = position
		
		get_parent().add_child(b)

Сцена игры

В дерево сцены игры, нужно добавить наше меню и скрыть его.

В функцию призыва героя, добавляем ему передачу пути дерева:

#Функция создания героя
func spawn_hero(pos):
	var p
	if(SelectedCharacter.Character != null):#Если герой выбран
		p = SelectedCharacter.Character.instance()
	else:#Если вдруг каким-то образом не выбран
		p = CharacterNames.BRUTALHERO.instance()
	p.name = "Player"#Задаём имя, которое будет в дереве объектов
	p.position = pos
	p._user_interface
	add_child(p)
	#Передаём путь к дереву
	p.skill_tree="../SkillTree"
	#Передаём путь к меню
	p.build_menu ="../BuildingsMenu"
	player = p
	p.z_index = 2#Задаём z_index - 2, чтоыб герой ходил сверху крови

В функцию _ready(), добавляем привязку игрока к меню построек:

#Функция старта(срабатывает после _init)
func _ready():
	_mob_spawn_timer.start()#Включили таймер спавна
	_skill_tree.player = player#привязываем игрока к дереву
	_skill_tree.add_connect()
	$BuildingsMenu.player = player#привязываем игрока к меню построек
	randomize()# подключаем генератор случайных чисел
	player.position = _character_spawn_point.global_position
	#Передали игроку установленное в редакторе местоположение
	player._user_interface.init_health(player.health)# инициализируем наш UI
	player.connect("dead",self,"_on_Player_dead")#Привязываем сигнал о смерти игрока

и в функцию clear_all(), добавляем удаление группы построек:

func clear_level():
	WeaponsName.clear_all()#убираем улучшения с оружия
	get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены
	get_tree().call_group("all_buildings", "queue_free")#Удаляем все постройки со сцены
Полный код сцены игры
extends Node2D


#Сцена Врага
export (PackedScene) var zombie_enemy

#Элементы дерева
onready var _mob_spawn_timer = $MobSpawnTimer
onready var player = $Player
onready var _character_spawn_point = $CharacterSpawnPoint
onready var spawn = $Spawn
onready var _skill_tree = $SkillTree
#Массив всего оружия
var weapon_massiv = [WeaponsName.BLASTER,WeaponsName.RIFLE, WeaponsName.BAZOOKA, WeaponsName.SHOTGUN]
#Сложность игры
var spawn_time = 5 #Время частоты спавна врагов
var zombie1_chance = 40#вероятность для обычного зомби
var smart_chance = 40#вероятность для умного зомби
var shield_chance = 12#вероятность для зомби с щитом
var scary_chance = 4#вероятность для страшного зомби
var fat_chance = 4#вероятность для толстого зомби
var spawn_count = 3#кол-во призываемых зомби
var difficult_tick = 0#кол-во раз, которое увеличилась сложность
var weapon_add_chance = 0#шанс добавления предметов

#Функция призыва точки спавна в качестве аргумента используется сцена с врагом
func spawn_point(enemy):
	var z = EnemyNames.SPAWNPOINT.instance()
	var rect_pos = spawn.rect_global_position
	var rect_size = spawn.rect_size
	#генерируем случайный вектор с местоположение зомби по следующему алгорится
	#для местоположению по х выбираем случайное значение из диапазона:
	#берём глобальное расположение квадрата оп х, как миннимум
	#и глобал местоположения по х + размер по х, как максимум
	#для y тоже самое, только вместо х-y
	z.position = Vector2(rand_range(rect_pos.x,rect_pos.x+rect_size.x),rand_range(rect_pos.y,rect_pos.y+rect_size.y))
	z.z_index = 100#Ставим z_index большим, чтобы точка спавна всегда распологалась поверх других объектов
	z.player = player#Задаём игрока
	z.zombie_enemy = enemy#Задаём врага
	get_parent().add_child(z)# добавляем точку спавна
	
#Функция инициализации
func _init():
	spawn_hero(Vector2(0,0))#Вызываем функцию создания героя
	
#Функция старта(срабатывает после _init)
func _ready():
	_mob_spawn_timer.start()#Включили таймер спавна
	_skill_tree.player = player#привязываем игрока к дереву
	_skill_tree.add_connect()
	$BuildingsMenu.player = player#привязываем игрока к меню построек
	randomize()# подключаем генератор случайных чисел
	player.position = _character_spawn_point.global_position
	#Передали игроку установленное в редакторе местоположение
	player._user_interface.init_health(player.health)# инициализируем наш UI
	player.connect("dead",self,"_on_Player_dead")#Привязываем сигнал о смерти игрока

#Функция срабатывания таймера MobSpawnTimer
func _on_MobSpawnTimer_timeout():
	#Задаём цикл для призыва зомби
	for i in range(spawn_count):
		#Генерируем шанс, делаем остаток от деления на 101 - будут числа в радиусе от (0 до 100)
		var chance = randi() % 101
		if (chance <= zombie1_chance):
			spawn_point(EnemyNames.ZOMBIE1)
		elif (chance <= zombie1_chance + smart_chance):
			spawn_point(EnemyNames.SMARTZOMBIE)
		elif (chance <= zombie1_chance + smart_chance + shield_chance):
			spawn_point(EnemyNames.ZOMBIESHEILD)
		elif (chance <= zombie1_chance + smart_chance + shield_chance + scary_chance):
			spawn_point(EnemyNames.ZOMBIESCARY)
		elif (chance <= zombie1_chance + smart_chance + + shield_chance + scary_chance + fat_chance):
			spawn_point(EnemyNames.FATZOMBIE)
		#Если вдруг что-то пошло не так, то спавним Zombie1
		else:
			spawn_point(EnemyNames.ZOMBIE1)
		#рассмотрим генерацию на примере след. данных
		#zombie1_chance = 40
		#smart_chance = 40
		#shield_chance = 12
		#scary_chance = 4
		#fat_chance = 4
		#Если от 0 до 40, то Zombie1, если от 41 до 80, то Smart_zombie,
		#Если то 81 до 92, то Zombie_shield, если от 93 до 96, то Zombie_scary
		#Если от 97 до 100, то Fat_zombie
	_mob_spawn_timer.wait_time = spawn_time#задали время срабатывания
	_mob_spawn_timer.start()#включили
	


#Добавляем сигнал, от нашего персонажа о смерти
func _on_Player_dead():
	save_record(player._user_interface.get_score())#записали результаты
	clear_level()
	SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
	
#Функция создания героя
func spawn_hero(pos):
	var p
	if(SelectedCharacter.Character != null):#Если герой выбран
		p = SelectedCharacter.Character.instance()
	else:#Если вдруг каким-то образом не выбран
		p = CharacterNames.BRUTALHERO.instance()
	p.name = "Player"#Задаём имя, которое будет в дереве объектов
	p.position = pos
	p._user_interface
	add_child(p)
	#Передаём путь к дереву
	p.skill_tree="../SkillTree"
	#Передаём путь к меню
	p.build_menu ="../BuildingsMenu"
	player = p
	p.z_index = 2#Задаём z_index - 2, чтоыб герой ходил сверху крови
	

#Усложняем игру
func _on_Difficult_timeout():
	#генерируем шанс на получение оружия
	var weapon_chance = randi() % 100
	#Если чисто меньше, нашего шанса и можно добавить
	if (weapon_chance <= weapon_add_chance && player.can_add()):
		#Добавляем случайное оружия из массива с оружие
		player.add_equip_item(weapon_massiv[randi() % weapon_massiv.size()])
		#Обнуляем шанс на получение
		weapon_add_chance = 0
	else:
		#Если не получили, то увеличиваем шанс
		weapon_add_chance+=5
	#Увеличиваем счётчик усложнения
	difficult_tick += 1
	#Когда счётчик кратен 3, то
	if (difficult_tick % 3 == 0):
		#Добавляем ещё одного зомби
		spawn_count+=1
		#и меняем вероятности
		shield_chance += 4
		if (shield_chance > 20): #ограничиваем вероятность спавна в 20%
			shield_chance = 20
		fat_chance += 2
		if (fat_chance > 20): #ограничиваем вероятность спавна в 20%
			fat_chance = 20
		scary_chance += 2
		if (scary_chance > 20):#ограничиваем вероятность спавна в 20%
			scary_chance = 20
		zombie1_chance -= 4
		if (zombie1_chance < 20):#ограничиваем вероятность спавна в 20%
			zombie1_chance = 20
		smart_chance -= 4	
		if (smart_chance < 20):#ограничиваем вероятность спавна в 20%
			smart_chance = 20
#zombie1 и smart крайте просты, поэтому их вероятность уменьшаем, за счёт этого
#увеличиваем вероятность на появление других зомби
#сумма шанса призыва всех зомби должна быть равна 100.

#функция сохранения в качестве аргумента берёт текущий счёт
func save_record(score):
	#Объявили нвоый файл
	var save_file = File.new()
	#Создали новый "словарь" записали в него лучший показатели на текущий момент
	var save_dict ={
		"BrutalHero": CharacterNames.brutalhero_score,
		"Cowboy":CharacterNames.cowboy_score,
		"Robot":CharacterNames.robot_score,
		"Soldier":CharacterNames.soldier_score,
	}
	#Если данный герой и текущий счёт больше лучшего, то записываем другой
	if(SelectedCharacter.Character == CharacterNames.BRUTALHERO && score > save_dict["BrutalHero"]):
		save_dict["BrutalHero"] = score
	if(SelectedCharacter.Character == CharacterNames.COWBOY && score > save_dict["Cowboy"]):
		save_dict["Cowboy"] = score
	if(SelectedCharacter.Character == CharacterNames.ROBOT && score > save_dict["Robot"]):
		save_dict["Robot"] = score
	if(SelectedCharacter.Character == CharacterNames.SOLDIER && score > save_dict["Soldier"]):
		save_dict["Soldier"] = score
	#Открываем фаил с сохранением
	save_file.open("user://save.save", File.WRITE)
	#Сохраняем
	save_file.store_line(to_json(save_dict))

func clear_level():
	WeaponsName.clear_all()#убираем улучшения с оружия
	get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены
	get_tree().call_group("all_buildings", "queue_free")#Удаляем все постройки со сцены

Подведение итогов

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

Игру уже можно куда-нибудь залить на всеобщее обозрение, возможно сделаем это позже.

По этому проекту позже должна будет выйти ещё одна статья с локальным мультиплеером, но пока нет возможности его реализовать, поэтому придётся подождать. Ниже будет несколько голосований, проголосуйте во всех, чтобы я понял, каким проектом заняться дальше. Голосования закончатся 10.07.2023 в 8 по МСК, успевайте проголосовать

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


  1. ildarin
    09.07.2023 06:59
    +1

    Эх, так я про ООП ответа и не дождался)


    1. IsaacBlog Автор
      09.07.2023 06:59
      +1

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


      1. ildarin
        09.07.2023 06:59

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

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

        Я вообще на чистом HTML5 ща написал игру) И там ООП вполне себе реализован, прям по Фаулеру. Так то я на юнити Zomoby писал, вот и интересно про годот, но по скринам компоненты как в юнити накидываются.


        1. WNeZRoS
          09.07.2023 06:59

          В Unity на один объект можно накидать сколько угодно компонент. В Godot один объект - один скрипт. Где-то был пропозал или ещё что-то про компоненты в годот, но автор написал что компоненты это сложно для художников и их не будет. Если очень надо, то можно создавать вложенные Node со своими скриптами. У базовой Node нет своего трансформа, так что вполне можно считать это альтернативой. Но альтернатива эта не супер удобная т.к. они болтаются в дереве объектов и на них нельзя повесить ограничений чтобы как-то защитится от ошибок в компоновке.