Продолжаем и дальше создавать свою первую игру на Godot 3.5. В предыдущей статье мы добавили различные механики для оружия, нечто похожее на пользовательский интерфейс, Главное меню и сцену игры. Сегодня добавим больше играбельных персонажей, меню выбора персонажа, для каждого персонажа будем вести свой маленький лидерборд, добавим больше врагов с разными механиками, добавим музыку к нашей игре и переработаем сцену игры.
Персонажи
Для начала нарисуем ещё 3-х персонажей. На данном этапе различие между персонажами может заключаться только в используемом ими оружие и небольшом различии в характеристиках( скорость, хп), поэтому добавим ещё каждому персонажу характеристику урона, это будет показатель на который умножается урон оружия.
Синглтон WeaponsName
Давайте начнём с написание маленького синглтона, который будет просто хранить в себе набор констант(ссылки на сцены оружия) чтобы нам было удобнее добавлять оружие персонажем.

Создаём новый скрипт обязательно проверьте, чтобы он наследовал Node и помещаем его в папку скриптов. В которой хранятся только синглтоны. После переходим в настройки проекта -> Автозагрузка и указываем путь к нашему синглтону, добавляем и ставим галочку в столбце "Глобальная переменная".
Переходим к редактированию скрипта. На данном этапе нам нужно добавить константы хранящие сцены нашего оружия:
extends Node #У вас путь к сценам может быть другим, обязательно проверьте что #указываете ссылку именно на оружие.tscn не на .gd или не на пуля.tscn const BLASTER = preload("res://scenes/Weapons/Blaster/Blaster.tscn") const SHOTGUN = preload("res://scenes/Weapons/Shotgun/Shotgun.tscn") const RIFLE = preload("res://scenes/Weapons/Rifle/Rifle.tscn") const BAZOOKA = preload("res://scenes/Weapons/Bazooka/Bazooka.tscn")
DefaultCharacter
Начнём с редактирование нашей болванки. В скрипте DefaultCharacter, нужно добавить переменную урона и сделать рюкзак пустым.
export var damage_scale:float = 1#Множитель урона var backpack_items = [null,null,null,null,null,null]#рюкзак
Так-же мы добавили новый атрибут, урон, нужно увеличивать урон оружия, для этого немного модифицируем функцию equip_item():
#функция прикрепления оружия 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)#Перемножаем урон с нашим показателем и округляем в большую сторону

Так-же прикрепляем к Camera2D дочернюю сцену нашего пользовательского интерфейса во время игры. и переносим узел в верхний левый угол камеры. Так-же уберём обработку получения урона с главной сцены и добавим это в скрипт персонажа. Дополнив ф��нкцию take_damage(dmg)
#Функция получения урона 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")#Отправляем сигнал о смерти
Создание персонажей
Создаём персонажей так-же просто, как и оружие в предыдущей статье. Создаём новую сцену, главным узлом сцены выбираем болванку персонажа, меняем ему анимации, не забывая сделать SpriteFrame уникальным. Добавляем музыкальную тему и Расширяем скрипт.
В скрипте нам нужно добавить одну или несколько строк добавления оружия персонажу в функции _ready():
func _ready(): add_equip_item(WeaponsName.BLASTER) #Вызываем функцию для добавления предмета в рюкзак и передаём в нею # константу из синглтона
Подобное нужно сделать со всеми персонажами. Ниже будет таблица моих настроек.
Персонаж | Оружие | ХП | Урон | Скорость |
BrutalHero | 1xБластер | 5 | 1 | 200 |
Cowboy | 1xДробовик | 3 | 1.5 | 150 |
Robot | 1xВинтовка | 10 | 0.75 | 300 |
Soldier | 1xБазука | 5 | 2 | 150 |
Синглтон CharacterNames

Помещаем его в ту же папку, что и два остальных синглтона в настройках добавляем его в автозагрузку и делаем глобальной переменной. Переходим к редактированию.
extends Node const BRUTALHERO = preload("res://scenes/Character/BrutalHero/BrutalHero.tscn") const COWBOY = preload("res://scenes/Character/Cowboy/Cowboy.tscn") const SOLDIER = preload("res://scenes/Character/Soldier/Soldier.tscn") const ROBOT = preload("res://scenes/Character/Robot/Robot.tscn")
На этом с персонажами закончили, давайте перейдём к небольшому редактированию оружия.
Оружие
В оружие всё просто, нам надо только добавить звуки выстрела.

