Сегодня на Godot 4.1 создадим простой редактор персонажа, как в старых рпг, когда ты выбираешь внешний вид персонажа из уже нарисованных ассетов.

Вступление

Для начала давайте немного обсудим, как это будет работать. У нас есть игровой персонаж, внешний вид которого будет устанавливаться игроком, на данном примере рассмотрим, что будет устанавливаться: цвет кожи, глаза, улыбка. Выбранные игроком спрайты должны куда-то сохраниться и мы в последующем будем загружать их на персонажа. По хорошему нужно хранить это в файле сохранения игрока и при запуске этого сейва заливать в синглтон, который объявлен, как глобальная переменная. Мы же в данном туториале будем хранить только в синглтоне и никуда сохранять не будет.

Графика

Что из себя должна представлять графика.

Для начала нужно создать что-то вроде манекена, на котором мы будем рисовать. То есть это будет наш персонаж, у которого есть отметены под глаза и рот(в нашем случае).

Манекен
Манекен

Дальше на нём всё рисуем и сохраняем, как отдельный спрайт.

Что-то типо такого
Что-то типо такого

Размеры изображений у вас должны быть одинаковые в каждой группе элементов. Например для всех пар глаз, размер - 100X30, для всех улыбок - 100X40 и т.д.

Персонаж

С графикой закончили, теперь перейдём к сцене персонажа в Godot.У меня она выглядит следующим образом:

Да, конечно следует ещё добавить узел коллизии, возможно ещё некоторые узлы, но в данном туториале мы просто сделаем персонажа, которого создаёт игрок.

Как правильно расставить местоположение Sprite2D(у вас может быть AnimatedSprite2D, тогда этот способ не подойдёт, если захотите, то рассмотрим это в отдельной статье). Для этого мы опять используем нашего маникена и примеряем всё на нём. То есть выбираем, как текстуру для Body, маникена и выставляем глаза и улыбку.

Сцена редактора

У меня сцена имеет примерно следующий вид

Думаю за что отвечает каждый узел, объяснять смысла нет. Это просто надписи, кнопки и элементы декора. У ColorRect(Preview), как дочерние элементы используются просто 3 спрайта, а не сцена персонажа.

Давайте перейдём к коду:

extends Node2D
#константы с путями к ассетами
const EYE_ROOT = "res://Assets/Eye/eye"
const SKIN_ROOT = "res://Assets/Skin/Skin"
const SMILE_ROOT = "res://Assets/Smile/Smile"
#Массивы которые будут хранить наши ассеты
var eye_array = []
var skin_array = []
var smile_array = []
#номер эллемента в массиве
var eye_number = 0
var skin_number = 0
var smile_number = 0
#элементы дерева
@onready var _eye = $CustomMenu/Preview/Body/Eye
@onready var _body = $CustomMenu/Preview/Body
@onready var _smile = $CustomMenu/Preview/Body/Smile

#Вспомогательные функции для получения полных путей ассетов
func get_eye_path(index):
	return EYE_ROOT + str(index) + ".png"

func get_skin_path(index):
	return SKIN_ROOT + str(index) + ".png"
	
func get_smile_path(index):
	return SMILE_ROOT + str(index) + ".png"
	
#Заполним массив ассетами глаз
func get_eye_array():
	#Счётчик
	var i = 1
	while true:
		#Если такая картинка есть, то добавляем в массив
		if load(get_eye_path(i)) != null:
			eye_array.append(load(get_eye_path(i)))
		#Иначе заканчиваем while
		#У меня все картинки идут по порядку(Eye1,Eye2...)
		#Можно сделать чуть иначе, но так проще...
		else:
			break
		i+=1

#тоже самое, но для улыбок
func get_smile_array():
	var i = 1
	
	while true:

		if load(get_smile_path(i)) != null:
			smile_array.append(load(get_smile_path(i)))
		else:
			break
			
		i+=1
		
#тоже самое, но для кожи
func get_skin_array():
	var i = 1
	
	while true:

		if load(get_skin_path(i)) != null:
			skin_array.append(load(get_skin_path(i)))
		else:
			break
			
		i+=1
#наполнили все массивы
func _ready():
	get_eye_array()
	get_smile_array()
	get_skin_array()
	
#функция получения новых глаз на превью
func get_new_eye():
	#Сделали вращалки цикличными
	if eye_number == eye_array.size():
		eye_number = 0
	if eye_number == -1:
		eye_number = eye_array.size() - 1
	#Залили новую текстурку
	_eye.texture = eye_array[eye_number]

#тоже самое для кожи
func get_new_skin():
	if skin_number == skin_array.size():
		skin_number = 0
	if skin_number == -1:
		skin_number = skin_array.size() - 1
	_body.texture = skin_array[skin_number] 

#тоже самое для улыбок
func get_new_smile():
	if smile_number == smile_array.size():
		smile_number = 0
	if smile_number == -1:
		smile_number = smile_array.size() - 1
	_smile.texture = smile_array[smile_number]

