В предыдущей части мы создали болванки сцен: Персонажа, Оружия, Снаряда и врага, сегодня немного доработаем эти болванки, добавим разных пушек, сцену игры и главное меню. Давайте начнём с доработки сцены оружия.
Оружие
Открываем нашу сцену DefaultWeapons, это та, которая болванка для оружия.
Для начала поработаем немного с анимацией стрельбы, сейчас мы имеем, что при удержании кнопки выстрела воспроизводится анимация выстрела, при отпускании она прекращается, то-есть у оружия с долгой перезарядкой анимация выстрела будет проигрываться, даже когда оружие на перезарядке. Или если быстро нажать кнопку выстрела, то пуля полетит, а анимация не воспроизведётся. Поэтому будем воспроизводить анимацию выстрела по условию, если нажата кнопка выстрела и оружие не на перезарядке.
func get_input():
if ((global_position - get_global_mouse_position()).x < 0):
_animated_sprite.flip_v = false
else:
_animated_sprite.flip_v = true
look_at(get_global_mouse_position())# направляем взгляд оружия на курсор мыши
if (Input.is_action_pressed('fire') && _fire_couldown_timer.is_stopped()):#нажат выстрел и нет перезарядки
_animated_sprite.play("Fire")#анимация выстрела
fire()#вызываем функцию выстрела
Добавляем сигнал от AnimatedSprite о том, что анимация проигралась до конца(animation_finished())
func _on_AnimatedSprite_animation_finished():
if (_animated_sprite.animation == "Fire"):#если анимация выстрела, то меняем на обычную
_animated_sprite.play("Default")
Дальше давайте разберёмся с функцией выстрела. Наше стандартное оружие не обязательно учить стрелять, поэтому можно просто прописать в функции выстрела pass и для каждого отдельного оружия писать механику выстрела именно для этого оружия.
Полный листинг Оружия
extends KinematicBody2D
#Добавляем элементы дерева объектов в код
onready var _animated_sprite = $AnimatedSprite
onready var _fire_couldown_timer = $FireCouldownTimer
#Объявляем переменные, которые можно менять извне
export (PackedScene) var bullet_scene # это будет сцена нашей пули
export var fire_rate = 0.2 # скорость атаки
export var damage = 1 # урон
#функция обработки нажатий
func get_input():
if ((global_position - get_global_mouse_position()).x < 0):
_animated_sprite.flip_v = false
else:
_animated_sprite.flip_v = true
look_at(get_global_mouse_position())# направляем взгляд оружия на курсор мыши
if (Input.is_action_pressed('fire') && _fire_couldown_timer.is_stopped()):#нажат выстрел и нет перезарядки
_animated_sprite.play("Fire")#анимация выстрела
fire()#вызываем функцию выстрела
func _ready():
_fire_couldown_timer.wait_time = fire_rate # выставляем скорость атаки
func spawn_bullet(rot):# передаём параметр дополнительного поворота пули, позже пригодится
var b = bullet_scene.instance()
var new_position = position
var direction = rotation - rot
get_parent().add_child(b)# добавляем пулю, как потомка оружия
b.start(new_position,direction)
b.damage = damage# задаём пуле урон
func _on_AnimatedSprite_animation_finished():
if (_animated_sprite.animation == "Fire"):#если анимация выстрела, то меняем на обычную
_animated_sprite.play("Default")
# функция выстрела
func fire():
pass #Это дефолтный класс, ему нет нужны стрелять, для каждого другого оружия
#будем определять класс стрельбы заного.
func _physics_process(delta):
get_input()
Пуля
Тут в принципе особо ничего изменять не надо, только добавить функцию обработки поведения при столкновении, потому-что например ракета должна взрываться и ей потребуется другая обработка, в отличии от обычного выстрела. Вызывать функцию стоит при любом обнаружении столкновений. Для болванки пули, также оставляем функцию пустой.
#Функция обработки поведения при столкновении
func collision_action(_collision_object):#_collision_object - объект столкновения
pass
Полный листинг Пули
onready var _animated_sprite = $AnimatedSprite
var velocity = Vector2()
export var damage = 1
export var speed = 750
#функция для задания стартового положения
func start(pos, dir):
rotation = dir
position = pos
velocity = Vector2(speed, 0).rotated(rotation)
func _physics_process(delta):
var collision = move_and_collide(velocity * delta)
if collision:
collision_action(collision.colider)
#Функция обработки сигнала от VisibilityNotifier, Сигнал screen_exited
func _on_VisibilityNotifier2D_screen_exited():
queue_free()
#Функция обработки поведения при столкновении
func collision_action(_collision_object):#_collision_object - объект столкновения
pass
С настройками оружия разобрались, давайте перейдём к созданию нового и доработке старого оружия.
Бластер
Сцены оружия и пули для бластера остаются без изменений. Для начала настроим пулю. Нам нужно добавить для пули обработчик столкновения. Пкм на корень сцены -> Расширить скрипт
В созданном скрипте, нужно переопределить функцию collision_action().
func collision_action(_collision_object):
if _collision_object.has_method("hit"): #Вызвали метод, если он есть
_collision_object.hit(damage)
queue_free()#Удалили пулю
Перейдём к настройке оружия, выставляем его характеристики, у меня (урон = 1, перезарядка = 0.2) и не забываем прицепить сцену нашей пули.
Дальше расширяем скрипт бластера и переопределяем функцию fire()
func fire():
if (_fire_couldown_timer.is_stopped()): # не на перезарядке
spawn_bullet(0) # создаём пулю с 0-м дополнительным поворотом
_fire_couldown_timer.start()
Наш бластер готов идём дальше
Дробовик
Для дробовика сцены оставляем стандартными(конечно ставим новые спрайтики и анимации). Расширяем скрипт для пули и переопределяем функцию collision_action(), она будет такая-же, как для бластера.
Расширяем скрипт для дробовика и переопределяем функцию fire()
func fire():
if (_fire_couldown_timer.is_stopped()):
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()# включаем перезарядку
Для дробовика я выставил урон = 1 и скорость атаки = 0.5
Винтовка
Сцены для винтовки никак изменять не надо, только конечно заменить анимации. Скрипт выстрела будет такой-же, как для бластера.
Пуля из винтовки должна будет способна пробить несколько врагов, если враг после попадания умер. Перейдём сразу к расширению скрипта пули. Функция collision_action() примет следующий вид:
export var fly_count = 3 #сколько врагов пробьёт на сквозь
var previous_enemy #враг который уже получил урон
func collision_action(_collision_object):
if (_collision_object.has_method("hit") && previous_enemy != _collision_object):
#Вызвали метод, если он есть
#и враг не равен предыдущему
_collision_object.hit(damage)
fly_count -=1 # уменьшили счётчик возможных пробиваний
previous_enemy = _collision_object # запомнили врага
if (fly_count > 0): # если счётчик не 0, продолжаем движение
move_and_slide(velocity)
else:
queue_free()
else:
queue_free()
И обязательно не забудьте объявить новые переменные.
Для винтовки я выставил урон 5 и перезарядку 2, функция fire(), ничем не отличается от бластера.
БАЗУУУУКААААА
Какой экшен-шутер может обойтись без красивых взрывов от выстрела из ракетницы, правильно никакой. Тут уже нужно будет немного повозиться с сценой ракеты, поэтому начнём с сцены самой базуки.
Тут ничего добавлять не нужно, только изменить характеристики и добавить спрайты базуки. У меня урон = 3, а перезарядка = 5.
Теперь переходим к интересному, к сцене ракеты.
Тут есть как говорится 2 стула. Можно либо создать отдельную сцену для взрыва, и когда снаряд касается любого игрового объекта через код создавать взрыв и удалять снаряд, либо создать взрыв, как вторую анимацию у ракеты и добавить ещё пару элементов на сцену. Если в вашем проекте предусмотрено несколько объектов, которые должны взрываться( различные гранаты, ракетницы ) , то скорее всего стоит выбрать первый вариант, я же выбрал второй вариант, он как-то попроще, на мой взгляд.
Для начала в дерево объектов следует добавить:
⦁ Area2D(Bum)
⦁ CollisionShape2D(CollisionShapeBum), как дочерний к Area2D
⦁ Timer(BumLiveTime),как дочерний к Area2D
И у CollisionShapeBum, в инспекторе параметр Disabled, включить. Подогнать все элементы. У меня это выглядит следующим образом:
⦁ красным обведено - CollisionShapeBum
⦁ зелёным - CollisionShape2D
Таймер будем задавать маленьким, у меня 0.2 секунды, это время жизни взрыва, он всё-таки не моментально взорвался и всё, у него обязательно сделать Oneshot = true.
Логика проста, когда объект с чем-то коснулся, он становится взрывом CollisionShape2D, выключается, а CollisionShapeBum включается. Добавляем в скрипт сигнал от Area2D body_entered, на вхождение тела в область коллизии и в нём уже наносим урон, если это возможно
Расширяем скрипт и переходим к его редактированию:
onready var _collision_shape = $CollisionShape2D#Фигура столкновений ракеты
onready var _collision_shape_bum = $Bum/CollisionShapeBum#Фигура столкновений взрыва
onready var _bum_live_time = $Bum/BumLiveTime #Таймер жизни взрыва
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()
func _on_Bum_body_entered(body):
if(body.has_method("hit")):
body.hit(damage)
func _on_BumLiveTime_timeout():
queue_free()
Перейдём к модификации сцены персонажа.
Персонаж
К болванке сцены персонажа, нужно привязать камеру.
Сделать что-то вроде рюкзака, в котором будет хранится используемое оружие.
Добавить сигналы о получении персонажем урона и смерти
Начнём с простого. На узел DefaultCharacter добавляем Camera2D и в инспекторе выставляем current = true. На этом всё, камеру добавили.
Что я подразумеваю под рюкзаком, на сцене будет узел содержащий 6 position2D, на этих позициях будет отображаться оружие которое сейчас есть у персонажа. У самого персонажа будет массив с ссылками на сцены оружия, которое он использует. Начнём с добавления узла рюкзак. К персонажу добавляем Node2D(Backpack) и 6 Position2D(Slot1...Slot6), как дочерние у Node2D:
Навешиваем на Backpack скрипт и переходим к его редактированию. Нам нужно добавить простенькую функцию, которая возвращает Вектор позиции данного слота.
extends Node2D
#Массив Слотов
onready var backpack = [$Slot1,$Slot2,$Slot3,$Slot4,$Slot5,$Slot6]
#функция получения текущего положения
func get_slot_position(elem):
return backpack[elem].position
Дальше переходим к редактированию скрипта персонажа:
В начале скрипта нам нужно объявить новую переменную, backpack_items, на 6 эллементов, в нём будет хранится либо сцена, либо null. Так-же объявить сигнал take_damage(damage) и dead
#Рюкзак персонажа
var backpack_items = [preload("res://scenes/Weapons/Shotgun/Shotgun.tscn"),null,null,null,null,null]
#Сигнал о получении урона
signal take_damage(damage)
#Сигнал о смерти
signal dead
В функцию получения урона нужно добавить отправку сигналов:
#Функция получения урона
func take_damage(dmg):
if(_immortal_timer.is_stopped()): #Проверяем не бессмертен ли наш персонаж
health -= dmg
_animated_sprite.play("TakeDamage")
emit_signal("take_damage",dmg) #Отправляем сигнал о получении урона
_immortal_timer.start() #Запускаем таймер после получения урона
if(health == 0):
emit_signal("dead")#Отправляем сигнал о смерти
Теперь перейдём к созданию функций по работе с рюкзаком.
Сначала напишем функцию, которая принимает в качестве аргумента номер слота от 0 до 5, в котором нужно отрисовать оружие:
#функция прикрепления оружия
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)# у меня стоит масштабировать оружие, возможно у вас нет
Дальше нам понадобится функция которая будет создавать все элементы рюкзака на сцене и если какой-то на сцене уже есть, то удалять его и рисовать заново:
#одеваем всё доступное оружие
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()#удаляем объект
И в конце понадобится функция которая будет добавлять предмет в рюкзак, в качестве аргумента будет принимать сцену:
#добавляем оружие
func add_equip_item(item):
for i in range(6):
if (backpack_items[i] == null):#Находим первый пустой элемент массива
backpack_items[i] = item#заливаем в него сцену оружия
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
#Объявляем переменные, которые можно менять извне
export var health = 5 #Жизни
export var speed = 200 #Скорость
#Объявляем переменные только для этого скрипта
var velocity = Vector2.ZERO #Вектор направления
var direction = Vector2.ZERO #Вектор движения
var backpack_items = [preload("res://scenes/Weapons/Blaster/Blaster.tscn"),preload("res://scenes/Weapons/Blaster/Blaster.tscn"),null,null,null,null]
#Сигнал о получении урона
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) #Отправляем сигнал о получении урона
_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)# у меня стоит масштабировать оружие, возможно у вас нет
#одеваем всё доступное оружие
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()#удаляем объект
#добавляем оружие
func add_equip_item(item):
for i in range(6):
if (backpack_items[i] == null):#Находим первый пустой элемент массива
backpack_items[i] = item#заливаем в него сцену оружия
equip_all()#Одеваем всё оружие
return
Давайте ещё изменим стандартный курсор, на нарисованный нами
Курсор
Переходим в настройки проекта -> Дисплей -> Курсор мыши. В пользовательском изображении, указываем путь к нашему курсору.
Перейдём к созданию меню и главной сцены, но для начала нам нужно написать небольшой синглтон, который будет отвечать за переключение сцен.
Синглтон SceneLoader
Для начала перейдите на вкладку скрипт и создайте скрипт файл. Обязательно проверьте чтобы он наследовал узел Node. Я сохраним его в отдельную папку scripts
Теперь переходим в настройки проекта, вкладка автозагрузка нажимаем на иконку папки и указываем путь до нашего скрипта, нажимаем добавить, в столбце глобальная переменная должно быть включить - true
Переходим к редактированию этого скрипта:
extends Node
# Константа хранящая путь к папке со сценами
const MAP_ROOT = "res://scenes/Game/"
var current_scene = null # текущая сцена
#При первом запуске объявляем текущую сцену
func _ready():
var root = get_tree().root
current_scene = root.get_child(root.get_child_count() - 1)
#подготавливаем полный путь сцены, тоесть сюда будет передаваться только название
func build_map_path(map_name):
var path = MAP_ROOT.plus_file(map_name + "Scene/"+ map_name + ".tscn")
#у меня сцены карт хранятся в папке scenes/Game/Папка с названием сцены/файл с названием сцены.tscn
_change_map(path)
#функция смены карты
func _change_map(path):
#безопасно удаляем текущую сцены
current_scene.free()
# загружаем в в переменную текущей сцены, требуемую сцену
current_scene = ResourceLoader.load(path).instance()
# добавляем требуемую сцену, как потомка корня дерева
get_tree().root.add_child(current_scene)
# передаём параметр установленной сцены через API
get_tree().current_scene = current_scene
Тут закончили теперь давайте накидаем простенькое главное меню
Главное меню
Будем рассуждать логически у главного меню должно быть что-то похожее на задний фон и кнопочки. В нашем проекте я ещё добавлю анимацию танцующего главного героя на задний фон. Главным узлом сцены выбираем Node2D(MainMenu). Дальше добавляем элементы в дерево объектов:
⦁ Sprite(Background)
⦁ AnimatedSprite
⦁ TextureButton(StartGameBtn)
⦁ Label(StartGameLbl) - подчинённый TextureButton
⦁ TextureButton(SettingBtn)
⦁ Label(SettingLbl) - подчинённый SettingBtn
⦁ TextureButton(ExitGameBtn)
⦁ Label(ExitGameLbl) - подчинённый ExitGameBtn
В инспекторе у надписей нужно написать сам текст кнопки, дальше в инспекторе переходим во вкладку Control-> Theme Overrides -> Fonts и в свойстве Font, выбираем новый DynamicFont, дважды нажимаем на него, открываем свойство Font и указываем путь к нашему шрифту. Дальше из меню Fonts->Font выбираем Settings и указываем желаемый размер шрифта( у меня 36). Шрифт настроили, теперь давайте настроим расположение. В верхней панели нажимаем "Макет" и выбираем в нём расположение "По центру".
Меню примерно должно выглядеть так:
Персонаж на экране телефона как раз таки и есть AnimatedSprite, он будет танцевать. Сцену настроек сделаем в следующей части статьи.
Перейдём к скрипту:
extends Node2D
#Переменные
onready var _animated_sprite = $AnimatedSprite
onready var _start_btn = $StartGameBtn
onready var _settings_btn = $SettingsBtn
onready var _exit_btn = $ExitGameBtn
#При запуске включается анимация
func _ready():
_animated_sprite.play("Dance")
#Когда анимация закончилась, персонаж поворачивается
func _on_AnimatedSprite_animation_finished():
_animated_sprite.flip_h = !_animated_sprite.flip_h
#При нажатии кнопки начала игры, вызывается функция нашего Синглтона и переключается на сцену игры
func _on_StartGameBtn_pressed():
SceneLoader.build_map_path("GameScene")
#При нажатии кнопки выхода, игра закрывается
func _on_ExitGameBtn_pressed():
get_tree().quit()
Скрипт короткий и вполне понятный, получаем сигналы с кнопок и обрабатываем их.
Сейчас мы сделаем простенькую сцену игры, враги будут появляться случайно по периметру карты. За каждую прожитую секунду будут начисляться очки.
Для начала создадим сцену с пользовательским интерфейсом:
Пользовательский интерфейс
Главным узлом сцены выбираем Node2D, дочерние к нему элементы:
⦁ ColorRect(CurrentHealthBar)
⦁ TextureRect(HealthBar) - дочерний у ColorRect
⦁ Label(Health) - дочерний у ColorRect
⦁ Label(Score)
⦁ Timer(ScoreAddTimer) - дочерний у Label
Label(Health) - будет отображать текущий и максимальный запас жизней, через слэш.
ColorRect - это просто зелёная полоска жизней, которая будет уменьшаться, как у врагов
TextureRect - это текстура HealthBar
Label(Score) - отображает заработанные очки
Timer - отчёт до начисления очков( 1 сек)
Теперь навешиваем скрипт на Node2D и переходим к его редактированию:
extends Node2D
#Элементы дерева
onready var _score_add_timer = $Score/ScoreAddTimer
onready var _score = $Score
onready var _current_health_bar = $CurrentHealthBar
onready var health = $CurrentHealthBar/Health
var max_health #Максимальное кол-во хп
var current_health #Текущее кол-во хп
var health_bar_size #Размер на который нужно уменьшать зелёный квадрат
# Функция заадния всех переменных
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 _on_ScoreAddTimer_timeout():
_score.text = String(int(_score.text) + 1)
Прокомментировал всё достаточно подробно, не вижу смысла что-то дополнять
Теперь создаём сцену игры и переходим к её редактированию.
Сцена игры
Главным элементом сцены выбираем Node2D, дочерние элементы:
⦁ StaticBody2D, дочерние к нему
⦁ Sprite(background), дочерний к StaticBody2D
⦁ 4 CollisionShape2D, дочерний к StaticBody2D
⦁ Timer(MobSpawnTimer)
⦁ Path2D(MobPath)
⦁ PathFollow2D(MobFollowPath), дочерний к Path2D
⦁ Сцена нашего персонажа
⦁ Сцена UserInterface, дочерняя к нашему персонажу
Для чего-же нам StaticBody с 4-мя CollisionShape, Это будут граница экрана за которые персонаж не может выйти, как примерно стоит всё это расположить:
Для StaticBody2D, Нужно настроить CollisionObject2D, как мы это делали в предыдущей части, теперь нужно дать название 5-му слою в списке(у меня это Map) и присвоить нашему StaticBody2D, Layer 5. Дальше отредактируем столкновение для DefaultCharacter и DefaultEnemy:
Спавнить зомби, будем так-же, как и в первой статье на тестовой сцене, в следующей части это переработаем.
Навешиваем на Node2D скрипт и переходим к его редактированию:
#Сцена Врага
export (PackedScene) var zombie_enemy
#Элементы дерева
onready var _mob_spawn_timer = $MobSpawnTimer
onready var player = $BrutalHero
onready var _user_interface = $BrutalHero/UserInterface
#Функция призыва Зомби
func spawn_zombie():
var z = zombie_enemy.instance()
var zombie_spawn_location = $MobPath/MobPathFollow
zombie_spawn_location.unit_offset = rand_range(0.0,1.0)#Генерируем случайную точку спавна
z.position = zombie_spawn_location.position#Настраиваем позицию
z.player = player
get_parent().add_child(z)# добавляем зомби
z.speed = 2# присваеваем ему статы
z.health = 5
func _ready():
_mob_spawn_timer.start()
randomize()# подключаем генератор случайных чисел
_user_interface.init_health(player.health)# инициализируем наш UI
func _on_MobSpawnTimer_timeout():
spawn_zombie()
spawn_zombie()
spawn_zombie()
_mob_spawn_timer.start()
#Добавляем сигнал, от нашего персонажа на получение урона
func _on_BrutalHero_take_damage(damage):
_user_interface.take_damage(damage)
#Добавляем сигнал, от нашего персонажа о смерти
func _on_BrutalHero_dead():
get_tree().call_group("all_enemy", "queue_free")#Удаляем всех врагов со сцены
SceneLoader.build_map_path("MainMenu")#Переходим в главное меню
Всё подробно прокомментировал, стоит остановится только на одном моменте, в строке get_tree().call_group("all_enemy", "queue_free") (строка 42), вызываем какую-то группу, что-то с ней делаем, как это так?
Для этого нам нужно перейти на сцену DefaultEnemy, Нажать на главный узел сцены и перейти на вкладку "Узел". На вкладке Узел перейти к Группам, ввести название(all_enemy) и нажать добавить. Теперь когда будет появляться экземпляр сцены DefaultEnemy, он будет добавляться в группу "all_enemy", и при вызове функции queue_free(), мы удаляем всех, кто находится в этой группе.
Вот в принципе и всё. На этом мы можно сказать сделали первую ULTRA-alfa версию нашей игры. В следующей части добавим музыку и звуковые эффекты, а то играть можно, но в тишине скучно. Так-же добавим цель для игры, а то на данном этапе набранные нами очки даже никуда не сохраняются и не отображаются. Добавим меню выбора персонажа и различных персонажей, переработаем сцену игры и добавим большое количество врагов.
Голосуйте в опросе, 3 самых популярных варианта добавим в следующей части.
Комментарии (8)
Zara6502
05.07.2023 06:00+1спасибо за материал. у меня вопрос (я сильно далек от темы) - моя супруга писала анимации в Macromedia Flash (к моменту появления Adobe и ActionScript она уже этим не занималась) и там всё (практически) делалось мышкой. Позднее я пытался поглядеть что же там есть в Юнити и с год назад в Годот и я так и не понял в чем конкретно их плюс - коллективная разработка например или какие-то API? В чем то самое преимущество нежели C#+D2D (или как там сейчас 2д API называется).
vedmak3
Вопрос к автору, почему выбор пал на Godot, а не на Unity например. И если есть опыт на Unity, ваша оценка на сколько Godot годный инструмент.
IsaacBlog Автор
В Godot заинтересовал ООП подход программирования, это пожалуй был самый главный плюс, почему выбрал именно его. О Godot узнал, решил почитать немного и как-то сразу затянуло, с Unity такого не случилось, поэтому выбрал Godot. На Unity опыта нет, но лично мне Godot нравится, для 2D проектом мне он кажется весьма удобным и понятным, в 3D не пробовал.
ildarin
На юнити тоже ООП, но там шарп луа и жс вроде как. В UE тоже ООП.
Жаль, что Вы не написали, что за язык в GODOT.
IsaacBlog Автор
На сколько я понимаю в Unity ооп есть, но не сказать, что реализован, так-же хорошо, как в Godot. В Godot используется собственный язык программирования GDScript, есть поддержка C#, и C++, еще в 3-й версии был VisualScript.
ildarin
Можно подробнее, как именно там ООП лучше реализован?
ChessMax
В Unity lua из коробки никогда не было (насколько мне известно). От поддержки js уже очень давно отказались.
ildarin
Мог ошибиться. Тогда годот хорош гибкостью выбора языка. Хотя, в шарпах можно делать плюсовые вставки, так что формально плюсы в юнити тоже есть.