На сцену болванку для оружия в дерево объектов добавляем AudioStreamPlayer(WeaponSound). Это будет проигрыватель звука выстрела оружия. Чтобы добавить музыку выбираем WeaponSound в дереве объектов. В инспекторе в параметр Stream загружаем свой файл с музыкой, для звуковых эффектов, например выстрела, лучше использовать .wav файлы, для фоновой музыки, лучше подойдёт .ogg формат, но если у вас wav или mp3, ничего страшного, данный проект достаточно маленький и никаких последствий от неправильно выбранного формата не будет.
И переходим к редактированию скрипта, в нём нужно объявить переменную ссылающуюся на WeaponSound:
onready var _weapon_sound = $WeaponSound
Теперь на каждой сцене с оружием добавляем звук выстрела от оружия и в функцию fire() для этого оружия добавляем:
_weapon_sound.play()
Так-же давайте настроим громкость звука, она конечно должна отличаться, но не должна глушить игрока. Уменьшить или Увеличить громкость можно путём изменения параметра Volume dB, у узла AudioPlayer.
Базука
На базуке остановимся отдельно, поскольку нам нужен не только звук выстрела снаряда, но и звук взрыва после столкновения.

Перейдём на сцену ракеты базуки и добавим AudioStreamPlayer(BumSound), как дочерний у Bum
Переходим к редактированию кода:
Нужно объявить ссылку на узел BumSound и воспроизводить звук взрыва в collision_action:
Полный код ракеты
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 # включаем фигуру столкновения взрыва 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()
Враги
Сейчас в проекте есть просто зомби который идёт на игрока и пытается его ударить. Мы добавим ещё 4 вида врага:
Умный зомби(когда игрок смотрит в его сторону, он бежит от игрока, когда нет бежит на игрока)
Зомби за щитом(он где-то подобрал кусок железа, если стрелять не точно, то сначала нужно сломать щит, потом только бить зомби)
Страшный зомби(быстрый и страшный)
Толстый зомби(Большой, толстый и страшный, при смерти создаёт ещё 3-х обычных зомби)
Так-же добавим, что после убийства каждого зомби будет появляться лужа крови
ZombieBlood
Создаём новую сцену, выбираем главным узлом сцены Area2D(ZombieBlood), добавляем дочерние элементы:

