Привет, 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)


  1. cry_san
    04.07.2023 07:25
    +2

    Чем не устроили новые версии Godot?

    Ваш код при переходе на них уже не будет работать...


  1. IsaacBlog Автор
    04.07.2023 07:25

    Я начинал своë изучение с 3 версии, и считаю, что лучше всем кто начинает изучать Godot использовать эту версию, посколько по ней больше документации. Это статья идет, как обучающая для людей которые практически ничего не знают о Godot.


    1. skymal4ik
      04.07.2023 07:25
      +4

      Совершил такую же ошибку, когда начинал изучать питон. Документации по 2 версии было больше, его и выбрал. Про дальнейшее понятно - позже срочно пришлось переучиваться.


      1. IsaacBlog Автор
        04.07.2023 07:25
        +1

        Спасибо за ваш совет. Я пишу эти туториалы, для людей которые совсем не знакомы с Godot и разработкой игр в целом, и если они захотят следовать данным руководствам и в дальнейшем лично модифицировать проект, то им это будет сделать проще на 3 версии, по которой много официальной документации и примеров работ.


    1. rimashi
      04.07.2023 07:25
      +1

      Сейчас есть официальные доки как в самом движке, так и на английском языке. Если это проблема..то это действительно огромная проблема для вас в будущем при освоении нового. Когда выходила бетка 4 версии, я старался сразу разбираться, да проблематично и на тот момент вообще не было доков, а только 2-3 видео с изменениями и то на английском и все же. В общем на текущий момент, статья не имеет огромного смысла, а лишь является вводной для понимания основ движка, но не более


      1. IsaacBlog Автор
        04.07.2023 07:25
        +1

        Только-что ответил комментатору под предыдущим постом, Я пишу эти туториалы, для людей которые совсем не знакомы с Godot и разработкой игр в целом, и если они захотят следовать данным руководствам и в дальнейшем лично модифицировать проект, то им это будет сделать проще на 3 версии, по которой много официальной документации и примеров работ. Скорее всего, статьи по следующим проектам уже не будут насколько подробны, что в них описан каждый клик мыши и будут уже на 4-й версии