接入Spine

This commit is contained in:
2025-09-23 02:52:33 +08:00
parent 89d751ac34
commit c90b46f430
291 changed files with 32686 additions and 473 deletions

View File

@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=4F8A2207_002DE0F5_002DA9C1_002D5E41_002D4243D0FAAC15_002Fd_003Aboost_002D1_005F82_005F0_002Fd_003Ainclude_002Fd_003Aboost_002Fd_003Acoroutine_002Fd_003Adetail_002Ff_003Apreallocated_002Ehpp/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=4F8A2207_002DE0F5_002DA9C1_002D5E41_002D4243D0FAAC15_002Fd_003Aboost_002D1_005F82_005F0_002Fd_003Ainclude_002Fd_003Aboost_002Fd_003Asafe_005Fnumerics_002Fd_003Aconcept_002Ff_003Apromotion_005Fpolicy_002Ehpp/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=5E061600_002DD0A1_002DF678_002DBF8E_002D1D2CD7670646_002Fd_003AEigen_002Fd_003Asrc_002Fd_003ACore_002Fd_003Autil_002Ff_003AConstants_002Eh/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=B5255153_002DA839_002D6956_002D3EF8_002D0716EE76F13A_002Fd_003A5414_002Fd_003AInclude_002Fd_003Atools_002Fd_003Aclang_002Fd_003Arewrite_005Fto_005Fchrome_005Fstyle_002Fd_003Atests_002Fd_003Agen_002Ff_003Athing_002Eh/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View File

@ -20,5 +20,11 @@ GameDefaultMap=/Game/Level/HomeLand.HomeLand
[/Script/Engine.RendererSettings]
r.Mobile.AntiAliasing=0
r.AntiAliasingMethod=2
r.AntiAliasingMethod=3
r.DefaultFeature.MotionBlur=False
[CoreRedirects]
+PropertyRedirects=(OldName="/Script/BusyRabbit.TerrainTileSetConfig.TerrainTileMapping",NewName="/Script/BusyRabbit.TerrainTileSetConfig.n")
+PropertyRedirects=(OldName="/Script/BusyRabbit.TerrainLayerComponent.TerrainLayers",NewName="/Script/BusyRabbit.TerrainLayerComponent.TerrainMeshes")
+ClassRedirects=(OldName="/Script/BusyRabbit.LevelPlayerController",NewName="/Script/BusyRabbit.LevelPlayerController")

View File

@ -35,12 +35,14 @@ NetIndexFirstBitSegment=16
+GameplayTagList=(Tag="Recover.Role.Health",DevComment="回复生命值")
+GameplayTagList=(Tag="Recover.Role.Hunger",DevComment="恢复饥饿值")
+GameplayTagList=(Tag="Status.Role.Invincible",DevComment="不掉血标签")
+GameplayTagList=(Tag="Terrain.Desert",DevComment="荒漠地形")
+GameplayTagList=(Tag="Terrain.Forest",DevComment="森林")
+GameplayTagList=(Tag="Terrain.Grassland",DevComment="草地")
+GameplayTagList=(Tag="Terrain.Water",DevComment="水体")
+GameplayTagList=(Tag="Terrain.Water.Shallow", DevComment="浅水区")
+GameplayTagList=(Tag="Terrain.Water.Deep", DevComment="深水区")
+GameplayTagList=(Tag="Terrain.Land",DevComment="土地")
+GameplayTagList=(Tag="Terrain.Swamp", DevComment="沼泽")
+GameplayTagList=(Tag="Terrain.Swamp.Land", DevComment="沼泽陆地")
+GameplayTagList=(Tag="Terrain.Swamp.Water", DevComment="沼泽水域")
+GameplayTagList=(Tag="Terrain.Swamp",DevComment="沼泽")
+GameplayTagList=(Tag="Terrain.Swamp.Land",DevComment="沼泽陆地")
+GameplayTagList=(Tag="Terrain.Swamp.Water",DevComment="沼泽水域")
+GameplayTagList=(Tag="Terrain.Water",DevComment="水体")
+GameplayTagList=(Tag="Terrain.Water.Deep",DevComment="深水区")
+GameplayTagList=(Tag="Terrain.Water.Shallow",DevComment="浅水区")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -42,11 +42,11 @@ function SubSystem:ReceiveWorldBeginPlay()
self.generator = CreateItemGenerator(row_data)
-- 创建初始篝火
local bonfire = BusyActorManagerSubSystem:SpawnBonfire(row_data.FirstBonfirePosition)
-- local bonfire = BusyActorManagerSubSystem:SpawnBonfire(row_data.FirstBonfirePosition)
-- 创建角色
local role = BusyActorManagerSubSystem:SpawnRole(bonfire)
GameplayStatics.GetPlayerController(self, 0):Possess(role)
-- local role = BusyActorManagerSubSystem:SpawnRole(bonfire)
-- GameplayStatics.GetPlayerController(self, 0):Possess(role)
end
function SubSystem:ReceiveSubSystemTick(DeltaTime)

View File

@ -0,0 +1,11 @@
local BusyPlayerRole = {}
function BusyPlayerRole:UpdateMoveDirection(InDirection)
if(InDirection.Y > 0) then
self["SpineAnimationComponent"]:SetSkin("front/move")
else
self["SpineAnimationComponent"]:SetSkin("back/move")
end
end
return Class(nil, nil, BusyPlayerRole)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,143 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineAtlasImportFactory.h"
#include "AssetToolsModule.h"
#include "SpineAtlasAsset.h"
#include "Editor.h"
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
USpineAtlasAssetFactory::USpineAtlasAssetFactory(const FObjectInitializer &objectInitializer) : Super(objectInitializer) {
bCreateNew = false;
bEditAfterNew = true;
bEditorImport = true;
SupportedClass = USpineAtlasAsset::StaticClass();
Formats.Add(TEXT("atlas;Spine Atlas file"));
}
FText USpineAtlasAssetFactory::GetToolTip() const {
return LOCTEXT("SpineAtlasAssetFactory", "Animations exported from Spine");
}
bool USpineAtlasAssetFactory::FactoryCanImport(const FString &Filename) {
return true;
}
UObject *USpineAtlasAssetFactory::FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) {
FString FileExtension = FPaths::GetExtension(Filename);
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPreImport(this, InClass, InParent, InName, *FileExtension);
FString rawString;
if (!FFileHelper::LoadFileToString(rawString, *Filename)) {
return nullptr;
}
FString currentSourcePath, filenameNoExtension, unusedExtension;
const FString longPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
FPaths::Split(UFactory::GetCurrentFilename(), currentSourcePath, filenameNoExtension, unusedExtension);
USpineAtlasAsset *asset = NewObject<USpineAtlasAsset>(InParent, InClass, InName, Flags);
asset->SetRawData(rawString);
asset->SetAtlasFileName(FName(*Filename));
LoadAtlas(asset, currentSourcePath, longPackagePath);
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetPostImport(this, asset);
return asset;
}
bool USpineAtlasAssetFactory::CanReimport(UObject *Obj, TArray<FString> &OutFilenames) {
USpineAtlasAsset *asset = Cast<USpineAtlasAsset>(Obj);
if (!asset) return false;
FString filename = asset->GetAtlasFileName().ToString();
if (!filename.IsEmpty())
OutFilenames.Add(filename);
return true;
}
void USpineAtlasAssetFactory::SetReimportPaths(UObject *Obj, const TArray<FString> &NewReimportPaths) {
USpineAtlasAsset *asset = Cast<USpineAtlasAsset>(Obj);
if (asset && ensure(NewReimportPaths.Num() == 1))
asset->SetAtlasFileName(FName(*NewReimportPaths[0]));
}
EReimportResult::Type USpineAtlasAssetFactory::Reimport(UObject *Obj) {
USpineAtlasAsset *asset = Cast<USpineAtlasAsset>(Obj);
FString rawString;
if (!FFileHelper::LoadFileToString(rawString, *asset->GetAtlasFileName().ToString())) return EReimportResult::Failed;
asset->SetRawData(rawString);
FString currentSourcePath, filenameNoExtension, unusedExtension;
const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
FString currentFileName = asset->GetAtlasFileName().ToString();
FPaths::Split(currentFileName, currentSourcePath, filenameNoExtension, unusedExtension);
LoadAtlas(asset, currentSourcePath, longPackagePath);
if (Obj->GetOuter()) Obj->GetOuter()->MarkPackageDirty();
else
Obj->MarkPackageDirty();
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetReimport(asset);
return EReimportResult::Succeeded;
}
UTexture2D *resolveTexture(USpineAtlasAsset *Asset, const FString &PageFileName, const FString &TargetSubPath) {
FAssetToolsModule &AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
TArray<FString> fileNames;
fileNames.Add(PageFileName);
TArray<UObject *> importedAsset = AssetToolsModule.Get().ImportAssets(fileNames, TargetSubPath);
UTexture2D *texture = (importedAsset.Num() > 0) ? Cast<UTexture2D>(importedAsset[0]) : nullptr;
return texture;
}
void USpineAtlasAssetFactory::LoadAtlas(USpineAtlasAsset *Asset, const FString &CurrentSourcePath, const FString &LongPackagePath) {
Atlas *atlas = Asset->GetAtlas();
Asset->atlasPages.Empty();
const FString targetTexturePath = LongPackagePath / TEXT("Textures");
Vector<AtlasPage *> &pages = atlas->getPages();
for (size_t i = 0, n = pages.size(); i < n; i++) {
AtlasPage *page = pages[i];
const FString sourceTextureFilename = FPaths::Combine(*CurrentSourcePath, UTF8_TO_TCHAR(page->name.buffer()));
UTexture2D *texture = resolveTexture(Asset, sourceTextureFilename, targetTexturePath);
Asset->atlasPages.Add(texture);
}
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,71 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineEditorPlugin.h"
#include "AssetTypeActions_Base.h"
#include "SpineAtlasAsset.h"
#include "SpineSkeletonDataAsset.h"
class FSpineAtlasAssetTypeActions : public FAssetTypeActions_Base {
public:
UClass *GetSupportedClass() const override { return USpineAtlasAsset::StaticClass(); };
FText GetName() const override { return INVTEXT("Spine atlas asset"); };
FColor GetTypeColor() const override { return FColor::Red; };
uint32 GetCategories() override { return EAssetTypeCategories::Misc; };
};
class FSpineSkeletonDataAssetTypeActions : public FAssetTypeActions_Base {
public:
UClass *GetSupportedClass() const override { return USpineSkeletonDataAsset::StaticClass(); };
FText GetName() const override { return INVTEXT("Spine data asset"); };
FColor GetTypeColor() const override { return FColor::Red; };
uint32 GetCategories() override { return EAssetTypeCategories::Misc; };
};
class FSpineEditorPlugin : public ISpineEditorPlugin {
virtual void StartupModule() override;
virtual void ShutdownModule() override;
TSharedPtr<FSpineAtlasAssetTypeActions> SpineAtlasAssetTypeActions;
TSharedPtr<FSpineSkeletonDataAssetTypeActions> SpineSkeletonDataAssetTypeActions;
};
IMPLEMENT_MODULE(FSpineEditorPlugin, SpineEditorPlugin)
void FSpineEditorPlugin::StartupModule() {
SpineAtlasAssetTypeActions = MakeShared<FSpineAtlasAssetTypeActions>();
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(SpineAtlasAssetTypeActions.ToSharedRef());
SpineSkeletonDataAssetTypeActions = MakeShared<FSpineSkeletonDataAssetTypeActions>();
FAssetToolsModule::GetModule().Get().RegisterAssetTypeActions(SpineSkeletonDataAssetTypeActions.ToSharedRef());
}
void FSpineEditorPlugin::ShutdownModule() {
if (!FModuleManager::Get().IsModuleLoaded("AssetTools")) return;
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(SpineAtlasAssetTypeActions.ToSharedRef());
FAssetToolsModule::GetModule().Get().UnregisterAssetTypeActions(SpineSkeletonDataAssetTypeActions.ToSharedRef());
}

View File

@ -0,0 +1,126 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineSkeletonImportFactory.h"
#include "AssetToolsModule.h"
#include "Developer/AssetTools/Public/IAssetTools.h"
#include "SpineSkeletonDataAsset.h"
#include <string.h>
#define LOCTEXT_NAMESPACE "Spine"
USpineSkeletonAssetFactory::USpineSkeletonAssetFactory(const FObjectInitializer &objectInitializer) : Super(objectInitializer) {
bCreateNew = false;
bEditAfterNew = true;
bEditorImport = true;
SupportedClass = USpineSkeletonDataAsset::StaticClass();
Formats.Add(TEXT("json;Spine skeleton file"));
Formats.Add(TEXT("skel;Spine skeleton file"));
}
FText USpineSkeletonAssetFactory::GetToolTip() const {
return LOCTEXT("USpineSkeletonAssetFactory", "Animations exported from Spine");
}
bool USpineSkeletonAssetFactory::FactoryCanImport(const FString &Filename) {
if (Filename.Contains(TEXT(".skel"))) return true;
if (Filename.Contains(TEXT(".json"))) {
TArray<uint8> rawData;
if (!FFileHelper::LoadFileToArray(rawData, *Filename, 0)) {
return false;
}
if (rawData.Num() == 0) return false;
return strcmp((const char *) rawData.GetData(), "skeleton") > 0 && strcmp((const char *) rawData.GetData(), "spine") > 0;
}
return false;
}
void LoadAtlas(const FString &Filename, const FString &TargetPath) {
FAssetToolsModule &AssetToolsModule = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools");
FString skelFile = Filename.Replace(TEXT(".skel"), TEXT(".atlas")).Replace(TEXT(".json"), TEXT(".atlas"));
if (!FPaths::FileExists(skelFile)) return;
TArray<FString> fileNames;
fileNames.Add(skelFile);
AssetToolsModule.Get().ImportAssets(fileNames, TargetPath);
}
UObject *USpineSkeletonAssetFactory::FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) {
USpineSkeletonDataAsset *asset = NewObject<USpineSkeletonDataAsset>(InParent, InClass, InName, Flags);
TArray<uint8> rawData;
if (!FFileHelper::LoadFileToArray(rawData, *Filename, 0)) {
return nullptr;
}
asset->SetSkeletonDataFileName(FName(*Filename));
asset->SetRawData(rawData);
const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
LoadAtlas(Filename, longPackagePath);
return asset;
}
bool USpineSkeletonAssetFactory::CanReimport(UObject *Obj, TArray<FString> &OutFilenames) {
USpineSkeletonDataAsset *asset = Cast<USpineSkeletonDataAsset>(Obj);
if (!asset) return false;
FString filename = asset->GetSkeletonDataFileName().ToString();
if (!filename.IsEmpty())
OutFilenames.Add(filename);
return true;
}
void USpineSkeletonAssetFactory::SetReimportPaths(UObject *Obj, const TArray<FString> &NewReimportPaths) {
USpineSkeletonDataAsset *asset = Cast<USpineSkeletonDataAsset>(Obj);
if (asset && ensure(NewReimportPaths.Num() == 1))
asset->SetSkeletonDataFileName(FName(*NewReimportPaths[0]));
}
EReimportResult::Type USpineSkeletonAssetFactory::Reimport(UObject *Obj) {
USpineSkeletonDataAsset *asset = Cast<USpineSkeletonDataAsset>(Obj);
TArray<uint8> rawData;
if (!FFileHelper::LoadFileToArray(rawData, *asset->GetSkeletonDataFileName().ToString(), 0)) return EReimportResult::Failed;
asset->SetRawData(rawData);
const FString longPackagePath = FPackageName::GetLongPackagePath(asset->GetOutermost()->GetPathName());
LoadAtlas(*asset->GetSkeletonDataFileName().ToString(), longPackagePath);
if (Obj->GetOuter()) Obj->GetOuter()->MarkPackageDirty();
else
Obj->MarkPackageDirty();
return EReimportResult::Succeeded;
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,52 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "SpineAtlasAsset.h"
#include "UnrealEd.h"
#include "SpineAtlasImportFactory.generated.h"
// clang-format on
UCLASS()
class USpineAtlasAssetFactory : public UFactory, public FReimportHandler {
GENERATED_UCLASS_BODY()
virtual FText GetToolTip() const override;
virtual bool FactoryCanImport(const FString &Filename) override;
virtual UObject *FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) override;
virtual bool CanReimport(UObject *Obj, TArray<FString> &OutFilenames) override;
virtual void SetReimportPaths(UObject *Obj, const TArray<FString> &NewReimportPaths) override;
virtual EReimportResult::Type Reimport(UObject *Obj) override;
void LoadAtlas(USpineAtlasAsset *Asset, const FString &CurrentSourcePath, const FString &LongPackagePath);
};

View File

@ -0,0 +1,44 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Modules/ModuleManager.h"
class ISpineEditorPlugin : public IModuleInterface {
public:
static inline ISpineEditorPlugin &Get() {
return FModuleManager::LoadModuleChecked<ISpineEditorPlugin>("SpineEditorPlugin");
}
static inline bool IsAvailable() {
return FModuleManager::Get().IsModuleLoaded("SpineEditorPlugin");
}
};

View File

@ -0,0 +1,49 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "SpineAtlasAsset.h"
#include "UnrealEd.h"
#include "SpineSkeletonImportFactory.generated.h"
// clang-format on
UCLASS()
class USpineSkeletonAssetFactory : public UFactory, public FReimportHandler {
GENERATED_UCLASS_BODY()
virtual FText GetToolTip() const override;
virtual bool FactoryCanImport(const FString &Filename) override;
virtual UObject *FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) override;
virtual bool CanReimport(UObject *Obj, TArray<FString> &OutFilenames) override;
virtual void SetReimportPaths(UObject *Obj, const TArray<FString> &NewReimportPaths) override;
virtual EReimportResult::Type Reimport(UObject *Obj) override;
};

View File

@ -0,0 +1,36 @@
using System.IO;
namespace UnrealBuildTool.Rules
{
public class SpineEditorPlugin : ModuleRules
{
public SpineEditorPlugin(ReadOnlyTargetRules target) : base(target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "Public"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "../SpinePlugin/Public/spine-cpp/include"));
PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "Private"));
PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "../SpinePlugin/Public/spine-cpp/include"));
PublicDependencyModuleNames.AddRange(new [] {
"Core",
"CoreUObject",
"Engine",
"UnrealEd",
"SpinePlugin"
});
PublicIncludePathModuleNames.AddRange(new [] {
"AssetTools",
"AssetRegistry"
});
DynamicallyLoadedModuleNames.AddRange(new [] {
"AssetTools",
"AssetRegistry"
});
}
}
}

View File

