Продолжаем и дальше создавать свою первую игру на 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, на удачу так сказать).