⦁ AnimatedSprite
⦁ Timer(BloodLive)
В AnimatedSprite создаём новый спрайт фрейм и создаём отдельную анимацию, для каждого вида кровавой лучше( у меня 3), лучше рисовать не 1, чтобы весь пол не был залит одним видом кровавых луж.
Таймер будет служить временем жизни лужи, One Shoot и Autostart - вкл, Wait Time я поставил 30 секунд, то есть через 30 секунд будет удаляться лужа.
Навешиваем скрипт на ZombieBlood и переходим к его редактированию:
extends Area2D #Объявляем переменные из дерева объектов onready var _animated_sprite = $AnimatedSprite onready var _blood_live = $BloodLive func _ready(): #генерируем случайный вид крови var animation_types = _animated_sprite.frames.get_animation_names() var animation = animation_types[randi() % animation_types.size()] _animated_sprite.play(animation) func _on_BloodLive_timeout(): #Вешаем сигнал таймера и по истичению удаляем queue_free()
Так-же добавляем кровь в группу all_enemy. Кто забыл, нужно нажать на Area2D(В дереве объектов), в инспекторе нажать "Узел"->Группы, ввести название и нажать добавить. Кровь создали теперь надо немного модифицировать
DefaultEnemy
Нужно объявить переменную содержащую ссылку на сцену крови, написать функцию которая будет создавать кровь на текущем местоположении с случайным поворотом, подключить в _ready() рандомайзер и вызывать функцию создания крови, когда кончились жизни, ещё сделать переменную скорости зомби float.
Попробуйте написать сами, если что под спойлером подсказка:
Полный код болванки врага
extends KinematicBody2D #подгрузили сцену с кровью var blood_scene = preload("res://scenes/Enemy/ZombieBlood/ZombieBlood.tscn") #Добавляем элементы дерева объектов в код 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, то удалился spawn_blood() #Выызваем функцию спавна крови queue_free() 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 spawn_blood(): var b = blood_scene.instance() b.position = position#задали местоположение b.rotation_degrees = randi() % 361#сгенерировали случайный угол поворота get_parent().add_child(b)#добавили кровь func get_health(): return health
Теперь наконец-то можно создавать наших зомби.
Умный зомби
Наследуем, как главный узел сцену DefaultEnemy, расширяем скрипт и переходим к его редактированию:
extends "res://scenes/Enemy/DefaultEnemy/DefaultEnemy.gd" #Переопределяем функцию поиска пути func find_position(pos): dir = (pos - position).normalized() #Если dir.x < 0, значит игрок слева, если _animated_sprite игрока не повёрнут # изначально у меня спрайты смотрят в право, значит игрок смотри на зомби # чтобы идти в обратно направлении, умножаем вектор направления на -скорость if (dir.x < 0 && !player._animated_sprite.flip_h): motion = dir.normalized() * -speed #Тоже самое, только если dir.x < 0, значит игрок справа и если он повёрнут # то смотрит на зомби elif (dir.x > 0 && player._animated_sprite.flip_h): motion = dir.normalized() * -speed else: # во всех других случаях идём на игрока motion = dir.normalized() * speed if(dir.x < 0):# поворачиваем зомби, чтобы правильно шёл _animated_sprite.set_flip_h(true) else: _animated_sprite.set_flip_h(false)
Всё подробно прокомментировал, переходим к следующему зомби.
Толстый зомби
Наследуем, как главный узел сцену DefaultEnemy, расширяем скрипт и переходим к его редактированию:
extends "res://scenes/Enemy/DefaultEnemy/DefaultEnemy.gd" #Подгружаем сцену с самым обычным зомби, можно использовать других зомби var zombie_scene = preload("res://scenes/Enemy/Zombie1/Zombie1.tscn") #Переопределяем функцию получения урона func hit(damage): health -= damage _red_health.rect_size.x -= health_size * damage if (health <= 0): #Если <= 0, то умер spawn_blood() # создаём кровь spawn_zombie(Vector2(position.x,position.y - 50)) #создаём зомби со смещениями spawn_zombie(Vector2(position.x,position.y + 50)) #создаём зомби со смещениями spawn_zombie(Vector2(position.x + 50,position.y)) #создаём зомби со смещениями queue_free() #Функция спавна зомби в качестве аргумента Vector2 func spawn_zombie(pos): var z = zombie_scene.instance() #задаём для зомби позицию z.position = pos #поворот z.rotation = rotation #игрока z.player = player get_parent().add_child(z)# добавляем зомби
Всё подробно прокомментировал, переходим к следующему зомби.
Зомби за щитом
Для начала нам нужно создать новую сцену для щита.
Главным узлом выбираем StaticBody2D(Shield), дочерние элементы:

Вместо спрайта можно использовать AnimatedSprite, и сделать что чем меньше у щита хп, тем он больше разрушается. Задавать объект столкновений будем через CollisionPolygon2D, нам в��жно чтобы размеры щита были точно, как спрайт, потому-что метким выстрелом мы должны наносить урон зомби. И добавляем 2 ColorRect, для полоски жизни, как у зомби.
Как пользоваться CollisionPolygon2D, Нажимаем на CollisionPolygon2D в дереве объектов, выбираем режим выделения(Q)
Если не выбран, и нажимаем по контуру спрайта
Так-же в настройках задаём имя новому слою столкновений( у меня 6-й и назвал Enemy_armor). Переходим на сцену DefaultBullet и ставим для неё новые настройки столкновений.

Навешиваем скрипт на Shield и переходим к его редактированию:
extends StaticBody2D #объявляем элементы дерева объектов onready var _red_health = $HealthBar/RedHealth #Цена одного деления жизней var health_size #Размер жизней export var health = 10 func _ready(): #Вычисляем цену деления health_size = round(_red_health.rect_size.x / health) #Функция получения урона func hit(damage): health -= damage _red_health.rect_size.x -= health_size * damage if (health <= 0): #Если <= 0, то удалился queue_free()
Теперь переходим к созданию самого зомби:

