This commit is contained in:
badlogic 2023-10-13 11:34:07 +02:00
commit 1c9b263955
44 changed files with 1459 additions and 80 deletions

View File

@ -15,8 +15,8 @@ jobs:
version:
[
{"tag": "4.0.4-stable", "version": "4.0.4.stable", "mono": false},
{"tag": "4.1.1-stable", "version": "4.1.1.stable", "mono": false},
{"tag": "4.1.1-stable", "version": "4.1.1.stable", "mono": true},
{"tag": "4.1.2-stable", "version": "4.1.2.stable", "mono": false},
{"tag": "4.1.2-stable", "version": "4.1.2.stable", "mono": true},
]
uses: ./.github/workflows/spine-godot-v4.yml
with:

View File

@ -12,8 +12,8 @@ env:
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_EC2_METADATA_DISABLED: true
EM_VERSION: 3.1.14
GODOT_TAG: 3.5.2-stable
GODOT_VERSION: 3.5.2.stable
GODOT_TAG: 3.5.3-stable
GODOT_VERSION: 3.5.3.stable
jobs:
godot-editor-windows:

View File

@ -113,6 +113,7 @@
If you are using `SkeletonRenderSeparator` and need to enable and disable the `SkeletonRenderSeparator` component at runtime, you can increase the `RenderCombinedMesh` `Reference Renderers` array by one and assign the `SkeletonRenderer` itself at the last entry after the parts renderers. Disabled `MeshRenderer` components will be skipped when combining the final mesh, so the combined mesh is automatically filled from the desired active renderers.
* Timeline extension package: Added static `EditorEvent` callback to allow editor scripts to react to animation events outside of play-mode. Register to the events via `Spine.Unity.Playables.SpineAnimationStateMixerBehaviour.EditorEvent += YourCallback;`.
* URP Shaders: Added `Depth Write` property to shaders `Universal Render Pipeline/Spine/Skeleton` and `Universal Render Pipeline/Spine/Skeleton Lit`. Defaults to false to maintain existing behaviour.
* Added `Animation Update` mode (called `UpdateTiming` in code) `In Late Update` for `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic`. This allows you to update the `SkeletonMecanim` skeleton in the same frame that the Mecanim Animator updated its state, which happens between `Update` and `LateUpdate`.
* **Breaking changes**
* Made `SkeletonGraphic.unscaledTime` parameter protected, use the new property `UnscaledTime` instead.

View File

@ -43,7 +43,6 @@ void _spMeshAttachment_dispose(spAttachment *attachment) {
FREE(self->edges);
} else
_spAttachment_deinit(attachment);
if (self->sequence) FREE(self->sequence);
FREE(self);
}

View File

@ -1,3 +1,9 @@
# 4.1.7
* Fix allocation patter for temporary structs on Windows, which resulted in a hard crash without a stack trace on the native side.
# 4.1.6
* Fixed bug in path handling on Windows.
# 4.1.5
* Updated http dependency to 1.1.0

File diff suppressed because it is too large Load Diff

View File

@ -156,7 +156,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {

View File

@ -114,7 +114,7 @@ class SimpleFlameExample extends FlameGame {
// Load the Spineboy atlas and skeleton data from asset files
// and create a SpineComponent from them, scaled down and
// centered on the screen
spineboy = await SpineComponent.fromAssets("assets/spineboy.atlas", "assets/spineboy-pro.skel",
spineboy = await SpineComponent.fromAssets("assets/spineboy.atlas", "assets/spineboy-pro.json",
scale: Vector2(0.4, 0.4), anchor: Anchor.center, position: Vector2(size.x / 2, size.y / 2));
// Set the "walk" animation on track 0 in looping mode
@ -129,6 +129,36 @@ class SimpleFlameExample extends FlameGame {
}
}
class DragonExample extends FlameGame {
late final Atlas cachedAtlas;
late final SkeletonData cachedSkeletonData;
late final SpineComponent dragon;
@override
Future<void> onLoad() async {
cachedAtlas = await Atlas.fromAsset("assets/dragon.atlas");
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/dragon-ess.json");
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
dragon = SpineComponent(
drawable,
scale: Vector2(0.4, 0.4),
anchor: Anchor.center,
position: Vector2(size.x / 2, size.y / 2 - 150),
);
// Set the "walk" animation on track 0 in looping mode
dragon.animationState.setAnimationByName(0, "flying", true);
await add(dragon);
}
@override
void onDetach() {
// Dispose the native resources that have been loaded for spineboy.
dragon.dispose();
cachedSkeletonData.dispose();
cachedAtlas.dispose();
}
}
class PreloadAndShareSpineDataExample extends FlameGame {
late final SkeletonData cachedSkeletonData;
late final Atlas cachedAtlas;

View File

@ -143,7 +143,19 @@ class ExampleSelector extends StatelessWidget {
);
},
),
spacer
spacer,
ElevatedButton(
child: const Text('Flame: Dragon Example'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => SpineFlameGameWidget(DragonExample()),
),
);
},
),
spacer,
])));
}
}

View File

@ -65,7 +65,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
appBar: AppBar(title: const Text('Play/Pause')),
body: SpineWidget.fromAsset(
"assets/dragon.atlas",
"assets/dragon-ess.skel",
"assets/dragon-ess.json",
controller,
boundsProvider: SkinAndAnimationBounds(animation: "flying"),
),

View File

