Привет, Habr. Сегодня я поиграл в Brotato, давайте сделаем что-то подобное на Godot 3.5.

Для начала рассмотрим игру на бумаге:

В игре есть брутальное яйцо на ножках, куча пушек рядом и ещё большая куча врагов.
План определили, приступим к реализации. Для начала создадим 4 сцены которые в дальнейшем будем наследовать для создания разных (персонажей, врагов, оружия).
Начнём с создания проекта:
Выбираем GLE2
В папке проекта создаём 2 папки для хранения графики и сцен.
В настройках проекта в список действий добавляем управление нашим персонажем и ЛКМ для стрельбы( стрелять и наводится будем сами, так веселее)
Проект подготовили, приступаем к сценам.
Для всех DefaultСцен вместо графики я использую встроенную иконку Godot engine, вы можете сделать так-же, либо сразу использовать свою графику.
Начнём со сцены персонажа:
Наше яйцо должно уметь ходить и получать урон, также добавим что у каждого персонажа будет брутальная секретная анимация, которая срабатывает после долгого простоя на месте.

Главным узлом сцены выбираем KinematicBody2D, называем DefaultCharacter. Дочерние элементы:
⦁ AnimatedSprite
⦁ CollisionShape2D
⦁ Timer(IdleAnimationTimer)
⦁ Timer(ImmortalTimer)
Переходим к настройке каждого элемента.
Создаём новый SpriteFrame и добавляем в него анимации:
IdleAnimation - та самая брутальная анимация простоя
Stand - анимация когда персонаж просто стоит
TakeDamage - анимация получения урона
Walk - анимация движения
CollisionShape2D просто подгоняем под Спрайт
У обоих таймеров ставим One Shot в true и выставляем произвольное время, я поставил 5 секунд для IdleAnimation, то-есть анимация начнётся через 5 секунд после простоя и для Immortal 1 секунду, то-есть секунда неуязвимости, после получения урона.
Навешиваем на KinematicBody скрипт и переходим к его редактированию.
extends KinematicBody2D #Добавляем элементы дерева объектов в код onready var _animated_sprite = $AnimatedSprite onready var _idle_animation_timer = $IdleAnimationTimer onready var _immortal_timer = $ImmortalTimer #Объявляем переменные, которые можно менять извне export var health = 5 #Жизни export var speed = 200 #Скорость #Объявляем переменные только для этого скрипта var velocity = Vector2.ZERO #Вектор направления var direction = Vector2.ZERO #Вектор движения #Функция считывания нажатий 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") _immortal_timer.start() #Запускаем таймер после получения урона func _ready(): _animated_sprite.animation = "Stand" # При старте персонаж должен просто стоять func _physics_process(delta): get_input() get_anim() var collider = move_and_collide(direction * delta) # записываем в переменную collider для дальнейшей обработки столкновения func _on_IdleAnimationTimer_timeout(): _animated_sprite.play("IdleAnimation") # Включаем БРУТАЛЬНУЮ анимацию по истечении таймера
Прокомментировал всё достаточно подробно, думаю нет смысла пояснять. Если кто не знает, как прицепить сигнал timeout таймера к скрипту, то следует нажать на Таймер в дереве объектов, вкладка Узел-> Даблклик на timeout() и выбрать скрипт для привязки.
Теперь сцена для пули:

Пуля должна лететь... Ну на этом как бы всё. добавляем:
⦁ KinematicBody2D(Bullet)
⦁ AnimatedSprite2D
⦁ CollisionShape2D
⦁ VisibilityNotifier2D
Добавляем анимацию полёта и выстраиваем CollisionShape
Навешиваем скрипт на KinematicBody2D и переходим к его редактированию
extends KinematicBody2D 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: #Если столкнулись, то удалить объект queue_free() if collision.collider.has_method("hit"): #Вызвали метод, если он есть collision.collider.hit(damage) #Функция обработки сигнала от VisibilityNotifier, Сигнал screen_exited func _on_VisibilityNotifier2D_screen_exited(): queue_free()
Теперь сцена Оружия:
Оружие должно крутится не зависимо от персонажа и стрелять(это конечно только для дальнобойного оружия)

