Привет, 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 зомби, примерно такой результат:
Респект Годоту!
В следующей статье добавим разные механики для оружия, полноценную сцену игры и главное меню.
Комментарии (6)
IsaacBlog Автор
04.07.2023 07:25Я начинал своë изучение с 3 версии, и считаю, что лучше всем кто начинает изучать Godot использовать эту версию, посколько по ней больше документации. Это статья идет, как обучающая для людей которые практически ничего не знают о Godot.
skymal4ik
04.07.2023 07:25+4Совершил такую же ошибку, когда начинал изучать питон. Документации по 2 версии было больше, его и выбрал. Про дальнейшее понятно - позже срочно пришлось переучиваться.
IsaacBlog Автор
04.07.2023 07:25+1Спасибо за ваш совет. Я пишу эти туториалы, для людей которые совсем не знакомы с Godot и разработкой игр в целом, и если они захотят следовать данным руководствам и в дальнейшем лично модифицировать проект, то им это будет сделать проще на 3 версии, по которой много официальной документации и примеров работ.
rimashi
04.07.2023 07:25+1Сейчас есть официальные доки как в самом движке, так и на английском языке. Если это проблема..то это действительно огромная проблема для вас в будущем при освоении нового. Когда выходила бетка 4 версии, я старался сразу разбираться, да проблематично и на тот момент вообще не было доков, а только 2-3 видео с изменениями и то на английском и все же. В общем на текущий момент, статья не имеет огромного смысла, а лишь является вводной для понимания основ движка, но не более
IsaacBlog Автор
04.07.2023 07:25+1Только-что ответил комментатору под предыдущим постом, Я пишу эти туториалы, для людей которые совсем не знакомы с Godot и разработкой игр в целом, и если они захотят следовать данным руководствам и в дальнейшем лично модифицировать проект, то им это будет сделать проще на 3 версии, по которой много официальной документации и примеров работ. Скорее всего, статьи по следующим проектам уже не будут насколько подробны, что в них описан каждый клик мыши и будут уже на 4-й версии
cry_san
Чем не устроили новые версии Godot?
Ваш код при переходе на них уже не будет работать...