Наследуем, как главный узел сцену DefaultEnemy, добавляем Position2D(ShieldPosition) в дерево объектов. Тут будет стоять щит, выстраиваем его под ваш спрайт. Расширяем скрипт и переходим к редактированию:
extends "res://scenes/Enemy/DefaultEnemy/DefaultEnemy.gd" #Объявляем элементы дерева объектов onready var _shield_position = $ShieldPosition #Сцена с щитом var shield_scene = preload("res://scenes/Enemy/ZombieShield/Shield.tscn") #Подключаем рандомайз, для крови func _ready(): randomize() _animated_sprite.playing = true #Включили анимацию spawn_shield()#функция спавна щита func spawn_shield(): var s = shield_scene.instance() #дали местоположения s.position = _shield_position.position #повернули s.rotation = rotation add_child(s)# добавляем щит
Всё подробно прокомментировал. Ещё у нас остались страшный зомби и обычный, которого создавали на первом уроке. Их не трогаем у них только выставляем характеристики.
Таблица с характеристиками:
Зомби | хп | скорость | урон |
Обычный | 5 | 2 | 1 |
Умный | 3 | 2 | 1 |
Страшный | 3 | 6 | 2 |
Толстый | 10 | 0.5 | 1 |
За щитом | 5 | 1 | 1 |
Щит | 10 | - | - |
Синглтон EnemyNames
Давайте ещё создадим синглтон, который будет хранить константы со сценами врагов.

Помещаем его в ту же папку, что и три остальных синглтона в настройках добавляем его в автозагрузку и делаем глобальной переменной. Переходим к редактированию.
extends Node #обычный const ZOMBIE1 = preload("res://scenes/Enemy/Zombie1/Zombie1.tscn") #умный const SMARTZOMBIE = preload("res://scenes/Enemy/SmartZombie/SmartZombie.tscn") #страшный const ZOMBIESCARY = preload("res://scenes/Enemy/ZombieScary/ZombieScary.tscn") #тослтый const FATZOMBIE = preload("res://scenes/Enemy/FatZombie/FatZombie.tscn") #за щитом const ZOMBIESHEILD = preload("res://scenes/Enemy/ZombieShield/ZombieShield.tscn") #щит const SHIELD = preload("res://scenes/Enemy/ZombieShield/Shield.tscn") #кровь const ZOMBIEBLOOD = preload("res://scenes/Enemy/ZombieBlood/ZombieBlood.tscn")
Теперь давайте везде, где использовали просто preload переопределим на константы, не трогая сцену игры.
Где менять?
#DefaultEnemy 4 строка var blood_scene = EnemyNames.ZOMBIEBLOOD
#FatZombie 3 строка var zombie_scene = EnemyNames.ZOMBIE1
#ZombieShield 5 строка var shield_scene = EnemyNames.SHIELD
На этом с врагами закончили.
Главное меню

Добавляем 2 AudioStreamPlayer2D, в дерево объектов (ClickSound) и (MusicTheme). ClickSound - будет проигрывать звук при нажатии мышкой, MusicTheme - будет проигрывать фоновую музыку. На MusicTheme ставим autoplay - true, и зацикливаем воспроизведение, для этого нажимаем на нашу добавленную аудиозапись, откроются расширенные настройки, там ставим выставляем loop, от выбранного вами формата настройки могут различаться, ниже 3 примера.. В код следует добавить:
#добавляем эллемент из дерева объектов onready var _click_sound = $ClickSound func _process(delta): if (Input.is_action_just_pressed("fire")):#Если нажата ЛКМ _click_sound.play()



Синглтон SelectedCharacter
Создаём новый синглтон, который будет хранить ссылку на сцену с выбранным персонажем. Создаём новый файл, помещаем его в папку scripts и в настройках ставим автозагрузку и делаем глобальной переменной.
extends Node #перменаная хранящая текущего героя var Character #функция задания переменной героя func set_character(scene): Character = scene
Меню выбора персонажа
Теперь у нас 4 игровых персонажа, значит после нажатия кнопки Start Game, главного меню должна появится сцена выбора персонажа, а только после начаться игра за конкретного персонажа. Значит на сцене выбора, игрок выбирает персонажа, выбранный персонаж записывается в синглтон и в функции _init() сцены игры добавляется на главный экран.
Для начала создадим сцену выбора персонажа, главным узлом выбираем Node2D, добавляем следующие дочерние элементы:

