Основы создания 2D персонажа в Godot. Часть 2: компилирование шаблонов, немного о GDScript, движение и анимация героя

  • Tutorial
В предыдущей статье мы рассмотрели азы создания нового проекта в Godot. И с этими поверхностными знаниями можно разве что поглядеть demo-версии игр.

Во второй части на повестке дня у нас:
1) Экспорт готового проекта в бинарные файлы для выбранной архитектуры.
2) Новые анимации. Параметры персонажа.
3) Управление.
3) GDScript. Добро пожаловать в настоящий кодинг!
4) Импорт простейших Tilesets.
5) Бонус: разбор устройства простейших задников.

Ну и как обычно, много картинок!


Компиляция....?


В прошлой статье я упоминал что авторы не предусматривали бинарники для linux x86. На время написания статьи бинарные файлы всё ещё были не готовы. Но экспортировать готовые проекты, demo или просто тесты уже хотелось. Но что делать, если нет Шаблонов экспорта? Правильно! Скомпилировать их самому!

Переходим в директорию с исходными файлами и компилируем

cd ./godot

Средства отладки:

scons bin/godot target=release_debug tools=no

После успешной компиляции переименовываем свежеполученный файл в linux_x11_32_debug

Сам шаблон экспорта:

scons bin/godot_rel target=release tools=no

Переименовываем в linux_x11_32_release

Запаковываем в zip-архив.