#Обработка сигналов для кнопок Skin
func _on_skin_next_pressed():
	skin_number+=1
	get_new_skin()

func _on_skin_prew_pressed():
	skin_number-=1
	get_new_skin()

#Обработка сигналов для кнопок Eye
func _on_eye_next_pressed():
	eye_number+=1
	get_new_eye()

func _on_eye_prew_pressed():
	eye_number-=1
	get_new_eye()

#Обработка сигналов для кнопок Smile
func _on_smile_next_pressed():
	smile_number+=1
	get_new_smile()

func _on_smile_prew_pressed():
	smile_number-=1
	get_new_smile()

В функции _ready, мы заполняем массивы картинками, и дальше просто заливаем в соответствующий Sprite2D, соответствующую текстурку.

Теперь нам потребуется написать синглтон HeroView и поставить его на автозагрузку и сделать глобальной переменной.

Синглтон HeroView:

extends Node

#Объявляем переменные хранящие картинки(не должны быть пустыми, чтобы не было ошибок)
var skin = load("res://Assets/Skin/Skin1.png")
var eye = load("res://Assets/Eye/eye1.png")
var smile = load("res://Assets/Smile/Smile1.png")

#Обычные сеттеры и геттеры
func set_skin(new_skin):
	skin = new_skin

func set_eye(new_eye):
	eye = new_eye

func set_smile(new_smile):
	smile = new_smile
	
func get_skin():
	return skin
	
func get_eye():
	return eye

func get_smile():
	return smile

Он просто хранить картинки и содержит сеттеры и геттеры для картинок.

Как вы могли в скрипте для сцены редактора нет обработки нажатия на кнопку accept, а вот и она:

#Обработка сигнала для кнопки принять
func _on_accept_pressed():
	HeroView.set_skin(_body.texture)
	HeroView.set_eye(_eye.texture)
	HeroView.set_smile(_smile.texture)

Мы просто обновляем переменные в синглтоне HeroView.

Завершающий штрих

Теперь у нас есть где хранятся картинки, но что с ними делать дальше? Дальше мы просто добавляем нашему персонажу следующую функцию:

func get_new_look():
	_body.texture = HeroView.get_skin()
	_eye.texture = HeroView.get_eye()
	_smile.texture = HeroView.get_smile()

И вызываем срабатывание этой функции когда потребуется обновить его внешний вид.

Результат

Я ещё добавил, что герой появляется на сцене, при нажатии кнопки Accept, и теперь мы имеет следующее:

Демонстрация результат
Демонстрация результат

Ну вот мы и создали простенький редактор персонажа.

Не большое обращение

Статей давно не было, потому-что я просто не знаю о чём вам было-бы интересно почитать. Поэтому если хотите чтобы статьи выходили чаще, пишите в комментариях о чём написать статью.

Комментарии (4)


  1. ValeryIvanov
    21.07.2023 11:22
    +1

    Круто, что вы популяризируете Godot, но ваша реализация явно нуждается в улучшении.

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

    2. Всё это:

    	if eye_number == eye_array.size():
    		eye_number = 0
    	if eye_number == -1:
    		eye_number = eye_array.size() - 1
    

    можно переписать в одну строчку:

    eye_number  = eye_number % eye_array.size():
    

    Читабельность повышается, вероятность ошибки при копипасте снижается.
    3) Синглтон всегда здесь избыточен. У вас есть три простых компонента, которые делают одно и то же — лишь переключают значения типа int. Самое лучшее, что можно сделать, это вынести эту логику в отдельный скрипт, а кнопки и лейбл, вынести в отдельную сцену. В скрипте объявляется сигнал, который сигнализирует о том, что значение поменялось(нажали на кнопку). Тем кому нужно, подписываются на этот сигнал. Эти самые значения могут храниться в сцене, которая отвечает за настройку персонажа и при необходимости, пробрасывать эти данные вниз по иерархии.

    И ещё, пускай это уже не относится к качеству реализации, но вместо скинов с заранее заданными цветами, можно навешивать на спрайт tint, который позволит игроку выбрать любой цвет. Если необходимо, чтобы некоторые места на скине сохраняли свой цвет, то можно для скина нарисовать маску, которая будет это всё дело регулировать.


    1. dopusteam
      21.07.2023 11:22

      можно переписать в одну строчку:

      Но ведь в вашем коде может получиться отрицательное число (например, -1 % 10 = -1), а в изначальном варианте такого быть не может. Если там массив не пустой, конечно.


      1. ValeryIvanov
        21.07.2023 11:22
        +1

        Да, тут я ошибся и насоветовал фигни. Перепутал с питоном, где остаток от деления вычисляется по другому.


    1. IsaacBlog Автор
      21.07.2023 11:22

      Спасибо за комментарий. Учту ваши советы.