⦁ Sprite
⦁ TextureButton(BrutalHeroBtn)
⦁ TextureButton(CowboyBtn)
⦁ TextureButton(SoldierBtn)
⦁ TextureButton(RobotBtn)
⦁ TextureButton(StartGameBtn)
⦁ AnimatedSprite
⦁ TextureButton(StartGameBtn)
⦁ label(StartGameLbl)-дочерний к StartGameBtn
⦁ TextureButton(ReturnBtn)
⦁ Label(ReturnLbl) - дочерний к ReturnBtn
⦁ AudioStreamPlayer2D(ClickSound)
⦁ Label(CharacterParam)
⦁ AudioStreamPlayer2D(MusicTheme)
Sprite - это наш задний фон меню выбора, первые 4 кнопки отвечают за выбор персонажа, AnimatedSprite - воспроизводит анимацию танца выбранного персонажа, StartGameBtn - запускает игру, ReturnBtn - возв��ащает в меню. CharacterParam - в него будет записываться информация о персонаже(урон, хп...), ClickSound - звук при нажатии, Musictheme - фоновая музыка.
Расположить элементы следует примерно следующим образом:


Персонажи в квадратиках - это кнопки выбора. Для кнопок выстраиваем текстурки, для надписей настраиваем шрифт, кнопку StartGameBtn - по умолчанию делаем disable, станет активной, только после выбора персонажа. В AnimatedSprite заливаем наши анимации танцев и добавляем пустую Default. CharacterParam, располагается в блокноте. Заливаем звуки. Навешиваем скрипт и переходим к его редактированию:
extends Node2D #Добавляем переменные дерева onready var _animated_sprite = $AnimatedSprite onready var _brutalhero_btn = $BrutalHeroBtn onready var _cowboy_btn = $CowboyBtn onready var _soldier_btn = $SoldierBtn onready var _robot_btn = $RobotBtn onready var _click_sound = $ClickSound onready var _start_game_btn = $StartGameBtn onready var _return_btn = $ReturnBtn onready var _character_param = $CharacterParam #Считываем щелчёк мыши, для воспроизведения звука func _process(delta): if (Input.is_action_just_pressed("fire")): _click_sound.play() #Если выбран BrutalHero func _on_BrutalHeroBtn_pressed(): _animated_sprite.play("Dance1")#Включаем его анимацию _start_game_btn.disabled = false#Включаем кнопку старта игры _character_param.text = "Character \nBrutalHero \nDamage: 1 \nHP: 5 \nSpeed: 200 \nEquip: Blaster"#Записываем текст SelectedCharacter.set_character(CharacterNames.BRUTALHERO)#В синглтон записываем BrutalHero #Если выбран Cowboy func _on_CowboyBtn_pressed(): _animated_sprite.play("Dance2")#Включаем его анимацию _start_game_btn.disabled = false#Включаем кнопку старта игры _character_param.text = "Character \nCowboy \nDamage: 2.5 \nHP: 3 \nSpeed: 150 \nEquip: Shotgun"#Записываем текст SelectedCharacter.set_character(CharacterNames.COWBOY)#В синглтон записываем Cowboy #Если выбран Soldier func _on_SoldierBtn_pressed(): _animated_sprite.play("Dance3")#Включаем его анимацию _start_game_btn.disabled = false#Включаем кнопку старта игры _character_param.text = "Character \nSoldier \nDamage: 1 \nHP: 5 \nSpeed: 200 \nEquip: Bazooka"#Записываем текст SelectedCharacter.set_character(CharacterNames.SOLDIER)#В синглтон записываем Soldier #Если выбран Robot func _on_RobotBtn_pressed(): _animated_sprite.play("Dance4")#Включаем его анимацию _start_game_btn.disabled = false#Включаем кнопку старта игры _character_param.text = "Character \nRobot \nDamage: 0.75 \nHP: 10 \nSpeed: 300 \nEquip: Rifle"#Записываем текст SelectedCharacter.set_character(CharacterNames.ROBOT)#В синглтон записываем Robot #Нажата кнопка Return func _on_ReturnBtn_pressed(): SceneLoader.build_map_path("MainMenu")#Вернулись в главное меню #Нажата кнопка StartGame func _on_StartGameBtn_pressed(): SceneLoader.build_map_path("GameScene")#Запустили сцену игры.
Возвращаемся к скрипту главного меню и изменяем функцию _on_StartGameBtn_pressed():
#При нажатии кнопки начала игры, вызывается функция нашего Синглтона и переключается на сцену выбора func _on_StartGameBtn_pressed(): SceneLoader.build_map_path("PickMenu")
Точка спавна врагов
На данный момент, мы имеем, что зомби просто берут и появляются без предупреждения по периметру нашего MobPath. Нужно добавить предупреждение, так скажем. В чём задумка, будет появляться не зомби, а точка где он появится и через секунду будет появляться враг.
В эту точку нужно сохранять сцену персонажа и сцену зомби.