Главный узел выбираем KinematicBody2D и добавляем дочерние элементы:
⦁ AnimatedSprite2D
⦁ Timer(FireCouldownTimer) - Перезарядка между выстрелами
Создаём новый SpriteFrame, в нём две анимации для простоя и выстрела.
Перезарядку оружия будем выставлять через код, поэтому просто делаем, что таймер срабатывает единожды.
У всех сцен, пока-что следует убрать столкновения.
Как это сделать, нажимаем на главный узел сцены -> инспектор ->CollisionObject2D -> и в таблице Mask убираем выделение с 1, это настроим позже
Навешиваем скрипт на KinematicBody2D и переходим к его редактированию
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 # урон export var bullet_speed= 1 # урон func get_input(): # поворачиваем оружие в сторону курсора мыши if ((global_position - get_global_mouse_position()).x < 0): _animated_sprite.flip_v = false look_at(get_global_mouse_position()) else: _animated_sprite.flip_v = true look_at(get_global_mouse_position()) if (Input.is_action_pressed('fire')): _animated_sprite.play("Fire") fire() else: _animated_sprite.play("Default") func _ready(): _fire_couldown_timer.wait_time = fire_rate # выставляем скорость атаки func spawn_bullet(rot):# передаём параметр дополнительного поворота пули, позже пригодится var b = bullet_scene.instance() var new_position = position b.start(new_position,rotation-rot) # выставляем для пули стартовую точку и направление взгляда(взгялд у пули...) get_parent().add_child(b)# добавляем пулю, как потомка оружия b.damage = damage# задаём пуле урон b.speed = bullet_speed # задаём пуле скорость # функция выстрела func fire(): if (_fire_couldown_timer.is_stopped()): spawn_bullet(0) _fire_couldown_timer.start()# включаем перезарядку func _physics_process(delta): get_input()
После объявления переменной bullet_scene(строка 6) и сохранения скрипта, в дереве объектов выбираем узел KinematicBody2D в инспекторе появится название нашей переменной и возможность загрузить сцену

Нажимаем стрелочку -> быстро загрузить -> выбираем название нашей сцены
И наконец сцена вражины.
У врага должна быть полоска жизней и враг должен идти в направлении игрока и бить игрока
добавляем:

⦁ KinematicBody2D(DefaultEnemy)
⦁ AnimatedSprite2D
⦁ CollisionShape2D
⦁ ColorRect(HealthBar)
⦁ ColorRect(RedHealth) как подчинённый у HealthBar
Настраиваем анимацию ходьбы и collisionShape. Зачем нам два квадрата для полоски жизни, задумка в чём HealthBar - Белый прямоугольник, RedHealth - красный прямоугольник одного размера и с одной стартовой точкой. При получении урона RedHealth становится короче, а HealthBar остаётся прежним.

Навешиваем скрипт на KinematicBody2D и переходим к его редактированию:
extends KinematicBody2D #Добавляем элементы дерева объектов в код onready var _animated_sprite = $AnimatedSprite onready var _red_health = $HealthBar/RedHealth #Добавляем переменную игрока, позже понадобится export onready var player #Характеристики врага export var health = 5 export var speed = 5 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(): _animated_sprite.playing = true #Включили анимацию #Функция получения урона func hit(damage): health -= damage _red_health.rect_size.x -= health_size * damage if (health <= 0): #Если <= 0, то удалился 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"): collision.collider.take_damage(damage)
Настройка столкновений объектов.
Для начала перейдём в настройки проект -> Основные -> Имена слоя -> 2D Физика и зададим для первых четырёх слоёв имена наших объектов

Это потребуется для правильной настройки столкновений, на примере рассмотрим сцену персонажа: Персонаж, не должен сталкиваться с оружием, и пулей, но должен сталкиваться с врагами. Переходим н�� сцену DefaultCharacter и в CollisionObject2D выставляем Layer 1, а в Mask 4. Для удобства можно нажать многоточие, там выпадет список с заданными именами.




С болванками для игровых объектов закончили, теперь немного порисуем и добавим графику.
Как-же теперь сделать из DefaultСцены, уже нашу сцену с игроком?
Создаём новую сцену и как основной элемент выбираем дочернюю сцену. Дальше просто меняем кадры в фреймах AnimatedSprite2D (перед этим делаем его уникальным, чтобы болванка не изменилась).

Все объекты готовы, давайте соберём тестовую сцену, на которой можно пострелять зомбей.

Дерево объектов:
⦁ Node2D
⦁ Сцена нашего персонажа( как подчинённые добавляем ему пушек)
⦁ Path2D(MobPath)
⦁ PathFollow2D(MobPathFollow)
⦁ Timer(MobSpawnTimer)
Давайте Настроим Path2D.
Нажимаем на него на верхней панели появятся новые кнопки. Сначала нажимаем «Использовать привязку к сетке»
(Shift+G), дальше выбираем «Добавить новую точку». На этой панели:

И нажимаем на 4 угла экрана на сцене

Затем кнопку «Сомкнуть кривую». На этой панели:

Всё, путь построен. Навешиваем скрипт на Node2D и переходим к его редактированию.
extends Node2D #Сцена Врага export (PackedScene) var zombie_enemy #Элементы дерева onready var _mob_spawn_timer = $MobSpawnTimer onready var player = $BrutalHero #Функция призыва Зомби 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.scale = Vector2(0.2,0.2)# у меня спрайты слишком большие для окна в 1024X748, поэтому уменьшаю размер врага z.player = player get_parent().add_child(z)# добавляем зомби z.speed = 2# присваеваем ему статы z.health = 5 func _ready(): randomize()# подключаем генератор случайных чисел _mob_spawn_timer.start()# запускаем таймер спавна # обработка сигнала от таймера func _on_MobSpawnTimer_timeout(): spawn_zombie() spawn_zombie() spawn_zombie() _mob_spawn_timer.start()
Я выставил настройки для таймера в 5 секунд, то есть каждые 5 секунд появляется 3 зомби, примерно такой результат:

Респект Годоту!

В следующей статье добавим разные механики для оружия, полноценную сцену игры и главное меню.
