В ходе разработки нашей игры мы столкнулись с необходимостью добавить возможность показывать элементы игрового интерфейса (виджеты) во время проигрывания катсцен. При этом требовалось обеспечить:
возможность настройки содержимого виджетов;
время их демонстрации;
сделать все это простым в использовании для геймдизайнеров.
В нашем проекте практически все катсцены созданы с использованием одного из инструментов UE4 — Sequencer.
Основной задачей Sequencer является предоставление удобного функционала для создания различных кинематографичных вставок на игровом движке. Наиболее популярным его применением является создание заскриптованных игровых событий — катсцен, пролетов камер и любых других событий, где мы хотим показать игроку заранее срежиссированное событие.
Кроме того, Sequencer позволяет не только показывать подготовленную последовательность прямо в игре, но и отрендерить предоставленную последовательность в видео-файл, что сделало его популярным инструментом для создания трейлеров и подготовки промо материалов.
В ходе изучения способов добавления новой логики, было принято решение расширить инструментарий Sequencer-а. Это позволяло продолжить создание катсцен с новыми событиями по единому пайплайну. Несмотря на то, что есть достаточно много подробной документации по работе с Sequencer-ом, способы его расширения и внутренние механизмы работы оставались покрыты тайной.
В этой статье я расскажу о том, как мы добавили нужную нам логику, в качестве новых типов треков в Sequencer, и настроили их поведение. Все модификации осуществляются на версии движка UE4 4.24.3, а сам способ расширения потребует знания C++.
Основные элементы Sequencer-а
Если вы следили за развитием движка, либо работали с его ранними версиями, то можете помнить предшественника Sequencer — Matinee. Со временем от Matinee отказались в пользу нового и более развитого Sequencer-а, но общее предназначение данной тулзы сохранилось. Можно также провести аналогию с одним из инструментов другого игрового движка Unity — Timeline.
В качестве одного из многочисленных примеров можно привести вступительную катсцену из одного демо проекта UE4:
А так, данная катсцена выглядит при настройке в редакторе:
Как видно, интерфейс для работы с Sequencer-ом во многом напоминает интерфейс видеоредакторов и в его использовании применяются аналогичные принципы.
Из основных элементов нас интересуют раздел со списком треков (1) и окно таймлайна с секциями треков (2), где мы выстраиваем изменение секций треков в зависимости от текущего кадра последовательности.
В нашем случае в роли треков выступают разнообразные игровые события: проигрывание анимаций, звуков, эффектов, манипуляции с игровыми объектами и по сути любые возможные изменения на игровой сцене.
Отмечу, что хранение всех файлов Sequencer-a в UE4 осуществляется, так же как и всех остальных файлов проекта UE4 — в виде ассетов:
И по сути работа с такими ассетами осуществляется точно так же, как и с любыми другим файлами проекта.
Sequencer уже содержит множество готовых треков с разнообразными параметрами для настройки:
Так, например, “Audio Track” позволяет добавить проигрывание звука в нужные нам моменты времени, а также изменять параметры этого звука:
Трек “Actor To Sequence” позволяет добавить ссылку на одного из акторов с игровой сцены и также изменять его параметры — трансформ, анимации и другие доступные параметры:
Со всем списком доступных треков и их настроек лучше всего ознакомиться в официальной документации по UE4, а мы перейдем к возможным способам добавить необходимый нам функционал по созданию и отображению виджетов во время проигрывания катсцен.
Одним из подходящих кандидатов является “Event Track”:
“Event Track” позволяет добавить скрипт в таймлайн и настроить его логику с помощью блюпринтов:
Добавляем трек:
Добавляем секцию трека в таймлайн:
Далее двойным кликом мыши по секции (“SequenceEvent_0”) можно перейти в редактор логики, где можно создать свое событие:
Такой способ позволяет покрыть собой все недостающие возможности готовых треков, но у такого подхода был ключевой для нас недостаток — это отсутствие простой возможности использовать один и тот же эвент с разными настройками в разных катсценах. Нам бы пришлось каждый раз копировать содержимое эвентов между катсценами, либо каждый раз заново писать одинаковую логику поведения трека. При этом возможность каким-то образом облегчить этот процесс с помощью правок в функционале Sequencer-a выглядела достаточно трудоемкой и неочевидной.
Тем не менее, “Event Track” хорошо подходит для создания дополнительных заскриптованных событий в катсценах.
Другим способом оказалась идея добавить добавить свой собственный тип треков в Sequencer, который сразу бы содержал нужную логику и предоставлял аналогичный всем текущим трекам интерфейс для своей настройки. О том, как этого можно достичь и пойдет речь дальше.
Создание нового трека
Функциональность Sequencer-а разнесена на несколько модулей:
Модуль Sequencer-a (Engine\Source\Editor\Sequencer\) который отвечает за всю логику работы в редакторе (это весь UI инструмента, а следовательно и вся логика по созданию новых треков).
Данный модуль доступен только из редактора и отсутствует в игре.
Модули MovieScene (Engine\Source\Runtime): MovieSceneTracks, MovieSceneTools, MovieSceneCapture, MovieSceneCaptureDialog. Данные модули отвечают за непосредственную логику работы разных элементов Sequencer-а.
Нас в основном будет интересовать модуль MovieSceneTracks, который реализует работу треков Sequencer-a. Если вам до этого не приходилось детально вникать в модульную архитектуру UE4, то поясню, что весь код движка разбит на множество модулей, каждый из которых имеет свое предназначение. Любой модуль, по сути, представляет собой отдельную dll-библиотеку.
Изучив исходники доступных треков, можно понять какие интерфейсы и классы отвечают за элементы Sequencer. Например, Audio Track состоит из следующих частей:
Внутренние связи между классами выглядят следующим образом:
Таким образом для создания нового типа трека нам потребуется совершить следующие шаги:
Создать наследника класса FMovieSceneTrackEditor.
Реализовать интерфейс ISequencerSection.
«Зарегистрировать» новый класс трека в модуле Sequencer.
Создать наследника класса UMovieSceneTrack.
Создать наследника класса UMovieSceneSection.
Создать наследника FMovieSceneEventTemplate.
Реализовать интерфейс IMovieSceneExecutionToken & IPersistentEvaluationData.
Из представленных классов часть относится к модулю редактора и реализует логику настройки треков и секций в редакторе — FMovieSceneTrackEditor, ISequencerSection. Класс-наследник FMovieSceneTrackEditor необходимо зарегистрировать в модуле Sequencer. Таким образом редактор узнает о новом типе треков.
Остальные классы отвечают за логику работы трека и в игре, и в редакторе — UMovieSceneTrack, UMovieSceneSection, FMovieSceneEventTemplate;
Перейдем к первому шагу создания нового трека — это создание класса, отвечающего за добавление нового трека в Sequencer — класс FMovieSceneTrackEditor.
Создадим наш класс. Я заранее приведу все необходимые методы, а дальше опишу их назначение и реализацию:
Код
class FMovieSceneSubtitlesTrackEditor : public FMovieSceneTrackEditor
{
public:
FMovieSceneSubtitlesTrackEditor(TSharedRef<ISequencer> InSequencer);
static TSharedRef<ISequencerTrackEditor> CreateTrackEditor(TSharedRef<ISequencer> OwningSequencer);
// ISequencerTrackEditor interface
virtual void BuildAddTrackMenu(FMenuBuilder& MenuBuilder) override;
virtual TSharedPtr<SWidget> BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params);
virtual const FSlateBrush* GetIconBrush() const override;
virtual TSharedRef<ISequencerSection> MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding) override;
virtual bool SupportsType(TSubclassOf<UMovieSceneTrack> Type) const override;
virtual bool SupportsSequence(UMovieSceneSequence* InSequence) const override;
private:
FReply AddNewTrack(UMovieSceneTrack* Track);
};
Условно содержимое класса можно разбить на две части:
Логика создания нового трека.
Логика создания новых секций трека.
Кроме класса реализующего «трек», нам потребуется сразу создать класс FMovieSceneSubtitlesEventSection, отвечающий за секцию данного трека. Сам класс секции трека является реализацией интерфейса ISequencerSection.
Код
class FMovieSceneSubtitlesEventSection : public ISequencerSection
{
public:
FMovieSceneSubtitlesEventSection(UMovieSceneSection& InSection, TWeakPtr<ISequencer> InSequencer);
virtual int32 OnPaintSection(FSequencerSectionPainter& InPainter) const override;
virtual UMovieSceneSection* GetSectionObject() override;
virtual FText GetSectionTitle() const override;
virtual float GetSectionHeight() const override;
private:
UMovieSceneSubtitleSection* Section;
};
Также сразу потребуется создать отдельный класс — UMovieSceneSubtitleSection. Новый класс будет хранить текст наших субтитров и использоваться секциями треков для запуска логики работы секции трека в игре:
Код
UCLASS()
class UE4MAGICHIGH_API UMovieSceneSubtitleSection : public UMovieSceneSection
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta = (MultiLine = true))
FText SubtitleText;
virtual FMovieSceneEvalTemplatePtr GenerateTemplate() const override;
};
В первую очередь нам нужно зарегистрировать новый класс в модуле Sequencer. Это делается достаточно просто:
Код
ISequencerModule& SequencerModule = FModuleManager::LoadModuleChecked<ISequencerModule>(TEXT("Sequencer"));
MovieSceneSubtitlesTrackEditorHandle = SequencerModule.RegisterTrackEditor(FOnCreateTrackEditor::CreateStatic(&FMovieSceneSubtitlesTrackEditor::CreateTrackEditor));
Регистрация по сути привязывает указатель на наш метод для создания объекта:
Код
TSharedRef<ISequencerTrackEditor> FMovieSceneSubtitlesTrackEditor::CreateTrackEditor(TSharedRef<ISequencer> InSequencer)
{
return MakeShareable(new FMovieSceneSubtitlesTrackEditor(InSequencer));
}
Кроме этого, переопределим несколько методов отвечающих за проверки на возможность добавления нового типа трека:
Код
bool FMovieSceneSubtitlesTrackEditor::SupportsType(TSubclassOf<UMovieSceneTrack> Type) const
{
return Type == UMovieSceneSubtitleTrack::StaticClass();
}
bool FMovieSceneSubtitlesTrackEditor::SupportsSequence(UMovieSceneSequence* InSequence) const
{
static UClass* LevelSequenceClass = FindObject<UClass>(ANY_PACKAGE, TEXT("LevelSequence"), true);
return InSequence != nullptr && LevelSequenceClass != nullptr && InSequence->GetClass()->IsChildOf(LevelSequenceClass);
}
Для корректной работы создания трека нам остается переопределить следующий метод:
Код
void FMovieSceneSubtitlesTrackEditor::BuildAddTrackMenu(FMenuBuilder& MenuBuilder)
{
// добавляем новую кнопку(опцию) в меню
MenuBuilder.AddMenuEntry(
// прописываем название опции
LOCTEXT("AddSubtitlesEventTrack", "Subtitles Track"),
LOCTEXT("AddSubtitlesTrackTooltip", "Adds a subtitles track."),
// создаем стиль кнопки
FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.TextRenderComponent"),
// добавляем лямбду, которая реализует логику нажатия на кнопку
FUIAction(FExecuteAction::CreateLambda([=] {
auto FocusedMovieScene = GetFocusedMovieScene();
if (FocusedMovieScene == nullptr)
{
return;
}
const FScopedTransaction Transaction(LOCTEXT("MovieSceneSubtitlesTrackEditor_Transaction", "Add Subtitle Track"));
FocusedMovieScene->Modify();
// непосредственно создаем и добавляем наш трек в Sequencer
auto NewTrack = FocusedMovieScene->AddMasterTrack<UMovieSceneSubtitleTrack>();
ensure(NewTrack);
NewTrack->SetDisplayName(FText::FromString("Subtitles"));
GetSequencer()->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded);
})));
В результате мы сможем увидеть и добавить новый трек и секцию трека.
Для создания кнопки по добавлению секции, которая появляется при выделении курсором мыши нашего нового трека, потребуется реализовать оставшиеся методы:
Код
TSharedPtr<SWidget> FMovieSceneSubtitlesTrackEditor::BuildOutlinerEditWidget(const FGuid& ObjectBinding, UMovieSceneTrack* Track, const FBuildEditWidgetParams& Params)
{
FSlateFontInfo SmallLayoutFont = FCoreStyle::GetDefaultFontStyle("Regular", 8);
// подготовим лямбду на отображение кнопки только при выделении ноды
auto OnGetVisibilityLambda = [this, Params]() -> EVisibility {
if (Params.NodeIsHovered.Get())
{
return EVisibility::SelfHitTestInvisible;
}
return EVisibility::Collapsed;
};
// текст кнопки
TSharedRef<STextBlock> ComboButtonText = SNew(STextBlock)
.Text(LOCTEXT("SubtitlesTextSequencer", "Subtitles")) .Font(SmallLayoutFont) .ColorAndOpacity(FSlateColor::UseForeground()) .Visibility_Lambda(OnGetVisibilityLambda);
// сама кнопка
TSharedRef<SButton> Button = SNew(SButton)
.ButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
.ForegroundColor(FSlateColor::UseForeground())
.IsEnabled_Lambda([=]()
{ return GetSequencer().IsValid() ? !GetSequencer()->IsReadOnly() : false;
.ContentPadding(FMargin(5, 2))
// добавим лямбду для создания нового трека при нажатии на кнопку
.OnClicked(this, &FMovieSceneSubtitlesTrackEditor::AddNewTrack, Track)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(FMargin(0, 0, 2, 0))
[SNew(SImage)
// стиль нашей кнопки
.ColorAndOpacity(FSlateColor::UseForeground())
.Image(FEditorStyle::GetBrush("Plus"))]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.AutoWidth()
[ComboButtonText]];
return Button;
}
// метод, вызываемый при нажатии на кнопку “добавить новую секцию”
FReply FMovieSceneSubtitlesTrackEditor::AddNewTrack(UMovieSceneTrack* Track)
{
UMovieScene* FocusedMovieScene = GetFocusedMovieScene();
Track->Modify();
// задаем начальные параметры секции
FFrameNumber KeyTime = GetSequencer()->GetGlobalTime().Time.FrameNumber;
auto SubtitleTrack = Cast<UMovieSceneSubtitleTrack>(Track);
TRange<FFrameNumber> SectionRange = FocusedMovieScene->GetPlaybackRange();
// добавляем секцию
SubtitleTrack->AddSection(KeyTime, SectionRange);
GetSequencer()->NotifyMovieSceneDataChanged(EMovieSceneDataChangeType::MovieSceneStructureItemAdded);
return FReply::Handled();
}
TSharedRef<ISequencerSection> FMovieSceneSubtitlesTrackEditor::MakeSectionInterface(UMovieSceneSection& SectionObject, UMovieSceneTrack& Track, FGuid ObjectBinding)
{
return MakeShareable(new FMovieSceneSubtitlesEventSection(SectionObject, GetSequencer()));
}
Таким образом мы сможем добавить новую секцию.
Возвращаясь к реализации класса FMovieSceneSubtitlesEventSection, который отвечает за отображение секций трека в Sequencer — в нашем случае ограничимся демонстрацией текста субтитров в поле секции.
Код
virtual FText GetSectionTitle() const override
{
return Section->SubtitleText;
}
Для корректного отображения размера секции будем использовать простой алгоритм подсчета новых строк в субтитрах.
Код
virtual float GetSectionHeight() const override
{
const FString StrSubtitles = Section->SubtitleText.ToString();
int NewLineSymbols = Algo::CountIf(StrSubtitles, [](const auto& symbol) {
return symbol == '\n';
});
NewLineSymbols++; // доп. место под первую строку
return SequencerSectionConstants::DefaultSectionHeight * NewLineSymbols;
}
Реализации методов в совокупности приводит к следующему виду секции трека:
Из остальных методов наибольший интерес представляет OnPaintSection.
Код
virtual int32 OnPaintSection(FSequencerSectionPainter& InPainter) const override
{
return InPainter.PaintSectionBackground();
}
В нашем случае мы его никак не используем, но именно этот метод позволяет «рисовать» различные данные поверх окна секции. Например, именно этот метод отображает форму звука в AudioTrack.
Пример реализации такой логики лучше всего изучить самостоятельно в AudioTrackEditor.cpp.
Остальные методы являются сугубо техническими.
Код
FMovieSceneSubtitlesEventSection(UMovieSceneSection& InSection, TWeakPtr<ISequencer> InSequencer)
{
Section = Cast<UMovieSceneSubtitleSection>(&InSection);
}
virtual UMovieSceneSection* GetSectionObject() override
{
return Section;
}
Нам остается реализовать оставшиеся классы, которые содержат непосредственную логику работы наших субтитров:
UMovieSceneSubtitleTrack;
UMovieSceneSubtitleSection;
FMovieSceneSubtitlesEventTemplate.
Класс UMovieSceneSubtitleSection является самым простым и содержит текст субтитров, которые мы хотим отображать. UPROPERTY этого класса будут доступны для настройки из самого Sequencer.
Код
UCLASS()
class UE4MAGICHIGH_API UMovieSceneSubtitleSection : public UMovieSceneSection
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta = (MultiLine = true))
FText SubtitleText;
virtual FMovieSceneEvalTemplatePtr GenerateTemplate() const override
{
return FMovieSceneSubtitlesEventTemplate(*this);
}
};
Класс UMovieSceneNameableTrack содержит логику по созданию объекта, который уже отвечает за:
создание виджета субтитров;
передачу параметров;
завершение работы виджета субтитров.
Код
void FMovieSceneSubtitlesEventTemplate::Evaluate(const FMovieSceneEvaluationOperand& Operand, const FMovieSceneContext& Context, const FPersistentEvaluationData& PersistentData, FMovieSceneExecutionTokens& ExecutionTokens) const
{
ExecutionTokens.Add(FSubtitlesSectionExecutionToken(Section));
}
void FMovieSceneSubtitlesEventTemplate::TearDown(FPersistentEvaluationData& PersistentData, IMovieScenePlayer& Player) const
{
FCachedSubtitlesTrackData& TrackData = PersistentData.GetOrAddTrackData<FCachedSubtitlesTrackData>();
TrackData.RemoveSubtitleWidget();
}
Сам класс, по сути, просто создает объект, реализующий интерфейс IMovieSceneExecutionToken. В IMovieSceneExecutionToken нас интересует метод Evaluate, который и будет создавать виджет на экране пользователя. Сам виджет будет просто отображать текст на экране пользователя.
Код
struct FSubtitlesSectionExecutionToken : IMovieSceneExecutionToken
{
FSubtitlesSectionExecutionToken(const UMovieSceneSubtitleSection* InSubtitleSection)
: SubtitleSection(InSubtitleSection), SectionKey(InSubtitleSection)
{
}
virtual void Execute(const FMovieSceneContext& Context, const FMovieSceneEvaluationOperand& Operand, FPersistentEvaluationData& PersistentData, IMovieScenePlayer& Player)
{
FCachedSubtitlesTrackData& TrackData = PersistentData.GetOrAddTrackData<FCachedSubtitlesTrackData>();
UObject* PlaybackContext = Player.GetPlaybackContext();
if (!PlaybackContext)
{
return;
}
UWorld* World = PlaybackContext->GetWorld();
if (!World)
{
return;
}
UMHSubtitlesWindow* SubtitlesWindow = Cast<UMHSubtitlesWindow>(UUserWidget::CreateWidgetInstance(*World, HUD->SubtitlesWidgetTemplate, FName(TEXT("Subtitles"))));
SubtitlesWindow->AddToViewport();
SubtitlesWindow->SetSubtitltesText(SubtitleSection->SubtitleText);
TrackData.AddSubtitleWidget(SubtitlesWindow);
}
}
const UMovieSceneSubtitleSection* SubtitleSection;
FObjectKey SectionKey;
};
Для хранения данных секции используется объект, реализующий интерфейс IPersistentEvaluationData.
Код
struct FCachedSubtitlesTrackData : IPersistentEvaluationData
{
void AddSubtitleWidget(UMHWindowWidget* Widget)
{
SubttitleWidget = Widget;
}
void RemoveSubtitleWidget()
{
if (SubttitleWidget &&
SubttitleWidget->IsValidLowLevel())
{
SubttitleWidget->RemoveFromViewport();
}
SubttitleWidget = nullptr;
}
UMHWindowWidget* GetSubtitleWidget() const
{
return SubttitleWidget;
}
private:
UMHWindowWidget* SubttitleWidget = nullptr;
};
В итоге мы сможем увидеть субтитры при проигрывании катсцены.
На этом наша цель достигнута!
ufna
Спасибо, что пишите о таких штуках!
Mernion Автор
Рад, что статья заинтересовала :)