@ -203,7 +203,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0920;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
33CC10EC2044A3C60003C045 = {

View File

@ -21,10 +21,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.dev"
source: hosted
version: "1.17.1"
version: "1.17.2"
crypto:
dependency: transitive
description:
@ -114,10 +114,10 @@ packages:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
version: "0.5.0"
meta:
dependency: transitive
description:
@ -202,6 +202,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.dev"
source: hosted
version: "0.1.4-beta"
web_ffi_fork:
dependency: transitive
description:
@ -211,5 +219,5 @@ packages:
source: hosted
version: "0.7.4"
sdks:
dart: ">=3.0.0 <4.0.0"
dart: ">=3.1.0-185.0.dev <4.0.0"
flutter: ">=3.10.5"

View File

@ -126,7 +126,7 @@ class Atlas {
final numImagePaths = _bindings.spine_atlas_get_num_image_paths(atlas);
for (int i = 0; i < numImagePaths; i++) {
final Pointer<Utf8> atlasPageFile = _bindings.spine_atlas_get_image_path(atlas, i).cast();
final imagePath = path.join(atlasDir, atlasPageFile.toDartString());
final imagePath = atlasDir + "/" + atlasPageFile.toDartString();
var imageData = await loadFile(imagePath);
final Codec codec = await instantiateImageCodec(imageData);
final FrameInfo frameInfo = await codec.getNextFrame();
@ -807,7 +807,6 @@ class Bone {
Vec2 worldToLocal(double worldX, double worldY) {
final local = _bindings.spine_bone_world_to_local(_bone, worldX, worldY);
final result = Vec2(_bindings.spine_vector_get_x(local), _bindings.spine_vector_get_y(local));
_allocator.free(local);
return result;
}
@ -815,7 +814,6 @@ class Bone {
Vec2 localToWorld(double localX, double localY) {
final world = _bindings.spine_bone_local_to_world(_bone, localX, localY);
final result = Vec2(_bindings.spine_vector_get_x(world), _bindings.spine_vector_get_y(world));
_allocator.free(world);
return result;
}
@ -1871,7 +1869,6 @@ class PointAttachment extends Attachment<spine_point_attachment> {
Vec2 computeWorldPosition(Bone bone) {
final position = _bindings.spine_point_attachment_compute_world_position(_attachment, bone._bone);
final result = Vec2(_bindings.spine_vector_get_x(position), _bindings.spine_vector_get_y(position));
_allocator.free(position);
return result;
}
@ -2888,7 +2885,6 @@ class Skeleton {
final nativeBounds = _bindings.spine_skeleton_get_bounds(_skeleton);
final bounds = Bounds(_bindings.spine_bounds_get_x(nativeBounds), _bindings.spine_bounds_get_y(nativeBounds),
_bindings.spine_bounds_get_width(nativeBounds), _bindings.spine_bounds_get_height(nativeBounds));
_allocator.free(nativeBounds);
return bounds;
}

View File

@ -1,6 +1,6 @@
name: spine_flutter
description: The official Spine Flutter Runtime to load, display and interact with Spine animations.
version: 4.1.5
version: 4.1.7
homepage: https://esotericsoftware.com
repository: https://github.com/esotericsoftware/spine-runtimes
issue_tracker: https://github.com/esotericsoftware/spine-runtimes/issues

View File

@ -1473,8 +1473,9 @@ spine_path_constraint spine_skeleton_find_path_constraint(spine_skeleton skeleto
return (spine_path_constraint) _skeleton->findPathConstraint(constraintName);
}
_spine_bounds tmp_bounds;
spine_bounds spine_skeleton_get_bounds(spine_skeleton skeleton) {
_spine_bounds *bounds = (_spine_bounds *) malloc(sizeof(_spine_bounds));
_spine_bounds *bounds = &tmp_bounds;
if (skeleton == nullptr) return (spine_bounds) bounds;
Skeleton *_skeleton = (Skeleton *) skeleton;
Vector<float> vertices;
@ -2128,8 +2129,9 @@ void spine_bone_set_to_setup_pose(spine_bone bone) {
_bone->setToSetupPose();
}
_spine_vector tmp_vector;
spine_vector spine_bone_world_to_local(spine_bone bone, float worldX, float worldY) {
_spine_vector *coords = SpineExtension::calloc<_spine_vector>(1, __FILE__, __LINE__);
_spine_vector *coords = &tmp_vector;
if (bone == nullptr) return (spine_vector) coords;
Bone *_bone = (Bone *) bone;
_bone->worldToLocal(worldX, worldY, coords->x, coords->y);
@ -2137,7 +2139,7 @@ spine_vector spine_bone_world_to_local(spine_bone bone, float worldX, float worl
}
spine_vector spine_bone_local_to_world(spine_bone bone, float localX, float localY) {
_spine_vector *coords = SpineExtension::calloc<_spine_vector>(1, __FILE__, __LINE__);
_spine_vector *coords = &tmp_vector;
if (bone == nullptr) return (spine_vector) coords;
Bone *_bone = (Bone *) bone;
_bone->localToWorld(localX, localY, coords->x, coords->y);
@ -2521,7 +2523,7 @@ void spine_attachment_dispose(spine_attachment attachment) {
// PointAttachment
spine_vector spine_point_attachment_compute_world_position(spine_point_attachment attachment, spine_bone bone) {
_spine_vector *result = SpineExtension::calloc<_spine_vector>(1, __FILE__, __LINE__);
_spine_vector *result = &tmp_vector;
if (attachment == nullptr) return (spine_vector) result;
PointAttachment *_attachment = (PointAttachment *) attachment;
_attachment->computeWorldPosition(*(Bone *) bone, result->x, result->y);

View File

@ -10,28 +10,28 @@ public partial class AnimationStateListener : Node2D
spineboy.AnimationStarted += (sprite, animationState, trackEntry) =>
{
var spineTrackEntry = trackEntry as SpineTrackEntry;
Console.WriteLine("Animation started: " + spineTrackEntry.GetAnimation().GetName());
GD.Print("Animation started: " + spineTrackEntry.GetAnimation().GetName());
};
spineboy.AnimationInterrupted += (sprite, animationState, trackEntry) =>
{
var spineTrackEntry = trackEntry as SpineTrackEntry;
Console.WriteLine("Animation interrupted: " + spineTrackEntry.GetAnimation().GetName());
GD.Print("Animation interrupted: " + spineTrackEntry.GetAnimation().GetName());
};
spineboy.AnimationCompleted += (sprite, animationState, trackEntry) =>
{
var spineTrackEntry = trackEntry as SpineTrackEntry;
Console.WriteLine("Animation completed: " + spineTrackEntry.GetAnimation().GetName());
GD.Print("Animation completed: " + spineTrackEntry.GetAnimation().GetName());
};
spineboy.AnimationDisposed += (sprite, animationState, trackEntry) =>
{
var spineTrackEntry = trackEntry as SpineTrackEntry;
Console.WriteLine("Animation disposed: " + spineTrackEntry.GetAnimation().GetName());
GD.Print("Animation disposed: " + spineTrackEntry.GetAnimation().GetName());
};
spineboy.AnimationEvent += (sprite, animationState, trackEntry, eventObject) =>
{
var spineTrackEntry = trackEntry as SpineTrackEntry;
var spineEvent = eventObject as SpineEvent;
Console.WriteLine("Animation event: " + spineTrackEntry.GetAnimation().GetName() + ", " + spineEvent.GetData().GetEventName());
GD.Print("Animation event: " + spineTrackEntry.GetAnimation().GetName() + ", " + spineEvent.GetData().GetEventName());
if (spineEvent.GetData().GetEventName() == "footstep")
footStepAudio.Play();
};

View File

@ -21,7 +21,7 @@ public partial class MixAndMatch : SpineSprite
foreach (SpineSkinEntry entry in custom_skin.GetAttachments())
{
Console.WriteLine(entry.GetSlotIndex() + " " + entry.GetName());
GD.Print(entry.GetSlotIndex() + " " + entry.GetName());
}
GetAnimationState().SetAnimation("dance", true, 0);

View File

@ -122,6 +122,8 @@ void SpineAtlasResource::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::STRING, "source_path"), "", "get_source_path");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "textures"), "", "get_textures");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "normal_maps"), "", "get_normal_maps");
ADD_SIGNAL(MethodInfo("skeleton_atlas_changed"));
}
SpineAtlasResource::SpineAtlasResource() : atlas(nullptr), texture_loader(nullptr), normal_map_prefix("n") {
@ -230,6 +232,28 @@ Error SpineAtlasResource::save_to_file(const String &path) {
return OK;
}
#if VERSION_MAJOR > 3
Error SpineAtlasResource::copy_from(const Ref<Resource> &p_resource) {
auto error = Resource::copy_from(p_resource);
if (error != OK) return error;
const Ref<SpineAtlasResource> &spineAtlas = static_cast<const Ref<SpineAtlasResource> &>(p_resource);
this->clear();
this->atlas = spineAtlas->atlas;
this->texture_loader = spineAtlas->texture_loader;
spineAtlas->clear_native_data();
this->source_path = spineAtlas->source_path;
this->atlas_data = spineAtlas->atlas_data;
this->normal_map_prefix = spineAtlas->normal_map_prefix;
this->textures = spineAtlas->textures;
this->normal_maps = spineAtlas->normal_maps;
emit_signal(SNAME("skeleton_file_changed"));
return OK;
}
#endif
#if VERSION_MAJOR > 3
RES SpineAtlasResourceFormatLoader::load(const String &path, const String &original_path, Error *error, bool use_sub_threads, float *progress, CacheMode cache_mode) {
#else

View File

@ -45,8 +45,8 @@ class SpineAtlasResource : public Resource {
protected:
static void _bind_methods();
spine::Atlas *atlas;
GodotSpineTextureLoader *texture_loader;
mutable spine::Atlas *atlas;
mutable GodotSpineTextureLoader *texture_loader;
String source_path;
String atlas_data;
@ -69,11 +69,20 @@ public:
Error save_to_file(const String &path);// .spatlas
#if VERSION_MAJOR > 3
virtual Error copy_from(const Ref<Resource> &p_resource);
#endif
String get_source_path();
Array get_textures();
Array get_normal_maps();
void clear_native_data() const {
this->atlas = nullptr;
this->texture_loader = nullptr;
}
};
class SpineAtlasResourceFormatLoader : public ResourceFormatLoader {

View File

@ -115,6 +115,7 @@ void SpineSkeletonDataResource::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_images_path"), &SpineSkeletonDataResource::get_images_path);
ClassDB::bind_method(D_METHOD("get_audio_path"), &SpineSkeletonDataResource::get_audio_path);
ClassDB::bind_method(D_METHOD("get_fps"), &SpineSkeletonDataResource::get_fps);
ClassDB::bind_method(D_METHOD("update_skeleton_data"), &SpineSkeletonDataResource::update_skeleton_data);
ADD_SIGNAL(MethodInfo("skeleton_data_changed"));
ADD_SIGNAL(MethodInfo("_internal_spine_objects_invalidated"));
@ -190,6 +191,15 @@ bool SpineSkeletonDataResource::is_skeleton_data_loaded() const {
void SpineSkeletonDataResource::set_atlas_res(const Ref<SpineAtlasResource> &atlas) {
atlas_res = atlas;
if (atlas_res.is_valid()) {
#if VERSION_MAJOR > 3
if (!atlas_res->is_connected(SNAME("skeleton_atlas_changed"), callable_mp(this, &SpineSkeletonDataResource::update_skeleton_data)))
atlas_res->connect(SNAME("skeleton_atlas_changed"), callable_mp(this, &SpineSkeletonDataResource::update_skeleton_data));
#else
if (!atlas_res->is_connected(SNAME("skeleton_atlas_changed"), this, SNAME("update_skeleton_data")))
atlas_res->connect(SNAME("skeleton_atlas_changed"), this, SNAME("update_skeleton_data"));
#endif
}
update_skeleton_data();
}
@ -199,6 +209,15 @@ Ref<SpineAtlasResource> SpineSkeletonDataResource::get_atlas_res() {
void SpineSkeletonDataResource::set_skeleton_file_res(const Ref<SpineSkeletonFileResource> &skeleton_file) {
skeleton_file_res = skeleton_file;
if (skeleton_file_res.is_valid()) {
#if VERSION_MAJOR > 3
if (!skeleton_file_res->is_connected(SNAME("skeleton_file_changed"), callable_mp(this, &SpineSkeletonDataResource::update_skeleton_data)))
skeleton_file_res->connect(SNAME("skeleton_file_changed"), callable_mp(this, &SpineSkeletonDataResource::update_skeleton_data));
#else
if (!skeleton_file_res->is_connected(SNAME("skeleton_file_changed"), this, SNAME("update_skeleton_data")))
skeleton_file_res->connect(SNAME("skeleton_file_changed"), this, SNAME("update_skeleton_data"));
#endif
}
update_skeleton_data();
}

View File

@ -29,8 +29,12 @@
#include "SpineSkeletonFileResource.h"
#if VERSION_MAJOR > 3
#include "core/error/error_list.h"
#include "core/error/error_macros.h"
#include "core/io/file_access.h"
#else
#include "core/error_list.h"
#include "core/error_macros.h"
#include "core/os/file_access.h"
#endif
#include <spine/Json.h>
@ -85,6 +89,7 @@ static char *readString(BinaryInput *input) {
}
void SpineSkeletonFileResource::_bind_methods() {
ADD_SIGNAL(MethodInfo("skeleton_file_changed"));
}
static bool checkVersion(const char *version) {
@ -157,6 +162,18 @@ Error SpineSkeletonFileResource::save_to_file(const String &path) {
return OK;
}
#if VERSION_MAJOR > 3
Error SpineSkeletonFileResource::copy_from(const Ref<Resource> &p_resource) {
auto error = Resource::copy_from(p_resource);
if (error != OK) return error;
const Ref<SpineSkeletonFileResource> &spineFile = static_cast<const Ref<SpineSkeletonFileResource> &>(p_resource);
this->json = spineFile->json;
this->binary = spineFile->binary;
emit_signal(SNAME("skeleton_file_changed"));
return OK;
}
#endif
#if VERSION_MAJOR > 3
RES SpineSkeletonFileResourceFormatLoader::load(const String &path, const String &original_path, Error *error, bool use_sub_threads, float *progress, CacheMode cache_mode) {
#else

View File

@ -52,6 +52,10 @@ public:
Error load_from_file(const String &path);
Error save_to_file(const String &path);
#if VERSION_MAJOR > 3
virtual Error copy_from(const Ref<Resource> &p_resource);
#endif
};
class SpineSkeletonFileResourceFormatLoader : public ResourceFormatLoader {

View File

@ -87,7 +87,7 @@ export class SpineCanvas {
update: () => { },
render: () => { },
error: () => { },
dispose: () => { },
dispose: () => { },
}
if (config.webglConfig) config.webglConfig = { alpha: true };
@ -131,7 +131,7 @@ export class SpineCanvas {
}
/** Disposes the app, so the update() and render() functions are no longer called. Calls the dispose() callback.*/
dispose() {
dispose () {
if (this.config.app.dispose) this.config.app.dispose(this);
this.disposed = true;
}

View File

@ -28,11 +28,9 @@
*****************************************************************************/
#include "SpineAtlasImportFactory.h"
#include "AssetRegistryModule.h"
#include "AssetToolsModule.h"
#include "Developer/AssetTools/Public/IAssetTools.h"
#include "PackageTools.h"
#include "SpineAtlasAsset.h"
#include "Editor.h"
#define LOCTEXT_NAMESPACE "Spine"
@ -56,6 +54,9 @@ bool USpineAtlasAssetFactory::FactoryCanImport(const FString &Filename) {
}
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;
@ -64,13 +65,12 @@ UObject *USpineAtlasAssetFactory::FactoryCreateFile(UClass *InClass, UObject *In
FString currentSourcePath, filenameNoExtension, unusedExtension;
const FString longPackagePath = FPackageName::GetLongPackagePath(InParent->GetOutermost()->GetPathName());
FPaths::Split(UFactory::GetCurrentFilename(), currentSourcePath, filenameNoExtension, unusedExtension);
FString name(InName.ToString());
name.Append("-atlas");
USpineAtlasAsset *asset = NewObject<USpineAtlasAsset>(InParent, InClass, FName(*name), Flags);
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;
}
@ -109,6 +109,7 @@ EReimportResult::Type USpineAtlasAssetFactory::Reimport(UObject *Obj) {
else
Obj->MarkPackageDirty();
GEditor->GetEditorSubsystem<UImportSubsystem>()->BroadcastAssetReimport(asset);
return EReimportResult::Succeeded;
}

View File

@ -28,16 +28,44 @@
*****************************************************************************/
#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

@ -76,10 +76,7 @@ void LoadAtlas(const FString &Filename, const FString &TargetPath) {
}
UObject *USpineSkeletonAssetFactory::FactoryCreateFile(UClass *InClass, UObject *InParent, FName InName, EObjectFlags Flags, const FString &Filename, const TCHAR *Parms, FFeedbackContext *Warn, bool &bOutOperationCanceled) {
FString name(InName.ToString());
name.Append("-data");
USpineSkeletonDataAsset *asset = NewObject<USpineSkeletonDataAsset>(InParent, InClass, FName(*name), Flags);
USpineSkeletonDataAsset *asset = NewObject<USpineSkeletonDataAsset>(InParent, InClass, InName, Flags);
TArray<uint8> rawData;
if (!FFileHelper::LoadFileToArray(rawData, *Filename, 0)) {
return nullptr;

View File

@ -53,10 +53,6 @@ void USpineAtlasAsset::PostInitProperties() {
}
void USpineAtlasAsset::GetAssetRegistryTags(TArray<FAssetRegistryTag> &OutTags) const {
if (importData) {
OutTags.Add(FAssetRegistryTag(SourceFileTagName(), importData->GetSourceData().ToJson(), FAssetRegistryTag::TT_Hidden));
}
Super::GetAssetRegistryTags(OutTags);
}

View File

@ -111,7 +111,7 @@ public:
protected:
UPROPERTY(VisibleAnywhere, Instanced, Category = ImportSettings)
class UAssetImportData *importData;
class UAssetImportData *importData = nullptr;
virtual void PostInitProperties() override;
virtual void GetAssetRegistryTags(TArray<FAssetRegistryTag> &OutTags) const override;

View File

@ -1,5 +1,5 @@
# spine-ue4
The spine-ue4 runtime provides functionality to load, manipulate and render [Spine](http://esotericsoftware.com) skeletal animation data using [Unreal Engine 4.21+](https://www.unrealengine.com/). spine-ue4 is based on [spine-cpp](../spine-cpp).
The spine-ue4 runtime provides functionality to load, manipulate and render [Spine](http://esotericsoftware.com) skeletal animation data using [Unreal Engine 4.27-5.2](https://www.unrealengine.com/). spine-ue4 is based on [spine-cpp](../spine-cpp).
## Licensing
@ -34,7 +34,7 @@ See the [Spine Runtimes documentation](http://esotericsoftware.com/spine-documen
## Example
### [Please see the spine-ue4 guide for full documentation](http://esotericsoftware.com/spine-ue4)
The Spine UE4 example works on all platforms supported by Unreal Engine. The samples require Unreal Engine 4.25+.
The Spine UE4 example works on all platforms supported by Unreal Engine. The samples require Unreal Engine 4.27-5.2.
1. Copy the `spine-cpp` folder from this repositories root directory to your `Plugins/SpinePlugin/Sources/SpinePlugin/Public/` directory. You can run the `setup.bat` or `setup.sh` scripts to accomplish this.
2. Open the SpineUE4.uproject file with Unreal Editor

View File

@ -1,6 +1,6 @@
{
"FileVersion": 3,
"EngineAssociation": "5.2",
"EngineAssociation": "5.3",
"Category": "",
"Description": "",
"Modules": [

View File

@ -71,6 +71,11 @@ namespace Spine.Unity {
onDemandTextureLoader.RequestLoadMaterialTextures(material, ref overrideMaterial);
}
public virtual void RequireTextureLoaded (Texture placeholderTexture, ref Texture replacementTexture, System.Action<Texture> onTextureLoaded) {
if (onDemandTextureLoader)
onDemandTextureLoader.RequestLoadTexture(placeholderTexture, ref replacementTexture, onTextureLoaded);
}
[SerializeField] protected LoadingMode textureLoadingMode = LoadingMode.Normal;
[SerializeField] protected OnDemandTextureLoader onDemandTextureLoader = null;
#endif

View File

@ -51,6 +51,27 @@ namespace Spine.Unity {
/// <param name="placeholderMaterials">A newly created list of materials which has a placeholder texture assigned.</param>
/// <returns>True, if any placeholder texture is assigned at a Material of the associated AtlasAssetBase.</returns>
public abstract bool HasPlaceholderTexturesAssigned (out List<Material> placeholderMaterials);
/// <summary>
/// Returns whether any main texture is null at a Material of the associated AtlasAssetBase.
/// </summary>
/// <param name="nullTextureMaterials">A newly created list of materials which has a null main texture assigned.</param>
/// <returns>True, if any null main texture is assigned at a Material of the associated AtlasAssetBase.</returns>
public virtual bool HasNullMainTexturesAssigned (out List<Material> nullTextureMaterials) {
nullTextureMaterials = null;
if (!atlasAsset) return false;
bool anyNullTexture = false;
foreach (Material material in atlasAsset.Materials) {
if (material.mainTexture == null) {
anyNullTexture = true;
if (nullTextureMaterials == null) nullTextureMaterials = new List<Material>();
nullTextureMaterials.Add(material);
}
}
return anyNullTexture;
}
/// <summary>
/// Assigns previously setup target textures at each Material where placeholder textures are setup.</summary>
/// <returns>True on success, false if the target texture could not be assigned at any of the
@ -60,13 +81,20 @@ namespace Spine.Unity {
public abstract void EndCustomTextureLoading ();
public abstract bool HasPlaceholderAssigned (Material material);
public abstract void RequestLoadMaterialTextures (Material material, ref Material overrideMaterial);
public abstract void RequestLoadTexture (Texture placeholderTexture, ref Texture replacementTexture,
System.Action<Texture> onTextureLoaded = null);
public abstract void Clear (bool clearAtlasAsset = false);
#region Event delegates
public delegate void TextureLoadDelegate (OnDemandTextureLoader loader, Material material, int textureIndex);
protected event TextureLoadDelegate onTextureRequested;
protected event TextureLoadDelegate onTextureLoaded;
protected event TextureLoadDelegate onTextureUnloaded;
public event TextureLoadDelegate TextureRequested {
add { onTextureRequested += value; }
remove { onTextureRequested -= value; }
}
public event TextureLoadDelegate TextureLoaded {
add { onTextureLoaded += value; }
remove { onTextureLoaded -= value; }
@ -76,6 +104,10 @@ namespace Spine.Unity {
remove { onTextureUnloaded -= value; }
}
protected void OnTextureRequested (Material material, int textureIndex) {
if (onTextureRequested != null)
onTextureRequested(this, material, textureIndex);
}
protected void OnTextureLoaded (Material material, int textureIndex) {
if (onTextureLoaded != null)
onTextureLoaded(this, material, textureIndex);

View File

@ -273,8 +273,12 @@ namespace Spine.Unity {
}
public override void LateUpdate () {
if (updateTiming == UpdateTiming.InLateUpdate && valid)
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
// instantiation can happen from Update() after this component, leading to a missing Update() call.
if (!wasUpdatedAfterInit) Update(0);
base.LateUpdate();
}

View File

@ -35,6 +35,8 @@
#define HAS_CULL_TRANSPARENT_MESH
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
@ -415,6 +417,9 @@ namespace Spine.Unity {
if (freeze) return;
if (updateMode != UpdateMode.FullUpdate) return;
if (updateTiming == UpdateTiming.InLateUpdate)
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
PrepareInstructionsAndRenderers();
SetVerticesDirty(); // triggers Rebuild and avoids potential double-update in a single frame
@ -818,14 +823,22 @@ namespace Spine.Unity {
else
canvasRenderer.SetMesh(null);
bool assignTexture = false;
if (currentInstructions.submeshInstructions.Count > 0) {
Material material = currentInstructions.submeshInstructions.Items[0].material;
if (material != null && baseTexture != material.mainTexture) {
baseTexture = material.mainTexture;
if (overrideTexture == null && assignAtCanvasRenderer)
canvasRenderer.SetTexture(this.mainTexture);
assignTexture = true;
}
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
if (assignTexture)
canvasRenderer.SetTexture(this.mainTexture);
}
protected void UpdateMaterialsMultipleCanvasRenderers (SkeletonRendererInstruction currentInstructions) {
@ -896,7 +909,6 @@ namespace Spine.Unity {
bool pmaVertexColors = meshGenerator.settings.pmaVertexColors;
Material[] usedMaterialItems = usedMaterials.Items;
Texture[] usedTextureItems = usedTextures.Items;
bool assignAtCanvasRenderer = (assignMeshOverrideSingle == null || !disableMeshAssignmentOnOverride);
for (int i = 0; i < submeshCount; i++) {
SubmeshInstruction submeshInstructionItem = currentInstructions.submeshInstructions.Items[i];
meshGenerator.Begin();
@ -929,13 +941,50 @@ namespace Spine.Unity {
#endif
}
canvasRenderer.materialCount = 1;
if (assignAtCanvasRenderer)
canvasRenderer.SetMaterial(usedMaterialItems[i], usedTextureItems[i]);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
bool assignAtCanvasRenderer = (assignMeshOverrideSingle == null || !disableMeshAssignmentOnOverride);
if (assignAtCanvasRenderer) {
for (int i = 0; i < submeshCount; i++) {
CanvasRenderer canvasRenderer = canvasRenderers[i];
canvasRenderer.SetMaterial(usedMaterialItems[i], usedTextureItems[i]);
}
}
if (assignMeshOverrideMultiple != null)
assignMeshOverrideMultiple(submeshCount, meshesItems, usedMaterialItems, usedTextureItems);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
void HandleOnDemandLoading () {
foreach (AtlasAssetBase atlasAsset in skeletonDataAsset.atlasAssets) {
if (atlasAsset.TextureLoadingMode != AtlasAssetBase.LoadingMode.Normal) {
atlasAsset.BeginCustomTextureLoading();
if (!this.allowMultipleCanvasRenderers) {
Texture loadedTexture = null;
atlasAsset.RequireTextureLoaded(this.mainTexture, ref loadedTexture, null);
if (loadedTexture)
this.baseTexture = loadedTexture;
} else {
Texture[] textureItems = usedTextures.Items;
for (int i = 0, count = usedTextures.Count; i < count; ++i) {
Texture loadedTexture = null;
atlasAsset.RequireTextureLoaded(textureItems[i], ref loadedTexture, null);
if (loadedTexture)
usedTextures.Items[i] = loadedTexture;
}
}
atlasAsset.EndCustomTextureLoading();
}
}
}
#endif
protected void EnsureCanvasRendererCount (int targetCount) {
#if UNITY_EDITOR
RemoveNullCanvasRenderers();

View File

@ -109,6 +109,13 @@ namespace Spine.Unity {
UpdateAnimation();
}
/// <summary>Manual animation update. Required when <c>updateTiming</c> is set to <c>ManualUpdate</c>.</summary>
/// <param name="deltaTime">Ignored parameter.</param>
public virtual void Update (float deltaTime) {
if (!valid) return;
UpdateAnimation();
}
protected void UpdateAnimation () {
wasUpdatedAfterInit = true;
@ -160,6 +167,8 @@ namespace Spine.Unity {
}
public override void LateUpdate () {
if (updateTiming == UpdateTiming.InLateUpdate && valid && translator != null && translator.Animator != null)
UpdateAnimation();
// instantiation can happen from Update() after this component, leading to a missing Update() call.
if (!wasUpdatedAfterInit) Update();
base.LateUpdate();

View File

@ -40,7 +40,8 @@ namespace Spine.Unity {
public enum UpdateTiming {
ManualUpdate = 0,
InUpdate,
InFixedUpdate
InFixedUpdate,
InLateUpdate
}
public delegate void ISkeletonAnimationDelegate (ISkeletonAnimation animated);

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.spine-unity",
"displayName": "spine-unity Runtime",
"description": "This plugin provides the spine-unity runtime core.",
"version": "4.1.25",
"version": "4.1.29",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",

View File

@ -47,9 +47,10 @@ namespace Spine.Unity.Editor {
[CustomEditor(typeof(AddressablesTextureLoader)), CanEditMultipleObjects]
public class AddressablesTextureLoaderInspector : GenericTextureLoaderInspector {
public string LoaderSuffix { get { return "_Addressable"; } }
public class AddressablesMethodImplementations : StaticMethodImplementations {
public override string LoaderSuffix { get { return "_Addressable"; } }
public override GenericTextureLoader GetOrCreateLoader (string loaderPath) {
AddressablesTextureLoader loader = AssetDatabase.LoadAssetAtPath<AddressablesTextureLoader>(loaderPath);
if (loader == null) {

View File

@ -76,13 +76,17 @@ namespace Spine.Unity {
[System.Serializable]
public class AddressablesTextureLoader : GenericOnDemandTextureLoader<AddressableTextureReference, AddressableRequest> {
public override void CreateTextureRequest (AddressableTextureReference targetReference,
MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate) {
MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate,
System.Action<Texture> onTextureLoaded) {
OnTextureRequested(materialToUpdate, textureIndex);
materialData.textureRequests[textureIndex].handle = targetReference.assetReference.LoadAssetAsync<Texture>();
materialData.textureRequests[textureIndex].handle.Completed += (obj) => {
if (obj.Status == AsyncOperationStatus.Succeeded) {
materialToUpdate.mainTexture = (Texture)targetReference.assetReference.Asset;
Texture loadedTexture = (Texture)targetReference.assetReference.Asset;
materialToUpdate.mainTexture = loadedTexture;
OnTextureLoaded(materialToUpdate, textureIndex);
if (onTextureLoaded != null) onTextureLoaded(loadedTexture);
}
};
}

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.addressables",
"displayName": "Spine Addressables Extensions [Experimental]",
"description": "This experimental plugin provides integration of Addressables on-demand texture loading for the spine-unity runtime.\nPlease be sure to test this package first and create backups of your project before using.\n\nUsage: First declare your target Material textures as addressable. Then select the SpineAtlasAsset, right-click the SpineAtlasAsset Inspector heading and select 'Add Addressables Loader'. This generates an 'AddressableTextureLoader' asset providing configuration parameters and sets up low-resolution placeholder textures which are automatically assigned in a pre-build step when building your game executable.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.1.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"version": "4.1.0-preview.1",
"version": "4.1.0-preview.2",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",

View File

@ -37,6 +37,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;
@ -92,7 +93,7 @@ namespace Spine.Unity.Editor {
/// When set to e.g. "_Addressable", the loader asset created for
/// the "Skeleton_Atlas" asset is named "Skeleton_Addressable".
/// </summary>
public string LoaderSuffix { get; }
public virtual string LoaderSuffix { get { return "_Loader"; } }
public abstract bool SetupOnDemandLoadingReference (
ref TargetReference targetTextureReference, Texture targetTexture);
@ -237,7 +238,7 @@ namespace Spine.Unity.Editor {
#if NEWPLAYMODECALLBACKS
static void OnPlaymodeChanged (PlayModeStateChange mode) {
bool assignTargetTextures = mode == PlayModeStateChange.ExitingPlayMode;
bool assignTargetTextures = mode == PlayModeStateChange.EnteredEditMode;
#else
static void OnPlaymodeChanged () {
bool assignTargetTextures = !Application.isPlaying;
@ -259,14 +260,23 @@ namespace Spine.Unity.Editor {
public static void AssignTargetTexturesAtLoader (OnDemandTextureLoader loader) {
List<Material> placeholderMaterials;
List<Material> nullTextureMaterials;
bool anyPlaceholdersAssigned = loader.HasPlaceholderTexturesAssigned(out placeholderMaterials);
if (anyPlaceholdersAssigned) {
Debug.Log("OnDemandTextureLoader detected placeholders assigned at one or more materials. Resetting to target textures.", loader);
bool anyMaterialNull = loader.HasNullMainTexturesAssigned(out nullTextureMaterials);
if (anyPlaceholdersAssigned || anyMaterialNull) {
Debug.Log("OnDemandTextureLoader detected placeholders assigned or null main textures at one or more materials. Resetting to target textures.", loader);
AssetDatabase.StartAssetEditing();
IEnumerable<Material> modifiedMaterials;
loader.AssignTargetTextures(out modifiedMaterials);
foreach (Material placeholderMaterial in placeholderMaterials) {
EditorUtility.SetDirty(placeholderMaterial);
if (placeholderMaterials != null) {
foreach (Material placeholderMaterial in placeholderMaterials) {
EditorUtility.SetDirty(placeholderMaterial);
}
}
if (nullTextureMaterials != null) {
foreach (Material nullTextureMaterial in nullTextureMaterials) {
EditorUtility.SetDirty(nullTextureMaterial);
}
}
AssetDatabase.StopAssetEditing();
AssetDatabase.SaveAssets();

View File

@ -164,6 +164,7 @@ namespace Spine.Unity {
if (placeholderMaterials == null) placeholderMaterials = new List<Material>();
placeholderMaterials.Add(material);
}
materialIndex++;
}
return anyPlaceholderAssigned;
}
@ -180,8 +181,7 @@ namespace Spine.Unity {
atlasAsset, i + 1, targetMaterial), this);
return false;
}
Material ignoredArgument = null;
RequestLoadMaterialTextures(targetMaterial, ref ignoredArgument);
AssignTargetTextures(targetMaterial, i);
++i;
}
modifiedMaterials = atlasAsset.Materials;
@ -223,7 +223,7 @@ namespace Spine.Unity {
int foundMaterialIndex = Array.FindIndex(placeholderMap, entry => entry.textures[textureIndex].placeholderTexture == currentTexture);
if (foundMaterialIndex >= 0)
RequestLoadTexture(material, foundMaterialIndex, textureIndex);
RequestLoadTexture(material, foundMaterialIndex, textureIndex, null);
int loadedMaterialIndex = Array.FindIndex(loadedDataAtMaterial, entry =>
entry.textureRequests[textureIndex].WasRequested &&
@ -232,33 +232,69 @@ namespace Spine.Unity {
loadedDataAtMaterial[loadedMaterialIndex].lastFrameRequested = Time.frameCount;
}
protected virtual void RequestLoadTexture (Material material, int materialIndex, int textureIndex) {
public override void RequestLoadTexture (Texture placeholderTexture, ref Texture replacementTexture,
System.Action<Texture> onTextureLoaded = null) {
if (placeholderTexture == null) return;
Texture currentTexture = placeholderTexture;
int textureIndex = 0; // Todo: currently only main texture is supported.
int foundMaterialIndex = Array.FindIndex(placeholderMap, entry => entry.textures[textureIndex].placeholderTexture == currentTexture);
if (foundMaterialIndex >= 0) {
Material material = atlasAsset.Materials.ElementAt(foundMaterialIndex);
Texture loadedTexture = RequestLoadTexture(material, foundMaterialIndex, textureIndex, onTextureLoaded);
if (loadedTexture != null)
replacementTexture = loadedTexture;
}
int loadedMaterialIndex = Array.FindIndex(loadedDataAtMaterial, entry =>
entry.textureRequests[textureIndex].WasRequested &&
entry.textureRequests[textureIndex].IsTarget(placeholderTexture));
if (loadedMaterialIndex >= 0)
loadedDataAtMaterial[loadedMaterialIndex].lastFrameRequested = Time.frameCount;
}
protected void AssignTargetTextures (Material material, int materialIndex) {
int textureIndex = 0; // Todo: currently only main texture is supported.
RequestLoadTexture(material, materialIndex, textureIndex, null);
}
protected virtual Texture RequestLoadTexture (Material material, int materialIndex, int textureIndex,
System.Action<Texture> onTextureLoaded) {
PlaceholderTextureMapping[] placeholderTextures = placeholderMap[materialIndex].textures;
TargetReference targetReference = placeholderTextures[textureIndex].targetTextureReference;
loadedDataAtMaterial[materialIndex].lastFrameRequested = Time.frameCount;
#if UNITY_EDITOR
if (!Application.isPlaying) {
if (targetReference.EditorTexture != null)
if (targetReference.EditorTexture != null) {
material.mainTexture = targetReference.EditorTexture;
return;
if (onTextureLoaded != null) onTextureLoaded(targetReference.EditorTexture);
}
return targetReference.EditorTexture;
}
#endif
MaterialOnDemandData materialData = loadedDataAtMaterial[materialIndex];
if (materialData.textureRequests[textureIndex].WasRequested) {
Texture loadedTexture = GetAlreadyLoadedTexture(materialIndex, textureIndex);
if (loadedTexture != null)
if (loadedTexture != null) {
material.mainTexture = loadedTexture;
return;
if (onTextureLoaded != null) onTextureLoaded(loadedTexture);
}
return loadedTexture;
}
CreateTextureRequest(targetReference, materialData, textureIndex, material);
CreateTextureRequest(targetReference, materialData, textureIndex, material, onTextureLoaded);
return null;
}
public abstract Texture GetAlreadyLoadedTexture (int materialIndex, int textureIndex);
public abstract void CreateTextureRequest (TargetReference targetReference,
MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate);
MaterialOnDemandData materialData, int textureIndex, Material materialToUpdate,
System.Action<Texture> onTextureLoaded);
public virtual void UnloadUnusedTextures () {
int currentFrameCount = Time.frameCount;

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.on-demand-loading",
"displayName": "Spine On-Demand Loading Extensions [Experimental]",
"description": "This experimental plugin provides a generic basic implementation of on-demand texture loading for the spine-unity runtime. You might want to use the available com.esotericsoftware.spine.addressables package which depends on this package.\nPlease be sure to test this package first and create backups of your project before using.\n\nPrerequisites:\nIt requires a working installation of the spine-unity runtime (via the spine-unity unitypackage), version 4.1.\n(See http://esotericsoftware.com/git/spine-runtimes/spine-unity)",
"version": "4.1.0",
"version": "4.1.0-preview.2",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",