We continue to create our first game on Godot 3.5 (part 4) The end is near …


Let’s continue developing our first game. In this part, we will add what you voted for, namely the Talent Tree and the ability to build buildings.

Talent tree

What is the whole idea. The player can look at his talent tree at any time and if he has upgrade points, then upgrade.

The tree itself will consist of branches, when you click on a branch, a window with a description and 2 buttons appears, whether to upgrade or not. If the player pumps, then a signal is sent about this. In short, something like this.

tree branch

Create a new scene and select the main node of the scene TextureButton, child elements:

  • Sprite

  • Node2D(DescriptionNode)

  • ColorRect(Description), child of DescriptionNode

  • Label(DescriptionLbl), child of Description

  • TextureButton(DescriptionAccept), child of Description

  • TextureButton(DescriptionCancel), child of Description

Add Texture to TreeBranch and set Disabled – true. In Sprite, fill in the default icon. At DescriptionNode, set Z Index = 10. In Description, set the texture, you also need to put the rotation point of the object in the center of the texture, for this we select and click in the approximate center of the texture. Add a font to DescriptionLbl. In DescriptionAccept and DescriptionCancel, add textures. That’s all for now, add the script to the root and proceed to editing it. After all the settings, set DescriptionLbl Visible = false.

How the tree branch looks like for me:

DescriptionLbl Visible - false

DescriptionLbl Visible – false

DescriptionLbl Visible - true

DescriptionLbl Visible – true

The upgrade tree options will be as follows: increase attack, increase health, increase speed, make weapons “SUPER”. We will set all the data through the editor, namely: the description, the previous branch, which parameter we improve, by what percentage we increase the indicator, which weapon is improved, the branches on this layer and the picture. It will also require 3 signals, one will be triggered when we have upgraded this branch, the second will be triggered when the description is opened and signal that the others need to be closed, the third will signal that the parameter needs to be upgraded and indicate by how much.

#Элементы дерева
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)

Now let’s write the _ready () function, the talent picture we have chosen will be filled in it, and signals will be attached, from the elements that we wrote in export – variables

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")

Let’s now write a function that will determine how to upgrade a character

#Функция обработки выбора ветви
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)

We also need to add to the WeaponsName Singleton, a declaration of variables that become true if the weapon has become super and a function that determines which weapon to make super and a cleanup function that makes all variables 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

Now let’s add signals from TreeBranch, DescriptionAccept, DescriptionCancel to the code, and write the signal handling functions that are tied in _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

The code is commented in detail, and now we have a generic tree branch scene that we will add to the tree scene, set up all the export variables and it will work.

talent tree scene

We select Node2d as the main node of the scene, add a sprite and then collect our tree. All its branches can be easily rotated.

Approximate view of the scene

Approximate view of the scene

view in game

view in game

How to arrange elements in the object tree and name them. I called it like this: Layer № Branch №.

Examples of branch settings:

This branch will increase the speed parameter by 10%, is the root of the tree and there are no other branches.

This branch will improve the weapon (rifle), Also on this same layer there are 4 branches and the previous one is indicated.

We figured out the scene settings, add the script and proceed to editing:

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)

Also, do not forget to add the “menu” button in the settings, I have a space.

We also set that the pause does not stop the work of this scene. To do this, select the root of the talent tree scene, find pause mode in the inspector and set it to Process

Now we need to add the processing of signals for increasing characteristics and change the weapon.

Weapon

Let’s start with weapon upgrades. The blaster will fire 2 bullets, the shotgun will fire 11 bullets, the reload time of the rifle will decrease by 50%, the explosion radius of the bazooka will increase by 50%. So we will need to edit the following scripts:

  • Blaster script

  • Shotgun script

  • Rifle script

  • bazooka rocket script

Blaster script

Add Timer(SecondFire) to the object tree, set 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)

Shotgun script

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()

Rifle script

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()

bazooka rocket script

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()

With the weapons done, let’s add signal processing to the character.

Blank character

First, we need to declare a variable to which we will pass the object tree scene.

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

in the _ready() function, we need to bind a signal from our tree.

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

and write a signal processing function from the _on_SkillTree_branch_skill_up tree, do not forget to add 2 parameters for the function, because the signal we have passes 2 variables.

#обработка сигнала от дерева
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
Full character blank script
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

Now let’s move on to setting up the game scene and editing the game script.

game scene

First, let’s add a skill tree scene to the object tree. And let’s move on to editing the game script.

First, let’s declare a tree variable in code.

onready var _skill_tree = $SkillTree

In the _ready() function, let’s add our player variable to the talent tree

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

We will also add a cleansing function that will make all weapons normal and remove all enemies from the scene and call it from the processing of the dead signal from the character.

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")#Переходим в главное меню
Full game scene script
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")#Удаляем всех врагов со сцены

Character level

Now we have a tree that is called when pressing the space bar, but we do not have the most important thing, the ability to level up and get a talent point. To do this, we need to edit the character script and the UI script a bit and add that zombies will leave experience when they die.

Experience Point Scene

Select StaticBody2D(ExpPoint) as the main node of the scene, child elements:

⦁ Sprite
⦁ Collision Shape

Fill the sprite, customize the CollisionShape. In the settings, add a name for the new collision layer (I have it 7, called Pick_up), settings for CollisionObject2D:

