Este tutorial é uma versão avançada específica para o Scene Graph do tutorial Como animar adereços. Se quiser aprender mais sobre o movimento baseado em animação no UEFN fora do Scene Graph, confira o tutorial e depois volte para cá.
Plataformas móveis aparecem na maioria dos jogos de plataforma e desafiam o jogador a realizar saltos precisos entre os alvos para atingir o objetivo.
Existem diversas maneiras de mover adereços no UEFN. Você pode usar funções como TeleportTo[] ou MoveTo() para modificar uma transformação diretamente ou usar outro dispositivo, como um propulsor de adereços, para mover um adereço em um caminho predefinido. No entanto, existe também uma outra opção bastante útil: as animações.
Animações têm alguns benefícios em relação à movimentação da transformação de um adereço. Animações geralmente têm movimentos mais suaves do que mover objetos com MoveTo() ou TeleportTo(), pois evitam a latência de rede de ter que chamar essas funções a cada marca de jogo.
Animações também têm colisões mais consistentes com jogadores ou outros objetos, e você tem um nível maior de controle sobre onde e como um objeto se move em comparação com o uso de um dispositivo Propulsor de Adereços. Você pode reproduzir animações em loop ou reproduzi-las no sentido normal e inverso com o modo ping-pong.
As animações também permitem escolher um tipo de interpolação. O tipo de interpolação determina o tipo de suavização, ou curva de animação, que sua animação segue. Por exemplo, o tipo de interpolação linear reproduz sua animação a uma velocidade constante, enquanto o tipo ease-in começa devagar e acelera chegando ao final.
Ao escolher o tipo de interpolação certo para a sua animação, você pode especificar em diferentes pontos se o adereço deve desacelerar, acelerar ou se mover linearmente. Ao aplicar um componente para implementar esses comportamentos nas entidades do seu nível, você pode criar plataformas móveis pelas quais os jogadores podem navegar.
Primeiro, considere que tipo de comportamento uma plataforma móvel deve ser capaz de realizar. Ela deve começar a ser animada a partir de uma determinada posição inicial e, em seguida, mover-se para vários pontos. Ao chegar ao final do movimento, ela deve ser capaz de retornar à posição inicial ou permanecer no lugar.
Ele deve fazer esses movimentos ao longo de uma duração específica e ser capaz de girar e se dimensionar corretamente em cada ponto ao longo de sua jornada. Cada um desses comportamentos requer um código específico para ser alcançado, mas, ao começar com uma classe simples e desenvolvê-la, você pode iterar rapidamente em diferentes ideias. Siga estas etapas para criar um componente de movimento baseado em animação.
Crie um novo componente do Verse chamado
animate_to_targets_componente abra-o no Visual Studio Code. Para obter mais informações sobre como criar seus próprios componentes do Verse, consulte Como criar seu próprio componente do Verse. Adicioneusinginstruções para o/Verse.org/SpatialMath,/Verse.org/SceneGraph/KeyframedMovement, e/UnrealEngine.com/Temporary/SpatialMathVerse.org/SpatialMath. Você precisará de funções de cada um deles mais tarde.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):As dicas de ferramenta usadas nesta seção do tutorial estão incluídas abaixo. Você pode copiá-las e colá-las acima da definição da classe
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."Adicione os seguintes campos à definição da classe
animate_to_targets_component:Uma variável float de número editável denominada
InitialPauseSeconds. Esse é o tempo necessário para a entidade começar a animar. Defina-a como10,0para que a entidade aguarde dez segundos antes de iniciar a animação.
Verse# Amount of time to pause before the animation starts. @editable_number(float): ToolTip := SpeedTip MinValue := option{0.0} var InitialPauseSeconds<public>:float = 10.0Um elemento
keyframed_movement_playback_modeeditável chamadoAnimationPlaybackMode. Este é o modo de animação para a animação da entidade. Defina comoloop_keyframed_movement_playback_mode. Isso significa que, por padrão, quando a animação for concluída, a entidade entrará em loop e iniciará sua animação novamente do início.
Verse# The playback mode used by this animation. @editable: ToolTip := PlaybackModeTip var PlaybackMode<public>:keyframed_movement_playback_mode = loop_keyframed_movement_playback_mode{}Salve o código e compile-o.
Como dividir animações em segmentos
Para criar animações em código, você usará keyframes. Animações são compostas por um ou mais keyframes, e cada keyframe especifica os valores de um objeto em pontos específicos na animação. Ao criar uma animação usando keyframes, você pode especificar vários pontos para seu adereço se mover, girar e se redimensionar.
O keyframed_movement_component usa um keyframed_movement_delta como seu tipo de keyframe. Esses deltas de movimento têm três valores:
Transform: especifica as alterações na transformação em relação ao keyframe anterior.Duração: o tempo em segundos que o quadro-chave demora.Easing: a função de suavização a ser usada ao reproduzir esse keyframe.
Como keyframed_movement_component usa uma matriz de keyframes e depois os reproduz, você precisa fornecer todos os keyframes de uma vez para cada animação que deseja reproduzir. Há duas maneiras de fazer isso:
Você poderia criar uma matriz de vários quadros-chave no código, depois passá-la para o componente de movimento com quadros-chave e reproduzir uma única animação.
Você pode criar várias matrizes contendo um único quadro chave e passá-las individualmente ao componente de movimento em quadro chave para reproduzir várias animações em sequência.
Ambas as opções têm vantagens e desvantagens. Matrizes de keyframes únicos permitem que você realize operações com mais facilidade entre keyframes, mas exigem mais código para serem manipulados. Construir todos os quadros-chave de uma só vez facilita o gerenciamento, mas torna mais difícil realizar operações enquanto a animação está sendo reproduzida. Tanto o tutorial Como animar movimento de adereços quanto este tutorial abrangem a primeira abordagem, mas uma implementação da segunda abordagem também será fornecida.
Para criar keyframes individuais no código, você definirá uma classe segment. Cada segmento representará um keyframe usado por keyframed_movement_component que você pode criar no editor. Você também poderá incluir dados extras, como o tempo de espera entre cada keyframe. Siga os passos a seguir para compilar sua classe de segmento.
Adicione uma nova classe chamada
segmentao arquivoanimate_to_targets_component.verse.Adicione o especificador<concrete>para permitir que essa classe seja usada como um valor@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>:Adicione os seguintes campos à definição da classe do segmento:
Uma opção de
entityeditável chamadaSegmentStartPosition. Essa entidade atuará como referência para a posição no mundo na qual a entidade deverá começar a animação.
Verse# An entity that represents the starting position of this entity during the animation segement. @editable: ToolTip := SourceTip SegmentStartPosition:?entity = falseUm
floateditável chamadoAnimationDuration. Este é o tempo que este segmento de animação levará para ser reproduzido. Defina como2.0para que cada segmento de animação leve dois segundos para ser reproduzido.
Verse# The duration of the animation segment. @editable: ToolTip := AnimationDurationTip AnimationDuration:float = 2.0Uma opção de função de suavização editável chamada
EasingFunction. Essa é a função de suavização usada durante esse segmento da animação.
Verse# The easing function to use during this segment of animation. @editable: ToolTip := EasingFunctionTip EasingFunction:?easing_function = falseCada função de suavização é definida por uma curva Bézier cúbica, que consiste em quatro números que criam o tipo de função de suavização que a animação usa. Por exemplo, os parâmetros para uma curva ease-in fazem a animação começar com velocidade reduzida e acelerar depois.
Os parâmetros para uma curva linear fazem a animação ser reproduzida a uma velocidade constante. Você mesmo pode definir esses valores para criar suas próprias curvas de animação personalizadas, mas não precisa fazer isso neste exemplo, pois usará as definidas no módulo
KeyframedMovement.Uma opção de float editável chamada
PauseSeconds. Essa é o tempo em segundos de pausa antes do início desse segmento de animação. Você pode pensar nisso como o tempo que uma entidade faz uma pausa antes de se mover de cada ponto ao longo de seu caminho.
Verse# The number of seconds to pause before starting this animation segment. @editable: ToolTip := PauseTip PauseSeconds:?float = falseVoltando à definição de classe
animate_to_targets_component, adicione os seguintes campos:Uma matriz editável de
segmentchamada Segments. Ela fará referência a cada segmento de animação que compõe a animação geral pela qual sua entidade é executada.
Verse# Segments of the keyframed movement animation. @editable: ToolTip := SegmentsTip var Segments<private>:[]segment = array{}Uma
função de suavizaçãoeditável chamadaDefaultEasingFunction. Se um segmento de animação não especificar uma função de suavização, este será o padrão usado. Defina-a comoeasily_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{}Salve o código e compile-o. No editor, você verá a matriz
Segmentsaparecer em nas entidades que têm o elemento animate_to_targets_component anexado.
Como criar keyframes com código
Com a classe do segmento concluída, é hora de criar os quadros-chave que os segmentos definem. Você criará keyframes individualmente e os adicionará a uma matriz, depois passará a matriz para seu keyframed_movement_component. Isso exigirá alguns cálculos, que você definirá agora.
Como operações matemáticas são úteis em várias situações, é útil colocar essa lógica em um arquivo utilitário para acessá-la de qualquer um dos seus componentes do Verse. Para saber mais práticas recomendadas ao trabalhar com entidades, consulte Como criar seu próprio componente do Verse. Siga as etapas abaixo para criar seu arquivo de utilitário:
Crie um novo módulo no seu arquivo
animate_to_targets.versechamadoUtilities. Assim, armazenaremos a lógica comum que usamos em todo o projeto.Verse# Module containing utility functions. Utilities<public> := module:Adicione um novo alias de tipo
vector3chamadoVectorOnesao seu módulo Utilities que cria umvector3em queLeft,UpeForwardestão todos definidos como1,0. Você usará este vetor mais tarde para facilitar uma parte dos cálculos, assim, definir um alias de tipo significa que você não precisa escrevervector3{Left := 1.0, Up := 1.0, Forward := 1.0}repetidamente. Como você importou ambos/Verse.org/SpatialMathe/UnrealEngine.com/Temporary/SpatialMathmódulos, você precisará especificar que este é um/Verse.org/SpatialMathvector3, pois ambos os módulos incluem uma definição para ele.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}Adicione uma nova função chamada
GetDeltaTransform()ao seu móduloUtilitários. Essa função calculará a diferença entre duas transformações e retornará o delta. Adicione o modificador<transacts>a essa função para permitir que ela seja revertida. Especifique/Verse.org/SpatialMathcomo o módulo para cadatransformar, pois você calculará a diferença entre as transformações de Entidade.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=Em
GetDeltaTransform, inicialize uma nova transformação/Verse.org/SpatialMath. Defina aTranslaçãocomo a diferença entre a translação de cada transformação. Defina aRotaçãopara o resultado da chamada deMakeComponentWiseDeltaRotation(). Como essa função está localizada no módulo/UnrealEngine.com/Temporary/SpatialMath, você precisará converter de rotações/Verse.org/SpatialMathpara rotações/UnrealEngine.com/Temporary/SpatialMath. Você pode fazer isso usando a funçãoFromRotation(). ChameMakeComponentWiseDeltaRotation()passando a rotação de cada transformação depois de convertê-la comFromRotation(). Em seguida, converta o resultado dessa chamada de função usandoFromRotation()novamente para converter de volta para um/Verse.org/SpatialMathrotação. Por fim, defina a escala para o resultado de adicionarVectorOnescomo a diferença entre a primeira e a segunda escalas dividida pela primeira escala. Isso garante que sua entidade seja dimensionada corretamente durante a animação. Sua funçãoGetDeltaTransform()completa deve ficar assim: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)Por fim, adicione uma função chamada
TryGetvalueOrDefault()ao seu móduloUtilitiese adicione o modificador<transacts>a ele. Essa função usa um valoroptionde algum tipo e um valor padrão do mesmo tipo e retorna o valor padrão ou o item dentro deValue, se existir. Isso é útil quando você deseja verificar se um valor em uma classe foi realmente inicializado e garante que você retorne algum valor se não for o caso. Dentro deTryGetValueOrDefault(), verifique seValuecontém um valor e retorne-o. Caso contrário, retorneDefault. Seu móduloUtilitiescompleto e a funçãoTryGetValurOrDefault()devem ficar assim: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=
Com a matemática definida, agora você pode criar os keyframes a partir do código.
Siga estas etapas para criar as funções de criação de keyframes:
Adicione uma nova função chamada
ConstructKeyframe()à sua definição de classeanimate_to_targets. Essa função usa uma entidade de origem, uma entidade de destino, uma função de suavização opcional e uma duração. Ele até mesmo retorna uma matriz dekeyframed_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=Em
ConstructKeyframe(), primeiro obtenha as transformações das entidadesSourceeDestinationchamandoGetGlobalTransform().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()Inicialize uma matriz com um único membro de
keyframed_movement_delta. Defina aTransformaçãocomo o resultado da chamada deGetDeltaTransform()passando as transformações de origem e destino e defina aDuraçãoe aSuavizaçãocomo os valores transmitidos para essa função. Sua funçãoConstructKeyframe()completa deve ficar assim: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
Essa função cria keyframes individuais, mas você precisará de mais lógica para criar animações completas.
Adicione uma nova função chamada
ConstructAndPlayAnimations()à sua definição de classeanimate_to_targets. Essa função usa uma matriz de segmentos e o modo de reprodução da animação para criar e reproduzir uma animação completa. Adicione o modificador<suspends>a essa função para permitir que ela seja executada de maneira assíncrona.Verse# Construct and play an animation from an array of animation segments. ConstructAndPlayAnimations<private>(InSegments:[]segment, AnimationPlayback:keyframed_movement_playback_mode)<suspends>:void=Em
ConstructAndPlayAnimations(), defina uma nova variávellogicchamadaShouldBreakOute inicialize-a comofalse. Dados os três modos de reprodução de movimento com keyframes, você precisará gerenciar cada um individualmente. Você usará uma expressãolooppara criar animações continuamente para gerenciar os modos de loop de pingue-pongue, mas o modo uso único deve sair do loop na primeira iteração. Verifique se o modo de reprodução de animação é o modo único e, em caso afirmativo, definaShouldBreakOutcomo verdadeiro.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 = trueEm seguida, em uma expressão
if, obtenha okeyframed_movement_componentda entidade em uma variávelKeyframedMovementComponent. Em seguida, obtenha a transformação inicial da animação em uma variável chamadaStartingTransform, obtendo o primeiro elemento na matrizInSegmentse, em seguida, sua transformação global.Verse# Position this entity in the correct starting position. if: KeyframedMovementComponent := Entity.GetComponent[keyframed_movement_component] StartingTransform := FirstSegment := InSegments[0].SegmentStartPosition?.GetGlobalTransform()Por fim, posicione a entidade em sua posição inicial definindo sua transformação global como a transformação inicial e aguarde
InitialPauseSecondsantes da animação ser reproduzida.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)Agora é hora de criar a matriz de quadros-chave a partir da qual você criará a animação. Primeiro, inicialize uma nova matriz de variáveis de
keyframed_movement_deltachamadaKeyframes. Em seguida, em uma expressãofor, percorra cada segmento na matrizInSegments, obtendo o segmento e seu índice em uma variável local chamadaIndex.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?Agora, obtenha a função de easing usada para esse keyframe em uma variável local chamada
EasingchamandotryGetValueOrDefault(), passandoSegment.EasingFunctioneDefaultEasingFunction. Além disso, obtenha a duração do segmento de animação em uma variável localDurationdeSegment.AnimationDuration. Com todos os valores no lugar, em uma expressãoif, construa o keyframe passando cada valor paraConstructKeyframe[]e adicione o resultado à matrizKeyframes. Quando todos os keyframes estiverem criados, defina a matriz de keyframes no componente de movimento de keyframes chamandoSetKeyframes()passando a matrizKeyframese oPlaybackMode.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.Com a sua matriz de keyframes definida, é hora de começar a reproduzi-los. Sua animação precisa ser executada em loop, mas deve parar após a primeira iteração se o modo de animação estiver definido como one-shot. Ele também precisa lidar com pausas em cada keyframe se o segmento tiver
PauseSeconds. Para lidar com isso, configure uma expressão"loop" comuma expressão "for"dentro dela. Na expressãofor, iterar por cada keyframe na matrizKeyframes, obtendo adicionalmente o índice de cada keyframe em uma variávelKeyframeIndex.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):Dentro da expressão "for", obtenha o segmento
associado aesse keyframe indexando na matrizInSegmentscom KeyframeIndex. Em seguida, se o segmento tiverPauseSeconds, chameSleep()por esse período de tempo. Depois, faça a chamadaKeyframedMovementComponent.Play(), seguido pela espera do eventoKeyframeReachedEvente chamandoKeyframedMovementComponent.Pause(). Isso faz com que a animação seja pausada em cada keyframe por um período de tempo dePauseSeconds, antes de reproduzir e aguardar o próximo keyframe e pausar novamente. Por fim, no final daexpressão "loop", verifique seShouldBreakOuté verdadeiro e, em caso afirmativo, saia do loop.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()Sua função
ContstructAndPlayAnimations()completa ficará desta forma: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.
Na próxima etapa, você criará uma estrutura pré-fabricada da sua entidade de animação e a instanciará no nível.