diff --git a/BUITween.uplugin b/BUITween.uplugin index ecef620..06f227e 100644 --- a/BUITween.uplugin +++ b/BUITween.uplugin @@ -5,9 +5,9 @@ "FriendlyName": "BUITween", "Description": "Tweening library for UMG", "Category": "Other", - "CreatedBy": "benui", + "CreatedBy": "benui & The Hoodie Guy", "CreatedByURL": "https://benui.ca/", - "DocsURL": "", + "DocsURL": "https://github.com/TheHoodieGuy02/UE4-UITween/", "MarketplaceURL": "", "SupportURL": "", "CanContainContent": false, @@ -21,4 +21,4 @@ "LoadingPhase": "Default" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 37f7a4f..f7109ae 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,35 @@ -# UI Tweening Libary for UE4/UMG +# UI Tweening Libary for Unreal Engine's UMG -Create simple tweens for UMG widgets from C++. +Create simple tweens for UMG widgets from C++ and BP. -```cpp -UBUITween::Create( SomeWidget, 0.2f ) - .FromTranslation( FVector2D( -100, 0 ) ) - .FromOpacity( 0.2f ) - .ToTranslation( FVector2D( 20, 10 ) ) - .ToOpacity( 1.0f ) - .Begin(); -``` +https://user-images.githubusercontent.com/26211033/137335549-8e20eefa-bf7f-4415-af83-bf68cabe9b1c.mp4 + + +## Compatible engine versions + +The plugin was tested to work in 4.27. However, the functions are rather simple, +it is expected to work on at least far back as Unreal Engine 4.5. +It has yet been tested in Unreal Engine 5, but should also work right away without +much hassle. +## Why using code tweening and not the UMG timeline? -## Usage +While UMG's timeline animator is serviceable for prototyping UI animations, or used +as is for fixed animations, it lacks the flexibility to animate more modular, +procedurally built widgets (e.g. Inventory list, commands menu, etc.) dynamically. + +For one thing, UMG Timeline can't expose animatable properties it controls to code. +This could be a problem if your animation needs to be adapting to prior interactions +(e.g. zooming in from / out to where a button is located on screen). It can also be +difficult to reuse in a different context, requiring to make a different sequence +for another possible context. Code driven tweening aims to solve that, by having +start point and end point properties exposed to code, and tweens to be reused as +functions or macro, allowing for more dynamic animations. + +Of course, you can combine the best of both worlds, to achieve cool looking widget +animations. + +## Usage in C++ The plugin module registers itself to tick automatically even during game-world pause. @@ -62,11 +79,75 @@ UBUITween::Create( MyWidget, 0.5f ) For the full API, check the source code. +## Usage in Blueprints + +(exposed to BP by TheHoodieGuy02) + +To use the widget tweening functions in BP, simply reparent your widget BP to +BUITweenWidget class. This User Widget class exposes the Create Widget Tween +to Blueprint, and also provides a struct for the parameters that Create Widget +Tween needs. + +![](ReadmeFiles/CreateWidgetTween.png) + +Please note that the animation will be executed in the tween instance's own C++ +tick thread, therefore, do not execute this on Blueprint tick / every frame. + +- Target Widget is the widget that you want to apply the tween on. +Expects a Widget Object. + +- Tween Duration is the duration for the tween to last, in seconds. +Expects float value. + +- Start Delay is the length of for the tween pausing at the start point, before +interpolating to the end point. +Expects float value. + +- Start Appearance is the start point parameter of the transforms. +Expects WidgetAppearance struct. + +- End Appearance is the end point parameter of the transforms. +Expects WidgetAppearance struct. + +- Easing Type is the interpolation type for the tween. +Expects BUI Easing Type enum. + +![](ReadmeFiles/WidgetAppearanceStruct.png) + +The WidgetAppearance struct contains parameters that control how the widget appears in +the viewport. Some parameters only available to certain widget types, however, Create +Widget Tween function already do the casting and they'll be unused if the cast fails. + +- Translation corresponds to the widget's render translation. +Widget type agnostic. + +- Rotation corresponds to the widget's render rotation. +Widget type agnostic. + +- Scale corresponds to the widget's render scale. +Widget type agnostic. + +- Opacity corresponds to the widget's render opacity. +Widget type agnostic. + +- Color corresponds to the widget's content color. +Applicable to User Widget, Image, and Border widgets. + +- Canvas Position corresponds to the widget's position relative to Canvas Panel parent. +Applicable to widgets in a Canvas Panel. + +- Widget Visibility corresponds to the widget's visibility type. +Widget type agnostic. + +- Max Desired Height corresponds to the widget's desired height limit. +Applicable to Size Box widgets. + +- Widget Padding corresponds to the widget's padding in flowing containers. +Applicable to widgets in an Overlay, Vertical Box, or Vertical Box. -## Caveats +## Caveats and Issues -* I haven't performance-tested it beyond having 5-6 tweens running simultaneously. -* No Blueprint support. +For more updated list of issues, see the [Issues page](https://github.com/TheHoodieGuy02/UE4-UITween/issues). ## License @@ -77,3 +158,4 @@ For the full API, check the source code. If you find it useful, drop me a line [@_benui](https://twitter.com/_benui) on Twitter [benui.ca](https://benui.ca) +** diff --git a/ReadmeFiles/CreateWidgetTween.png b/ReadmeFiles/CreateWidgetTween.png new file mode 100644 index 0000000..b92e63f Binary files /dev/null and b/ReadmeFiles/CreateWidgetTween.png differ diff --git a/ReadmeFiles/EasingTypeEnum.png b/ReadmeFiles/EasingTypeEnum.png new file mode 100644 index 0000000..48df831 Binary files /dev/null and b/ReadmeFiles/EasingTypeEnum.png differ diff --git a/ReadmeFiles/UITweenExample.mp4 b/ReadmeFiles/UITweenExample.mp4 new file mode 100644 index 0000000..42f60fc Binary files /dev/null and b/ReadmeFiles/UITweenExample.mp4 differ diff --git a/ReadmeFiles/WidgetAppearanceStruct.png b/ReadmeFiles/WidgetAppearanceStruct.png new file mode 100644 index 0000000..76511b0 Binary files /dev/null and b/ReadmeFiles/WidgetAppearanceStruct.png differ diff --git a/Source/BUITween/Private/BUITweenInstance.cpp b/Source/BUITween/Private/BUITweenInstance.cpp index 9f2f9a8..976d8ac 100644 --- a/Source/BUITween/Private/BUITweenInstance.cpp +++ b/Source/BUITween/Private/BUITweenInstance.cpp @@ -25,26 +25,26 @@ void FBUITweenInstance::Begin() } // Set all the props to the existng state - TranslationProp.OnBegin( pWidget->RenderTransform.Translation ); - ScaleProp.OnBegin( pWidget->RenderTransform.Scale ); - RotationProp.OnBegin( pWidget->RenderTransform.Angle ); + TranslationProp.OnBegin( pWidget->GetRenderTransform().Translation ); + ScaleProp.OnBegin( pWidget->GetRenderTransform().Scale ); + RotationProp.OnBegin( pWidget->GetRenderTransform().Angle ); OpacityProp.OnBegin( pWidget->GetRenderOpacity() ); { UUserWidget* UW = Cast( pWidget ); if ( UW ) { - ColorProp.OnBegin( UW->ColorAndOpacity ); + ColorProp.OnBegin( UW->GetColorAndOpacity() ); } UImage* UI = Cast( pWidget ); if ( UI ) { - ColorProp.OnBegin( UI->ColorAndOpacity ); + ColorProp.OnBegin( UI->GetColorAndOpacity() ); } UBorder* Border = Cast( pWidget ); if ( Border ) { - ColorProp.OnBegin( Border->ContentColorAndOpacity ); + ColorProp.OnBegin( Border->GetContentColorAndOpacity() ); } } @@ -58,35 +58,46 @@ void FBUITweenInstance::Begin() UOverlaySlot* OverlaySlot = Cast( pWidget->Slot ); UHorizontalBoxSlot* HorizontalBoxSlot = Cast( pWidget->Slot ); UVerticalBoxSlot* VerticalBoxSlot = Cast( pWidget->Slot ); + UCanvasPanelSlot* CanvasPanelSlot = Cast(pWidget->Slot); if ( OverlaySlot ) { PaddingProp.OnBegin( FVector4( - OverlaySlot->Padding.Left, - OverlaySlot->Padding.Top, - OverlaySlot->Padding.Bottom, - OverlaySlot->Padding.Right ) ); + OverlaySlot->GetPadding().Left, + OverlaySlot->GetPadding().Top, + OverlaySlot->GetPadding().Bottom, + OverlaySlot->GetPadding().Right ) ); } else if ( HorizontalBoxSlot ) { PaddingProp.OnBegin( FVector4( - HorizontalBoxSlot->Padding.Left, - HorizontalBoxSlot->Padding.Top, - HorizontalBoxSlot->Padding.Bottom, - HorizontalBoxSlot->Padding.Right ) ); + HorizontalBoxSlot->GetPadding().Left, + HorizontalBoxSlot->GetPadding().Top, + HorizontalBoxSlot->GetPadding().Bottom, + HorizontalBoxSlot->GetPadding().Right ) ); } else if ( VerticalBoxSlot ) { PaddingProp.OnBegin( FVector4( - VerticalBoxSlot->Padding.Left, - VerticalBoxSlot->Padding.Top, - VerticalBoxSlot->Padding.Bottom, - VerticalBoxSlot->Padding.Right ) ); + VerticalBoxSlot->GetPadding().Left, + VerticalBoxSlot->GetPadding().Top, + VerticalBoxSlot->GetPadding().Bottom, + VerticalBoxSlot->GetPadding().Right ) ); + } + else if(CanvasPanelSlot) + { + FVector2D position = CanvasPanelSlot->GetPosition(); + FVector2D size = CanvasPanelSlot->GetSize(); + PaddingProp.OnBegin( FVector4( + position.X, + position.Y, + size.X, + size.Y ) ); } USizeBox* SizeBox = Cast( pWidget ); if ( SizeBox ) { - MaxDesiredHeightProp.OnBegin( SizeBox->MaxDesiredHeight ); + MaxDesiredHeightProp.OnBegin( SizeBox->GetMaxDesiredHeight() ); } // Apply the starting conditions, even if we delay @@ -173,7 +184,7 @@ void FBUITweenInstance::Apply( float EasedAlpha ) } bool bChangedRenderTransform = false; - FWidgetTransform CurrentTransform = Target->RenderTransform; + FWidgetTransform CurrentTransform = Target->GetRenderTransform(); if ( TranslationProp.IsSet() ) { @@ -211,12 +222,18 @@ void FBUITweenInstance::Apply( float EasedAlpha ) UOverlaySlot* OverlaySlot = Cast( pWidget->Slot ); UHorizontalBoxSlot* HorizontalBoxSlot = Cast( pWidget->Slot ); UVerticalBoxSlot* VerticalBoxSlot = Cast( pWidget->Slot ); + UCanvasPanelSlot* CanvasPanelSlot = Cast(pWidget->Slot); if ( OverlaySlot ) OverlaySlot->SetPadding( PaddingProp.CurrentValue ); else if ( HorizontalBoxSlot ) HorizontalBoxSlot->SetPadding( PaddingProp.CurrentValue ); else if ( VerticalBoxSlot ) VerticalBoxSlot->SetPadding( PaddingProp.CurrentValue ); + else if(CanvasPanelSlot) + { + CanvasPanelSlot->SetPosition( FVector2D(PaddingProp.CurrentValue.X,PaddingProp.CurrentValue.Y)); + CanvasPanelSlot->SetSize(FVector2D(PaddingProp.CurrentValue.Z,PaddingProp.CurrentValue.W)); + } } } if ( MaxDesiredHeightProp.IsSet() ) diff --git a/Source/BUITween/Private/BUITweenWidget.cpp b/Source/BUITween/Private/BUITweenWidget.cpp new file mode 100644 index 0000000..50e5458 --- /dev/null +++ b/Source/BUITween/Private/BUITweenWidget.cpp @@ -0,0 +1,115 @@ +// Copyright 2021 The Hoodie Guy + + +#include "BUITweenWidget.h" +#include "BUITween.h" +#include "BUIEasing.h" + +#include "Components/Widget.h" +#include "Components/Image.h" +#include "Components/Border.h" +#include "Components/CanvasPanelSlot.h" +#include "Components/OverlaySlot.h" +#include "Components/VerticalBoxSlot.h" +#include "Components/HorizontalBoxSlot.h" +#include "Components/SizeBox.h" +#include "Blueprint/UserWidget.h" + +void UBUITweenWidget::CreateWidgetTween(UWidget* TargetWidget, float TweenDuration, float StartDelay, FWidgetAppearance StartAppearance, FWidgetAppearance EndAppearance, EBUIEasingType EasingType) +{ + UBUITween::Create(TargetWidget, TweenDuration, StartDelay) + .FromTranslation(StartAppearance.Translation) + .FromRotation(StartAppearance.Rotation) + .FromScale(StartAppearance.Scale) + .FromOpacity(StartAppearance.Opacity) + .FromColor(StartAppearance.Color) + .FromVisibility(StartAppearance.WidgetVisibility) + .FromCanvasPosition(StartAppearance.CanvasPosition) + .FromPadding(StartAppearance.WidgetPadding) + .ToTranslation(EndAppearance.Translation) + .ToRotation(EndAppearance.Rotation) + .ToScale(EndAppearance.Scale) + .ToOpacity(EndAppearance.Opacity) + .ToColor(EndAppearance.Color) + .ToVisibility(EndAppearance.WidgetVisibility) + .ToCanvasPosition(EndAppearance.CanvasPosition) + .ToPadding(EndAppearance.WidgetPadding) + .Easing(EasingType) + .Begin(); +} + +FWidgetAppearance UBUITweenWidget::GetInitialAppearanceStruct(UWidget* TargetWidget) +{ + // The whole thing is almost the same with BUITweenInstance.cpp, + // except we're just getting the initial values. + + FVector2D Translation = TargetWidget->RenderTransform.Translation; + float Rotation = TargetWidget->RenderTransform.Angle; + FVector2D Scale = TargetWidget->RenderTransform.Translation; + float Opacity = TargetWidget->GetRenderOpacity(); + + FLinearColor Color; + UUserWidget* UW = Cast(TargetWidget); + if (UW) + { + Color = UW->ColorAndOpacity; + } + UImage* UI = Cast(TargetWidget); + if (UI) + { + Color = UI->ColorAndOpacity; + } + UBorder* Border = Cast(TargetWidget); + if (Border) + { + Color = Border->ContentColorAndOpacity; + } + + FVector2D CanvasPosition; + UCanvasPanelSlot* CanvasSlot = Cast(TargetWidget->Slot); + if (CanvasSlot) + { + CanvasPosition = CanvasSlot->GetPosition(); + } + + ESlateVisibility WidgetVisibility = TargetWidget->GetVisibility(); + + float MaxDesiredHeight; + USizeBox* SizeBox = Cast(TargetWidget); + if (SizeBox) + { + MaxDesiredHeight = SizeBox->MaxDesiredHeight; + } + + FMargin WidgetPadding; + UOverlaySlot* OverlaySlot = Cast(TargetWidget->Slot); + UHorizontalBoxSlot* HorizontalBoxSlot = Cast(TargetWidget->Slot); + UVerticalBoxSlot* VerticalBoxSlot = Cast(TargetWidget->Slot); + if (OverlaySlot) + { + WidgetPadding = FVector4( + OverlaySlot->Padding.Left, + OverlaySlot->Padding.Top, + OverlaySlot->Padding.Bottom, + OverlaySlot->Padding.Right); + } + else if (HorizontalBoxSlot) + { + WidgetPadding = FVector4( + HorizontalBoxSlot->Padding.Left, + HorizontalBoxSlot->Padding.Top, + HorizontalBoxSlot->Padding.Bottom, + HorizontalBoxSlot->Padding.Right); + } + else if (VerticalBoxSlot) + { + WidgetPadding = FVector4( + VerticalBoxSlot->Padding.Left, + VerticalBoxSlot->Padding.Top, + VerticalBoxSlot->Padding.Bottom, + VerticalBoxSlot->Padding.Right); + } + + return FWidgetAppearance(Translation, Rotation, Scale, Opacity, Color, CanvasPosition, Visibility, MaxDesiredHeight, Padding); +} + diff --git a/Source/BUITween/Public/BUIEasing.h b/Source/BUITween/Public/BUIEasing.h index a077920..60f3e99 100644 --- a/Source/BUITween/Public/BUIEasing.h +++ b/Source/BUITween/Public/BUIEasing.h @@ -3,8 +3,8 @@ #include "CoreMinimal.h" #include "CoreUObject.h" -UENUM() -enum class EBUIEasingType +UENUM(BlueprintType) +enum class EBUIEasingType : uint8 { Linear, Smoothstep, diff --git a/Source/BUITween/Public/BUITweenWidget.h b/Source/BUITween/Public/BUITweenWidget.h new file mode 100644 index 0000000..834a781 --- /dev/null +++ b/Source/BUITween/Public/BUITweenWidget.h @@ -0,0 +1,110 @@ +// Copyright 2021 The Hoodie Guy + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "BUIEasing.h" +#include "BUITweenWidget.generated.h" + + +/* TODO: Maybe consider adding On Complete exec output. + As for now, this was hindered by the way UBUITween + handles On Completion callback, so consider adding + Delay node with duration equal to duration and the + start delay. +*/ + +UENUM(BlueprintType) +enum class EWaitForCompletion : uint8 +{ + Then, + OnCompleted +}; + + +USTRUCT(BlueprintType) +struct FWidgetAppearance +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + FVector2D Translation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + float Rotation; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + FVector2D Scale; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + float Opacity; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + FLinearColor Color; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + FVector2D CanvasPosition; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + ESlateVisibility WidgetVisibility; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + float MaxDesiredHeight; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "BUITween") + FMargin WidgetPadding; + + FWidgetAppearance() + { + + } + + FWidgetAppearance( + FVector2D Translation, + float Rotation, + FVector2D Scale, + float Opacity, + FLinearColor Color, + FVector2D CanvasPosition, + ESlateVisibility Visibility, + float MaxDesiredHeight, + FMargin Padding + ) + { + + } +}; + + +/** + User Widget class with BUITween integration. + */ +UCLASS() +class BUITWEEN_API UBUITweenWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** + Gets initial value of the appearance struct for the specified widget, as defined in the UMG Designer. + */ + UFUNCTION(BlueprintCallable, BlueprintPure) + FWidgetAppearance GetInitialAppearanceStruct(UWidget* TargetWidget); + + /** + Creates a tween instance for the specified widget. + + The animation will be executed in the tween instance's own C++ tick, therefore, do not execute this node on Blueprint tick/every frame. + */ + UFUNCTION(BlueprintCallable) + void CreateWidgetTween(UWidget* TargetWidget, + float TweenDuration, + float StartDelay, + FWidgetAppearance StartAppearance, + FWidgetAppearance EndAppearance, + EBUIEasingType EasingType + ); + + +};