Это урок — расширенная версия урока по анимированию перемещения объектов окружения в Scene Graph. Если вы хотите узнать больше об анимировании движения в UEFN без использования Scene Graph, ознакомьтесь с тем уроком, а затем вернитесь к этому!
Перемещающиеся платформы есть в большинстве играх-«платформерах». В таких играх игроки должны совершать точные прыжки между целевыми объектами, чтобы достичь финальной цели.
В UEFN есть несколько способов перемещения объектов окружения. Вы можете использовать такие функции, как TeleportTo[] или MoveTo(), чтобы напрямую изменить преобразование, или же другое устройство, например устройство перемещения объектов окружения, чтобы переместить объект окружения по заданной траектории. Однако для этого также можно использовать анимации.
Анимации имеют несколько преимуществ по сравнению с перемещением преобразования объекта окружения. Анимации обычно обеспечивают более плавное перемещение объектов по сравнению с функциями MoveTo() или TeleportTo(), поскольку позволяют избежать сетевой задержки, возникающей при вызове этих функций во время каждого игрового такта.
Также анимации обеспечивают более последовательные коллизии с игроками и другими объектами, и вы можете лучше контролировать, куда и как перемещаются объекты, по сравнению с использованием устройства перемещения объектов. Вы можете воспроизводить анимации в цикле или же проигрывать их вперёд и назад в стиле «пинг-понг».
Кроме того, анимации позволяют выбрать тип интерполяции. Тип интерполяции определяет тип замедления, или кривую анимации, которой будет следовать анимация. Например, при линейной интерполяции анимация воспроизводится с постоянной скоростью, тогда как при интерполяции с замедлением в начале анимация начинается медленно и постепенно ускоряется.
Выбрав тип интерполяции, подходящий вашей анимации, вы можете задать разные точки, в которых объект окружения должен замедляться, ускоряться или перемещаться линейно. Применив компонент для реализации такого поведения модулей на уровне, вы сможете создавать перемещающиеся платформы, по которым смогут передвигаться игроки.
Для начала подумайте, какие типы поведения должна поддерживать перемещающаяся платформа. Во время анимации платформа должна начать перемещаться с заданной начальной точки, а затем переместиться ещё в несколько точек. Когда она достигнет точки завершения своего перемещения, она должна либо вернуться в исходную точку, либо остаться на месте.
Объекты перемещаются в течение определённого времени и должны иметь возможность поворачиваться и изменять гамму соответствующим образом в каждой точке своего пути. Для каждого из этих вариантов поведения требуется специальный код, однако чтобы быстро реализовать самые разные идеи, лучше начать с простого класса и постепенно его развить. Выполните следующие действия, чтобы создать компонент движения на основе анимации.
Создайте новый компонент Verse с именем
animate_to_targets_componentи откройте его в Visual Studio Code. Подробнее о создании пользовательских компонентов Verse: Создание пользовательского компонента Verse. Добавьте инструкцииusingдля/Verse.org/SpatialMath,/Verse.org/SceneGraph/KeyframedMovement, и/UnrealEngine.com/Temporary/SpatialMath. Функции каждого из них вам понадобятся позже.Verseusing { /Verse.org } using { /Verse.org/Native } using { /Verse.org/SceneGraph } using { /Verse.org/Simulation } using { /Verse.org/SpatialMath } using { /Verse.org/SceneGraph/KeyframedMovement } using { /UnrealEngine.com/Temporary/SpatialMath } # Place this component to an entity to move between preset targets. animate_to_targets_component<public> := class<final_super>(component):Подсказки к инструментам, используемые в этом руководстве, приведены ниже. Вы можете скопировать и вставить их над определением класса
animate_to_targets_component.Verse# Editor Tool Tips DefaultSpeedTip<localizes><public>:message = "Default speed simulation entity moves during any segment that does not specify a speed." SpeedTip<localizes><public>:message = "Speed simulation entity moves during this segment." AnimationDurationTip<localizes><public>:message = "The duration of the animation segment in seconds." EasingFunctionTip<localizes><public>:message = "Movement cubic-bezier easing function during this segment." DefaultEasingFunctionTip<localizes><public>:message = "Default movement cubic-bezier easing function during any segment that does not specify an easing function." PlaybackModeTip<localizes><public>:message = "Animation playback mode\n\tOneshot: Animation plays once.\n\tPingPong: Animation plays in order and reverse order repeating.\n\tLoop: Animation repeats in a loop. Tip: To construct an animation in a closed loop, make the last segment starting position the same as the first segment starting position." TargetsTip<localizes><public>:message = "Entities that are targets for the parent entity." RandomizeTargetsTip<localizes><public>:message = "Randomize the order of the segments.\n\tNever: Always use the order specified.\n\tOnBegin: Only randomize the order on begin simulation.\n\tEveryIteration: Randomize the order of the segments every loop iteration." PauseTip<localizes><public>:message = "Duration simulation entity pauses at the beginning of this segment."Добавьте следующие поля в определение класса
animate_to_targets_component:Редактируемая переменная типа float
InitialPauseSeconds. Это время, через которое начнётся анимация модуля. Задайте для этого параметра значение10,0,чтобы модуль ожидал десять секунд, прежде чем его анимация начнётся.
Verse# Amount of time to pause before the animation starts. @editable_number(float): ToolTip := SpeedTip MinValue := option{0.0} var InitialPauseSeconds<public>:float = 10.0Редактируемый компонент
keyframed_movement_playback_modeAnimationPlaybackMode. Это режим анимации для модуля. Задайте для него значениеloop_keyframed_movement_playback_mode. Это означает, что по умолчанию по завершении анимации модуль повторит цикл, а анимация начнётся заново.
Verse# The playback mode used by this animation. @editable: ToolTip := PlaybackModeTip var PlaybackMode<public>:keyframed_movement_playback_mode = loop_keyframed_movement_playback_mode{}Сохраните свой код и скомпилируйте его.
Разделение анимации на сегменты
Для создания анимаций в коде будут использоваться опорные кадры. Анимация состоит из одного или нескольких опорных кадров, и каждый опорный кадр определяет параметры объекта в определённых точках анимации. Создав анимацию с использованием опорных кадров, можно указать несколько точек, к которым объект окружения будет перемещаться, вращаться и масштабироваться.
В качестве типа опорного кадра keyframed_movement_component использует keyframed_movement_delta. Эти дельты перемещения имеют три значения:
Transform: определяет изменения в преобразовании по отношению к предыдущему опорному кадру.Длительность: продолжительность опорного кадра в секундах.Easing: функция замедления, используемая при воспроизведении данного опорного кадра.
Поскольку keyframed_movement_component принимает массив опорных кадров и затем воспроизводит их, вам нужно указать сразу все опорные кадры для каждой воспроизводимой анимации. Это можно сделать двумя способами:
Вы можете создать массив из нескольких опорных кадров в коде, затем передать его компонент перемещения по опорным кадрам и воспроизвести одну анимацию.
Вы можете создать несколько массивов, содержащих один опорный кадр, и передавать их по отдельности в компонент перемещения по опорным кадрам для последовательного воспроизведения нескольких анимаций.
В каждом варианте есть свои плюсы и минусы. Массивы одиночных опорных кадров позволяют проще выполнять операции между опорными кадрами, но требуют больше кода для обработки. Одновременное создание всех опорных кадров облегчает управление, но усложняет выполнение операций во время воспроизведения анимации. И в этом уроке, и в уроке Анимация движения объектов описывается первый подход, но будет реализован и второй подход.
Для создания отдельных опорных кадров в коде нужно определить класс сегмента. Каждый сегмент будет представлять собой опорный кадр, используемый компонентом keyframed_movement_component, который можно создать в редакторе. Вы также можете включить дополнительные данные, например время ожидания между опорными кадрами. Чтобы создать класс сегмента, выполните следующие действия:
Добавьте новый класс
segmentв файлanimate_to_targets_component.verse. Добавьте спецификатор<concrete>, чтобы этот класс можно было использовать в качестве значения типа@editable.Verse# Defines a segment of animation, which includes a starting position, animation speed and duration, and easing function. # Each segment acts as a single animation, and multiple segments make up an animation sequence. segment<public> := class<concrete>:Добавьте следующие поля в определение класса сегмента:
Редактируемая настройка
модуляпод названиемSegmentStartPosition. Этот модуль будет ссылаться на позицию в мире, из которой он должен начинать воспроизведение анимации.
Verse# An entity that represents the starting position of this entity during the animation segement. @editable: ToolTip := SourceTip SegmentStartPosition:?entity = falseРедактируемая переменная
floatAnimationDuration. Это время, в течение которого будет воспроизводиться данный сегмент анимации. Задайте для него значение2,0, чтобы каждый сегмент анимации воспроизводился две секунды.
Verse# The duration of the animation segment. @editable: ToolTip := AnimationDurationTip AnimationDuration:float = 2.0Редактируемая настройка функции замедления
EasingFunction. Это функция замедления, используемая в данном сегменте анимации.
Verse# The easing function to use during this segment of animation. @editable: ToolTip := EasingFunctionTip EasingFunction:?easing_function = falseКаждая функция замедления определяется кубической кривой Безье, которая состоит из четырёх чисел, определяющих тип функции замедления для анимации. Например, параметры кривой перемещения с замедлением в начале замедляют анимацию в начале, а затем ускоряют её.
Параметры линейной кривой обеспечивают воспроизведение анимации с постоянной скоростью. Вы можете самостоятельно определять эти значения, чтобы создать собственные кривые анимации, но в данном примере это не нужно, поскольку вы будете использовать значения, определённые в модуле
KeyframedMovement.Редактируемый параметр типа float
PauseSeconds. Это время в секундах на паузу перед запуском данного сегмента анимации. Представьте, что это время, на которое модуль замирает перед тем, как двигаться дальше из каждой точки своего пути.
Verse# The number of seconds to pause before starting this animation segment. @editable: ToolTip := PauseTip PauseSeconds:?float = falseВ определении класса
animate_to_targets_componentдобавьте следующие поля:Редактируемый массив сегментов, который будет называться
Segments. Он будет ссылаться на каждый сегмент анимации, составляющий общую анимацию, через которую проходит ваш модуль.
Verse# Segments of the keyframed movement animation. @editable: ToolTip := SegmentsTip var Segments<private>:[]segment = array{}Редактируемая
функция замедленияс названиемDefaultEasingFunction. Если в сегменте анимации не указана функция замедления, она будет использоваться по умолчанию. Задайте для неё значениеease_in_out_cubic_bezier_easing_function.
Verse# Movement easing function between two targets @editable: ToolTip := DefaultEasingFunctionTip var DefaultEasingFunction<public>:easing_function = ease_in_out_cubic_bezier_easing_function{}Сохраните свой код и скомпилируйте его. В редакторе вы должны увидеть, что массив
Segmentsпоявляется на модулях с прикреплённым компонентом animate_to_targets_component.
Создание опорных кадров с помощью кода
После завершения работы над классом сегментов нужно создать опорные кадры, которые определяют ваши сегменты. Вы будете создавать опорные кадры по одному и добавлять их в массив, а затем передадите массив в ваш keyframed_movement_component. Для этого потребуются математические операции, которые мы сейчас определим.
Поскольку арифметические операции очень удобны в различных сценариях, рекомендуем поместить эту логику в служебный файл, чтобы можно было обратиться к ней из любого компонента Verse. Подробнее о практических рекомендациях Verse при работе с модулями: Создание пользовательского компонента Verse. Чтобы создать служебный файл, выполните следующие действия:
Создайте новый модуль в файле
animate_to_targets.verseс именемУтилиты. В нём будет храниться общая логика, которую вы будете использовать во всём проекте.Verse# Module containing utility functions. Utilities<public> := module:Добавьте новый псевдоним типа
vector3с именемVectorOnesв ваш модуль «Утилиты». Это вектор типаvector3, в котором для параметровВлево,ВверхиВперёдбудет задано значение1.0. Мы будем использовать этот вектор позже для упрощения вычислений — после определения псевдонима типа нам не придётся постоянно записыватьvector3{Left := 1.0, Up := 1.0, Forward := 1.0}. Поскольку вы импортировали/Verse.org/SpatialMathи/UnrealEngine.com/Temporary/SpatialMathмодулей, необходимо указать, что это/Verse.org/SpatialMathvector3, поскольку оба модуля включают его определение.Verse# Utility function for the identity of component-wise vector multiplication. VectorOnes<public>()<transacts>:(/Verse.org/SpatialMath:)vector3 = (/Verse.org/SpatialMath:)vector3{Left := 1.0, Up := 1.0, Forward := 1.0}Добавьте новую функцию
GetDeltaTransform()в модульUtilities. Эта функция будет рассчитывать и возвращать разницу между двумя преобразованиями. Добавьте к этой функции модификатор<transacts>, чтобы можно было выполнить откат. Укажите/Verse.org/SpatialMathв качестве модуля для каждогопреобразования, поскольку вам нужно будет рассчитывать разницу между преобразованиями модулей.Verse# Get the delta transform between two given transforms. GetDeltaTransform<public>(TransformOne:(/Verse.org/SpatialMath:)transform, TransformTwo:(/Verse.org/SpatialMath:)transform)<transacts>:(/Verse.org/SpatialMath:)transform=В
GetDeltaTransformинициализируйте/Verse.org/SpatialMath— новоепреобразование. Установите дляTranslationразницу между переносом каждого преобразования. Задайте для параметраRotationрезультат вызоваMakeComponentWiseDeltaRotation(). Поскольку эта функция находится в модуле/UnrealEngine.com/Temporary/SpatialMath, который потребуется для преобразования из вращений/Verse.org/SpatialMathво вращения/UnrealEngine.com/Temporary/SpatialMath. Это можно сделать с помощью функцииfromRotation(). ВызовитеMakeComponentWiseDeltaRotation(), передав поворот каждого преобразования после его преобразования с помощьюFromRotation(). Затем преобразуйте результат вызова этой функции снова с помощьюFromRotation(), чтобы преобразовать его обратно в/Verse.org/SpatialMathвращение. Наконец, установите для параметра Scale результат сложенияVectorOnesкак разницу между первым и вторым значениями масштаба, деленную на первое значение масштаба. Это обеспечит правильное масштабирование модуля во время анимации. Готовая функцияGetDeltaTransform()должна выглядеть следующим образом:Verse# Get the delta transform between two given transforms. GetDeltaTransform<public>(TransformOne:(/Verse.org/SpatialMath:)transform, TransformTwo:(/Verse.org/SpatialMath:)transform)<transacts>:(/Verse.org/SpatialMath:)transform= (/Verse.org/SpatialMath:)transform: Translation := TransformTwo.Translation - TransformOne.Translation Rotation := FromRotation(MakeComponentWiseDeltaRotation( FromRotation(TransformTwo.Rotation), FromRotation(TransformOne.Rotation))) Scale := VectorOnes() + ((TransformTwo.Scale - TransformOne.Scale) / TransformOne.Scale)Наконец, добавьте функцию
TryGetvalueOrDefault()в модульУтилитыи добавьте к ней модификатор<transacts>. Эта функция принимает значение переменной типаoptionи значение по умолчанию того же типа и возвращает либо значение по умолчанию, либо элемент изValue, если он существует. Вам это пригодится, когда нужно будет проверить, инициализировано ли значение в классе, и гарантирует, что вернётся некоторое значение, если инициализация не произошла. Убедитесь, что параметрValueсодержит значение вПопробуйтеGetValueOrDefault()и возвращает его. Если нет, то возвращается значениеDefault. Готовый модульUtilitiesс функциейTryGetValurOrDefault()должны выглядеть следующим образом:Verse# Module containing utility functions. Utilities<public> := module: # Utility function for the identity of component-wise vector multiplication. VectorOnes<public>()<transacts>:(/Verse.org/SpatialMath:)vector3 = (/Verse.org/SpatialMath:)vector3{Left := 1.0, Up := 1.0, Forward := 1.0} # Get the delta transform between two given transforms. GetDeltaTransform<public>(TransformOne:(/Verse.org/SpatialMath:)transform, TransformTwo:(/Verse.org/SpatialMath:)transform)<transacts>:(/Verse.org/SpatialMath:)transform=
После определения математических параметров вы можете создавать опорные кадры из кода!
Чтобы настроить функции, создающие опорные кадры, выполните следующее:
Добавьте новую функцию
ConstuctKeyframe()к определению классаanimate_to_targets. Эта функция содержит исходный модуль, целевой модуль, опциональную функцию замедления и длительность. Она даже возвращает массивkeyframed_movements_delta.Verse# Construct a single keyframe that animates between the Source and Destination entity using the given easing function over a set duration. ConstructKeyframe<private>(Source:entity, Destination:entity, Easing:?easing_function, Duration:float)<transacts><decides>:[]keyframed_movement_delta=В
ConstructKeyframe()сначала получите преобразованияисходногоицелевогомодулей, вызвавGetGlobalTransform().Verse# Construct a single keyframe which animates between the Source and Destination entity using the given easing function over a set duration. ConstructKeyframe<private>(Source:entity, Destination:entity, EasingFunction:easing_function, Duration:float)<transacts><decides>:[]keyframed_movement_delta= var SourceTransform:(/Verse.org/SpatialMath:)transform = Source.GetGlobalTransform() var DestinationTransform:(/Verse.org/SpatialMath:)transform = Destination.GetGlobalTransform()Инициализируйте массив с одной составляющей
keyframed_movement_delta. Задайте дляTransformрезультат вызоваGetDeltaTransform(),передав исходное и целевое преобразования, а для параметровDurationиEasingзадайте значения, переданные в эту функцию. Готовая функцияConstructKeyframe()должна выглядеть следующим образом:Verse# Construct a single keyframe which animates between the Source and Destination entity using the given easing function over a set duration. ConstructKeyframe<private>(Source:entity, Destination:entity, EasingFunction:easing_function, Duration:float)<transacts><decides>:[]keyframed_movement_delta= var SourceTransform:(/Verse.org/SpatialMath:)transform = Source.GetGlobalTransform() var DestinationTransform:(/Verse.org/SpatialMath:)transform = Destination.GetGlobalTransform() array: keyframed_movement_delta: Transform := Utilities.GetDeltaTransform(SourceTransform, DestinationTransform) Duration := Duration Easing := EasingFunction
Эта функция создаёт отдельные опорные кадры, но для создания полной анимации вам потребуется больше логики.
Добавьте новую функцию
ConstructAndPlayAnimations()в определение классаanimate_to_targets. Эта функция берёт массив сегментов и режим воспроизведения анимации и использует их для создания и воспроизведения полной анимации. Добавьте к этой функции модификатор<suspends>, чтобы она могла выполняться асинхронно.Verse# Construct and play an animation from an array of animation segments. ConstructAndPlayAnimations<private>(InSegments:[]segment, AnimationPlayback:keyframed_movement_playback_mode)<suspends>:void=В
ConstructAndPlayAnimations()укажите новые названиялогическихпеременныхShouldBreakOutи инициализируйте их со значениемfalse. Поскольку существует три режима воспроизведения движения по опорным кадрам, каждый из них нужно рассматривать по отдельности. Мы будем использовать выражениеloopдля непрерывного создания анимаций для обработки режимов циклического движения вперёд-назад, на для режима однократного движения цикл должен прерываться на первой итерации. Проверьте, установлено ли однократное воспроизведение анимации, — если да, то задайте дляShouldBreakOutзначение true.Verse# Construct and play an animation from an array of animation segments. ConstructAndPlayAnimations<private>(InSegments:[]segment, AnimationPlayback:keyframed_movement_playback_mode)<suspends>:void= var ShouldBreakOut:logic = false # If this is a oneshot animation, break out of loop after it plays once. if (oneshot := oneshot_keyframed_movement_playback_mode[AnimationPlayback]): set ShouldBreakOut = trueЗатем в выражении
ifполучитеkeyframed_movement_componentмодуля в переменнойKeyframedMovementComponent. Затем получите начальное преобразование анимации в переменной с именемStartingTransform, получив первый элемент в массивеInSegments, а затем его глобальное преобразование.Verse# Position this entity in the correct starting position. if: KeyframedMovementComponent := Entity.GetComponent[keyframed_movement_component] StartingTransform := FirstSegment := InSegments[0].SegmentStartPosition?.GetGlobalTransform()Наконец, разместите модуль в начальном местоположении, задав для него начальное преобразование в качестве глобального преобразования, и добавьте задержку в течение значения, указанного в
InitialPauseSeconds, перед воспроизведением анимации.Verse# Position this entity in the correct starting position. if: KeyframedMovementComponent := Entity.GetComponent[keyframed_movement_component] StartingTransform := FirstSegment := InSegments[0].SegmentStartPosition?.GetGlobalTransform() then: Entity.SetGlobalTransform(StartingTransform) # Sleep for initial pause. Sleep(InitialPauseSeconds)Теперь пора создать массив опорных кадров, из которых будет состоять ваша анимация. Сначала инициализируйте новый массив переменных
keyframed_movement_delta, который называетсяKeyframes. В выраженииforпереберите все сегменты в массивеInSegments, получив сегмент и его индекс в локальной переменнойIndex.Verse# Build the array of keyframes to play the animation from. var Keyframes:[]keyframed_movement_delta = array{} for: Index -> Segment:InSegments SourceEntity := Segment.SegmentStartPosition? DestinationEntity := InSegments[Index + 1].SegmentStartPosition?Теперь получите функцию замедления, используемую для этого опорного кадра, в локальной переменной с именем
Easing, вызвавПопробуйтеGetValueOrDefault(), передавSegment.EasingFunctionиDefaultEasingFunction. Также получите длительность сегмента анимации в локальной переменнойDurationизSegment.AnimationDuration. После расстановки всех значений в выраженииifсоздайте опорный кадр, передав каждое значение в функциюConstructKeyframe[]и добавьте результат в массивопорных кадров. Когда все опорные кадры будут созданы, задайте массив опорных кадров для компонента движения с опорными кадрами, вызвавSetKeyframes(), передав массивопорных кадровиPlaybackMode.Verse# Build the array of keyframes to play the animation from. var Keyframes:[]keyframed_movement_delta = array{} for: Index -> Segment:InSegments SourceEntity := Segment.SegmentStartPosition? DestinationEntity := InSegments[Index + 1].SegmentStartPosition? do: Easing := Utilities.TryGetValueOrDefault(Segment.EasingFunction, DefaultEasingFunction) Duration := Segment.AnimationDuration # Construct each keyframe and add it to the array.Итак, массив опорных кадров готов и пора приступить к воспроизведению. Ваша анимация должна выполняться циклически, но с возможностью остановиться после первой итерации, если будет выбран режим анимации «Одиночный кадр». Он также должен обрабатывать паузы в каждом опорном кадре, если сегмент имеет
PauseSeconds. Чтобы обработать это, задайте выражениециклс выражениемforвнутри него. В выраженииforпереберите каждый опорный кадр в массивеопорных кадров, дополнительно получив индекс каждого опорного кадра в переменнойKeyframeIndex.VerseKeyframedMovementComponent.SetKeyframes(Keyframes, PlaybackMode) # Loop playing the animation from the keyframed_movement_component, pausing at each # keyframe for a specified duration. Will break out of the loop if the animation mode # is set to oneshot. loop: for(KeyframeIndex -> Keyframe:Keyframes):Внутри выражения
forполучите сегмент, связанный с этим опорным кадром, указав индексKeyframeIndexв массивеInSegments. Затем, если у сегмента естьPauseSeconds, вызовитеSleep()на это время. После этого вызовитеKeyframedMovementComponent.Play(), после чего дождитесь событияKeyframeReachedEventи вызовитеKeyframedMovementComponent.Pause(). При этом анимация приостанавливается на каждом опорном кадре на времяPauseSeconds, перед тем как воспроизводить его и ждать следующего опорного кадра и снова приостанавливать. Наконец, в конце выраженияloopпроверьте, имеет лиShouldBreakOutзначение true, и, если да, выйдите из цикла.Verse# Loop playing the animation from the keyframed_movement_component, pausing at each # keyframe for a specified duration. Will break out of the loop if the animation mode # is set to oneshot. loop: for(KeyframeIndex -> Keyframe:Keyframes): if: SegmentReached := InSegments[KeyframeIndex] then: Sleep(SegmentReached.PauseSeconds) KeyframedMovementComponent.Play()Готовая функция
ContstructAndPlayAnimations()должна выглядеть следующим образом:Verse# Construct and play an animation from an array of animation segments. ConstructAndPlayAnimations<private>(InSegments:[]segment, AnimationPlayback:keyframed_movement_playback_mode)<suspends>:void= var ShouldBreakOut:logic = false # If this is a oneshot animation, break out of loop after it plays once. if (oneshot := oneshot_keyframed_movement_playback_mode[AnimationPlayback]): set ShouldBreakOut = true # Position this entity in the correct starting position.
Следующий этап: создание заготовки анимированного модуля и её реализация на уровне!