Продолжим разработку нашей первой игры. В этой части мы добавим то, за что вы проголосовали, а именно Дерево талантов и возможность возведения построек.
Дерево талантов
В чём вообще вся задумка. Игрок может в любое время посмотреть своё дерево талантов и если у него есть очки прокачки, то прокачать.
Само по себе дерево будет состоять из ветвей, при нажатии на ветвь появляется окно с описанием и 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.
Как ветвь дерева выглядит у меня:


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