@ -0,0 +1,415 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SSpineWidget.h"
#include "Framework/Application/SlateApplication.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "Rendering/DrawElements.h"
#include "Runtime/SlateRHIRenderer/Public/Interfaces/ISlateRHIRendererModule.h"
#include "Slate/SMeshWidget.h"
#include "Slate/SlateVectorArtData.h"
#include "SlateMaterialBrush.h"
#include "SpineWidget.h"
#include <spine/spine.h>
using namespace spine;
static int brushNameId = 0;
// Workaround for https://github.com/EsotericSoftware/spine-runtimes/issues/1458
// See issue comments for more information.
struct SpineSlateMaterialBrush : public FSlateBrush {
static TArray<FName> NamePool;
static FCriticalSection NamePoolLock;
SpineSlateMaterialBrush(class UMaterialInterface &InMaterial, const FVector2D &InImageSize)
: FSlateBrush(ESlateBrushDrawType::Image, FName(TEXT("None")), FMargin(0), ESlateBrushTileType::NoTile, ESlateBrushImageType::FullColor, InImageSize, FLinearColor::White, &InMaterial) {
// Workaround for https://github.com/EsotericSoftware/spine-runtimes/issues/2006
FScopeLock Lock(&NamePoolLock);
if (NamePool.Num() > 0) {
ResourceName = NamePool.Pop(false);
} else {
static uint32 NextId = 0;
FString brushName = TEXT("SpineSlateMatBrush");
brushName.AppendInt(NextId++);
ResourceName = FName(*brushName);
}
}
~SpineSlateMaterialBrush() {
FScopeLock Lock(&NamePoolLock);
NamePool.Add(ResourceName);
}
};
TArray<FName> SpineSlateMaterialBrush::NamePool;
FCriticalSection SpineSlateMaterialBrush::NamePoolLock;
void SSpineWidget::Construct(const FArguments &args) {
}
void SSpineWidget::SetData(USpineWidget *Widget) {
this->widget = Widget;
if (widget && widget->skeleton && widget->Atlas) {
Skeleton *skeleton = widget->skeleton;
skeleton->setToSetupPose();
skeleton->updateWorldTransform(Physics_None);
Vector<float> scratchBuffer;
float x, y, w, h;
skeleton->getBounds(x, y, w, h, scratchBuffer);
boundsMin.X = x;
boundsMin.Y = y;
boundsSize.X = w;
boundsSize.Y = h;
}
}
static void setVertex(FSlateVertex *vertex, float x, float y, float u, float v, const FColor &color, const FVector2D &offset) {
vertex->Position.X = offset.X + x;
vertex->Position.Y = offset.Y + y;
vertex->TexCoords[0] = u;
vertex->TexCoords[1] = v;
vertex->TexCoords[2] = u;
vertex->TexCoords[3] = v;
vertex->MaterialTexCoords.X = u;
vertex->MaterialTexCoords.Y = v;
vertex->Color = color;
vertex->PixelSize[0] = 1;
vertex->PixelSize[1] = 1;
}
int32 SSpineWidget::OnPaint(const FPaintArgs &Args, const FGeometry &AllottedGeometry, const FSlateRect &MyClippingRect, FSlateWindowElementList &OutDrawElements,
int32 LayerId, const FWidgetStyle &InWidgetStyle, bool bParentEnabled) const {
SSpineWidget *self = (SSpineWidget *) this;
UMaterialInstanceDynamic *MatNow = nullptr;
if (widget && widget->skeleton && widget->Atlas) {
widget->skeleton->getColor().set(widget->Color.R, widget->Color.G, widget->Color.B, widget->Color.A);
if (widget->atlasNormalBlendMaterials.Num() != widget->Atlas->atlasPages.Num()) {
widget->atlasNormalBlendMaterials.SetNum(0);
widget->pageToNormalBlendMaterial.Empty();
widget->atlasAdditiveBlendMaterials.SetNum(0);
widget->pageToAdditiveBlendMaterial.Empty();
widget->atlasMultiplyBlendMaterials.SetNum(0);
widget->pageToMultiplyBlendMaterial.Empty();
widget->atlasScreenBlendMaterials.SetNum(0);
widget->pageToScreenBlendMaterial.Empty();
for (int i = 0; i < widget->Atlas->atlasPages.Num(); i++) {
AtlasPage *currPage = widget->Atlas->GetAtlas()->getPages()[i];
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->NormalBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
widget->atlasNormalBlendMaterials.Add(material);
widget->pageToNormalBlendMaterial.Add(currPage, material);
material = UMaterialInstanceDynamic::Create(widget->AdditiveBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
widget->atlasAdditiveBlendMaterials.Add(material);
widget->pageToAdditiveBlendMaterial.Add(currPage, material);
material = UMaterialInstanceDynamic::Create(widget->MultiplyBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
widget->atlasMultiplyBlendMaterials.Add(material);
widget->pageToMultiplyBlendMaterial.Add(currPage, material);
material = UMaterialInstanceDynamic::Create(widget->ScreenBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, widget->Atlas->atlasPages[i]);
widget->atlasScreenBlendMaterials.Add(material);
widget->pageToScreenBlendMaterial.Add(currPage, material);
}
} else {
widget->pageToNormalBlendMaterial.Empty();
widget->pageToAdditiveBlendMaterial.Empty();
widget->pageToMultiplyBlendMaterial.Empty();
widget->pageToScreenBlendMaterial.Empty();
for (int i = 0; i < widget->Atlas->atlasPages.Num(); i++) {
AtlasPage *currPage = widget->Atlas->GetAtlas()->getPages()[i];
UTexture2D *texture = widget->Atlas->atlasPages[i];
UTexture *oldTexture = nullptr;
UMaterialInstanceDynamic *current = widget->atlasNormalBlendMaterials[i];
if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->NormalBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, texture);
widget->atlasNormalBlendMaterials[i] = material;
}
widget->pageToNormalBlendMaterial.Add(currPage, widget->atlasNormalBlendMaterials[i]);
current = widget->atlasAdditiveBlendMaterials[i];
if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->AdditiveBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, texture);
widget->atlasAdditiveBlendMaterials[i] = material;
}
widget->pageToAdditiveBlendMaterial.Add(currPage, widget->atlasAdditiveBlendMaterials[i]);
current = widget->atlasMultiplyBlendMaterials[i];
if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->MultiplyBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, texture);
widget->atlasMultiplyBlendMaterials[i] = material;
}
widget->pageToMultiplyBlendMaterial.Add(currPage, widget->atlasMultiplyBlendMaterials[i]);
current = widget->atlasScreenBlendMaterials[i];
if (!current || !current->GetTextureParameterValue(widget->TextureParameterName, oldTexture) || oldTexture != texture) {
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(widget->ScreenBlendMaterial, widget);
material->SetTextureParameterValue(widget->TextureParameterName, texture);
widget->atlasScreenBlendMaterials[i] = material;
}
widget->pageToScreenBlendMaterial.Add(currPage, widget->atlasScreenBlendMaterials[i]);
}
}
self->UpdateMesh(LayerId, OutDrawElements, AllottedGeometry, widget->skeleton);
}
return LayerId;
}
void SSpineWidget::Flush(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, int &Idx, TArray<FVector> &Vertices, TArray<int32> &Indices, TArray<FVector2D> &Uvs, TArray<FColor> &Colors, TArray<FVector> &Colors2, UMaterialInstanceDynamic *Material) {
if (Vertices.Num() == 0) return;
SSpineWidget *self = (SSpineWidget *) this;
const FVector2D widgetSize = AllottedGeometry.GetLocalSize();
const FVector2D sizeScale = widgetSize / FVector2D(boundsSize.X, boundsSize.Y);
const float setupScale = sizeScale.GetMin();
for (int i = 0; i < Vertices.Num(); i++) {
Vertices[i] = (Vertices[i] + FVector(-boundsMin.X - boundsSize.X / 2, boundsMin.Y + boundsSize.Y / 2, 0)) * setupScale + FVector(widgetSize.X / 2, widgetSize.Y / 2, 0);
}
self->renderData.IndexData.SetNumUninitialized(Indices.Num());
SlateIndex *indexData = (SlateIndex *) renderData.IndexData.GetData();
for (int i = 0; i < Indices.Num(); i++) {
indexData[i] = (SlateIndex) Indices[i];
}
self->renderData.VertexData.SetNumUninitialized(Vertices.Num());
FSlateVertex *vertexData = (FSlateVertex *) renderData.VertexData.GetData();
FVector2D offset = AllottedGeometry.GetAbsolutePositionAtCoordinates(FVector2D(0.0f, 0.0f));
FColor white = FColor(0xffffffff);
const FSlateRenderTransform &Transform = AllottedGeometry.GetAccumulatedRenderTransform();
for (size_t i = 0; i < (size_t) Vertices.Num(); i++) {
setVertex(&vertexData[i], 0, 0, Uvs[i].X, Uvs[i].Y, Colors[i], Transform.TransformPoint(FVector2D(Vertices[i])));
}
brush = &widget->Brush;
if (Material) {
renderData.Brush = MakeShareable(new SpineSlateMaterialBrush(*Material, FVector2D(64, 64)));
renderData.RenderingResourceHandle = FSlateApplication::Get().GetRenderer()->GetResourceHandle(*renderData.Brush);
}
if (renderData.RenderingResourceHandle.IsValid()) {
FSlateDrawElement::MakeCustomVerts(OutDrawElements, LayerId, renderData.RenderingResourceHandle, renderData.VertexData, renderData.IndexData, nullptr, 0, 0);
}
Vertices.SetNum(0);
Indices.SetNum(0);
Uvs.SetNum(0);
Colors.SetNum(0);
Colors2.SetNum(0);
Idx++;
}
FVector2D SSpineWidget::ComputeDesiredSize(float X) const {
if (widget && widget->skeleton && widget->Atlas) {
return FVector2D(boundsSize.X, boundsSize.Y);
} else {
return FVector2D(256, 256);
}
}
void SSpineWidget::UpdateMesh(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, Skeleton *Skeleton) {
TArray<FVector> vertices;
TArray<int32> indices;
TArray<FVector2D> uvs;
TArray<FColor> colors;
TArray<FVector> darkColors;
int idx = 0;
int meshSection = 0;
UMaterialInstanceDynamic *lastMaterial = nullptr;
SkeletonClipping &clipper = widget->clipper;
Vector<float> &worldVertices = widget->worldVertices;
float depthOffset = 0;
unsigned short quadIndices[] = {0, 1, 2, 0, 2, 3};
for (int i = 0; i < (int) Skeleton->getSlots().size(); ++i) {
Vector<float> *attachmentVertices = &worldVertices;
unsigned short *attachmentIndices = nullptr;
int numVertices;
int numIndices;
AtlasRegion *attachmentAtlasRegion = nullptr;
Color attachmentColor;
attachmentColor.set(1, 1, 1, 1);
float *attachmentUvs = nullptr;
Slot *slot = Skeleton->getDrawOrder()[i];
if (!slot->getBone().isActive()) {
clipper.clipEnd(*slot);
continue;
}
Attachment *attachment = slot->getAttachment();
if (!attachment) {
clipper.clipEnd(*slot);
continue;
}
if (!attachment->getRTTI().isExactly(RegionAttachment::rtti) && !attachment->getRTTI().isExactly(MeshAttachment::rtti) && !attachment->getRTTI().isExactly(ClippingAttachment::rtti)) {
clipper.clipEnd(*slot);
continue;
}
if (attachment->getRTTI().isExactly(RegionAttachment::rtti)) {
RegionAttachment *regionAttachment = (RegionAttachment *) attachment;
attachmentColor.set(regionAttachment->getColor());
attachmentVertices->setSize(8, 0);
regionAttachment->computeWorldVertices(*slot, *attachmentVertices, 0, 2);
attachmentAtlasRegion = (AtlasRegion *) regionAttachment->getRegion();
attachmentIndices = quadIndices;
attachmentUvs = regionAttachment->getUVs().buffer();
numVertices = 4;
numIndices = 6;
} else if (attachment->getRTTI().isExactly(MeshAttachment::rtti)) {
MeshAttachment *mesh = (MeshAttachment *) attachment;
attachmentColor.set(mesh->getColor());
attachmentVertices->setSize(mesh->getWorldVerticesLength(), 0);
mesh->computeWorldVertices(*slot, 0, mesh->getWorldVerticesLength(), attachmentVertices->buffer(), 0, 2);
attachmentAtlasRegion = (AtlasRegion *) mesh->getRegion();
attachmentIndices = mesh->getTriangles().buffer();
attachmentUvs = mesh->getUVs().buffer();
numVertices = mesh->getWorldVerticesLength() >> 1;
numIndices = mesh->getTriangles().size();
} else /* clipping */ {
ClippingAttachment *clip = (ClippingAttachment *) attachment;
clipper.clipStart(*slot, clip);
continue;
}
// if the user switches the atlas data while not having switched
// to the correct skeleton data yet, we won't find any regions.
// ignore regions for which we can't find a material
UMaterialInstanceDynamic *material = nullptr;
switch (slot->getData().getBlendMode()) {
case BlendMode_Normal:
if (!widget->pageToNormalBlendMaterial.Contains(attachmentAtlasRegion->page)) {
clipper.clipEnd(*slot);
continue;
}
material = widget->pageToNormalBlendMaterial[attachmentAtlasRegion->page];
break;
case BlendMode_Additive:
if (!widget->pageToAdditiveBlendMaterial.Contains(attachmentAtlasRegion->page)) {
clipper.clipEnd(*slot);
continue;
}
material = widget->pageToAdditiveBlendMaterial[attachmentAtlasRegion->page];
break;
case BlendMode_Multiply:
if (!widget->pageToMultiplyBlendMaterial.Contains(attachmentAtlasRegion->page)) {
clipper.clipEnd(*slot);
continue;
}
material = widget->pageToMultiplyBlendMaterial[attachmentAtlasRegion->page];
break;
case BlendMode_Screen:
if (!widget->pageToScreenBlendMaterial.Contains(attachmentAtlasRegion->page)) {
clipper.clipEnd(*slot);
continue;
}
material = widget->pageToScreenBlendMaterial[attachmentAtlasRegion->page];
break;
default:
if (!widget->pageToNormalBlendMaterial.Contains(attachmentAtlasRegion->page)) {
clipper.clipEnd(*slot);
continue;
}
material = widget->pageToNormalBlendMaterial[attachmentAtlasRegion->page];
}
if (clipper.isClipping()) {
clipper.clipTriangles(attachmentVertices->buffer(), attachmentIndices, numIndices, attachmentUvs, 2);
attachmentVertices = &clipper.getClippedVertices();
numVertices = clipper.getClippedVertices().size() >> 1;
attachmentIndices = clipper.getClippedTriangles().buffer();
numIndices = clipper.getClippedTriangles().size();
attachmentUvs = clipper.getClippedUVs().buffer();
if (clipper.getClippedTriangles().size() == 0) {
clipper.clipEnd(*slot);
continue;
}
}
if (lastMaterial != material) {
Flush(LayerId, OutDrawElements, AllottedGeometry, meshSection, vertices, indices, uvs, colors, darkColors, lastMaterial);
lastMaterial = material;
idx = 0;
}
uint8 r = static_cast<uint8>(Skeleton->getColor().r * slot->getColor().r * attachmentColor.r * 255);
uint8 g = static_cast<uint8>(Skeleton->getColor().g * slot->getColor().g * attachmentColor.g * 255);
uint8 b = static_cast<uint8>(Skeleton->getColor().b * slot->getColor().b * attachmentColor.b * 255);
uint8 a = static_cast<uint8>(Skeleton->getColor().a * slot->getColor().a * attachmentColor.a * 255);
float dr = slot->hasDarkColor() ? slot->getDarkColor().r : 0.0f;
float dg = slot->hasDarkColor() ? slot->getDarkColor().g : 0.0f;
float db = slot->hasDarkColor() ? slot->getDarkColor().b : 0.0f;
float *verticesPtr = attachmentVertices->buffer();
for (int j = 0; j < numVertices << 1; j += 2) {
colors.Add(FColor(r, g, b, a));
darkColors.Add(FVector(dr, dg, db));
vertices.Add(FVector(verticesPtr[j], -verticesPtr[j + 1], depthOffset));
uvs.Add(FVector2D(attachmentUvs[j], attachmentUvs[j + 1]));
}
for (int j = 0; j < numIndices; j++) {
indices.Add(idx + attachmentIndices[j]);
}
idx += numVertices;
depthOffset += widget->DepthOffset;
clipper.clipEnd(*slot);
}
Flush(LayerId, OutDrawElements, AllottedGeometry, meshSection, vertices, indices, uvs, colors, darkColors, lastMaterial);
clipper.clipEnd();
}

View File

@ -0,0 +1,124 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineAtlasAsset.h"
#include "spine/spine.h"
#include <string.h>
#include <string>
#include "EditorFramework/AssetImportData.h"
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
#if WITH_EDITORONLY_DATA
void USpineAtlasAsset::SetAtlasFileName(const FName &AtlasFileName) {
importData->UpdateFilenameOnly(AtlasFileName.ToString());
TArray<FString> files;
importData->ExtractFilenames(files);
if (files.Num() > 0)
atlasFileName = FName(*files[0]);
}
void USpineAtlasAsset::PostInitProperties() {
if (!HasAnyFlags(RF_ClassDefaultObject))
importData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
Super::PostInitProperties();
}
void USpineAtlasAsset::Serialize(FArchive &Ar) {
Super::Serialize(Ar);
#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 27
if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
#else
if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
#endif
importData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
}
#endif
FName USpineAtlasAsset::GetAtlasFileName() const {
#if WITH_EDITORONLY_DATA
TArray<FString> files;
if (importData)
importData->ExtractFilenames(files);
if (files.Num() > 0)
return FName(*files[0]);
else
return atlasFileName;
#else
return atlasFileName;
#endif
}
void USpineAtlasAsset::SetRawData(const FString &RawData) {
this->rawData = RawData;
if (atlas) {
delete atlas;
atlas = nullptr;
}
}
void USpineAtlasAsset::BeginDestroy() {
if (atlas) {
delete atlas;
atlas = nullptr;
}
Super::BeginDestroy();
}
class UETextureLoader : public TextureLoader {
void load(AtlasPage &page, const String &path) {
page.texture = (void *) (uintptr_t) page.index;
}
void unload(void *texture) {
}
};
UETextureLoader _spineUETextureLoader;
Atlas *USpineAtlasAsset::GetAtlas() {
if (!atlas) {
if (atlas) {
delete atlas;
atlas = nullptr;
}
std::string t = TCHAR_TO_UTF8(*rawData);
atlas = new (__FILE__, __LINE__)
Atlas(t.c_str(), strlen(t.c_str()), "", &_spineUETextureLoader);
}
return this->atlas;
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,67 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineBoneDriverComponent.h"
#include "SpineSkeletonComponent.h"
#include "GameFramework/Actor.h"
USpineBoneDriverComponent::USpineBoneDriverComponent() {
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
}
void USpineBoneDriverComponent::BeginPlay() {
Super::BeginPlay();
}
void USpineBoneDriverComponent::BeforeUpdateWorldTransform(USpineSkeletonComponent *skeleton) {
if (skeleton == lastBoundComponent && skeleton) {
if (UseComponentTransform) {
skeleton->SetBoneWorldPosition(BoneName, GetComponentLocation());
} else {
AActor *owner = GetOwner();
if (owner) skeleton->SetBoneWorldPosition(BoneName, owner->GetActorLocation());
}
}
}
void USpineBoneDriverComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (Target) {
USpineSkeletonComponent *skeleton = static_cast<USpineSkeletonComponent *>(Target->GetComponentByClass(USpineSkeletonComponent::StaticClass()));
if (skeleton != lastBoundComponent && skeleton) {
// if (lastBoundComponent) lastBoundComponent->BeforeUpdateWorldTransform.RemoveAll(this);
if (!skeleton->BeforeUpdateWorldTransform.GetAllObjects().Contains(this))
skeleton->BeforeUpdateWorldTransform.AddDynamic(this, &USpineBoneDriverComponent::BeforeUpdateWorldTransform);
lastBoundComponent = skeleton;
}
}
}

