Создаём свою первую игру на Godot 3.5

от автора

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

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

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

План определили, приступим к реализации. Для начала создадим 4 сцены которые в дальнейшем будем наследовать для создания разных (персонажей, врагов, оружия).

Начнём с создания проекта:

  • Выбираем GLE2

  • В папке проекта создаём 2 папки для хранения графики и сцен.

  • В настройках проекта в список действий добавляем управление нашим персонажем и ЛКМ для стрельбы( стрелять и наводится будем сами, так веселее)

Проект подготовили, приступаем к сценам.

Для всех DefaultСцен вместо графики я использую иконку Годота, вы можете сделать так-же, либо сразу использовать свою графику.

Начнём со сцены персонажа:

Наше яйцо должно уметь ходить и получать урон, также добавим что у каждого персонажа будет брутальная секретная анимация, которая срабатывает после долгого простоя на месте.

Главным узлом сцены выбираем 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 зомби, примерно такой результат:

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/745716/