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/BUITweenWidget.cpp b/Source/BUITween/Private/BUITweenWidget.cpp new file mode 100644 index 0000000..9ea370c --- /dev/null +++ b/Source/BUITween/Private/BUITweenWidget.cpp @@ -0,0 +1,127 @@ +// 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->GetRenderTransform().Translation; + float Rotation = TargetWidget->GetRenderTransform().Angle; + FVector2D Scale = TargetWidget->GetRenderTransform().Scale; + float Opacity = TargetWidget->GetRenderOpacity(); + + FLinearColor Color; + UUserWidget* UW = Cast(TargetWidget); + if (UW) + { + Color = UW->GetColorAndOpacity(); + } + UImage* UI = Cast(TargetWidget); + if (UI) + { + Color = UI->GetColorAndOpacity(); + } + UBorder* Border = Cast(TargetWidget); + if (Border) + { + Color = Border->GetContentColorAndOpacity(); + } + + FVector2D CanvasPosition; + UCanvasPanelSlot* CanvasSlot = Cast(TargetWidget->Slot); + if (CanvasSlot) + { + CanvasPosition = CanvasSlot->GetPosition(); + } + + ESlateVisibility WidgetVisibility = TargetWidget->GetVisibility(); + + float MaxDesiredHeight = 0; + USizeBox* SizeBox = Cast(TargetWidget); + if (SizeBox) + { + MaxDesiredHeight = SizeBox->GetMaxDesiredHeight(); + } + + FMargin WidgetPadding; + UOverlaySlot* OverlaySlot = Cast(TargetWidget->Slot); + UHorizontalBoxSlot* HorizontalBoxSlot = Cast(TargetWidget->Slot); + UVerticalBoxSlot* VerticalBoxSlot = Cast(TargetWidget->Slot); + UCanvasPanelSlot* CanvasPanelSlot = Cast(TargetWidget->Slot); + + if (OverlaySlot) + { + WidgetPadding = FVector4( + OverlaySlot->GetPadding().Left, + OverlaySlot->GetPadding().Top, + OverlaySlot->GetPadding().Bottom, + OverlaySlot->GetPadding().Right); + } + else if (HorizontalBoxSlot) + { + WidgetPadding = FVector4( + HorizontalBoxSlot->GetPadding().Left, + HorizontalBoxSlot->GetPadding().Top, + HorizontalBoxSlot->GetPadding().Bottom, + HorizontalBoxSlot->GetPadding().Right); + } + else if (VerticalBoxSlot) + { + WidgetPadding = FVector4( + VerticalBoxSlot->GetPadding().Left, + VerticalBoxSlot->GetPadding().Top, + VerticalBoxSlot->GetPadding().Bottom, + VerticalBoxSlot->GetPadding().Right); + } + else if(CanvasPanelSlot) + { + FVector2D position = CanvasPanelSlot->GetPosition(); + FVector2D size = CanvasPanelSlot->GetSize(); + WidgetPadding = FVector4( + position.X, + position.Y, + size.X, + size.Y); + } + + return FWidgetAppearance(Translation, Rotation, Scale, Opacity, Color, CanvasPosition, WidgetVisibility, MaxDesiredHeight, WidgetPadding); +} + 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 + ); + + +};