[Разработка игр, Unreal Engine] Как и зачем мы добавили новый тип треков для Sequencer UE4

Автор Сообщение
news_bot ®

Стаж: 6 лет 3 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
11-Мар-2021 17:31


В ходе разработки нашей игры мы столкнулись с необходимостью добавить возможность показывать элементы игрового интерфейса (виджеты) во время проигрывания катсцен. При этом требовалось обеспечить: 
  • возможность настройки содержимого виджетов;
  • время их демонстрации; 
  • сделать все это простым в использовании для геймдизайнеров.
В нашем проекте практически все катсцены созданы с использованием одного из инструментов 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;
};
В итоге мы сможем увидеть субтитры при проигрывании катсцены.
На этом наша цель достигнута!
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_igr (Разработка игр), #_unreal_engine, #_social_quantum, #_unreal_engine_4, #_razrabotka_igr (разработка игр), #_sequencer, #_blog_kompanii_social_quantum (
Блог компании Social Quantum
)
, #_razrabotka_igr (
Разработка игр
)
, #_unreal_engine
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 17-Май 10:57
Часовой пояс: UTC + 5