Главным узлом сцены выбираем StaticBody2D(SpawnPoint) и добавляем AnimationSprite2D(Анимация расширения круга, в моём случае) чтобы было видно, когда появится.
Добавляем скрипт и переходим к его редактированию:
extends StaticBody2D #переменная хранящая сцену зомби, объявлять её будем на сцене игры export onready var zombie_enemy #переменная хранящая сцену игрока, объявлять её будем на сцене игры export onready var player #Включаем анимацию func _ready(): $AnimatedSprite.play("Idle") #Функция спавна зомби func spawn_zombie(): var z = zombie_enemy.instance() z.position = position#Настраиваем позицию z.player = player get_parent().add_child(z)# добавляем зомби queue_free() #У меня анимация идёт ровно 1 секунду и по её завершению появляется зомби. func _on_AnimatedSprite_animation_finished(): spawn_zombie()
Добавляем эту сцену в наш синглтон EnemyNames.
#СпавнПоинт const SPAWNPOINT = preload("res://scenes/Enemy/SpawnPoint/SpawnPoint.tscn")
И не забываем добавить в группу all_enemy.
С этим разобрались, теперь нужно немного усовершенствовать скрипт DefaultCharacter.
DefaultCharacter
Нам нужно добавить переменную, хранящую количество оружия у персонажа и функцию, которая возвращает true, если ещё есть слоты для оружия и false, если нет слотов.
var weapon_count = 0 #Переменная хранящая количество оружия func can_add(): if (weapon_count < 6): return true else: return false
Так-же стоит добавить в функции add_equip_item() и remove_equip_item() увеличение и уменьшение этой переменной.
#удаляем оружие 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
Полный код болванки персонажа
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#Множитель урона #Объявляем переменные только для этого скрипта 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() 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
Сцена игры

На сцену игры добавляем Position2D(CharacterSpawnPoint), AudioStreamPlayer2D(MusicTheme), Timer(Difficult), ColorRect(Spawn). CharacterSpawnPoint - будет местоположение персонажа при начале игры.MusicTheme - наша фоновая музыка,Difficult- таймер по истечению которого будет увеличиваться сложность,Spawn - квадрат внутри которого, в любой точке может появится зомби.