find ./* -name "linux_x11_32_*" -exec zip ./linux_x11_32_templates.zip "{}" +

Скармливаем архив Godot. Вуаля, теперь можно экспортировать проект для ОС GNU/Linux любой разрядности.



Да даже под ОС и разрядности, отличные от Вашей. Бинарные файлы для Windows без проблем смог завести в wine, главное не забыть снять или поставить галочку 64bits, и выключить debugging, если не нужен.



Подробнее про настройки экспорта можно почитать на сайте проекта на английском языке. Если будет актуально и востребовано, можно будет подумать о русской локализации статей и создания русскоязычной wiki по Godot.

Анимация бега и прыжков


В прошлом уроке я заранее подготовил текстурку со спрайтами бега и прыжка нашего Капитана. Поэтому я думаю создать две анимации jumping и run не составит труда. Я думаю это не трудно будет сделать. Скажу лишь параметры:
run:
Len(s): 0.9
Step(s): 0.1
Looping: yes

jumping:
Len(s): 1
Step(s): 0.1
Animation: ○х○○○○х○х○х , где кружки - последовательность кадров от 24 до 30, а крестики - их отсутствие.
Looping: no



Да, прыжок — это только прыжок. Значит нужна ещё анимация падения.
falling:
Len(s): 0.1
Step(s): 0.1
Animation: кадр №30
Looping: yes

Параметры персонажа


Три пункта:

  • Камера
  • Касание с землей
  • Геометрия персонажа


1) Напоминаю. Проверьте, стоит ли галочка на параметре Camera2D/Current, отвечающая за привязку объекта «камера» к объекту player.

2) Добавим Node CollisionShape2D. Эта «Нода» отвечает за положение нашего Капитана в 2D-пространстве.

В Inspector выбираем CollisionShape2D -> Shape -> New RayShape2D. В 2-х миллиметрах правее появится небольшая стрелочка — нажимаем на неё — откроются параметры RayShape2D.
Параметр RayShape2D/Lenght оставим 20, а вот Custom Solver Bias выставим 0.5 (этим мы разрешим небольшое смещение)
Теперь в самой игре в будущем, если персонаж будет «не попадать ногами по полу», его положение можно будет отрегулировать этой стрелочкой. Именно она проверяет «касается ли спрайт пола».

3) С геометрией веселее. Для начала надо добавить Node CollisionPolygon2D. Теперь нам надо нарисовать этот самый полигон, дающий персонажу «массу».То-есть надо нарисовать то, чем герой будет «биться об стенки» при столкновении с ними. Иначе он будет просто проходить «сквозь стены».

Выбираем карандаш. Теперь левой клавишей мыши «ставим» 3 вершины треугольника. Два на каждом из плечей, и один в районе живота. И правой клавишей (просто щёлкнуть на рабочей области) рисуем треугольник. Желательно чтобы он в итоге получился равнобедренный или равносторонний. С нижней вершиной на оси Y. Теперь увеличим его. Немного. Иначе персонаж будет проходить сквозь потолок например.


Готово, наш персонаж — болванчик готов. Теперь самое лёгкое — управление.

Создание клавиш управления


  1. Стандартное управление.

    Создать такое довольно легко. Идём в Scene -> Project Settings -> Input Map

    Создаём 3 клавиши управления — move_left, move_right и jump. Назначаем им клавиши. Готово!
    Как видно, Godot умеет работать и с геймпадами, правда для каждого надо будет настраивать своё управление. Но об этом как-нибудь в другой раз.
  2. Управление с тачскрина.
    Тут тоже особо ничего сложного нет. Привязываем к нашему герою Node CanvasLayer с названием «ui». А к ней в свою очередь — 3 копии TouchScreenButton. Называем их left, right и jump.
    К каждой привязываем изображение клавиши. Они сразу появятся в рабочей области программы. Расставляем так, как нам кажется будет удобнее. Не забываем что синее окошко — это «область показа камеры».
    В разделы Action вписываем параметры move_left, move_right и jump соответственно. Ну и выставляем параметр Visibility Mode в TouchScreen Only. Готово!


GDScript


Тут я немного в ступоре. Дело в том что из меня программист честно сказать не очень. Я сам многое не понимаю. И читаю книжки по Python чтобы лучше разбираться в вопросе. Потому что GDScript очень похож на него.
Внимание!
  • Движок не понимает кириллицу. Но если случайно на ней что-то написать, он её просто не отображает. А просто помечает всю строчку с ней, как одну большую ошибку.
  • Отступы очень важны. Не там отступ — и герой может провалиться в пол, могут выполняться не те функции.
  • Ещё раз повторяю, в программировании я нуб, но постараюсь объяснить всё как можно проще.


Небольшая просьба — если знаете как оформить лучше — напишите пожалуйста. Буду рад учесть все замечания.

Напомню предыдущий код отображения простейшей анимации:
Что было
extends RigidBody2D

var anim=""

func _integrate_forces(s):

    var new_anim=anim
    new_anim=«idle»

    if (new_anim!=anim):
        anim=new_anim
        get_node(«anim»).play(anim)

Добавляем и изменяем код:
Объявляем переменные
extends RigidBody2D

var anim=""    #переменная анимации
var siding_left=false    #переменная перемещения влево
var jumping=false    #переменная прыжка
var stopping_jump=false    #переменная завершения прыжка

var WALK_ACCEL = 800.0    #скорость перемещения по горизонтали
var WALK_DEACCEL= 800.0    #скорость торможения 
var WALK_MAX_VELOCITY= 800.0    #максимальная скорость
var GRAVITY = 900.0    #гравитация
var AIR_ACCEL = 200.0    #скорость перемещения по горизонтали в прыжке
var AIR_DEACCEL= 200.0    #торможение прыжка (перемещения в обратном направлении) в прыжке. Значение, близкое к нулю будет делать игру похожую на Mario Bros'
var JUMP_VELOCITY=460    #скорость прыжка
var STOP_JUMP_FORCE=900.0    #сила торможения прыжка

var MAX_FLOOR_AIRBORNE_TIME = 0.15    #Время касания, после которого уже прыгнуть нельзя. То-есть время, в течении которого можно сделать второй прыжок. Не меняйте, если не хотите "двойных" или бесконечных прыжков.

var airborne_time=1e20  #время в воздухе

var floor_h_velocity=0.0 

Начинаем функцию integrate_forces. Вот что пронеё сказано в Help:
Измените функцию, чтобы использовать свою физику взаимодействий. Ну хорошо, этим и займёмся.
PS не знаю как комментировать код дальше. Поэтому просто оставлю его так:
Пишем функцию взаимодействия
func _integrate_forces(s):

	var lv = s.get_linear_velocity()
	var step = s.get_step()
	
	var new_anim=anim
	var new_siding_left=siding_left
Получаем управление
	var move_left = Input.is_action_pressed("move_left")
	var move_right = Input.is_action_pressed("move_right")
	var jump = Input.is_action_pressed("jump")
	
#Замедление по оси x (торможение)
	lv.x-=floor_h_velocity
	floor_h_velocity=0.0
Поиск земли (проверяем контакт текстуры с полом)
	var found_floor=false
	var floor_index=-1
	
	for x in range(s.get_contact_count()):

		var ci = s.get_contact_local_normal(x)
		if (ci.dot(Vector2(0,-1))>0.6):
			found_floor=true
			floor_index=x

	if (found_floor):
		airborne_time=0.0 
	else:
		airborne_time+=step #время, проведенное в воздухе
		
	var on_floor = airborne_time < MAX_FLOOR_AIRBORNE_TIME
Процесс прыжка
	if (jumping):
		if (lv.y>0):
#Завершаем прыжок если он закончен (достигли наивысшей точки прыжка)
			jumping=false
		elif (not jump):
			stopping_jump=true
			
		if (stopping_jump):
			lv.y+=STOP_JUMP_FORCE*step
Движение персонажа на земле
	if (on_floor):

		if (move_left and not move_right):
			if (lv.x > -WALK_MAX_VELOCITY):
				lv.x-=WALK_ACCEL*step			
		elif (move_right and not move_left):
			if (lv.x < WALK_MAX_VELOCITY):
				lv.x+=WALK_ACCEL*step
		else:
			var xv = abs(lv.x)
			xv-=WALK_DEACCEL*step
			if (xv<0):
				xv=0
			lv.x=sign(lv.x)*xv
			
		#Проверка прыжка
		if (not jumping and jump):
			lv.y=-JUMP_VELOCITY
			jumping=true
			stopping_jump=false
			
		#Проверка перемещения и смена анимации
		
		if (lv.x < 0 and move_left):
			new_siding_left=true
		elif (lv.x > 0 and move_right):
			new_siding_left=false
		if (jumping):
			new_anim="jumping"	
		elif (abs(lv.x)<0.1):
			new_anim="idle"
		else:
			new_anim="run"
Движение персонажа в воздухе
	else:

		if (move_left and not move_right):
			if (lv.x > -WALK_MAX_VELOCITY):
				lv.x-=AIR_ACCEL*step			
		elif (move_right and not move_left):
			if (lv.x < WALK_MAX_VELOCITY):
				lv.x+=AIR_ACCEL*step
		else:
			var xv = abs(lv.x)
			xv-=AIR_DEACCEL*step
			if (xv<0):
				xv=0
			lv.x=sign(lv.x)*xv
			
		if (lv.y<0):
			new_anim="jumping"
		else:
			new_anim="falling"

Перемещение персонажа
	if (new_siding_left!=siding_left):
		if (new_siding_left):
			get_node("sprite").set_scale( Vector2(-1,1) )
		else:
			get_node("sprite").set_scale( Vector2(1,1) )
			
		siding_left=new_siding_left

Смена анимации
	if (new_anim!=anim):
		anim=new_anim
		get_node("anim").play(anim)

Применение скорости перемещения на земле
	if (found_floor):
		floor_h_velocity=s.get_contact_collider_velocity_at_pos(floor_index).x
		lv.x+=floor_h_velocity

Применение гравитации на это всё безобразие
	lv+=s.get_total_gravity()*step
	s.set_linear_velocity(lv)


Блин, это был ужас.
Теперь всё вместе без комментариев:
extends RigidBody2D

var anim=""
var siding_left=false
var jumping=false
var stopping_jump=false

var WALK_ACCEL = 300.0
var WALK_DEACCEL= 300.0
var WALK_MAX_VELOCITY= 400.0
var GRAVITY = 900.0
var AIR_ACCEL = 300.0
var AIR_DEACCEL= 300.0
var JUMP_VELOCITY=460
var STOP_JUMP_FORCE=200.0

var MAX_FLOOR_AIRBORNE_TIME = 0.15
var airborne_time=1e20
var floor_h_velocity=0.0

func _integrate_forces(s):

	var lv = s.get_linear_velocity()
	var step = s.get_step()
	
	var new_anim=anim
	var new_siding_left=siding_left
	
	var move_left = Input.is_action_pressed("move_left")
	var move_right = Input.is_action_pressed("move_right")
	var jump = Input.is_action_pressed("jump")
	
	lv.x-=floor_h_velocity
	floor_h_velocity=0.0
	
	
	var found_floor=false
	var floor_index=-1
	
	for x in range(s.get_contact_count()):

		var ci = s.get_contact_local_normal(x)
		if (ci.dot(Vector2(0,-1))>0.6):
			found_floor=true
			floor_index=x

	if (found_floor):
		airborne_time=0.0 
	else:
		airborne_time+=step
		
	var on_floor = airborne_time < MAX_FLOOR_AIRBORNE_TIME

	if (jumping):
		if (lv.y>0):
			jumping=false
		elif (not jump):
			stopping_jump=true
			
		if (stopping_jump):
			lv.y+=STOP_JUMP_FORCE*step

	if (on_floor):


		if (move_left and not move_right):
			if (lv.x > -WALK_MAX_VELOCITY):
				lv.x-=WALK_ACCEL*step			
		elif (move_right and not move_left):
			if (lv.x < WALK_MAX_VELOCITY):
				lv.x+=WALK_ACCEL*step
		else:
			var xv = abs(lv.x)
			xv-=WALK_DEACCEL*step
			if (xv<0):
				xv=0
			lv.x=sign(lv.x)*xv
			
		if (not jumping and jump):
			lv.y=-JUMP_VELOCITY
			jumping=true
			stopping_jump=false
			
		
		if (lv.x < 0 and move_left):
			new_siding_left=true
		elif (lv.x > 0 and move_right):
			new_siding_left=false
		if (jumping):
			new_anim="jumping"	
		elif (abs(lv.x)<0.1):
			new_anim="idle"
		else:
			new_anim="run"

	else:

		if (move_left and not move_right):
			if (lv.x > -WALK_MAX_VELOCITY):
				lv.x-=AIR_ACCEL*step			
		elif (move_right and not move_left):
			if (lv.x < WALK_MAX_VELOCITY):
				lv.x+=AIR_ACCEL*step
		else:
			var xv = abs(lv.x)
			xv-=AIR_DEACCEL*step
			if (xv<0):
				xv=0
			lv.x=sign(lv.x)*xv
			
		if (lv.y<0):
			new_anim="jumping"
		else:
			new_anim="falling"

	if (new_siding_left!=siding_left):
		if (new_siding_left):
			get_node("sprite").set_scale( Vector2(-1,1) )
		else:
			get_node("sprite").set_scale( Vector2(1,1) )
			
		siding_left=new_siding_left

	if (new_anim!=anim):
		anim=new_anim
		get_node("anim").play(anim)

	if (found_floor):
		floor_h_velocity=s.get_contact_collider_velocity_at_pos(floor_index).x
		lv.x+=floor_h_velocity

	lv+=s.get_total_gravity()*step
	s.set_linear_velocity(lv)


PS чтобы заработала гравитация, надо поставить галочку в Scene -> Project Settings -> Physics2D -> Default Gravity

Импорт TileSets

Рассказать подробно про tileset я в этот раз не успеваю. Могу лишь предложить свою готовую сцену (из моего проекта) или Вам создать свою из TileSet. Скачать сет можно по ссылке ниже, после чего добавить в проект Node TileMap. В настройках к ней надо добавить готовый TileSet ( TileSet -> Load -> tileset.xml ). Простейший пол/землю нарисовать сможете. Как рисовать уровни, TileSets я подробно расскажу в следующем уроке.

Бонус! Добавление задников!

Создаём новую сцену. Добавляем в неё «Node» ParallaxBackground. Называем её parallax_bg и сохраняем как parallax_bg.xml.
К ней создаём ParallaxLayer 4 штуки. Называем их sky, clouds, mount_1, mount_2. Соответственно небо, облака, и горы.
Разрешение нашей игры 800x600 (Посмотрите в настройках Scene -> Project Settings -> Display). Поэтому ставим каждой «Ноде» параметр Mirroring 800,0 — зеркалирование по оси Y. Поменяйте параметры Scale у «Нод» clouds, mount_1, mount_2 на 0.1,1; 0.2,1 и 0.4,1 соответственно.



К Node sky добавляем спрайт самого неба. Свой я уже нарисовал заранее. А вы можете скачать всё по ссылкам ниже.
На рабочей области появилось сразу 2 полоски. Переносим на рабочую область изображения, масштабируем. Видим что изменяя главный спрайт меняется и его зеркальное отражение.

К Node clouds добавляем облачка. Думаю можно будет продублировать каждый из 3-х чтобы в итоге было 6, mirror дублирует ещё раз, итого у нас будет аж 12 облаков!

Теперь к Node mount_1 и mount_2 добавим горы. Но слой mount_2 поднимем выше mount_1.


Сохраняем сцену. Открываем stage.xml. И к нашему Stage добавляем через «плюсик» сцену с задниками.

И как обычно небольшое видео геймплея:



Небольшой FAQ (Будет пополняться):
  • Если персонаж не двигается — проверьте добавили и правильно ли добавили клавиши управления
  • Проблемы с анимацией — правильно ли добавили анимацию. Проверьте looping анимаций idle, run и falling.
  • Персонаж проваливается в пол/проходит сквозь стены, потолки — увеличьте треугольник CollisionPolygon2D.
  • Персонаж частично в текстурах/выше них — поработайте со стрелкой CollisionShape2D

Скачать:

Спасибо, если осилили и дочитали до конца. В случае недочётов пишите в личку. Спасибо, увидимся!
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 18

    +1
    С клавишами пока не исправленный глюк. Иногда нужно всё удалить и пересоздать. Багрепорт писал на почту пока еще не было открытого гита, но до сих пор повторяется.
      +1
      Великолепно!
      Огромнейшее спасибо за статью!
      Есть еще куча вопросов, но я боюсь показаться очень глупым, если их задам, поэтому попозже и через лс.

      А до тех пор только один вопрос.
      Если задуматься — то возможно ли сделать на этом что-то файтингообразное в перспективе? Имеются ли у GoDot какие-то возможности, связанные с взаимодействиями двух динамических полигонов на экране друг с другом?)
        0
        Сделать возможно, но нужно самому высчитывать удары с помощью скриптов.
          0
          зачем?

          Если (проверка пересечения хитбоксов а и б), то объект_а -%ХП.

          разве нет?
            0
            Я бы в простейшем случае (представляю МК1) просто проверял бы координаты объектов и в зависимости от координат, того какой удар произведён, нажат ли блок рассчитывал бы потерю ХП. Как тут без скриптов — даже и не знаю, проверку пересечения чего-либо и потеря ХП — тоже без скрипта не сделаешь. Хотя может я не правильно понимаю вопрос.
              0
              я к тому, что вот к примеру на as банальная проверка пересечения хитбоксов писалась в 3-4 строчки. Имхо, куда проще, чем пересчет координат. Просто хотелось узнать, как подобная функция реализована здесь)
                0
                ну пересечение хитбоксов — это тоже банальная проверка координат, поняв как два объекта расположены друг относительно друга можно довольно просто (те же пару строк) вычислять удары ногами, руками, с прыжка и подката.
                  0
                  *очень и очень сильно тупит*
                  Можно для особо одаренных под ночь на примере что-нибудь, если вас не затруднит?) К примеру хай-панч)
                    +1
                    алгоритмом:

                    1) получаем координаты бойцов (xi, yi)
                    2) получаем текущие действия
                    3) если действие одного из бойцов хай панч (пусть для определённости бьёт первый) и расстояние abs(x1-x2) <= HIGH_PUNCH_DIST (они достаточно близко по горизонтали чтобы удар попал) а также y1<=y2 (второй не присел и первый не подпрыгнул а значит не промазал) то
                    а) если действие второго блок — реагируем на блок
                    b) если действие второго не блок — считаем удар

                    аналогично для остальных ударов. регулируем константы NAME_PUNCH_DIST чтобы это менее походило на бесконтактное карате и удары руками-ногами как-то отличались.
                      0
                      Как все оказывается просто. Спасибо)

                      Я почему-то зациклился на костях и куклах и мой скудный разум на тот момент подумал, что имеется в виду координата крайнего пикселя в руке на момент удара.

                        0
                        Была когда-то славная игра регдол файтинг, там действительно так просто не пройдёт и нужны колижн детекторы в разных частях тела, так что не надо на себя наговаривать.
                          0
                          Пожалуй, вы правы.)
                          Интересно, а на дроид\айось есть какой-нибудь аналог того самого регдола? По-моему была бы отличная убивалка времени и генератор фана!
                            0
                            для дроида точно есть, а для иос не знаю, не фанат яблок.
          +1
          Конечно можно. В следующей статье я буду разбирать взаимодействие игровых персонажей между собой. Разберем привязку клавиш ударов ближнего и дальнего боя. Ну и как и обещал — будет подробный рассказ про TileSets. Будем рисовать сами. Точнее рисовать буду я. Но как обычно всё будет доступно в конце урока для скачивания. Так что ненадолго беру небольшой перерыв, и сажусь за написание следующей части.
            0
            Очень и очень ждем всей командой!)
          0
          для прыжка можно пользоваться кубической интерполяцией вместо квадратной, чтобы не ставить точки вручную.

          анимация на 1 кадр с петлёй — это как то странно, можно просто пользоваться методом set_frame класса Sprite.
            0
            Спасибо за отличные статьи. Нет ли планов о продолжении?
              0
              Внезапно зашёл на habr, а мне пишут :)
              Спасибо за комментарий, они действительно важны для меня. Без иронии.
              Планы есть. Их много. Но я жду, когда допилят Godot 3 до нормального состояния.
              Ну и опять же — нужно время на это всё.
              А его в данный момент :(

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

            Самое читаемое