View File

@ -0,0 +1,65 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineBoneFollowerComponent.h"
#include "SpineSkeletonComponent.h"
#include "GameFramework/Actor.h"
USpineBoneFollowerComponent::USpineBoneFollowerComponent() {
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
}
void USpineBoneFollowerComponent::BeginPlay() {
Super::BeginPlay();
}
void USpineBoneFollowerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (Target) {
USpineSkeletonComponent *skeleton = static_cast<USpineSkeletonComponent *>(Target->GetComponentByClass(USpineSkeletonComponent::StaticClass()));
if (skeleton) {
FTransform transform = skeleton->GetBoneWorldTransform(BoneName);
if (UseComponentTransform) {
if (UsePosition) SetWorldLocation(transform.GetLocation());
if (UseRotation) SetWorldRotation(transform.GetRotation());
if (UseScale) SetWorldScale3D(transform.GetScale3D());
} else {
AActor *owner = GetOwner();
if (owner) {
if (UsePosition) owner->SetActorLocation(transform.GetLocation());
if (UseRotation) owner->SetActorRotation(transform.GetRotation());
if (UseScale) owner->SetActorScale3D(transform.GetScale3D());
}
}
}
}
}

View File

@ -0,0 +1,74 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpinePlugin.h"
#include "spine/Extension.h"
DEFINE_LOG_CATEGORY(SpineLog);
class FSpinePlugin : public SpinePlugin {
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
IMPLEMENT_MODULE(FSpinePlugin, SpinePlugin)
void FSpinePlugin::StartupModule() {
}
void FSpinePlugin::ShutdownModule() {}
class Ue4Extension : public spine::DefaultSpineExtension {
public:
Ue4Extension() : spine::DefaultSpineExtension() {}
virtual ~Ue4Extension() {}
virtual void *_alloc(size_t size, const char *file, int line) {
return FMemory::Malloc(size);
}
virtual void *_calloc(size_t size, const char *file, int line) {
void *result = FMemory::Malloc(size);
FMemory::Memset(result, 0, size);
return result;
}
virtual void *_realloc(void *ptr, size_t size, const char *file, int line) {
return FMemory::Realloc(ptr, size);
}
virtual void _free(void *mem, const char *file, int line) {
FMemory::Free(mem);
}
};
spine::SpineExtension *spine::getDefaultExtension() {
return new Ue4Extension();
}

View File

@ -0,0 +1,314 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineSkeletonAnimationComponent.h"
#include "SpineAtlasAsset.h"
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
void UTrackEntry::SetTrackEntry(TrackEntry *trackEntry) {
if (entry) entry->setRendererObject(nullptr);
this->entry = trackEntry;
if (entry) entry->setRendererObject((void *) this);
}
void callback(AnimationState *state, spine::EventType type, TrackEntry *entry, Event *event) {
USpineSkeletonAnimationComponent *component = (USpineSkeletonAnimationComponent *) state->getRendererObject();
if (entry->getRendererObject()) {
UTrackEntry *uEntry = (UTrackEntry *) entry->getRendererObject();
if (type == EventType_Start) {
component->AnimationStart.Broadcast(uEntry);
uEntry->AnimationStart.Broadcast(uEntry);
} else if (type == EventType_Interrupt) {
component->AnimationInterrupt.Broadcast(uEntry);
uEntry->AnimationInterrupt.Broadcast(uEntry);
} else if (type == EventType_Event) {
FSpineEvent evt;
evt.SetEvent(event);
component->AnimationEvent.Broadcast(uEntry, evt);
uEntry->AnimationEvent.Broadcast(uEntry, evt);
} else if (type == EventType_Complete) {
component->AnimationComplete.Broadcast(uEntry);
uEntry->AnimationComplete.Broadcast(uEntry);
} else if (type == EventType_End) {
component->AnimationEnd.Broadcast(uEntry);
uEntry->AnimationEnd.Broadcast(uEntry);
} else if (type == EventType_Dispose) {
component->AnimationDispose.Broadcast(uEntry);
uEntry->AnimationDispose.Broadcast(uEntry);
uEntry->SetTrackEntry(nullptr);
component->GCTrackEntry(uEntry);
}
}
}
USpineSkeletonAnimationComponent::USpineSkeletonAnimationComponent() {
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
bAutoPlaying = true;
physicsTimeScale = 1;
}
void USpineSkeletonAnimationComponent::BeginPlay() {
Super::BeginPlay();
for (UTrackEntry *entry : trackEntries) {
if (entry && entry->GetTrackEntry()) {
entry->GetTrackEntry()->setRendererObject(nullptr);
}
}
trackEntries.Empty();
}
void UTrackEntry::BeginDestroy() {
Super::BeginDestroy();
}
void USpineSkeletonAnimationComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
Super::Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
InternalTick(DeltaTime, true, TickType == LEVELTICK_ViewportsOnly);
}
void USpineSkeletonAnimationComponent::InternalTick(float DeltaTime, bool CallDelegates, bool Preview) {
CheckState();
if (state && bAutoPlaying) {
if (Preview) {
if (lastPreviewAnimation != PreviewAnimation) {
if (PreviewAnimation != "") SetAnimation(0, PreviewAnimation, true);
else
SetEmptyAnimation(0, 0);
lastPreviewAnimation = PreviewAnimation;
}
if (lastPreviewSkin != PreviewSkin) {
if (PreviewSkin != "") SetSkin(PreviewSkin);
else
SetSkin("default");
lastPreviewSkin = PreviewSkin;
}
}
state->update(DeltaTime);
state->apply(*skeleton);
if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
skeleton->update(physicsTimeScale * DeltaTime);
skeleton->updateWorldTransform(Physics_Update);
if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
}
}
void USpineSkeletonAnimationComponent::CheckState() {
bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
if (!needsUpdate) {
// Are we doing a re-import? Then check if the underlying spine-cpp data
// has changed.
if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
spine::Atlas *atlas = Atlas->GetAtlas();
if (lastSpineAtlas != atlas) {
needsUpdate = true;
}
if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
needsUpdate = true;
}
}
}
if (needsUpdate) {
DisposeState();
if (Atlas && SkeletonData) {
spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
if (data) {
skeleton = new (__FILE__, __LINE__) Skeleton(data);
AnimationStateData *stateData = SkeletonData->GetAnimationStateData(Atlas->GetAtlas());
state = new (__FILE__, __LINE__) AnimationState(stateData);
state->setRendererObject((void *) this);
state->setListener(callback);
trackEntries.Empty();
}
}
lastAtlas = Atlas;
lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
lastData = SkeletonData;
}
}
void USpineSkeletonAnimationComponent::DisposeState() {
if (state) {
delete state;
state = nullptr;
}
if (skeleton) {
delete skeleton;
skeleton = nullptr;
}
trackEntries.Empty();
}
void USpineSkeletonAnimationComponent::FinishDestroy() {
DisposeState();
Super::FinishDestroy();
}
void USpineSkeletonAnimationComponent::SetAutoPlay(bool bInAutoPlays) {
bAutoPlaying = bInAutoPlays;
}
void USpineSkeletonAnimationComponent::SetPlaybackTime(float InPlaybackTime, bool bCallDelegates) {
CheckState();
if (state && state->getCurrent(0)) {
spine::Animation *CurrentAnimation = state->getCurrent(0)->getAnimation();
const float CurrentTime = state->getCurrent(0)->getTrackTime();
InPlaybackTime = FMath::Clamp(InPlaybackTime, 0.0f, CurrentAnimation->getDuration());
const float DeltaTime = InPlaybackTime - CurrentTime;
state->update(DeltaTime);
state->apply(*skeleton);
//Call delegates and perform the world transform
if (bCallDelegates) {
BeforeUpdateWorldTransform.Broadcast(this);
}
skeleton->updateWorldTransform(Physics_Update);
if (bCallDelegates) {
AfterUpdateWorldTransform.Broadcast(this);
}
}
}
void USpineSkeletonAnimationComponent::SetTimeScale(float timeScale) {
CheckState();
if (state) state->setTimeScale(timeScale);
}
float USpineSkeletonAnimationComponent::GetTimeScale() {
CheckState();
if (state) return state->getTimeScale();
return 1;
}
UTrackEntry *USpineSkeletonAnimationComponent::SetAnimation(int trackIndex, FString animationName, bool loop) {
CheckState();
if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
state->disableQueue();
TrackEntry *entry = state->setAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop);
state->enableQueue();
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineSkeletonAnimationComponent::AddAnimation(int trackIndex, FString animationName, bool loop, float delay) {
CheckState();
if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
state->disableQueue();
TrackEntry *entry = state->addAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop, delay);
state->enableQueue();
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineSkeletonAnimationComponent::SetEmptyAnimation(int trackIndex, float mixDuration) {
CheckState();
if (state) {
TrackEntry *entry = state->setEmptyAnimation(trackIndex, mixDuration);
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineSkeletonAnimationComponent::AddEmptyAnimation(int trackIndex, float mixDuration, float delay) {
CheckState();
if (state) {
TrackEntry *entry = state->addEmptyAnimation(trackIndex, mixDuration, delay);
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineSkeletonAnimationComponent::GetCurrent(int trackIndex) {
CheckState();
if (state && state->getCurrent(trackIndex)) {
TrackEntry *entry = state->getCurrent(trackIndex);
if (entry->getRendererObject()) {
return (UTrackEntry *) entry->getRendererObject();
} else {
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
}
} else
return NewObject<UTrackEntry>();
}
void USpineSkeletonAnimationComponent::ClearTracks() {
CheckState();
if (state) {
state->clearTracks();
}
}
void USpineSkeletonAnimationComponent::ClearTrack(int trackIndex) {
CheckState();
if (state) {
state->clearTrack(trackIndex);
}
}
void USpineSkeletonAnimationComponent::SetPhysicsTimeScale(float scale) {
physicsTimeScale = scale;
}
float USpineSkeletonAnimationComponent::GetPhysicsTimeScale() {
return physicsTimeScale;
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,376 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineSkeletonComponent.h"
#include "SpineSkeletonRendererComponent.h"
#include "SpineAtlasAsset.h"
#include "spine/spine.h"
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
USpineSkeletonComponent::USpineSkeletonComponent() {
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
}
bool USpineSkeletonComponent::SetSkins(UPARAM(ref) TArray<FString> &SkinNames) {
CheckState();
if (skeleton) {
spine::Skin *newSkin = new spine::Skin("__spine-ue3_custom_skin");
for (auto &skinName : SkinNames) {
spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
if (!skin) {
delete newSkin;
return false;
}
newSkin->addSkin(skin);
}
skeleton->setSkin(newSkin);
if (customSkin != nullptr) {
delete customSkin;
}
customSkin = newSkin;
return true;
} else
return false;
}
bool USpineSkeletonComponent::SetSkin(const FString skinName) {
CheckState();
if (skeleton) {
Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
if (!skin) return false;
skeleton->setSkin(skin);
return true;
} else
return false;
}
void USpineSkeletonComponent::GetSkins(TArray<FString> &Skins) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getData()->getSkins().size(); i < n; i++) {
Skins.Add(skeleton->getData()->getSkins()[i]->getName().buffer());
}
}
}
bool USpineSkeletonComponent::HasSkin(const FString skinName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName)) != nullptr;
}
return false;
}
bool USpineSkeletonComponent::SetAttachment(const FString slotName, const FString attachmentName) {
CheckState();
if (skeleton) {
if (attachmentName.IsEmpty()) {
skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), NULL);
return true;
}
if (!skeleton->getAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName))) return false;
skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName));
return true;
}
return false;
}
FTransform USpineSkeletonComponent::GetBoneWorldTransform(const FString &BoneName) {
CheckState();
if (skeleton) {
Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
if (!bone) return FTransform();
// Need to fetch the renderer component to get world transform of actor plus
// offset by renderer component and its parent component(s). If no renderer
// component is found, this components owner's transform is used as a fallback
FTransform baseTransform;
AActor *owner = GetOwner();
if (owner) {
USpineSkeletonRendererComponent *rendererComponent = static_cast<USpineSkeletonRendererComponent *>(owner->GetComponentByClass(USpineSkeletonRendererComponent::StaticClass()));
if (rendererComponent) baseTransform = rendererComponent->GetComponentTransform();
else
baseTransform = owner->GetActorTransform();
}
FVector position(bone->getWorldX(), 0, bone->getWorldY());
FMatrix localTransform;
localTransform.SetIdentity();
localTransform.SetAxis(2, FVector(bone->getA(), 0, bone->getC()));
localTransform.SetAxis(0, FVector(bone->getB(), 0, bone->getD()));
localTransform.SetOrigin(FVector(bone->getWorldX(), 0, bone->getWorldY()));
localTransform = localTransform * baseTransform.ToMatrixWithScale();
FTransform result;
result.SetFromMatrix(localTransform);
return result;
}
return FTransform();
}
void USpineSkeletonComponent::SetBoneWorldPosition(const FString &BoneName, const FVector &position) {
CheckState();
if (skeleton) {
Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
if (!bone) return;
// Need to fetch the renderer component to get world transform of actor plus
// offset by renderer component and its parent component(s). If no renderer
// component is found, this components owner's transform is used as a fallback
FTransform baseTransform;
AActor *owner = GetOwner();
if (owner) {
USpineSkeletonRendererComponent *rendererComponent = static_cast<USpineSkeletonRendererComponent *>(owner->GetComponentByClass(USpineSkeletonRendererComponent::StaticClass()));
if (rendererComponent) baseTransform = rendererComponent->GetComponentTransform();
else
baseTransform = owner->GetActorTransform();
}
baseTransform = baseTransform.Inverse();
FVector localPosition = baseTransform.TransformPosition(position);
float localX = 0, localY = 0;
if (bone->getParent()) {
bone->getParent()->worldToLocal(localPosition.X, localPosition.Z, localX, localY);
} else {
bone->worldToLocal(localPosition.X, localPosition.Z, localX, localY);
}
bone->setX(localX);
bone->setY(localY);
}
}
void USpineSkeletonComponent::UpdateWorldTransform() {
CheckState();
if (skeleton) {
skeleton->updateWorldTransform(Physics_Update);
}
}
void USpineSkeletonComponent::SetToSetupPose() {
CheckState();
if (skeleton) skeleton->setToSetupPose();
}
void USpineSkeletonComponent::SetBonesToSetupPose() {
CheckState();
if (skeleton) skeleton->setBonesToSetupPose();
}
void USpineSkeletonComponent::SetSlotsToSetupPose() {
CheckState();
if (skeleton) skeleton->setSlotsToSetupPose();
}
void USpineSkeletonComponent::SetScaleX(float scaleX) {
CheckState();
if (skeleton) skeleton->setScaleX(scaleX);
}
float USpineSkeletonComponent::GetScaleX() {
CheckState();
if (skeleton) return skeleton->getScaleX();
return 1;
}
void USpineSkeletonComponent::SetScaleY(float scaleY) {
CheckState();
if (skeleton) skeleton->setScaleY(scaleY);
}
float USpineSkeletonComponent::GetScaleY() {
CheckState();
if (skeleton) return skeleton->getScaleY();
return 1;
}
void USpineSkeletonComponent::GetBones(TArray<FString> &Bones) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getBones().size(); i < n; i++) {
Bones.Add(skeleton->getBones()[i]->getData().getName().buffer());
}
}
}
bool USpineSkeletonComponent::HasBone(const FString BoneName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findBone(TCHAR_TO_UTF8(*BoneName)) != nullptr;
}
return false;
}
void USpineSkeletonComponent::GetSlots(TArray<FString> &Slots) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getSlots().size(); i < n; i++) {
Slots.Add(skeleton->getSlots()[i]->getData().getName().buffer());
}
}
}
bool USpineSkeletonComponent::HasSlot(const FString SlotName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findSlot(TCHAR_TO_UTF8(*SlotName)) != nullptr;
}
return false;
}
void USpineSkeletonComponent::SetSlotColor(const FString SlotName, const FColor color) {
CheckState();
if (skeleton) {
Slot *slot = skeleton->findSlot(TCHAR_TO_UTF8(*SlotName));
if (slot) {
slot->getColor().set(color.R / 255.f, color.G / 255.f, color.B / 255.f, color.A / 255.f);
}
}
}
void USpineSkeletonComponent::GetAnimations(TArray<FString> &Animations) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getData()->getAnimations().size(); i < n; i++) {
Animations.Add(skeleton->getData()->getAnimations()[i]->getName().buffer());
}
}
}
bool USpineSkeletonComponent::HasAnimation(FString AnimationName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName)) != nullptr;
}
return false;
}
float USpineSkeletonComponent::GetAnimationDuration(FString AnimationName) {
CheckState();
if (skeleton) {
Animation *animation = skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName));
if (animation == nullptr) return 0;
else
return animation->getDuration();
}
return 0;
}
void USpineSkeletonComponent::PhysicsTranslate(float x, float y) {
CheckState();
if (skeleton) {
skeleton->physicsTranslate(x, y);
}
}
void USpineSkeletonComponent::PhysicsRotate(float x, float y, float degrees) {
CheckState();
if (skeleton) {
skeleton->physicsRotate(x, y, degrees);
}
}
void USpineSkeletonComponent::ResetPhysicsConstraints() {
CheckState();
if (skeleton) {
Vector<PhysicsConstraint *> &constraints = skeleton->getPhysicsConstraints();
for (int i = 0, n = (int) constraints.size(); i < n; i++) {
constraints[i]->reset();
}
}
}
void USpineSkeletonComponent::BeginPlay() {
Super::BeginPlay();
}
void USpineSkeletonComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
InternalTick(DeltaTime);
}
void USpineSkeletonComponent::InternalTick(float DeltaTime, bool CallDelegates, bool Preview) {
CheckState();
if (skeleton) {
if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
skeleton->updateWorldTransform(Physics_Update);
if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
}
}
void USpineSkeletonComponent::CheckState() {
bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
if (!needsUpdate) {
// Are we doing a re-import? Then check if the underlying spine-cpp data
// has changed.
if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
spine::Atlas *atlas = Atlas->GetAtlas();
if (lastSpineAtlas != atlas) {
needsUpdate = true;
}
if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
needsUpdate = true;
}
}
}
if (needsUpdate) {
DisposeState();
if (Atlas && SkeletonData) {
spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
skeleton = new (__FILE__, __LINE__) Skeleton(data);
}
lastAtlas = Atlas;
lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
lastData = SkeletonData;
}
}
void USpineSkeletonComponent::DisposeState() {
if (skeleton) {
delete skeleton;
skeleton = nullptr;
}
}
void USpineSkeletonComponent::FinishDestroy() {
DisposeState();
Super::FinishDestroy();
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,404 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineSkeletonDataAsset.h"
#include "EditorFramework/AssetImportData.h"
#include "Runtime/Core/Public/Misc/MessageDialog.h"
#include "SpinePlugin.h"
#include "spine/Version.h"
#include "spine/spine.h"
#include <string>
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
FName USpineSkeletonDataAsset::GetSkeletonDataFileName() const {
#if WITH_EDITORONLY_DATA
TArray<FString> files;
if (importData)
importData->ExtractFilenames(files);
if (files.Num() > 0)
return FName(*files[0]);
else
return skeletonDataFileName;
#else
return skeletonDataFileName;
#endif
}
#if WITH_EDITORONLY_DATA
void USpineSkeletonDataAsset::SetSkeletonDataFileName(
const FName &SkeletonDataFileName) {
importData->UpdateFilenameOnly(SkeletonDataFileName.ToString());
TArray<FString> files;
importData->ExtractFilenames(files);
if (files.Num() > 0)
this->skeletonDataFileName = FName(*files[0]);
}
void USpineSkeletonDataAsset::PostInitProperties() {
if (!HasAnyFlags(RF_ClassDefaultObject))
importData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
Super::PostInitProperties();
}
#if ((ENGINE_MAJOR_VERSION >= 5) && (ENGINE_MINOR_VERSION >= 4))
void USpineSkeletonDataAsset::GetAssetRegistryTags(FAssetRegistryTagsContext Context) const {
if (importData) {
Context.AddTag(FAssetRegistryTag(SourceFileTagName(), importData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden));
}
Super::GetAssetRegistryTags(Context);
}
#else
void USpineSkeletonDataAsset::GetAssetRegistryTags(
TArray<FAssetRegistryTag> &OutTags) const {
if (importData) {
OutTags.Add(FAssetRegistryTag(SourceFileTagName(),
importData->GetSourceData().ToJson(),
FAssetRegistryTag::TT_Hidden));
}
Super::GetAssetRegistryTags(OutTags);
}
#endif
void USpineSkeletonDataAsset::Serialize(FArchive &Ar) {
Super::Serialize(Ar);
#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 27
if (Ar.IsLoading() && Ar.UE4Ver() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
#else
if (Ar.IsLoading() && Ar.UEVer() < VER_UE4_ASSET_IMPORT_DATA_AS_JSON && !importData)
#endif
importData = NewObject<UAssetImportData>(this, TEXT("AssetImportData"));
LoadInfo();
}
#endif
void USpineSkeletonDataAsset::ClearNativeData() {
for (auto &pair : atlasToNativeData) {
if (pair.Value.skeletonData)
delete pair.Value.skeletonData;
if (pair.Value.animationStateData)
delete pair.Value.animationStateData;
}
atlasToNativeData.Empty();
}
void USpineSkeletonDataAsset::BeginDestroy() {
ClearNativeData();
Super::BeginDestroy();
}
class SP_API NullAttachmentLoader : public AttachmentLoader {
public:
virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name,
const String &path,
Sequence *sequence) {
return new (__FILE__, __LINE__) RegionAttachment(name);
}
virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name,
const String &path,
Sequence *sequence) {
return new (__FILE__, __LINE__) MeshAttachment(name);
}
virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin,
const String &name) {
return new (__FILE__, __LINE__) BoundingBoxAttachment(name);
}
virtual PathAttachment *newPathAttachment(Skin &skin, const String &name) {
return new (__FILE__, __LINE__) PathAttachment(name);
}
virtual PointAttachment *newPointAttachment(Skin &skin, const String &name) {
return new (__FILE__, __LINE__) PointAttachment(name);
}
virtual ClippingAttachment *newClippingAttachment(Skin &skin,
const String &name) {
return new (__FILE__, __LINE__) ClippingAttachment(name);
}
virtual void configureAttachment(Attachment *attachment) {}
};
void USpineSkeletonDataAsset::SetRawData(TArray<uint8> &Data) {
this->rawData.Empty();
this->rawData.Append(Data);
ClearNativeData();
LoadInfo();
}
static bool checkVersion(const char *version) {
if (!version)
return false;
char *result = (char *) (strstr(version, SPINE_VERSION_STRING) - version);
return result == 0;
}
static bool checkJson(const char *jsonData) {
Json json(jsonData);
Json *skeleton = Json::getItem(&json, "skeleton");
if (!skeleton)
return false;
const char *version = Json::getString(skeleton, "spine", 0);
if (!version)
return false;
return checkVersion(version);
}
struct BinaryInput {
const unsigned char *cursor;
const unsigned char *end;
};
static unsigned char readByte(BinaryInput *input) { return *input->cursor++; }
static int readVarint(BinaryInput *input, bool optimizePositive) {
unsigned char b = readByte(input);
int value = b & 0x7F;
if (b & 0x80) {
b = readByte(input);
value |= (b & 0x7F) << 7;
if (b & 0x80) {
b = readByte(input);
value |= (b & 0x7F) << 14;
if (b & 0x80) {
b = readByte(input);
value |= (b & 0x7F) << 21;
if (b & 0x80)
value |= (readByte(input) & 0x7F) << 28;
}
}
}
if (!optimizePositive) {
value = (((unsigned int) value >> 1) ^ -(value & 1));
}
return value;
}
static char *readString(BinaryInput *input) {
int length = readVarint(input, true);
char *string;
if (length == 0) {
return NULL;
}
string = SpineExtension::alloc<char>(length, __FILE__, __LINE__);
memcpy(string, input->cursor, length - 1);
input->cursor += length - 1;
string[length - 1] = '\0';
return string;
}
static bool checkBinary(const char *binaryData, int length) {
BinaryInput input;
input.cursor = (const unsigned char *) binaryData;
input.end = (const unsigned char *) binaryData + length;
// Skip hash
input.cursor += 8;
char *version = readString(&input);
bool result = checkVersion(version);
SpineExtension::free(version, __FILE__, __LINE__);
return result;
}
void USpineSkeletonDataAsset::LoadInfo() {
#if WITH_EDITORONLY_DATA
int dataLen = rawData.Num();
if (dataLen == 0)
return;
NullAttachmentLoader loader;
SkeletonData *skeletonData = nullptr;
if (skeletonDataFileName.GetPlainNameString().Contains(TEXT(".json"))) {
SkeletonJson *json = new (__FILE__, __LINE__) SkeletonJson(&loader);
if (checkJson((const char *) rawData.GetData()))
skeletonData = json->readSkeletonData((const char *) rawData.GetData());
if (!skeletonData) {
FMessageDialog::Debugf(FText::FromString(
FString("Couldn't load skeleton data and/or atlas. Please ensure the "
"version of your exported data matches your runtime "
"version.\n\n") +
skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
UTF8_TO_TCHAR(json->getError().buffer())));
UE_LOG(SpineLog, Error, TEXT("Couldn't load skeleton data and atlas: %s"),
UTF8_TO_TCHAR(json->getError().buffer()));
}
delete json;
} else {
SkeletonBinary *binary = new (__FILE__, __LINE__) SkeletonBinary(&loader);
if (checkBinary((const char *) rawData.GetData(), (int) rawData.Num()))
skeletonData = binary->readSkeletonData(
(const unsigned char *) rawData.GetData(), (int) rawData.Num());
if (!skeletonData) {
FMessageDialog::Debugf(FText::FromString(
FString("Couldn't load skeleton data and/or atlas. Please ensure the "
"version of your exported data matches your runtime "
"version.\n\n") +
skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
UTF8_TO_TCHAR(binary->getError().buffer())));
UE_LOG(SpineLog, Error, TEXT("Couldn't load skeleton data and atlas: %s"),
UTF8_TO_TCHAR(binary->getError().buffer()));
}
delete binary;
}
if (skeletonData) {
Bones.Empty();
for (int i = 0; i < skeletonData->getBones().size(); i++)
Bones.Add(UTF8_TO_TCHAR(skeletonData->getBones()[i]->getName().buffer()));
Skins.Empty();
for (int i = 0; i < skeletonData->getSkins().size(); i++)
Skins.Add(UTF8_TO_TCHAR(skeletonData->getSkins()[i]->getName().buffer()));
Slots.Empty();
for (int i = 0; i < skeletonData->getSlots().size(); i++)
Slots.Add(UTF8_TO_TCHAR(skeletonData->getSlots()[i]->getName().buffer()));
Animations.Empty();
for (int i = 0; i < skeletonData->getAnimations().size(); i++)
Animations.Add(
UTF8_TO_TCHAR(skeletonData->getAnimations()[i]->getName().buffer()));
Events.Empty();
for (int i = 0; i < skeletonData->getEvents().size(); i++)
Events.Add(
UTF8_TO_TCHAR(skeletonData->getEvents()[i]->getName().buffer()));
delete skeletonData;
}
#endif
}
SkeletonData *USpineSkeletonDataAsset::GetSkeletonData(Atlas *Atlas) {
SkeletonData *skeletonData = nullptr;
AnimationStateData *animationStateData = nullptr;
if (atlasToNativeData.Contains(Atlas)) {
skeletonData = atlasToNativeData[Atlas].skeletonData;
animationStateData = atlasToNativeData[Atlas].animationStateData;
}
if (!skeletonData) {
int dataLen = rawData.Num();
if (skeletonDataFileName.GetPlainNameString().Contains(TEXT(".json"))) {
SkeletonJson *json = new (__FILE__, __LINE__) SkeletonJson(Atlas);
if (checkJson((const char *) rawData.GetData()))
skeletonData = json->readSkeletonData((const char *) rawData.GetData());
if (!skeletonData) {
#if WITH_EDITORONLY_DATA
FMessageDialog::Debugf(FText::FromString(
FString("Couldn't load skeleton data and/or atlas. Please ensure "
"the version of your exported data matches your runtime "
"version.\n\n") +
skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
UTF8_TO_TCHAR(json->getError().buffer())));
#endif
UE_LOG(SpineLog, Error,
TEXT("Couldn't load skeleton data and atlas: %s"),
UTF8_TO_TCHAR(json->getError().buffer()));
}
delete json;
} else {
SkeletonBinary *binary = new (__FILE__, __LINE__) SkeletonBinary(Atlas);
if (checkBinary((const char *) rawData.GetData(), (int) rawData.Num()))
skeletonData = binary->readSkeletonData(
(const unsigned char *) rawData.GetData(), (int) rawData.Num());
if (!skeletonData) {
#if WITH_EDITORONLY_DATA
FMessageDialog::Debugf(FText::FromString(
FString("Couldn't load skeleton data and/or atlas. Please ensure "
"the version of your exported data matches your runtime "
"version.\n\n") +
skeletonDataFileName.GetPlainNameString() + FString("\n\n") +
UTF8_TO_TCHAR(binary->getError().buffer())));
#endif
UE_LOG(SpineLog, Error,
TEXT("Couldn't load skeleton data and atlas: %s"),
UTF8_TO_TCHAR(binary->getError().buffer()));
}
delete binary;
}
if (skeletonData) {
animationStateData =
new (__FILE__, __LINE__) AnimationStateData(skeletonData);
SetMixes(animationStateData);
atlasToNativeData.Add(Atlas, {skeletonData, animationStateData});
}
}
return skeletonData;
}
void USpineSkeletonDataAsset::SetMixes(AnimationStateData *animationStateData) {
for (auto &data : MixData) {
if (!data.From.IsEmpty() && !data.To.IsEmpty()) {
std::string fromChar = TCHAR_TO_UTF8(*data.From);
std::string toChar = TCHAR_TO_UTF8(*data.To);
animationStateData->setMix(fromChar.c_str(), toChar.c_str(), data.Mix);
}
}
animationStateData->setDefaultMix(DefaultMix);
}
AnimationStateData *
USpineSkeletonDataAsset::GetAnimationStateData(Atlas *atlas) {
if (!atlasToNativeData.Contains(atlas))
return nullptr;
AnimationStateData *data = atlasToNativeData[atlas].animationStateData;
SetMixes(data);
return data;
}
void USpineSkeletonDataAsset::SetMix(const FString &from, const FString &to,
float mix) {
FSpineAnimationStateMixData data;
data.From = from;
data.To = to;
data.Mix = mix;
this->MixData.Add(data);
for (auto &pair : atlasToNativeData) {
SetMixes(pair.Value.animationStateData);
}
}
float USpineSkeletonDataAsset::GetMix(const FString &from, const FString &to) {
for (auto &data : MixData) {
if (data.From.Equals(from) && data.To.Equals(to))
return data.Mix;
}
return 0;
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,360 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineSkeletonRendererComponent.h"
#include "SpineAtlasAsset.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "spine/spine.h"
#include "UObject/ConstructorHelpers.h"
#if ENGINE_MAJOR_VERSION >= 5
#include "PhysicsEngine/BodySetup.h"
#endif
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
USpineSkeletonRendererComponent::USpineSkeletonRendererComponent(const FObjectInitializer &ObjectInitializer)
: UProceduralMeshComponent(ObjectInitializer) {
PrimaryComponentTick.bCanEverTick = true;
bTickInEditor = true;
bAutoActivate = true;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> NormalMaterialRef(TEXT("/SpinePlugin/SpineUnlitNormalMaterial"));
NormalBlendMaterial = NormalMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> AdditiveMaterialRef(TEXT("/SpinePlugin/SpineUnlitAdditiveMaterial"));
AdditiveBlendMaterial = AdditiveMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> MultiplyMaterialRef(TEXT("/SpinePlugin/SpineUnlitMultiplyMaterial"));
MultiplyBlendMaterial = MultiplyMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> ScreenMaterialRef(TEXT("/SpinePlugin/SpineUnlitScreenMaterial"));
ScreenBlendMaterial = ScreenMaterialRef.Object;
TextureParameterName = FName(TEXT("SpriteTexture"));
worldVertices.ensureCapacity(1024 * 2);
SetTickGroup(TG_EndPhysics);
}
void USpineSkeletonRendererComponent::FinishDestroy() {
Super::FinishDestroy();
}
void USpineSkeletonRendererComponent::BeginPlay() {
Super::BeginPlay();
}
void USpineSkeletonRendererComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
AActor *owner = GetOwner();
if (owner) {
UClass *skeletonClass = USpineSkeletonComponent::StaticClass();
USpineSkeletonComponent *skeletonComponent = Cast<USpineSkeletonComponent>(owner->GetComponentByClass(skeletonClass));
UpdateRenderer(skeletonComponent);
}
}
void USpineSkeletonRendererComponent::UpdateRenderer(USpineSkeletonComponent *component) {
if (component && !component->IsBeingDestroyed() && component->GetSkeleton() && component->Atlas) {
component->GetSkeleton()->getColor().set(Color.R, Color.G, Color.B, Color.A);
if (atlasNormalBlendMaterials.Num() != component->Atlas->atlasPages.Num()) {
atlasNormalBlendMaterials.SetNum(0);
atlasAdditiveBlendMaterials.SetNum(0);
atlasMultiplyBlendMaterials.SetNum(0);
atlasScreenBlendMaterials.SetNum(0);
for (int i = 0; i < component->Atlas->atlasPages.Num(); i++) {
AtlasPage *currPage = component->Atlas->GetAtlas()->getPages()[i];
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(NormalBlendMaterial, this);
material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
atlasNormalBlendMaterials.Add(material);
material = UMaterialInstanceDynamic::Create(AdditiveBlendMaterial, this);
material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
atlasAdditiveBlendMaterials.Add(material);
material = UMaterialInstanceDynamic::Create(MultiplyBlendMaterial, this);
material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
atlasMultiplyBlendMaterials.Add(material);
material = UMaterialInstanceDynamic::Create(ScreenBlendMaterial, this);
material->SetTextureParameterValue(TextureParameterName, component->Atlas->atlasPages[i]);
atlasScreenBlendMaterials.Add(material);
}
} else {
for (int i = 0; i < component->Atlas->atlasPages.Num(); i++) {
UTexture2D *texture = component->Atlas->atlasPages[i];
UpdateMaterial(texture, atlasNormalBlendMaterials[i], NormalBlendMaterial);
UpdateMaterial(texture, atlasAdditiveBlendMaterials[i], AdditiveBlendMaterial);
UpdateMaterial(texture, atlasMultiplyBlendMaterials[i], MultiplyBlendMaterial);
UpdateMaterial(texture, atlasScreenBlendMaterials[i], ScreenBlendMaterial);
}
}
UpdateMesh(component, component->GetSkeleton());
} else {
ClearAllMeshSections();
}
}
void USpineSkeletonRendererComponent::UpdateMaterial(UTexture2D *Texture, UMaterialInstanceDynamic *&CurrentInstance, UMaterialInterface *ParentMaterial) {
UTexture *oldTexture = nullptr;
if (!CurrentInstance || !CurrentInstance->GetTextureParameterValue(TextureParameterName, oldTexture) ||
oldTexture != Texture || CurrentInstance->Parent != ParentMaterial) {
UMaterialInstanceDynamic *material = UMaterialInstanceDynamic::Create(ParentMaterial, this);
material->SetTextureParameterValue(TextureParameterName, Texture);
CurrentInstance = material;
}
}
void USpineSkeletonRendererComponent::Flush(int &Idx, TArray<FVector> &Vertices, TArray<int32> &Indices, TArray<FVector> &Normals, TArray<FVector2D> &Uvs, TArray<FColor> &Colors, UMaterialInstanceDynamic *Material) {
if (Vertices.Num() == 0) return;
SetMaterial(Idx, Material);
bool bShouldCreateCollision = false;
if (bCreateCollision) {
UWorld *world = GetWorld();
if (world && world->IsGameWorld()) {
bShouldCreateCollision = true;
}
}
GetBodySetup()->bGenerateMirroredCollision = GetComponentScale().X < 0 || GetComponentScale().Y < 0 || GetComponentScale().Z < 0;
CreateMeshSection(Idx, Vertices, Indices, Normals, Uvs, Colors, TArray<FProcMeshTangent>(), bShouldCreateCollision);
Vertices.SetNum(0);
Indices.SetNum(0);
Normals.SetNum(0);
Uvs.SetNum(0);
Colors.SetNum(0);
Idx++;
}
void USpineSkeletonRendererComponent::UpdateMesh(USpineSkeletonComponent *component, Skeleton *Skeleton) {
vertices.Empty();
indices.Empty();
normals.Empty();
uvs.Empty();
colors.Empty();
int idx = 0;
int meshSection = 0;
UMaterialInstanceDynamic *lastMaterial = nullptr;
ClearAllMeshSections();
// Early out if skeleton is invisible
if (Skeleton->getColor().a == 0) return;
float depthOffset = 0;
unsigned short quadIndices[] = {0, 1, 2, 0, 2, 3};
for (size_t i = 0; i < Skeleton->getSlots().size(); ++i) {
Vector<float> *attachmentVertices = &worldVertices;
unsigned short *attachmentIndices = nullptr;
int numVertices;
int numIndices;
AtlasRegion *attachmentAtlasRegion = nullptr;
spine::Color attachmentColor;
attachmentColor.set(1, 1, 1, 1);
float *attachmentUvs = nullptr;
Slot *slot = Skeleton->getDrawOrder()[i];
Attachment *attachment = slot->getAttachment();
if (slot->getColor().a == 0 || !slot->getBone().isActive()) {
clipper.clipEnd(*slot);
continue;
}
if (!attachment) {
clipper.clipEnd(*slot);
continue;
}
if (!attachment->getRTTI().isExactly(RegionAttachment::rtti) && !attachment->getRTTI().isExactly(MeshAttachment::rtti) && !attachment->getRTTI().isExactly(ClippingAttachment::rtti)) {
clipper.clipEnd(*slot);
continue;
}
if (attachment->getRTTI().isExactly(RegionAttachment::rtti)) {
RegionAttachment *regionAttachment = (RegionAttachment *) attachment;
// Early out if region is invisible
if (regionAttachment->getColor().a == 0) {
clipper.clipEnd(*slot);
continue;
}
attachmentColor.set(regionAttachment->getColor());
attachmentVertices->setSize(8, 0);
regionAttachment->computeWorldVertices(*slot, *attachmentVertices, 0, 2);
attachmentAtlasRegion = (AtlasRegion *) regionAttachment->getRegion();
attachmentIndices = quadIndices;
attachmentUvs = regionAttachment->getUVs().buffer();
numVertices = 4;
numIndices = 6;
} else if (attachment->getRTTI().isExactly(MeshAttachment::rtti)) {
MeshAttachment *mesh = (MeshAttachment *) attachment;
// Early out if region is invisible
if (mesh->getColor().a == 0) {
clipper.clipEnd(*slot);
continue;
}
attachmentColor.set(mesh->getColor());
attachmentVertices->setSize(mesh->getWorldVerticesLength(), 0);
mesh->computeWorldVertices(*slot, 0, mesh->getWorldVerticesLength(), attachmentVertices->buffer(), 0, 2);
attachmentAtlasRegion = (AtlasRegion *) mesh->getRegion();
attachmentIndices = mesh->getTriangles().buffer();
attachmentUvs = mesh->getUVs().buffer();
numVertices = mesh->getWorldVerticesLength() >> 1;
numIndices = mesh->getTriangles().size();
} else /* clipping */ {
ClippingAttachment *clip = (ClippingAttachment *) attachment;
clipper.clipStart(*slot, clip);
continue;
}
if (clipper.isClipping()) {
clipper.clipTriangles(attachmentVertices->buffer(), attachmentIndices, numIndices, attachmentUvs, 2);
attachmentVertices = &clipper.getClippedVertices();
numVertices = clipper.getClippedVertices().size() >> 1;
attachmentIndices = clipper.getClippedTriangles().buffer();
numIndices = clipper.getClippedTriangles().size();
attachmentUvs = clipper.getClippedUVs().buffer();
if (clipper.getClippedTriangles().size() == 0) {
clipper.clipEnd(*slot);
continue;
}
}
// if the user switches the atlas data while not having switched
// to the correct skeleton data yet, we won't find any regions.
// ignore regions for which we can't find a material
UMaterialInstanceDynamic *material = nullptr;
int foundPageIndex = (int) (intptr_t) attachmentAtlasRegion->rendererObject;
if (foundPageIndex == -1) {
clipper.clipEnd(*slot);
continue;
}
switch (slot->getData().getBlendMode()) {
case BlendMode_Additive:
if (foundPageIndex >= atlasAdditiveBlendMaterials.Num()) {
clipper.clipEnd(*slot);
continue;
}
material = atlasAdditiveBlendMaterials[foundPageIndex];
break;
case BlendMode_Multiply:
if (foundPageIndex >= atlasMultiplyBlendMaterials.Num()) {
clipper.clipEnd(*slot);
continue;
}
material = atlasMultiplyBlendMaterials[foundPageIndex];
break;
case BlendMode_Screen:
if (foundPageIndex >= atlasScreenBlendMaterials.Num()) {
clipper.clipEnd(*slot);
continue;
}
material = atlasScreenBlendMaterials[foundPageIndex];
break;
case BlendMode_Normal:
default:
if (foundPageIndex >= atlasNormalBlendMaterials.Num()) {
clipper.clipEnd(*slot);
continue;
}
material = atlasNormalBlendMaterials[foundPageIndex];
break;
}
if (lastMaterial != material) {
Flush(meshSection, vertices, indices, normals, uvs, colors, lastMaterial);
lastMaterial = material;
idx = 0;
}
SetMaterial(meshSection, material);
uint8 r = static_cast<uint8>(Skeleton->getColor().r * slot->getColor().r * attachmentColor.r * 255);
uint8 g = static_cast<uint8>(Skeleton->getColor().g * slot->getColor().g * attachmentColor.g * 255);
uint8 b = static_cast<uint8>(Skeleton->getColor().b * slot->getColor().b * attachmentColor.b * 255);
uint8 a = static_cast<uint8>(Skeleton->getColor().a * slot->getColor().a * attachmentColor.a * 255);
float *verticesPtr = attachmentVertices->buffer();
for (int j = 0; j < numVertices << 1; j += 2) {
colors.Add(FColor(r, g, b, a));
vertices.Add(FVector(verticesPtr[j], depthOffset, verticesPtr[j + 1]));
uvs.Add(FVector2D(attachmentUvs[j], attachmentUvs[j + 1]));
}
for (int j = 0; j < numIndices; j++) {
indices.Add(idx + attachmentIndices[j]);
}
int numTriangles = indices.Num() / 3;
for (int j = 0; j < numTriangles; j++) {
const int triangleIndex = j * 3;
if (FVector::CrossProduct(
vertices[indices[triangleIndex + 2]] - vertices[indices[triangleIndex]],
vertices[indices[triangleIndex + 1]] - vertices[indices[triangleIndex]])
.Y < 0.f) {
const int32 targetVertex = indices[triangleIndex];
indices[triangleIndex] = indices[triangleIndex + 2];
indices[triangleIndex + 2] = targetVertex;
}
}
FVector normal = FVector(0, 1, 0);
for (int j = 0; j < numVertices; j++) {
normals.Add(normal);
}
idx += numVertices;
depthOffset += this->DepthOffset;
clipper.clipEnd(*slot);
}
Flush(meshSection, vertices, indices, normals, uvs, colors, lastMaterial);
clipper.clipEnd();
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,564 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include "SpineWidget.h"
#include "SSpineWidget.h"
#include "SpineSkeletonAnimationComponent.h"
#include "spine/spine.h"
#define LOCTEXT_NAMESPACE "Spine"
using namespace spine;
void callbackWidget(AnimationState *state, spine::EventType type, TrackEntry *entry, Event *event) {
USpineWidget *component = (USpineWidget *) state->getRendererObject();
if (entry->getRendererObject()) {
UTrackEntry *uEntry = (UTrackEntry *) entry->getRendererObject();
if (type == EventType_Start) {
component->AnimationStart.Broadcast(uEntry);
uEntry->AnimationStart.Broadcast(uEntry);
} else if (type == EventType_Interrupt) {
component->AnimationInterrupt.Broadcast(uEntry);
uEntry->AnimationInterrupt.Broadcast(uEntry);
} else if (type == EventType_Event) {
FSpineEvent evt;
evt.SetEvent(event);
component->AnimationEvent.Broadcast(uEntry, evt);
uEntry->AnimationEvent.Broadcast(uEntry, evt);
} else if (type == EventType_Complete) {
component->AnimationComplete.Broadcast(uEntry);
uEntry->AnimationComplete.Broadcast(uEntry);
} else if (type == EventType_End) {
component->AnimationEnd.Broadcast(uEntry);
uEntry->AnimationEnd.Broadcast(uEntry);
} else if (type == EventType_Dispose) {
component->AnimationDispose.Broadcast(uEntry);
uEntry->AnimationDispose.Broadcast(uEntry);
uEntry->SetTrackEntry(nullptr);
component->GCTrackEntry(uEntry);
}
}
}
USpineWidget::USpineWidget(const FObjectInitializer &ObjectInitializer) : Super(ObjectInitializer) {
static ConstructorHelpers::FObjectFinder<UMaterialInterface> NormalMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitNormalMaterial"));
NormalBlendMaterial = NormalMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> AdditiveMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitAdditiveMaterial"));
AdditiveBlendMaterial = AdditiveMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> MultiplyMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitMultiplyMaterial"));
MultiplyBlendMaterial = MultiplyMaterialRef.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface> ScreenMaterialRef(TEXT("/SpinePlugin/UI_SpineUnlitScreenMaterial"));
ScreenBlendMaterial = ScreenMaterialRef.Object;
TextureParameterName = FName(TEXT("SpriteTexture"));
physicsTimeScale = 1.0f;
worldVertices.ensureCapacity(1024 * 2);
bAutoPlaying = true;
}
void USpineWidget::SynchronizeProperties() {
Super::SynchronizeProperties();
if (slateWidget.IsValid()) {
CheckState();
if (skeleton) {
if (!bSkinInitialized) {// blueprint On Initialized may be called beforehand
if (InitialSkin != "") SetSkin(InitialSkin);
#if WITH_EDITOR
if (IsDesignTime()) {
if (InitialSkin == "") SetSkin("default");
bSkinInitialized = false;// allow multiple edits in editor
}
#endif
}
Tick(0, false);
slateWidget->SetData(this);
} else {
slateWidget->SetData(nullptr);
}
for (UTrackEntry *entry : trackEntries) {
if (entry && entry->GetTrackEntry()) {
entry->GetTrackEntry()->setRendererObject(nullptr);
}
}
trackEntries.Empty();
}
}
void USpineWidget::ReleaseSlateResources(bool bReleaseChildren) {
Super::ReleaseSlateResources(bReleaseChildren);
slateWidget.Reset();
}
TSharedRef<SWidget> USpineWidget::RebuildWidget() {
this->slateWidget = SNew(SSpineWidget);
return this->slateWidget.ToSharedRef();
}
#if WITH_EDITOR
const FText USpineWidget::GetPaletteCategory() {
return LOCTEXT("Spine", "Spine");
}
#endif
void USpineWidget::Tick(float DeltaTime, bool CallDelegates) {
CheckState();
if (state && bAutoPlaying) {
state->update(DeltaTime);
state->apply(*skeleton);
if (CallDelegates) BeforeUpdateWorldTransform.Broadcast(this);
skeleton->update(physicsTimeScale * DeltaTime);
skeleton->updateWorldTransform(Physics_Update);
if (CallDelegates) AfterUpdateWorldTransform.Broadcast(this);
}
}
void USpineWidget::CheckState() {
bool needsUpdate = lastAtlas != Atlas || lastData != SkeletonData;
if (!needsUpdate) {
// Are we doing a re-import? Then check if the underlying spine-cpp data
// has changed.
if (lastAtlas && lastAtlas == Atlas && lastData && lastData == SkeletonData) {
spine::Atlas *atlas = Atlas->GetAtlas();
if (lastSpineAtlas != atlas) {
needsUpdate = true;
}
if (skeleton && skeleton->getData() != SkeletonData->GetSkeletonData(atlas)) {
needsUpdate = true;
}
}
}
if (needsUpdate) {
DisposeState();
if (Atlas && SkeletonData) {
spine::SkeletonData *data = SkeletonData->GetSkeletonData(Atlas->GetAtlas());
if (data) {
skeleton = new (__FILE__, __LINE__) Skeleton(data);
AnimationStateData *stateData = SkeletonData->GetAnimationStateData(Atlas->GetAtlas());
state = new (__FILE__, __LINE__) AnimationState(stateData);
state->setRendererObject((void *) this);
state->setListener(callbackWidget);
trackEntries.Empty();
skeleton->setToSetupPose();
skeleton->updateWorldTransform(Physics_Update);
slateWidget->SetData(this);
}
}
lastAtlas = Atlas;
lastSpineAtlas = Atlas ? Atlas->GetAtlas() : nullptr;
lastData = SkeletonData;
}
}
void USpineWidget::DisposeState() {
if (state) {
delete state;
state = nullptr;
}
if (skeleton) {
delete skeleton;
skeleton = nullptr;
}
if (customSkin) {
delete customSkin;
customSkin = nullptr;
}
trackEntries.Empty();
}
void USpineWidget::FinishDestroy() {
DisposeState();
Super::FinishDestroy();
}
bool USpineWidget::SetSkin(const FString skinName) {
CheckState();
if (skeleton) {
spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
if (!skin) return false;
skeleton->setSkin(skin);
bSkinInitialized = true;
return true;
} else
return false;
}
bool USpineWidget::SetSkins(UPARAM(ref) TArray<FString> &SkinNames) {
CheckState();
if (skeleton) {
spine::Skin *newSkin = new spine::Skin("__spine-ue3_custom_skin");
for (auto &skinName : SkinNames) {
spine::Skin *skin = skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName));
if (!skin) {
delete newSkin;
return false;
}
newSkin->addSkin(skin);
}
skeleton->setSkin(newSkin);
bSkinInitialized = true;
if (customSkin != nullptr) {
delete customSkin;
}
customSkin = newSkin;
return true;
} else
return false;
}
void USpineWidget::GetSkins(TArray<FString> &Skins) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getData()->getSkins().size(); i < n; i++) {
Skins.Add(skeleton->getData()->getSkins()[i]->getName().buffer());
}
}
}
bool USpineWidget::HasSkin(const FString skinName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findSkin(TCHAR_TO_UTF8(*skinName)) != nullptr;
}
return false;
}
bool USpineWidget::SetAttachment(const FString slotName, const FString attachmentName) {
CheckState();
if (skeleton) {
if (attachmentName.IsEmpty()) {
skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), NULL);
return true;
}
if (!skeleton->getAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName))) return false;
skeleton->setAttachment(TCHAR_TO_UTF8(*slotName), TCHAR_TO_UTF8(*attachmentName));
return true;
}
return false;
}
void USpineWidget::UpdateWorldTransform() {
CheckState();
if (skeleton) {
skeleton->updateWorldTransform(Physics_Update);
}
}
void USpineWidget::SetToSetupPose() {
CheckState();
if (skeleton) skeleton->setToSetupPose();
}
void USpineWidget::SetBonesToSetupPose() {
CheckState();
if (skeleton) skeleton->setBonesToSetupPose();
}
void USpineWidget::SetSlotsToSetupPose() {
CheckState();
if (skeleton) skeleton->setSlotsToSetupPose();
}
void USpineWidget::SetScaleX(float scaleX) {
CheckState();
if (skeleton) skeleton->setScaleX(scaleX);
}
float USpineWidget::GetScaleX() {
CheckState();
if (skeleton) return skeleton->getScaleX();
return 1;
}
void USpineWidget::SetScaleY(float scaleY) {
CheckState();
if (skeleton) skeleton->setScaleY(scaleY);
}
float USpineWidget::GetScaleY() {
CheckState();
if (skeleton) return skeleton->getScaleY();
return 1;
}
void USpineWidget::GetBones(TArray<FString> &Bones) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getBones().size(); i < n; i++) {
Bones.Add(skeleton->getBones()[i]->getData().getName().buffer());
}
}
}
bool USpineWidget::HasBone(const FString BoneName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findBone(TCHAR_TO_UTF8(*BoneName)) != nullptr;
}
return false;
}
FTransform USpineWidget::GetBoneTransform(const FString &BoneName) {
CheckState();
if (skeleton) {
Bone *bone = skeleton->findBone(TCHAR_TO_UTF8(*BoneName));
if (!bone) return FTransform();
FMatrix localTransform;
localTransform.SetIdentity();
localTransform.SetAxis(2, FVector(bone->getA(), 0, bone->getC()));
localTransform.SetAxis(0, FVector(bone->getB(), 0, bone->getD()));
localTransform.SetOrigin(FVector(bone->getWorldX(), 0, bone->getWorldY()));
FTransform result;
result.SetFromMatrix(localTransform);
return result;
}
return FTransform();
}
void USpineWidget::GetSlots(TArray<FString> &Slots) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getSlots().size(); i < n; i++) {
Slots.Add(skeleton->getSlots()[i]->getData().getName().buffer());
}
}
}
bool USpineWidget::HasSlot(const FString SlotName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findSlot(TCHAR_TO_UTF8(*SlotName)) != nullptr;
}
return false;
}
void USpineWidget::SetSlotColor(const FString SlotName, const FColor SlotColor) {
CheckState();
if (skeleton) {
spine::Slot *slot = skeleton->findSlot(TCHAR_TO_UTF8(*SlotName));
if (slot) {
slot->getColor().set(SlotColor.R / 255.f, SlotColor.G / 255.f, SlotColor.B / 255.f, SlotColor.A / 255.f);
}
}
}
void USpineWidget::GetAnimations(TArray<FString> &Animations) {
CheckState();
if (skeleton) {
for (size_t i = 0, n = skeleton->getData()->getAnimations().size(); i < n; i++) {
Animations.Add(skeleton->getData()->getAnimations()[i]->getName().buffer());
}
}
}
bool USpineWidget::HasAnimation(FString AnimationName) {
CheckState();
if (skeleton) {
return skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName)) != nullptr;
}
return false;
}
float USpineWidget::GetAnimationDuration(FString AnimationName) {
CheckState();
if (skeleton) {
spine::Animation *animation = skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*AnimationName));
if (animation == nullptr) return 0;
else
return animation->getDuration();
}
return 0;
}
void USpineWidget::SetAutoPlay(bool bInAutoPlays) {
bAutoPlaying = bInAutoPlays;
}
void USpineWidget::SetPlaybackTime(float InPlaybackTime, bool bCallDelegates) {
CheckState();
if (state && state->getCurrent(0)) {
spine::Animation *CurrentAnimation = state->getCurrent(0)->getAnimation();
const float CurrentTime = state->getCurrent(0)->getTrackTime();
InPlaybackTime = FMath::Clamp(InPlaybackTime, 0.0f, CurrentAnimation->getDuration());
const float DeltaTime = InPlaybackTime - CurrentTime;
state->update(DeltaTime);
state->apply(*skeleton);
//Call delegates and perform the world transform
if (bCallDelegates) {
BeforeUpdateWorldTransform.Broadcast(this);
}
skeleton->updateWorldTransform(Physics_Update);
if (bCallDelegates) {
AfterUpdateWorldTransform.Broadcast(this);
}
}
}
void USpineWidget::SetTimeScale(float timeScale) {
CheckState();
if (state) state->setTimeScale(timeScale);
}
float USpineWidget::GetTimeScale() {
CheckState();
if (state) return state->getTimeScale();
return 1;
}
UTrackEntry *USpineWidget::SetAnimation(int trackIndex, FString animationName, bool loop) {
CheckState();
if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
state->disableQueue();
TrackEntry *entry = state->setAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop);
state->enableQueue();
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineWidget::AddAnimation(int trackIndex, FString animationName, bool loop, float delay) {
CheckState();
if (state && skeleton->getData()->findAnimation(TCHAR_TO_UTF8(*animationName))) {
state->disableQueue();
TrackEntry *entry = state->addAnimation(trackIndex, TCHAR_TO_UTF8(*animationName), loop, delay);
state->enableQueue();
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineWidget::SetEmptyAnimation(int trackIndex, float mixDuration) {
CheckState();
if (state) {
TrackEntry *entry = state->setEmptyAnimation(trackIndex, mixDuration);
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineWidget::AddEmptyAnimation(int trackIndex, float mixDuration, float delay) {
CheckState();
if (state) {
TrackEntry *entry = state->addEmptyAnimation(trackIndex, mixDuration, delay);
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
} else
return NewObject<UTrackEntry>();
}
UTrackEntry *USpineWidget::GetCurrent(int trackIndex) {
CheckState();
if (state && state->getCurrent(trackIndex)) {
TrackEntry *entry = state->getCurrent(trackIndex);
if (entry->getRendererObject()) {
return (UTrackEntry *) entry->getRendererObject();
} else {
UTrackEntry *uEntry = NewObject<UTrackEntry>();
uEntry->SetTrackEntry(entry);
trackEntries.Add(uEntry);
return uEntry;
}
} else
return NewObject<UTrackEntry>();
}
void USpineWidget::ClearTracks() {
CheckState();
if (state) {
state->clearTracks();
}
}
void USpineWidget::ClearTrack(int trackIndex) {
CheckState();
if (state) {
state->clearTrack(trackIndex);
}
}
void USpineWidget::PhysicsTranslate(float x, float y) {
CheckState();
if (skeleton) {
skeleton->physicsTranslate(x, y);
}
}
void USpineWidget::PhysicsRotate(float x, float y, float degrees) {
CheckState();
if (skeleton) {
skeleton->physicsRotate(x, y, degrees);
}
}
void USpineWidget::ResetPhysicsConstraints() {
CheckState();
if (skeleton) {
Vector<PhysicsConstraint *> &constraints = skeleton->getPhysicsConstraints();
for (int i = 0, n = (int) constraints.size(); i < n; i++) {
constraints[i]->reset();
}
}
}
void USpineWidget::SetPhysicsTimeScale(float scale) {
physicsTimeScale = scale;
}
float USpineWidget::GetPhysicsTimeScale() {
return physicsTimeScale;
}

View File

@ -0,0 +1,64 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Slate/SMeshWidget.h"
#include "SlateCore.h"
#include "SpineAtlasAsset.h"
#include <spine/spine.h>
class USpineWidget;
class SSpineWidget : public SMeshWidget {
public:
SLATE_BEGIN_ARGS(SSpineWidget) : _MeshData(nullptr) {}
SLATE_ARGUMENT(USlateVectorArtData *, MeshData)
SLATE_END_ARGS()
void Construct(const FArguments &Args);
void SetData(USpineWidget *Widget);
FSlateBrush *brush;
protected:
virtual int32 OnPaint(const FPaintArgs &Args, const FGeometry &AllottedGeometry, const FSlateRect &MyCullingRect, FSlateWindowElementList &OutDrawElements, int32 LayerId, const FWidgetStyle &InWidgetStyle, bool bParentEnabled) const override;
void UpdateMesh(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, spine::Skeleton *Skeleton);
void Flush(int32 LayerId, FSlateWindowElementList &OutDrawElements, const FGeometry &AllottedGeometry, int &Idx, TArray<FVector> &Vertices, TArray<int32> &Indices, TArray<FVector2D> &Uvs, TArray<FColor> &Colors, TArray<FVector> &Colors2, UMaterialInstanceDynamic *Material);
virtual FVector2D ComputeDesiredSize(float) const override;
USpineWidget *widget;
FRenderData renderData;
FVector boundsMin;
FVector boundsSize;
};

View File

@ -0,0 +1,75 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "Engine/Texture2D.h"
#include "spine/spine.h"
#include "SpineAtlasAsset.generated.h"
// clang-format on
UCLASS(BlueprintType, ClassGroup = (Spine))
class SPINEPLUGIN_API USpineAtlasAsset : public UPrimaryDataAsset {
GENERATED_BODY()
public:
spine::Atlas *GetAtlas();
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<UTexture2D *> atlasPages;
void SetRawData(const FString &RawData);
FName GetAtlasFileName() const;
virtual void BeginDestroy() override;
protected:
spine::Atlas *atlas = nullptr;
UPROPERTY()
FString rawData;
UPROPERTY()
FName atlasFileName;
#if WITH_EDITORONLY_DATA
public:
void SetAtlasFileName(const FName &AtlasFileName);
protected:
UPROPERTY(VisibleAnywhere, Instanced, Category = ImportSettings)
class UAssetImportData *importData;
virtual void PostInitProperties() override;
virtual void Serialize(FArchive &Ar) override;
#endif
};

View File

@ -0,0 +1,72 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Components/SceneComponent.h"
#include "SpineBoneDriverComponent.generated.h"
class USpineSkeletonComponent;
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineBoneDriverComponent : public USceneComponent {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
AActor *Target = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString BoneName;
//Uses just this component when set to true. Updates owning actor otherwise.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseComponentTransform = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UsePosition = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseRotation = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseScale = true;
USpineBoneDriverComponent();
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
protected:
UFUNCTION()
void BeforeUpdateWorldTransform(USpineSkeletonComponent *skeleton);
USpineSkeletonComponent *lastBoundComponent = nullptr;
};

View File

@ -0,0 +1,66 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Components/ActorComponent.h"
#include "Components/SceneComponent.h"
#include "SpineBoneFollowerComponent.generated.h"
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineBoneFollowerComponent : public USceneComponent {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
AActor *Target = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString BoneName;
//Updates just this component when set to true. Updates owning actor otherwise.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseComponentTransform = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UsePosition = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseRotation = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool UseScale = true;
USpineBoneFollowerComponent();
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
};

View File

@ -0,0 +1,46 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Modules/ModuleManager.h"
DECLARE_LOG_CATEGORY_EXTERN(SpineLog, Log, All);
class SPINEPLUGIN_API SpinePlugin : public IModuleInterface {
public:
static inline SpinePlugin &Get() {
return FModuleManager::LoadModuleChecked<SpinePlugin>("SpinePlugin");
}
static inline bool IsAvailable() {
return FModuleManager::Get().IsModuleLoaded("SpinePlugin");
}
};

View File

@ -0,0 +1,339 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "Components/ActorComponent.h"
#include "SpineSkeletonComponent.h"
#include "spine/spine.h"
#include "SpineSkeletonAnimationComponent.generated.h"
// clang-format on
USTRUCT(BlueprintType, Category = "Spine")
struct SPINEPLUGIN_API FSpineEvent {
GENERATED_BODY();
public:
FSpineEvent() : IntValue(0), FloatValue(0.0f), Time(0.0f) {}
void SetEvent(spine::Event *event) {
Name = FString(UTF8_TO_TCHAR(event->getData().getName().buffer()));
if (!event->getStringValue().isEmpty()) {
StringValue = FString(UTF8_TO_TCHAR(event->getStringValue().buffer()));
}
this->IntValue = event->getIntValue();
this->FloatValue = event->getFloatValue();
this->Time = event->getTime();
}
UPROPERTY(BlueprintReadonly)
FString Name;
UPROPERTY(BlueprintReadOnly)
FString StringValue;
UPROPERTY(BlueprintReadOnly)
int IntValue;
UPROPERTY(BlueprintReadOnly)
float FloatValue;
UPROPERTY(BlueprintReadOnly)
float Time;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationStartDelegate, UTrackEntry *, entry);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FSpineAnimationEventDelegate, UTrackEntry *, entry, FSpineEvent, evt);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationInterruptDelegate, UTrackEntry *, entry);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationCompleteDelegate, UTrackEntry *, entry);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationEndDelegate, UTrackEntry *, entry);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAnimationDisposeDelegate, UTrackEntry *, entry);
UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent), BlueprintType)
class SPINEPLUGIN_API UTrackEntry : public UObject {
GENERATED_BODY()
public:
UTrackEntry() {}
void SetTrackEntry(spine::TrackEntry *trackEntry);
spine::TrackEntry *GetTrackEntry() { return entry; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
int GetTrackIndex() { return entry ? entry->getTrackIndex() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
bool GetLoop() { return entry ? entry->getLoop() : false; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetLoop(bool loop) {
if (entry) entry->setLoop(loop);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetEventThreshold() { return entry ? entry->getEventThreshold() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetEventThreshold(float eventThreshold) {
if (entry) entry->setEventThreshold(eventThreshold);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetAlphaAttachmentThreshold() { return entry ? entry->getAlphaAttachmentThreshold() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetAlphaAttachmentThreshold(float attachmentThreshold) {
if (entry) entry->setAlphaAttachmentThreshold(attachmentThreshold);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetMixDrawOrderThreshold() { return entry ? entry->getMixDrawOrderThreshold() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetMixDrawOrderThreshold(float drawOrderThreshold) {
if (entry) entry->setMixDrawOrderThreshold(drawOrderThreshold);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetMixAttachmentThreshold() { return entry ? entry->getMixAttachmentThreshold() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetMixAttachmentThreshold(float drawOrderThreshold) {
if (entry) entry->setMixAttachmentThreshold(drawOrderThreshold);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetAnimationStart() { return entry ? entry->getAnimationStart() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetAnimationStart(float animationStart) {
if (entry) entry->setAnimationStart(animationStart);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetAnimationEnd() { return entry ? entry->getAnimationEnd() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetAnimationEnd(float animationEnd) {
if (entry) entry->setAnimationEnd(animationEnd);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetAnimationLast() { return entry ? entry->getAnimationLast() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetAnimationLast(float animationLast) {
if (entry) entry->setAnimationLast(animationLast);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetDelay() { return entry ? entry->getDelay() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetDelay(float delay) {
if (entry) entry->setDelay(delay);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetTrackTime() { return entry ? entry->getTrackTime() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetTrackTime(float trackTime) {
if (entry) entry->setTrackTime(trackTime);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetTrackEnd() { return entry ? entry->getTrackEnd() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetTrackEnd(float trackEnd) {
if (entry) entry->setTrackEnd(trackEnd);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetTimeScale() { return entry ? entry->getTimeScale() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetTimeScale(float timeScale) {
if (entry) entry->setTimeScale(timeScale);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetAlpha() { return entry ? entry->getAlpha() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetAlpha(float alpha) {
if (entry) entry->setAlpha(alpha);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetMixTime() { return entry ? entry->getMixTime() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetMixTime(float mixTime) {
if (entry) entry->setMixTime(mixTime);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float GetMixDuration() { return entry ? entry->getMixDuration() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
void SetMixDuration(float mixDuration) {
if (entry) entry->setMixDuration(mixDuration);
}
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
FString getAnimationName() { return entry ? entry->getAnimation()->getName().buffer() : ""; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
float getAnimationDuration() { return entry ? entry->getAnimation()->getDuration() : 0; }
UFUNCTION(BlueprintCallable, Category = "Components|Spine|TrackEntry")
bool isValidAnimation() { return entry != nullptr; }
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationStartDelegate AnimationStart;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationInterruptDelegate AnimationInterrupt;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationEventDelegate AnimationEvent;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationCompleteDelegate AnimationComplete;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationEndDelegate AnimationEnd;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|TrackEntry")
FSpineAnimationDisposeDelegate AnimationDispose;
virtual void BeginDestroy() override;
protected:
spine::TrackEntry *entry = nullptr;
};
class USpineAtlasAsset;
UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineSkeletonAnimationComponent : public USpineSkeletonComponent {
GENERATED_BODY()
public:
spine::AnimationState *GetAnimationState() { return state; };
USpineSkeletonAnimationComponent();
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
virtual void FinishDestroy() override;
//Added functions for manual configuration
/* Manages if this skeleton should update automatically or is paused. */
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetAutoPlay(bool bInAutoPlays);
/* Directly set the time of the current animation, will clamp to animation range. */
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetPlaybackTime(float InPlaybackTime, bool bCallDelegates = true);
// Blueprint functions
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetTimeScale(float timeScale);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
float GetTimeScale();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *SetAnimation(int trackIndex, FString animationName, bool loop);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *AddAnimation(int trackIndex, FString animationName, bool loop, float delay);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *SetEmptyAnimation(int trackIndex, float mixDuration);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *AddEmptyAnimation(int trackIndex, float mixDuration, float delay);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *GetCurrent(int trackIndex);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void ClearTracks();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void ClearTrack(int trackIndex);
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationStartDelegate AnimationStart;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationInterruptDelegate AnimationInterrupt;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationEventDelegate AnimationEvent;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationCompleteDelegate AnimationComplete;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationEndDelegate AnimationEnd;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationDisposeDelegate AnimationDispose;
UPROPERTY(EditAnywhere, Category = Spine)
FString PreviewAnimation;
UPROPERTY(EditAnywhere, Category = Spine)
FString PreviewSkin;
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetPhysicsTimeScale(float scale);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetPhysicsTimeScale();
// used in C event callback. Needs to be public as we can't call
// protected methods from plain old C function.
void GCTrackEntry(UTrackEntry *entry) { trackEntries.Remove(entry); }
protected:
virtual void CheckState() override;
virtual void InternalTick(float DeltaTime, bool CallDelegates = true, bool Preview = false) override;
virtual void DisposeState() override;
spine::AnimationState *state;
// keep track of track entries so they won't get GCed while
// in transit within a blueprint
UPROPERTY()
TSet<UTrackEntry *> trackEntries;
float physicsTimeScale;
private:
/* If the animation should update automatically. */
UPROPERTY()
bool bAutoPlaying;
FString lastPreviewAnimation;
FString lastPreviewSkin;
};

View File

@ -0,0 +1,163 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "Components/ActorComponent.h"
#include "SpineSkeletonDataAsset.h"
#include "spine/spine.h"
#include "SpineSkeletonComponent.generated.h"
// clang-format on
class USpineSkeletonComponent;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineBeforeUpdateWorldTransformDelegate, USpineSkeletonComponent *, skeleton);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineAfterUpdateWorldTransformDelegate, USpineSkeletonComponent *, skeleton);
class USpineAtlasAsset;
UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineSkeletonComponent : public UActorComponent {
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
USpineAtlasAsset *Atlas;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
USpineSkeletonDataAsset *SkeletonData;
spine::Skeleton *GetSkeleton() {
CheckState();
return skeleton;
};
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetSkins(TArray<FString> &Skins);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetSkins(UPARAM(ref) TArray<FString> &SkinNames);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetSkin(const FString SkinName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasSkin(const FString SkinName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetAttachment(const FString slotName, const FString attachmentName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
FTransform GetBoneWorldTransform(const FString &BoneName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetBoneWorldPosition(const FString &BoneName, const FVector &position);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void UpdateWorldTransform();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetBonesToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetSlotsToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetScaleX(float scaleX);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetScaleX();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetScaleY(float scaleY);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetScaleY();
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetBones(TArray<FString> &Bones);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasBone(const FString BoneName);
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetSlots(TArray<FString> &Slots);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasSlot(const FString SlotName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetSlotColor(const FString SlotName, const FColor color);
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetAnimations(TArray<FString> &Animations);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasAnimation(FString AnimationName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetAnimationDuration(FString AnimationName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void PhysicsTranslate(float x, float y);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void PhysicsRotate(float x, float y, float degrees);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void ResetPhysicsConstraints();
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
FSpineBeforeUpdateWorldTransformDelegate BeforeUpdateWorldTransform;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
FSpineAfterUpdateWorldTransformDelegate AfterUpdateWorldTransform;
USpineSkeletonComponent();
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
virtual void FinishDestroy() override;
protected:
virtual void CheckState();
virtual void InternalTick(float DeltaTime, bool CallDelegates = true, bool Preview = false);
virtual void DisposeState();
spine::Skeleton *skeleton;
USpineAtlasAsset *lastAtlas = nullptr;
spine::Atlas *lastSpineAtlas = nullptr;
USpineSkeletonDataAsset *lastData = nullptr;
spine::Skin *customSkin = nullptr;
};

View File

@ -0,0 +1,127 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "spine/spine.h"
#include "SpineSkeletonDataAsset.generated.h"
// clang-format on
USTRUCT(BlueprintType, Category = "Spine")
struct SPINEPLUGIN_API FSpineAnimationStateMixData {
GENERATED_BODY();
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString From;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString To;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Mix = 0;
};
UCLASS(BlueprintType, ClassGroup = (Spine))
class SPINEPLUGIN_API USpineSkeletonDataAsset : public UObject {
GENERATED_BODY()
public:
spine::SkeletonData *GetSkeletonData(spine::Atlas *Atlas);
spine::AnimationStateData *GetAnimationStateData(spine::Atlas *atlas);
void SetMix(const FString &from, const FString &to, float mix);
float GetMix(const FString &from, const FString &to);
FName GetSkeletonDataFileName() const;
void SetRawData(TArray<uint8> &Data);
virtual void BeginDestroy() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float DefaultMix = 0;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray<FSpineAnimationStateMixData> MixData;
UPROPERTY(Transient, VisibleAnywhere)
TArray<FString> Bones;
UPROPERTY(Transient, VisibleAnywhere)
TArray<FString> Slots;
UPROPERTY(Transient, VisibleAnywhere)
TArray<FString> Skins;
UPROPERTY(Transient, VisibleAnywhere)
TArray<FString> Animations;
UPROPERTY(Transient, VisibleAnywhere)
TArray<FString> Events;
protected:
UPROPERTY()
TArray<uint8> rawData;
UPROPERTY()
FName skeletonDataFileName;
// These are created at runtime
struct NativeSkeletonData {
spine::SkeletonData *skeletonData;
spine::AnimationStateData *animationStateData;
};
TMap<spine::Atlas *, NativeSkeletonData> atlasToNativeData;
void ClearNativeData();
void SetMixes(spine::AnimationStateData *animationStateData);
#if WITH_EDITORONLY_DATA
public:
void SetSkeletonDataFileName(const FName &skeletonDataFileName);
protected:
UPROPERTY(VisibleAnywhere, Instanced, Category = ImportSettings)
class UAssetImportData *importData = nullptr;
virtual void PostInitProperties() override;
#if ((ENGINE_MAJOR_VERSION >= 5) && (ENGINE_MINOR_VERSION >= 4))
virtual void GetAssetRegistryTags(FAssetRegistryTagsContext Context) const override;
#else
virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag> &OutTags) const override;
#endif
virtual void Serialize(FArchive &Ar) override;
#endif
void LoadInfo();
};

View File

@ -0,0 +1,113 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
#include "Components/ActorComponent.h"
#include "ProceduralMeshComponent.h"
#include "SpineSkeletonAnimationComponent.h"
#include "SpineSkeletonRendererComponent.generated.h"
UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineSkeletonRendererComponent : public UProceduralMeshComponent {
GENERATED_BODY()
public:
USpineSkeletonRendererComponent(const FObjectInitializer &ObjectInitializer);
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;
/* Updates this skeleton renderer using the provided skeleton animation component. */
void UpdateRenderer(USpineSkeletonComponent *Skeleton);
// Material Instance parents
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
UMaterialInterface *NormalBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
UMaterialInterface *AdditiveBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
UMaterialInterface *MultiplyBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
UMaterialInterface *ScreenBlendMaterial;
// Need to hold on to the dynamic instances, or the GC will kill us while updating them
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
TArray<UMaterialInstanceDynamic *> atlasNormalBlendMaterials;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
TArray<UMaterialInstanceDynamic *> atlasAdditiveBlendMaterials;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
TArray<UMaterialInstanceDynamic *> atlasMultiplyBlendMaterials;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
TArray<UMaterialInstanceDynamic *> atlasScreenBlendMaterials;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
float DepthOffset = 0.1f;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
FName TextureParameterName;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
FLinearColor Color = FLinearColor(1, 1, 1, 1);
/** Whether to generate collision geometry for the skeleton, or not. */
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
bool bCreateCollision;
virtual void FinishDestroy() override;
protected:
void UpdateMaterial(UTexture2D *Texture, UMaterialInstanceDynamic *&CurrentInstance, UMaterialInterface *ParentMaterial);
void UpdateMesh(USpineSkeletonComponent *component, spine::Skeleton *Skeleton);
void Flush(int &Idx, TArray<FVector> &Vertices, TArray<int32> &Indices, TArray<FVector> &Normals, TArray<FVector2D> &Uvs, TArray<FColor> &Colors, UMaterialInstanceDynamic *Material);
spine::Vector<float> worldVertices;
spine::SkeletonClipping clipper;
UPROPERTY();
TArray<FVector> vertices;
UPROPERTY();
TArray<int32> indices;
UPROPERTY();
TArray<FVector> normals;
UPROPERTY();
TArray<FVector2D> uvs;
UPROPERTY();
TArray<FColor> colors;
};

View File

@ -0,0 +1,287 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#pragma once
// clang-format off
#include "Runtime/UMG/Public/UMG.h"
#include "SpineSkeletonDataAsset.h"
#include "SpineSkeletonAnimationComponent.h"
#include "spine/spine.h"
#include "SpineWidget.generated.h"
// clang-format on
class SSpineWidget;
class USpineWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineWidgetBeforeUpdateWorldTransformDelegate, USpineWidget *, skeleton);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FSpineWidgetAfterUpdateWorldTransformDelegate, USpineWidget *, skeleton);
UCLASS(ClassGroup = (Spine), meta = (BlueprintSpawnableComponent))
class SPINEPLUGIN_API USpineWidget : public UWidget {
GENERATED_UCLASS_BODY()
public:
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
virtual void SynchronizeProperties() override;
#if WITH_EDITOR
virtual const FText GetPaletteCategory() override;
#endif
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
FString InitialSkin;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
USpineAtlasAsset *Atlas;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Spine)
USpineSkeletonDataAsset *SkeletonData;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
UMaterialInterface *NormalBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
UMaterialInterface *AdditiveBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
UMaterialInterface *MultiplyBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
UMaterialInterface *ScreenBlendMaterial;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
FName TextureParameterName;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
float DepthOffset = 0.1f;
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadWrite)
FLinearColor Color = FLinearColor(1, 1, 1, 1);
UPROPERTY(Category = Spine, EditAnywhere, BlueprintReadOnly)
FSlateBrush Brush;
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetSkins(TArray<FString> &Skins);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetSkin(const FString SkinName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetSkins(UPARAM(ref) TArray<FString> &SkinNames);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasSkin(const FString SkinName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool SetAttachment(const FString slotName, const FString attachmentName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void UpdateWorldTransform();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetBonesToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetSlotsToSetupPose();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetScaleX(float scaleX);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetScaleX();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetScaleY(float scaleY);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetScaleY();
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetBones(TArray<FString> &Bones);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasBone(const FString BoneName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
FTransform GetBoneTransform(const FString &BoneName);
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetSlots(TArray<FString> &Slots);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasSlot(const FString SlotName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetSlotColor(const FString SlotName, const FColor SlotColor);
UFUNCTION(BlueprintPure, Category = "Components|Spine|Skeleton")
void GetAnimations(TArray<FString> &Animations);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
bool HasAnimation(FString AnimationName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetAnimationDuration(FString AnimationName);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void PhysicsTranslate(float x, float y);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void PhysicsRotate(float x, float y, float degrees);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void ResetPhysicsConstraints();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
void SetPhysicsTimeScale(float scale);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Skeleton")
float GetPhysicsTimeScale();
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
FSpineWidgetBeforeUpdateWorldTransformDelegate BeforeUpdateWorldTransform;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Skeleton")
FSpineWidgetAfterUpdateWorldTransformDelegate AfterUpdateWorldTransform;
/* Manages if this skeleton should update automatically or is paused. */
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetAutoPlay(bool bInAutoPlays);
/* Directly set the time of the current animation, will clamp to animation range. */
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetPlaybackTime(float InPlaybackTime, bool bCallDelegates = true);
// Blueprint functions
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void SetTimeScale(float timeScale);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
float GetTimeScale();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *SetAnimation(int trackIndex, FString animationName, bool loop);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *AddAnimation(int trackIndex, FString animationName, bool loop, float delay);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *SetEmptyAnimation(int trackIndex, float mixDuration);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *AddEmptyAnimation(int trackIndex, float mixDuration, float delay);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
UTrackEntry *GetCurrent(int trackIndex);
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void ClearTracks();
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void ClearTrack(int trackIndex);
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationStartDelegate AnimationStart;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationInterruptDelegate AnimationInterrupt;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationEventDelegate AnimationEvent;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationCompleteDelegate AnimationComplete;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationEndDelegate AnimationEnd;
UPROPERTY(BlueprintAssignable, Category = "Components|Spine|Animation")
FSpineAnimationDisposeDelegate AnimationDispose;
UFUNCTION(BlueprintCallable, Category = "Components|Spine|Animation")
void Tick(float DeltaTime, bool CallDelegates = true);
virtual void FinishDestroy() override;
// used in C event callback. Needs to be public as we can't call
// protected methods from plain old C function.
void GCTrackEntry(UTrackEntry *entry) { trackEntries.Remove(entry); }
protected:
friend class SSpineWidget;
virtual TSharedRef<SWidget> RebuildWidget() override;
virtual void CheckState();
virtual void DisposeState();
TSharedPtr<SSpineWidget> slateWidget;
spine::Skeleton *skeleton;
spine::AnimationState *state;
USpineAtlasAsset *lastAtlas = nullptr;
spine::Atlas *lastSpineAtlas = nullptr;
USpineSkeletonDataAsset *lastData = nullptr;
spine::Skin *customSkin = nullptr;
float physicsTimeScale;
// Need to hold on to the dynamic instances, or the GC will kill us while updating them
UPROPERTY()
TArray<UMaterialInstanceDynamic *> atlasNormalBlendMaterials;
TMap<spine::AtlasPage *, UMaterialInstanceDynamic *> pageToNormalBlendMaterial;
UPROPERTY()
TArray<UMaterialInstanceDynamic *> atlasAdditiveBlendMaterials;
TMap<spine::AtlasPage *, UMaterialInstanceDynamic *> pageToAdditiveBlendMaterial;
UPROPERTY()
TArray<UMaterialInstanceDynamic *> atlasMultiplyBlendMaterials;
TMap<spine::AtlasPage *, UMaterialInstanceDynamic *> pageToMultiplyBlendMaterial;
UPROPERTY()
TArray<UMaterialInstanceDynamic *> atlasScreenBlendMaterials;
TMap<spine::AtlasPage *, UMaterialInstanceDynamic *> pageToScreenBlendMaterial;
spine::Vector<float> worldVertices;
spine::SkeletonClipping clipper;
// keep track of track entries so they won't get GCed while
// in transit within a blueprint
UPROPERTY()
TSet<UTrackEntry *> trackEntries;
private:
/* If the animation should update automatically. */
UPROPERTY()
bool bAutoPlaying;
bool bSkinInitialized = false;
};

View File

@ -0,0 +1,131 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_Animation_h
#define Spine_Animation_h
#include <spine/Vector.h>
#include <spine/HashMap.h>
#include <spine/MixBlend.h>
#include <spine/MixDirection.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
#include <spine/Property.h>
namespace spine {
class Timeline;
class Skeleton;
class Event;
class AnimationState;
class SP_API Animation : public SpineObject {
friend class AnimationState;
friend class TrackEntry;
friend class AnimationStateData;
friend class AttachmentTimeline;
friend class RGBATimeline;
friend class RGBTimeline;
friend class AlphaTimeline;
friend class RGBA2Timeline;
friend class RGB2Timeline;
friend class DeformTimeline;
friend class DrawOrderTimeline;
friend class EventTimeline;
friend class IkConstraintTimeline;
friend class PathConstraintMixTimeline;
friend class PathConstraintPositionTimeline;
friend class PathConstraintSpacingTimeline;
friend class RotateTimeline;
friend class ScaleTimeline;
friend class ShearTimeline;
friend class TransformConstraintTimeline;
friend class TranslateTimeline;
friend class TranslateXTimeline;
friend class TranslateYTimeline;
friend class TwoColorTimeline;
public:
Animation(const String &name, Vector<Timeline *> &timelines, float duration);
~Animation();
/// Applies all the animation's timelines to the specified skeleton.
/// See also Timeline::apply(Skeleton&, float, float, Vector, float, MixPose, MixDirection)
void apply(Skeleton &skeleton, float lastTime, float time, bool loop, Vector<Event *> *pEvents, float alpha,
MixBlend blend, MixDirection direction);
const String &getName();
Vector<Timeline *> &getTimelines();
bool hasTimeline(Vector<PropertyId> &ids);
float getDuration();
void setDuration(float inValue);
/// @param target After the first and before the last entry.
static int search(Vector<float> &values, float target);
static int search(Vector<float> &values, float target, int step);
private:
Vector<Timeline *> _timelines;
HashMap<PropertyId, bool> _timelineIds;
float _duration;
String _name;
};
}
#endif /* Spine_Animation_h */

View File

@ -0,0 +1,521 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AnimationState_h
#define Spine_AnimationState_h
#include <spine/Vector.h>
#include <spine/Pool.h>
#include <spine/Property.h>
#include <spine/MixBlend.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
#include <spine/HasRendererObject.h>
#include "Slot.h"
#ifdef SPINE_USE_STD_FUNCTION
#include <functional>
#endif
namespace spine {
enum EventType {
EventType_Start = 0,
EventType_Interrupt,
EventType_End,
EventType_Complete,
EventType_Dispose,
EventType_Event
};
class AnimationState;
class TrackEntry;
class Animation;
class Event;
class AnimationStateData;
class Skeleton;
class RotateTimeline;
class AttachmentTimeline;
#ifdef SPINE_USE_STD_FUNCTION
typedef std::function<void (AnimationState* state, EventType type, TrackEntry* entry, Event* event)> AnimationStateListener;
#else
typedef void (*AnimationStateListener)(AnimationState *state, EventType type, TrackEntry *entry, Event *event);
#endif
/// Abstract class to inherit from to create a callback object
class SP_API AnimationStateListenerObject {
public:
AnimationStateListenerObject() {};
virtual ~AnimationStateListenerObject() {};
public:
/// The callback function to be called
virtual void callback(AnimationState *state, EventType type, TrackEntry *entry, Event *event) = 0;
};
/// State for the playback of an animation
class SP_API TrackEntry : public SpineObject, public HasRendererObject {
friend class EventQueue;
friend class AnimationState;
public:
TrackEntry();
virtual ~TrackEntry();
/// The index of the track where this entry is either current or queued.
int getTrackIndex();
/// The animation to apply for this track entry.
Animation *getAnimation();
TrackEntry *getPrevious();
/// If true, the animation will repeat. If false, it will not, instead its last frame is applied if played beyond its duration.
bool getLoop();
void setLoop(bool inValue);
/// If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
/// of being mixed out.
///
/// When mixing between animations that key the same property, if a lower track also keys that property then the value will
/// briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
/// while the second animation mixes from 0% to 100%. Setting holdPrevious to true applies the first animation
/// at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
/// keys the property, only when a higher track also keys the property.
///
/// Snapping will occur if holdPrevious is true and this animation does not key all the same properties as the
/// previous animation.
bool getHoldPrevious();
void setHoldPrevious(bool inValue);
bool getReverse();
void setReverse(bool inValue);
bool getShortestRotation();
void setShortestRotation(bool inValue);
/// Seconds to postpone playing the animation. When a track entry is the current track entry, delay postpones incrementing
/// the track time. When a track entry is queued, delay is the time from the start of the previous animation to when the
/// track entry will become the current track entry.
float getDelay();
void setDelay(float inValue);
/// Current time in seconds this track entry has been the current track entry. The track time determines
/// TrackEntry.AnimationTime. The track time can be set to start the animation at a time other than 0, without affecting looping.
float getTrackTime();
void setTrackTime(float inValue);
/// The track time in seconds when this animation will be removed from the track. Defaults to the animation duration for
/// non-looping animations and to int.MaxValue for looping animations. If the track end time is reached and no
/// other animations are queued for playback, and mixing from any previous animations is complete, properties keyed by the animation,
/// are set to the setup pose and the track is cleared.
///
/// It may be desired to use AnimationState.addEmptyAnimation(int, float, float) to mix the properties back to the
/// setup pose over time, rather than have it happen instantly.
float getTrackEnd();
void setTrackEnd(float inValue);
/// Seconds when this animation starts, both initially and after looping. Defaults to 0.
///
/// When changing the animation start time, it often makes sense to set TrackEntry.AnimationLast to the same value to
/// prevent timeline keys before the start time from triggering.
float getAnimationStart();
void setAnimationStart(float inValue);
/// Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
/// loop back to TrackEntry.AnimationStart at this time. Defaults to the animation duration.
float getAnimationEnd();
void setAnimationEnd(float inValue);
/// The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this
/// animation is applied, event timelines will fire all events between the animation last time (exclusive) and animation time
/// (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation is applied.
float getAnimationLast();
void setAnimationLast(float inValue);
/// Uses TrackEntry.TrackTime to compute the animation time between TrackEntry.AnimationStart. and
/// TrackEntry.AnimationEnd. When the track time is 0, the animation time is equal to the animation start time.
float getAnimationTime();
/// Multiplier for the delta time when the animation state is updated, causing time for this animation to play slower or
/// faster. Defaults to 1.
float getTimeScale();
void setTimeScale(float inValue);
/// Values less than 1 mix this animation with the last skeleton pose. Defaults to 1, which overwrites the last skeleton pose with
/// this animation.
///
/// Typically track 0 is used to completely pose the skeleton, then alpha can be used on higher tracks. It doesn't make sense
/// to use alpha on track 0 if the skeleton pose is from the last frame render.
float getAlpha();
void setAlpha(float inValue);
///
/// When the mix percentage (mix time / mix duration) is less than the event threshold, event timelines for the animation
/// being mixed out will be applied. Defaults to 0, so event timelines are not applied for an animation being mixed out.
float getEventThreshold();
void setEventThreshold(float inValue);
/// When the mix percentage (mix time / mix duration) is less than the attachment threshold, attachment timelines for the
/// animation being mixed out will be applied. Defaults to 0, so attachment timelines are not applied for an animation being
/// mixed out.
float getMixAttachmentThreshold();
void setMixAttachmentThreshold(float inValue);
/// When getAlpha() is greater than alphaAttachmentThreshold, attachment timelines are applied.
/// Defaults to 0, so attachment timelines are always applied. */
float getAlphaAttachmentThreshold();
void setAlphaAttachmentThreshold(float inValue);
/// When the mix percentage (mix time / mix duration) is less than the draw order threshold, draw order timelines for the
/// animation being mixed out will be applied. Defaults to 0, so draw order timelines are not applied for an animation being
/// mixed out.
float getMixDrawOrderThreshold();
void setMixDrawOrderThreshold(float inValue);
/// The animation queued to start after this animation, or NULL.
TrackEntry *getNext();
/// Returns true if at least one loop has been completed.
bool isComplete();
/// Seconds from 0 to the mix duration when mixing from the previous animation to this animation. May be slightly more than
/// TrackEntry.MixDuration when the mix is complete.
float getMixTime();
void setMixTime(float inValue);
/// Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
/// AnimationStateData based on the animation before this animation (if any).
///
/// The mix duration can be set manually rather than use the value from AnimationStateData.GetMix.
/// In that case, the mixDuration must be set before AnimationState.update(float) is next called.
///
/// When using AnimationState::addAnimation(int, Animation, bool, float) with a delay
/// less than or equal to 0, note the Delay is set using the mix duration from the AnimationStateData
float getMixDuration();
void setMixDuration(float inValue);
void setMixDuration(float mixDuration, float delay);
MixBlend getMixBlend();
void setMixBlend(MixBlend blend);
/// The track entry for the previous animation when mixing from the previous animation to this animation, or NULL if no
/// mixing is currently occuring. When mixing from multiple animations, MixingFrom makes up a double linked list with MixingTo.
TrackEntry *getMixingFrom();
/// The track entry for the next animation when mixing from this animation, or NULL if no mixing is currently occuring.
/// When mixing from multiple animations, MixingTo makes up a double linked list with MixingFrom.
TrackEntry *getMixingTo();
/// Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
/// long way around when using alpha and starting animations on other tracks.
///
/// Mixing involves finding a rotation between two others, which has two possible solutions: the short way or the long way around.
/// The two rotations likely change over time, so which direction is the short or long way also changes.
/// If the short way was always chosen, bones would flip to the other side when that direction became the long way.
/// TrackEntry chooses the short way the first time it is applied and remembers that direction.
void resetRotationDirections();
float getTrackComplete();
void setListener(AnimationStateListener listener);
void setListener(AnimationStateListenerObject *listener);
/// Returns true if this track entry has been applied at least once.
///
/// See AnimationState::apply(Skeleton).
bool wasApplied();
/// Returns true if there is a getNext() track entry that is ready to become the current track entry during the
/// next AnimationState::update(float)}
bool isNextReady () {
return _next != NULL && _nextTrackLast - _next->_delay >= 0;
}
private:
Animation *_animation;
TrackEntry *_previous;
TrackEntry *_next;
TrackEntry *_mixingFrom;
TrackEntry *_mixingTo;
int _trackIndex;
bool _loop, _holdPrevious, _reverse, _shortestRotation;
float _eventThreshold, _mixAttachmentThreshold, _alphaAttachmentThreshold, _mixDrawOrderThreshold;
float _animationStart, _animationEnd, _animationLast, _nextAnimationLast;
float _delay, _trackTime, _trackLast, _nextTrackLast, _trackEnd, _timeScale;
float _alpha, _mixTime, _mixDuration, _interruptAlpha, _totalAlpha;
MixBlend _mixBlend;
Vector<int> _timelineMode;
Vector<TrackEntry *> _timelineHoldMix;
Vector<float> _timelinesRotation;
AnimationStateListener _listener;
AnimationStateListenerObject *_listenerObject;
void reset();
};
class SP_API EventQueueEntry : public SpineObject {
friend class EventQueue;
public:
EventType _type;
TrackEntry *_entry;
Event *_event;
EventQueueEntry(EventType eventType, TrackEntry *trackEntry, Event *event = NULL);
};
class SP_API EventQueue : public SpineObject {
friend class AnimationState;
private:
Vector<EventQueueEntry> _eventQueueEntries;
AnimationState &_state;
bool _drainDisabled;
static EventQueue *newEventQueue(AnimationState &state);
static EventQueueEntry newEventQueueEntry(EventType eventType, TrackEntry *entry, Event *event = NULL);
EventQueue(AnimationState &state);
~EventQueue();
void start(TrackEntry *entry);
void interrupt(TrackEntry *entry);
void end(TrackEntry *entry);
void dispose(TrackEntry *entry);
void complete(TrackEntry *entry);
void event(TrackEntry *entry, Event *event);
/// Raises all events in the queue and drains the queue.
void drain();
};
class SP_API AnimationState : public SpineObject, public HasRendererObject {
friend class TrackEntry;
friend class EventQueue;
public:
explicit AnimationState(AnimationStateData *data);
~AnimationState();
/// Increments the track entry times, setting queued animations as current if needed
/// @param delta delta time
void update(float delta);
/// Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
/// animation state can be applied to multiple skeletons to pose them identically.
bool apply(Skeleton &skeleton);
/// Removes all animations from all tracks, leaving skeletons in their previous pose.
/// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
/// rather than leaving them in their previous pose.
void clearTracks();
/// Removes all animations from the tracks, leaving skeletons in their previous pose.
/// It may be desired to use AnimationState.setEmptyAnimations(float) to mix the skeletons back to the setup pose,
/// rather than leaving them in their previous pose.
void clearTrack(size_t trackIndex);
/// Sets an animation by name. setAnimation(int, Animation, bool)
TrackEntry *setAnimation(size_t trackIndex, const String &animationName, bool loop);
/// Sets the current animation for a track, discarding any queued animations.
/// @param loop If true, the animation will repeat.
/// If false, it will not, instead its last frame is applied if played beyond its duration.
/// In either case TrackEntry.TrackEnd determines when the track is cleared.
/// @return
/// A track entry to allow further customization of animation playback. References to the track entry must not be kept
/// after AnimationState.Dispose.
TrackEntry *setAnimation(size_t trackIndex, Animation *animation, bool loop);
/// Queues an animation by name.
/// addAnimation(int, Animation, bool, float)
TrackEntry *addAnimation(size_t trackIndex, const String &animationName, bool loop, float delay);
/// Adds an animation to be played delay seconds after the current or last queued animation
/// for a track. If the track is empty, it is equivalent to calling setAnimation.
/// @param delay
/// Seconds to begin this animation after the start of the previous animation. May be &lt;= 0 to use the animation
/// duration of the previous track minus any mix duration plus the negative delay.
///
/// @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
/// after AnimationState.Dispose
TrackEntry *addAnimation(size_t trackIndex, Animation *animation, bool loop, float delay);
/// Sets an empty animation for a track, discarding any queued animations, and mixes to it over the specified mix duration.
TrackEntry *setEmptyAnimation(size_t trackIndex, float mixDuration);
/// Adds an empty animation to be played after the current or last queued animation for a track, and mixes to it over the
/// specified mix duration.
/// @return
/// A track entry to allow further customization of animation playback. References to the track entry must not be kept after AnimationState.Dispose.
///
/// @param trackIndex Track number.
/// @param mixDuration Mix duration.
/// @param delay Seconds to begin this animation after the start of the previous animation. May be &lt;= 0 to use the animation
/// duration of the previous track minus any mix duration plus the negative delay.
TrackEntry *addEmptyAnimation(size_t trackIndex, float mixDuration, float delay);
/// Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
void setEmptyAnimations(float mixDuration);
/// @return The track entry for the animation currently playing on the track, or NULL if no animation is currently playing.
TrackEntry *getCurrent(size_t trackIndex);
AnimationStateData *getData();
/// A list of tracks that have animations, which may contain NULLs.
Vector<TrackEntry *> &getTracks();
float getTimeScale();
void setTimeScale(float inValue);
void setListener(AnimationStateListener listener);
void setListener(AnimationStateListenerObject *listener);
void disableQueue();
void enableQueue();
void setManualTrackEntryDisposal(bool inValue);
bool getManualTrackEntryDisposal();
void disposeTrackEntry(TrackEntry *entry);
private:
static const int Subsequent = 0;
static const int First = 1;
static const int HoldSubsequent = 2;
static const int HoldFirst = 3;
static const int HoldMix = 4;
static const int Setup = 1;
static const int Current = 2;
AnimationStateData *_data;
Pool<TrackEntry> _trackEntryPool;
Vector<TrackEntry *> _tracks;
Vector<Event *> _events;
EventQueue *_queue;
HashMap<PropertyId, bool> _propertyIDs;
bool _animationsChanged;
AnimationStateListener _listener;
AnimationStateListenerObject *_listenerObject;
int _unkeyedState;
float _timeScale;
bool _manualTrackEntryDisposal;
static Animation *getEmptyAnimation();
static void
applyRotateTimeline(RotateTimeline *rotateTimeline, Skeleton &skeleton, float time, float alpha, MixBlend pose,
Vector<float> &timelinesRotation, size_t i, bool firstFrame);
void applyAttachmentTimeline(AttachmentTimeline *attachmentTimeline, Skeleton &skeleton, float animationTime,
MixBlend pose, bool firstFrame);
/// Returns true when all mixing from entries are complete.
bool updateMixingFrom(TrackEntry *to, float delta);
float applyMixingFrom(TrackEntry *to, Skeleton &skeleton, MixBlend currentPose);
void queueEvents(TrackEntry *entry, float animationTime);
/// Sets the active TrackEntry for a given track number.
void setCurrent(size_t index, TrackEntry *current, bool interrupt);
/// Removes the next entry and all entries after it for the specified entry. */
void clearNext(TrackEntry *entry);
TrackEntry *expandToIndex(size_t index);
/// Object-pooling version of new TrackEntry. Obtain an unused TrackEntry from the pool and clear/initialize its values.
/// @param last May be NULL.
TrackEntry *newTrackEntry(size_t trackIndex, Animation *animation, bool loop, TrackEntry *last);
void animationsChanged();
void computeHold(TrackEntry *entry);
void setAttachment(Skeleton &skeleton, spine::Slot &slot, const String &attachmentName, bool attachments);
};
}
#endif /* Spine_AnimationState_h */

View File

@ -0,0 +1,90 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AnimationStateData_h
#define Spine_AnimationStateData_h
#include <spine/HashMap.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
#include <assert.h>
namespace spine {
class SkeletonData;
class Animation;
/// Stores mix (crossfade) durations to be applied when AnimationState animations are changed.
class SP_API AnimationStateData : public SpineObject {
friend class AnimationState;
public:
explicit AnimationStateData(SkeletonData *skeletonData);
/// The SkeletonData to look up animations when they are specified by name.
SkeletonData *getSkeletonData();
/// The mix duration to use when no mix duration has been specifically defined between two animations.
float getDefaultMix();
void setDefaultMix(float inValue);
/// Sets a mix duration by animation names.
void setMix(const String &fromName, const String &toName, float duration);
/// Sets a mix duration when changing from the specified animation to the other.
/// See TrackEntry.MixDuration.
void setMix(Animation *from, Animation *to, float duration);
/// The mix duration to use when changing from the specified animation to the other,
/// or the DefaultMix if no mix duration has been set.
float getMix(Animation *from, Animation *to);
/// Removes all mixes and sets the default mix to 0.
void clear();
private:
class AnimationPair : public SpineObject {
public:
Animation *_a1;
Animation *_a2;
explicit AnimationPair(Animation *a1 = NULL, Animation *a2 = NULL);
bool operator==(const AnimationPair &other) const;
};
SkeletonData *_skeletonData;
float _defaultMix;
HashMap<AnimationPair, float> _animationToMixTime;
};
}
#endif /* Spine_AnimationStateData_h */

View File

@ -0,0 +1,139 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_Atlas_h
#define Spine_Atlas_h
#include <spine/Vector.h>
#include <spine/Extension.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
#include <spine/HasRendererObject.h>
#include "TextureRegion.h"
namespace spine {
enum Format {
Format_Alpha,
Format_Intensity,
Format_LuminanceAlpha,
Format_RGB565,
Format_RGBA4444,
Format_RGB888,
Format_RGBA8888
};
// Our TextureFilter collides with UE4's TextureFilter in unity builds. We rename
// TextureFilter to SpineTextureFilter in UE4.
#ifdef SPINE_UE4
#define TEXTURE_FILTER_ENUM SpineTextureFilter
#else
#define TEXTURE_FILTER_ENUM TextureFilter
#endif
enum TEXTURE_FILTER_ENUM {
TextureFilter_Unknown,
TextureFilter_Nearest,
TextureFilter_Linear,
TextureFilter_MipMap,
TextureFilter_MipMapNearestNearest,
TextureFilter_MipMapLinearNearest,
TextureFilter_MipMapNearestLinear,
TextureFilter_MipMapLinearLinear
};
enum TextureWrap {
TextureWrap_MirroredRepeat,
TextureWrap_ClampToEdge,
TextureWrap_Repeat
};
class SP_API AtlasPage : public SpineObject {
public:
String name;
String texturePath;
Format format;
TEXTURE_FILTER_ENUM minFilter;
TEXTURE_FILTER_ENUM magFilter;
TextureWrap uWrap;
TextureWrap vWrap;
int width, height;
bool pma;
int index;
void *texture;
explicit AtlasPage(const String &inName) : name(inName), format(Format_RGBA8888),
minFilter(TextureFilter_Nearest),
magFilter(TextureFilter_Nearest), uWrap(TextureWrap_ClampToEdge),
vWrap(TextureWrap_ClampToEdge), width(0), height(0), pma(false), index(0), texture(NULL) {
}
};
class SP_API AtlasRegion : public TextureRegion {
public:
AtlasPage *page;
String name;
int index;
int x, y;
Vector<int> splits;
Vector<int> pads;
Vector <String> names;
Vector<float> values;
};
class TextureLoader;
class SP_API Atlas : public SpineObject {
public:
Atlas(const String &path, TextureLoader *textureLoader, bool createTexture = true);
Atlas(const char *data, int length, const char *dir, TextureLoader *textureLoader, bool createTexture = true);
~Atlas();
void flipV();
/// Returns the first region found with the specified name. This method uses String comparison to find the region, so the result
/// should be cached rather than calling this method multiple times.
/// @return The region, or NULL.
AtlasRegion *findRegion(const String &name);
Vector<AtlasPage *> &getPages();
Vector<AtlasRegion *> &getRegions();
private:
Vector<AtlasPage *> _pages;
Vector<AtlasRegion *> _regions;
TextureLoader *_textureLoader;
void load(const char *begin, int length, const char *dir, bool createTexture);
};
}
#endif /* Spine_Atlas_h */

View File

@ -0,0 +1,72 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AtlasAttachmentLoader_h
#define Spine_AtlasAttachmentLoader_h
#include <spine/AttachmentLoader.h>
#include <spine/Vector.h>
#include <spine/SpineString.h>
namespace spine {
class Atlas;
class AtlasRegion;
/// An AttachmentLoader that configures attachments using texture regions from an Atlas.
/// See http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data about Loading Skeleton Data in the Spine Runtimes Guide.
class SP_API AtlasAttachmentLoader : public AttachmentLoader {
public:
RTTI_DECL
explicit AtlasAttachmentLoader(Atlas *atlas);
virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence);
virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin, const String &name);
virtual PathAttachment *newPathAttachment(Skin &skin, const String &name);
virtual PointAttachment *newPointAttachment(Skin &skin, const String &name);
virtual ClippingAttachment *newClippingAttachment(Skin &skin, const String &name);
virtual void configureAttachment(Attachment *attachment);
AtlasRegion *findRegion(const String &name);
private:
Atlas *_atlas;
};
}
#endif /* Spine_AtlasAttachmentLoader_h */

View File

@ -0,0 +1,62 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_Attachment_h
#define Spine_Attachment_h
#include <spine/RTTI.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
namespace spine {
class SP_API Attachment : public SpineObject {
RTTI_DECL
public:
explicit Attachment(const String &name);
virtual ~Attachment();
const String &getName() const;
virtual Attachment *copy() = 0;
int getRefCount();
void reference();
void dereference();
private:
const String _name;
int _refCount;
};
}
#endif /* Spine_Attachment_h */

View File

@ -0,0 +1,84 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AttachmentLoader_h
#define Spine_AttachmentLoader_h
#include <spine/RTTI.h>
#include <spine/SpineObject.h>
#include <spine/SpineString.h>
namespace spine {
class Skin;
class Attachment;
class RegionAttachment;
class MeshAttachment;
class BoundingBoxAttachment;
class PathAttachment;
class PointAttachment;
class ClippingAttachment;
class Sequence;
class SP_API AttachmentLoader : public SpineObject {
public:
RTTI_DECL
AttachmentLoader();
virtual ~AttachmentLoader();
/// @return May be NULL to not load any attachment.
virtual RegionAttachment *newRegionAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence) = 0;
/// @return May be NULL to not load any attachment.
virtual MeshAttachment *newMeshAttachment(Skin &skin, const String &name, const String &path, Sequence *sequence) = 0;
/// @return May be NULL to not load any attachment.
virtual BoundingBoxAttachment *newBoundingBoxAttachment(Skin &skin, const String &name) = 0;
/// @return May be NULL to not load any attachment
virtual PathAttachment *newPathAttachment(Skin &skin, const String &name) = 0;
virtual PointAttachment *newPointAttachment(Skin &skin, const String &name) = 0;
virtual ClippingAttachment *newClippingAttachment(Skin &skin, const String &name) = 0;
virtual void configureAttachment(Attachment *attachment) = 0;
};
}
#endif /* Spine_AttachmentLoader_h */

View File

@ -0,0 +1,82 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AttachmentTimeline_h
#define Spine_AttachmentTimeline_h
#include <spine/Timeline.h>
#include <spine/SpineObject.h>
#include <spine/Vector.h>
#include <spine/MixBlend.h>
#include <spine/MixDirection.h>
#include <spine/SpineString.h>
namespace spine {
class Skeleton;
class Slot;
class Event;
class SP_API AttachmentTimeline : public Timeline {
friend class SkeletonBinary;
friend class SkeletonJson;
RTTI_DECL
public:
explicit AttachmentTimeline(size_t frameCount, int slotIndex);
virtual ~AttachmentTimeline();
virtual void
apply(Skeleton &skeleton, float lastTime, float time, Vector<Event *> *pEvents, float alpha, MixBlend blend,
MixDirection direction);
/// Sets the time and value of the specified keyframe.
void setFrame(int frame, float time, const String &attachmentName);
Vector<String> &getAttachmentNames();
int getSlotIndex() { return _slotIndex; }
void setSlotIndex(int inValue) { _slotIndex = inValue; }
protected:
int _slotIndex;
Vector<String> _attachmentNames;
void setAttachment(Skeleton &skeleton, Slot &slot, String *attachmentName);
};
}
#endif /* Spine_AttachmentTimeline_h */

View File

@ -0,0 +1,45 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_AttachmentType_h
#define Spine_AttachmentType_h
namespace spine {
enum AttachmentType {
AttachmentType_Region,
AttachmentType_Boundingbox,
AttachmentType_Mesh,
AttachmentType_Linkedmesh,
AttachmentType_Path,
AttachmentType_Point,
AttachmentType_Clipping
};
}
#endif /* Spine_AttachmentType_h */

View File

@ -0,0 +1,42 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_BlendMode_h
#define Spine_BlendMode_h
namespace spine {
enum BlendMode {
BlendMode_Normal = 0,
BlendMode_Additive,
BlendMode_Multiply,
BlendMode_Screen
};
}
#endif /* Spine_BlendMode_h */

View File

@ -0,0 +1,114 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#ifndef Spine_BlockAllocator_h
#define Spine_BlockAllocator_h
#include <cstdint>
#include <spine/SpineObject.h>
#include <spine/Extension.h>
#include <spine/MathUtil.h>
#include <spine/Vector.h>
namespace spine {
struct Block {
int size;
int allocated;
uint8_t *memory;
int free() {
return size - allocated;
}
bool canFit(int numBytes) {
return free() >= numBytes;
}
uint8_t *allocate(int numBytes) {
uint8_t *ptr = memory + allocated;
allocated += numBytes;
return ptr;
}
};
class BlockAllocator : public SpineObject {
int initialBlockSize;
Vector <Block> blocks;
public:
BlockAllocator(int initialBlockSize) : initialBlockSize(initialBlockSize) {
blocks.add(newBlock(initialBlockSize));
}
~BlockAllocator() {
for (int i = 0, n = (int) blocks.size(); i < n; i++) {
SpineExtension::free(blocks[i].memory, __FILE__, __LINE__);
}
}
template<typename T>
T *allocate(size_t num) {
return (T *) _allocate((int) (sizeof(T) * num));
}
void compress() {
if (blocks.size() == 1) {
blocks[0].allocated = 0;
return;
}
int totalSize = 0;
for (int i = 0, n = (int)blocks.size(); i < n; i++) {
totalSize += blocks[i].size;
SpineExtension::free(blocks[i].memory, __FILE__, __LINE__);
}
blocks.clear();
blocks.add(newBlock(totalSize));
}
private:
void *_allocate(int numBytes) {
// 16-byte align allocations
int alignedNumBytes = numBytes + (numBytes % 16 != 0 ? 16 - (numBytes % 16) : 0);
Block *block = &blocks[blocks.size() - 1];
if (!block->canFit(alignedNumBytes)) {
blocks.add(newBlock(MathUtil::max(initialBlockSize, alignedNumBytes)));
block = &blocks[blocks.size() - 1];
}
return block->allocate(alignedNumBytes);
}
Block newBlock(int numBytes) {
Block block = {MathUtil::max(initialBlockSize, numBytes), 0, nullptr};
block.memory = SpineExtension::alloc<uint8_t>(block.size, __FILE__, __LINE__);
return block;
}
};
}
#endif

Some files were not shown because too many files have changed in this diff Show More