Теперь в дерево объектов нажимаем на ColorRect(Spawn), в инспекторе color, и показатель A(alfa) - делаем равным 0. Для Timer(Difficult), выставляем autostart = true и Wait Time( у меня 10, то есть каждые 10 секунд игра становится сложнее). Добавляем музыку MusicTheme и зацикливаем её, не забываем про autoplay.
Переходим в скрипт и начинаем его изменять.
Функция спавна героя
#Функция создания героя 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) player = p p.z_index = 2#Задаём z_index - 2, чтоыб герой ходил сверху крови
Вызываем эту функцию в функции _init():
#Функция инициализации func _init(): spawn_hero(Vector2(0,0))#Вызываем функцию создания героя
Функция _ready
Для начала объявляем наши новые переменные из дерева объектов
onready var _character_spawn_point = $CharacterSpawnPoint onready var spawn = $Spawn
Изменённая функция _ready():
#Функция старта(срабатывает после _init) func _ready(): _mob_spawn_timer.start()#Включили таймер спавна randomize()# подключаем генератор случайных чисел player.position = _character_spawn_point.global_position #Передали игроку установленное в редакторе местоположение player._user_interface.init_health(player.health)# инициализируем наш UI player.connect("dead",self,"_on_Player_dead")#Привязываем сигнал о смерти игрока
Функция создания точки спавна врага
Теперь удалим функцию spawn_zombie() и создадим функцию spawn_point():
#Функция призыва точки спавна в качестве аргумента используется сцена с врагом 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)# добавляем точку спавна
Усложнение игры с течением времени
Объявляем переменные для усложнения игры:
#Массив всего оружия 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#шанс добавления предметов
Сама функция усложнения игры, будет срабатывать по сигналу таймера Difficult:
#Усложняем игру 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.
Обработка сигнала от MobSpawnTimer
#Функция срабатывания таймера 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()#включили
Полный код сцены игры
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 #Массив всего оружия 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()#Включили таймер спавна 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(): get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены 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) 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.
Сохранение лучшего счёта
Под конец статьи добавим сохранение лучшего счёта. Будем сохранять в файл Save.Save, в директорию пользователя (C:\Users\имя_пользователя\AppData\Roaming\Godot\app_userdata\Название проекта). Предварительно создавать там файл не нужно.
Сначала модифицируем скрипт пользовательского интерфейса, добавив в него функцию для получения счёта:
#Функция передачи счёта func get_score(): return int(_score.text)
Дальше изменим наш синглтон с именами персонажей, добавим в него так-же их лучшие счета и функцию считывания их:
#Переменные с лучшими счетами var brutalhero_score var cowboy_score var soldier_score var robot_score #Получаем лучший счёт func set_score(): var file = File.new()#новый файл file.open("user://Save.Save", File.READ)#Открыли var content = file.get_as_text()#Записали контент файла var dir = parse_json(content)#Перезаписали контент, как "Словарь" #Если словать не null, то записываем значения из него if(dir!=null): brutalhero_score = dir["BrutalHero"] cowboy_score = dir["Cowboy"] soldier_score = dir["Soldier"] robot_score = dir["Robot"] #Иначе нолики else: brutalhero_score = 0 cowboy_score = 0 soldier_score = 0 robot_score = 0
Дальше модифицируем скрипт сцены игры, добавив функцию сохранения счёта и будет вызывать её при смерти игрока.
Функция сохранения:
#функция сохранения в качестве аргумента берёт текущий счёт 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 _on_Player_dead(): get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены save_record(player._user_interface.get_score())#записали результаты SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
Осталось немного доработать сцену с выбором персонажа. в функции _ready() будем вызывать метод set_score, синглтона CharacterNames. и при нажатии на кнопку персонажа в надпись будем дополнительно выводить рекорд.
Функция _ready():
func _ready(): CharacterNames.set_score()#Взяли записи из файла сохранения
Один из обработчиков нажатия кнопки персонажа:
#Если выбран BrutalHero func _on_BrutalHeroBtn_pressed(): _animated_sprite.play("Dance1")#Включаем его анимацию _start_game_btn.disabled = false#Включаем кнопку старта игры _character_param.text = "Character \nBrutalHero \nDamage: 1 \nHP: 5 \nSpeed: 200 \nEquip: Blaster \nMax score: " + String(CharacterNames.brutalhero_score)#Записываем текст SelectedCharacter.set_character(CharacterNames.BRUTALHERO)#В синглтон записываем BrutalHero
Полный код меню выбора персонажа
На этом эта статья подошла к концу. В принципе на этом можно считать игру законченной в неё уже можно играть и пытаться ставить всё большие и большие рекорды. Ещё точно выйдет одна статья по этому проекту, в которой мы добавим: локальный мультиплеер, возможность создания различных построек, механику поднятия уровня персонажа с чем-то вроде дерева талантов, вы выбрали именно эти 3 пункта в опросе под предыдущей статьей(все набрали по 7, получается 777, на удачу так сказать).
