Продолжим разработку нашей первой игры. В этой части мы добавим то, за что вы проголосовали, а именно Дерево талантов и возможность возведения построек.
Дерево талантов
В чём вообще вся задумка. Игрок может в любое время посмотреть своё дерево талантов и если у него есть очки прокачки, то прокачать.
Само по себе дерево будет состоять из ветвей, при нажатии на ветвь появляется окно с описанием и 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 по МСК, успевайте проголосовать
ildarin
Эх, так я про ООП ответа и не дождался)
IsaacBlog Автор
В статьях это описано, в первой части создаются базовые классы, врагов, оружия, снаряды оружия и игрового персонажа. В последующих частях, мы расширяем эти классы и создаем разных врагов, оружия и тд. Статьи писались для людей которые только начинают изучать программирование, по этому я называл их не классами, а болванками.
ildarin
Я вообще на чистом HTML5 ща написал игру) И там ООП вполне себе реализован, прям по Фаулеру. Так то я на юнити Zomoby писал, вот и интересно про годот, но по скринам компоненты как в юнити накидываются.
WNeZRoS
В Unity на один объект можно накидать сколько угодно компонент. В Godot один объект - один скрипт. Где-то был пропозал или ещё что-то про компоненты в годот, но автор написал что компоненты это сложно для художников и их не будет. Если очень надо, то можно создавать вложенные Node со своими скриптами. У базовой Node нет своего трансформа, так что вполне можно считать это альтернативой. Но альтернатива эта не супер удобная т.к. они болтаются в дереве объектов и на них нельзя повесить ограничений чтобы как-то защитится от ошибок в компоновке.