experience point collisions

experience point collisions

Add all_enemy to the group to be deleted after the end of the game.

We hang the script on ExpPoint, and proceed to editing it:

extends StaticBody2D

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

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

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

Enemy scene

The experience itself has been created, now you need to teach the zombies to leave this experience after death. For this:

We add to our singleton EnemyNames, a constant with an experience scene

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

In the script blank for the enemies, we declare a variable with the scene, rename the spawn_blood blood spawn function to the dead () function, and expand it, then call the dead () function in the damage handler, if hp is over:

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()
Full script of the enemy blank
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

UI Scene

We need to add another strip to the stage, which will display the experience, as we did in the second article.

Let’s move on to editing the script:

You need to add variables responsible for the experience, the function for initializing the strip with experience and the function for adding experience

#Элементы дерева
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#увеличиваем длину квадрата
Complete UI script
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)

character scene

You need to declare experience variables (experience required to increase, the parameter how many times experience will increase when increasing, current experience), add a level increase signal, call the init_exp () function, in the _ready () function, add collision handling for collision with an experience point, add level_up() function

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()#обновляем интерфейс
Full character blank script
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()#обновляем интерфейс

Adding buildings

Now we need to teach the character to build buildings. By pressing a certain key, a menu will appear in which you can select a building and build it for the current experience.

Blank for all buildings

Create a new scene, select KinematicBody2D(DefaultBuilding) as the main node, its children:

We add the scene to the new group all_buildings in order to remove them in the game’s scene cleanup function. On this blank construction is ready.

Defensive building blank

We select the Blank for construction as the main node of the scene and add the following elements:

We line up 2 ColorRect, which will be the life bar, just as we did it for the enemy blank. We hang the script and proceed to its editing:

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

Now you need to set the collision layers, in the settings we name the new collision layer (I have 8, I called it Protect_building), assign this level to the defensive building and indicate in the mask that it collides only with enemies

Regular barricade

We select the blank of the defensive building as the main node of the scene, specify the xp in the editor, and expand the script, we need to redefine the death function:

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

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

I cost 10 lives

Exploding barricade

We select the blank of the defensive building as the main node of the scene. Add the following elements:

When the barricade runs out of lives, it will explode. Adding explosion animation. Expand the script and proceed to editing it:

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()

I cost 5 life and damage 2

Attack building blank

We select the main node of the scene, the blank of the building and add Timer(CouldownTimer) to the tree of objects:

We hang the script and proceed to its editing:

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# задаём пуле урон

It is not we, not the enemies, who will face the attacking buildings.

Turret shooting around

We select the blank of the attacking building as the main node of the scene. Expand the script and proceed to editing:

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()

I cost 1 damage, reload 2, shotgun bullet scene

Missile firing turret

We select the blank of the attacking building as the main node of the scene. The turret will fire missiles in a random direction. Expand the script and proceed to editing:

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

The main thing in the bullet scene, fill the bazooka projectile scene. I have 2 damage and 5 cooldown.

Okay, we have created some buildings, now we need to create a menu for them.

Build button blank scene

Let’s create a new scene. Select the main node of the scene TextureButton (BuildingsButton), child elements:

Fill in the sprite in the BuildingsButton, the price of the building will be written in the label, and the picture of the building will be drawn in the sprite. We hang the script on the root and proceed to editing it:

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)

Building selection menu

We select Node2D (BuildingMenu) as the main node of the scene, select Sprite as children and add our buttons created earlier, not forgetting to configure them.

We hang the script on the BuildingMenu and proceed to editing:

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#выключаем паузу

We also set that the pause does not stop the work of this scene. To do this, select the root of the building menu scene, find pause mode in the inspector and set it to Process.

An example of what it looks like:

User interface

In the user interface, we need to add a function to reduce the experience:

# функция списывания опыта
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)#записываем в лэйбэл наш опыт
Complete UI script
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)

character blank

We need to add a signal connection from the menu and its processing to the character.

Adding our menu variable:

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

In the _ready() function, we bind the signal:

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

Signal processing function:

#Функция обработки сигнала постройки
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)
Full character script
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)

game scene

In the game scene tree, we need to add our menu and hide it.

In the function of summoning the hero, we add the path of the tree to him:

#Функция создания героя
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, чтоыб герой ходил сверху крови

In the _ready() function, we add the player’s binding to the building menu:

#Функция старта(срабатывает после _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")#Привязываем сигнал о смерти игрока

and in the clear_all () function, we add the removal of a group of buildings:

func clear_level():
	WeaponsName.clear_all()#убираем улучшения с оружия
	get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены
	get_tree().call_group("all_buildings", "queue_free")#Удаляем все постройки со сцены
Full game scene code
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")#Удаляем все постройки со сцены

Summarizing

The article turned out to be heavy, a lot of code, I tried to describe everything in as much detail as possible. But now we have added a skill tree to the character and the ability to build buildings.

The game can already be uploaded somewhere for everyone to see, maybe we’ll do it later.

For this project, another article with local multiplayer will be released later, but there is no way to implement it yet, so we’ll have to wait. There will be several votes below, vote in all so that I understand what project to do next. Voting will end on 07/10/2023 at 08:00 Moscow time, have time to vote

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *