[unity] Major Breaking Change: Split skeleton components into renderer and animation components. Added threading system. See 4.3-split-component-upgrade-guide.md for migration.

This commit is contained in:
Harald Csaszar 2025-10-08 22:21:55 +02:00
parent d29eb5fa99
commit 32dfaf1c0f
123 changed files with 11152 additions and 4888 deletions

View File

@ -336,12 +336,12 @@
- **Officially supported Unity versions are 2017.1-6000.1**.
- **Additions**
- Added Spine Preferences `Switch Texture Workflow` functionality to quickly switch to the respective PMA or straight-alpha texture and material presets.
- Added a workflow mismatch dialog showing whenever problematic PMA vs. straight alpha settings are detected at a newly imported `.atlas.txt` file. Invalid settings include the atlas being PMA and project using Linear color space, and a mismatch of Auto-Import presets set to straight alpha compared to the atlas being PMA and vice versa. The dialog offers an option to automatically fix the problematic setting on the import side and links website documentation for export settings. This dialog can be disabled and re-enabled via Spine preferences.
- **Breaking changes**
- Updated to use new C# runtime with all breaking changes above
- **MAJOR ARCHITECTURE CHANGE: Main skeleton components have been split into separate rendering and animation components.** Components will be automatically upgraded when scenes/prefabs are opened in the Unity Editor. See the `Documentation/4.3-split-component-upgrade-guide.md` document for detailed migration instructions. The major changes are:
- `SkeletonAnimation` is now split into `SkeletonAnimation` + `SkeletonRenderer` components
- `SkeletonMecanim` is now split into `SkeletonMecanim` + `SkeletonRenderer` components
- `SkeletonGraphic` is now split into `SkeletonAnimation` + `SkeletonGraphic` components
- Example skeletons in Spine Examples are now using straight alpha textures and materials for better compatibility with Linear colorspace.
- `Skeleton.Physics` was moved to `Physics` directly in `Spine` namespace, thus might clash with `UnityEngine.Physics`.
- Spine Physics: `UpdateWorldTransform(Skeleton.Physics.Update)``UpdateWorldTransform(Spine.Physics.Update)`
@ -349,6 +349,14 @@
- **Changes of default values**
- Changed default atlas texture workflow from PMA to straight alpha textures. This move was done because straight alpha textures are compatible with both Gamma and Linear color space, with the latter being the default for quite some time now in Unity. Note that `PMA Vertex Color` is unaffected and shall be enabled as usual to allow for single-pass additive rendering.
- **Additions**
- Added Spine Preferences `Switch Texture Workflow` functionality to quickly switch to the respective PMA or straight-alpha texture and material presets.
- Added a workflow mismatch dialog showing whenever problematic PMA vs. straight alpha settings are detected at a newly imported `.atlas.txt` file. Invalid settings include the atlas being PMA and project using Linear color space, and a mismatch of Auto-Import presets set to straight alpha compared to the atlas being PMA and vice versa. The dialog offers an option to automatically fix the problematic setting on the import side and links website documentation for export settings. This dialog can be disabled and re-enabled via Spine preferences.
- Added threading support for all skeleton rendering and animation components, disabled by default. Threading can be activated per component or globally via Edit → Preferences → Spine → Threading Defaults. Two threading options are available:
- `Threaded MeshGeneration`: Default value for SkeletonRenderer and SkeletonGraphic threaded mesh generation
- `Threaded Animation`: Default value for SkeletonAnimation and SkeletonMecanim threaded animation updates
- Even when threading is enabled, the threading system defaults to user callbacks like `UpdateWorld` being issued on the main thread to support existing user code. Can be configured via `SkeletonUpdateSystem.Instance.MainThreadUpdateCallbacks = false` to perform callbacks on worker threads if parallel execution is supported and desired by the user code. Note that most Unity API calls are restricted to the main thread.
- **Deprecated**
- **Restructuring (Non-Breaking)**

View File

@ -2,7 +2,7 @@
"name": "com.esotericsoftware.spine.spine-unity-examples",
"displayName": "spine-unity Runtime Examples",
"description": "Examples have been moved to main spine-unity Runtime package. Please install via Package Manager, select 'spine-unity Runtime', navigate to tab 'Samples' and select 'Import'.",
"version": "4.3.3",
"version": "4.3.4",
"unity": "2018.3",
"author": {
"name": "Esoteric Software",

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 51f28f46cad34314f8f275f5e4434487
folderAsset: yes
timeCreated: 1759784347
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,367 @@
# spine-unity 4.3 コンポーネント分離アップグレードガイド
## コンポーネントアーキテクチャの再構築
---
## 📋 本ドキュメントの対象範囲
**本ドキュメントは spine-unity コンポーネント分離の移行のみを扱います。** バージョン 4.2 から 4.3 では、spine-csharp と spine-unity の両方に追加の破壊的変更があり、コンポーネント分離を処理する前に対処する必要があります:
- **spine-csharp API の変更:** ボーン、スロット、コンストレイントのプロパティに影響を与える新しいポーズシステム
4.2 から 4.3 へのすべての変更を網羅する完全な移行手順については、以下を参照してください:
- [CHANGELOG.md](https://github.com/EsotericSoftware/spine-runtimes/blob/4.3-beta/CHANGELOG.md#c-2) ドキュメントC# および Unity セクション)
- 包括的な移行ガイドとして、フォーラム投稿 ["Spine-Unity 4.2 to 4.3 Upgrade Guide"](https://esotericsoftware.com/forum) を参照してください
**重要:** spine-csharp API の移行を最初に完了してから、本ドキュメントで説明されているコンポーネント分離の移行に進んでください。
---
## ⚠️ 重要:アップグレード前の必読事項
**この警告は、アップグレードされる既存のプロジェクトにのみ適用されます。新規プロジェクトはこのセクションを安全に無視できます。**
### 何が起こるか
コンポーネントは**自動的にアップグレード**され、個別のアニメーションとレンダリングコンポーネントに分離されます:
- `SkeletonAnimation``SkeletonAnimation` + `SkeletonRenderer` コンポーネント
- `SkeletonMecanim``SkeletonMecanim` + `SkeletonRenderer` コンポーネント
- `SkeletonGraphic``SkeletonAnimation` + `SkeletonGraphic` コンポーネント
すべてのコンポーネント設定とフィールドは自動的に転送されます - 何も失われません。
**ただし:** これらの型の変更により、コンポーネントタイプが一致しなくなるため、カスタムスクリプトの既存の参照が失われる可能性があります(例:`SkeletonAnimation` はもはや `SkeletonRenderer` のサブクラスではありません)。
### 必要なアップグレード手順(順番に):
1. **🔒 プロジェクトのバックアップ**
アップグレード前に完全なバックアップを作成します。これらの変更により、シーンとプレハブが変更されます。
2. **📖 このドキュメント全体を読む**
続行する前にすべての破壊的変更を理解します。
3. **✏️ コードのテストと更新**
- **スクリプトの更新:** このドキュメントに従って Spine コンポーネントを参照するカスタムスクリプトを修正します。メンバーが分離されたクラスに移動されると、コードがコンパイルされない可能性があります
- **テスト:** いくつかのテストシーンを開いて、どのコンポーネント参照が失われるかを確認します。未保存のシーンには古いコンポーネントデータが含まれており、セーフティバックアップとして機能します - 保存すると、分離されたコンポーネントが古いコンポーネントを置き換え、参照が失われます。
**⚠️ 警告:** プレハブは動作が異なりますPrefab Edit Mode でプレハブを開くと、Unity のプレハブ自動保存機能によりコンポーネントの移行が自動的に保存されます。常に最初にシーンでテストし、プレハブではテストしないでください。
- **決定:** 失われた参照を手動で再割り当てするか、移行コードを記述して(以下で説明)自動的に処理します
**⚠️ 重要:** 自動コンポーネント分離は、Unity エディターでシーン/プレハブが開かれたときにのみ発生します。ゲームをビルドする前に `Upgrade All` を選択するか、各シーン/プレハブを保存する必要があります。そうしないと、古い単一コンポーネントが分離されず、ビルドで必要なコンポーネントの半分が欠落する可能性があります!
**💡 ヒント:** Console ログ出力「SendMessage cannot be called during Awake, CheckConsistency, or OnValidate」が表示される場合、これは古い未移行のアセットがシーンに存在し、自動アップグレードされたことを示しています。これらのメッセージの後に、古いコンポーネントが分離コンポーネントに自動移行されたことを確認するログメッセージが表示されます。
4. **🔄 アップグレードパスを選択**
**オプション A - 手動再割り当て(小規模プロジェクトに推奨):**
- 各シーンとプレハブを個別に開く
- インスペクターで失われた参照を手動で再割り当て
- 満足したら保存
**オプション B - 自動移行(多数のシーン/プレハブを持つ大規模プロジェクト用):**
- 最初に移行コードを記述:
- 参照移行パターンを実装(下記の「コンポーネント参照の損失を防ぐ」セクションを参照)
- `SkeletonGraphic` 参照については、「既存の参照を SkeletonAnimation に変更」セクションを参照
- いくつかのファイルで移行コードをテスト
- その後 `Upgrade All` を使用:
- `Edit → Preferences → Spine` に移動
- `Upgrade Scenes & Prefabs` の下で
- `Upgrade All` ボタンをクリック
### 準備しないと何が壊れるか:
- シーンまたはプレハブを保存すると、シーンまたはプレハブ内のコンポーネント参照が失われる可能性がありますnull に設定)
- すべてのシーン/プレハブをアップグレードする前にビルドすると、ビルドでコンポーネントが欠落します
**ステップ 1-3 を完了せずに続行しないでください。プロジェクトが壊れるリスクがあります。**
---
## 📦 4.2 からのオプショナル 2 ステップ移行
spine-unity 4.2 から移行する場合、潜在的な問題を分離するために 2 ステップでアップグレードする方が簡単な場合があります:
### ステップ 14.3-beta プリスプリットバージョンへのアップグレード
まず、コンポーネント分離変更前の 4.3-beta バージョンにアップグレードします:
- **コミットハッシュ**: `a07b1de`
- **Git URL でパッケージを追加**: `https://github.com/EsotericSoftware/spine-runtimes.git?path=spine-csharp/src#a07b1de`
- または **unitypackage**: https://esotericsoftware.com/files/runtimes/unity/spine-unity-4.2-2025-09-26.unitypackage
この中間ステップにより:
- 4.2 → 4.3 spine-csharp API 変更に関連する問題を最初に修正し、プロジェクトが動作することを確認
- 続行する前にプロジェクトが安定していることを検証
- この 4.3-beta バージョンでプロジェクトが正常に動作したら、ステップ 2 に進む前に別のバックアップを作成
### ステップ 2最新の 4.3-beta バージョンへのアップグレード(分離されたコンポーネント付き)
4.3-beta でプロジェクトが正常に動作したら:
- 最新の 4.3-beta パッケージにアップグレード
- 上記のコンポーネント分離移行ガイドに従う
- 分離されたアニメーションとレンダリングコンポーネントを処理
この 2 ステップアプローチは問題の分離に役立ちます - 何か問題が発生した場合、4.2 → 4.3 spine-csharp の変更によるものか、コンポーネント分離に特有のものかがわかります。
---
## 📋 はじめに
spine-unity 4.3 ランタイムは、主要なアーキテクチャ変更を導入します:**コンポーネント継承ではなく、2 つの独立したコンポーネントを使用してアニメーションとレンダリングを分離**。これにより、以前は不可能だった `SkeletonMecanim` をアニメーションに、`SkeletonGraphic` をレンダリングに使用するなどの柔軟な組み合わせが可能になります。
### 主な変更点:
- **コンポーネント分離**: 以前の一体型コンポーネントが個別のレンダリングとアニメーションコンポーネントに分離されました。
- **個別のコンポーネント**: `SkeletonAnimation``SkeletonMecanim``SkeletonRenderer` から継承されなくなりました。現在は、`SkeletonRenderer``SkeletonGraphic` などの個別のレンダラーコンポーネント(`ISkeletonRenderer` のサブクラス)と連携して動作します。
- **インターフェースの更新**: 更新されたプロパティ名を持つ新しい `ISkeletonRenderer` および `ISkeletonAnimation` インターフェース。
- **設定のグループ化**: メッシュ生成設定が `MeshSettings` プロパティの下にグループ化されました。
- **自動移行**: `AUTO_UPGRADE_TO_43_COMPONENTS` が定義されている場合(*デフォルト*、Unity エディターは自動的にコンポーネントを新しい分離コンポーネントにアップグレードし、廃止されたフィールドを転送します。
- **すべてのシーンとプレハブをアップグレード**: すべてのシーンとプレハブを一度にアップグレードするには、`Edit - Preferences - Spine` に移動し、`Automatic Component Upgrade` の下で `Upgrade Scenes & Prefabs` - `Upgrade All` をクリックします。
### コンポーネントの関係:
| **旧アーキテクチャ** | **新アーキテクチャ** |
|---------------------|---------------------|
| `SkeletonAnimation``SkeletonRenderer` から継承 | `SkeletonAnimation` + 個別の `SkeletonRenderer` コンポーネント |
| `SkeletonMecanim``SkeletonRenderer` から継承 | `SkeletonMecanim` + 個別の `SkeletonRenderer` コンポーネント |
| `SkeletonGraphic` は埋め込み AnimationState を提供 | `SkeletonGraphic` + 個別の `SkeletonAnimation` コンポーネント |
---
## コードの適応
各コンポーネントの変更点:
## ▶️ SkeletonRenderer
### 破壊的変更
#### 1. インターフェースの変更
- `IHasSkeletonRenderer` インターフェースのプロパティ名が `SkeletonRenderer` から `Renderer` に、型が `SkeletonRenderer` から `ISkeletonRenderer` に変更。
#### 2. イベントの変更
- `SkeletonRendererDelegate` 型は `SkeletonRenderer` のネストされた型ではなくなりました。
コンパイルエラーを修正するには、`SkeletonRenderer.SkeletonRendererDelegate``SkeletonRendererDelegate` に置き換えます。
- `BeforeApply` デリゲート型が `SkeletonRendererDelegate` から `SkeletonAnimationDelegate` に変更。
- `SkeletonRendererDelegate` 型が `SkeletonRendererDelegate(SkeletonRenderer)` から `SkeletonRendererDelegate(ISkeletonRenderer)` に変更。これは次のイベントに影響します:`OnRebuild``OnMeshAndMaterialsUpdated`
コンパイルエラーを修正するには、メソッドパラメータを `SkeletonRenderer` から `ISkeletonRenderer` に変更します。
- ボーンイベント `UpdateLocal``UpdateWorld``UpdateComplete``ISkeletonAnimation` クラス(`SkeletonAnimation``SkeletonMecanim`)から `ISkeletonRenderer` クラスSkeletonRenderer、SkeletonGraphicに移動。デリゲート型を `UpdateBonesDelegate` から `SkeletonRendererDelegate` に変更、パラメータは `ISkeletonAnimation` ではなく `ISkeletonRenderer`
#### 2. メソッドの変更
- `LateUpdateMesh()``UpdateMesh()` に変更されました。
- `MeshGenerator.TryReplaceMaterials` を削除。
#### 3. 実行順序
- `SkeletonRenderer``SkeletonGraphic` コンポーネントは `DefaultExecutionOrder(1)]` を受け取り、デフォルト*order=0*スクリプトの後に実行されます。これにより、`UpdateTiming``InLateUpdate` に設定されていても、スケルトンが更新される前にアニメーションが適用されます。
#### 4. 動作の変更
- `singleSubmesh` が有効な場合、`generateMeshOverride` も呼び出されるようになりました。
### フィールドとプロパティの移行
#### メッシュ生成設定
| **旧 API** | **新 API** | **備考** |
|-------------------|-------------------|-----------|
| `skeletonRenderer.zSpacing` | `skeletonRenderer.MeshSettings.zSpacing` | MeshSettings に移動 |
| `skeletonRenderer.useClipping` | `skeletonRenderer.MeshSettings.useClipping` | MeshSettings に移動 |
| `skeletonRenderer.immutableTriangles` | `skeletonRenderer.MeshSettings.immutableTriangles` | MeshSettings に移動 |
| `skeletonRenderer.pmaVertexColors` | `skeletonRenderer.MeshSettings.pmaVertexColors` | MeshSettings に移動 |
| `skeletonRenderer.tintBlack` | `skeletonRenderer.MeshSettings.tintBlack` | MeshSettings に移動 |
| `skeletonRenderer.addNormals` | `skeletonRenderer.MeshSettings.addNormals` | MeshSettings に移動 |
| `skeletonRenderer.calculateTangents` | `skeletonRenderer.MeshSettings.calculateTangents` | MeshSettings に移動 |
#### 非推奨の小文字属性
- 小文字の属性 `initialFlipX``initialFlipY``initialSkinName` は非推奨となり、将来のランタイムバージョンで削除されます。代わりに同じ名前の大文字のプロパティを使用してください:`InitialFlipX``InitialFlipY``InitialSkinName`
---
## ▶️ SkeletonAnimation
### 破壊的変更
#### 1. コンポーネントアーキテクチャ
- **SkeletonAnimation は SkeletonRenderer とは別のコンポーネントになりました**、もはやサブクラスではありません。
- レンダラーへのアクセス:`skeletonAnimation.Renderer`
- レンダラーからのアニメーションアクセス:`skeletonRenderer.Animation`
#### 2. プロパティの変更
- `state` はパブリックではなくなりました。代わりに `AnimationState` プロパティを使用します。
- `valid` は削除されました。代わりに `IsValid` を使用します。
#### 3. メソッドの変更
- `UpdateOncePerFrame()` を追加 - このフレームで `Update` が呼び出されていない場合に更新します。既存の `Update(float time)` は呼び出されると常に更新します。
- パラメータなしの `Update()` はパブリックではなくなりました。このフレームで更新が実行されていない場合は `UpdateOncePerFrame()` を使用し、
アニメーションの更新を強制する場合は `Update(0)` を使用します。
### フィールドとプロパティの移行
`SkeletonAnimation` は独立したコンポーネントになったため、`SkeletonRenderer` が提供するメソッドとプロパティは `SkeletonAnimation` オブジェクトから直接アクセスできなくなりました。`SkeletonAnimation` オブジェクトから関連する `ISkeletonRenderer` にアクセスするための `skeletonAnimation.Renderer` プロパティと、
`SkeletonRenderer` または `SkeletonGraphic` オブジェクトから関連する `ISkeletonAnimation` にアクセスするための `skeletonRenderer.Animation` プロパティがあります。`ISkeletonRenderer` インターフェースで公開されていないメンバーについては、`skeletonAnimation.Renderer``SkeletonRenderer` または `SkeletonGraphic` にキャストしてレンダラーメンバー変数にアクセスできます。
### サンプルコード
```csharp
// SkeletonAnimation から SkeletonRenderer プロパティへの古いアクセス
skeletonAnimation.zSpacing = 0.1f;
skeletonAnimation.AnySkeletonRendererProperty;
// SkeletonAnimation から SkeletonRenderer プロパティへの新しいアクセス
skeletonAnimation.Renderer.MeshSettings.zSpacing = 0.1f; // ISkeletonRenderer インターフェースで公開
var skeletonRenderer = (SkeletonRenderer)skeletonAnimation.Renderer;
skeletonRenderer.AnySkeletonRendererProperty; // ISkeletonRenderer インターフェースで公開されていない
```
---
## ▶️ SkeletonMecanim
### 破壊的変更
#### 1. コンポーネントアーキテクチャ
- **SkeletonMecanim は SkeletonRenderer とは別のコンポーネントになりました**、もはやサブクラスではありません。
- [SkeletonAnimation](#▶️-skeletonanimation) と同じアクセスパターン。
#### 2. メソッドの変更
- `Update()` はパブリックではなくなりました。
- 強制更新には `UpdateIfNecessary()` または `Update(0)` を使用します。
### フィールドとプロパティの移行
上記の [SkeletonAnimation](#▶️-skeletonanimation) と同じ。
---
## ▶️ SkeletonGraphic
### 破壊的変更
#### 1. コンポーネントアーキテクチャ
- **SkeletonGraphic はアニメーションをカバーしなくなりました、個別のアニメーションコンポーネントとして SkeletonAnimation を追加**
- アニメーションへのアクセス:`skeletonGraphic.Animation`
#### 2. 既存の参照を SkeletonAnimation に変更
コンポーネントがアニメーションプロパティを変更するためだけに `SkeletonGraphic` への参照を保持している場合は、参照タイプを `SkeletonAnimation` に変更することをお勧めします。
これにより、`((SkeletonAnimation)skeletonGraphic.Animation).AnimationState` のようにキャストする代わりに
`skeletonAnimation.AnimationState` のようにアニメーション状態にアクセスできます。シリアライズされたコンポーネント変数の名前を変更したい場合は、変数定義の前に `[FormerlySerializedAs("previousName")]` 属性を使用して、既存のシリアライズ値を自動的に再割り当てできることに注意してください。
#### サンプルコード
```csharp
// 以前のシリアライズされた値を自動的に再割り当て
[FormerlySerializedAs("skeletonGraphic")]
public SkeletonAnimation skeletonAnimation; // アップグレード後も参照を維持
```
#### 3. プロパティの変更
- プロパティ `AnimationState` を削除 - 代わりに `SkeletonAnimation` コンポーネントから照会します。
- `MeshGenerator` はパブリックではなくなりました - 代わりに `MeshSettings` プロパティと `SetMeshSettings()` を使用します。
- `MaterialsMultipleCanvasRenderers` の型が `ExposedList<Material>` から `Material[]` に変更。
#### 4. 作成ヘルパーメソッド
- `NewSkeletonGraphicGameObject` は既存の動作を維持するために `SkeletonGraphic``SkeletonAnimation` の両方を作成するようになりました。戻り値の型が変更され、両方のコンポーネント参照を単一の構造体で返します。
- `AddSkeletonGraphicComponent` は削除され、次のものに置き換えられました:
- `AddSkeletonGraphicAnimationComponents` - `SkeletonGraphic``SkeletonAnimation` コンポーネントの両方を作成。
- `AddSkeletonGraphicRenderingComponent` - `SkeletonGraphic` コンポーネントのみを作成。
#### 5. イベントの変更
- デリゲートシグネチャが `SkeletonRendererDelegate(SkeletonGraphic)` から `SkeletonRendererDelegate(ISkeletonRenderer)` に変更。これは次のイベントに影響します:`OnRebuild``OnMeshAndMaterialsUpdated`
コンパイルエラーを修正するには、メソッドパラメータを SkeletonGraphic から ISkeletonRenderer に変更します。
#### 6. 実行順序
- `SkeletonRenderer``SkeletonGraphic` コンポーネントは `DefaultExecutionOrder(1)]` を受け取り、デフォルト*order=0*スクリプトの後に実行されます。これにより、`UpdateTiming``InLateUpdate` に設定されていても、スケルトンが更新される前にアニメーションが適用されます。
#### 7. 動作の変更 - マテリアルの更新
- 各 `CanvasRenderer` のマテリアルは、毎回 `LateUpdate` で更新されなくなりました。代わりに次の場合に更新されます:
- a) 更新されたスケルトンがマテリアルの変更を必要とする場合、または
- b) `CustomMaterialOverride` または `CustomTextureOverride` がアクセスされ、潜在的に変更された場合。
### フィールドとプロパティの移行
#### アニメーションプロパティ
| **旧 API** | **新 API** | **備考** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.AnimationState` | `((SkeletonAnimation)skeletonGraphic.Animation).AnimationState` | キャストが必要 |
| `skeletonGraphic.startingAnimation` | `skeletonAnimation.AnimationName` | アニメーションコンポーネント経由 |
| `skeletonGraphic.startingLoop` | `skeletonAnimation.loop` | アニメーションコンポーネント経由 |
| `skeletonGraphic.timeScale` | `skeletonAnimation.timeScale` | アニメーションコンポーネント経由 |
| `skeletonGraphic.unscaledTime` | `skeletonAnimation.unscaledTime` | アニメーションコンポーネント経由 |
#### メッシュジェネレーター設定
| **旧 API** | **新 API** | **備考** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.MeshGenerator.settings` | `skeletonGraphic.MeshSettings` | 設定への直接アクセス |
---
## ⚠️ その他の重要な注意事項
### コンポーネント参照の損失を防ぐ
`SkeletonRenderer` コンポーネントを参照し、`SkeletonAnimation` または `SkeletonMecanim` ターゲットを持っていた他のコンポーネント(例:`SkeletonRenderSeparator`)は、アップグレード後に *null* を指すようになります。`SkeletonAnimation``SkeletonMecanim` コンポーネントはもはや `SkeletonRenderer` のサブクラスではなく、有効な参照ではないためです。手動の解決策は、型を `SkeletonRenderer` のままにして `SkeletonAnimation` への参照を失うことです(*none* に設定されます)。その後、シーンとプレハブで失われた参照を手動で再割り当てする必要があります。半自動の代替解決策は次のとおりです:`SkeletonRenderer` 変数の型を `Component` に変更して(オブジェクト参照をキャプチャして失わないようにする)、`SkeletonRenderer`(または `SkeletonAnimation`)型の 2 番目の変数を追加し、プログラム的に `Component` 変数から読み取って新しく追加された変数に割り当てます。たとえば、コンポーネントに `[ExecuteAlways]` タグを追加して、Unity エディターの `Awake()` で自動的にこれを実行できます。
#### サンプルコード
```csharp
// アップグレード前の古いクラス
public class TestMigrateReferences : MonoBehaviour {
public SkeletonRenderer skeletonRenderer; // アップグレード後、ここに割り当てられた SkeletonAnimation 参照は失われます。
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
}
// アップグレード後の新しいクラス
[ExecuteAlways] // または [ExecuteInEditMode]
public class TestMigrateReferences : MonoBehaviour {
#if UNITY_EDITOR
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer; // これは skeletonRenderer という名前で割り当てられた古い SkeletonAnimation 参照をキャプチャします。
#endif
public SkeletonRenderer skeletonRenderer;
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
#if UNITY_EDITOR
public void Awake () {
AutoUpgradeReferences();
}
public void AutoUpgradeReferences () {
if (previousSkeletonRenderer != null && skeletonRenderer == null) {
skeletonRenderer = previousSkeletonRenderer.GetComponent<SkeletonRenderer>();
if (skeletonRenderer != null)
Debug.Log("Upgraded SkeletonRenderer reference.");
}
}
#endif
}
```
### コンポーネントの有効化/無効化
`ISkeletonRenderer``ISkeletonAnimation` コンポーネントが分離されたため、これらのコンポーネントのいずれかを有効/無効にするスクリプトは、**両方を有効/無効にする**ように調整する必要があります。
### SkeletonUtilityBone の動作変更
Override モードでは、`SkeletonUtilityBone``UpdatePhase.World` で Transform を調整しなくなり、`UpdatePhase.Complete` でのみ調整します(冗長な更新を削除)。
### 自動移行
- `AUTO_UPGRADE_TO_43_COMPONENTS` が定義されている場合、Unity エディターは廃止されたフィールドを自動的に転送します。
- すべてのシーンとプレハブを一度にアップグレードするには、`Edit - Preferences - Spine` に移動し、`Upgrade Scenes & Prefabs` - `Upgrade All` を選択します。
- 各クラスの `UpgradeTo43``TransferDeprecatedFields()` メソッドがシリアライズされたデータの移行を処理します。
- ランタイムアクセスには手動のコード更新が必要です。
### 最も一般的な変更の要約
1. アニメーションコンポーネントからレンダリングプロパティにアクセスするには **`.Renderer.`** プレフィックスを追加。
2. メッシュ生成設定にアクセスするには **`.MeshSettings.`** を追加。
3. SkeletonGraphic から AnimationState にアクセスする際に **SkeletonAnimation にキャスト**
4. 具体的な型からインターフェースへの**デリゲートメソッドシグネチャの更新**。
5. アップグレード後、上記の移行パターンを使用して**失われた参照を再割り当て**。
---
## 自動アップグレードチェックの無効化
すべての Spine アセット、シーン、プレハブの移行が完了したら、エディターのパフォーマンスを向上させるために自動アップグレードチェックを無効にできます:
1. `Edit → Preferences → Spine` に移動
2. `Automatic Component Upgrade` の下で、`Split Component Upgrade``Disable` をクリック
これにより、Unity エディターがシーンまたはプレハブのロード時にコンポーネントの移行が必要かどうかを判断するためのエディター内チェックの実行を停止します。追加のアセットを移行する必要がある場合は、いつでも再度有効にできます。
---
## ヘルプが必要ですか?
移行中に予期しない問題が発生した場合、またはコンポーネントプロパティが正しく移行されないことがわかった場合は、[Spine フォーラム](https://esotericsoftware.com/forum) に投稿してください。自動移行をできるだけ痛みのないものにするために、喜んでお手伝いし、問題を修正します。

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 84b6c350ead7f64469652d531034991c
timeCreated: 1759835002
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,367 @@
# spine-unity 4.3 分离组件升级指南
## 组件架构重构
---
## 📋 本文档范围
**本文档仅涵盖 spine-unity 组件分离迁移。** 从 4.2 版本到 4.3 版本spine-csharp 和 spine-unity 中还有其他破坏性更改,您需要在处理组件分离之前解决这些问题:
- **spine-csharp API 更改:** 影响骨骼、插槽和约束属性的新姿势系统
有关涵盖所有 4.2 到 4.3 更改的完整迁移步骤,请参阅:
- [CHANGELOG.md](https://github.com/EsotericSoftware/spine-runtimes/blob/4.3-beta/CHANGELOG.md#c-2) 文档C# 和 Unity 部分)
- 论坛帖子 ["Spine-Unity 4.2 to 4.3 Upgrade Guide"](https://esotericsoftware.com/forum) 以获取全面的迁移指南
**重要:** 首先完成 spine-csharp API 迁移,然后继续执行本文档中描述的组件分离迁移。
---
## ⚠️ 重要提示:升级前必读
**此警告仅适用于正在升级的现有项目。新项目可以安全地忽略此部分。**
### 将会发生什么
组件将**自动升级**并分离为独立的动画和渲染组件:
- `SkeletonAnimation``SkeletonAnimation` + `SkeletonRenderer` 组件
- `SkeletonMecanim``SkeletonMecanim` + `SkeletonRenderer` 组件
- `SkeletonGraphic``SkeletonAnimation` + `SkeletonGraphic` 组件
所有组件设置和字段将自动转移 - 不会丢失任何内容。
**但是:** 由于这些类型更改,您自定义脚本中的现有引用可能会丢失,因为组件类型不再匹配(例如,`SkeletonAnimation` 不再是 `SkeletonRenderer` 的子类)。
### 必需的升级步骤(按顺序):
1. **🔒 备份您的项目**
在升级之前创建完整备份。这些更改将修改您的场景和预制件。
2. **📖 阅读整个文档**
在继续之前了解所有破坏性更改。
3. **✏️ 测试并更新您的代码**
- **更新您的脚本:** 根据本文档修复引用 Spine 组件的自定义脚本,因为当成员被移动到分离的类时,您的代码可能无法编译
- **测试:** 打开一些测试场景以查看哪些组件引用丢失。未保存的场景仍然包含旧的组件数据并作为安全备份 - 只有当您保存时,分离的组件才会替换旧组件并且引用会丢失。
**⚠️ 警告:** 预制件的行为不同!在 Prefab Edit Mode 中打开预制件会由于 Unity 的预制件自动保存功能而自动保存组件迁移。请始终先用场景进行测试,而不是预制件。
- **决定:** 要么手动重新分配丢失的引用,要么编写迁移代码(下面说明)来自动处理
**⚠️ 重要:** 自动组件分离仅在 Unity 编辑器中打开场景/预制件时发生。您必须选择 `Upgrade All` 或保存每个场景/预制件后再构建游戏,否则旧的单一组件不会被分离,构建中可能会缺少一半所需的组件!
**💡 提示:** 如果您遇到控制台日志输出 "SendMessage cannot be called during Awake, CheckConsistency, or OnValidate",这表明场景中存在旧的未迁移资产,它们刚刚被自动升级。这些消息之后会跟随一条日志消息,确认旧组件已自动迁移为分离组件。
4. **🔄 选择您的升级路径**
**选项 A - 手动重新分配(推荐用于小型项目):**
- 逐个打开每个场景和预制件
- 在检查器中手动重新分配丢失的引用
- 满意后保存
**选项 B - 自动迁移(用于具有许多场景/预制件的大型项目):**
- 首先编写迁移代码:
- 实现引用迁移模式(参见下面的"防止丢失组件引用"部分)
- 对于 `SkeletonGraphic` 引用,请参见"将现有引用更改为 SkeletonAnimation"部分
- 在几个文件上测试迁移代码
- 然后使用 `Upgrade All`
- 转到 `Edit → Preferences → Spine`
- 在 `Upgrade Scenes & Prefabs`
- 点击 `Upgrade All` 按钮
### 如果您不准备会发生什么破坏:
- 一旦您保存场景或预制件,场景或预制件中的组件引用可能会丢失(设置为 null
- 在升级所有场景/预制件之前构建将导致构建中缺少组件
**在完成步骤 1-3 之前不要继续,否则您可能会破坏您的项目。**
---
## 📦 从 4.2 的可选两步迁移
如果您正在从 spine-unity 4.2 迁移,您可能会发现分两步升级更容易隔离潜在问题:
### 步骤 1升级到 4.3-beta 预分离版本
首先升级到组件分离更改之前的 4.3-beta 版本:
- **提交哈希**: `a07b1de`
- **通过 Git URL 添加包**: `https://github.com/EsotericSoftware/spine-runtimes.git?path=spine-csharp/src#a07b1de`
- 或 **unitypackage**: https://esotericsoftware.com/files/runtimes/unity/spine-unity-4.2-2025-09-26.unitypackage
此中间步骤允许您:
- 首先修复与 4.2 → 4.3 spine-csharp API 更改相关的任何问题并确保您的项目正常工作
- 在继续之前验证您的项目是否稳定
- 一旦您的项目使用此 4.3-beta 版本正常工作,在继续步骤 2 之前创建另一个备份
### 步骤 2升级到最新的 4.3-beta 版本(带有分离的组件)
一旦您的项目使用 4.3-beta 正常工作:
- 升级到最新的 4.3-beta 包
- 按照上面的组件分离迁移指南
- 处理分离的动画和渲染组件
这种两步方法有助于隔离问题 - 如果出现问题,您将知道是由于 4.2 → 4.3 spine-csharp 更改还是组件分离特定导致的。
---
## 📋 介绍
spine-unity 4.3 运行时引入了一个重大的架构更改:**通过使用两个独立的组件而不是组件继承来分离动画和渲染**。这使得灵活的组合成为可能,例如使用 `SkeletonMecanim` 进行动画和 `SkeletonGraphic` 进行渲染,这在以前是不可能的。
### 主要更改:
- **组件分离**: 以前的单一组件已分离为独立的渲染和动画组件。
- **独立组件**: `SkeletonAnimation``SkeletonMecanim` 不再继承自 `SkeletonRenderer`。它们现在与单独的渲染器组件(`ISkeletonRenderer` 的子类)一起工作,例如 `SkeletonRenderer``SkeletonGraphic`
- **接口更新**: 新的 `ISkeletonRenderer``ISkeletonAnimation` 接口具有更新的属性名称。
- **设置分组**: 网格生成设置现在分组在 `MeshSettings` 属性下。
- **自动迁移**: 当定义了 `AUTO_UPGRADE_TO_43_COMPONENTS`*默认*Unity 编辑器将自动升级您的组件到新的分离组件并转移已弃用的字段。
- **升级所有场景和预制件**: 要一次升级所有场景和预制件,请转到 `Edit - Preferences - Spine` 并在 `Automatic Component Upgrade` 下点击 `Upgrade Scenes & Prefabs` - `Upgrade All`
### 组件关系:
| **旧架构** | **新架构** |
|---------------------|---------------------|
| `SkeletonAnimation` 继承自 `SkeletonRenderer` | `SkeletonAnimation` + 独立的 `SkeletonRenderer` 组件 |
| `SkeletonMecanim` 继承自 `SkeletonRenderer` | `SkeletonMecanim` + 独立的 `SkeletonRenderer` 组件 |
| `SkeletonGraphic` 提供嵌入式 AnimationState | `SkeletonGraphic` + 独立的 `SkeletonAnimation` 组件 |
---
## 调整您的代码
以下是每个组件的更改:
## ▶️ SkeletonRenderer
### 破坏性更改
#### 1. 接口更改
- `IHasSkeletonRenderer` 接口属性名称从 `SkeletonRenderer` 更改为 `Renderer`,类型从 `SkeletonRenderer` 更改为 `ISkeletonRenderer`
#### 2. 事件更改
- `SkeletonRendererDelegate` 类型不再是 `SkeletonRenderer` 中的嵌套类型。
要修复任何编译错误,请将 `SkeletonRenderer.SkeletonRendererDelegate` 替换为 `SkeletonRendererDelegate`
- `BeforeApply` 委托类型从 `SkeletonRendererDelegate` 更改为 `SkeletonAnimationDelegate`
- `SkeletonRendererDelegate` 类型从 `SkeletonRendererDelegate(SkeletonRenderer)` 更改为 `SkeletonRendererDelegate(ISkeletonRenderer)`。这影响事件:`OnRebuild``OnMeshAndMaterialsUpdated`
要修复任何编译错误,请将您的方法参数从 `SkeletonRenderer` 更改为 `ISkeletonRenderer`
- 将骨骼事件 `UpdateLocal``UpdateWorld``UpdateComplete``ISkeletonAnimation` 类(`SkeletonAnimation``SkeletonMecanim`)移动到 `ISkeletonRenderer`SkeletonRenderer、SkeletonGraphic。委托类型从 `UpdateBonesDelegate` 更改为 `SkeletonRendererDelegate`,参数为 `ISkeletonRenderer` 而不是 `ISkeletonAnimation`
#### 2. 方法更改
- `LateUpdateMesh()` 已更改为 `UpdateMesh()`
- 移除 `MeshGenerator.TryReplaceMaterials`
#### 3. 执行顺序
- `SkeletonRenderer``SkeletonGraphic` 组件收到 `DefaultExecutionOrder(1)]`,使它们在默认*order=0*脚本之后运行。这确保即使 `UpdateTiming` 设置为 `InLateUpdate`,动画也会在骨架更新之前应用。
#### 4. 行为更改
- 当 `singleSubmesh` 启用时,`generateMeshOverride` 现在也会被调用。
### 字段和属性迁移
#### 网格生成设置
| **旧 API** | **新 API** | **备注** |
|-------------------|-------------------|-----------|
| `skeletonRenderer.zSpacing` | `skeletonRenderer.MeshSettings.zSpacing` | 移动到 MeshSettings |
| `skeletonRenderer.useClipping` | `skeletonRenderer.MeshSettings.useClipping` | 移动到 MeshSettings |
| `skeletonRenderer.immutableTriangles` | `skeletonRenderer.MeshSettings.immutableTriangles` | 移动到 MeshSettings |
| `skeletonRenderer.pmaVertexColors` | `skeletonRenderer.MeshSettings.pmaVertexColors` | 移动到 MeshSettings |
| `skeletonRenderer.tintBlack` | `skeletonRenderer.MeshSettings.tintBlack` | 移动到 MeshSettings |
| `skeletonRenderer.addNormals` | `skeletonRenderer.MeshSettings.addNormals` | 移动到 MeshSettings |
| `skeletonRenderer.calculateTangents` | `skeletonRenderer.MeshSettings.calculateTangents` | 移动到 MeshSettings |
#### 已弃用的小写属性
- 小写属性 `initialFlipX``initialFlipY``initialSkinName` 现已弃用,将在未来的运行时版本中删除。使用添加的相同名称但大写的属性:`InitialFlipX``InitialFlipY``InitialSkinName`
---
## ▶️ SkeletonAnimation
### 破坏性更改
#### 1. 组件架构
- **SkeletonAnimation 现在是与 SkeletonRenderer 分离的组件**,不再是子类。
- 通过以下方式访问渲染器:`skeletonAnimation.Renderer`
- 从渲染器访问动画:`skeletonRenderer.Animation`
#### 2. 属性更改
- `state` 不再是公共的。使用 `AnimationState` 属性代替。
- `valid` 已移除。使用 `IsValid` 代替。
#### 3. 方法更改
- 添加 `UpdateOncePerFrame()` - 如果本帧尚未调用 `Update` 则更新。现有的 `Update(float time)` 仍然在调用时始终更新。
- 不带参数的 `Update()` 不再是公共的,使用 `UpdateOncePerFrame()` 在本帧未执行更新时更新,
或使用 `Update(0)` 强制动画更新。
### 字段和属性迁移
由于 `SkeletonAnimation` 现在是一个独立的组件,`SkeletonRenderer` 提供的方法和属性不能再通过 `SkeletonAnimation` 对象直接访问。有一个 `skeletonAnimation.Renderer` 属性可用于从 `SkeletonAnimation` 对象访问关联的 `ISkeletonRenderer`
以及一个 `skeletonRenderer.Animation` 属性从 `SkeletonRenderer``SkeletonGraphic` 对象访问关联的 `ISkeletonAnimation`。对于 `ISkeletonRenderer` 接口未公开的成员,您可以将 `skeletonAnimation.Renderer` 转换为 `SkeletonRenderer``SkeletonGraphic` 以访问渲染器成员变量。
### 示例代码
```csharp
// 旧的从 SkeletonAnimation 访问 SkeletonRenderer 属性
skeletonAnimation.zSpacing = 0.1f;
skeletonAnimation.AnySkeletonRendererProperty;
// 新的从 SkeletonAnimation 访问 SkeletonRenderer 属性
skeletonAnimation.Renderer.MeshSettings.zSpacing = 0.1f; // 在 ISkeletonRenderer 接口中公开
var skeletonRenderer = (SkeletonRenderer)skeletonAnimation.Renderer;
skeletonRenderer.AnySkeletonRendererProperty; // 未在 ISkeletonRenderer 接口中公开
```
---
## ▶️ SkeletonMecanim
### 破坏性更改
#### 1. 组件架构
- **SkeletonMecanim 现在是与 SkeletonRenderer 分离的组件**,不再是子类。
- 与 [SkeletonAnimation](#▶️-skeletonanimation) 相同的访问模式。
#### 2. 方法更改
- `Update()` 不再是公共的。
- 使用 `UpdateIfNecessary()``Update(0)` 强制更新。
### 字段和属性迁移
与上面的 [SkeletonAnimation](#▶️-skeletonanimation) 相同。
---
## ▶️ SkeletonGraphic
### 破坏性更改
#### 1. 组件架构
- **SkeletonGraphic 不再涵盖动画,添加 SkeletonAnimation 作为独立的动画组件**
- 通过以下方式访问动画:`skeletonGraphic.Animation`
#### 2. 将现有引用更改为 SkeletonAnimation
如果您的组件仅为了修改其动画属性而持有对 `SkeletonGraphic` 的引用,建议将引用类型更改为 `SkeletonAnimation`
这样您可以像 `skeletonAnimation.AnimationState` 一样访问动画状态
而不必像 `((SkeletonAnimation)skeletonGraphic.Animation).AnimationState` 那样进行转换。请注意,如果您想更改序列化组件变量的名称,可以在变量定义前使用 `[FormerlySerializedAs("previousName")]` 属性来自动重新分配您现有的序列化值。
#### 示例代码
```csharp
// 自动重新分配先前的序列化值
[FormerlySerializedAs("skeletonGraphic")]
public SkeletonAnimation skeletonAnimation; // 升级后将保持引用
```
#### 3. 属性更改
- 移除属性 `AnimationState` - 改为从 `SkeletonAnimation` 组件查询。
- `MeshGenerator` 不再是公共的 - 使用 `MeshSettings` 属性和 `SetMeshSettings()` 代替。
- `MaterialsMultipleCanvasRenderers` 类型从 `ExposedList<Material>` 更改为 `Material[]`
#### 4. 创建辅助方法
- `NewSkeletonGraphicGameObject` 现在创建 `SkeletonGraphic``SkeletonAnimation` 以保持现有行为。返回类型已更改为在单个结构中返回两个组件引用。
- `AddSkeletonGraphicComponent` 被移除并替换为:
- `AddSkeletonGraphicAnimationComponents` - 创建 `SkeletonGraphic``SkeletonAnimation` 组件。
- `AddSkeletonGraphicRenderingComponent` - 仅创建 `SkeletonGraphic` 组件。
#### 5. 事件更改
- 委托签名从 `SkeletonRendererDelegate(SkeletonGraphic)` 更改为 `SkeletonRendererDelegate(ISkeletonRenderer)`。这影响事件:`OnRebuild``OnMeshAndMaterialsUpdated`
要修复任何编译错误,请将您的方法参数从 SkeletonGraphic 更改为 ISkeletonRenderer。
#### 6. 执行顺序
- `SkeletonRenderer``SkeletonGraphic` 组件收到 `DefaultExecutionOrder(1)]`,使它们在默认*order=0*脚本之后运行。这确保即使 `UpdateTiming` 设置为 `InLateUpdate`,动画也会在骨架更新之前应用。
#### 7. 行为更改 - 材质更新
- 每个 `CanvasRenderer` 的材质不再在每个 `LateUpdate` 中更新,而是在以下情况下更新:
- a) 更新的骨架需要更改材质,或
- b) 当 `CustomMaterialOverride``CustomTextureOverride` 被访问并因此可能被修改时。
### 字段和属性迁移
#### 动画属性
| **旧 API** | **新 API** | **备注** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.AnimationState` | `((SkeletonAnimation)skeletonGraphic.Animation).AnimationState` | 需要转换 |
| `skeletonGraphic.startingAnimation` | `skeletonAnimation.AnimationName` | 通过动画组件 |
| `skeletonGraphic.startingLoop` | `skeletonAnimation.loop` | 通过动画组件 |
| `skeletonGraphic.timeScale` | `skeletonAnimation.timeScale` | 通过动画组件 |
| `skeletonGraphic.unscaledTime` | `skeletonAnimation.unscaledTime` | 通过动画组件 |
#### 网格生成器设置
| **旧 API** | **新 API** | **备注** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.MeshGenerator.settings` | `skeletonGraphic.MeshSettings` | 直接访问设置 |
---
## ⚠️ 其他重要说明
### 防止丢失组件引用
任何引用 `SkeletonRenderer` 组件并具有 `SkeletonAnimation``SkeletonMecanim` 目标的其他组件(例如 `SkeletonRenderSeparator`)在升级后将指向 *null*,因为 `SkeletonAnimation``SkeletonMecanim` 组件不再是 `SkeletonRenderer` 的子类,因此不是有效的引用。手动解决方案是将类型保留为 `SkeletonRenderer` 并丢失对 `SkeletonAnimation` 的引用(将设置为 *none*)。然后您需要在场景和预制件中手动重新分配丢失的引用。半自动替代解决方案如下:将您的 `SkeletonRenderer` 变量的类型更改为 `Component`(以捕获对象引用而不丢失它)并添加类型为 `SkeletonRenderer`(或 `SkeletonAnimation`)的第二个变量,然后以编程方式从 `Component` 变量读取它并将其分配给新添加的变量。例如,您可以在 Unity 编辑器中的 `Awake()` 中自动执行此操作,并向您的组件添加 `[ExecuteAlways]` 标签。
#### 示例代码
```csharp
// 升级前的旧类
public class TestMigrateReferences : MonoBehaviour {
public SkeletonRenderer skeletonRenderer; // 升级后此处分配的 SkeletonAnimation 引用将丢失。
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
}
// 升级后的新类
[ExecuteAlways] // 或 [ExecuteInEditMode]
public class TestMigrateReferences : MonoBehaviour {
#if UNITY_EDITOR
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer; // 这捕获在名称 skeletonRenderer 处分配的旧 SkeletonAnimation 引用。
#endif
public SkeletonRenderer skeletonRenderer;
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
#if UNITY_EDITOR
public void Awake () {
AutoUpgradeReferences();
}
public void AutoUpgradeReferences () {
if (previousSkeletonRenderer != null && skeletonRenderer == null) {
skeletonRenderer = previousSkeletonRenderer.GetComponent<SkeletonRenderer>();
if (skeletonRenderer != null)
Debug.Log("Upgraded SkeletonRenderer reference.");
}
}
#endif
}
```
### 组件启用/禁用
由于 `ISkeletonRenderer``ISkeletonAnimation` 组件现在是分离的,启用/禁用任何这些组件的脚本需要调整以**启用/禁用两者**。
### SkeletonUtilityBone 行为更改
在 Override 模式下,`SkeletonUtilityBone` 不再在 `UpdatePhase.World` 中调整 Transform仅在 `UpdatePhase.Complete` 中(移除冗余更新)。
### 自动迁移
- 当定义了 `AUTO_UPGRADE_TO_43_COMPONENTS`Unity 编辑器会自动转移已弃用的字段。
- 要一次升级所有场景和预制件,请转到 `Edit - Preferences - Spine` 并选择 `Upgrade Scenes & Prefabs` - `Upgrade All`
- 每个类中的 `UpgradeTo43``TransferDeprecatedFields()` 方法处理序列化数据迁移。
- 运行时访问仍需要手动代码更新。
### 最常见更改的摘要
1. **添加 `.Renderer.`** 前缀以从动画组件访问渲染属性。
2. **添加 `.MeshSettings.`** 以访问网格生成设置。
3. **转换为 SkeletonAnimation** 当从 SkeletonGraphic 访问 AnimationState 时。
4. **更新委托方法签名** 从具体类型到接口。
5. **重新分配丢失的引用** 使用上述迁移模式升级后。
---
## 禁用自动升级检查
一旦您完成了所有 Spine 资产、场景和预制件的迁移,您可以禁用自动升级检查以提高编辑器性能:
1. 转到 `Edit → Preferences → Spine`
2. 在 `Automatic Component Upgrade` 下,点击 `Split Component Upgrade``Disable`
这将阻止 Unity 编辑器在场景或预制件加载时执行编辑器内检查以确定组件是否需要迁移。如果您需要迁移其他资产,您可以随时重新启用它。
---
## 需要帮助?
如果您在迁移期间遇到任何意外问题或发现组件属性迁移不正确,请在 [Spine 论坛](https://esotericsoftware.com/forum) 发帖。我们很乐意提供帮助并修复任何问题,使自动迁移尽可能无痛。

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6c12bf9dde3ca4146b5d422361d438de
timeCreated: 1759835002
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,371 @@
# spine-unity 4.3 Split Component Upgrade Guide
## Component Architecture Restructuring
> **Note:** You can find Chinese and Japanese translations of this upgrade guide right next to this file.
> * [中文版 4.3 分体组件升级指南 - Chinese 4.3 Split Component Upgrade Guide](https://github.com/EsotericSoftware/spine-runtimes/tree/4.3-beta/spine-unity/Assets/Spine/Documentation/4.3-split-component-upgrade-guide-zh.md)
> * [日本語版 4.3 分割コンポーネントアップグレードガイド - Japanese 4.3 Split Component Upgrade Guide](https://github.com/EsotericSoftware/spine-runtimes/tree/4.3-beta/spine-unity/Assets/Spine/Documentation/4.3-split-component-upgrade-guide-ja.md)
---
## 📋 Scope of This Document
**This document covers only the spine-unity component split migration.** From version 4.2 to 4.3, there have been additional breaking changes in both spine-csharp and spine-unity that you need to address before handling the component split:
- **spine-csharp API changes:** New pose system affecting bone, slot, and constraint properties
For complete migration steps covering all 4.2 to 4.3 changes, please refer to:
- The [CHANGELOG.md](https://github.com/EsotericSoftware/spine-runtimes/blob/4.3-beta/CHANGELOG.md#c-2) document (sections C# and Unity)
- The forum post ["Spine-Unity 4.2 to 4.3 Upgrade Guide"](https://esotericsoftware.com/forum) for a comprehensive migration guide
**Important:** Complete the spine-csharp API migration first, then proceed with the component split migration described in this document.
---
## ⚠️ IMPORTANT: Before You Upgrade
**This warning only applies to existing projects being upgraded. New projects can safely ignore this section.**
### What Will Happen
Components will be **automatically upgraded** and split into separate animation and rendering components:
- `SkeletonAnimation``SkeletonAnimation` + `SkeletonRenderer` components
- `SkeletonMecanim``SkeletonMecanim` + `SkeletonRenderer` components
- `SkeletonGraphic``SkeletonAnimation` + `SkeletonGraphic` components
All component settings and fields will be automatically transferred - nothing will be lost.
**However:** Due to these type changes, existing references in your custom scripts may be lost because the component types no longer match (e.g., `SkeletonAnimation` is no longer a subclass of `SkeletonRenderer`).
### Required Upgrade Steps (In Order):
1. **🔒 BACKUP YOUR PROJECT**
Create a complete backup before upgrading. These changes will modify your scenes and prefabs.
2. **📖 READ THIS ENTIRE DOCUMENT**
Understand all breaking changes before proceeding.
3. **✏️ TEST AND UPDATE YOUR CODE**
- **Update your scripts:** Fix custom scripts that reference Spine components according to this document, as your code may no longer compile when members were moved to the split classes
- **Test:** Open a few test scenes to see which component references are lost. Unsaved scenes still contain the old component data and serve as a safety backup - only when you save will the split components replace the old ones and references be lost.
**⚠️ WARNING:** Prefabs behave differently! Opening a prefab in Prefab Edit Mode will automatically save the component migration due to Unity's prefab auto-save feature. Always test with scenes first, not prefabs.
- **Decide:** Either manually re-assign lost references, or write migration code (explained below) to handle this automatically
**⚠️ IMPORTANT:** Automatic component splitting only happens when scenes/prefabs are opened in the Unity Editor. You must choose 'Upgrade All' or save each scene/prefab before building your game, otherwise the old single components won't be split and half the required components might be missing in the build!
**💡 HINT:** If you encounter the Console log output "SendMessage cannot be called during Awake, CheckConsistency, or OnValidate", this is a sign that old un-migrated assets were present in the scene which were just auto-upgraded. These messages will be followed by a log message confirming that an old component was auto-migrated to split components.
4. **🔄 CHOOSE YOUR UPGRADE PATH**
**Option A - Manual re-assignment (recommended for small projects):**
- Open each scene and prefab individually
- Manually re-assign lost references in the Inspector
- Save when satisfied
**Option B - Automatic migration (for large projects with many scenes/prefabs):**
- Write migration code first:
- Implement the reference migration pattern (see "Preventing Lost Component References" section below)
- For `SkeletonGraphic` references, see "Changing Existing References to SkeletonAnimation" section
- Test migration code on a few files
- Then use `Upgrade All`:
- Go to `Edit → Preferences → Spine`
- Under "Upgrade Scenes & Prefabs"
- Click `Upgrade All` button
### What Will Break If You Don't Prepare:
- Component references in scenes or prefabs may be lost (set to null) once you save your scenes or prefabs
- Building before upgrading all scenes/prefabs may result in missing components in the build
**Do not proceed without completing steps 1-3, or you risk breaking your project.**
---
## 📦 Optional Two-Step Migration from 4.2
If you're migrating from spine-unity 4.2, you may find it easier to upgrade in two steps to isolate potential issues:
### Step 1: Upgrade to a 4.3-beta pre-split version
First upgrade to a 4.3-beta version before the component split changes:
- **Commit Hash**: `a07b1de`
- **add package via Git URL**: `https://github.com/EsotericSoftware/spine-runtimes.git?path=spine-csharp/src#a07b1de`
- or **unitypackage**: https://esotericsoftware.com/files/runtimes/unity/spine-unity-4.2-2025-09-26.unitypackage
This intermediate step allows you to:
- Fix any issues related to the 4.2 → 4.3 spine-csharp API changes first and ensures your project works
- Verify your project is stable before proceeding
- Once your project is working with this 4.3-beta version, create another backup before proceeding to step 2
### Step 2: Upgrade to latest 4.3-beta version (with split components)
Once your project is working with 4.3-beta:
- Upgrade to the latest 4.3-beta package
- Follow the component split migration guide above
- Handle the separated animation and rendering components
This two-step approach helps isolate issues - if something breaks, you'll know whether it's due to the 4.2 → 4.3 spine-csharp changes or the component split specifically.
---
## 📋 Introduction
The spine-unity 4.3 runtime introduces a major architectural change: **separation of animation from rendering by using two separate components instead of component inheritance**. This enables flexible combinations like using `SkeletonMecanim` for animation with `SkeletonGraphic` for rendering, which was previously not possible.
### Key Changes:
- **Component Split**: The previously monolithic components have been split into separate rendering and animation components.
- **Separate Components**: `SkeletonAnimation` and `SkeletonMecanim` no longer inherit from `SkeletonRenderer`. They now work alongside a separate renderer component (a subclass of `ISkeletonRenderer`), such as `SkeletonRenderer` and `SkeletonGraphic`.
- **Interface Updates**: New `ISkeletonRenderer` and `ISkeletonAnimation` interfaces with updated property names.
- **Settings Grouping**: Mesh generation settings are now grouped under `MeshSettings` property.
- **Automatic Migration**: The Unity Editor will automatically upgrade your components to the new split components and transfer deprecated fields when `AUTO_UPGRADE_TO_43_COMPONENTS` is defined (*the default*).
- **Upgrade all Scenes and Prefabs**: To upgrade all scenes and prefabs at once, go to `Edit - Preferences - Spine` and under `Automatic Component Upgrade` hit `Upgrade Scenes & Prefabs` - `Upgrade All`.
### Component Relationships:
| **Old Architecture** | **New Architecture** |
|---------------------|---------------------|
| `SkeletonAnimation` inherits from `SkeletonRenderer` | `SkeletonAnimation` + separate `SkeletonRenderer` component |
| `SkeletonMecanim` inherits from `SkeletonRenderer` | `SkeletonMecanim` + separate `SkeletonRenderer` component |
| `SkeletonGraphic` provides embedded AnimationState | `SkeletonGraphic` + separate `SkeletonAnimation` component |
---
## Adapting Your Code
Here's what changes for each component:
## ▶️ SkeletonRenderer
### Breaking Changes
#### 1. Interface Changes
- `IHasSkeletonRenderer` interface changed property name from `SkeletonRenderer` to `Renderer`, type from `SkeletonRenderer` to `ISkeletonRenderer`.
#### 2. Event Changes
- `SkeletonRendererDelegate` type is no longer a nested type in `SkeletonRenderer`.
To fix any compile errors, replace `SkeletonRenderer.SkeletonRendererDelegate` with `SkeletonRendererDelegate`.
- `BeforeApply` delegate type changed from `SkeletonRendererDelegate` to `SkeletonAnimationDelegate`.
- `SkeletonRendererDelegate` type changed from `SkeletonRendererDelegate(SkeletonRenderer)` to `SkeletonRendererDelegate(ISkeletonRenderer)`. This affects events: `OnRebuild`, `OnMeshAndMaterialsUpdated`.
To fix any compile errors, change your method parameter from `SkeletonRenderer` to `ISkeletonRenderer`.
- Moved bone events `UpdateLocal`, `UpdateWorld`, and `UpdateComplete` from `ISkeletonAnimation` classes (`SkeletonAnimation`, `SkeletonMecanim`) to `ISkeletonRenderer classes (SkeletonRenderer, SkeletonGraphic)`. Changed delegate type from `UpdateBonesDelegate` to `SkeletonRendererDelegate`, with parameter `ISkeletonRenderer` instead of `ISkeletonAnimation`.
#### 2. Method Changes
- `LateUpdateMesh()` has been changed to `UpdateMesh()`.
- Removed `MeshGenerator.TryReplaceMaterials`.
#### 3. Execution Order
- `SkeletonRenderer` and `SkeletonGraphic` components received `DefaultExecutionOrder(1)]` which makes them run after default *(order=0)* scripts. This ensures animations are applied before the skeleton is updated even if `UpdateTiming` is set to `InLateUpdate`.
#### 4. Behaviour Changes
- `generateMeshOverride` is now also called when `singleSubmesh` is enabled.
### Field and Property Migration
#### Mesh Generation Settings
| **Old API** | **New API** | **Notes** |
|-------------------|-------------------|-----------|
| `skeletonRenderer.zSpacing` | `skeletonRenderer.MeshSettings.zSpacing` | Moved to MeshSettings |
| `skeletonRenderer.useClipping` | `skeletonRenderer.MeshSettings.useClipping` | Moved to MeshSettings |
| `skeletonRenderer.immutableTriangles` | `skeletonRenderer.MeshSettings.immutableTriangles` | Moved to MeshSettings |
| `skeletonRenderer.pmaVertexColors` | `skeletonRenderer.MeshSettings.pmaVertexColors` | Moved to MeshSettings |
| `skeletonRenderer.tintBlack` | `skeletonRenderer.MeshSettings.tintBlack` | Moved to MeshSettings |
| `skeletonRenderer.addNormals` | `skeletonRenderer.MeshSettings.addNormals` | Moved to MeshSettings |
| `skeletonRenderer.calculateTangents` | `skeletonRenderer.MeshSettings.calculateTangents` | Moved to MeshSettings |
#### Deprecated Lowercase Attributes
- Lowercase attributes `initialFlipX`, `initialFlipY` and `initialSkinName` are now deprecated and will be removed in future runtime versions. Use the added properties of the same name but uppercase instead: `InitialFlipX`, `InitialFlipY` and `InitialSkinName`.
---
## ▶️ SkeletonAnimation
### Breaking Changes
#### 1. Component Architecture
- **SkeletonAnimation is now a separate component from SkeletonRenderer**, no longer a subclass.
- Access renderer via: `skeletonAnimation.Renderer`.
- Access animation from renderer via: `skeletonRenderer.Animation`.
#### 2. Property Changes
- `state` is no longer public. Use `AnimationState` property instead.
- `valid` removed. Use `IsValid` instead.
#### 3. Method Changes
- Added `UpdateOncePerFrame()` - updates if `Update` has not been called this frame. The existing `Update(float time)` still always updates when called.
- `Update()` without parameters is no longer public, use either `UpdateOncePerFrame()` to update when no update has been performed this frame,
or `Update(0)` to force an animation update.
### Field and Property Migration
As `SkeletonAnimation` is now a separate component, methods and properties provided by `SkeletonRenderer` can no longer be accessed directly via a `SkeletonAnimation` object. There is a `skeletonAnimation.Renderer` property available to access the associated `ISkeletonRenderer` from a `SkeletonAnimation` object,
and an `skeletonRenderer.Animation` property to access the associated `ISkeletonAnimation` from a `SkeletonRenderer` or `SkeletonGraphic` object. For members that are not exposed by the `ISkeletonRenderer` interface, you can cast `skeletonAnimation.Renderer` to `SkeletonRenderer` or `SkeletonGraphic` respectively to access the renderer member variables.
### Example Code
```csharp
// Old SkeletonRenderer property access from SkeletonAnimation
skeletonAnimation.zSpacing = 0.1f;
skeletonAnimation.AnySkeletonRendererProperty;
// New SkeletonRenderer property access from SkeletonAnimation
skeletonAnimation.Renderer.MeshSettings.zSpacing = 0.1f; // exposed in ISkeletonRenderer interface
var skeletonRenderer = (SkeletonRenderer)skeletonAnimation.Renderer;
skeletonRenderer.AnySkeletonRendererProperty; // not exposed in ISkeletonRenderer interface
```
---
## ▶️ SkeletonMecanim
### Breaking Changes
#### 1. Component Architecture
- **SkeletonMecanim is now a separate component from SkeletonRenderer**, no longer a subclass.
- Same access pattern as [SkeletonAnimation](#▶️-skeletonanimation).
#### 2. Method Changes
- `Update()` is no longer public.
- Use `UpdateIfNecessary()` or `Update(0)` to force update.
### Field and Property Migration
Same as [SkeletonAnimation](#▶️-skeletonanimation) above.
---
## ▶️ SkeletonGraphic
### Breaking Changes
#### 1. Component Architecture
- **SkeletonGraphic no longer covers animation, add SkeletonAnimation as a separate animation component**.
- Animation accessed via: `skeletonGraphic.Animation`.
#### 2. Changing Existing References to SkeletonAnimation
If your components are holding a reference to `SkeletonGraphic` only to modify its animation properties, it is recommended to change the reference type to `SkeletonAnimation`.
This way you can access animation state like `skeletonAnimation.AnimationState`
instead of having to cast it like `((SkeletonAnimation)skeletonGraphic.Animation).AnimationState`. Note that if you want to change the name of serialized component variables, you can use the `[FormerlySerializedAs("previousName")]` attribute in front of a variable definition to automatically reassign your existing serialized value.
#### Example Code
```csharp
// Automatically reassign previous serialized values
[FormerlySerializedAs("skeletonGraphic")]
public SkeletonAnimation skeletonAnimation; // Will maintain the reference after upgrade
```
#### 3. Property Changes
- Removed property `AnimationState` - query it from the `SkeletonAnimation` component instead.
- `MeshGenerator` is no longer public - use `MeshSettings` property and `SetMeshSettings()` instead.
- `MaterialsMultipleCanvasRenderers` type changed from `ExposedList<Material>` to `Material[]`.
#### 4. Creation Helper Methods
- `NewSkeletonGraphicGameObject` now creates both `SkeletonGraphic` and `SkeletonAnimation` to maintain existing behaviour. Return type changed to return both component references in a single struct.
- `AddSkeletonGraphicComponent` is removed and replaced by:
- `AddSkeletonGraphicAnimationComponents` - creates both `SkeletonGraphic` and `SkeletonAnimation` components.
- `AddSkeletonGraphicRenderingComponent` - creates only `SkeletonGraphic` component.
#### 5. Event Changes
- Delegate signature changed from `SkeletonRendererDelegate(SkeletonGraphic)` to `SkeletonRendererDelegate(ISkeletonRenderer)`. This affects events: `OnRebuild`, `OnMeshAndMaterialsUpdated`.
To fix any compile errors, change your method parameter from SkeletonGraphic to ISkeletonRenderer.
#### 6. Execution Order
- `SkeletonRenderer` and `SkeletonGraphic` components received `DefaultExecutionOrder(1)]` which makes them run after default *(order=0)* scripts. This ensures animations are applied before the skeleton is updated even if `UpdateTiming` is set to `InLateUpdate`.
#### 7. Behaviour Changes - Material Updates
- Materials at each `CanvasRenderer` are no longer updated every `LateUpdate`, instead they are updated when either:
- a) the updated skeleton requires a change of materials, or
- b) when `CustomMaterialOverride` or `CustomTextureOverride` were accessed and thus potentially modified.
### Field and Property Migration
#### Animation Properties
| **Old API** | **New API** | **Notes** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.AnimationState` | `((SkeletonAnimation)skeletonGraphic.Animation).AnimationState` | Cast required |
| `skeletonGraphic.startingAnimation` | `skeletonAnimation.AnimationName` | Via Animation component |
| `skeletonGraphic.startingLoop` | `skeletonAnimation.loop` | Via Animation component |
| `skeletonGraphic.timeScale` | `skeletonAnimation.timeScale` | Via Animation component |
| `skeletonGraphic.unscaledTime` | `skeletonAnimation.unscaledTime` | Via Animation component |
#### Mesh Generator Settings
| **Old API** | **New API** | **Notes** |
|-------------------|-------------------|-----------|
| `skeletonGraphic.MeshGenerator.settings` | `skeletonGraphic.MeshSettings` | Direct access to settings |
---
## ⚠️ Additional Important Notes
### Preventing Lost Component References
Any references by other components (e.g. `SkeletonRenderSeparator`) that reference a `SkeletonRenderer` component and had a `SkeletonAnimation` or `SkeletonMecanim` target will be pointing to *null* after the upgrade, since the `SkeletonAnimation` and `SkeletonMecanim` components are no longer subclasses of `SkeletonRenderer` and thus no valid reference. The manual solution is to leave the type as `SkeletonRenderer` and lose references to `SkeletonAnimation` (will be set to *none*). Then you need to manually re-assign the lost references in your scenes and prefabs. A semi-automatic alternative solution is as follows: change the type of your `SkeletonRenderer` variable to `Component` (to capture the object reference and not lose it) and add a second variable of type `SkeletonRenderer` (or `SkeletonAnimation`) and then programmatically read it from the `Component` variable and assign it to the newly added variable. You can e.g. do this automatically in the Unity Editor in `Awake()` with an `[ExecuteAlways]` tag added to your component.
#### Example code
```csharp
// Old class before upgrading
public class TestMigrateReferences : MonoBehaviour {
public SkeletonRenderer skeletonRenderer; // this SkeletonAnimation reference assigned here would be lost after upgrading.
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
}
// New class after upgrading
[ExecuteAlways] // or [ExecuteInEditMode]
public class TestMigrateReferences : MonoBehaviour {
#if UNITY_EDITOR
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer; // this captures the old SkeletonAnimation reference assigned at the name skeletonRenderer.
#endif
public SkeletonRenderer skeletonRenderer;
public void Foo () {
skeletonRenderer.skeleton.ScaleX = -1;
}
#if UNITY_EDITOR
public void Awake () {
AutoUpgradeReferences();
}
public void AutoUpgradeReferences () {
if (previousSkeletonRenderer != null && skeletonRenderer == null) {
skeletonRenderer = previousSkeletonRenderer.GetComponent<SkeletonRenderer>();
if (skeletonRenderer != null)
Debug.Log("Upgraded SkeletonRenderer reference.");
}
}
#endif
}
```
### Component Enable/Disable
Since `ISkeletonRenderer` and `ISkeletonAnimation` components are now separate, scripts that enable/disable any of these components need adjustment to **enable/disable both**.
### SkeletonUtilityBone Behaviour Change
In mode Override, `SkeletonUtilityBone` no longer adjusts the Transform in `UpdatePhase.World`, only in `UpdatePhase.Complete` (removes redundant update).
### Automatic Migration
- Unity Editor automatically transfers deprecated fields when `AUTO_UPGRADE_TO_43_COMPONENTS` is defined.
- To upgrade all scenes and prefabs at once, go to `Edit - Preferences - Spine` and select `Upgrade Scenes & Prefabs` - `Upgrade All`.
- The `UpgradeTo43` and `TransferDeprecatedFields()` methods in each class handles serialized data migration.
- Manual code updates are still required for runtime access.
### Summary of Most Common Changes
1. **Add `.Renderer.`** prefix to access rendering properties from animation components.
2. **Add `.MeshSettings.`** to access mesh generation settings.
3. **Cast to SkeletonAnimation** when accessing AnimationState from SkeletonGraphic.
4. **Update delegate method signatures** from concrete types to interfaces.
5. **Re-assign lost references** after upgrade using the migration pattern above.
---
## Disabling Automatic Upgrade Checks
Once you have completed the migration of all your Spine assets, scenes, and prefabs, you can disable the automatic upgrade checks to improve editor performance:
1. Go to `Edit → Preferences → Spine`
2. Under `Automatic Component Upgrade`, click `Split Component Upgrade``Disable`
This will stop the Unity Editor from performing in-editor checks upon scene or prefab loading to determine whether components need to be migrated. You can re-enable it at any time if you need to migrate additional assets.
---
## Need Help?
If you encounter any unexpected problems during migration or find that component properties are incorrectly migrated, please post on the [Spine forum](https://esotericsoftware.com/forum). We're happy to help and fix any issues to make automatic migration as painless as possible.

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3c30cad54c352c542a01a50ad3b6b8fe
timeCreated: 1759519802
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,90 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated September 24, 2021. Replaces all prior versions.
*
* Copyright (c) 2013-2023, 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.
*****************************************************************************/
using Spine;
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
[CustomPropertyDrawer(typeof(MaterialOverrideSet))]
public class MaterialOverrideSetDrawer : PropertyDrawer {
private const float Padding = 5f;
private const float ButtonWidth = 20f;
public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {
SerializedProperty dictionaryKeysProp = property.FindPropertyRelative("dictionaryKeys");
return EditorGUIUtility.singleLineHeight * (dictionaryKeysProp.arraySize + 2);
}
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) {
EditorGUI.BeginProperty(position, label, property);
SerializedProperty nameProperty = property.FindPropertyRelative("name");
SerializedProperty dictionaryKeysProperty = property.FindPropertyRelative("dictionaryKeys");
SerializedProperty dictionaryValuesProperty = property.FindPropertyRelative("dictionaryValues");
Rect labelPosition = new Rect(position.x, position.y, position.width * 0.5f, EditorGUIUtility.singleLineHeight);
Rect namePosition = new Rect(position.x + position.width * 0.5f, position.y, position.width * 0.5f, EditorGUIUtility.singleLineHeight);
EditorGUI.LabelField(labelPosition, label);
nameProperty.stringValue = EditorGUI.TextField(namePosition, GUIContent.none, nameProperty.stringValue);
Rect contentPosition = EditorGUI.IndentedRect(new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight, position.width, position.height - EditorGUIUtility.singleLineHeight));
float lineHeight = EditorGUIUtility.singleLineHeight;
for (int i = 0; i < dictionaryKeysProperty.arraySize; i++) {
Rect keyRect = new Rect(contentPosition.x, contentPosition.y + i * lineHeight, contentPosition.width * 0.5f, lineHeight);
Rect valueRect = new Rect(contentPosition.x + contentPosition.width * 0.5f, contentPosition.y + i * lineHeight, contentPosition.width * 0.5f - ButtonWidth, lineHeight);
Rect removeButtonRect = new Rect(contentPosition.xMax - ButtonWidth, contentPosition.y + i * lineHeight, ButtonWidth, lineHeight);
EditorGUI.PropertyField(keyRect, dictionaryKeysProperty.GetArrayElementAtIndex(i), GUIContent.none);
EditorGUI.PropertyField(valueRect, dictionaryValuesProperty.GetArrayElementAtIndex(i), GUIContent.none);
if (GUI.Button(removeButtonRect, "-")) {
dictionaryKeysProperty.DeleteArrayElementAtIndex(i);
dictionaryValuesProperty.DeleteArrayElementAtIndex(i);
break;
}
}
int indent = 15;
Rect addButtonRect = new Rect(contentPosition.x + indent, contentPosition.y + dictionaryKeysProperty.arraySize * lineHeight, contentPosition.width - indent, lineHeight);
if (GUI.Button(addButtonRect, "+ Add Entry")) {
dictionaryKeysProperty.InsertArrayElementAtIndex(dictionaryKeysProperty.arraySize);
dictionaryValuesProperty.InsertArrayElementAtIndex(dictionaryValuesProperty.arraySize);
}
EditorGUI.EndProperty();
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 90d399311223a9040a5e7d32742a1bbe
timeCreated: 1686129585
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -33,7 +33,6 @@
#define SPINE_UNITY_2018_PREVIEW_API
#endif
using System;
using System.Collections.Generic;
using System.Linq;
@ -793,8 +792,7 @@ namespace Spine.Unity.Editor {
List<Spine.Event> currentAnimationEvents = new List<Spine.Event>();
List<float> currentAnimationEventTimes = new List<float>();
List<SpineEventTooltip> currentAnimationEventTooltips = new List<SpineEventTooltip>();
public bool IsValid { get { return skeletonAnimation != null && skeletonAnimation.valid; } }
public bool IsValid { get { return skeletonAnimation != null && skeletonAnimation.IsValid; } }
public Skeleton Skeleton { get { return IsValid ? skeletonAnimation.Skeleton : null; } }
@ -885,8 +883,9 @@ namespace Spine.Unity.Editor {
previewGameObject.hideFlags = HideFlags.HideAndDontSave;
previewGameObject.layer = PreviewLayer;
skeletonAnimation = previewGameObject.GetComponent<SkeletonAnimation>();
skeletonAnimation.initialSkinName = skinName;
skeletonAnimation.LateUpdate();
ISkeletonRenderer skeletonRenderer = skeletonAnimation.Renderer;
skeletonRenderer.InitialSkinName = skinName;
skeletonRenderer.LateUpdate();
previewGameObject.GetComponent<Renderer>().enabled = false;
#if SPINE_UNITY_2018_PREVIEW_API
@ -953,7 +952,7 @@ namespace Spine.Unity.Editor {
float deltaTime = (current - animationLastTime);
skeletonAnimation.Update(deltaTime);
animationLastTime = current;
skeletonAnimation.LateUpdate();
skeletonAnimation.Renderer.LateUpdate();
}
Camera thisPreviewUtilityCamera = this.PreviewUtilityCamera;
@ -1051,7 +1050,7 @@ namespace Spine.Unity.Editor {
return;
}
if (!skeletonAnimation.valid) return;
if (!skeletonAnimation.IsValid) return;
if (string.IsNullOrEmpty(animationName)) {
skeletonAnimation.Skeleton.SetupPose();
@ -1154,7 +1153,7 @@ namespace Spine.Unity.Editor {
void HandleSkinDropdownSelection (object o) {
Skin skin = (Skin)o;
skeletonAnimation.initialSkinName = skin.Name;
skeletonAnimation.Renderer.InitialSkinName = skin.Name;
skeletonAnimation.Initialize(true);
RefreshOnNextUpdate();
if (OnSkinChanged != null) OnSkinChanged(skin.Name);

View File

@ -42,7 +42,7 @@ namespace Spine.Unity.Editor {
[CustomEditor(typeof(SpineAtlasAsset)), CanEditMultipleObjects]
public class SpineAtlasAssetInspector : UnityEditor.Editor {
SerializedProperty atlasFile, materials, textureLoadingMode, onDemandTextureLoader;
SerializedProperty atlasFile, materials, materialOverrides, textureLoadingMode, onDemandTextureLoader;
SpineAtlasAsset atlasAsset;
GUIContent spriteSlicesLabel;
@ -73,6 +73,7 @@ namespace Spine.Unity.Editor {
textureLoadingMode = serializedObject.FindProperty("textureLoadingMode");
onDemandTextureLoader = serializedObject.FindProperty("onDemandTextureLoader");
materials.isExpanded = true;
materialOverrides = serializedObject.FindProperty("serializedMaterialOverrides");
atlasAsset = (SpineAtlasAsset)target;
#if REGION_BAKING_MESH
UpdateBakedList();
@ -134,12 +135,14 @@ namespace Spine.Unity.Editor {
}
}
EditorGUILayout.PropertyField(materialOverrides, true);
if (textureLoadingMode != null) {
EditorGUILayout.Space();
EditorGUILayout.PropertyField(textureLoadingMode);
EditorGUILayout.PropertyField(onDemandTextureLoader);
}
EditorGUILayout.Space();
if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Set Mipmap Bias to " + SpinePreferences.DEFAULT_MIPMAPBIAS, tooltip: "This may help textures with mipmaps be less blurry when used for 2D sprites."))) {
foreach (Material m in atlasAsset.materials) {

View File

@ -39,7 +39,7 @@ namespace Spine.Unity.Editor {
[CustomEditor(typeof(SpineSpriteAtlasAsset)), CanEditMultipleObjects]
public class SpineSpriteAtlasAssetInspector : UnityEditor.Editor {
SerializedProperty atlasFile, materials;
SerializedProperty atlasFile, materials, materialOverrides;
SpineSpriteAtlasAsset atlasAsset;
static List<AtlasRegion> GetRegions (Atlas atlas) {
@ -52,6 +52,7 @@ namespace Spine.Unity.Editor {
atlasFile = serializedObject.FindProperty("spriteAtlasFile");
materials = serializedObject.FindProperty("materials");
materials.isExpanded = true;
materialOverrides = serializedObject.FindProperty("materialOverrides");
atlasAsset = (SpineSpriteAtlasAsset)target;
if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded())
@ -82,6 +83,7 @@ namespace Spine.Unity.Editor {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(atlasFile);
EditorGUILayout.PropertyField(materials, true);
EditorGUILayout.PropertyField(materialOverrides, true);
if (EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
atlasAsset.Clear();

View File

@ -63,7 +63,7 @@ namespace Spine.Unity.Editor {
[MenuItem("CONTEXT/SkeletonRenderer/Add BoneFollower GameObject", true)]
static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) {
SkeletonRenderer skeletonRenderer = cmd.context as SkeletonRenderer;
return skeletonRenderer.valid;
return skeletonRenderer.IsValid;
}
[MenuItem("CONTEXT/BoneFollower/Rename BoneFollower GameObject")]
@ -109,7 +109,7 @@ namespace Spine.Unity.Editor {
if (skeletonRendererComponent == null) return;
Transform transform = skeletonRendererComponent.transform;
Skeleton skeleton = skeletonRendererComponent.skeleton;
Skeleton skeleton = skeletonRendererComponent.Skeleton;
if (string.IsNullOrEmpty(boneName.stringValue)) {
SpineHandles.DrawBones(transform, skeleton);
@ -200,10 +200,10 @@ namespace Spine.Unity.Editor {
} else {
boneFollowerSkeletonRenderer.Initialize(false);
if (boneFollowerSkeletonRenderer.skeletonDataAsset == null)
if (boneFollowerSkeletonRenderer.SkeletonDataAsset == null)
EditorGUILayout.HelpBox("Assigned SkeletonRenderer does not have SkeletonData assigned to it.", MessageType.Warning);
if (!boneFollowerSkeletonRenderer.valid)
if (!boneFollowerSkeletonRenderer.IsValid)
EditorGUILayout.HelpBox("Assigned SkeletonRenderer is invalid. Check target SkeletonRenderer, its SkeletonData asset or the console for other errors.", MessageType.Warning);
}
}

View File

@ -0,0 +1,463 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated January 1, 2020. Replaces all prior versions.
*
* Copyright (c) 2013-2020, 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.
*****************************************************************************/
#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#else
#define NO_PREFAB_MESH
#endif
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
using Icons = SpineEditorUtilities.Icons;
public class ISkeletonRendererInspector : UnityEditor.Editor {
public static bool advancedFoldout;
protected bool loadingFailed = false;
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
protected SerializedProperty skeletonDataAsset, initialSkinName;
protected SerializedProperty initialFlipX, initialFlipY;
protected SerializedProperty updateWhenInvisible, separatorSlotNames, enableSeparatorSlots;
protected SerializedProperty clearStateOnDisable, fixDrawOrder;
protected SerializedProperty useClipping, zSpacing, immutableTriangles;
protected SerializedProperty threadedMeshGeneration;
// Vertex Data parameters
protected SerializedProperty tintBlack, canvasGroupCompatible, pmaVertexColors, addNormals, calculateTangents;
protected SerializedProperty physicsPositionInheritanceFactor, physicsRotationInheritanceFactor, physicsMovementRelativeTo;
protected bool isInspectingPrefab;
protected bool forceReloadQueued = false;
private GUIContent SkeletonDataAssetLabel, SkeletonUtilityButtonContent;
protected readonly GUIContent ClearStateOnDisableLabel = new GUIContent(
"Clear State On Disable", "Use this if you are pooling or enabling/disabling your Spine GameObject.");
protected readonly GUIContent UseClippingLabel = new GUIContent("Use Clipping",
"When disabled, clipping attachments are ignored. This may be used to save performance.");
protected readonly GUIContent ZSpacingLabel = new GUIContent("Z Spacing",
"A value other than 0 adds a space between each rendered attachment to prevent Z Fighting when using shaders" +
" that read or write to the depth buffer. Large values may cause unwanted parallax and spaces depending on " +
"camera setup.");
protected readonly GUIContent ThreadedMeshGenerationLabel = new GUIContent("Use Threading",
"When enabled, mesh generation is performed on multiple threads in parallel.");
protected readonly GUIContent TintBlackLabel = new GUIContent("Tint Black (!)",
"Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret " +
"UV2 and UV3 as black tint colors for this effect to work. You may then want to use the " +
"[Spine/SkeletonGraphic Tint Black] shader.");
protected readonly GUIContent CanvasGroupCompatibleLabel = new GUIContent("CanvasGroup Compatible",
"Enable when using SkeletonGraphic under a CanvasGroup. " +
"When enabled, PMA Vertex Color alpha value is stored at uv2.g instead of color.a to capture " +
"CanvasGroup modifying color.a. Also helps to detect correct parameter setting combinations.");
protected readonly GUIContent PMAVertexColorsLabel = new GUIContent("PMA Vertex Colors",
"Use this if you are using the default Spine/Skeleton shader or any premultiply-alpha shader.");
protected readonly GUIContent AddNormalsLabel = new GUIContent("Add Normals",
"Use this if your shader requires vertex normals. A more efficient solution for 2D setups is to modify the " +
"shader to assume a single normal value for the whole mesh.");
protected readonly GUIContent CalculateTangentsLabel = new GUIContent("Solve Tangents",
"Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that " +
"require vertex tangents.");
protected readonly GUIContent ImmutableTrianglesLabel = new GUIContent("Immutable Triangles",
"Enable to optimize rendering for skeletons that never change attachment visibility");
private static GUIContent EnableSeparatorSlotsLabel;
private GUIContent UpdateWhenInvisibleLabel, FixDrawOrderLabel;
readonly GUIContent PhysicsPositionInheritanceFactorLabel = new GUIContent("Position",
"When set to non-zero, Transform position movement in X and Y direction is applied to skeleton " +
"PhysicsConstraints, multiplied by these " +
"\nX and Y scale factors to the right. Typical (X,Y) values are " +
"\n(1,1) to apply XY movement normally, " +
"\n(2,2) to apply movement with double intensity, " +
"\n(1,0) to apply only horizontal movement, or" +
"\n(0,0) to not apply any Transform position movement at all.");
readonly GUIContent PhysicsRotationInheritanceFactorLabel = new GUIContent("Rotation",
"When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints, " +
"multiplied by this scale factor to the right. Typical values are " +
"\n1 to apply movement normally, " +
"\n2 to apply movement with double intensity, or " +
"\n0 to not apply any Transform rotation movement at all.");
readonly GUIContent PhysicsMovementRelativeToLabel = new GUIContent("Movement relative to",
"Reference transform relative to which physics movement will be calculated, or null to use world location.");
protected SerializedProperty meshSettings;
const string ReloadButtonString = "Reload";
static GUILayoutOption reloadButtonWidth;
static GUILayoutOption ReloadButtonWidth { get { return reloadButtonWidth = reloadButtonWidth ?? GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonString)).x + 20); } }
static GUIStyle ReloadButtonStyle { get { return EditorStyles.miniButton; } }
protected virtual bool TargetIsValid {
get {
foreach (var o in targets) {
var component = (ISkeletonRenderer)o;
if (!component.IsValid)
return false;
}
return true;
}
}
protected virtual void OnEnable () {
#if NEW_PREFAB_SYSTEM
isInspectingPrefab = false;
#else
isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#endif
SpineEditorUtilities.ConfirmInitialization();
loadingFailed = false;
// Labels
SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
SkeletonUtilityButtonContent = new GUIContent("Add Skeleton Utility", Icons.skeletonUtility);
UpdateWhenInvisibleLabel = new GUIContent("Update When Invisible", "Update mode used when the MeshRenderer becomes invisible. Update mode is automatically reset to UpdateMode.FullUpdate when the mesh becomes visible again.");
FixDrawOrderLabel = new GUIContent("Fix Draw Order", "Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. \"A B A\"). If true, GPU instancing will be disabled at all materials and MaterialPropertyBlocks are assigned at each material to prevent aggressive batching of submeshes by e.g. the LWRP renderer, leading to incorrect draw order (e.g. \"A1 B A2\" changed to \"A1A2 B\"). You can disable this parameter when everything is drawn correctly to save the additional performance cost. Note: the GPU instancing setting will remain disabled at affected material assets after exiting play mode, you have to enable it manually if you accidentally enabled this parameter.");
skeletonDataAsset = serializedObject.FindProperty("skeletonDataAsset");
initialSkinName = serializedObject.FindProperty("initialSkinName");
initialFlipX = serializedObject.FindProperty("initialFlipX");
initialFlipY = serializedObject.FindProperty("initialFlipY");
clearStateOnDisable = serializedObject.FindProperty("clearStateOnDisable");
updateWhenInvisible = serializedObject.FindProperty("updateWhenInvisible");
fixDrawOrder = serializedObject.FindProperty("fixDrawOrder");
meshSettings = serializedObject.FindProperty("meshSettings");
meshSettings.isExpanded = SkeletonRendererInspector.advancedFoldout;
useClipping = meshSettings.FindPropertyRelative("useClipping");
zSpacing = meshSettings.FindPropertyRelative("zSpacing");
tintBlack = meshSettings.FindPropertyRelative("tintBlack");
canvasGroupCompatible = meshSettings.FindPropertyRelative("canvasGroupCompatible");
pmaVertexColors = meshSettings.FindPropertyRelative("pmaVertexColors");
addNormals = meshSettings.FindPropertyRelative("addNormals");
calculateTangents = meshSettings.FindPropertyRelative("calculateTangents");
immutableTriangles = meshSettings.FindPropertyRelative("immutableTriangles");
threadedMeshGeneration = serializedObject.FindProperty("threadedMeshGeneration");
separatorSlotNames = serializedObject.FindProperty("separatorSlotNames");
separatorSlotNames.isExpanded = true;
enableSeparatorSlots = serializedObject.FindProperty("enableSeparatorSlots");
physicsPositionInheritanceFactor = serializedObject.FindProperty("physicsPositionInheritanceFactor");
physicsRotationInheritanceFactor = serializedObject.FindProperty("physicsRotationInheritanceFactor");
physicsMovementRelativeTo = serializedObject.FindProperty("physicsMovementRelativeTo");
}
public virtual void OnSceneGUI () {
var skeletonRenderer = (ISkeletonRenderer)target;
if (loadingFailed)
return;
var skeleton = skeletonRenderer.Skeleton;
if (skeleton == null) {
loadingFailed = true;
return;
}
var transform = skeletonRenderer.Component.transform;
if (skeleton == null) return;
SpineHandles.DrawBones(transform, skeleton, skeletonRenderer.MeshScale);
}
override public void OnInspectorGUI () {
bool multi = serializedObject.isEditingMultipleObjects;
DrawInspectorGUI(multi);
serializedObject.ApplyModifiedProperties();
}
protected virtual void InspectorDrawPreparation () { }
protected virtual void FirstPropertyFields () { }
protected virtual void MaterialWarningsBox () { }
protected virtual void AdditionalSeparatorSlotProperties () { }
protected virtual void VertexDataProperties () { }
protected virtual void AfterAdvancedPropertyFields () { }
protected virtual void AdvancedPropertyFields () {
EditorGUILayout.Space();
EditorGUILayout.LabelField("Renderer Settings", EditorStyles.boldLabel);
using (new SpineInspectorUtility.LabelWidthScope()) {
// Optimization options
if (updateWhenInvisible != null) EditorGUILayout.PropertyField(updateWhenInvisible, UpdateWhenInvisibleLabel);
#if PER_MATERIAL_PROPERTY_BLOCKS
if (fixDrawOrder != null) EditorGUILayout.PropertyField(fixDrawOrder, FixDrawOrderLabel);
#endif
if (immutableTriangles != null) EditorGUILayout.PropertyField(immutableTriangles, ImmutableTrianglesLabel);
EditorGUILayout.PropertyField(clearStateOnDisable, ClearStateOnDisableLabel);
EditorGUILayout.Space();
}
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
SeparatorSlotProperties(separatorSlotNames, enableSeparatorSlots);
AdditionalSeparatorSlotProperties();
}
EditorGUILayout.Space();
// Render options
EditorGUILayout.PropertyField(useClipping, UseClippingLabel);
const float MinZSpacing = -0.1f;
const float MaxZSpacing = 0f;
EditorGUILayout.Slider(zSpacing, MinZSpacing, MaxZSpacing, ZSpacingLabel);
EditorGUILayout.Space();
if (threadedMeshGeneration != null) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Threaded Mesh Generation", SpineEditorUtilities.Icons.subMeshRenderer), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(threadedMeshGeneration, ThreadedMeshGenerationLabel);
EditorGUILayout.Space();
}
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Vertex Data", SpineInspectorUtility.UnityIcon<MeshFilter>()), EditorStyles.boldLabel);
VertexDataProperties();
}
EditorGUILayout.Space();
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Physics Inheritance", SpineEditorUtilities.Icons.constraintPhysics), EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(PhysicsPositionInheritanceFactorLabel, GUILayout.Width(EditorGUIUtility.labelWidth));
int savedIndentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUILayout.PropertyField(physicsPositionInheritanceFactor, GUIContent.none, GUILayout.MinWidth(60));
EditorGUI.indentLevel = savedIndentLevel;
}
EditorGUILayout.PropertyField(physicsRotationInheritanceFactor, PhysicsRotationInheritanceFactorLabel);
EditorGUILayout.PropertyField(physicsMovementRelativeTo, PhysicsMovementRelativeToLabel);
}
}
protected virtual void DrawInspectorGUI (bool multi) {
// Initialize.
if (Event.current.type == EventType.Layout) {
if (forceReloadQueued) {
forceReloadQueued = false;
foreach (var c in targets) {
SpineEditorUtilities.ReloadSkeletonDataAssetAndComponent(c as ISkeletonRenderer);
}
} else {
foreach (var c in targets) {
var component = c as ISkeletonRenderer;
if (!component.IsValid) {
SpineEditorUtilities.ReinitializeComponent(component);
}
}
}
InspectorDrawPreparation();
#if NO_PREFAB_MESH
if (isInspectingPrefab) {
foreach (var c in targets) {
var component = c as SkeletonRenderer;
if (component != null) {
MeshFilter meshFilter = component.GetComponent<MeshFilter>();
if (meshFilter != null && meshFilter.sharedMesh != null)
meshFilter.sharedMesh = null;
}
}
}
#endif
}
bool valid = TargetIsValid;
// Fields.
bool skeletonAssetValid = CommonSkeletonAssetProperties(multi);
if (!skeletonAssetValid || !valid)
return;
EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
using (new EditorGUILayout.HorizontalScope()) {
SpineInspectorUtility.ToggleLeftLayout(initialFlipX);
SpineInspectorUtility.ToggleLeftLayout(initialFlipY);
EditorGUILayout.Space();
}
FirstPropertyFields();
MaterialWarningsBox();
// More Render Options...
using (new SpineInspectorUtility.BoxScope()) {
EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5));
advancedFoldout = EditorGUILayout.Foldout(advancedFoldout, "Advanced");
if (advancedFoldout) {
EditorGUILayout.Space();
if (GUILayout.Button("Debug", EditorStyles.miniButton, GUILayout.Width(65f)))
SkeletonDebugWindow.Init();
} else {
EditorGUILayout.Space();
}
EditorGUILayout.EndHorizontal();
if (advancedFoldout) {
using (new SpineInspectorUtility.IndentScope()) {
AdvancedPropertyFields();
EditorGUILayout.Space();
if (valid && !isInspectingPrefab) {
if (multi) {
// Support multi-edit SkeletonUtility button.
// EditorGUILayout.Space();
// bool addSkeletonUtility = GUILayout.Button(buttonContent, GUILayout.Height(30));
// foreach (var t in targets) {
// var component = t as Component;
// if (addSkeletonUtility && component.GetComponent<SkeletonUtility>() == null)
// component.gameObject.AddComponent<SkeletonUtility>();
// }
} else {
var component = (Component)target;
if (component.GetComponent<SkeletonUtility>() == null) {
if (SpineInspectorUtility.CenteredButton(SkeletonUtilityButtonContent, 21, true, 200f))
component.gameObject.AddComponent<SkeletonUtility>();
}
}
}
EditorGUILayout.Space();
}
}
}
AfterAdvancedPropertyFields();
}
/// <returns>True when the SkeletonDataAsset is valid, false otherwise.</returns>
protected bool CommonSkeletonAssetProperties (bool multi) {
if (multi) {
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
forceReloadQueued = true;
}
} else {
var component = (ISkeletonRenderer)target;
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
if (component.IsValid) {
if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
forceReloadQueued = true;
}
}
if (component.SkeletonDataAsset == null) {
EditorGUILayout.HelpBox("Skeleton Data Asset required", MessageType.Warning);
return false;
}
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(component.SkeletonDataAsset)) {
EditorGUILayout.HelpBox("Skeleton Data Asset error. Please check Skeleton Data Asset.", MessageType.Error);
return false;
}
}
return true;
}
public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) {
var field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
field.SetValue(skeletonRenderer, newSlotNames);
}
public static string[] GetSeparatorSlotNames (SkeletonRenderer skeletonRenderer) {
var field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
return field.GetValue(skeletonRenderer) as string[];
}
public static string TerminalSlotWarningString (SerializedProperty separatorSlotNames) {
bool multi = separatorSlotNames.serializedObject.isEditingMultipleObjects;
bool hasTerminalSlot = false;
if (!multi) {
var sr = separatorSlotNames.serializedObject.targetObject as ISkeletonComponent;
var skeleton = sr.Skeleton;
int lastSlot = skeleton.Slots.Count - 1;
if (skeleton != null) {
for (int i = 0, n = separatorSlotNames.arraySize; i < n; i++) {
string slotName = separatorSlotNames.GetArrayElementAtIndex(i).stringValue;
SlotData slot = skeleton.Data.FindSlot(slotName);
int index = slot != null ? slot.Index : -1;
if (index == 0 || index == lastSlot) {
hasTerminalSlot = true;
break;
}
}
}
}
return hasTerminalSlot ? " (!)" : "";
}
public static void SeparatorSlotProperties (SerializedProperty separatorSlotNames,
SerializedProperty enableSeparatorSlots) {
string terminalSlotWarning = TerminalSlotWarningString(separatorSlotNames);
const string SeparatorsDescription = "Stored names of slots where the Skeleton's render will be split into different batches. This is used by separate components that split the render into different MeshRenderers or GameObjects.";
if (separatorSlotNames.isExpanded) {
EditorGUILayout.PropertyField(separatorSlotNames, SpineInspectorUtility.TempContent(separatorSlotNames.displayName + terminalSlotWarning, Icons.slotRoot, SeparatorsDescription), true);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("+", GUILayout.MaxWidth(28f), GUILayout.MaxHeight(15f))) {
separatorSlotNames.arraySize++;
}
GUILayout.EndHorizontal();
EditorGUILayout.Space();
} else
EditorGUILayout.PropertyField(separatorSlotNames, new GUIContent(separatorSlotNames.displayName + string.Format("{0} [{1}]", terminalSlotWarning, separatorSlotNames.arraySize), SeparatorsDescription), true);
if (EnableSeparatorSlotsLabel == null)
EnableSeparatorSlotsLabel = new GUIContent("Enable Separation", "Whether to enable separation at the above separator slots.");
EditorGUILayout.PropertyField(enableSeparatorSlots, EnableSeparatorSlotsLabel);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 04d6404d1e6803b48974da739a5fefcc
timeCreated: 1626445114
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -32,103 +32,127 @@ using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
using Icons = SpineEditorUtilities.Icons;
[CustomEditor(typeof(SkeletonAnimation))]
[CanEditMultipleObjects]
public class SkeletonAnimationInspector : SkeletonRendererInspector {
protected SerializedProperty animationName, loop, timeScale, unscaledTime, autoReset;
protected bool wasAnimationParameterChanged = false;
readonly GUIContent LoopLabel = new GUIContent("Loop", "Whether or not .AnimationName should loop. This only applies to the initial animation specified in the inspector, or any subsequent Animations played through .AnimationName. Animations set through state.SetAnimation are unaffected.");
readonly GUIContent TimeScaleLabel = new GUIContent("Time Scale", "The rate at which animations progress over time. 1 means normal speed. 0.5 means 50% speed.");
public class SkeletonAnimationInspector : UnityEditor.Editor {
protected SerializedProperty updateTiming, animationName, loop, timeScale, unscaledTime, autoReset, threadedAnimation;
readonly GUIContent UpdateTimingLabel = new GUIContent("Animation Update",
"Whether to update the animation in normal Update (the default), " +
"physics step FixedUpdate, or manually via a user call.");
readonly GUIContent LoopLabel = new GUIContent("Loop",
"Whether or not .AnimationName should loop. This only applies to the initial " +
"animation specified in the inspector, or any subsequent Animations played through .AnimationName. " +
"Animations set through state.SetAnimation are unaffected.");
readonly GUIContent TimeScaleLabel = new GUIContent("Time Scale",
"The rate at which animations progress over time. 1 means normal speed. 0.5 means 50% speed.");
readonly GUIContent UnscaledTimeLabel = new GUIContent("Unscaled Time",
"If enabled, AnimationState uses unscaled game time (Time.unscaledDeltaTime), " +
"When enabled, AnimationState uses unscaled game time (Time.unscaledDeltaTime), " +
"running animations independent of e.g. game pause (Time.timeScale). " +
"Instance SkeletonAnimation.timeScale will still be applied.");
readonly GUIContent ThreadedAnimationLabel = new GUIContent("Use Threading",
"When enabled, animations are processed on multiple threads in parallel.");
protected override void OnEnable () {
base.OnEnable();
animationName = serializedObject.FindProperty("_animationName");
protected bool TargetIsValid {
get {
foreach (UnityEngine.Object o in targets) {
ISkeletonAnimation component = (ISkeletonAnimation)o;
if (!component.IsValid)
return false;
}
return true;
}
}
protected void OnEnable () {
animationName = serializedObject.FindProperty("animationName");
loop = serializedObject.FindProperty("loop");
timeScale = serializedObject.FindProperty("timeScale");
unscaledTime = serializedObject.FindProperty("unscaledTime");
updateTiming = serializedObject.FindProperty("updateTiming");
threadedAnimation = serializedObject.FindProperty("threadedAnimation");
}
protected override void DrawInspectorGUI (bool multi) {
base.DrawInspectorGUI(multi);
if (!TargetIsValid) return;
override public void OnInspectorGUI () {
DrawInspectorGUI();
serializedObject.ApplyModifiedProperties();
}
protected virtual void DrawInspectorGUI () {
foreach (UnityEngine.Object c in targets) {
ISkeletonAnimation component = c as ISkeletonAnimation;
if (!component.IsValid) {
SpineEditorUtilities.ReinitializeComponent(component);
}
}
bool sameData = SpineInspectorUtility.TargetsUseSameData(serializedObject);
foreach (UnityEngine.Object o in targets)
TrySetAnimation(o as SkeletonAnimation);
EditorGUILayout.Space();
if (!sameData) {
EditorGUILayout.DelayedTextField(animationName);
} else {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(animationName);
wasAnimationParameterChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(loop, LoopLabel);
wasAnimationParameterChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
EditorGUILayout.PropertyField(timeScale, TimeScaleLabel);
foreach (UnityEngine.Object o in targets) {
SkeletonAnimation component = o as SkeletonAnimation;
component.timeScale = Mathf.Max(component.timeScale, 0);
}
EditorGUILayout.PropertyField(unscaledTime, UnscaledTimeLabel);
EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
EditorGUILayout.Space();
if (threadedAnimation != null) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Threaded Animation", SpineEditorUtilities.Icons.subMeshRenderer), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(threadedAnimation, ThreadedAnimationLabel);
EditorGUILayout.Space();
}
EditorGUILayout.Space();
SkeletonRootMotionParameter();
serializedObject.ApplyModifiedProperties();
}
protected void TrySetAnimation (SkeletonAnimation skeletonAnimation) {
if (skeletonAnimation == null) return;
if (!skeletonAnimation.valid || skeletonAnimation.AnimationState == null)
return;
protected void SkeletonRootMotionParameter () {
SkeletonRootMotionParameter(targets);
}
TrackEntry current = skeletonAnimation.AnimationState.GetCurrent(0);
if (!isInspectingPrefab) {
string activeAnimation = (current != null) ? current.Animation.Name : "";
bool activeLoop = (current != null) ? current.Loop : false;
bool animationParameterChanged = this.wasAnimationParameterChanged &&
((activeAnimation != animationName.stringValue) || (activeLoop != loop.boolValue));
if (animationParameterChanged) {
this.wasAnimationParameterChanged = false;
Skeleton skeleton = skeletonAnimation.Skeleton;
AnimationState state = skeletonAnimation.AnimationState;
public static void SkeletonRootMotionParameter (Object[] targets) {
int rootMotionComponentCount = 0;
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
if (component.GetComponent<SkeletonRootMotion>() != null) {
++rootMotionComponentCount;
}
}
bool allHaveRootMotion = rootMotionComponentCount == targets.Length;
bool anyHaveRootMotion = rootMotionComponentCount > 0;
if (!Application.isPlaying) {
if (state != null) state.ClearTrack(0);
skeleton.SetupPose();
}
Spine.Animation animationToUse = skeleton.Data.FindAnimation(animationName.stringValue);
if (!Application.isPlaying) {
if (animationToUse != null) {
skeletonAnimation.AnimationState.SetAnimation(0, animationToUse, loop.boolValue);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Root Motion");
if (!allHaveRootMotion) {
if (GUILayout.Button(SpineInspectorUtility.TempContent("Add Component", Icons.constraintTransform), GUILayout.MaxWidth(130), GUILayout.Height(18))) {
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
if (component.GetComponent<SkeletonRootMotion>() == null) {
component.gameObject.AddComponent<SkeletonRootMotion>();
}
}
skeletonAnimation.Update(0);
skeletonAnimation.LateUpdate();
requireRepaint = true;
} else {
if (animationToUse != null)
state.SetAnimation(0, animationToUse, loop.boolValue);
else
state.ClearTrack(0);
}
}
// Reflect animationName serialized property in the inspector even if SetAnimation API was used.
if (Application.isPlaying) {
if (current != null && current.Animation != null) {
if (skeletonAnimation.AnimationName != animationName.stringValue)
animationName.stringValue = current.Animation.Name;
if (anyHaveRootMotion) {
if (GUILayout.Button(SpineInspectorUtility.TempContent("Remove Component", Icons.constraintTransform), GUILayout.MaxWidth(140), GUILayout.Height(18))) {
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
SkeletonRootMotion rootMotionComponent = component.GetComponent<SkeletonRootMotion>();
if (rootMotionComponent != null) {
DestroyImmediate(rootMotionComponent);
}
}
}
}
}

View File

@ -39,7 +39,6 @@
#define NEWPLAYMODECALLBACKS
#endif
using System.Reflection;
using UnityEditor;
using UnityEngine;
@ -48,164 +47,56 @@ namespace Spine.Unity.Editor {
[CustomEditor(typeof(SkeletonGraphic))]
[CanEditMultipleObjects]
public class SkeletonGraphicInspector : UnityEditor.Editor {
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
const string ReloadButtonString = "Reload";
protected GUIContent SkeletonDataAssetLabel, UpdateTimingLabel;
static GUILayoutOption reloadButtonWidth;
static GUILayoutOption ReloadButtonWidth { get { return reloadButtonWidth = reloadButtonWidth ?? GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonString)).x + 20); } }
static GUIStyle ReloadButtonStyle { get { return EditorStyles.miniButton; } }
public class SkeletonGraphicInspector : ISkeletonRendererInspector {
protected SerializedProperty material, color;
protected SerializedProperty additiveMaterial, multiplyMaterial, screenMaterial;
protected SerializedProperty skeletonDataAsset, initialSkinName;
protected SerializedProperty startingAnimation, startingLoop, timeScale, freeze,
updateTiming, updateWhenInvisible, unscaledTime, layoutScaleMode, editReferenceRect;
protected SerializedProperty physicsPositionInheritanceFactor, physicsRotationInheritanceFactor, physicsMovementRelativeTo;
protected SerializedProperty initialFlipX, initialFlipY;
protected SerializedProperty meshGeneratorSettings;
protected SerializedProperty useClipping, zSpacing, tintBlack, canvasGroupCompatible, pmaVertexColors, addNormals, calculateTangents, immutableTriangles;
protected SerializedProperty allowMultipleCanvasRenderers, separatorSlotNames, enableSeparatorSlots,
protected SerializedProperty freeze;
protected SerializedProperty allowMultipleCanvasRenderers,
updateSeparatorPartLocation, updateSeparatorPartScale;
protected SerializedProperty raycastTarget, maskable;
protected SerializedProperty layoutScaleMode, editReferenceRect;
readonly GUIContent UseClippingLabel = new GUIContent("Use Clipping",
"When disabled, clipping attachments are ignored. This may be used to save performance.");
readonly GUIContent ZSpacingLabel = new GUIContent("Z Spacing",
"A value other than 0 adds a space between each rendered attachment to prevent Z Fighting when using shaders" +
" that read or write to the depth buffer. Large values may cause unwanted parallax and spaces depending on " +
"camera setup.");
readonly GUIContent TintBlackLabel = new GUIContent("Tint Black (!)",
"Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret " +
"UV2 and UV3 as black tint colors for this effect to work. You may then want to use the " +
"[Spine/SkeletonGraphic Tint Black] shader.");
readonly GUIContent CanvasGroupCompatibleLabel = new GUIContent("CanvasGroup Compatible",
"Enable when using SkeletonGraphic under a CanvasGroup. " +
"When enabled, PMA Vertex Color alpha value is stored at uv2.g instead of color.a to capture " +
"CanvasGroup modifying color.a. Also helps to detect correct parameter setting combinations.");
readonly GUIContent PMAVertexColorsLabel = new GUIContent("PMA Vertex Colors",
"Use this if you are using the default Spine/Skeleton shader or any premultiply-alpha shader.");
readonly GUIContent AddNormalsLabel = new GUIContent("Add Normals",
"Use this if your shader requires vertex normals. A more efficient solution for 2D setups is to modify the " +
"shader to assume a single normal value for the whole mesh.");
readonly GUIContent CalculateTangentsLabel = new GUIContent("Solve Tangents",
"Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that " +
"require vertex tangents.");
readonly GUIContent ImmutableTrianglesLabel = new GUIContent("Immutable Triangles",
"Enable to optimize rendering for skeletons that never change attachment visibility");
protected GUIContent allowMultipleCanvasRenderersLabel, updateSeparatorPartLocationLabel,
updateSeparatorPartScaleLabel;
readonly GUIContent UnscaledTimeLabel = new GUIContent("Unscaled Time",
"If enabled, AnimationState uses unscaled game time (Time.unscaledDeltaTime), " +
"running animations independent of e.g. game pause (Time.timeScale). " +
"Instance SkeletonAnimation.timeScale will still be applied.");
readonly GUIContent PhysicsPositionInheritanceFactorLabel = new GUIContent("Position",
"When set to non-zero, Transform position movement in X and Y direction is applied to skeleton " +
"PhysicsConstraints, multiplied by these " +
"\nX and Y scale factors to the right. Typical (X,Y) values are " +
"\n(1,1) to apply XY movement normally, " +
"\n(2,2) to apply movement with double intensity, " +
"\n(1,0) to apply only horizontal movement, or" +
"\n(0,0) to not apply any Transform position movement at all.");
readonly GUIContent PhysicsRotationInheritanceFactorLabel = new GUIContent("Rotation",
"When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints, " +
"multiplied by this scale factor to the right. Typical values are " +
"\n1 to apply movement normally, " +
"\n2 to apply movement with double intensity, or " +
"\n0 to not apply any Transform rotation movement at all.");
readonly GUIContent PhysicsMovementRelativeToLabel = new GUIContent("Movement relative to",
"Reference transform relative to which physics movement will be calculated, or null to use world location.");
protected SkeletonGraphic thisSkeletonGraphic;
SkeletonGraphic thisSkeletonGraphic;
protected bool isInspectingPrefab;
protected bool slotsReapplyRequired = false;
protected bool forceReloadQueued = false;
protected bool TargetIsValid {
get {
if (serializedObject.isEditingMultipleObjects) {
foreach (UnityEngine.Object c in targets) {
SkeletonGraphic component = c as SkeletonGraphic;
if (component == null) continue;
if (!component.IsValid)
return false;
}
return true;
} else {
SkeletonGraphic component = target as SkeletonGraphic;
if (component == null)
return false;
return component.IsValid;
}
}
}
protected virtual void OnEnable () {
#if NEW_PREFAB_SYSTEM
isInspectingPrefab = false;
#else
isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#endif
SpineEditorUtilities.ConfirmInitialization();
protected override void OnEnable () {
base.OnEnable();
// Labels
SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
UpdateTimingLabel = new GUIContent("Animation Update", "Whether to update the animation in normal Update (the default), physics step FixedUpdate, or manually via a user call.");
allowMultipleCanvasRenderersLabel = new GUIContent("Multiple CanvasRenderers",
"When set to true, SkeletonGraphic no longer uses a single CanvasRenderer" +
"but automatically creates the required number of child CanvasRenderer" +
"GameObjects for each required draw call (submesh).");
updateSeparatorPartLocationLabel = new GUIContent("Update Part Location",
"Update separator part GameObject location to match the position of the SkeletonGraphic. " +
"This can be helpful when re-parenting parts to a different GameObject.");
updateSeparatorPartScaleLabel = new GUIContent("Update Part Scale",
"Update separator part GameObject scale to match the scale (lossyScale) of the SkeletonGraphic. " +
"This can be helpful when re-parenting parts to a different GameObject.");
SerializedObject so = this.serializedObject;
// Properties
thisSkeletonGraphic = target as SkeletonGraphic;
// MaskableGraphic
material = so.FindProperty("m_Material");
color = so.FindProperty("m_SkeletonColor");
raycastTarget = so.FindProperty("m_RaycastTarget");
maskable = so.FindProperty("m_Maskable");
material = serializedObject.FindProperty("m_Material");
color = serializedObject.FindProperty("m_SkeletonColor");
raycastTarget = serializedObject.FindProperty("m_RaycastTarget");
maskable = serializedObject.FindProperty("m_Maskable");
// SkeletonRenderer
additiveMaterial = so.FindProperty("additiveMaterial");
multiplyMaterial = so.FindProperty("multiplyMaterial");
screenMaterial = so.FindProperty("screenMaterial");
skeletonDataAsset = so.FindProperty("skeletonDataAsset");
initialSkinName = so.FindProperty("initialSkinName");
initialFlipX = so.FindProperty("initialFlipX");
initialFlipY = so.FindProperty("initialFlipY");
// SkeletonAnimation
startingAnimation = so.FindProperty("startingAnimation");
startingLoop = so.FindProperty("startingLoop");
timeScale = so.FindProperty("timeScale");
unscaledTime = so.FindProperty("unscaledTime");
freeze = so.FindProperty("freeze");
updateTiming = so.FindProperty("updateTiming");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
layoutScaleMode = so.FindProperty("layoutScaleMode");
editReferenceRect = so.FindProperty("editReferenceRect");
physicsPositionInheritanceFactor = so.FindProperty("physicsPositionInheritanceFactor");
physicsRotationInheritanceFactor = so.FindProperty("physicsRotationInheritanceFactor");
physicsMovementRelativeTo = so.FindProperty("physicsMovementRelativeTo");
meshGeneratorSettings = so.FindProperty("meshGenerator").FindPropertyRelative("settings");
meshGeneratorSettings.isExpanded = SkeletonRendererInspector.advancedFoldout;
useClipping = meshGeneratorSettings.FindPropertyRelative("useClipping");
zSpacing = meshGeneratorSettings.FindPropertyRelative("zSpacing");
tintBlack = meshGeneratorSettings.FindPropertyRelative("tintBlack");
canvasGroupCompatible = meshGeneratorSettings.FindPropertyRelative("canvasGroupCompatible");
pmaVertexColors = meshGeneratorSettings.FindPropertyRelative("pmaVertexColors");
calculateTangents = meshGeneratorSettings.FindPropertyRelative("calculateTangents");
addNormals = meshGeneratorSettings.FindPropertyRelative("addNormals");
immutableTriangles = meshGeneratorSettings.FindPropertyRelative("immutableTriangles");
allowMultipleCanvasRenderers = so.FindProperty("allowMultipleCanvasRenderers");
updateSeparatorPartLocation = so.FindProperty("updateSeparatorPartLocation");
updateSeparatorPartScale = so.FindProperty("updateSeparatorPartScale");
enableSeparatorSlots = so.FindProperty("enableSeparatorSlots");
separatorSlotNames = so.FindProperty("separatorSlotNames");
separatorSlotNames.isExpanded = true;
// SkeletonGraphic
additiveMaterial = serializedObject.FindProperty("additiveMaterial");
multiplyMaterial = serializedObject.FindProperty("multiplyMaterial");
screenMaterial = serializedObject.FindProperty("screenMaterial");
freeze = serializedObject.FindProperty("freeze");
allowMultipleCanvasRenderers = serializedObject.FindProperty("allowMultipleCanvasRenderers");
updateSeparatorPartLocation = serializedObject.FindProperty("updateSeparatorPartLocation");
updateSeparatorPartScale = serializedObject.FindProperty("updateSeparatorPartScale");
layoutScaleMode = serializedObject.FindProperty("layoutScaleMode");
editReferenceRect = serializedObject.FindProperty("editReferenceRect");
#if NEWPLAYMODECALLBACKS
EditorApplication.playModeStateChanged += OnPlaymodeChanged;
@ -214,7 +105,7 @@ namespace Spine.Unity.Editor {
#endif
}
protected virtual void OnDisable () {
void OnDisable () {
#if NEWPLAYMODECALLBACKS
EditorApplication.playModeStateChanged -= OnPlaymodeChanged;
#else
@ -224,14 +115,14 @@ namespace Spine.Unity.Editor {
}
#if NEWPLAYMODECALLBACKS
protected virtual void OnPlaymodeChanged (PlayModeStateChange mode) {
void OnPlaymodeChanged (PlayModeStateChange mode) {
#else
void OnPlaymodeChanged () {
#endif
DisableEditReferenceRectMode();
}
protected virtual void DisableEditReferenceRectMode () {
void DisableEditReferenceRectMode () {
foreach (UnityEngine.Object c in targets) {
SkeletonGraphic component = c as SkeletonGraphic;
if (component == null) continue;
@ -239,45 +130,9 @@ namespace Spine.Unity.Editor {
}
}
public override void OnInspectorGUI () {
if (UnityEngine.Event.current.type == EventType.Layout) {
if (forceReloadQueued) {
forceReloadQueued = false;
foreach (UnityEngine.Object c in targets) {
SpineEditorUtilities.ReloadSkeletonDataAssetAndComponent(c as SkeletonGraphic);
}
} else {
foreach (UnityEngine.Object c in targets) {
SkeletonGraphic component = c as SkeletonGraphic;
if (!component.IsValid) {
SpineEditorUtilities.ReinitializeComponent(component);
if (!component.IsValid) continue;
}
}
}
}
bool wasChanged = false;
EditorGUI.BeginChangeCheck();
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
forceReloadQueued = true;
}
if (thisSkeletonGraphic.skeletonDataAsset == null) {
EditorGUILayout.HelpBox("You need to assign a SkeletonData asset first.", MessageType.Info);
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
return;
}
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(thisSkeletonGraphic.skeletonDataAsset)) {
EditorGUILayout.HelpBox("SkeletonData asset error. Please check SkeletonData asset.", MessageType.Error);
return;
}
protected override void FirstPropertyFields () {
using (new SpineInspectorUtility.LabelWidthScope(100)) {
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(material);
@ -292,12 +147,78 @@ namespace Spine.Unity.Editor {
}
EditorGUILayout.PropertyField(color);
}
}
protected override void MaterialWarningsBox () {
string errorMessage = null;
if (SpineEditorUtilities.Preferences.componentMaterialWarning &&
MaterialChecks.IsMaterialSetupProblematic(thisSkeletonGraphic, ref errorMessage)) {
EditorGUILayout.HelpBox(errorMessage, MessageType.Error, true);
}
}
protected override void VertexDataProperties () {
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(tintBlack, TintBlackLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect Tint Black");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectTintBlack(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(canvasGroupCompatible, CanvasGroupCompatibleLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect CanvasGroup Compatible");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectCanvasGroupCompatible(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(pmaVertexColors, PMAVertexColorsLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect PMA Vertex Colors");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectPMAVertexColors(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.FlexibleSpace();
if (GUILayout.Button("Detect Settings", EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Detect Settings");
foreach (UnityEngine.Object targets in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectTintBlack(skeletonGraphic);
DetectCanvasGroupCompatible(skeletonGraphic);
DetectPMAVertexColors(skeletonGraphic);
}
}
if (GUILayout.Button("Detect Material", EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Detect Material");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectMaterial(skeletonGraphic);
}
}
}
EditorGUILayout.PropertyField(addNormals, AddNormalsLabel);
EditorGUILayout.PropertyField(calculateTangents, CalculateTangentsLabel);
EditorGUILayout.PropertyField(immutableTriangles, ImmutableTrianglesLabel);
}
protected override void AdvancedPropertyFields () {
bool isSingleRendererOnly = (!allowMultipleCanvasRenderers.hasMultipleDifferentValues && allowMultipleCanvasRenderers.boolValue == false);
bool isSeparationEnabledButNotMultipleRenderers =
@ -306,134 +227,88 @@ namespace Spine.Unity.Editor {
isSingleRendererOnly && SkeletonHasMultipleSubmeshes();
if (isSeparationEnabledButNotMultipleRenderers || meshRendersIncorrectlyWithSingleRenderer)
meshGeneratorSettings.isExpanded = true;
advancedFoldout = true;
using (new SpineInspectorUtility.BoxScope()) {
base.AdvancedPropertyFields();
EditorGUILayout.PropertyField(meshGeneratorSettings, SpineInspectorUtility.TempContent("Advanced..."), includeChildren: false);
SkeletonRendererInspector.advancedFoldout = meshGeneratorSettings.isExpanded;
if (meshGeneratorSettings.isExpanded) {
EditorGUILayout.Space();
using (new SpineInspectorUtility.IndentScope()) {
DrawMeshSettings();
EditorGUILayout.Space();
if (advancedFoldout) {
EditorGUILayout.Space();
using (new SpineInspectorUtility.IndentScope()) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(allowMultipleCanvasRenderers, allowMultipleCanvasRenderersLabel);
using (new SpineInspectorUtility.LabelWidthScope()) {
if (GUILayout.Button(new GUIContent("Trim Renderers", "Remove currently unused CanvasRenderer GameObjects. These will be regenerated whenever needed."),
EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Trim Renderers");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
skeletonGraphic.TrimRenderers();
}
}
EditorGUILayout.EndHorizontal();
BlendModeMaterials blendModeMaterials = thisSkeletonGraphic.skeletonDataAsset.blendModeMaterials;
if (allowMultipleCanvasRenderers.boolValue == true && blendModeMaterials.RequiresBlendModeMaterials) {
using (new SpineInspectorUtility.IndentScope()) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(allowMultipleCanvasRenderers, SpineInspectorUtility.TempContent("Multiple CanvasRenderers"));
EditorGUILayout.LabelField("Blend Mode Materials", EditorStyles.boldLabel);
if (GUILayout.Button(new GUIContent("Trim Renderers", "Remove currently unused CanvasRenderer GameObjects. These will be regenerated whenever needed."),
if (GUILayout.Button(new GUIContent("Detect", "Auto-Assign Blend Mode Materials according to Vertex Data and Texture settings."),
EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Trim Renderers");
Undo.RecordObjects(targets, "Detect Blend Mode Materials");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
skeletonGraphic.TrimRenderers();
DetectBlendModeMaterials(skeletonGraphic);
}
}
EditorGUILayout.EndHorizontal();
BlendModeMaterials blendModeMaterials = thisSkeletonGraphic.skeletonDataAsset.blendModeMaterials;
if (allowMultipleCanvasRenderers.boolValue == true && blendModeMaterials.RequiresBlendModeMaterials) {
using (new SpineInspectorUtility.IndentScope()) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Blend Mode Materials", EditorStyles.boldLabel);
if (GUILayout.Button(new GUIContent("Detect", "Auto-Assign Blend Mode Materials according to Vertex Data and Texture settings."),
EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Detect Blend Mode Materials");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectBlendModeMaterials(skeletonGraphic);
}
}
EditorGUILayout.EndHorizontal();
bool usesAdditiveMaterial = blendModeMaterials.applyAdditiveMaterial;
bool pmaVertexColors = thisSkeletonGraphic.MeshGenerator.settings.pmaVertexColors;
if (pmaVertexColors)
using (new EditorGUI.DisabledGroupScope(true)) {
EditorGUILayout.LabelField("Additive Material - Unused with PMA Vertex Colors", EditorStyles.label);
}
else if (usesAdditiveMaterial)
EditorGUILayout.PropertyField(additiveMaterial, SpineInspectorUtility.TempContent("Additive Material", null, "SkeletonGraphic Material for 'Additive' blend mode slots. Unused when 'PMA Vertex Colors' is enabled."));
else
using (new EditorGUI.DisabledGroupScope(true)) {
EditorGUILayout.LabelField("No Additive Mat - 'Apply Additive Material' disabled at SkeletonDataAsset", EditorStyles.label);
}
EditorGUILayout.PropertyField(multiplyMaterial, SpineInspectorUtility.TempContent("Multiply Material", null, "SkeletonGraphic Material for 'Multiply' blend mode slots."));
EditorGUILayout.PropertyField(screenMaterial, SpineInspectorUtility.TempContent("Screen Material", null, "SkeletonGraphic Material for 'Screen' blend mode slots."));
bool usesAdditiveMaterial = blendModeMaterials.applyAdditiveMaterial;
bool pmaVertexColors = thisSkeletonGraphic.MeshSettings.pmaVertexColors;
if (pmaVertexColors)
using (new EditorGUI.DisabledGroupScope(true)) {
EditorGUILayout.LabelField("Additive Material - Unused with PMA Vertex Colors", EditorStyles.label);
}
}
EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
EditorGUILayout.PropertyField(updateWhenInvisible);
}
// warning box
if (isSeparationEnabledButNotMultipleRenderers) {
using (new SpineInspectorUtility.BoxScope()) {
meshGeneratorSettings.isExpanded = true;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("'Multiple Canvas Renderers' must be enabled\nwhen 'Enable Separation' is enabled.", Icons.warning), GUILayout.Height(42), GUILayout.Width(340));
}
} else if (meshRendersIncorrectlyWithSingleRenderer) {
using (new SpineInspectorUtility.BoxScope()) {
meshGeneratorSettings.isExpanded = true;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("This mesh uses multiple atlas pages or blend modes.\n" +
"You need to enable 'Multiple Canvas Renderers'\n" +
"for correct rendering. Consider packing\n" +
"attachments to a single atlas page if possible.", Icons.warning), GUILayout.Height(60), GUILayout.Width(380));
}
else if (usesAdditiveMaterial)
EditorGUILayout.PropertyField(additiveMaterial, SpineInspectorUtility.TempContent("Additive Material", null, "SkeletonGraphic Material for 'Additive' blend mode slots. Unused when 'PMA Vertex Colors' is enabled."));
else
using (new EditorGUI.DisabledGroupScope(true)) {
EditorGUILayout.LabelField("No Additive Mat - 'Apply Additive Material' disabled at SkeletonDataAsset", EditorStyles.label);
}
EditorGUILayout.PropertyField(multiplyMaterial, SpineInspectorUtility.TempContent("Multiply Material", null, "SkeletonGraphic Material for 'Multiply' blend mode slots."));
EditorGUILayout.PropertyField(screenMaterial, SpineInspectorUtility.TempContent("Screen Material", null, "SkeletonGraphic Material for 'Screen' blend mode slots."));
}
}
EditorGUILayout.Space();
SeparatorsField(separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation, updateSeparatorPartScale);
EditorGUILayout.Space();
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Physics Inheritance", SpineEditorUtilities.Icons.constraintPhysics), EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(PhysicsPositionInheritanceFactorLabel, GUILayout.Width(EditorGUIUtility.labelWidth));
int savedIndentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUILayout.PropertyField(physicsPositionInheritanceFactor, GUIContent.none, GUILayout.MinWidth(60));
EditorGUI.indentLevel = savedIndentLevel;
// warning box
if (isSeparationEnabledButNotMultipleRenderers) {
using (new SpineInspectorUtility.BoxScope()) {
meshSettings.isExpanded = true;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("'Multiple Canvas Renderers' must be enabled\nwhen 'Enable Separation' is enabled.", Icons.warning), GUILayout.Height(42), GUILayout.Width(340));
}
} else if (meshRendersIncorrectlyWithSingleRenderer) {
using (new SpineInspectorUtility.BoxScope()) {
meshSettings.isExpanded = true;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("This mesh uses multiple atlas pages or blend modes.\n" +
"You need to enable 'Multiple Canvas Renderers'\n" +
"for correct rendering. Consider packing\n" +
"attachments to a single atlas page if possible.", Icons.warning), GUILayout.Height(60), GUILayout.Width(340));
}
EditorGUILayout.PropertyField(physicsRotationInheritanceFactor, PhysicsRotationInheritanceFactorLabel);
EditorGUILayout.PropertyField(physicsMovementRelativeTo, PhysicsMovementRelativeToLabel);
}
}
}
}
EditorGUILayout.Space();
EditorGUILayout.PropertyField(initialSkinName);
{
Rect rect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, EditorGUIUtility.singleLineHeight);
EditorGUI.PrefixLabel(rect, SpineInspectorUtility.TempContent("Initial Flip"));
rect.x += EditorGUIUtility.labelWidth;
rect.width = 30f;
SpineInspectorUtility.ToggleLeft(rect, initialFlipX, SpineInspectorUtility.TempContent("X", tooltip: "initialFlipX"));
rect.x += 35f;
SpineInspectorUtility.ToggleLeft(rect, initialFlipY, SpineInspectorUtility.TempContent("Y", tooltip: "initialFlipY"));
}
protected override void AfterAdvancedPropertyFields () {
EditorGUILayout.Space();
EditorGUILayout.LabelField("Animation", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(startingAnimation);
EditorGUILayout.PropertyField(startingLoop);
EditorGUILayout.PropertyField(timeScale);
EditorGUILayout.PropertyField(unscaledTime, UnscaledTimeLabel);
EditorGUILayout.Space();
EditorGUILayout.PropertyField(freeze);
EditorGUILayout.Space();
SkeletonRendererInspector.SkeletonRootMotionParameter(targets);
EditorGUILayout.Space();
EditorGUILayout.LabelField("UI", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(raycastTarget);
if (maskable != null) EditorGUILayout.PropertyField(maskable);
@ -463,105 +338,6 @@ namespace Spine.Unity.Editor {
}
EditorGUILayout.EndHorizontal();
}
if (TargetIsValid && !isInspectingPrefab) {
EditorGUILayout.Space();
if (SpineInspectorUtility.CenteredButton(new GUIContent("Add Skeleton Utility", Icons.skeletonUtility), 21, true, 200f))
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
if (component.GetComponent<SkeletonUtility>() == null) {
component.gameObject.AddComponent<SkeletonUtility>();
}
}
}
wasChanged |= EditorGUI.EndChangeCheck();
if (wasChanged) {
serializedObject.ApplyModifiedProperties();
slotsReapplyRequired = true;
}
if (slotsReapplyRequired && UnityEngine.Event.current.type == EventType.Repaint) {
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
skeletonGraphic.ReapplySeparatorSlotNames();
skeletonGraphic.LateUpdate();
SceneView.RepaintAll();
}
slotsReapplyRequired = false;
}
}
protected void DrawMeshSettings () {
EditorGUILayout.PropertyField(useClipping, UseClippingLabel);
const float MinZSpacing = -0.1f;
const float MaxZSpacing = 0f;
EditorGUILayout.Slider(zSpacing, MinZSpacing, MaxZSpacing, ZSpacingLabel);
EditorGUILayout.Space();
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Vertex Data", SpineInspectorUtility.UnityIcon<MeshFilter>()), EditorStyles.boldLabel);
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(tintBlack, TintBlackLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect Tint Black");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectTintBlack(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(canvasGroupCompatible, CanvasGroupCompatibleLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect CanvasGroup Compatible");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectCanvasGroupCompatible(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.PropertyField(pmaVertexColors, PMAVertexColorsLabel);
if (GUILayout.Button("Detect", EditorStyles.miniButton, GUILayout.Width(65f))) {
Undo.RecordObjects(targets, "Detect PMA Vertex Colors");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectPMAVertexColors(skeletonGraphic);
}
}
}
using (new EditorGUILayout.HorizontalScope()) {
GUILayout.FlexibleSpace();
if (GUILayout.Button("Detect Settings", EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Detect Settings");
foreach (UnityEngine.Object targets in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectTintBlack(skeletonGraphic);
DetectCanvasGroupCompatible(skeletonGraphic);
DetectPMAVertexColors(skeletonGraphic);
}
}
if (GUILayout.Button("Detect Material", EditorStyles.miniButton, GUILayout.Width(100f))) {
Undo.RecordObjects(targets, "Detect Material");
foreach (UnityEngine.Object target in targets) {
SkeletonGraphic skeletonGraphic = target as SkeletonGraphic;
if (skeletonGraphic == null) continue;
DetectMaterial(skeletonGraphic);
}
}
}
EditorGUILayout.PropertyField(addNormals, AddNormalsLabel);
EditorGUILayout.PropertyField(calculateTangents, CalculateTangentsLabel);
EditorGUILayout.PropertyField(immutableTriangles, ImmutableTrianglesLabel);
}
}
protected bool SkeletonHasMultipleSubmeshes () {
@ -574,7 +350,14 @@ namespace Spine.Unity.Editor {
return false;
}
protected void OnSceneGUI () {
protected override void AdditionalSeparatorSlotProperties () {
EditorGUILayout.PropertyField(updateSeparatorPartLocation, updateSeparatorPartLocationLabel);
EditorGUILayout.PropertyField(updateSeparatorPartScale, updateSeparatorPartScaleLabel);
}
public override void OnSceneGUI () {
base.OnSceneGUI();
SkeletonGraphic skeletonGraphic = (SkeletonGraphic)target;
if (skeletonGraphic.layoutScaleMode != SkeletonGraphic.LayoutMode.None) {
@ -588,61 +371,6 @@ namespace Spine.Unity.Editor {
SpineHandles.DrawPivotOffsetHandle(skeletonGraphic, Color.green);
}
public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) {
FieldInfo field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
field.SetValue(skeletonRenderer, newSlotNames);
}
public static string[] GetSeparatorSlotNames (SkeletonRenderer skeletonRenderer) {
FieldInfo field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
return field.GetValue(skeletonRenderer) as string[];
}
public static void SeparatorsField (SerializedProperty separatorSlotNames, SerializedProperty enableSeparatorSlots,
SerializedProperty updateSeparatorPartLocation, SerializedProperty updateSeparatorPartScale) {
bool multi = separatorSlotNames.serializedObject.isEditingMultipleObjects;
bool hasTerminalSlot = false;
if (!multi) {
ISkeletonComponent sr = separatorSlotNames.serializedObject.targetObject as ISkeletonComponent;
Skeleton skeleton = sr.Skeleton;
int lastSlot = skeleton.Slots.Count - 1;
if (skeleton != null) {
for (int i = 0, n = separatorSlotNames.arraySize; i < n; i++) {
string slotName = separatorSlotNames.GetArrayElementAtIndex(i).stringValue;
SlotData slot = skeleton.Data.FindSlot(slotName);
int index = slot != null ? slot.Index : -1;
if (index == 0 || index == lastSlot) {
hasTerminalSlot = true;
break;
}
}
}
}
string terminalSlotWarning = hasTerminalSlot ? " (!)" : "";
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
const string SeparatorsDescription = "Stored names of slots where the Skeleton's render will be split into different batches. This is used by separate components that split the render into different MeshRenderers or GameObjects.";
if (separatorSlotNames.isExpanded) {
EditorGUILayout.PropertyField(separatorSlotNames, SpineInspectorUtility.TempContent(separatorSlotNames.displayName + terminalSlotWarning, Icons.slotRoot, SeparatorsDescription), true);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("+", GUILayout.MaxWidth(28f), GUILayout.MaxHeight(15f))) {
separatorSlotNames.arraySize++;
}
GUILayout.EndHorizontal();
} else
EditorGUILayout.PropertyField(separatorSlotNames, new GUIContent(separatorSlotNames.displayName + string.Format("{0} [{1}]", terminalSlotWarning, separatorSlotNames.arraySize), SeparatorsDescription), true);
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.PropertyField(enableSeparatorSlots, SpineInspectorUtility.TempContent("Enable Separation", tooltip: "Whether to enable separation at the above separator slots."));
EditorGUILayout.PropertyField(updateSeparatorPartLocation, SpineInspectorUtility.TempContent("Update Part Location", tooltip: "Update separator part GameObject location to match the position of the SkeletonGraphic. This can be helpful when re-parenting parts to a different GameObject."));
EditorGUILayout.PropertyField(updateSeparatorPartScale, SpineInspectorUtility.TempContent("Update Part Scale", tooltip: "Update separator part GameObject scale to match the scale (lossyScale) of the SkeletonGraphic. This can be helpful when re-parenting parts to a different GameObject."));
}
}
}
#region Auto Detect Setting
static void DetectTintBlack (SkeletonGraphic skeletonGraphic) {
bool requiresTintBlack = HasTintBlackSlot(skeletonGraphic);
@ -650,7 +378,7 @@ namespace Spine.Unity.Editor {
Debug.Log(string.Format("Found Tint-Black slot at '{0}'", skeletonGraphic));
else
Debug.Log(string.Format("No Tint-Black slot found at '{0}'", skeletonGraphic));
skeletonGraphic.MeshGenerator.settings.tintBlack = requiresTintBlack;
skeletonGraphic.MeshSettings.tintBlack = requiresTintBlack;
}
static bool HasTintBlackSlot (SkeletonGraphic skeletonGraphic) {
@ -669,7 +397,7 @@ namespace Spine.Unity.Editor {
Debug.Log(string.Format("Skeleton is a child of CanvasGroup: '{0}'", skeletonGraphic));
else
Debug.Log(string.Format("Skeleton is not a child of CanvasGroup: '{0}'", skeletonGraphic));
skeletonGraphic.MeshGenerator.settings.canvasGroupCompatible = requiresCanvasGroupCompatible;
skeletonGraphic.MeshSettings.canvasGroupCompatible = requiresCanvasGroupCompatible;
}
static bool IsBelowCanvasGroup (SkeletonGraphic skeletonGraphic) {
@ -677,22 +405,22 @@ namespace Spine.Unity.Editor {
}
static void DetectPMAVertexColors (SkeletonGraphic skeletonGraphic) {
MeshGenerator.Settings settings = skeletonGraphic.MeshGenerator.settings;
MeshGenerator.Settings settings = skeletonGraphic.MeshSettings;
bool usesSpineShader = MaterialChecks.UsesSpineShader(skeletonGraphic.material);
if (!usesSpineShader) {
Debug.Log(string.Format("Skeleton is not using a Spine shader, thus the shader is likely " +
"not using PMA vertex color: '{0}'", skeletonGraphic));
skeletonGraphic.MeshGenerator.settings.pmaVertexColors = false;
skeletonGraphic.MeshSettings.pmaVertexColors = false;
return;
}
bool requiresPMAVertexColorsDisabled = settings.canvasGroupCompatible && !settings.tintBlack;
if (requiresPMAVertexColorsDisabled) {
Debug.Log(string.Format("Skeleton requires PMA Vertex Colors disabled: '{0}'", skeletonGraphic));
skeletonGraphic.MeshGenerator.settings.pmaVertexColors = false;
skeletonGraphic.MeshSettings.pmaVertexColors = false;
} else {
Debug.Log(string.Format("Skeleton requires or permits PMA Vertex Colors enabled: '{0}'", skeletonGraphic));
skeletonGraphic.MeshGenerator.settings.pmaVertexColors = true;
skeletonGraphic.MeshSettings.pmaVertexColors = true;
}
}
@ -712,7 +440,7 @@ namespace Spine.Unity.Editor {
}
static void DetectMaterial (SkeletonGraphic skeletonGraphic) {
MeshGenerator.Settings settings = skeletonGraphic.MeshGenerator.settings;
MeshGenerator.Settings settings = skeletonGraphic.MeshSettings;
bool detectionSucceeded;
bool usesPMATexture = IsSkeletonTexturePMA(skeletonGraphic, out detectionSucceeded);
@ -767,7 +495,7 @@ namespace Spine.Unity.Editor {
}
static void DetectBlendModeMaterial (SkeletonGraphic skeletonGraphic, BlendMode blendMode, bool usesPMATexture) {
MeshGenerator.Settings settings = skeletonGraphic.MeshGenerator.settings;
MeshGenerator.Settings settings = skeletonGraphic.MeshSettings;
string optionalTintBlack = settings.tintBlack ? "TintBlack" : "";
string blendModeString = blendMode.ToString();
@ -815,7 +543,7 @@ namespace Spine.Unity.Editor {
if (parentTransform == null)
Debug.LogWarning("Your new SkeletonGraphic will not be visible until it is placed under a Canvas");
GameObject gameObject = NewSkeletonGraphicGameObject("New SkeletonGraphic");
GameObject gameObject = NewSkeletonGraphicGameObject("New SkeletonGraphic", typeof(SkeletonAnimation));
gameObject.transform.SetParent(parentTransform, false);
EditorUtility.FocusProjectWindow();
Selection.activeObject = gameObject;
@ -833,8 +561,9 @@ namespace Spine.Unity.Editor {
public static SkeletonGraphic InstantiateSkeletonGraphic (SkeletonDataAsset skeletonDataAsset, Skin skin = null) {
string spineGameObjectName = string.Format("SkeletonGraphic ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
GameObject go = NewSkeletonGraphicGameObject(spineGameObjectName);
GameObject go = NewSkeletonGraphicGameObject(spineGameObjectName, typeof(SkeletonAnimation));
SkeletonGraphic graphic = go.GetComponent<SkeletonGraphic>();
SkeletonAnimation animation = go.GetComponent<SkeletonAnimation>();
graphic.skeletonDataAsset = skeletonDataAsset;
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
@ -849,12 +578,11 @@ namespace Spine.Unity.Editor {
}
skin = skin ?? data.DefaultSkin ?? data.Skins.Items[0];
graphic.MeshGenerator.settings.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
graphic.MeshSettings.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
graphic.startingLoop = SpineEditorUtilities.Preferences.defaultInstantiateLoop;
graphic.PhysicsPositionInheritanceFactor = SpineEditorUtilities.Preferences.defaultPhysicsPositionInheritance;
graphic.PhysicsRotationInheritanceFactor = SpineEditorUtilities.Preferences.defaultPhysicsRotationInheritance;
animation.loop = SpineEditorUtilities.Preferences.defaultInstantiateLoop;
graphic.Initialize(false);
animation.Initialize(false);
if (skin != null) graphic.Skeleton.SetSkin(skin);
graphic.initialSkinName = skin.Name;
graphic.Skeleton.UpdateWorldTransform(Physics.Update);
@ -862,8 +590,14 @@ namespace Spine.Unity.Editor {
return graphic;
}
static GameObject NewSkeletonGraphicGameObject (string gameObjectName) {
GameObject go = EditorInstantiation.NewGameObject(gameObjectName, true, typeof(RectTransform), typeof(CanvasRenderer), typeof(SkeletonGraphic));
static GameObject NewSkeletonGraphicGameObject (string gameObjectName, System.Type animationComponentType) {
GameObject go = EditorInstantiation.NewGameObject(gameObjectName, true, typeof(RectTransform),
typeof(CanvasRenderer), typeof(SkeletonGraphic));
// Note: SkeletonAnimation component was already implicitly added by
// SkeletonGraphic.Awake() above, calling UpgradeTo43Components().
if (go.GetComponent(animationComponentType) == null)
EditorInstantiation.AddComponent(go, true, animationComponentType);
SkeletonGraphic graphic = go.GetComponent<SkeletonGraphic>();
graphic.material = SkeletonGraphicInspector.DefaultSkeletonGraphicMaterial;
graphic.additiveMaterial = SkeletonGraphicInspector.DefaultSkeletonGraphicAdditiveMaterial;

View File

@ -33,41 +33,71 @@ using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
[CustomEditor(typeof(SkeletonMecanim))]
[CanEditMultipleObjects]
public class SkeletonMecanimInspector : SkeletonRendererInspector {
public class SkeletonMecanimInspector : UnityEditor.Editor {
public static bool mecanimSettingsFoldout;
public static bool enableScenePreview;
protected SerializedProperty autoReset;
protected SerializedProperty useCustomMixMode;
protected SerializedProperty layerMixModes;
protected SerializedProperty layerBlendModes;
protected SerializedProperty updateTiming, autoReset, useCustomMixMode, layerMixModes, layerBlendModes, threadedAnimation;
protected override void OnEnable () {
base.OnEnable();
readonly GUIContent UpdateTimingLabel = new GUIContent("Animation Update",
"Whether to update the animation in normal Update (the default), " +
"physics step FixedUpdate, or manually via a user call.");
readonly GUIContent AutoResetLabel = new GUIContent("Auto Reset",
"When set to true, the skeleton state is mixed out to setup-" +
"pose when an animation finishes, according to the " +
"animation's keyed items.");
readonly GUIContent UseCustomMixModeLabel = new GUIContent("Custom MixMode",
"When disabled, the recommended MixMode is used according to the layer blend mode. " +
"Enable to specify a custom MixMode for each Mecanim layer.");
readonly GUIContent ThreadedAnimationLabel = new GUIContent("Use Threading",
"When enabled, animations are processed on multiple threads in parallel.");
protected virtual void OnEnable () {
SerializedProperty mecanimTranslator = serializedObject.FindProperty("translator");
autoReset = mecanimTranslator.FindPropertyRelative("autoReset");
useCustomMixMode = mecanimTranslator.FindPropertyRelative("useCustomMixMode");
layerMixModes = mecanimTranslator.FindPropertyRelative("layerMixModes");
layerBlendModes = mecanimTranslator.FindPropertyRelative("layerBlendModes");
updateTiming = serializedObject.FindProperty("updateTiming");
threadedAnimation = serializedObject.FindProperty("threadedAnimation");
}
protected override void DrawInspectorGUI (bool multi) {
override public void OnInspectorGUI () {
DrawInspectorGUI();
serializedObject.ApplyModifiedProperties();
}
protected virtual void DrawInspectorGUI () {
foreach (UnityEngine.Object c in targets) {
ISkeletonAnimation component = c as ISkeletonAnimation;
if (!component.IsValid) {
SpineEditorUtilities.ReinitializeComponent(component);
}
}
AddRootMotionComponentIfEnabled();
base.DrawInspectorGUI(multi);
EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
EditorGUILayout.Space();
if (threadedAnimation != null) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Threaded Animation", SpineEditorUtilities.Icons.subMeshRenderer), EditorStyles.boldLabel);
EditorGUILayout.PropertyField(threadedAnimation, ThreadedAnimationLabel);
EditorGUILayout.Space();
}
using (new SpineInspectorUtility.BoxScope()) {
mecanimSettingsFoldout = EditorGUILayout.Foldout(mecanimSettingsFoldout, "Mecanim Translator");
if (mecanimSettingsFoldout) {
EditorGUILayout.PropertyField(autoReset, new GUIContent("Auto Reset",
"When set to true, the skeleton state is mixed out to setup-" +
"pose when an animation finishes, according to the " +
"animation's keyed items."));
EditorGUILayout.PropertyField(autoReset, AutoResetLabel);
EditorGUILayout.PropertyField(useCustomMixMode, new GUIContent("Custom MixMode",
"When disabled, the recommended MixMode is used according to the layer blend mode. Enable to specify a custom MixMode for each Mecanim layer."));
EditorGUILayout.PropertyField(useCustomMixMode, UseCustomMixModeLabel);
if (useCustomMixMode.hasMultipleDifferentValues || useCustomMixMode.boolValue == true) {
DrawLayerSettings();
@ -182,6 +212,17 @@ namespace Spine.Unity.Editor {
int maxIndex = 0;
for (int i = 0; i < targets.Length; ++i) {
SkeletonMecanim skeletonMecanim = ((SkeletonMecanim)targets[i]);
Animator animator = skeletonMecanim.Translator.Animator;
if (!Application.isPlaying) {
if (animator != null && animator.isInitialized &&
animator.isActiveAndEnabled && animator.runtimeAnimatorController != null) {
// Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController"
// when saving and perhaps also with prefabs.
animator.Rebind();
}
}
int count = skeletonMecanim.Translator.MecanimLayerCount;
if (count > maxLayerCount) {
maxLayerCount = count;

View File

@ -27,8 +27,6 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#define SPINE_OPTIONAL_MATERIALOVERRIDE
// Contributed by: Lost Polygon
using Spine.Unity.Examples;
@ -137,9 +135,7 @@ namespace Spine.Unity.Editor {
if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Clear and Reapply Changes", tooltip: "Removes all non-serialized overrides in the SkeletonRenderer and reapplies the overrides on this component."))) {
if (skeletonRenderer != null) {
#if SPINE_OPTIONAL_MATERIALOVERRIDE
skeletonRenderer.CustomMaterialOverride.Clear();
#endif
skeletonRenderer.CustomSlotMaterials.Clear();
RemoveCustomMaterials();
SetCustomMaterials();

View File

@ -45,638 +45,85 @@
#define HAS_ON_POSTPROCESS_PREFAB
#endif
using System.Reflection;
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
using Icons = SpineEditorUtilities.Icons;
[CustomEditor(typeof(SkeletonRenderer))]
[CanEditMultipleObjects]
public class SkeletonRendererInspector : UnityEditor.Editor {
public static bool advancedFoldout;
protected bool loadingFailed = false;
public class SkeletonRendererInspector : ISkeletonRendererInspector {
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
protected SerializedProperty skeletonDataAsset, initialSkinName;
protected SerializedProperty initialFlipX, initialFlipY;
protected SerializedProperty updateTiming, updateWhenInvisible, singleSubmesh, separatorSlotNames,
clearStateOnDisable, immutableTriangles, fixDrawOrder, fixPrefabOverrideViaMeshFilter;
protected SerializedProperty normals, tangents, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings
protected SerializedProperty singleSubmesh;
protected SerializedProperty fixPrefabOverrideViaMeshFilter;
protected SerializedProperty maskInteraction;
protected SerializedProperty maskMaterialsNone, maskMaterialsInside, maskMaterialsOutside;
protected SerializedProperty physicsPositionInheritanceFactor, physicsRotationInheritanceFactor, physicsMovementRelativeTo;
protected SpineInspectorUtility.SerializedSortingProperties sortingProperties;
protected bool wasInitParameterChanged = false;
protected bool requireRepaint = false;
protected bool isInspectingPrefab;
protected bool forceReloadQueued = false;
protected bool setMaskNoneMaterialsQueued = false;
protected bool setInsideMaskMaterialsQueued = false;
protected bool setOutsideMaskMaterialsQueued = false;
protected bool deleteInsideMaskMaterialsQueued = false;
protected bool deleteOutsideMaskMaterialsQueued = false;
protected GUIContent SkeletonDataAssetLabel, SkeletonUtilityButtonContent;
protected GUIContent PMAVertexColorsLabel, ClearStateOnDisableLabel, ZSpacingLabel, ImmubleTrianglesLabel,
TintBlackLabel, UpdateTimingLabel, UpdateWhenInvisibleLabel, SingleSubmeshLabel, FixDrawOrderLabel, FixPrefabOverrideViaMeshFilterLabel;
protected GUIContent NormalsLabel, TangentsLabel, MaskInteractionLabel;
protected GUIContent MaskMaterialsHeadingLabel, MaskMaterialsNoneLabel, MaskMaterialsInsideLabel, MaskMaterialsOutsideLabel;
protected GUIContent SetMaterialButtonLabel, ClearMaterialButtonLabel, DeleteMaterialButtonLabel;
readonly GUIContent PhysicsPositionInheritanceFactorLabel = new GUIContent("Position",
"When set to non-zero, Transform position movement in X and Y direction is applied to skeleton " +
"PhysicsConstraints, multiplied by these " +
"\nX and Y scale factors to the right. Typical (X,Y) values are " +
"\n(1,1) to apply XY movement normally, " +
"\n(2,2) to apply movement with double intensity, " +
"\n(1,0) to apply only horizontal movement, or" +
"\n(0,0) to not apply any Transform position movement at all.");
readonly GUIContent PhysicsRotationInheritanceFactorLabel = new GUIContent("Rotation",
"When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints, " +
"multiplied by this scale factor to the right. Typical values are " +
"\n1 to apply movement normally, " +
"\n2 to apply movement with double intensity, or " +
"\n0 to not apply any Transform rotation movement at all.");
readonly GUIContent PhysicsMovementRelativeToLabel = new GUIContent("Movement relative to",
"Reference transform relative to which physics movement will be calculated, or null to use world location.");
protected GUIContent SingleSubmeshLabel;
protected GUIContent FixPrefabOverrideViaMeshFilterLabel;
protected GUIContent MaskInteractionLabel;
const string ReloadButtonString = "Reload";
static GUILayoutOption reloadButtonWidth;
static GUILayoutOption ReloadButtonWidth { get { return reloadButtonWidth = reloadButtonWidth ?? GUILayout.Width(GUI.skin.label.CalcSize(new GUIContent(ReloadButtonString)).x + 20); } }
static GUIStyle ReloadButtonStyle { get { return EditorStyles.miniButton; } }
protected bool TargetIsValid {
get {
if (serializedObject.isEditingMultipleObjects) {
foreach (UnityEngine.Object o in targets) {
SkeletonRenderer component = (SkeletonRenderer)o;
if (!component.valid)
return false;
}
return true;
} else {
SkeletonRenderer component = (SkeletonRenderer)target;
return component.valid;
}
}
}
protected virtual void OnEnable () {
#if NEW_PREFAB_SYSTEM
isInspectingPrefab = false;
#else
isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#endif
SpineEditorUtilities.ConfirmInitialization();
loadingFailed = false;
protected override void OnEnable () {
base.OnEnable();
// Labels
SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
SkeletonUtilityButtonContent = new GUIContent("Add Skeleton Utility", Icons.skeletonUtility);
ImmubleTrianglesLabel = new GUIContent("Immutable Triangles", "Enable to optimize rendering for skeletons that never change attachment visbility");
PMAVertexColorsLabel = new GUIContent("PMA Vertex Colors", "Use this if you are using the default Spine/Skeleton shader or any premultiply-alpha shader.");
ClearStateOnDisableLabel = new GUIContent("Clear State On Disable", "Use this if you are pooling or enabling/disabling your Spine GameObject.");
ZSpacingLabel = new GUIContent("Z Spacing", "A value other than 0 adds a space between each rendered attachment to prevent Z Fighting when using shaders that read or write to the depth buffer. Large values may cause unwanted parallax and spaces depending on camera setup.");
NormalsLabel = new GUIContent("Add Normals", "Use this if your shader requires vertex normals. A more efficient solution for 2D setups is to modify the shader to assume a single normal value for the whole mesh.");
TangentsLabel = new GUIContent("Solve Tangents", "Calculates the tangents per frame. Use this if you are using lit shaders (usually with normal maps) that require vertex tangents.");
TintBlackLabel = new GUIContent("Tint Black (!)", "Adds black tint vertex data to the mesh as UV2 and UV3. Black tinting requires that the shader interpret UV2 and UV3 as black tint colors for this effect to work. You may also use the default [Spine/Skeleton Tint Black] shader.\n\nIf you only need to tint the whole skeleton and not individual parts, the [Spine/Skeleton Tint] shader is recommended for better efficiency and changing/animating the _Black material property via MaterialPropertyBlock.");
SingleSubmeshLabel = new GUIContent("Use Single Submesh", "Simplifies submesh generation by assuming you are only using one Material and need only one submesh. This is will disable multiple materials, render separation, and custom slot materials.");
UpdateTimingLabel = new GUIContent("Animation Update", "Whether to update the animation in normal Update (the default), physics step FixedUpdate, or manually via a user call.");
UpdateWhenInvisibleLabel = new GUIContent("Update When Invisible", "Update mode used when the MeshRenderer becomes invisible. Update mode is automatically reset to UpdateMode.FullUpdate when the mesh becomes visible again.");
FixDrawOrderLabel = new GUIContent("Fix Draw Order", "Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. \"A B A\"). If true, GPU instancing will be disabled at all materials and MaterialPropertyBlocks are assigned at each material to prevent aggressive batching of submeshes by e.g. the LWRP renderer, leading to incorrect draw order (e.g. \"A1 B A2\" changed to \"A1A2 B\"). You can disable this parameter when everything is drawn correctly to save the additional performance cost. Note: the GPU instancing setting will remain disabled at affected material assets after exiting play mode, you have to enable it manually if you accidentally enabled this parameter.");
FixPrefabOverrideViaMeshFilterLabel = new GUIContent("Fix Prefab Overr. MeshFilter", "Fixes the prefab always being marked as changed (sets the MeshFilter's hide flags to DontSaveInEditor), but at the cost of references to the MeshFilter by other components being lost. For global settings see Edit - Preferences - Spine.");
MaskInteractionLabel = new GUIContent("Mask Interaction", "SkeletonRenderer's interaction with a Sprite Mask.");
MaskMaterialsHeadingLabel = new GUIContent("Mask Interaction Materials", "Materials used for different interaction with sprite masks.");
MaskMaterialsNoneLabel = new GUIContent("Normal Materials", "Normal materials used when Mask Interaction is set to None.");
MaskMaterialsInsideLabel = new GUIContent("Inside Mask", "Materials used when Mask Interaction is set to Inside Mask.");
MaskMaterialsOutsideLabel = new GUIContent("Outside Mask", "Materials used when Mask Interaction is set to Outside Mask.");
SetMaterialButtonLabel = new GUIContent("Set", "Prepares material references for switching to the corresponding Mask Interaction mode at runtime. Creates the required materials if they do not exist.");
ClearMaterialButtonLabel = new GUIContent("Clear", "Clears unused material references. Note: when switching to the corresponding Mask Interaction mode at runtime, a new material is generated on the fly.");
DeleteMaterialButtonLabel = new GUIContent("Delete", "Clears unused material references and deletes the corresponding assets. Note: when switching to the corresponding Mask Interaction mode at runtime, a new material is generated on the fly.");
SerializedObject so = this.serializedObject;
skeletonDataAsset = so.FindProperty("skeletonDataAsset");
initialSkinName = so.FindProperty("initialSkinName");
initialFlipX = so.FindProperty("initialFlipX");
initialFlipY = so.FindProperty("initialFlipY");
normals = so.FindProperty("addNormals");
tangents = so.FindProperty("calculateTangents");
immutableTriangles = so.FindProperty("immutableTriangles");
pmaVertexColors = so.FindProperty("pmaVertexColors");
clearStateOnDisable = so.FindProperty("clearStateOnDisable");
tintBlack = so.FindProperty("tintBlack");
updateTiming = so.FindProperty("updateTiming");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
singleSubmesh = so.FindProperty("singleSubmesh");
fixDrawOrder = so.FindProperty("fixDrawOrder");
fixPrefabOverrideViaMeshFilter = so.FindProperty("fixPrefabOverrideViaMeshFilter");
maskInteraction = so.FindProperty("maskInteraction");
maskMaterialsNone = so.FindProperty("maskMaterials.materialsMaskDisabled");
maskMaterialsInside = so.FindProperty("maskMaterials.materialsInsideMask");
maskMaterialsOutside = so.FindProperty("maskMaterials.materialsOutsideMask");
physicsPositionInheritanceFactor = so.FindProperty("physicsPositionInheritanceFactor");
physicsRotationInheritanceFactor = so.FindProperty("physicsRotationInheritanceFactor");
physicsMovementRelativeTo = so.FindProperty("physicsMovementRelativeTo");
separatorSlotNames = so.FindProperty("separatorSlotNames");
separatorSlotNames.isExpanded = true;
zSpacing = so.FindProperty("zSpacing");
// Properties
singleSubmesh = serializedObject.FindProperty("singleSubmesh");
fixPrefabOverrideViaMeshFilter = serializedObject.FindProperty("fixPrefabOverrideViaMeshFilter");
maskInteraction = serializedObject.FindProperty("maskInteraction");
SerializedObject renderersSerializedObject = SpineInspectorUtility.GetRenderersSerializedObject(serializedObject); // Allows proper multi-edit behavior.
sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(renderersSerializedObject);
}
public void OnSceneGUI () {
SkeletonRenderer skeletonRenderer = (SkeletonRenderer)target;
if (loadingFailed)
return;
Skeleton skeleton = skeletonRenderer.Skeleton;
if (skeleton == null) {
loadingFailed = true;
return;
}
Transform transform = skeletonRenderer.transform;
if (skeleton == null) return;
SpineHandles.DrawBones(transform, skeleton);
}
override public void OnInspectorGUI () {
bool multi = serializedObject.isEditingMultipleObjects;
DrawInspectorGUI(multi);
HandleSkinChange();
if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current) ||
AreAnyMaskMaterialsMissing()) {
if (!Application.isPlaying) {
foreach (UnityEngine.Object o in targets)
SpineEditorUtilities.ReinitializeComponent((SkeletonRenderer)o);
SceneView.RepaintAll();
}
}
if (!isInspectingPrefab) {
if (requireRepaint) {
SceneView.RepaintAll();
requireRepaint = false;
}
}
}
protected virtual void DrawInspectorGUI (bool multi) {
// Initialize.
if (Event.current.type == EventType.Layout) {
if (forceReloadQueued) {
forceReloadQueued = false;
foreach (UnityEngine.Object c in targets) {
SpineEditorUtilities.ReloadSkeletonDataAssetAndComponent(c as SkeletonRenderer);
}
} else {
foreach (UnityEngine.Object c in targets) {
SkeletonRenderer component = c as SkeletonRenderer;
if (!component.valid) {
SpineEditorUtilities.ReinitializeComponent(component);
if (!component.valid) continue;
}
}
}
protected override void InspectorDrawPreparation () {
#if BUILT_IN_SPRITE_MASK_COMPONENT
if (setMaskNoneMaterialsQueued) {
setMaskNoneMaterialsQueued = false;
foreach (UnityEngine.Object c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.None);
}
if (setInsideMaskMaterialsQueued) {
setInsideMaskMaterialsQueued = false;
foreach (UnityEngine.Object c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleInsideMask);
}
if (setOutsideMaskMaterialsQueued) {
setOutsideMaskMaterialsQueued = false;
foreach (UnityEngine.Object c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleOutsideMask);
}
if (deleteInsideMaskMaterialsQueued) {
deleteInsideMaskMaterialsQueued = false;
foreach (UnityEngine.Object c in targets)
EditorDeleteMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleInsideMask);
}
if (deleteOutsideMaskMaterialsQueued) {
deleteOutsideMaskMaterialsQueued = false;
foreach (UnityEngine.Object c in targets)
EditorDeleteMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleOutsideMask);
}
foreach (UnityEngine.Object t in targets)
SpineMaskUtilities.EditorSetupSpriteMaskMaterials((SkeletonRenderer)t);
#endif
}
#if NO_PREFAB_MESH
if (isInspectingPrefab) {
foreach (UnityEngine.Object c in targets) {
SkeletonRenderer component = (SkeletonRenderer)c;
MeshFilter meshFilter = component.GetComponent<MeshFilter>();
if (meshFilter != null && meshFilter.sharedMesh != null)
meshFilter.sharedMesh = null;
}
}
#endif
}
bool valid = TargetIsValid;
foreach (UnityEngine.Object o in targets)
ApplyModifiedMeshParameters(o as SkeletonRenderer);
// Fields.
if (multi) {
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
forceReloadQueued = true;
}
if (valid) EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
} else {
SkeletonRenderer component = (SkeletonRenderer)target;
using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) {
SpineInspectorUtility.PropertyFieldFitLabel(skeletonDataAsset, SkeletonDataAssetLabel);
if (component.valid) {
if (GUILayout.Button(ReloadButtonString, ReloadButtonStyle, ReloadButtonWidth))
forceReloadQueued = true;
}
}
if (component.skeletonDataAsset == null) {
EditorGUILayout.HelpBox("SkeletonData asset required", MessageType.Warning);
return;
}
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(component.skeletonDataAsset)) {
EditorGUILayout.HelpBox("SkeletonData asset error. Please check SkeletonData asset.", MessageType.Error);
return;
}
if (valid)
EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
}
protected override void FirstPropertyFields () {
EditorGUILayout.Space();
// Sorting Layers
SpineInspectorUtility.SortingPropertyFields(sortingProperties, applyModifiedProperties: true);
if (maskInteraction != null) EditorGUILayout.PropertyField(maskInteraction, MaskInteractionLabel);
}
if (!valid)
return;
protected override void VertexDataProperties () {
EditorGUILayout.PropertyField(pmaVertexColors, PMAVertexColorsLabel);
EditorGUILayout.PropertyField(tintBlack, TintBlackLabel);
EditorGUILayout.PropertyField(addNormals, AddNormalsLabel);
EditorGUILayout.PropertyField(calculateTangents, CalculateTangentsLabel);
}
protected override void AdvancedPropertyFields () {
base.AdvancedPropertyFields();
if (singleSubmesh != null) EditorGUILayout.PropertyField(singleSubmesh, SingleSubmeshLabel);
#if HAS_ON_POSTPROCESS_PREFAB
if (fixPrefabOverrideViaMeshFilter != null) EditorGUILayout.PropertyField(fixPrefabOverrideViaMeshFilter, FixPrefabOverrideViaMeshFilterLabel);
EditorGUILayout.Space();
#endif
}
protected override void MaterialWarningsBox () {
string errorMessage = null;
if (SpineEditorUtilities.Preferences.componentMaterialWarning &&
MaterialChecks.IsMaterialSetupProblematic((SkeletonRenderer)this.target, ref errorMessage)) {
EditorGUILayout.HelpBox(errorMessage, MessageType.Error, true);
}
// More Render Options...
using (new SpineInspectorUtility.BoxScope()) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5));
advancedFoldout = EditorGUILayout.Foldout(advancedFoldout, "Advanced");
if (advancedFoldout) {
EditorGUILayout.Space();
if (GUILayout.Button("Debug", EditorStyles.miniButton, GUILayout.Width(65f)))
SkeletonDebugWindow.Init();
} else {
EditorGUILayout.Space();
}
EditorGUILayout.EndHorizontal();
if (advancedFoldout) {
using (new SpineInspectorUtility.IndentScope()) {
using (new EditorGUILayout.HorizontalScope()) {
EditorGUI.BeginChangeCheck();
SpineInspectorUtility.ToggleLeftLayout(initialFlipX);
SpineInspectorUtility.ToggleLeftLayout(initialFlipY);
wasInitParameterChanged |= EditorGUI.EndChangeCheck(); // Value used in the next update.
EditorGUILayout.Space();
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Renderer and Update Settings", EditorStyles.boldLabel);
using (new SpineInspectorUtility.LabelWidthScope()) {
// Optimization options
if (updateTiming != null) EditorGUILayout.PropertyField(updateTiming, UpdateTimingLabel);
if (updateWhenInvisible != null) EditorGUILayout.PropertyField(updateWhenInvisible, UpdateWhenInvisibleLabel);
if (singleSubmesh != null) EditorGUILayout.PropertyField(singleSubmesh, SingleSubmeshLabel);
#if PER_MATERIAL_PROPERTY_BLOCKS
if (fixDrawOrder != null) EditorGUILayout.PropertyField(fixDrawOrder, FixDrawOrderLabel);
#endif
if (immutableTriangles != null) EditorGUILayout.PropertyField(immutableTriangles, ImmubleTrianglesLabel);
EditorGUILayout.PropertyField(clearStateOnDisable, ClearStateOnDisableLabel);
EditorGUILayout.Space();
#if HAS_ON_POSTPROCESS_PREFAB
if (fixPrefabOverrideViaMeshFilter != null) EditorGUILayout.PropertyField(fixPrefabOverrideViaMeshFilter, FixPrefabOverrideViaMeshFilterLabel);
EditorGUILayout.Space();
#endif
}
SeparatorsField(separatorSlotNames);
EditorGUILayout.Space();
// Render options
const float MinZSpacing = -0.1f;
const float MaxZSpacing = 0f;
EditorGUILayout.Slider(zSpacing, MinZSpacing, MaxZSpacing, ZSpacingLabel);
EditorGUILayout.Space();
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Vertex Data", SpineInspectorUtility.UnityIcon<MeshFilter>()), EditorStyles.boldLabel);
if (pmaVertexColors != null) EditorGUILayout.PropertyField(pmaVertexColors, PMAVertexColorsLabel);
EditorGUILayout.PropertyField(tintBlack, TintBlackLabel);
// Optional fields. May be disabled in SkeletonRenderer.
if (normals != null) EditorGUILayout.PropertyField(normals, NormalsLabel);
if (tangents != null) EditorGUILayout.PropertyField(tangents, TangentsLabel);
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
EditorGUILayout.Space();
if (maskMaterialsNone.arraySize > 0 || maskMaterialsInside.arraySize > 0 || maskMaterialsOutside.arraySize > 0) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Mask Interaction Materials", SpineInspectorUtility.UnityIcon<SpriteMask>()), EditorStyles.boldLabel);
bool differentMaskModesSelected = maskInteraction.hasMultipleDifferentValues;
int activeMaskInteractionValue = differentMaskModesSelected ? -1 : maskInteraction.intValue;
bool ignoredParam = true;
MaskMaterialsEditingField(ref setMaskNoneMaterialsQueued, ref ignoredParam, maskMaterialsNone, MaskMaterialsNoneLabel,
differentMaskModesSelected, allowDelete: false, isActiveMaterial: activeMaskInteractionValue == (int)SpriteMaskInteraction.None);
MaskMaterialsEditingField(ref setInsideMaskMaterialsQueued, ref deleteInsideMaskMaterialsQueued, maskMaterialsInside, MaskMaterialsInsideLabel,
differentMaskModesSelected, allowDelete: true, isActiveMaterial: activeMaskInteractionValue == (int)SpriteMaskInteraction.VisibleInsideMask);
MaskMaterialsEditingField(ref setOutsideMaskMaterialsQueued, ref deleteOutsideMaskMaterialsQueued, maskMaterialsOutside, MaskMaterialsOutsideLabel,
differentMaskModesSelected, allowDelete: true, isActiveMaterial: activeMaskInteractionValue == (int)SpriteMaskInteraction.VisibleOutsideMask);
}
#endif
using (new SpineInspectorUtility.LabelWidthScope()) {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent("Physics Inheritance", SpineEditorUtilities.Icons.constraintPhysics), EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(PhysicsPositionInheritanceFactorLabel, GUILayout.Width(EditorGUIUtility.labelWidth));
int savedIndentLevel = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
EditorGUILayout.PropertyField(physicsPositionInheritanceFactor, GUIContent.none, GUILayout.MinWidth(60));
EditorGUI.indentLevel = savedIndentLevel;
}
EditorGUILayout.PropertyField(physicsRotationInheritanceFactor, PhysicsRotationInheritanceFactorLabel);
EditorGUILayout.PropertyField(physicsMovementRelativeTo, PhysicsMovementRelativeToLabel);
}
EditorGUILayout.Space();
if (valid && !isInspectingPrefab) {
if (multi) {
// Support multi-edit SkeletonUtility button.
// EditorGUILayout.Space();
// bool addSkeletonUtility = GUILayout.Button(buttonContent, GUILayout.Height(30));
// foreach (UnityEngine.Object t in targets) {
// Component component = t as Component;
// if (addSkeletonUtility && component.GetComponent<SkeletonUtility>() == null)
// component.gameObject.AddComponent<SkeletonUtility>();
// }
} else {
Component component = (Component)target;
if (component.GetComponent<SkeletonUtility>() == null) {
if (SpineInspectorUtility.CenteredButton(SkeletonUtilityButtonContent, 21, true, 200f))
component.gameObject.AddComponent<SkeletonUtility>();
}
}
}
EditorGUILayout.Space();
}
}
if (EditorGUI.EndChangeCheck())
SceneView.RepaintAll();
}
}
protected void ApplyModifiedMeshParameters (SkeletonRenderer skeletonRenderer) {
if (skeletonRenderer == null) return;
if (!skeletonRenderer.valid)
return;
if (!isInspectingPrefab) {
if (wasInitParameterChanged) {
wasInitParameterChanged = false;
if (!Application.isPlaying) {
skeletonRenderer.Initialize(true);
skeletonRenderer.LateUpdate();
requireRepaint = true;
}
}
}
}
protected void SkeletonRootMotionParameter () {
SkeletonRootMotionParameter(targets);
}
public static void SkeletonRootMotionParameter (Object[] targets) {
int rootMotionComponentCount = 0;
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
if (component.GetComponent<SkeletonRootMotion>() != null) {
++rootMotionComponentCount;
}
}
bool allHaveRootMotion = rootMotionComponentCount == targets.Length;
bool anyHaveRootMotion = rootMotionComponentCount > 0;
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Root Motion");
if (!allHaveRootMotion) {
if (GUILayout.Button(SpineInspectorUtility.TempContent("Add Component", Icons.constraintTransform), GUILayout.MaxWidth(130), GUILayout.Height(18))) {
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
if (component.GetComponent<SkeletonRootMotion>() == null) {
component.gameObject.AddComponent<SkeletonRootMotion>();
}
}
}
}
if (anyHaveRootMotion) {
if (GUILayout.Button(SpineInspectorUtility.TempContent("Remove Component", Icons.constraintTransform), GUILayout.MaxWidth(140), GUILayout.Height(18))) {
foreach (UnityEngine.Object t in targets) {
Component component = t as Component;
SkeletonRootMotion rootMotionComponent = component.GetComponent<SkeletonRootMotion>();
if (rootMotionComponent != null) {
DestroyImmediate(rootMotionComponent);
}
}
}
}
}
}
public static void SetSeparatorSlotNames (SkeletonRenderer skeletonRenderer, string[] newSlotNames) {
FieldInfo field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
field.SetValue(skeletonRenderer, newSlotNames);
}
public static string[] GetSeparatorSlotNames (SkeletonRenderer skeletonRenderer) {
FieldInfo field = SpineInspectorUtility.GetNonPublicField(typeof(SkeletonRenderer), SeparatorSlotNamesFieldName);
return field.GetValue(skeletonRenderer) as string[];
}
public static void SeparatorsField (SerializedProperty separatorSlotNames) {
bool multi = separatorSlotNames.serializedObject.isEditingMultipleObjects;
bool hasTerminalSlot = false;
if (!multi) {
ISkeletonComponent sr = separatorSlotNames.serializedObject.targetObject as ISkeletonComponent;
Skeleton skeleton = sr.Skeleton;
int lastSlot = skeleton.Slots.Count - 1;
if (skeleton != null) {
for (int i = 0, n = separatorSlotNames.arraySize; i < n; i++) {
string slotName = separatorSlotNames.GetArrayElementAtIndex(i).stringValue;
SlotData slot = skeleton.Data.FindSlot(slotName);
int index = slot != null ? slot.Index : -1;
if (index == 0 || index == lastSlot) {
hasTerminalSlot = true;
break;
}
}
}
}
string terminalSlotWarning = hasTerminalSlot ? " (!)" : "";
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
const string SeparatorsDescription = "Stored names of slots where the Skeleton's render will be split into different batches. This is used by separate components that split the render into different MeshRenderers or GameObjects.";
if (separatorSlotNames.isExpanded) {
EditorGUILayout.PropertyField(separatorSlotNames, SpineInspectorUtility.TempContent(separatorSlotNames.displayName + terminalSlotWarning, Icons.slotRoot, SeparatorsDescription), true);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("+", GUILayout.MaxWidth(28f), GUILayout.MaxHeight(15f))) {
separatorSlotNames.arraySize++;
}
GUILayout.EndHorizontal();
EditorGUILayout.Space();
} else
EditorGUILayout.PropertyField(separatorSlotNames, new GUIContent(separatorSlotNames.displayName + string.Format("{0} [{1}]", terminalSlotWarning, separatorSlotNames.arraySize), SeparatorsDescription), true);
}
}
public void MaskMaterialsEditingField (ref bool wasSetRequested, ref bool wasDeleteRequested,
SerializedProperty maskMaterials, GUIContent label,
bool differentMaskModesSelected, bool allowDelete, bool isActiveMaterial) {
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField(label, isActiveMaterial ? EditorStyles.boldLabel : EditorStyles.label, GUILayout.MinWidth(80f), GUILayout.MaxWidth(140));
EditorGUILayout.LabelField(maskMaterials.hasMultipleDifferentValues ? "-" : maskMaterials.arraySize.ToString(), EditorStyles.miniLabel, GUILayout.Width(42f));
bool enableSetButton = differentMaskModesSelected || maskMaterials.arraySize == 0;
bool enableClearButtons = differentMaskModesSelected || (maskMaterials.arraySize != 0 && !isActiveMaterial);
EditorGUI.BeginDisabledGroup(!enableSetButton);
if (GUILayout.Button(SetMaterialButtonLabel, EditorStyles.miniButtonLeft, GUILayout.Width(46f))) {
wasSetRequested = true;
}
EditorGUI.EndDisabledGroup();
EditorGUI.BeginDisabledGroup(!enableClearButtons);
{
if (GUILayout.Button(ClearMaterialButtonLabel, allowDelete ? EditorStyles.miniButtonMid : EditorStyles.miniButtonRight, GUILayout.Width(46f))) {
maskMaterials.ClearArray();
} else if (allowDelete && GUILayout.Button(DeleteMaterialButtonLabel, EditorStyles.miniButtonRight, GUILayout.Width(46f))) {
wasDeleteRequested = true;
}
if (!allowDelete)
GUILayout.Space(46f);
}
EditorGUI.EndDisabledGroup();
}
}
void HandleSkinChange () {
if (!Application.isPlaying && Event.current.type == EventType.Layout && !initialSkinName.hasMultipleDifferentValues) {
bool mismatchDetected = false;
string newSkinName = initialSkinName.stringValue;
foreach (UnityEngine.Object o in targets) {
mismatchDetected |= UpdateIfSkinMismatch((SkeletonRenderer)o, newSkinName);
}
if (mismatchDetected) {
mismatchDetected = false;
SceneView.RepaintAll();
}
}
}
static bool UpdateIfSkinMismatch (SkeletonRenderer skeletonRenderer, string componentSkinName) {
if (!skeletonRenderer.valid || skeletonRenderer.EditorSkipSkinSync) return false;
Skin skin = skeletonRenderer.Skeleton.Skin;
string skeletonSkinName = skin != null ? skin.Name : null;
bool defaultCase = skin == null && string.IsNullOrEmpty(componentSkinName);
bool fieldMatchesSkin = defaultCase || string.Equals(componentSkinName, skeletonSkinName, System.StringComparison.Ordinal);
if (!fieldMatchesSkin) {
Skin skinToSet = string.IsNullOrEmpty(componentSkinName) ? null : skeletonRenderer.Skeleton.Data.FindSkin(componentSkinName);
skeletonRenderer.Skeleton.SetSkin(skinToSet);
skeletonRenderer.Skeleton.SetupPoseSlots();
// Note: the UpdateIfSkinMismatch concept shall be replaced with e.g. an OnValidate based
// solution or in a separate commit. The current solution does not repaint the Game view because
// it is first applying values and in the next editor pass is calling this skin-changing method.
if (skeletonRenderer is SkeletonAnimation)
((SkeletonAnimation)skeletonRenderer).Update(0f);
else if (skeletonRenderer is SkeletonMecanim)
((SkeletonMecanim)skeletonRenderer).Update();
skeletonRenderer.LateUpdate();
return true;
}
return false;
}
bool AreAnyMaskMaterialsMissing () {
#if BUILT_IN_SPRITE_MASK_COMPONENT
foreach (UnityEngine.Object o in targets) {
SkeletonRenderer component = (SkeletonRenderer)o;
if (!component.valid)
continue;
if (SpineMaskUtilities.AreMaskMaterialsMissing(component))
return true;
}
#endif
return false;
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
static void EditorSetMaskMaterials (SkeletonRenderer component, SpriteMaskInteraction maskType) {
if (component == null) return;
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return;
SpineMaskUtilities.EditorInitMaskMaterials(component, component.maskMaterials, maskType);
}
static void EditorDeleteMaskMaterials (SkeletonRenderer component, SpriteMaskInteraction maskType) {
if (component == null) return;
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return;
SpineMaskUtilities.EditorDeleteMaskMaterials(component.maskMaterials, maskType);
}
#endif
}
}

View File

@ -0,0 +1,80 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. 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 warning disable 0219
#if UNITY_2017_2_OR_NEWER
#define NEWPLAYMODECALLBACKS
#endif
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
[InitializeOnLoad]
public static class ApplicationStateHandler {
static ApplicationStateHandler () {
Initialize();
}
static void Initialize () {
#if NEWPLAYMODECALLBACKS
EditorApplication.playModeStateChanged -= OnPlaymodeStateChanged;
EditorApplication.playModeStateChanged += OnPlaymodeStateChanged;
OnPlaymodeStateChanged(PlayModeStateChange.EnteredEditMode);
#else
EditorApplication.playmodeStateChanged -= OnPlaymodeStateChanged;
EditorApplication.playmodeStateChanged += OnPlaymodeStateChanged;
OnPlaymodeStateChanged();
#endif
}
#if NEWPLAYMODECALLBACKS
internal static void OnPlaymodeStateChanged (PlayModeStateChange stateChange) {
bool isPlaying = stateChange == PlayModeStateChange.EnteredPlayMode ||
stateChange == PlayModeStateChange.ExitingEditMode;
#else
internal static void OnPlaymodeStateChanged () {
bool isPlaying = false;
if (EditorApplication.isPaused ||
EditorApplication.isPlaying ||
EditorApplication.isPlayingOrWillChangePlaymode) isPlaying = true;
#endif
UpdateApplicationStateToPlaying(isPlaying);
}
public static void UpdateApplicationStateToPlaying (bool isPlaying) {
SkeletonRenderer.ApplicationIsPlaying = isPlaying;
SkeletonGraphic.ApplicationIsPlaying = isPlaying;
SkeletonAnimationBase.ApplicationIsPlaying = isPlaying;
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 795971ea6ab1f214eac09ad8814226e6
guid: 34aa4bd70dc99bb4b8f3ebcc3b902c27
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -1133,7 +1133,7 @@ namespace Spine.Unity.Editor {
AssetDatabase.CreateAsset(skeletonDataAsset, filePath);
AssetDatabase.SaveAssets();
} else {
skeletonDataAsset.Clear();
SpineEditorUtilities.ClearSkeletonDataAsset(targetSkeletonDataAsset);
skeletonDataAsset.GetSkeletonData(true);
}
@ -1379,12 +1379,12 @@ namespace Spine.Unity.Editor {
internal static readonly List<SkeletonComponentSpawnType> additionalSpawnTypes = new List<SkeletonComponentSpawnType>();
public static void TryInitializeSkeletonRendererSettings (SkeletonRenderer skeletonRenderer, Skin skin = null) {
public static void TryInitializeSkeletonRendererSettings (ISkeletonRenderer skeletonRenderer, Skin skin = null) {
const string PMAShaderQuery = "Spine/";
const string TintBlackShaderQuery = "Tint Black";
if (skeletonRenderer == null) return;
SkeletonDataAsset skeletonDataAsset = skeletonRenderer.skeletonDataAsset;
SkeletonDataAsset skeletonDataAsset = skeletonRenderer.SkeletonDataAsset;
if (skeletonDataAsset == null) return;
bool pmaVertexColors = false;
@ -1409,9 +1409,11 @@ namespace Spine.Unity.Editor {
}
}
skeletonRenderer.pmaVertexColors = pmaVertexColors;
skeletonRenderer.tintBlack = tintBlack;
skeletonRenderer.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
MeshGenerator.Settings meshSettings = skeletonRenderer.MeshSettings;
meshSettings.pmaVertexColors = pmaVertexColors;
meshSettings.tintBlack = tintBlack;
meshSettings.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
skeletonRenderer.PhysicsPositionInheritanceFactor = SpineEditorUtilities.Preferences.defaultPhysicsPositionInheritance;
skeletonRenderer.PhysicsRotationInheritanceFactor = SpineEditorUtilities.Preferences.defaultPhysicsRotationInheritance;
@ -1419,7 +1421,7 @@ namespace Spine.Unity.Editor {
bool noSkins = data.DefaultSkin == null && (data.Skins == null || data.Skins.Count == 0); // Support attachmentless/skinless SkeletonData.
skin = skin ?? data.DefaultSkin ?? (noSkins ? null : data.Skins.Items[0]);
if (skin != null && skin != data.DefaultSkin) {
skeletonRenderer.initialSkinName = skin.Name;
skeletonRenderer.InitialSkinName = skin.Name;
}
}
@ -1451,10 +1453,11 @@ namespace Spine.Unity.Editor {
string spineGameObjectName = string.Format("Spine GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, ""));
GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonAnimation));
typeof(MeshFilter), typeof(MeshRenderer), typeof(SkeletonRenderer), typeof(SkeletonAnimation));
SkeletonRenderer skeletonRenderer = go.GetComponent<SkeletonRenderer>();
SkeletonAnimation newSkeletonAnimation = go.GetComponent<SkeletonAnimation>();
newSkeletonAnimation.skeletonDataAsset = skeletonDataAsset;
TryInitializeSkeletonRendererSettings(newSkeletonAnimation, skin);
skeletonRenderer.skeletonDataAsset = skeletonDataAsset;
TryInitializeSkeletonRendererSettings(skeletonRenderer, skin);
// Initialize
try {
@ -1468,8 +1471,8 @@ namespace Spine.Unity.Editor {
}
newSkeletonAnimation.loop = SpineEditorUtilities.Preferences.defaultInstantiateLoop;
newSkeletonAnimation.state.Update(0);
newSkeletonAnimation.state.Apply(newSkeletonAnimation.skeleton);
newSkeletonAnimation.Update(0);
newSkeletonAnimation.AnimationState.Apply(newSkeletonAnimation.skeleton);
newSkeletonAnimation.skeleton.UpdateWorldTransform(Physics.Update);
return newSkeletonAnimation;
@ -1493,6 +1496,16 @@ namespace Spine.Unity.Editor {
return new GameObject(name, components);
}
/// <summary>Handles adding a Component to a GameObject in the Unity Editor.
/// This uses the new ObjectFactory API where applicable.</summary>
public static Component AddComponent (GameObject gameObject, bool useObjectFactory, System.Type type) {
#if NEW_PREFAB_SYSTEM
if (useObjectFactory)
return ObjectFactory.AddComponent(gameObject, type);
#endif
return gameObject.AddComponent(type);
}
public static void InstantiateEmptySpineGameObject<T> (string name, bool useObjectFactory) where T : MonoBehaviour {
GameObject parentGameObject = Selection.activeObject as GameObject;
Transform parentTransform = parentGameObject == null ? null : parentGameObject.transform;
@ -1529,7 +1542,7 @@ namespace Spine.Unity.Editor {
string spineGameObjectName = string.Format("Spine Mecanim GameObject ({0})", skeletonDataAsset.name.Replace(AssetUtility.SkeletonDataSuffix, ""));
GameObject go = EditorInstantiation.NewGameObject(spineGameObjectName, useObjectFactory,
typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonMecanim));
typeof(MeshFilter), typeof(MeshRenderer), typeof(Animator), typeof(SkeletonRenderer), typeof(SkeletonMecanim));
if (skeletonDataAsset.controller == null) {
SkeletonBaker.GenerateMecanimAnimationClips(skeletonDataAsset);
@ -1538,9 +1551,10 @@ namespace Spine.Unity.Editor {
go.GetComponent<Animator>().runtimeAnimatorController = skeletonDataAsset.controller;
SkeletonRenderer skeletonRenderer = go.GetComponent<SkeletonRenderer>();
SkeletonMecanim newSkeletonMecanim = go.GetComponent<SkeletonMecanim>();
newSkeletonMecanim.skeletonDataAsset = skeletonDataAsset;
TryInitializeSkeletonRendererSettings(newSkeletonMecanim, skin);
skeletonRenderer.skeletonDataAsset = skeletonDataAsset;
TryInitializeSkeletonRendererSettings(skeletonRenderer, skin);
// Initialize
try {
@ -1553,8 +1567,8 @@ namespace Spine.Unity.Editor {
throw e;
}
newSkeletonMecanim.skeleton.UpdateWorldTransform(Physics.Update);
newSkeletonMecanim.LateUpdate();
newSkeletonMecanim.UpdateOncePerFrame(0);
newSkeletonMecanim.Renderer.LateUpdate();
return newSkeletonMecanim;
}

View File

@ -84,6 +84,7 @@ namespace Spine.Unity.Editor {
public static class SpineBuildEnvUtility {
public const string SPINE_ALLOW_UNSAFE_CODE = "SPINE_ALLOW_UNSAFE";
public const string SPINE_AUTO_UPGRADE_COMPONENTS_OFF = "SPINE_AUTO_UPGRADE_COMPONENTS_OFF";
static bool IsInvalidGroup (BuildTargetGroup group) {
int gi = (int)group;

View File

@ -43,6 +43,10 @@
#define NEW_PREFERENCES_SETTINGS_PROVIDER
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -149,6 +153,15 @@ namespace Spine.Unity.Editor {
}
}
public static bool ShowSplitComponentChangeWarning {
get { return splitComponentChangeWarning; }
set {
if (splitComponentChangeWarning == value) return;
splitComponentChangeWarning = value;
EditorPrefs.SetBool(SPLIT_COMPONENT_CHANGE_WARNING_KEY, splitComponentChangeWarning);
}
}
const string APPLY_ADDITIVE_MATERIAL_KEY = "SPINE_APPLY_ADDITIVE_MATERIAL";
const string BLEND_MODE_MATERIAL_MULTIPLY_KEY = "SPINE_BLENDMODE_MATERIAL_MULTIPLY";
const string BLEND_MODE_MATERIAL_SCREEN_KEY = "SPINE_BLENDMODE_MATERIAL_SCREEN";
@ -201,6 +214,9 @@ namespace Spine.Unity.Editor {
const string WORKFLOW_MISMATCH_DIALOG_KEY = "SPINE_WORKFLOW_MISMATCH_DIALOG";
public static bool workflowMismatchDialog = SpinePreferences.DEFAULT_WORKFLOW_MISMATCH_DIALOG;
const string SPLIT_COMPONENT_CHANGE_WARNING_KEY = "SPINE_SPLIT_COMPONENT_CHANGE_WARNING";
public static bool splitComponentChangeWarning = SpinePreferences.DEFAULT_SPLIT_COMPONENT_CHANGE_WARNING;
public const float DEFAULT_MIPMAPBIAS = SpinePreferences.DEFAULT_MIPMAPBIAS;
public const string SCENE_ICONS_SCALE_KEY = "SPINE_SCENE_ICONS_SCALE";
@ -247,6 +263,7 @@ namespace Spine.Unity.Editor {
componentMaterialWarning = EditorPrefs.GetBool(COMPONENTMATERIAL_WARNING_KEY, SpinePreferences.DEFAULT_COMPONENTMATERIAL_WARNING);
skeletonDataAssetNoFileError = EditorPrefs.GetBool(SKELETONDATA_ASSET_NO_FILE_ERROR_KEY, SpinePreferences.DEFAULT_SKELETONDATA_ASSET_NO_FILE_ERROR);
workflowMismatchDialog = EditorPrefs.GetBool(WORKFLOW_MISMATCH_DIALOG_KEY, SpinePreferences.DEFAULT_WORKFLOW_MISMATCH_DIALOG);
splitComponentChangeWarning = EditorPrefs.GetBool(SPLIT_COMPONENT_CHANGE_WARNING_KEY, SpinePreferences.DEFAULT_SPLIT_COMPONENT_CHANGE_WARNING);
timelineDefaultMixDuration = EditorPrefs.GetBool(TIMELINE_DEFAULT_MIX_DURATION_KEY, SpinePreferences.DEFAULT_TIMELINE_DEFAULT_MIX_DURATION);
timelineUseBlendDuration = EditorPrefs.GetBool(TIMELINE_USE_BLEND_DURATION_KEY, SpinePreferences.DEFAULT_TIMELINE_USE_BLEND_DURATION);
handleScale = EditorPrefs.GetFloat(SCENE_ICONS_SCALE_KEY, SpinePreferences.DEFAULT_SCENE_ICONS_SCALE);
@ -299,6 +316,7 @@ namespace Spine.Unity.Editor {
EditorPrefs.SetBool(COMPONENTMATERIAL_WARNING_KEY, preferences.componentMaterialWarning);
EditorPrefs.SetBool(SKELETONDATA_ASSET_NO_FILE_ERROR_KEY, preferences.skeletonDataAssetNoFileError);
EditorPrefs.SetBool(WORKFLOW_MISMATCH_DIALOG_KEY, preferences.workflowMismatchDialog);
EditorPrefs.SetBool(SPLIT_COMPONENT_CHANGE_WARNING_KEY, preferences.splitComponentChangeWarning);
EditorPrefs.SetBool(TIMELINE_DEFAULT_MIX_DURATION_KEY, preferences.timelineDefaultMixDuration);
EditorPrefs.SetBool(TIMELINE_USE_BLEND_DURATION_KEY, preferences.timelineUseBlendDuration);
EditorPrefs.SetFloat(SCENE_ICONS_SCALE_KEY, preferences.handleScale);
@ -444,6 +462,51 @@ namespace Spine.Unity.Editor {
#endif
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Automatic Component Upgrade", EditorStyles.boldLabel);
#if SPINE_AUTO_UPGRADE_COMPONENTS_OFF
bool upgradeComponentsEnabled = false;
#else
bool upgradeComponentsEnabled = true;
#endif
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Split Component Upgrade", "Allow automatic upgrade of skeleton components to new split components."));
EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_AUTO_UPGRADE_COMPONENTS_OFF, upgradeComponentsEnabled, invert: true);
}
using (new EditorGUI.DisabledScope(!upgradeComponentsEnabled)) {
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Upgrade Scenes & Prefabs", "Upgrades all scenes and " +
"prefabs in the project to Spine 4.3 split animation components."));
if (GUILayout.Button("Upgrade All", GUILayout.Width(132))) {
if (EditorUtility.DisplayDialog("Upgrade to Spine 4.3",
"This will open and process all scenes and prefabs in your project to upgrade Spine " +
"components to split animation components of version 4.3.\n\n" +
"This process may take a while for large projects.\n\n" +
"Make sure to backup your project before proceeding.\n\n" +
"Continue?", "Yes, Upgrade", "Cancel")) {
#if AUTO_UPGRADE_TO_43_COMPONENTS
SpineEditorUtilities.UpgradeAllScenesAndPrefabsTo43();
#endif
}
}
}
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Threading Defaults", EditorStyles.boldLabel);
{
bool useThreadedMeshGeneration = RuntimeSettings.UseThreadedMeshGeneration;
bool useThreadedAnimation = RuntimeSettings.UseThreadedAnimation;
SpineEditorUtilities.BoolRuntimePropertiesField(
() => RuntimeSettings.UseThreadedMeshGeneration,
value => RuntimeSettings.UseThreadedMeshGeneration = value,
new GUIContent("Threaded MeshGeneration", "Default value for SkeletonRenderer and SkeletonGraphic Threaded Mesh Generation."));
SpineEditorUtilities.BoolRuntimePropertiesField(
() => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value,
new GUIContent("Threaded Animation", "Default value for SkeletonAnimation and SkeletonMecanim Threaded Animation."));
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Timeline Extension", EditorStyles.boldLabel);
{
@ -483,7 +546,17 @@ namespace Spine.Unity.Editor {
EditorPrefs.SetBool(editorPrefsKey, currentValue);
}
static void FloatPrefsField (ref float currentValue, string editorPrefsKey, GUIContent label, float min = float.NegativeInfinity, float max = float.PositiveInfinity) {
public static void BoolRuntimePropertiesField (System.Func<bool> propertyGetter, System.Action<bool> propertySetter, GUIContent label) {
bool value = propertyGetter();
EditorGUI.BeginChangeCheck();
value = EditorGUILayout.Toggle(label, value);
if (EditorGUI.EndChangeCheck()) {
propertySetter(value);
RuntimeSettingsEditor.SaveToRuntimeAsset();
}
}
public static void FloatPrefsField (ref float currentValue, string editorPrefsKey, GUIContent label, float min = float.NegativeInfinity, float max = float.PositiveInfinity) {
EditorGUI.BeginChangeCheck();
currentValue = EditorGUILayout.DelayedFloatField(label, currentValue);
if (EditorGUI.EndChangeCheck()) {
@ -530,6 +603,30 @@ namespace Spine.Unity.Editor {
property.stringValue = material ? AssetDatabase.GetAssetPath(material) : "";
}
public static void EnableDisableDefineButtons (string define, bool isEnabled, bool invert = false) {
bool changed = false;
bool enable = false;
using (new EditorGUI.DisabledScope(!isEnabled)) {
if (GUILayout.Button("Disable", GUILayout.Width(64))) {
changed = true;
enable = invert;
}
}
using (new EditorGUI.DisabledScope(isEnabled)) {
if (GUILayout.Button("Enable", GUILayout.Width(64))) {
changed = true;
enable = !invert;
}
}
if (changed) {
if (enable)
SpineBuildEnvUtility.EnableBuildDefine(define);
else
SpineBuildEnvUtility.DisableBuildDefine(define);
}
}
#if NEW_PREFERENCES_SETTINGS_PROVIDER
public static void PresetAssetPropertyField (SerializedProperty property, GUIContent label) {
UnityEditor.Presets.Preset texturePreset = (EditorGUILayout.ObjectField(label, AssetDatabase.LoadAssetAtPath<UnityEditor.Presets.Preset>(property.stringValue), typeof(UnityEditor.Presets.Preset), false) as UnityEditor.Presets.Preset);

View File

@ -0,0 +1,55 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System.IO;
using UnityEditor;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Editor part of <see cref="RuntimeSettings"/>.
/// </summary>
public class RuntimeSettingsEditor {
public static void SaveToRuntimeAsset () {
Directory.CreateDirectory("Assets/Resources");
string assetPath = System.IO.Path.Combine("Assets/Resources/", RuntimeSettings.ResourcePath + ".asset");
RuntimeSettings asset = AssetDatabase.LoadAssetAtPath<RuntimeSettings>(assetPath);
if (asset == null) {
asset = ScriptableObject.CreateInstance<RuntimeSettings>();
AssetDatabase.CreateAsset(asset, assetPath);
}
RuntimeSettings instance = RuntimeSettings.Instance;
asset.useThreadedMeshGeneration = instance.useThreadedMeshGeneration;
asset.useThreadedAnimation = instance.useThreadedAnimation;
EditorUtility.SetDirty(asset);
AssetDatabase.SaveAssets();
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 58bb7391599576e449572ba212fb2099
timeCreated: 1752165942
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -39,17 +39,6 @@
#define HAS_ON_POSTPROCESS_PREFAB
#endif
#if (UNITY_2020_3 && !(UNITY_2020_3_1 || UNITY_2020_3_2 || UNITY_2020_3_3 || UNITY_2020_3_4 || UNITY_2020_3_5 || UNITY_2020_3_6 || UNITY_2020_3_7 || UNITY_2020_3_8 || UNITY_2020_3_9 || UNITY_2020_3_10 || UNITY_2020_3_11 || UNITY_2020_3_12 || UNITY_2020_3_13 || UNITY_2020_3_14 || UNITY_2020_3_15))
#define UNITY_2020_3_16_OR_NEWER
#endif
#if (UNITY_2021_1 && !(UNITY_2021_1_1 || UNITY_2021_1_2 || UNITY_2021_1_3 || UNITY_2021_1_4 || UNITY_2021_1_5 || UNITY_2021_1_6 || UNITY_2021_1_7 || UNITY_2021_1_8 || UNITY_2021_1_9 || UNITY_2021_1_10 || UNITY_2021_1_11 || UNITY_2021_1_12 || UNITY_2021_1_13 || UNITY_2021_1_14 || UNITY_2021_1_15 || UNITY_2021_1_16))
#define UNITY_2021_1_17_OR_NEWER
#endif
#if UNITY_2020_3_16_OR_NEWER || UNITY_2021_1_17_OR_NEWER
#define HAS_SAVE_ASSET_IF_DIRTY
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
@ -75,8 +64,7 @@ namespace Spine.Unity.Editor {
internal static void PreprocessBuild () {
isBuilding = true;
#if HAS_ON_POSTPROCESS_PREFAB
if (SpineEditorUtilities.Preferences.removePrefabPreviewMeshes)
PreprocessSpinePrefabMeshes();
PreprocessSpinePrefabMeshes();
#endif
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
PreprocessOnDemandTextureLoaders();
@ -87,8 +75,7 @@ namespace Spine.Unity.Editor {
internal static void PostprocessBuild () {
isBuilding = false;
#if HAS_ON_POSTPROCESS_PREFAB
if (SpineEditorUtilities.Preferences.removePrefabPreviewMeshes)
PostprocessSpinePrefabMeshes();
PostprocessSpinePrefabMeshes();
#endif
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
PostprocessOnDemandTextureLoaders();
@ -109,18 +96,13 @@ namespace Spine.Unity.Editor {
GameObject prefabGameObject = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
if (SpineEditorUtilities.CleanupSpinePrefabMesh(prefabGameObject)) {
#if HAS_SAVE_ASSET_IF_DIRTY
AssetDatabase.SaveAssetIfDirty(prefabGameObject);
#endif
prefabsToRestore.Add(assetPath);
}
EditorUtility.UnloadUnusedAssetsImmediate();
}
EditorUtility.UnloadUnusedAssetsImmediate();
AssetDatabase.StopAssetEditing();
#if !HAS_SAVE_ASSET_IF_DIRTY
if (prefabAssets.Length > 0)
AssetDatabase.SaveAssets();
#endif
} finally {
BuildUtilities.IsInSkeletonAssetBuildPreProcessing = false;
}
@ -132,14 +114,9 @@ namespace Spine.Unity.Editor {
foreach (string assetPath in prefabsToRestore) {
GameObject g = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
SpineEditorUtilities.SetupSpinePrefabMesh(g, null);
#if HAS_SAVE_ASSET_IF_DIRTY
AssetDatabase.SaveAssetIfDirty(g);
#endif
}
#if !HAS_SAVE_ASSET_IF_DIRTY
if (prefabsToRestore.Count > 0)
AssetDatabase.SaveAssets();
#endif
prefabsToRestore.Clear();
} finally {
@ -222,16 +199,11 @@ namespace Spine.Unity.Editor {
spriteAtlasTexturesToRestore[assetPath] = AssetDatabase.GetAssetPath(atlasAsset.materials[0].mainTexture);
atlasAsset.materials[0].mainTexture = null;
}
#if HAS_SAVE_ASSET_IF_DIRTY
AssetDatabase.SaveAssetIfDirty(atlasAsset);
#endif
EditorUtility.UnloadUnusedAssetsImmediate();
}
EditorUtility.UnloadUnusedAssetsImmediate();
AssetDatabase.StopAssetEditing();
#if !HAS_SAVE_ASSET_IF_DIRTY
if (spriteAtlasAssets.Length > 0)
AssetDatabase.SaveAssets();
#endif
} finally {
BuildUtilities.IsInSpriteAtlasBuildPreProcessing = false;
}
@ -247,14 +219,9 @@ namespace Spine.Unity.Editor {
Texture atlasTexture = AssetDatabase.LoadAssetAtPath<Texture>(pair.Value);
atlasAsset.materials[0].mainTexture = atlasTexture;
}
#if HAS_SAVE_ASSET_IF_DIRTY
AssetDatabase.SaveAssetIfDirty(atlasAsset);
#endif
}
#if !HAS_SAVE_ASSET_IF_DIRTY
if (spriteAtlasTexturesToRestore.Count > 0)
AssetDatabase.SaveAssets();
#endif
spriteAtlasTexturesToRestore.Clear();
} finally {
BuildUtilities.IsInSpriteAtlasBuildPostProcessing = false;

View File

@ -54,6 +54,14 @@
#define HAS_ON_POSTPROCESS_PREFAB
#endif
#if UNITY_2020_1_OR_NEWER
#define HAS_EDIT_PREFAB_CONTENTS_SCOPE
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -61,6 +69,7 @@ using System.Linq;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace Spine.Unity.Editor {
@ -124,7 +133,7 @@ namespace Spine.Unity.Editor {
renderer.EditorUpdateMeshFilterHideFlags();
renderer.Initialize(true, true);
renderer.LateUpdateMesh();
renderer.UpdateMesh();
Mesh mesh = meshFilter.sharedMesh;
if (mesh == null) continue;
@ -232,11 +241,19 @@ namespace Spine.Unity.Editor {
EditorApplication.playmodeStateChanged += DataReloadHandler.OnPlaymodeStateChanged;
DataReloadHandler.OnPlaymodeStateChanged();
#endif
EditorBridge.OnRequestMarkDirty += OnRequestMarkDirty;
if (SpineEditorUtilities.Preferences.textureImporterWarning) {
IssueWarningsForUnrecommendedTextureSettings();
}
#if BUILT_IN_SPRITE_MASK_COMPONENT && AUTO_UPGRADE_TO_43_COMPONENTS
SpineMaskUtilities.EditorGatherAtlasAssetsMaskMaterials();
#endif
if (SpineEditorUtilities.Preferences.ShowSplitComponentChangeWarning) {
ComponentUpgradeWarningDialog.ShowDialog();
}
initialized = true;
}
@ -266,16 +283,9 @@ namespace Spine.Unity.Editor {
}
}
public static void ReloadSkeletonDataAssetAndComponent (SkeletonRenderer component) {
public static void ReloadSkeletonDataAssetAndComponent (ISkeletonRenderer component) {
if (component == null) return;
ReloadSkeletonDataAsset(component.skeletonDataAsset);
ReinitializeComponent(component);
}
public static void ReloadSkeletonDataAssetAndComponent (SkeletonGraphic component) {
if (component == null) return;
ReloadSkeletonDataAsset(component.skeletonDataAsset);
// Reinitialize.
ReloadSkeletonDataAsset(component.SkeletonDataAsset);
ReinitializeComponent(component);
}
@ -298,34 +308,36 @@ namespace Spine.Unity.Editor {
DataReloadHandler.ReloadAnimationReferenceAssets(skeletonDataAsset);
}
public static void ReinitializeComponent (SkeletonRenderer component) {
public static void ReinitializeComponent (ISkeletonRenderer component) {
if (component == null) return;
if (!SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return;
IAnimationStateComponent stateComponent = component as IAnimationStateComponent;
AnimationState oldAnimationState = null;
if (stateComponent != null) {
oldAnimationState = stateComponent.AnimationState;
}
component.Initialize(true);
component.Initialize(true); // implicitly clears any subscribers
if (oldAnimationState != null) {
stateComponent.AnimationState.AssignEventSubscribersFrom(oldAnimationState);
}
if (stateComponent != null) {
if (component.Animation != null) {
// Any set animation needs to be applied as well since it might set attachments,
// having an effect on generated SpriteMaskMaterials below.
stateComponent.AnimationState.Apply(component.skeleton);
component.Animation.ApplyAnimation();
component.LateUpdate();
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
SpineMaskUtilities.EditorAssignSpriteMaskMaterials(component);
SkeletonRenderer skeletonRenderer = component as SkeletonRenderer;
if (skeletonRenderer != null)
SpineMaskUtilities.EditorSetupSpriteMaskMaterials(skeletonRenderer);
#endif
component.LateUpdate();
}
public static void ReinitializeComponent (ISkeletonAnimation component) {
if (component == null || component.Renderer == null) return;
if (!SkeletonDataAssetIsValid(component.Renderer.SkeletonDataAsset)) return;
component.Initialize(true);
component.UpdateOncePerFrame(0);
component.Renderer.LateUpdate();
}
public static void ReinitializeComponent (SkeletonGraphic component) {
if (component == null) return;
if (!SkeletonDataAssetIsValid(component.SkeletonDataAsset)) return;
@ -337,6 +349,156 @@ namespace Spine.Unity.Editor {
return asset != null && asset.GetSkeletonData(quiet: true) != null;
}
#if AUTO_UPGRADE_TO_43_COMPONENTS
public static void UpgradeAllScenesAndPrefabsTo43 () {
int scenesUpdated = 0;
int prefabsUpdated = 0;
int componentsUpdated = 0;
// Find all scene assets
string[] sceneGuids = AssetDatabase.FindAssets("t:Scene");
List<string> scenePaths = new List<string>();
foreach (string guid in sceneGuids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path) && !path.StartsWith("Packages/"))
scenePaths.Add(path);
}
// Find all prefab assets
string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
List<string> prefabPaths = new List<string>();
foreach (string guid in prefabGuids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path) && !path.StartsWith("Packages/"))
prefabPaths.Add(path);
}
// Process scenes
UnityEngine.SceneManagement.Scene currentScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
string currentScenePath = currentScene.path;
foreach (string scenePath in scenePaths) {
try {
EditorUtility.DisplayProgressBar("Upgrading Spine Components",
"Processing scene: " + Path.GetFileName(scenePath),
(float)scenesUpdated / scenePaths.Count);
// Open the scene
UnityEngine.SceneManagement.Scene scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(scenePath,
UnityEditor.SceneManagement.OpenSceneMode.Single);
bool sceneModified = false;
// Find all IUpgradable components in the scene
GameObject[] rootObjects = scene.GetRootGameObjects();
List<IUpgradable> upgradableComponents = new List<IUpgradable>();
foreach (GameObject root in rootObjects) {
IUpgradable[] componentsInObject = root.GetComponentsInChildren<IUpgradable>(true);
upgradableComponents.AddRange(componentsInObject);
}
// Upgrade all found components
foreach (IUpgradable upgradable in upgradableComponents) {
if (upgradable != null) {
upgradable.UpgradeTo43();
componentsUpdated++;
sceneModified = true;
}
}
// Save the scene if modified
if (sceneModified) {
UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene);
scenesUpdated++;
}
} catch (System.Exception e) {
Debug.LogError(string.Format("Failed to process scene {0}: {1}", scenePath, e.Message));
}
}
// Process prefabs
for (int i = 0; i < prefabPaths.Count; i++) {
string prefabPath = prefabPaths[i];
try {
EditorUtility.DisplayProgressBar("Migrating Spine Components to 4.3",
"Processing prefab: " + Path.GetFileName(prefabPath),
(float)(scenePaths.Count + i) / (scenePaths.Count + prefabPaths.Count));
GameObject prefabRoot = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
if (prefabRoot != null) {
bool prefabModified = false;
#if HAS_EDIT_PREFAB_CONTENTS_SCOPE
using (var editingScope = new PrefabUtility.EditPrefabContentsScope(prefabPath)) {
GameObject prefabContents = editingScope.prefabContentsRoot;
IUpgradable[] upgradableComponents = prefabContents.GetComponentsInChildren<IUpgradable>(true);
foreach (IUpgradable upgradable in upgradableComponents) {
if (upgradable != null) {
upgradable.UpgradeTo43();
componentsUpdated++;
prefabModified = true;
}
}
if (prefabModified) {
prefabsUpdated++;
}
}
#else // HAS_EDIT_PREFAB_CONTENTS_SCOPE
// Unity 2017.1 compatible approach
// Instantiate the prefab temporarily to modify it
GameObject tempInstance = PrefabUtility.InstantiatePrefab(prefabRoot) as GameObject;
if (tempInstance != null) {
// Find all IUpgradable components in the prefab instance
IUpgradable[] upgradableComponents = tempInstance.GetComponentsInChildren<IUpgradable>(true);
// Upgrade all found components
foreach (IUpgradable upgradable in upgradableComponents) {
if (upgradable != null) {
upgradable.UpgradeTo43();
componentsUpdated++;
prefabModified = true;
}
}
if (prefabModified) {
// Apply changes back to the prefab asset
PrefabUtility.ReplacePrefab(tempInstance, prefabRoot, ReplacePrefabOptions.ConnectToPrefab);
prefabsUpdated++;
}
// Clean up the temporary instance
GameObject.DestroyImmediate(tempInstance);
}
#endif
}
} catch (System.Exception e) {
Debug.LogError(string.Format("Failed to process prefab {0}: {1}", prefabPath, e.Message));
}
}
// Restore original scene if needed
if (!string.IsNullOrEmpty(currentScenePath) && currentScenePath != UnityEngine.SceneManagement.SceneManager.GetActiveScene().path) {
UnityEditor.SceneManagement.EditorSceneManager.OpenScene(currentScenePath,
UnityEditor.SceneManagement.OpenSceneMode.Single);
}
EditorUtility.ClearProgressBar();
// Show results
string message = string.Format("Migration to Spine 4.3 complete!\n\n" +
"Scenes processed: {0}/{1}\n" +
"Prefabs processed: {2}/{3}\n" +
"Components upgraded: {4}",
scenesUpdated, scenePaths.Count,
prefabsUpdated, prefabPaths.Count,
componentsUpdated);
EditorUtility.DisplayDialog("Spine 4.3 Migration Complete", message, "OK");
Debug.Log("[Spine] " + message);
}
#endif // AUTO_UPGRADE_TO_43_COMPONENTS
public static bool IssueWarningsForUnrecommendedTextureSettings (string texturePath) {
TextureImporter texImporter = (TextureImporter)TextureImporter.GetAtPath(texturePath);
if (texImporter == null) {
@ -358,6 +520,15 @@ namespace Spine.Unity.Editor {
}
return true;
}
static void OnRequestMarkDirty (GameObject go) {
if (go == null) return;
EditorApplication.delayCall += () => {
EditorUtility.SetDirty(go);
EditorSceneManager.MarkSceneDirty(go.scene);
};
}
#endregion
public static class HierarchyHandler {

View File

@ -552,7 +552,9 @@ namespace Spine.Unity.Editor {
Undo.RecordObject(skeletonGraphic, "Change Offset to Pivot");
Vector3 localScaledOffset = skeletonGraphic.transform.InverseTransformPoint(newWorldSpacePosition);
skeletonGraphic.SetScaledPivotOffset(localScaledOffset);
skeletonGraphic.UpdateMeshToInstructions();
skeletonGraphic.UpdateBuffersToInstructions(true);
skeletonGraphic.UpdateMeshAndMaterialsToBuffers();
}
Handles.DrawSolidDisc(newWorldSpacePosition, skeletonGraphic.transform.forward, discSize);
Handles.color = savedColor;

View File

@ -64,159 +64,112 @@ namespace Spine.Unity.Editor {
private const string MATERIAL_FILENAME_SUFFIX_INSIDE_MASK = "_InsideMask";
private const string MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK = "_OutsideMask";
public static void EditorAssignSpriteMaskMaterials (SkeletonRenderer skeleton) {
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
SpriteMaskInteraction maskInteraction = skeleton.maskInteraction;
MeshRenderer meshRenderer = skeleton.GetComponent<MeshRenderer>();
if (maskMaterials.materialsMaskDisabled.Length > 0 && maskMaterials.materialsMaskDisabled[0] != null &&
maskInteraction == SpriteMaskInteraction.None) {
meshRenderer.materials = maskMaterials.materialsMaskDisabled;
} else if (maskInteraction == SpriteMaskInteraction.VisibleInsideMask) {
if (maskMaterials.materialsInsideMask.Length == 0 || maskMaterials.materialsInsideMask[0] == null)
EditorInitSpriteMaskMaterialsInsideMask(skeleton);
meshRenderer.materials = maskMaterials.materialsInsideMask;
} else if (maskInteraction == SpriteMaskInteraction.VisibleOutsideMask) {
if (maskMaterials.materialsOutsideMask.Length == 0 || maskMaterials.materialsOutsideMask[0] == null)
EditorInitSpriteMaskMaterialsOutsideMask(skeleton);
meshRenderer.materials = maskMaterials.materialsOutsideMask;
}
}
public static bool AreMaskMaterialsMissing (SkeletonRenderer skeleton) {
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
SpriteMaskInteraction maskInteraction = skeleton.maskInteraction;
if (maskInteraction == SpriteMaskInteraction.VisibleInsideMask) {
return (maskMaterials.materialsInsideMask.Length == 0 || maskMaterials.materialsInsideMask[0] == null);
} else if (maskInteraction == SpriteMaskInteraction.VisibleOutsideMask) {
return (maskMaterials.materialsOutsideMask.Length == 0 || maskMaterials.materialsOutsideMask[0] == null);
}
return false;
}
public static void EditorInitMaskMaterials (SkeletonRenderer skeleton, SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials, SpriteMaskInteraction maskType) {
if (maskType == SpriteMaskInteraction.None) {
EditorConfirmDisabledMaskMaterialsInit(skeleton);
} else if (maskType == SpriteMaskInteraction.VisibleInsideMask) {
EditorInitSpriteMaskMaterialsInsideMask(skeleton);
} else if (maskType == SpriteMaskInteraction.VisibleOutsideMask) {
EditorInitSpriteMaskMaterialsOutsideMask(skeleton);
}
}
public static void EditorDeleteMaskMaterials (SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials, SpriteMaskInteraction maskType) {
Material[] targetMaterials;
if (maskType == SpriteMaskInteraction.VisibleInsideMask) {
targetMaterials = maskMaterials.materialsInsideMask;
} else if (maskType == SpriteMaskInteraction.VisibleOutsideMask) {
targetMaterials = maskMaterials.materialsOutsideMask;
} else {
Debug.LogWarning("EditorDeleteMaskMaterials: Normal materials are kept as a reference and shall never be deleted.");
return;
}
for (int i = 0; i < targetMaterials.Length; ++i) {
Material material = targetMaterials[i];
if (material != null) {
string materialPath = UnityEditor.AssetDatabase.GetAssetPath(material);
UnityEditor.AssetDatabase.DeleteAsset(materialPath);
Debug.Log(string.Concat("Deleted material '", materialPath, "'"));
public static void EditorGatherAtlasAssetsMaskMaterials () {
string[] guids = AssetDatabase.FindAssets("t:AtlasAssetBase");
foreach (string guid in guids) {
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path)) {
AtlasAssetBase atlasAsset = AssetDatabase.LoadAssetAtPath<AtlasAssetBase>(path);
if (atlasAsset && !atlasAsset.HasMaterialOverrideSets)
EditorGatherAtlasAssetMaskMaterials(atlasAsset);
}
}
}
if (maskType == SpriteMaskInteraction.VisibleInsideMask) {
maskMaterials.materialsInsideMask = new Material[0];
} else if (maskType == SpriteMaskInteraction.VisibleOutsideMask) {
maskMaterials.materialsOutsideMask = new Material[0];
public static void EditorGatherAtlasAssetMaskMaterials (AtlasAssetBase atlasAsset) {
EditorGatherAtlasAssetMaskMaterials(atlasAsset,
SkeletonRenderer.MATERIAL_OVERRIDE_SET_INSIDE_MASK_NAME,
SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE);
EditorGatherAtlasAssetMaskMaterials(atlasAsset,
SkeletonRenderer.MATERIAL_OVERRIDE_SET_OUTSIDE_MASK_NAME,
SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE);
}
public static void EditorGatherAtlasAssetMaskMaterials (AtlasAssetBase atlasAsset,
string overrideSetName, UnityEngine.Rendering.CompareFunction maskFunction) {
MaterialOverrideSet overrideSet = atlasAsset.GetMaterialOverrideSet(overrideSetName);
foreach (Material originalMaterial in atlasAsset.Materials) {
string originalMaterialPath = AssetDatabase.GetAssetPath(originalMaterial);
if (string.IsNullOrEmpty(originalMaterialPath)) continue;
string maskMaterialPath = MaskMaterialPath(originalMaterialPath, maskFunction);
Material maskMaterial = AssetDatabase.LoadAssetAtPath<Material>(maskMaterialPath);
if (maskMaterial != null) {
if (overrideSet == null)
overrideSet = atlasAsset.AddMaterialOverrideSet(overrideSetName);
overrideSet.SetOverride(originalMaterial, maskMaterial);
}
}
}
public static void EditorSetupSpriteMaskMaterials (SkeletonRenderer skeleton) {
SpriteMaskInteraction maskInteraction = skeleton.maskInteraction;
if (maskInteraction == SpriteMaskInteraction.VisibleInsideMask) {
if (skeleton.insideMaskMaterials == null)
EditorInitSpriteMaskMaterialsInsideMask(skeleton);
} else if (maskInteraction == SpriteMaskInteraction.VisibleOutsideMask) {
if (skeleton.outsideMaskMaterials == null)
EditorInitSpriteMaskMaterialsOutsideMask(skeleton);
}
}
private static void EditorInitSpriteMaskMaterialsInsideMask (SkeletonRenderer skeleton) {
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
EditorInitSpriteMaskMaterialsForMaskType(skeleton, SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE,
ref maskMaterials.materialsInsideMask);
EditorInitSpriteMaskMaterialsMaskMode(ref skeleton.insideMaskMaterials,
skeleton,
SkeletonRenderer.MATERIAL_OVERRIDE_SET_INSIDE_MASK_NAME,
SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE);
}
private static void EditorInitSpriteMaskMaterialsOutsideMask (SkeletonRenderer skeleton) {
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
EditorInitSpriteMaskMaterialsForMaskType(skeleton, SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE,
ref maskMaterials.materialsOutsideMask);
EditorInitSpriteMaskMaterialsMaskMode(ref skeleton.outsideMaskMaterials,
skeleton,
SkeletonRenderer.MATERIAL_OVERRIDE_SET_OUTSIDE_MASK_NAME,
SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE);
}
private static void EditorInitSpriteMaskMaterialsForMaskType (SkeletonRenderer skeleton, UnityEngine.Rendering.CompareFunction maskFunction,
ref Material[] materialsToFill) {
if (!EditorConfirmDisabledMaskMaterialsInit(skeleton))
return;
private static void EditorInitSpriteMaskMaterialsMaskMode (ref MaterialOverrideSet[] maskMaterials,
SkeletonRenderer skeleton,
string overrideSetName, UnityEngine.Rendering.CompareFunction maskFunction) {
AtlasAssetBase[] atlasAssets = skeleton.skeletonDataAsset.atlasAssets;
int atlasAssetCount = atlasAssets.Length;
if (maskMaterials == null || maskMaterials.Length != atlasAssetCount)
maskMaterials = new MaterialOverrideSet[atlasAssetCount];
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
Material[] originalMaterials = maskMaterials.materialsMaskDisabled;
materialsToFill = new Material[originalMaterials.Length];
for (int i = 0; i < originalMaterials.Length; i++) {
Material newMaterial = null;
if (!Application.isPlaying) {
newMaterial = EditorCreateOrLoadMaskMaterialAsset(maskMaterials, maskFunction, originalMaterials[i]);
for (int i = 0, n = atlasAssetCount; i < n; ++i) {
AtlasAssetBase atlasAsset = atlasAssets[i];
maskMaterials[i] = atlasAsset.GetMaterialOverrideSet(overrideSetName);
if (maskMaterials[i] == null && !Application.isPlaying) {
maskMaterials[i] = EditorInitSpriteMaskOverrideSet(
atlasAsset, overrideSetName, maskFunction);
}
if (newMaterial == null) {
newMaterial = new Material(originalMaterials[i]);
newMaterial.SetFloat(SkeletonRenderer.STENCIL_COMP_PARAM_ID, (int)maskFunction);
}
materialsToFill[i] = newMaterial;
}
skeleton.UpdateMaterials();
}
private static bool EditorConfirmDisabledMaskMaterialsInit (SkeletonRenderer skeleton) {
SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials = skeleton.maskMaterials;
if (maskMaterials.materialsMaskDisabled.Length > 0 && maskMaterials.materialsMaskDisabled[0] != null) {
return true;
}
private static MaterialOverrideSet EditorInitSpriteMaskOverrideSet(
AtlasAssetBase atlasAsset, string overrideSetName, UnityEngine.Rendering.CompareFunction maskFunction) {
MeshRenderer meshRenderer = skeleton.GetComponent<MeshRenderer>();
Material[] currentMaterials = meshRenderer.sharedMaterials;
if (currentMaterials.Length == 0 || currentMaterials[0] == null) {
// Note: if no attachments are visible, no materials are set. This is a valid state.
return false;
}
// We have to be sure that there has not been a recompilation or similar events that led to
// inside- or outside-mask materials being assigned to meshRenderer.sharedMaterials.
string firstMaterialPath = UnityEditor.AssetDatabase.GetAssetPath(currentMaterials[0]);
if (firstMaterialPath.Contains(MATERIAL_FILENAME_SUFFIX_INSIDE_MASK) ||
firstMaterialPath.Contains(MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK)) {
maskMaterials.materialsMaskDisabled = new Material[currentMaterials.Length];
for (int i = 0; i < currentMaterials.Length; ++i) {
string path = UnityEditor.AssetDatabase.GetAssetPath(currentMaterials[i]);
string correctPath = null;
if (path.Contains(MATERIAL_FILENAME_SUFFIX_INSIDE_MASK)) {
correctPath = path.Replace(MATERIAL_FILENAME_SUFFIX_INSIDE_MASK, "");
} else if (path.Contains(MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK)) {
correctPath = path.Replace(MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK, "");
}
if (correctPath != null) {
Material material = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(correctPath);
if (material == null)
Debug.LogWarning("No original ignore-mask material found for path " + correctPath);
maskMaterials.materialsMaskDisabled[i] = material;
}
MaterialOverrideSet overrideSet = atlasAsset.AddMaterialOverrideSet(overrideSetName);
foreach (Material originalMaterial in atlasAsset.Materials) {
Material maskMaterial = EditorCreateOrLoadMaskMaterialAsset(maskFunction, originalMaterial);
if (maskMaterial == null) {
maskMaterial = new Material(originalMaterial);
maskMaterial.name += overrideSetName;
maskMaterial.SetFloat(SkeletonRenderer.STENCIL_COMP_PARAM_ID, (int)maskFunction);
}
} else {
maskMaterials.materialsMaskDisabled = currentMaterials;
overrideSet.AddOverride(originalMaterial, maskMaterial);
}
return true;
UnityEditor.EditorUtility.SetDirty(atlasAsset);
UnityEditor.AssetDatabase.SaveAssets();
return overrideSet;
}
public static Material EditorCreateOrLoadMaskMaterialAsset (SkeletonRenderer.SpriteMaskInteractionMaterials maskMaterials,
UnityEngine.Rendering.CompareFunction maskFunction, Material originalMaterial) {
public static Material EditorCreateOrLoadMaskMaterialAsset (UnityEngine.Rendering.CompareFunction maskFunction,
Material originalMaterial) {
string originalMaterialPath = UnityEditor.AssetDatabase.GetAssetPath(originalMaterial);
int posOfExtensionDot = originalMaterialPath.LastIndexOf('.');
string materialPath = (maskFunction == SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE) ?
originalMaterialPath.Insert(posOfExtensionDot, MATERIAL_FILENAME_SUFFIX_INSIDE_MASK) :
originalMaterialPath.Insert(posOfExtensionDot, MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK);
string materialPath = MaskMaterialPath(originalMaterialPath, maskFunction);
Material material = UnityEditor.AssetDatabase.LoadAssetAtPath<Material>(materialPath);
if (material != null) {
return material;
@ -231,6 +184,23 @@ namespace Spine.Unity.Editor {
UnityEditor.AssetDatabase.SaveAssets();
return material;
}
public static string InsideMaskMaterialPath (string originalMaterialPath) {
return MaskMaterialPath(originalMaterialPath, SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE);
}
public static string OutsideMaskMaterialPath (string originalMaterialPath) {
return MaskMaterialPath(originalMaterialPath, SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_OUTSIDE);
}
public static string MaskMaterialPath (string originalMaterialPath,
UnityEngine.Rendering.CompareFunction maskFunction) {
int posOfExtensionDot = originalMaterialPath.LastIndexOf('.');
return (maskFunction == SkeletonRenderer.STENCIL_COMP_MASKINTERACTION_VISIBLE_INSIDE) ?
originalMaterialPath.Insert(posOfExtensionDot, MATERIAL_FILENAME_SUFFIX_INSIDE_MASK) :
originalMaterialPath.Insert(posOfExtensionDot, MATERIAL_FILENAME_SUFFIX_OUTSIDE_MASK);
}
}
}
#endif // BUILT_IN_SPRITE_MASK_COMPONENT

View File

@ -0,0 +1,184 @@
/******************************************************************************
* 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.
*****************************************************************************/
#if UNITY_2019_1_OR_NEWER
#define HAS_MODAL_UTILITY
#endif
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Icons = SpineEditorUtilities.Icons;
public class ComponentUpgradeWarningDialog : EditorWindow {
public enum DialogResult {
None = -1,
OpenGuide = 0,
Continue = 1
}
const string UPGRADE_GUIDE_URL = "https://github.com/EsotericSoftware/spine-runtimes/tree/4.3-beta/spine-unity/Assets/Spine/Documentation/4.3-split-component-upgrade-guide.md";
private static DialogResult dialogResult = DialogResult.None;
private static ComponentUpgradeWarningDialog currentWindow;
public static DialogResult ShowDialog () {
if (currentWindow != null) {
currentWindow.Close();
}
dialogResult = DialogResult.None;
currentWindow = CreateInstance<ComponentUpgradeWarningDialog>();
string title = "Spine Unity 4.3 - Critical Upgrade Notice";
currentWindow.titleContent = new GUIContent(title);
Vector2 windowSize = new Vector2(500, 450);
currentWindow.minSize = windowSize;
currentWindow.maxSize = windowSize;
float x = (Screen.currentResolution.width - windowSize.x) / 2;
float y = (Screen.currentResolution.height - windowSize.y) * 0.25f;
currentWindow.position = new Rect(x, y, windowSize.x, windowSize.y);
#if HAS_MODAL_UTILITY
currentWindow.ShowModalUtility();
#else
currentWindow.ShowUtility();
#endif
return dialogResult;
}
void OnGUI () {
GUIStyle dialogStyle = new GUIStyle("window");
dialogStyle.padding = new RectOffset(0, 0, 0, 0);
GUILayout.BeginArea(new Rect(15, 15, position.width - 30, position.height - 30));
GUIStyle messageStyle = new GUIStyle(EditorStyles.label) {
richText = true,
wordWrap = true,
fontSize = EditorStyles.label.fontSize,
alignment = TextAnchor.UpperLeft
};
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) {
richText = true,
fontSize = EditorStyles.boldLabel.fontSize,
alignment = TextAnchor.UpperLeft
};
GUILayout.BeginHorizontal();
var warningIcon = Icons.warning;
if (warningIcon != null) {
GUI.DrawTexture(new Rect(0, 0, 60, 60), warningIcon);
GUILayout.Space(70);
}
GUILayout.BeginVertical();
GUILayout.Label("<b>New projects:</b> Ignore this message.", messageStyle);
GUILayout.Label("<b>Existing projects:</b> You MUST read the upgrade guide!", messageStyle);
GUILayout.Space(10);
GUILayout.Label("<b>Major Architecture Change</b>", headerStyle);
GUILayout.Label(
"Main skeleton components are now " +
"<b>split into separate rendering and animation components</b>.\n\n" +
"Components will be <b>automatically split</b> when scenes/prefabs are opened:\n" +
"• <b>SkeletonAnimation</b> → SkeletonAnimation + SkeletonRenderer\n" +
"• <b>SkeletonMecanim</b> → SkeletonMecanim + SkeletonRenderer\n" +
"• <b>SkeletonGraphic</b> → SkeletonAnimation + SkeletonGraphic",
messageStyle
);
GUILayout.Space(10);
GUILayout.Label("<b>Without proper preparation:</b>", headerStyle);
GUILayout.Label(
"• Component references in your scripts may be lost\n" +
"• Builds may have missing components",
messageStyle
);
GUILayout.Space(10);
GUILayout.Label("<b>Required Steps:</b>", headerStyle);
GUILayout.Label(
"1. <b>Backup your project NOW</b>\n" +
"2. <b>READ: 4.3-split-component-upgrade-guide.md</b>\n" +
"3. <b>Proceed according to the guide.</b>",
messageStyle
);
GUILayout.Space(15);
GUILayout.Label(
"<color=red><b>Do not proceed without reading the guide or you risk breaking your project.</b></color>",
messageStyle
);
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Open Upgrade Guide", GUILayout.Width(140), GUILayout.Height(28))) {
dialogResult = DialogResult.OpenGuide;
Application.OpenURL(UPGRADE_GUIDE_URL);
currentWindow = null;
Close();
}
GUILayout.Space(10);
if (GUILayout.Button("I understand the risks - Continue", GUILayout.Width(200), GUILayout.Height(28))) {
dialogResult = DialogResult.Continue;
currentWindow = null;
SpineEditorUtilities.Preferences.ShowSplitComponentChangeWarning = false;
Close();
}
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
void OnDestroy () {
if (currentWindow == this) {
currentWindow = null;
if (dialogResult == DialogResult.None)
dialogResult = DialogResult.Continue;
}
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b89fbd6813869e04f8ec689143d0bb13
timeCreated: 1759516908
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -47,6 +47,10 @@
#define HAS_ANY_UNSAFE_OPTIONS
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Threading;
using UnityEditor;
using UnityEngine;
@ -144,6 +148,18 @@ namespace Spine.Unity.Editor {
set { workflowMismatchDialog = value; }
}
public bool ShowSplitComponentChangeWarning {
get { return splitComponentChangeWarning; }
set {
if (splitComponentChangeWarning == value) return;
SerializedObject serializedSettings = new SerializedObject(this);
SerializedProperty splitComponentChangeProperty = serializedSettings.FindProperty("splitComponentChangeWarning");
splitComponentChangeProperty.boolValue = value;
serializedSettings.ApplyModifiedProperties();
}
}
internal const bool DEFAULT_APPLY_ADDITIVE_MATERIAL = false;
public bool applyAdditiveMaterial = DEFAULT_APPLY_ADDITIVE_MATERIAL;
@ -219,6 +235,9 @@ namespace Spine.Unity.Editor {
internal const bool DEFAULT_WORKFLOW_MISMATCH_DIALOG = true;
public bool workflowMismatchDialog = DEFAULT_WORKFLOW_MISMATCH_DIALOG;
internal const bool DEFAULT_SPLIT_COMPONENT_CHANGE_WARNING = true;
public bool splitComponentChangeWarning = DEFAULT_SPLIT_COMPONENT_CHANGE_WARNING;
public const float DEFAULT_MIPMAPBIAS = -0.5f;
public const bool DEFAULT_AUTO_RELOAD_SCENESKELETONS = true;
@ -425,12 +444,41 @@ namespace Spine.Unity.Editor {
EditorGUILayout.LabelField("Unsafe Build Defines", EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Direct data access", "Allow unsafe direct data access. Currently affects reading .skel.bytes files, reading with fewer allocations."));
if (GUILayout.Button("Enable", GUILayout.Width(64)))
SpineBuildEnvUtility.EnableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
if (GUILayout.Button("Disable", GUILayout.Width(64)))
SpineBuildEnvUtility.DisableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
if (GUILayout.Button("Enable", GUILayout.Width(64)))
SpineBuildEnvUtility.EnableBuildDefine(SpineBuildEnvUtility.SPINE_ALLOW_UNSAFE_CODE);
}
#endif
GUILayout.Space(20);
EditorGUILayout.LabelField("Automatic Component Upgrade", EditorStyles.boldLabel);
#if SPINE_AUTO_UPGRADE_COMPONENTS_OFF
bool upgradeComponentsEnabled = false;
#else
bool upgradeComponentsEnabled = true;
#endif
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Split Component Upgrade", "Allow automatic upgrade of skeleton components to new split components."));
SpineEditorUtilities.EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_AUTO_UPGRADE_COMPONENTS_OFF, upgradeComponentsEnabled, invert: true);
}
using (new EditorGUI.DisabledScope(!upgradeComponentsEnabled)) {
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel(new GUIContent("Upgrade Scenes & Prefabs", "Upgrades all scenes and " +
"prefabs in the project to Spine 4.3 split animation components."));
if (GUILayout.Button("Upgrade All", GUILayout.Width(132))) {
if (EditorUtility.DisplayDialog("Upgrade All",
"This will open and process all scenes and prefabs in your project to upgrade Spine components to version 4.3.\n\n" +
"This process may take a while for large projects.\n\n" +
"Make sure to backup your project before proceeding.\n\n" +
"Continue?", "Yes, Upgrade", "Cancel")) {
#if AUTO_UPGRADE_TO_43_COMPONENTS
SpineEditorUtilities.UpgradeAllScenesAndPrefabsTo43();
#endif
}
}
}
}
#if SPINE_TK2D_DEFINE
bool isTK2DDefineSet = true;
@ -443,10 +491,10 @@ namespace Spine.Unity.Editor {
EditorGUILayout.LabelField("3rd Party Settings", EditorStyles.boldLabel);
using (new GUILayout.HorizontalScope()) {
EditorGUILayout.PrefixLabel("Define TK2D");
if (isTK2DAllowed && GUILayout.Button("Enable", GUILayout.Width(64)))
SpineEditorUtilities.SpineTK2DEditorUtility.EnableTK2D();
if (GUILayout.Button("Disable", GUILayout.Width(64)))
SpineEditorUtilities.SpineTK2DEditorUtility.DisableTK2D();
if (isTK2DAllowed && GUILayout.Button("Enable", GUILayout.Width(64)))
SpineEditorUtilities.SpineTK2DEditorUtility.EnableTK2D();
}
#if !SPINE_TK2D_DEFINE
if (!isTK2DAllowed) {
@ -456,6 +504,20 @@ namespace Spine.Unity.Editor {
#endif
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Threading Defaults", EditorStyles.boldLabel);
{
bool useThreadedMeshGeneration = RuntimeSettings.UseThreadedMeshGeneration;
bool useThreadedAnimation = RuntimeSettings.UseThreadedAnimation;
SpineEditorUtilities.BoolRuntimePropertiesField(
() => RuntimeSettings.UseThreadedMeshGeneration,
value => RuntimeSettings.UseThreadedMeshGeneration = value,
new GUIContent("Threaded MeshGeneration", "Default value for SkeletonRenderer and SkeletonGraphic Threaded Mesh Generation."));
SpineEditorUtilities.BoolRuntimePropertiesField(
() => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value,
new GUIContent("Threaded Animation", "Default value for SkeletonAnimation and SkeletonMecanim Threaded Animation."));
}
GUILayout.Space(20);
EditorGUILayout.LabelField("Timeline Extension", EditorStyles.boldLabel);
{

View File

@ -48,8 +48,8 @@ namespace Spine.Unity.Examples {
static bool partsRenderersExpanded = false;
// For separator field.
SerializedObject skeletonRendererSerializedObject;
SerializedProperty separatorNamesProp;
SerializedObject skeletonRendererSerialized;
SerializedProperty separatorSlotNames, enableSeparatorSlots;
static bool skeletonRendererExpanded = true;
bool slotsReapplyRequired = false;
bool partsRendererInitRequired = false;
@ -75,7 +75,7 @@ namespace Spine.Unity.Examples {
if (Application.isPlaying)
return component.SkeletonRenderer.separatorSlots.Count;
else
return separatorNamesProp == null ? 0 : separatorNamesProp.arraySize;
return separatorSlotNames == null ? 0 : separatorSlotNames.arraySize;
}
}
@ -109,7 +109,7 @@ namespace Spine.Unity.Examples {
Undo.RecordObject(target, "Enable SkeletonRenderSeparator");
EditorUtility.SetObjectEnabled(target, checkBox);
}
if (component.SkeletonRenderer.disableRenderingOnOverride && !component.enabled)
if (component.SkeletonRenderer && component.SkeletonRenderer.disableRenderingOnOverride && !component.enabled)
EditorGUILayout.HelpBox("By default, SkeletonRenderer's MeshRenderer is disabled while the SkeletonRenderSeparator takes over rendering. It is re-enabled when SkeletonRenderSeparator is disabled.", MessageType.Info);
EditorGUILayout.PropertyField(copyPropertyBlock_);
@ -137,19 +137,22 @@ namespace Spine.Unity.Examples {
if (component.SkeletonRenderer != null) {
// Separators from SkeletonRenderer
{
bool skeletonRendererMismatch = skeletonRendererSerializedObject != null && skeletonRendererSerializedObject.targetObject != component.SkeletonRenderer;
if (separatorNamesProp == null || skeletonRendererMismatch) {
bool skeletonRendererMismatch = skeletonRendererSerialized != null && skeletonRendererSerialized.targetObject != component.SkeletonRenderer;
if (separatorSlotNames == null || skeletonRendererMismatch) {
if (component.SkeletonRenderer != null) {
skeletonRendererSerializedObject = new SerializedObject(component.SkeletonRenderer);
separatorNamesProp = skeletonRendererSerializedObject.FindProperty("separatorSlotNames");
separatorNamesProp.isExpanded = true;
skeletonRendererSerialized = new SerializedObject(component.SkeletonRenderer);
separatorSlotNames = skeletonRendererSerialized.FindProperty("separatorSlotNames");
enableSeparatorSlots = skeletonRendererSerialized.FindProperty("enableSeparatorSlots");
separatorSlotNames.isExpanded = true;
}
}
if (separatorNamesProp != null) {
if (separatorSlotNames != null) {
if (skeletonRendererExpanded) {
EditorGUI.indentLevel++;
SkeletonRendererInspector.SeparatorsField(separatorNamesProp);
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
SkeletonRendererInspector.SeparatorSlotProperties(separatorSlotNames, enableSeparatorSlots);
}
EditorGUI.indentLevel--;
}
separatorCount = this.SkeletonRendererSeparatorCount;
@ -162,7 +165,7 @@ namespace Spine.Unity.Examples {
}
if (EditorGUI.EndChangeCheck()) {
skeletonRendererSerializedObject.ApplyModifiedProperties();
skeletonRendererSerialized.ApplyModifiedProperties();
if (!Application.isPlaying)
slotsReapplyRequired = true;
@ -186,7 +189,7 @@ namespace Spine.Unity.Examples {
EditorGUILayout.HelpBox("Some items in the parts renderers list are null and may cause problems.\n\nYou can right-click on that element and choose 'Delete Array Element' to remove it.", MessageType.Warning);
// (Button) Match Separators count
if (separatorNamesProp != null) {
if (separatorSlotNames != null) {
int currentRenderers = 0;
foreach (SkeletonPartsRenderer r in componentRenderers) {
if (r != null)
@ -244,7 +247,7 @@ namespace Spine.Unity.Examples {
if (slotsReapplyRequired && UnityEngine.Event.current.type == EventType.Repaint) {
component.SkeletonRenderer.ReapplySeparatorSlotNames();
component.SkeletonRenderer.LateUpdateMesh();
component.SkeletonRenderer.UpdateMesh();
SceneView.RepaintAll();
slotsReapplyRequired = false;
}

View File

@ -29,6 +29,7 @@
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System;
using System.Collections.Generic;
using UnityEngine;
@ -42,6 +43,59 @@ namespace Spine.Unity {
public abstract void Clear ();
public abstract Atlas GetAtlas (bool onlyMetaData = false);
protected void OnEnable () {
runtimeMaterialOverrides = null;
InitializeRuntimeOverrides();
}
public bool HasMaterialOverrideSets {
get {
#if UNITY_EDITOR
if (!Application.isPlaying) {
return serializedMaterialOverrides.Count > 0;
}
#endif
return serializedMaterialOverrides.Count > 0 ||
(runtimeMaterialOverrides != null && runtimeMaterialOverrides.Count > 0);
}
}
public MaterialOverrideSet GetMaterialOverrideSet (string name) {
#if UNITY_EDITOR
if (!Application.isPlaying) {
return serializedMaterialOverrides.Find(entry => (entry.name == name));
}
#endif
InitializeRuntimeOverrides();
return runtimeMaterialOverrides.Find(entry => (entry.name == name));
}
public MaterialOverrideSet AddMaterialOverrideSet (string name) {
var overrideSet = new MaterialOverrideSet(name);
#if UNITY_EDITOR
if (!Application.isPlaying) {
serializedMaterialOverrides.Add(overrideSet);
return overrideSet;
}
#endif
InitializeRuntimeOverrides();
runtimeMaterialOverrides.Add(overrideSet);
return overrideSet;
}
protected void InitializeRuntimeOverrides () {
if (runtimeMaterialOverrides == null) {
runtimeMaterialOverrides = new List<MaterialOverrideSet>(serializedMaterialOverrides.Count);
foreach (var srcOverrideSet in serializedMaterialOverrides) {
runtimeMaterialOverrides.Add(new MaterialOverrideSet(srcOverrideSet));
}
}
}
[SerializeField] protected List<MaterialOverrideSet> serializedMaterialOverrides = new List<MaterialOverrideSet>();
protected List<MaterialOverrideSet> runtimeMaterialOverrides;
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
public enum LoadingMode {
Normal = 0,

View File

@ -30,7 +30,6 @@
using Spine;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace Spine.Unity {

View File

@ -0,0 +1,101 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated September 24, 2021. Replaces all prior versions.
*
* Copyright (c) 2013-2023, 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.
*****************************************************************************/
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// A set of Material overrides to replace one material with another (e.g. an inside-mask variant).
/// Used by <see cref="AtlasAssetBase"/> to hold all mask material overrides in a common location
/// to avoid duplicate identical materials, and thus unnecessary draw calls.
/// </summary>
[System.Serializable]
public class MaterialOverrideSet {
public string name;
[SerializeField] protected List<Material> dictionaryKeys = new List<Material>();
[SerializeField] protected List<Material> dictionaryValues = new List<Material>();
public MaterialOverrideSet (string name) {
this.name = name;
}
public MaterialOverrideSet (MaterialOverrideSet src) {
this.name = src.name;
this.dictionaryKeys = new List<Material>(src.dictionaryKeys);
this.dictionaryValues = new List<Material>(src.dictionaryValues);
}
/// <summary>Adds an override from <c>originalMaterial</c> to an <c>overrideMaterial</c>.
/// The caller is responsible to ensure that an entry mapping from <c>originalMaterial</c>
/// is not already present.</summary>
public void AddOverride (Material originalMaterial, Material overrideMaterial) {
dictionaryKeys.Add(originalMaterial);
dictionaryValues.Add(overrideMaterial);
}
/// <summary>Removes a previously added override from <c>originalMaterial</c>.</summary>
public void RemoveOverride (Material originalMaterial) {
int existingIndex = dictionaryKeys.IndexOf(originalMaterial);
if (existingIndex >= 0) {
dictionaryKeys.RemoveAt(existingIndex);
dictionaryValues.RemoveAt(existingIndex);
}
}
/// <summary>Sets an existing override from <c>originalMaterial</c> to an <c>overrideMaterial</c>
/// if an entry for <c>originalMaterial</c> already exists. Otherwise it adds the respective entry.
/// </summary>
public void SetOverride (Material originalMaterial, Material overrideMaterial) {
int existingIndex = dictionaryKeys.IndexOf(originalMaterial);
if (existingIndex < 0) {
dictionaryKeys.Add(originalMaterial);
dictionaryValues.Add(overrideMaterial);
} else {
dictionaryValues[existingIndex] = overrideMaterial;
}
}
/// <summary>Applies previously set overrides in-place to the given <c>materials</c> array,
/// replacing the respective reference to an <c>originalMaterial</c> with the corresponding
/// <c>overrideMaterial</c>.</summary>
public void ApplyOverrideTo (Material[] materials) {
if (dictionaryKeys.Count == 0) return;
for (int i = 0, count = materials.Length; i < count; ++i) {
int dictIndex = dictionaryKeys.IndexOf(materials[i]);
if (dictIndex >= 0)
materials[i] = dictionaryValues[dictIndex];
}
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: c19aeacc73140c14796901a28c27cdf9
timeCreated: 1686127376
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -27,6 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
using Spine;
using System;
using System.Collections.Generic;
using System.IO;

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 4c4e5bff8901a8a4fa1d2ab7f5d2bda6
folderAsset: yes
timeCreated: 1686835686
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,480 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated September 24, 2021. Replaces all prior versions.
*
* Copyright (c) 2013-2021, 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.
*****************************************************************************/
#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#endif
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#define USE_THREADED_ANIMATION_UPDATE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[System.Serializable]
public abstract class SkeletonAnimationBase : MonoBehaviour,
ISkeletonAnimation, ISkeletonComponent, IHasSkeletonRenderer,
ISkeletonRendererEvents, IHasModifyableSkeletonDataAsset, IUpgradable {
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
protected int frameOfLastUpdate = -1;
protected ISkeletonRenderer skeletonRenderer;
protected bool skipUpdate = false;
public UpdateTiming UpdateTiming {
get { return updateTiming; }
set {
if (updateTiming == value) return;
#if USE_THREADED_ANIMATION_UPDATE
if (Application.isPlaying && UsesThreadedAnimation && isUpdatedExternally) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
// unregister from old update timing mode, register for new one.
system.UnregisterFromUpdate(updateTiming, this);
system.RegisterForUpdate(value, this);
}
}
#endif
updateTiming = value;
}
}
#if UNITY_EDITOR
protected bool requiresEditorUpdate = false;
#endif
#if USE_THREADED_ANIMATION_UPDATE
#region Threaded update system
[SerializeField] protected SettingsTriState threadedAnimation = SettingsTriState.UseGlobalSetting;
public bool isUpdatedExternally = false;
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedAnimation {
get { return threadedAnimation == SettingsTriState.Enable || RuntimeSettings.UseThreadedAnimation; }
}
public SettingsTriState ThreadedAnimation {
get { return threadedAnimation; }
set {
if (threadedAnimation == value) return;
threadedAnimation = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedAnimation == SettingsTriState.Enable || RuntimeSettings.UseThreadedAnimation)
system.RegisterForUpdate(updateTiming, this);
else
system.UnregisterFromUpdate(updateTiming, this);
}
}
}
#endregion
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else
protected bool UseThreading {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
#endif
#region Interface Implementation
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonRenderer Renderer {
get {
if (skeletonRenderer == null) {
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
if (skeletonRenderer == null) {
UpgradeTo43();
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
}
#endif
}
return skeletonRenderer;
}
}
public Skeleton Skeleton {
get { return Renderer.Skeleton; }
set { Renderer.Skeleton = value; }
}
public Skeleton skeleton {
get { return Renderer.Skeleton; }
set { Renderer.Skeleton = value; }
}
public SkeletonDataAsset SkeletonDataAsset {
get { return Renderer.SkeletonDataAsset; }
set { Renderer.SkeletonDataAsset = value; }
}
public SkeletonDataAsset skeletonDataAsset {
get { return Renderer.SkeletonDataAsset; }
set { Renderer.SkeletonDataAsset = value; }
}
protected event SkeletonAnimationDelegate _BeforeUpdate, _BeforeApply, _OnAnimationRebuild;
/// <summary>OnAnimationRebuild is raised after the SkeletonAnimation component is successfully initialized.</summary>
public event SkeletonAnimationDelegate OnAnimationRebuild { add { _OnAnimationRebuild += value; } remove { _OnAnimationRebuild -= value; } }
/// <summary>
/// Occurs before the animation state is updated.
/// Use this callback when you want to change the skeleton state before animation state is updated.
/// </summary>
public event SkeletonAnimationDelegate BeforeUpdate { add { _BeforeUpdate += value; } remove { _BeforeUpdate -= value; } }
/// <summary>
/// Occurs before the animations are applied.
/// Use this callback when you want to change the skeleton state before animations are applied on top.
/// </summary>
public event SkeletonAnimationDelegate BeforeApply { add { _BeforeApply += value; } remove { _BeforeApply -= value; } }
/// <summary>A compatibility wrapper for <see cref="SkeletonRenderer.UpdateLocal"/></summary>
public event SkeletonRendererDelegate UpdateLocal { add { Renderer.UpdateLocal += value; } remove { Renderer.UpdateLocal -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.UpdateWorld"/></summary>
public event SkeletonRendererDelegate UpdateWorld { add { Renderer.UpdateWorld += value; } remove { Renderer.UpdateWorld -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.UpdateComplete"/></summary>
public event SkeletonRendererDelegate UpdateComplete { add { Renderer.UpdateComplete += value; } remove { Renderer.UpdateComplete -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.OnRebuild"/></summary>
public event SkeletonRendererDelegate OnRebuild { add { Renderer.OnRebuild += value; } remove { Renderer.OnRebuild -= value; } }
/// <summary>A compatibility wrapper for <see cref="Renderer.OnMeshAndMaterialsUpdated"/></summary>
public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated { add { Renderer.OnMeshAndMaterialsUpdated += value; } remove { Renderer.OnMeshAndMaterialsUpdated -= value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return Renderer.UpdateMode; } set { Renderer.UpdateMode = value; } }
public abstract bool IsValid { get; }
#endregion
public virtual void Awake () {
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
UpgradeTo43();
#endif
#if UNITY_EDITOR
SkeletonAnimationBase.ApplicationIsPlaying = Application.isPlaying;
#endif
EnsureRendererEventsSubscribed();
}
public void EnsureRendererEventsSubscribed () {
Renderer.OnRebuild -= OnRendererRebuild;
Renderer.OnRebuild += OnRendererRebuild;
}
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public abstract void UpgradeTo43 ();
#endif
/// <summary>
/// Initialize the associated renderer component and subsequently this animation component.
/// Creates the internal Spine objects and buffers.</summary>
/// <param name="overwrite">If set to <c>true</c>, force overwrite an already initialized object.</param>
public virtual void Initialize (bool overwrite, bool quiet = false, bool calledFromRendererCallback = false) {
#if UNITY_EDITOR
if (BuildUtilities.IsInSkeletonAssetBuildPreProcessing)
return;
#endif
if (!calledFromRendererCallback) {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
if (!skeletonRenderer.IsValid || !this.IsValid || overwrite)
skeletonRenderer.Initialize(overwrite, quiet);
}
if (this.IsValid && !overwrite)
return;
InitializeAnimationComponent();
if (_OnAnimationRebuild != null)
_OnAnimationRebuild(this);
}
/// <summary>
/// Manually initializes just this animation component without initializing the associated renderer component.
/// The renderer component has to be initialized before calling this method, which happens automatically via
/// renderer component Awake, or when needed earlier by calling <see cref="Initialize(bool, bool, bool)"/> on
/// this animation component which initializes both components in the correct order.
/// </summary>
public virtual void InitializeAnimationComponent () {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<ISkeletonRenderer>();
#if UNITY_EDITOR
if (!ApplicationIsPlaying && skeletonRenderer != null && skeletonRenderer.Skeleton != null)
skeletonRenderer.Skeleton.SetupPose();
requiresEditorUpdate = false;
#endif
}
/// <summary>
/// Clears the previously generated mesh, resets the skeleton's pose, and clears all previously active animations.</summary>
public void ClearState () {
skeletonRenderer.ClearSkeletonState();
ClearAnimationState();
}
public virtual void ClearAnimationState () { }
public void OnEnable () {
if (skeletonRenderer == null)
skeletonRenderer = this.GetComponent<SkeletonRenderer>();
#if USE_THREADED_ANIMATION_UPDATE
if (Application.isPlaying && UsesThreadedAnimation && !isUpdatedExternally) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.RegisterForUpdate(updateTiming, this);
}
#endif
}
#if USE_THREADED_ANIMATION_UPDATE
public void OnDisable () {
if (Application.isPlaying && UsesThreadedAnimation) {
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system)
system.UnregisterFromUpdate(updateTiming, this);
}
}
#endif
protected void OnRendererRebuild (ISkeletonRenderer skeletonRenderer) {
Initialize(overwrite: true, quiet: false, calledFromRendererCallback: true); //InitializeAnimationComponent();
}
#if UNITY_EDITOR
protected void OnValidate () {
if (!Application.isPlaying) {
Renderer.OnRebuild -= OnRendererRebuild;
Renderer.OnRebuild += OnRendererRebuild;
requiresEditorUpdate = true;
} else if (Time.frameCount != 0) {
// OnValidate is called once when starting play mode in the Editor, don't trigger re-init then.
requiresEditorUpdate = true;
}
}
#endif
protected virtual void Update () {
#if UNITY_EDITOR
if (requiresEditorUpdate)
InitializeAnimationComponent();
if (!ApplicationIsPlaying) {
if (!IsValid)
Initialize(false);
Update(0f);
return;
}
#endif
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
protected virtual void FixedUpdate () {
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InFixedUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
protected virtual void LateUpdate () {
#if USE_THREADED_ANIMATION_UPDATE
if (isUpdatedExternally) return;
#endif
if (updateTiming != UpdateTiming.InLateUpdate) return;
UpdateOncePerFrame(DeltaTime);
}
/// <summary>Calls <see cref="Update()"/> if it has not yet been called this frame.</summary>
public virtual void UpdateOncePerFrame (float deltaTime) {
if (frameOfLastUpdate != Time.frameCount) {
MainThreadBeforeUpdateInternal();
UpdateInternal(deltaTime, Time.frameCount, calledFromOnlyMainThread: true);
}
}
/// <summary>
/// Main thread update part preparing properties only accessible from main thread.
/// To be followed by a potentially threaded call to <see cref="UpdateInternal"/>.
/// </summary>
public virtual void MainThreadBeforeUpdateInternal () {
if (skeletonRenderer == null || !skeletonRenderer.IsValid || skeletonRenderer.Freeze || !this.IsValid
|| skeletonRenderer.UpdateMode < UpdateMode.OnlyAnimationStatus) {
skipUpdate = true;
return;
}
skipUpdate = false;
skeletonRenderer.GatherTransformMovementForPhysics();
if (_BeforeUpdate != null)
_BeforeUpdate(this);
}
public virtual void MainThreadAfterUpdateInternal () { }
public virtual void UpdateInternal (float deltaTime, int currentFrameCount, bool calledFromOnlyMainThread = true) {
if (skipUpdate)
return;
frameOfLastUpdate = currentFrameCount;
UpdateAnimationStatus(deltaTime);
skeletonRenderer.ApplyTransformMovementToPhysics();
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus)
return;
ApplyAnimation(calledFromOnlyMainThread);
}
/// <summary>Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton.
/// Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time.</summary>
public virtual CoroutineIterator UpdateInternalSplit (CoroutineIterator coroutineIterator, float deltaTime,
int currentFrameCount) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
const int StateBits = 1;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (skipUpdate)
return CoroutineIterator.Done;
frameOfLastUpdate = currentFrameCount;
UpdateAnimationStatus(deltaTime);
skeletonRenderer.ApplyTransformMovementToPhysics();
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus)
return CoroutineIterator.Done;
goto case 1;
case 1:
return ApplyAnimationSplit(coroutineIterator.ToNestedCall(StateBits))
.FromNestedCall(1, StateBits);
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonAnimationBase.UpdateInternalSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
/// <summary>Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton.
/// Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time.</summary>
public virtual void Update (float deltaTime) {
MainThreadBeforeUpdateInternal();
UpdateInternal(deltaTime, Time.frameCount, calledFromOnlyMainThread: true);
}
public virtual void ApplyAnimation (bool calledFromMainThread = true) {
if (_BeforeApply != null)
_BeforeApply(this);
ApplyStateToSkeleton(calledFromMainThread);
skeletonRenderer.AfterAnimationApplied(calledFromMainThread);
}
public virtual CoroutineIterator ApplyAnimationSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
const int StateBits = 1;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_BeforeApply != null)
_BeforeApply(this);
ApplyStateToSkeleton(calledFromMainThread: false);
goto case 1;
case 1:
return skeletonRenderer.AfterAnimationAppliedSplit(coroutineIterator.ToNestedCall(StateBits))
.FromNestedCall(1, StateBits);
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonAnimationBase.ApplyAnimationSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
public void OnBecameVisibleFromMode (UpdateMode previousUpdateMode) {
// OnBecameVisible is called after Update and LateUpdate(),
// so update if previousUpdateMode didn't already update this frame.
if (previousUpdateMode != UpdateMode.FullUpdate &&
previousUpdateMode != UpdateMode.EverythingExceptMesh)
Update(0);
}
protected virtual float DeltaTime { get { return Time.deltaTime; } }
protected abstract void UpdateAnimationStatus (float deltaTime);
protected abstract void ApplyStateToSkeleton (bool calledFromMainThread = true);
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 70671e8c557db624b9ba695f42aa543e
timeCreated: 1622830533
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -40,12 +40,12 @@ namespace Spine.Unity {
/// </summary>
[ExecuteInEditMode]
[AddComponentMenu("Spine/EditorSkeletonPlayer")]
[RequireComponent(typeof(ISkeletonAnimation))]
[RequireComponent(typeof(SkeletonAnimation))]
public class EditorSkeletonPlayer : MonoBehaviour {
public bool playWhenSelected = true;
public bool playWhenDeselected = true;
public float fixedTrackTime = 0.0f;
private IEditorSkeletonWrapper skeletonWrapper;
private SkeletonAnimation skeletonAnimation;
private TrackEntry trackEntry;
private string oldAnimationName;
private bool oldLoop;
@ -71,13 +71,8 @@ namespace Spine.Unity {
private void Start () {
if (Application.isPlaying) return;
if (skeletonWrapper == null) {
SkeletonAnimation skeletonAnimation;
SkeletonGraphic skeletonGraphic;
if (skeletonAnimation = this.GetComponent<SkeletonAnimation>())
skeletonWrapper = new SkeletonAnimationWrapper(skeletonAnimation);
else if (skeletonGraphic = this.GetComponent<SkeletonGraphic>())
skeletonWrapper = new SkeletonGraphicWrapper(skeletonGraphic);
if (skeletonAnimation == null) {
skeletonAnimation = this.GetComponent<SkeletonAnimation>();
}
oldTime = EditorApplication.timeSinceStartup;
@ -90,10 +85,11 @@ namespace Spine.Unity {
private void Update () {
if (enabled == false || Application.isPlaying) return;
if (skeletonWrapper == null) return;
if (skeletonWrapper.State == null || skeletonWrapper.State.Tracks.Count == 0) return;
if (skeletonAnimation == null) return;
AnimationState animationState = skeletonAnimation.AnimationState;
if (animationState == null || animationState.Tracks.Count == 0) return;
TrackEntry currentEntry = skeletonWrapper.State.Tracks.Items[0];
TrackEntry currentEntry = animationState.Tracks.Items[0];
if (currentEntry != null && fixedTrackTime != 0) {
currentEntry.TrackTime = fixedTrackTime;
}
@ -101,32 +97,36 @@ namespace Spine.Unity {
private void EditorUpdate () {
if (enabled == false || Application.isPlaying) return;
if (skeletonWrapper == null) return;
if (skeletonWrapper.State == null) return;
if (skeletonAnimation == null) return;
AnimationState animationState = skeletonAnimation.AnimationState;
if (animationState == null) return;
bool isSelected = Selection.Contains(this.gameObject);
if (!this.playWhenSelected && isSelected) return;
if (!this.playWhenDeselected && !isSelected) return;
if (fixedTrackTime != 0) return;
// Update animation
if (oldAnimationName != skeletonWrapper.AnimationName || oldLoop != skeletonWrapper.Loop) {
SkeletonData skeletonData = skeletonWrapper.SkeletonData;
Spine.Animation animation = (skeletonData == null || skeletonWrapper.AnimationName == null) ?
null : skeletonData.FindAnimation(skeletonWrapper.AnimationName);
string animationName = skeletonAnimation.AnimationName;
bool loop = skeletonAnimation.loop;
if (oldAnimationName != animationName || oldLoop != loop) {
SkeletonData skeletonData = skeletonAnimation.Skeleton.Data;
Spine.Animation animation = (skeletonData == null || animationName == null) ?
null : skeletonData.FindAnimation(animationName);
if (animation != null)
trackEntry = skeletonWrapper.State.SetAnimation(0, skeletonWrapper.AnimationName, skeletonWrapper.Loop);
trackEntry = animationState.SetAnimation(0, animationName, loop);
else
trackEntry = skeletonWrapper.State.SetEmptyAnimation(0, 0);
oldAnimationName = skeletonWrapper.AnimationName;
oldLoop = skeletonWrapper.Loop;
trackEntry = animationState.SetEmptyAnimation(0, 0);
oldAnimationName = animationName;
oldLoop = loop;
}
// Update speed
if (trackEntry != null)
trackEntry.TimeScale = skeletonWrapper.Speed;
trackEntry.TimeScale = skeletonAnimation.timeScale;
float deltaTime = (float)(EditorApplication.timeSinceStartup - oldTime);
skeletonWrapper.Update(deltaTime);
skeletonAnimation.Update(deltaTime);
skeletonAnimation.Renderer.UpdateMesh();
oldTime = EditorApplication.timeSinceStartup;
// Force repaint to update animation smoothly
@ -136,57 +136,6 @@ namespace Spine.Unity {
SceneView.RepaintAll();
#endif
}
private class SkeletonAnimationWrapper : IEditorSkeletonWrapper {
private SkeletonAnimation skeletonAnimation;
public SkeletonAnimationWrapper (SkeletonAnimation skeletonAnimation) {
this.skeletonAnimation = skeletonAnimation;
}
public Spine.SkeletonData SkeletonData {
get {
if (!skeletonAnimation.SkeletonDataAsset) return null;
return skeletonAnimation.SkeletonDataAsset.GetSkeletonData(true);
}
}
public string AnimationName { get { return skeletonAnimation.AnimationName; } }
public bool Loop { get { return skeletonAnimation.loop; } }
public float Speed { get { return skeletonAnimation.timeScale; } }
public Spine.AnimationState State { get { return skeletonAnimation.state; } }
public void Update (float deltaTime) {
skeletonAnimation.Update(deltaTime);
}
}
private class SkeletonGraphicWrapper : IEditorSkeletonWrapper {
private SkeletonGraphic skeletonGraphic;
public SkeletonGraphicWrapper (SkeletonGraphic skeletonGraphic) {
this.skeletonGraphic = skeletonGraphic;
}
public Spine.SkeletonData SkeletonData { get { return skeletonGraphic.SkeletonData; } }
public string AnimationName { get { return skeletonGraphic.startingAnimation; } }
public bool Loop { get { return skeletonGraphic.startingLoop; } }
public float Speed { get { return skeletonGraphic.timeScale; } }
public Spine.AnimationState State { get { return skeletonGraphic.AnimationState; } }
public void Update (float deltaTime) {
skeletonGraphic.Update(deltaTime);
}
}
private interface IEditorSkeletonWrapper {
string AnimationName { get; }
Spine.SkeletonData SkeletonData { get; }
bool Loop { get; }
float Speed { get; }
Spine.AnimationState State { get; }
void Update (float deltaTime);
}
}
}
#endif

View File

@ -31,8 +31,13 @@
#define NEW_PREFAB_SYSTEM
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -44,7 +49,7 @@ namespace Spine.Unity {
#endif
[AddComponentMenu("Spine/BoneFollower")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#BoneFollower")]
public class BoneFollower : MonoBehaviour {
public class BoneFollower : MonoBehaviour, IUpgradable {
#region Inspector
public SkeletonRenderer skeletonRenderer;
@ -107,10 +112,15 @@ namespace Spine.Unity {
}
public void Awake () {
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
#endif
if (initializeOnAwake) Initialize();
}
public void HandleRebuildRenderer (SkeletonRenderer skeletonRenderer) {
public void HandleRebuildRenderer (ISkeletonRenderer skeletonRenderer) {
Initialize();
}
@ -126,7 +136,6 @@ namespace Spine.Unity {
if (!string.IsNullOrEmpty(boneName))
bone = skeletonRenderer.skeleton.FindBone(boneName);
#if UNITY_EDITOR
if (Application.isEditor)
LateUpdate();
@ -248,5 +257,22 @@ namespace Spine.Unity {
if (boneIndex < 0) return 0f;
return skeletonRenderer.zSpacing * boneIndex;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at BoneFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -222,7 +222,7 @@ namespace Spine.Unity {
float GetAttachmentZPosition () {
int boneIndex = skeletonGraphic.Skeleton.DrawOrder.FindIndex(slot => slot.Bone == bone);
if (boneIndex < 0) return 0f;
return skeletonGraphic.MeshGenerator.settings.zSpacing * skeletonGraphic.MeshScale * boneIndex;
return skeletonGraphic.MeshSettings.zSpacing * skeletonGraphic.MeshScale * boneIndex;
}
}
}

View File

@ -31,8 +31,13 @@
#define NEW_PREFAB_SYSTEM
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -42,7 +47,7 @@ namespace Spine.Unity {
[ExecuteInEditMode]
#endif
[HelpURL("http://esotericsoftware.com/spine-unity-utility-components#BoundingBoxFollower")]
public class BoundingBoxFollower : MonoBehaviour {
public class BoundingBoxFollower : MonoBehaviour, IUpgradable {
internal static bool DebugMessages = true;
#region Inspector
@ -67,6 +72,14 @@ namespace Spine.Unity {
public PolygonCollider2D CurrentCollider { get { return currentCollider; } }
public bool IsTrigger { get { return isTrigger; } }
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Start () {
Initialize();
}
@ -80,7 +93,7 @@ namespace Spine.Unity {
Initialize();
}
void HandleRebuild (SkeletonRenderer sr) {
void HandleRebuild (ISkeletonRenderer sr) {
//if (BoundingBoxFollower.DebugMessages) Debug.Log("Skeleton was rebuilt. Repopulating BoundingBoxFollower.");
Initialize();
}
@ -248,6 +261,22 @@ namespace Spine.Unity {
}
}
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at BoundingBoxFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -80,7 +80,7 @@ namespace Spine.Unity {
Initialize();
}
void HandleRebuild (SkeletonGraphic sr) {
void HandleRebuild (ISkeletonRenderer sr) {
//if (BoundingBoxFollowerGraphic.DebugMessages) Debug.Log("Skeleton was rebuilt. Repopulating BoundingBoxFollowerGraphic.");
Initialize();
}

View File

@ -31,7 +31,12 @@
#define NEW_PREFAB_SYSTEM
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -42,10 +47,11 @@ namespace Spine.Unity {
#endif
[AddComponentMenu("Spine/Point Follower")]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#PointFollower")]
public class PointFollower : MonoBehaviour, IHasSkeletonRenderer, IHasSkeletonComponent {
public class PointFollower : MonoBehaviour, IHasSkeletonRenderer, IHasSkeletonComponent, IUpgradable {
public SkeletonRenderer skeletonRenderer;
public SkeletonRenderer SkeletonRenderer { get { return this.skeletonRenderer; } }
public ISkeletonRenderer SkeletonRenderer { get { return this.skeletonRenderer; } }
public ISkeletonRenderer Renderer { get { return this.skeletonRenderer; } }
public ISkeletonComponent SkeletonComponent { get { return skeletonRenderer as ISkeletonComponent; } }
[SpineSlot(dataField: "skeletonRenderer", includeNone: true)]
@ -65,6 +71,14 @@ namespace Spine.Unity {
bool valid;
public bool IsValid { get { return valid; } }
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
public void Initialize () {
valid = skeletonRenderer != null && skeletonRenderer.valid;
if (!valid)
@ -77,7 +91,7 @@ namespace Spine.Unity {
#endif
}
private void HandleRebuildRenderer (SkeletonRenderer skeletonRenderer) {
private void HandleRebuildRenderer (ISkeletonRenderer skeletonRenderer) {
Initialize();
}
@ -162,5 +176,22 @@ namespace Spine.Unity {
thisTransform.localScale = localScale;
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at PointFollower, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -90,7 +90,7 @@ namespace Spine.Unity {
public override void Initialize () {
base.Initialize();
IAnimationStateComponent animstateComponent = skeletonComponent as IAnimationStateComponent;
IAnimationStateComponent animstateComponent = animationComponent as IAnimationStateComponent;
this.animationState = (animstateComponent != null) ? animstateComponent.AnimationState : null;
skeletonGraphic = this.GetComponent<SkeletonGraphic>();

View File

@ -128,15 +128,14 @@ namespace Spine.Unity {
protected bool SkeletonAnimationUsesFixedUpdate {
get {
ISkeletonAnimation skeletonAnimation = skeletonComponent as ISkeletonAnimation;
if (skeletonAnimation != null) {
return skeletonAnimation.UpdateTiming == UpdateTiming.InFixedUpdate;
if (animationComponent != null) {
return animationComponent.UpdateTiming == UpdateTiming.InFixedUpdate;
}
return false;
}
}
protected ISkeletonComponent skeletonComponent;
protected SkeletonAnimationBase animationComponent;
protected Bone rootMotionBone;
protected int rootMotionBoneIndex;
protected List<int> transformConstraintIndices = new List<int>();
@ -168,7 +167,7 @@ namespace Spine.Unity {
}
public virtual void Initialize () {
skeletonComponent = GetComponent<ISkeletonComponent>();
animationComponent = GetComponent<SkeletonAnimationBase>();
GatherTopLevelBones();
SetRootMotionBone(rootMotionBoneName);
if (rootMotionBone != null) {
@ -176,13 +175,12 @@ namespace Spine.Unity {
initialOffsetRotation = rootMotionBone.Pose.Rotation;
}
ISkeletonAnimation skeletonAnimation = skeletonComponent as ISkeletonAnimation;
if (skeletonAnimation != null) {
skeletonAnimation.UpdateLocal -= HandleUpdateLocal;
skeletonAnimation.UpdateLocal += HandleUpdateLocal;
if (animationComponent != null) {
animationComponent.UpdateLocal -= HandleUpdateLocal;
animationComponent.UpdateLocal += HandleUpdateLocal;
skeletonAnimation.OnAnimationRebuild -= InitializeOnRebuild;
skeletonAnimation.OnAnimationRebuild += InitializeOnRebuild;
animationComponent.OnAnimationRebuild -= InitializeOnRebuild;
animationComponent.OnAnimationRebuild += InitializeOnRebuild;
SkeletonUtility skeletonUtility = GetComponent<SkeletonUtility>();
if (skeletonUtility != null) {
@ -243,7 +241,7 @@ namespace Spine.Unity {
Vector2 parentBoneScale;
GetScaleAffectingRootMotion(out parentBoneScale);
ClearEffectiveBoneOffsets(parentBoneScale);
skeletonComponent.Skeleton.UpdateWorldTransform(Physics.Pose);
animationComponent.Skeleton.UpdateWorldTransform(Physics.Pose);
}
ClearRigidbodyTempMovement();
@ -283,18 +281,26 @@ namespace Spine.Unity {
public ISkeletonComponent TargetSkeletonComponent {
get {
if (skeletonComponent == null)
skeletonComponent = GetComponent<ISkeletonComponent>();
return skeletonComponent;
return TargetSkeletonAnimation;
}
}
public ISkeletonAnimation TargetSkeletonAnimationComponent {
get { return TargetSkeletonComponent as ISkeletonAnimation; }
get {
return TargetSkeletonAnimation;
}
}
public SkeletonAnimationBase TargetSkeletonAnimation {
get {
if (animationComponent == null)
animationComponent = GetComponent<SkeletonAnimationBase>();
return animationComponent;
}
}
public void SetRootMotionBone (string name) {
Skeleton skeleton = skeletonComponent.Skeleton;
Skeleton skeleton = animationComponent.Skeleton;
Bone bone = skeleton.FindBone(name);
if (bone != null) {
this.rootMotionBoneIndex = bone.Data.Index;
@ -359,7 +365,7 @@ namespace Spine.Unity {
endPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, endTime);
startPos = TimelineExtensions.Evaluate(xTimeline, yTimeline, startTime);
}
ExposedList<IConstraint> constraints = skeletonComponent.Skeleton.Constraints;
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
@ -411,7 +417,7 @@ namespace Spine.Unity {
startRotation = rotateTimeline.Evaluate(startTime);
}
ExposedList<IConstraint> constraints = skeletonComponent.Skeleton.Constraints;
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
@ -518,12 +524,12 @@ namespace Spine.Unity {
}
int GetConstraintLastPosIndex (int constraintIndex) {
ExposedList<IConstraint> constraints = skeletonComponent.Skeleton.Constraints;
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
return transformConstraintIndices.FindIndex(addedIndex => addedIndex == constraintIndex);
}
void FindTransformConstraintsAffectingBone () {
ExposedList<IConstraint> constraints = skeletonComponent.Skeleton.Constraints;
ExposedList<IConstraint> constraints = animationComponent.Skeleton.Constraints;
IConstraint[] constraintsItems = constraints.Items;
for (int constraintIndex = 0, n = constraints.Count; constraintIndex < n; ++constraintIndex) {
TransformConstraint constraint = constraintsItems[constraintIndex] as TransformConstraint;
@ -558,14 +564,14 @@ namespace Spine.Unity {
void GatherTopLevelBones () {
topLevelBones.Clear();
Skeleton skeleton = skeletonComponent.Skeleton;
Skeleton skeleton = animationComponent.Skeleton;
foreach (Bone bone in skeleton.Bones) {
if (bone.Parent == null)
topLevelBones.Add(bone);
}
}
void HandleUpdateLocal (ISkeletonAnimation animatedSkeletonComponent) {
void HandleUpdateLocal (ISkeletonRenderer skeletonRenderer) {
if (!this.isActiveAndEnabled)
return; // Root motion is only applied when component is enabled.
@ -636,13 +642,13 @@ namespace Spine.Unity {
rootMotionBone.AppliedPose.X = rootMotionBone.Pose.X;
rootMotionBone.AppliedPose.Y = rootMotionBone.Pose.Y;
rootMotionBone.AppliedPose.Rotation = rootMotionBone.Pose.Rotation;
IConstraint[] transformConstraintsItems = skeletonComponent.Skeleton.Constraints.Items;
IConstraint[] transformConstraintsItems = animationComponent.Skeleton.Constraints.Items;
foreach (int constraintIndex in this.transformConstraintIndices) {
TransformConstraint constraint = (TransformConstraint)transformConstraintsItems[constraintIndex];
// apply the constraint and sets Bone.ax, Bone.ay and Bone.arotation values.
/// Update is based on Bone.x, Bone.y and Bone.rotation, so skeleton.UpdateWorldTransform()
/// can be called afterwards without having a different starting point.
constraint.Update(skeletonComponent.Skeleton, Physics.None);
constraint.Update(animationComponent.Skeleton, Physics.None);
}
}
@ -652,7 +658,7 @@ namespace Spine.Unity {
}
Vector2 GetScaleAffectingRootMotion (out Vector2 parentBoneScale) {
Skeleton skeleton = skeletonComponent.Skeleton;
Skeleton skeleton = animationComponent.Skeleton;
Vector2 totalScale = Vector2.one;
totalScale.x *= skeleton.ScaleX;
totalScale.y *= skeleton.ScaleY;
@ -702,7 +708,7 @@ namespace Spine.Unity {
ApplyTransformConstraints();
// Move top level bones in opposite direction of the root motion bone
Skeleton skeleton = skeletonComponent.Skeleton;
Skeleton skeleton = animationComponent.Skeleton;
foreach (Bone topLevelBone in topLevelBones) {
if (topLevelBone == rootMotionBone) {
if (transformPositionX) topLevelBone.Pose.X = displacementSkeletonSpace.x / skeleton.ScaleX;

View File

@ -31,7 +31,19 @@
#define NEW_PREFAB_SYSTEM
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#define USE_THREADED_ANIMATION_UPDATE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -41,107 +53,11 @@ namespace Spine.Unity {
[ExecuteInEditMode]
#endif
[AddComponentMenu("Spine/SkeletonAnimation")]
[HelpURL("https://esotericsoftware.com/spine-unity-main-components#SkeletonAnimation-Component")]
public class SkeletonAnimation : SkeletonRenderer, ISkeletonAnimation, IAnimationStateComponent {
#region IAnimationStateComponent
/// <summary>
/// This is the Spine.AnimationState object of this SkeletonAnimation. You can control animations through it.
/// Note that this object, like .skeleton, is not guaranteed to exist in Awake. Do all accesses and caching to it in Start</summary>
public Spine.AnimationState state;
/// <summary>
/// This is the Spine.AnimationState object of this SkeletonAnimation. You can control animations through it.
/// Note that this object, like .skeleton, is not guaranteed to exist in Awake. Do all accesses and caching to it in Start</summary>
public Spine.AnimationState AnimationState {
get {
Initialize(false);
return this.state;
}
}
private bool wasUpdatedAfterInit = true;
#endregion
#region Bone and Initialization Callbacks ISkeletonAnimation
protected event ISkeletonAnimationDelegate _OnAnimationRebuild;
protected event UpdateBonesDelegate _BeforeApply;
protected event UpdateBonesDelegate _UpdateLocal;
protected event UpdateBonesDelegate _UpdateWorld;
protected event UpdateBonesDelegate _UpdateComplete;
/// <summary>OnAnimationRebuild is raised after the SkeletonAnimation component is successfully initialized.</summary>
public event ISkeletonAnimationDelegate OnAnimationRebuild { add { _OnAnimationRebuild += value; } remove { _OnAnimationRebuild -= value; } }
/// <summary>
/// Occurs before the animations are applied.
/// Use this callback when you want to change the skeleton state before animations are applied on top.
/// </summary>
public event UpdateBonesDelegate BeforeApply { add { _BeforeApply += value; } remove { _BeforeApply -= value; } }
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.
/// </summary>
public event UpdateBonesDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.
/// </summary>
public event UpdateBonesDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
public UpdateTiming UpdateTiming { get { return updateTiming; } set { updateTiming = value; } }
/// <summary>If enabled, AnimationState uses unscaled game time
/// (<c>Time.unscaledDeltaTime</c> instead of normal game time(<c>Time.deltaTime</c>),
/// running animations independent of e.g. game pause (<c>Time.timeScale</c>).
/// Instance SkeletonAnimation.timeScale will still be applied.</summary>
[SerializeField] protected bool unscaledTime;
public bool UnscaledTime { get { return unscaledTime; } set { unscaledTime = value; } }
#endregion
[HelpURL("http://esotericsoftware.com/spine-unity#SkeletonAnimation-Component")]
public class SkeletonAnimation : SkeletonAnimationBase, IAnimationStateComponent, IUpgradable {
#region Serialized state and Beginner API
[SerializeField]
[SpineAnimation]
private string _animationName;
/// <summary>
/// Setting this property sets the animation of the skeleton. If invalid, it will store the animation name for the next time the skeleton is properly initialized.
/// Getting this property gets the name of the currently playing animation. If invalid, it will return the last stored animation name set through this property.</summary>
public string AnimationName {
get {
if (!valid) {
return _animationName;
} else {
TrackEntry entry = state.GetCurrent(0);
return entry == null ? null : entry.Animation.Name;
}
}
set {
Initialize(false);
if (_animationName == value) {
TrackEntry entry = state.GetCurrent(0);
if (entry != null && entry.Loop == loop)
return;
}
_animationName = value;
if (string.IsNullOrEmpty(value)) {
state.ClearTrack(0);
} else {
Spine.Animation animationObject = skeletonDataAsset.GetSkeletonData(false).FindAnimation(value);
if (animationObject != null)
state.SetAnimation(0, animationObject, loop);
}
}
}
[FormerlySerializedAs("_animationName")] [SerializeField] [SpineAnimation] protected string animationName = "";
/// <summary>Whether or not <see cref="AnimationName"/> should loop. This only applies to the initial animation specified in the inspector, or any subsequent Animations played through .AnimationName. Animations set through state.SetAnimation are unaffected.</summary>
public bool loop;
@ -150,167 +66,465 @@ namespace Spine.Unity {
/// The rate at which animations progress over time. 1 means 100%. 0.5 means 50%.</summary>
/// <remarks>AnimationState and TrackEntry also have their own timeScale. These are combined multiplicatively.</remarks>
public float timeScale = 1;
/// <summary>If enabled, AnimationState time is advanced by Unscaled Game Time
/// (<c>Time.unscaledDeltaTime</c> instead of the default Game Time(<c>Time.deltaTime</c>).
/// to animate independent of game <c>Time.timeScale</c>.
/// Instance timeScale will still be applied.</summary>
public bool unscaledTime;
#endregion
#region Runtime Instantiation
/// <summary>Adds and prepares a SkeletonAnimation component to a GameObject at runtime.</summary>
/// <returns>The newly instantiated SkeletonAnimation</returns>
public static SkeletonAnimation AddToGameObject (GameObject gameObject, SkeletonDataAsset skeletonDataAsset,
bool quiet = false) {
return SkeletonRenderer.AddSpineComponent<SkeletonAnimation>(gameObject, skeletonDataAsset, quiet);
#region Animation State Callbacks on Main Thread
#if USE_THREADED_ANIMATION_UPDATE
public event AnimationState.TrackEntryDelegate mainThreadStart, mainThreadInterrupt, mainThreadEnd, mainThreadDispose, mainThreadComplete;
public event AnimationState.TrackEntryEventDelegate mainThreadEvent;
protected struct EventQueueEntry {
public EventType type;
public TrackEntry entry;
public Event e;
public EventQueueEntry (EventType eventType, TrackEntry trackEntry, Event e = null) {
this.type = eventType;
this.entry = trackEntry;
this.e = e;
}
}
protected enum EventType {
Start, Interrupt, End, Dispose, Complete, Event
}
protected List<EventQueueEntry> mainThreadEventQueue = null;
protected void DrainThreadedEventQueue () {
for (int i = 0; i < mainThreadEventQueue.Count; i++) {
EventQueueEntry queueEntry = mainThreadEventQueue[i];
TrackEntry trackEntry = queueEntry.entry;
switch (queueEntry.type) {
case EventType.Start:
if (mainThreadStart != null) mainThreadStart(trackEntry);
break;
case EventType.Interrupt:
if (mainThreadInterrupt != null) mainThreadInterrupt(trackEntry);
break;
case EventType.End:
if (mainThreadEnd != null) mainThreadEnd(trackEntry);
break;
case EventType.Dispose:
if (mainThreadDispose != null) mainThreadDispose(trackEntry);
break;
case EventType.Complete:
if (mainThreadComplete != null) mainThreadComplete(trackEntry);
break;
case EventType.Event:
if (mainThreadEvent != null) mainThreadEvent(trackEntry, queueEntry.e);
break;
}
}
mainThreadEventQueue.Clear();
}
/// <summary>Instantiates a new UnityEngine.GameObject and adds a prepared SkeletonAnimation component to it.</summary>
/// <returns>The newly instantiated SkeletonAnimation component.</returns>
public static SkeletonAnimation NewSkeletonAnimationGameObject (SkeletonDataAsset skeletonDataAsset,
bool quiet = false) {
return SkeletonRenderer.NewSpineGameObject<SkeletonAnimation>(skeletonDataAsset, quiet);
private void ThreadedStart (TrackEntry entry) {
if (!UsesThreadedAnimation) {
if (mainThreadStart != null) mainThreadStart(entry);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.Start, entry));
}
private void ThreadedInterrupt (TrackEntry entry) {
if (!UsesThreadedAnimation) {
if (mainThreadInterrupt != null) mainThreadInterrupt(entry);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.Interrupt, entry));
}
private void ThreadedEnd (TrackEntry entry) {
if (!UsesThreadedAnimation) {
if (mainThreadEnd != null) mainThreadEnd(entry);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.End, entry));
}
private void ThreadedDispose (TrackEntry entry) {
if (!UsesThreadedAnimation) {
if (mainThreadDispose != null) mainThreadDispose(entry);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.Dispose, entry));
}
private void ThreadedComplete (TrackEntry entry) {
if (!UsesThreadedAnimation) {
if (mainThreadComplete != null) mainThreadComplete(entry);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.Complete, entry));
}
private void ThreadedEvent (TrackEntry entry, Event e) {
if (!UsesThreadedAnimation) {
if (mainThreadEvent != null) mainThreadEvent(entry, e);
return;
}
mainThreadEventQueue.Add(new EventQueueEntry(EventType.Event, entry, e));
}
public event AnimationState.TrackEntryDelegate MainThreadStart {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadStart += value;
this.AnimationState.Start -= ThreadedStart;
this.AnimationState.Start += ThreadedStart;
}
remove {
mainThreadStart -= value;
this.AnimationState.Start -= ThreadedStart;
}
}
public event AnimationState.TrackEntryDelegate MainThreadInterrupt {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadInterrupt += value;
this.AnimationState.Interrupt -= ThreadedInterrupt;
this.AnimationState.Interrupt += ThreadedInterrupt;
}
remove {
mainThreadInterrupt -= value;
this.AnimationState.Interrupt -= ThreadedInterrupt;
}
}
public event AnimationState.TrackEntryDelegate MainThreadEnd {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadEnd += value;
this.AnimationState.End -= ThreadedEnd;
this.AnimationState.End += ThreadedEnd;
}
remove {
mainThreadEnd -= value;
this.AnimationState.End -= ThreadedEnd;
}
}
public event AnimationState.TrackEntryDelegate MainThreadDispose {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadDispose += value;
this.AnimationState.Dispose -= ThreadedDispose;
this.AnimationState.Dispose += ThreadedDispose;
}
remove {
mainThreadDispose -= value;
this.AnimationState.Dispose -= ThreadedDispose;
}
}
public event AnimationState.TrackEntryDelegate MainThreadComplete {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadComplete += value;
this.AnimationState.Complete -= ThreadedComplete;
this.AnimationState.Complete += ThreadedComplete;
}
remove {
mainThreadComplete -= value;
this.AnimationState.Complete -= ThreadedComplete;
}
}
public event AnimationState.TrackEntryEventDelegate MainThreadEvent {
add {
if (mainThreadEventQueue == null) mainThreadEventQueue = new List<EventQueueEntry>();
mainThreadEvent += value;
this.AnimationState.Event -= ThreadedEvent;
this.AnimationState.Event += ThreadedEvent;
}
remove {
mainThreadEvent -= value;
this.AnimationState.Event -= ThreadedEvent;
}
}
public override void MainThreadAfterUpdateInternal () {
if (mainThreadEventQueue != null)
DrainThreadedEventQueue();
base.MainThreadAfterUpdateInternal();
}
#else // USE_THREADED_ANIMATION_UPDATE
public event AnimationState.TrackEntryDelegate MainThreadStart {
add { this.AnimationState.Start += value; }
remove { this.AnimationState.Start -= value; }
}
public event AnimationState.TrackEntryDelegate MainThreadInterrupt {
add { this.AnimationState.Interrupt += value; }
remove { this.AnimationState.Interrupt -= value; }
}
public event AnimationState.TrackEntryDelegate MainThreadEnd {
add { this.AnimationState.End += value; }
remove { this.AnimationState.End -= value; }
}
public event AnimationState.TrackEntryDelegate MainThreadDispose {
add { this.AnimationState.Dispose += value; }
remove { this.AnimationState.Dispose -= value; }
}
public event AnimationState.TrackEntryDelegate MainThreadComplete {
add { this.AnimationState.Complete += value; }
remove { this.AnimationState.Complete -= value; }
}
public event AnimationState.TrackEntryEventDelegate MainThreadEvent {
add { this.AnimationState.Event += value; }
remove { this.AnimationState.Event -= value; }
}
#endif // USE_THREADED_ANIMATION_UPDATE
#endregion
protected Spine.AnimationState state;
/// <summary>
/// This is the Spine.AnimationState object of this SkeletonAnimation. You can control animations through it.
/// Note that this object, like .skeleton, is not guaranteed to exist in Awake. Do all accesses and caching to it in Start</summary>
public Spine.AnimationState AnimationState {
get {
Initialize(false);
return state;
}
set { state = value; }
}
public override bool IsValid {
get { return skeletonRenderer != null && skeletonRenderer.IsValid && state != null; }
}
public bool UnscaledTime { get { return unscaledTime; } set { unscaledTime = value; } }
#region Serialized state and Beginner API
/// <summary>
/// Setting this property sets the animation of the skeleton. If invalid, it will store the animation name for the next time the skeleton is properly initialized.
/// Getting this property gets the name of the currently playing animation. If invalid, it will return the last stored animation name set through this property.</summary>
public string AnimationName {
get {
if (!this.IsValid) {
return animationName;
} else {
TrackEntry entry = state.GetCurrent(0);
return entry == null ? null : entry.Animation.Name;
}
}
set {
Initialize(false);
if (!IsValid) {
animationName = value;
return;
}
if (animationName == value) {
TrackEntry entry = state.GetCurrent(0);
if (entry != null && entry.Loop == loop)
return;
}
animationName = value;
if (string.IsNullOrEmpty(value)) {
state.ClearTrack(0);
} else {
SkeletonData skeletonData = skeletonRenderer.SkeletonDataAsset.GetSkeletonData(false);
if (skeletonData == null)
return;
Spine.Animation animationObject = skeletonData.FindAnimation(value);
if (animationObject != null)
state.SetAnimation(0, animationObject, loop);
}
}
}
#endregion
/// <summary>
/// Clears the previously generated mesh, resets the skeleton's pose, and clears all previously active animations.</summary>
public override void ClearState () {
base.ClearState();
public override void ClearAnimationState () {
if (state != null) state.ClearTracks();
}
/// <summary>
/// Initialize this component. Attempts to load the SkeletonData and creates the internal Spine objects and buffers.</summary>
/// <param name="overwrite">If set to <c>true</c>, force overwrite an already initialized object.</param>
public override void Initialize (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
public override void InitializeAnimationComponent () {
base.InitializeAnimationComponent();
if (!skeletonRenderer.IsValid)
return;
#if UNITY_EDITOR
if (BuildUtilities.IsInSkeletonAssetBuildPreProcessing)
return;
#endif
state = null; // prevent applying leftover AnimationState
base.Initialize(overwrite, quiet);
if (!valid)
return;
state = new Spine.AnimationState(skeletonDataAsset.GetAnimationStateData());
AnimationStateData data = skeletonRenderer.SkeletonDataAsset.GetAnimationStateData();
#if UNITY_EDITOR
AnimationState oldAnimationState = state;
#endif
state = new Spine.AnimationState(data);
state.Dispose += OnAnimationDisposed;
wasUpdatedAfterInit = false;
if (!string.IsNullOrEmpty(_animationName)) {
Spine.Animation animationObject = skeletonDataAsset.GetSkeletonData(false).FindAnimation(_animationName);
if (animationObject != null) {
state.SetAnimation(0, animationObject, loop);
if (state == null)
return;
#if UNITY_EDITOR
if (!Application.isPlaying)
Update(0f);
if (oldAnimationState != null)
state.AssignEventSubscribersFrom(oldAnimationState);
#endif
}
}
if (_OnAnimationRebuild != null)
_OnAnimationRebuild(this);
}
virtual protected void Update () {
#if UNITY_EDITOR
if (!Application.isPlaying) {
Update(0f);
return;
}
#endif
if (updateTiming != UpdateTiming.InUpdate) return;
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
}
virtual protected void FixedUpdate () {
if (updateTiming != UpdateTiming.InFixedUpdate) return;
Update(unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime);
}
/// <summary>Progresses the AnimationState according to the given deltaTime, and applies it to the Skeleton. Use Time.deltaTime to update manually. Use deltaTime 0 to update without progressing the time.</summary>
public void Update (float deltaTime) {
if (!valid || state == null)
return;
wasUpdatedAfterInit = true;
if (updateMode < UpdateMode.OnlyAnimationStatus)
return;
UpdateAnimationStatus(deltaTime);
if (updateMode == UpdateMode.OnlyAnimationStatus)
return;
ApplyAnimation();
}
protected void UpdateAnimationStatus (float deltaTime) {
deltaTime *= timeScale;
state.Update(deltaTime);
skeleton.Update(deltaTime);
ApplyTransformMovementToPhysics();
if (updateMode == UpdateMode.OnlyAnimationStatus) {
state.ApplyEventTimelinesOnly(skeleton, issueEvents: false);
return;
}
}
public virtual void ApplyAnimation () {
if (_BeforeApply != null)
_BeforeApply(this);
if (updateMode != UpdateMode.OnlyEventTimelines)
state.Apply(skeleton);
else
state.ApplyEventTimelinesOnly(skeleton, issueEvents: true);
AfterAnimationApplied();
}
public void AfterAnimationApplied () {
if (_UpdateLocal != null)
_UpdateLocal(this);
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
}
if (_UpdateComplete != null) {
_UpdateComplete(this);
}
}
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();
}
public override void OnBecameVisible () {
UpdateMode previousUpdateMode = updateMode;
updateMode = UpdateMode.FullUpdate;
// OnBecameVisible is called after LateUpdate()
if (previousUpdateMode != UpdateMode.FullUpdate &&
previousUpdateMode != UpdateMode.EverythingExceptMesh)
Update(0);
if (previousUpdateMode != UpdateMode.FullUpdate)
LateUpdate();
UpdateInitialAnimation();
}
protected virtual void OnAnimationDisposed (TrackEntry entry) {
// when updateMode disables applying animations, still ensure animations are mixed out
UpdateMode updateMode = skeletonRenderer.UpdateMode;
if (updateMode != UpdateMode.FullUpdate &&
updateMode != UpdateMode.EverythingExceptMesh) {
entry.Animation.Apply(skeleton, 0, 0, false, null, 0f, MixBlend.Setup, MixDirection.Out, false);
}
}
}
public virtual void UpdateInitialAnimation () {
state.ClearTrack(0);
if (!string.IsNullOrEmpty(animationName)) {
SkeletonData skeletonData = skeletonRenderer.SkeletonDataAsset.GetSkeletonData(false);
if (skeletonData == null)
return;
Spine.Animation animation = skeletonData.FindAnimation(animationName);
if (animation != null) {
state.SetAnimation(0, animation, loop);
#if UNITY_EDITOR
if (!ApplicationIsPlaying)
Update(0f);
#endif
}
}
}
protected override float DeltaTime {
get {
return unscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
}
}
protected override void UpdateAnimationStatus (float deltaTime) {
deltaTime *= timeScale;
if (state != null) {
state.Update(deltaTime);
skeleton.Update(deltaTime);
#if UNITY_EDITOR
if (ApplicationIsPlaying)
UpdatePropertyToCurrentAnimationEditor();
#endif
if (skeletonRenderer.UpdateMode == UpdateMode.OnlyAnimationStatus) {
state.ApplyEventTimelinesOnly(skeleton, issueEvents: false);
}
}
}
protected override void ApplyStateToSkeleton (bool calledFromMainThread) {
if (skeletonRenderer.UpdateMode != UpdateMode.OnlyEventTimelines)
state.Apply(skeletonRenderer.Skeleton);
else
state.ApplyEventTimelinesOnly(skeletonRenderer.Skeleton, issueEvents: true);
}
#if UNITY_EDITOR
protected void UpdatePropertyToCurrentAnimationEditor () {
if (state.Tracks.Count == 0)
return;
Animation currentAnimation = state.Tracks.Items[0].Animation;
animationName = currentAnimation == null ? "<None>" : currentAnimation.Name;
}
#endif
#region Runtime Instantiation
/// <summary>Adds and prepares SkeletonAnimation and SkeletonRenderer components to a GameObject at runtime.</summary>
/// <returns>A struct referencing the newly instantiated SkeletonAnimation and SkeletonRenderer components.</returns>
public static SkeletonComponents<SkeletonRenderer, SkeletonAnimation> AddToGameObject (
GameObject gameObject, SkeletonDataAsset skeletonDataAsset, bool quiet = false) {
return Spine.Unity.SkeletonRenderer.AddSpineComponents<SkeletonRenderer, SkeletonAnimation>(
gameObject, skeletonDataAsset, quiet);
}
/// <summary>Instantiates a new UnityEngine.GameObject and adds SkeletonAnimation and SkeletonRenderer components to it.</summary>
/// <returns>A struct referencing the newly instantiated SkeletonAnimation and SkeletonRenderer components.</returns>
public static SkeletonComponents<SkeletonRenderer, SkeletonAnimation> NewSkeletonAnimationGameObject (SkeletonDataAsset skeletonDataAsset,
bool quiet = false) {
return Spine.Unity.SkeletonRenderer.NewSpineGameObject<SkeletonRenderer, SkeletonAnimation>(
skeletonDataAsset, quiet);
}
#endregion
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public override void UpgradeTo43 () {
if (!Application.isPlaying && !wasDeprecatedTransferred) {
UpgradeTo43Components();
TransferDeprecatedFields();
InitializeAnimationComponent();
}
}
protected void UpgradeTo43Components () {
if (gameObject.GetComponent<SkeletonRenderer>() == null &&
gameObject.GetComponent<SkeletonGraphic>() == null) {
gameObject.AddComponent<SkeletonRenderer>();
EditorBridge.RequestMarkDirty(gameObject);
Debug.Log(string.Format("{0}: Auto-migrated old SkeletonAnimation component to split SkeletonAnimation + SkeletonRenderer components.",
gameObject.name), gameObject);
}
}
/// <summary>Transfer of former base class SkeletonRenderer parameters.</summary>
protected void TransferDeprecatedFields () {
wasDeprecatedTransferred = true;
SkeletonRenderer skeletonRenderer = gameObject.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
return;
skeletonRenderer.skeletonDataAsset = this.skeletonDataAssetDeprecated;
skeletonRenderer.initialSkinName = this.initialSkinNameDeprecated;
skeletonRenderer.EditorSkipSkinSync = this.editorSkipSkinSyncDeprecated;
skeletonRenderer.initialFlipX = this.initialFlipXDeprecated;
skeletonRenderer.initialFlipY = this.initialFlipYDeprecated;
skeletonRenderer.UpdateMode = this.updateModeDeprecated;
skeletonRenderer.updateWhenInvisible = this.updateWhenInvisibleDeprecated;
skeletonRenderer.separatorSlotNames = this.separatorSlotNamesDeprecated;
skeletonRenderer.MeshSettings.zSpacing = this.zSpacingDeprecated;
skeletonRenderer.MeshSettings.useClipping = this.useClippingDeprecated;
skeletonRenderer.MeshSettings.immutableTriangles = this.immutableTrianglesDeprecated;
skeletonRenderer.MeshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated;
skeletonRenderer.MeshSettings.tintBlack = this.tintBlackDeprecated;
skeletonRenderer.MeshSettings.addNormals = this.addNormalsDeprecated;
skeletonRenderer.MeshSettings.calculateTangents = this.calculateTangentsDeprecated;
skeletonRenderer.clearStateOnDisable = this.clearStateOnDisableDeprecated;
skeletonRenderer.singleSubmesh = this.singleSubmeshDeprecated;
skeletonRenderer.maskInteraction = this.maskInteractionDeprecated;
}
[SerializeField] protected bool wasDeprecatedTransferred = false;
// SkeletonRenderer former base class parameters
[FormerlySerializedAs("skeletonDataAsset")] [SerializeField] private SkeletonDataAsset skeletonDataAssetDeprecated;
[FormerlySerializedAs("initialSkinName")] [SpineSkin(defaultAsEmptyString: true)] [SerializeField] private string initialSkinNameDeprecated;
[FormerlySerializedAs("editorSkipSkinSync")] [SerializeField] private bool editorSkipSkinSyncDeprecated = false;
[FormerlySerializedAs("initialFlipX")] [SerializeField] private bool initialFlipXDeprecated = false;
[FormerlySerializedAs("initialFlipY")] [SerializeField] private bool initialFlipYDeprecated = false;
[FormerlySerializedAs("updateMode")] [SerializeField] private UpdateMode updateModeDeprecated = UpdateMode.FullUpdate;
[FormerlySerializedAs("updateWhenInvisible")] [SerializeField] private UpdateMode updateWhenInvisibleDeprecated = UpdateMode.FullUpdate;
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators"),
UnityEngine.Serialization.FormerlySerializedAs("separatorSlotNames")]
[SerializeField] private string[] separatorSlotNamesDeprecated = new string[0];
[FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f;
[FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true;
[FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false;
[FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true;
[FormerlySerializedAs("clearStateOnDisable")] [SerializeField] private bool clearStateOnDisableDeprecated = false;
[FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false;
[FormerlySerializedAs("singleSubmesh")] [SerializeField] private bool singleSubmeshDeprecated = false;
[FormerlySerializedAs("calculateNormals"),
FormerlySerializedAs("addNormals")]
[SerializeField] private bool addNormalsDeprecated = false;
[FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false;
#if BUILT_IN_SPRITE_MASK_COMPONENT
[FormerlySerializedAs("maskInteraction")] [SerializeField] private SpriteMaskInteraction maskInteractionDeprecated = SpriteMaskInteraction.None;
#endif // BUILT_IN_SPRITE_MASK_COMPONENT
#endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
#endregion
}
}

View File

@ -0,0 +1,732 @@
/******************************************************************************
* 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.
*****************************************************************************/
#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#endif
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#define USE_THREADED_SKELETON_UPDATE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace Spine.Unity {
// Partial class: covers common identical attributes, properties and methods shared across all
// ISkeletonRenderer subclasses. This is a workaround for single inheritance limitations and covers code which
// would otherwise be located in a base-class of SkeletonGraphic.
public partial class SkeletonGraphic : MaskableGraphic, ISkeletonRenderer {
// Identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
#region Identical common ISkeletonRenderer code
#region ISkeletonRenderer Attributes
// Core Attributes
public ISkeletonAnimation skeletonAnimation;
public SkeletonDataAsset skeletonDataAsset;
[System.NonSerialized] public Skeleton skeleton;
[System.NonSerialized] public bool valid;
protected bool wasMeshUpdatedAfterInit = false;
protected bool updateTriangles = true;
// Initialization Settings
/// <summary>Skin name to use when the Skeleton is initialized.</summary>
[SpineSkin(dataField: "skeletonDataAsset", defaultAsEmptyString: true)] public string initialSkinName;
/// <summary>Flip X and Y to use when the Skeleton is initialized.</summary>
public bool initialFlipX, initialFlipY;
// Render Settings
[SerializeField] protected MeshGenerator.Settings meshSettings = MeshGenerator.Settings.Default;
[System.NonSerialized] protected readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
protected UpdateMode updateMode = UpdateMode.FullUpdate;
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate;
/// <summary>Clears the state of the render and skeleton when this component or its GameObject is disabled. This prevents previous state from being retained when it is enabled again. When pooling your skeleton, setting this to true can be helpful.</summary>
public bool clearStateOnDisable = false;
// Submesh Separation
/// <summary>Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.</summary>
[SpineSlot] public string[] separatorSlotNames = new string[0];
/// <summary>Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects.</summary>
[System.NonSerialized] public readonly List<Slot> separatorSlots = new List<Slot>();
public bool enableSeparatorSlots = false;
// Overrides Attributes
// These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
event InstructionDelegate generateMeshOverride;
[System.NonSerialized] readonly Dictionary<Slot, Material> customSlotMaterials = new Dictionary<Slot, Material>();
[System.NonSerialized] protected bool materialsNeedUpdate = false;
// Physics Attributes
/// <seealso cref="PhysicsPositionInheritanceFactor"/>
[SerializeField] protected Vector2 physicsPositionInheritanceFactor = Vector2.one;
/// <seealso cref="PhysicsRotationInheritanceFactor"/>
[SerializeField] protected float physicsRotationInheritanceFactor = 1.0f;
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
[SerializeField] protected Transform physicsMovementRelativeTo = null;
/// <summary>Used for applying Transform translation to skeleton PhysicsConstraints.</summary>
protected Vector3 lastPosition;
/// <summary>Used for applying Transform rotation to skeleton PhysicsConstraints.</summary>
protected float lastRotation;
/// <summary>Position delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected Vector3 positionDelta;
/// <summary>Rotation delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected float rotationDelta;
// Threaded Update System Attributes
#if USE_THREADED_SKELETON_UPDATE
protected static int mainThreadID = -1;
[SerializeField] protected SettingsTriState threadedMeshGeneration = SettingsTriState.UseGlobalSetting;
protected bool isUpdatedExternally = false;
protected bool requiresMeshBufferAssignmentMainThread = false;
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
#endif
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
protected bool editorSkipSkinSync = false;
protected bool requiresEditorUpdate = false;
#endif
#endregion ISkeletonRenderer Attributes
#region ISkeletonRenderer Properties
public ISkeletonRenderer Renderer { get { return this; } }
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonAnimation Animation { get { return skeletonAnimation; } set { skeletonAnimation = value; } }
public SkeletonDataAsset SkeletonDataAsset {
get { return skeletonDataAsset; }
set { skeletonDataAsset = value; }
}
public Skeleton Skeleton {
get {
Initialize(false);
return skeleton;
}
set {
skeleton = value;
}
}
public SkeletonData SkeletonData {
get {
Initialize(false);
return skeleton == null ? null : skeleton.Data;
}
}
public bool IsValid { get { return valid; } }
public string InitialSkinName { get { return initialSkinName; } set { initialSkinName = value; } }
/// <summary>Flip X to use when the Skeleton is initialized.</summary>
public bool InitialFlipX { get { return initialFlipX; } set { initialFlipX = value; } }
/// <summary>Flip Y to use when the Skeleton is initialized.</summary>
public bool InitialFlipY { get { return initialFlipY; } set { initialFlipY = value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode UpdateWhenInvisible { get { return updateWhenInvisible; } set { updateWhenInvisible = value; } }
public MeshGenerator.Settings MeshSettings { get { return meshSettings; } set { meshSettings = value; } }
// Submesh Separation
/// <summary>Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects.</summary>
public List<Slot> SeparatorSlots { get { return separatorSlots; } }
public bool EnableSeparatorSlots { get { return enableSeparatorSlots; } set { enableSeparatorSlots = value; } }
// Overrides Properties
public bool HasGenerateMeshOverride { get { return generateMeshOverride != null; } }
/// <summary>Allows separate code to take over rendering for this SkeletonRenderer component. The subscriber is passed a SkeletonRendererInstruction argument to determine how to render a skeleton.</summary>
public event InstructionDelegate GenerateMeshOverride {
add {
generateMeshOverride += value;
if (disableRenderingOnOverride && generateMeshOverride != null) {
Initialize(false);
DisableRenderers();
updateMode = UpdateMode.FullUpdate;
}
}
remove {
generateMeshOverride -= value;
if (disableRenderingOnOverride && generateMeshOverride == null) {
Initialize(false);
EnableRenderers();
}
}
}
/// <summary>Use this Dictionary to use a different Material to render specific Slots.</summary>
public Dictionary<Slot, Material> CustomSlotMaterials {
get { materialsNeedUpdate = true; return customSlotMaterials; }
}
// Physics Properties
/// <summary>When set to non-zero, Transform position movement in X and Y direction
/// is applied to skeleton PhysicsConstraints, multiplied by this scale factor.
/// Typical values are <c>Vector2.one</c> to apply XY movement 1:1,
/// <c>Vector2(2f, 2f)</c> to apply movement with double intensity,
/// <c>Vector2(1f, 0f)</c> to apply only horizontal movement, or
/// <c>Vector2.zero</c> to not apply any Transform position movement at all.</summary>
public Vector2 PhysicsPositionInheritanceFactor {
get {
return physicsPositionInheritanceFactor;
}
set {
if (physicsPositionInheritanceFactor == Vector2.zero && value != Vector2.zero) ResetLastPosition();
physicsPositionInheritanceFactor = value;
}
}
/// <summary>When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints,
/// multiplied by this scale factor. Typical values are <c>1</c> to apply movement 1:1,
/// <c>2</c> to apply movement with double intensity, or
/// <c>0</c> to not apply any Transform rotation movement at all.</summary>
public float PhysicsRotationInheritanceFactor {
get {
return physicsRotationInheritanceFactor;
}
set {
if (physicsRotationInheritanceFactor == 0f && value != 0f) ResetLastRotation();
physicsRotationInheritanceFactor = value;
}
}
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
public Transform PhysicsMovementRelativeTo {
get {
return physicsMovementRelativeTo;
}
set {
physicsMovementRelativeTo = value;
if (physicsPositionInheritanceFactor != Vector2.zero) ResetLastPosition();
if (physicsRotationInheritanceFactor != 0f) ResetLastRotation();
}
}
// Threaded Update System Properties
#if USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedMeshGeneration {
get { return threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration; }
}
public bool RequiresMeshBufferAssignmentMainThread {
get { return requiresMeshBufferAssignmentMainThread; }
}
public SettingsTriState ThreadedMeshGeneration {
get { return threadedMeshGeneration; }
set {
if (threadedMeshGeneration == value) return;
threadedMeshGeneration = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration)
system.RegisterForUpdate(this);
else
system.UnregisterFromUpdate(this);
}
}
}
#if UNITY_EDITOR
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else // USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
public bool RequiresMeshBufferAssignmentMainThread { get { return true; } }
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
public bool EditorSkipSkinSync {
get { return editorSkipSkinSync; }
set { editorSkipSkinSync = value; }
}
#endif
#endregion ISkeletonRenderer Properties
#region ISkeletonRenderer Methods
protected virtual void ClearCommon () {
// Note: do not reset meshFilter.sharedMesh or meshRenderer.sharedMaterial to null,
// otherwise constant reloading will be triggered at prefabs.
currentInstructions.Clear();
rendererBuffers.Clear();
ClearMeshGenerator();
skeleton = null;
valid = false;
if (skeletonAnimation != null)
skeletonAnimation.ClearAnimationState();
}
protected virtual void InitializeCommon (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
if (skeletonDataAsset == null || skeletonDataAsset.skeletonJSON == null) {
Clear();
return;
} else if (skeleton != null && skeletonDataAsset.GetSkeletonData(true) != skeleton.Data) {
Clear();
} else if (this.Freeze) {
return;
} else {
ClearCommon();
}
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(quiet);
if (skeletonData == null) return;
rendererBuffers.Initialize();
skeleton = new Skeleton(skeletonData) {
ScaleX = initialFlipX ? -1 : 1,
ScaleY = initialFlipY ? -1 : 1
};
valid = true;
ResetLastPositionAndRotation();
AssignInitialSkin();
separatorSlots.Clear();
for (int i = 0; i < separatorSlotNames.Length; i++)
separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i]));
AfterAnimationApplied();
wasMeshUpdatedAfterInit = false;
if (OnRebuild != null)
OnRebuild(this);
#if UNITY_EDITOR
if (!Application.isPlaying) {
string errorMessage = null;
if (!quiet && MaterialChecks.IsMaterialSetupProblematic(this, ref errorMessage))
Debug.LogWarningFormat(this, "Problematic material setup at {0}: {1}", this.name, errorMessage);
}
#endif
}
protected virtual void AssignInitialSkin () {
if (string.IsNullOrEmpty(initialSkinName) || string.Equals(initialSkinName, "default", System.StringComparison.Ordinal))
skeleton.SetSkin((Skin)null);
else
skeleton.SetSkin(initialSkinName);
}
public void ResetLastPosition () {
lastPosition = GetPhysicsTransformPosition();
}
public void ResetLastRotation () {
lastRotation = GetPhysicsTransformRotation();
}
public void ResetLastPositionAndRotation () {
lastPosition = GetPhysicsTransformPosition();
lastRotation = GetPhysicsTransformRotation();
}
/// <summary>
/// Gathers Transform movement for later application in <see cref="ApplyTransformMovementToPhysics"/>.
/// Must be called in main thread.
/// </summary>
public virtual void GatherTransformMovementForPhysics () {
#if UNITY_EDITOR
bool isPlaying = ApplicationIsPlaying;
#else
bool isPlaying = true;
#endif
if (isPlaying) {
if (physicsPositionInheritanceFactor != Vector2.zero) {
Vector3 position = GetPhysicsTransformPosition();
positionDelta = (position - lastPosition) / this.MeshScale;
positionDelta = transform.InverseTransformVector(positionDelta);
if (physicsMovementRelativeTo != null) {
positionDelta = physicsMovementRelativeTo.TransformVector(positionDelta);
}
positionDelta.x *= physicsPositionInheritanceFactor.x;
positionDelta.y *= physicsPositionInheritanceFactor.y;
lastPosition = position;
}
if (physicsRotationInheritanceFactor != 0f) {
float rotation = GetPhysicsTransformRotation();
rotationDelta = rotation - lastRotation;
lastRotation = rotation;
}
}
}
/// <summary>
/// Applies position and rotation Transform movement previously gathered via
/// <see cref="GatherTransformMovementForPhysics"/>. May be called in worker thread.
/// </summary>
public virtual void ApplyTransformMovementToPhysics () {
if (physicsPositionInheritanceFactor != Vector2.zero) {
skeleton.PhysicsTranslate(positionDelta.x, positionDelta.y);
}
if (physicsRotationInheritanceFactor != 0f) {
skeleton.PhysicsRotate(0, 0, physicsRotationInheritanceFactor * rotationDelta);
}
}
protected Vector3 GetPhysicsTransformPosition () {
if (physicsMovementRelativeTo == null) {
return transform.position;
} else {
if (physicsMovementRelativeTo == transform.parent)
return transform.localPosition;
else
return physicsMovementRelativeTo.InverseTransformPoint(transform.position);
}
}
protected float GetPhysicsTransformRotation () {
if (physicsMovementRelativeTo == null) {
return this.transform.rotation.eulerAngles.z;
} else {
if (physicsMovementRelativeTo == this.transform.parent)
return this.transform.localRotation.eulerAngles.z;
else {
Quaternion relative = Quaternion.Inverse(physicsMovementRelativeTo.rotation) * this.transform.rotation;
return relative.eulerAngles.z;
}
}
}
public virtual void AfterAnimationApplied (bool calledFromMainThread = true) {
if (_UpdateLocal != null)
_UpdateLocal(this);
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
}
if (calledFromMainThread && _UpdateComplete != null) {
_UpdateComplete(this);
}
}
public CoroutineIterator AfterAnimationAppliedSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
/*
0:
if (_UpdateLocal != null) {
yield return true; // continue in main thread
1:
_UpdateLocal(this);
yield return false; // continue in worker thread
}
2:
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
// goto 5
} else {
UpdateWorldTransform(Physics.Pose);
yield return true; // continue in main thread
3:
_UpdateWorld(this);
yield return false; // continue in worker thread
4:
UpdateWorldTransform(Physics.Update);
// goto 5
}
5:
if (_UpdateComplete != null) {
yield return true; // continue in main thread
6:
_UpdateComplete(this);
// last call, no need to switch back to worker thread.
}
*/
const int StateBits = 3;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_UpdateLocal != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(1, StateMask);
} else {
goto case 2;
}
case 1:
AssertIsMainThread();
_UpdateLocal(this);
return coroutineIterator.YieldReturnAtState(2, StateMask);
case 2:
if (_UpdateWorld == null) {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
} else {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Pose);
return coroutineIterator.YieldReturnAtState(3, StateMask);
}
case 3:
AssertIsMainThread();
_UpdateWorld(this);
return coroutineIterator.YieldReturnAtState(4, StateMask);
case 4:
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
case 5:
if (_UpdateComplete != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(6, StateMask);
} else {
return CoroutineIterator.Done;
}
case 6:
AssertIsMainThread();
_UpdateComplete(this);
return CoroutineIterator.Done;
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonRenderer.AfterAnimationAppliedSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
protected void IssueOnPostProcessVertices (MeshGeneratorBuffers buffers) {
if (OnPostProcessVertices != null)
OnPostProcessVertices.Invoke(buffers);
}
protected virtual void UpdateWorldTransform (Physics physics) {
skeleton.UpdateWorldTransform(physics);
}
public virtual void UpdateMesh (bool calledFromMainThread = true) {
#if USE_THREADED_SKELETON_UPDATE
bool canPrepareInstructions = calledFromMainThread || !NeedsMainThreadRendererPreparation;
#else
bool canPrepareInstructions = true;
#endif
if (canPrepareInstructions)
PrepareInstructionsAndRenderers();
updateTriangles = UpdateBuffersToInstructions(calledFromMainThread);
#if USE_THREADED_SKELETON_UPDATE
if (calledFromMainThread) {
requiresMeshBufferAssignmentMainThread = false;
UpdateMeshAndMaterialsToBuffers();
} else {
requiresMeshBufferAssignmentMainThread = true;
}
#else
UpdateMeshAndMaterialsToBuffers();
#endif
}
/// <returns>True if triangles (indices array) need to be updated.</returns>
public virtual bool UpdateBuffersToInstructions (bool calledFromMainThread = true) {
if (!valid || currentInstructions.rawVertexCount < 0) return false;
wasMeshUpdatedAfterInit = true;
if (this.generateMeshOverride != null) {
if (calledFromMainThread)
this.generateMeshOverride(currentInstructions);
if (disableRenderingOnOverride) return false;
}
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetNextMesh(); // Double-buffer for performance.
// Update vertex buffers based on vertices from the attachments and assign buffers to a target UnityEngine.Mesh.
bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed, calledFromMainThread);
FillBuffersFromSubmeshInstructions(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
return updateTriangles;
}
public virtual void UpdateMeshAndMaterialsToBuffers () {
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetCurrentMesh();
UpdateMeshAndMaterialsToBuffers(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
}
protected virtual void UpdateMeshAndMaterialsToBuffers (
ExposedList<SubmeshInstruction> workingSubmeshInstructions, MeshRendererBuffers.SmartMesh currentSmartMesh,
bool updateTriangles) {
FillMeshFromBuffers(currentSmartMesh, updateTriangles);
bool materialsChanged;
rendererBuffers.GatherMaterialsFromInstructions(workingSubmeshInstructions, out materialsChanged);
if (materialsChanged || materialsNeedUpdate) {
UpdateUsedMaterialsForRenderers(workingSubmeshInstructions);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
// The UnityEngine.Mesh is ready. Set it as the MeshFilter's mesh. Store the instructions used for that mesh.
AssignMeshAtRenderer(workingSubmeshInstructions, currentSmartMesh);
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this);
}
public virtual void UpdateMaterials () {
UpdateUsedMaterialsForRenderers(currentInstructions.submeshInstructions);
}
// Threading Asserts
[System.Diagnostics.Conditional("UNITY_EDITOR")]
protected void InitializeMainThreadID () {
#if USE_THREADED_SKELETON_UPDATE
if (mainThreadID == -1)
mainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsMainThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId != mainThreadID)
Debug.LogError("AssertIsMainThread failed: worker thread calling main thread code. Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsWorkerThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadID)
Debug.LogError("AssertIsWorkerThread failed: main thread calling worker thread code! Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
#endregion ISkeletonRenderer Methods
#region ISkeletonRenderer Events
protected event SkeletonRendererDelegate _UpdateLocal;
protected event SkeletonRendererDelegate _UpdateWorld;
protected event SkeletonRendererDelegate _UpdateComplete;
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.
/// </summary>
public event SkeletonRendererDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.</summary>
public event SkeletonRendererDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event SkeletonRendererDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
/// <summary> Occurs after the vertex data is populated every frame, before the vertices are pushed into the mesh.</summary>
public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
public event SkeletonRendererDelegate OnRebuild;
/// <summary>OnInstructionsPrepared is raised at the end of <c>LateUpdate</c> after render instructions
/// are done, target renderers are prepared, and the mesh is ready to be generated.</summary>
public event InstructionDelegate OnInstructionsPrepared;
#endregion ISkeletonRenderer Events
#endregion Identical common ISkeletonRenderer code
// End of identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 903c50a86a432684aa719e571dfd38ae
timeCreated: 1754679077
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -27,171 +27,178 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor.Animations;
#endif
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
[ExecuteAlways]
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(Animator))]
[HelpURL("https://esotericsoftware.com/spine-unity-main-components#SkeletonMecanim-Component")]
public class SkeletonMecanim : SkeletonRenderer, ISkeletonAnimation {
public class SkeletonMecanim : SkeletonAnimationBase, IUpgradable {
[SerializeField] protected MecanimTranslator translator;
public SkeletonMecanim.MecanimTranslator translator;
public MecanimTranslator Translator { get { return translator; } }
private bool wasUpdatedAfterInit = true;
#region Bone and Initialization Callbacks ISkeletonAnimation
protected event ISkeletonAnimationDelegate _OnAnimationRebuild;
protected event UpdateBonesDelegate _BeforeApply;
protected event UpdateBonesDelegate _UpdateLocal;
protected event UpdateBonesDelegate _UpdateWorld;
protected event UpdateBonesDelegate _UpdateComplete;
public UnityEngine.Animator AnimatorComponent {
get { return this.GetComponent<Animator>(); }
}
/// <summary>OnAnimationRebuild is raised after the SkeletonAnimation component is successfully initialized.</summary>
public event ISkeletonAnimationDelegate OnAnimationRebuild { add { _OnAnimationRebuild += value; } remove { _OnAnimationRebuild -= value; } }
public override bool IsValid {
get {
return skeletonRenderer != null && skeletonRenderer.IsValid && translator != null &&
translator.Animator && translator.Animator.isInitialized;
}
}
/// <summary>
/// Occurs before the animations are applied.
/// Use this callback when you want to change the skeleton state before animations are applied on top.
/// </summary>
public event UpdateBonesDelegate BeforeApply { add { _BeforeApply += value; } remove { _BeforeApply -= value; } }
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.</summary>
public event UpdateBonesDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.
/// </summary>
public event UpdateBonesDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event UpdateBonesDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
[SerializeField] protected UpdateTiming updateTiming = UpdateTiming.InUpdate;
public UpdateTiming UpdateTiming { get { return updateTiming; } set { updateTiming = value; } }
#endregion
public override void Initialize (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
#if UNITY_EDITOR
if (BuildUtilities.IsInSkeletonAssetBuildPreProcessing)
return;
#endif
base.Initialize(overwrite, quiet);
if (!valid)
public override void InitializeAnimationComponent () {
base.InitializeAnimationComponent();
if (!skeletonRenderer.IsValid)
return;
if (translator == null) translator = new MecanimTranslator();
translator.Initialize(GetComponent<Animator>(), this.skeletonDataAsset);
wasUpdatedAfterInit = false;
if (_OnAnimationRebuild != null)
_OnAnimationRebuild(this);
translator.Initialize(this.AnimatorComponent, skeletonRenderer.SkeletonDataAsset);
}
public virtual void Update () {
if (!valid || updateTiming != UpdateTiming.InUpdate) return;
UpdateAnimation(Time.deltaTime);
}
public virtual void FixedUpdate () {
if (!valid || updateTiming != UpdateTiming.InFixedUpdate) return;
UpdateAnimation(Time.deltaTime);
}
/// <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(deltaTime);
}
protected void UpdateAnimation (float deltaTime) {
wasUpdatedAfterInit = true;
// animation status is kept by Mecanim Animator component
if (updateMode <= UpdateMode.OnlyAnimationStatus)
return;
protected override void UpdateAnimationStatus (float deltaTime) {
// Note: main animation status is updated by Mecanim Animator component
skeleton.Update(deltaTime);
ApplyTransformMovementToPhysics();
ApplyAnimation();
}
public virtual void ApplyAnimation () {
if (_BeforeApply != null)
_BeforeApply(this);
public override void MainThreadBeforeUpdateInternal () {
base.MainThreadBeforeUpdateInternal();
translator.GatherAnimatorState();
}
protected override void ApplyStateToSkeleton (bool calledFromMainThread) {
#if UNITY_EDITOR
Animator translatorAnimator = translator.Animator;
if (translatorAnimator != null && !translatorAnimator.isInitialized)
translatorAnimator.Rebind();
if (calledFromMainThread)
EditorRebindAnimator();
if (Application.isPlaying) {
translator.Apply(skeleton);
if (ApplicationIsPlaying || !calledFromMainThread) {
translator.Apply(skeletonRenderer.Skeleton);
} else {
Animator translatorAnimator = translator.Animator;
if (translatorAnimator != null && translatorAnimator.isInitialized &&
translatorAnimator.isActiveAndEnabled && translatorAnimator.runtimeAnimatorController != null) {
// Note: Rebind is required to prevent warning "Animator is not playing an AnimatorController" with prefabs
translatorAnimator.Rebind();
translator.Apply(skeleton);
translator.Apply(skeletonRenderer.Skeleton);
}
}
#else
translator.Apply(skeleton);
translator.Apply(skeletonRenderer.Skeleton);
#endif
AfterAnimationApplied();
}
public virtual void AfterAnimationApplied () {
if (_UpdateLocal != null)
_UpdateLocal(this);
#if UNITY_EDITOR
private void EditorRebindAnimator () {
Animator translatorAnimator = translator.Animator;
if (translatorAnimator != null && !translatorAnimator.isInitialized)
translatorAnimator.Rebind();
}
#endif
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
// compatibility layer between 4.1 and 4.2, automatically transfer serialized attributes.
public override void UpgradeTo43 () {
if (!Application.isPlaying && !wasDeprecatedTransferred) {
UpgradeTo43Components();
TransferDeprecatedFields();
InitializeAnimationComponent();
}
if (_UpdateComplete != null)
_UpdateComplete(this);
}
public override void LateUpdate () {
if (updateTiming == UpdateTiming.InLateUpdate && valid && translator != null && translator.Animator != null)
UpdateAnimation(Time.deltaTime);
// instantiation can happen from Update() after this component, leading to a missing Update() call.
if (!wasUpdatedAfterInit) Update();
base.LateUpdate();
protected void UpgradeTo43Components () {
if (gameObject.GetComponent<SkeletonRenderer>() == null &&
gameObject.GetComponent<SkeletonGraphic>() == null) {
gameObject.AddComponent<SkeletonRenderer>();
Debug.Log(string.Format("{0}: Auto-migrated old SkeletonMecanim component to split SkeletonMecanim + SkeletonRenderer components.",
gameObject.name), gameObject);
EditorBridge.RequestMarkDirty(gameObject);
}
}
public override void OnBecameVisible () {
UpdateMode previousUpdateMode = updateMode;
updateMode = UpdateMode.FullUpdate;
/// <summary>Transfer of former base class SkeletonRenderer parameters.</summary>
protected void TransferDeprecatedFields () {
wasDeprecatedTransferred = true;
// OnBecameVisible is called after LateUpdate()
if (previousUpdateMode != UpdateMode.FullUpdate &&
previousUpdateMode != UpdateMode.EverythingExceptMesh)
Update();
if (previousUpdateMode != UpdateMode.FullUpdate)
LateUpdate();
SkeletonRenderer skeletonRenderer = gameObject.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
return;
skeletonRenderer.skeletonDataAsset = this.skeletonDataAssetDeprecated;
skeletonRenderer.initialSkinName = this.initialSkinNameDeprecated;
skeletonRenderer.EditorSkipSkinSync = this.editorSkipSkinSyncDeprecated;
skeletonRenderer.initialFlipX = this.initialFlipXDeprecated;
skeletonRenderer.initialFlipY = this.initialFlipYDeprecated;
skeletonRenderer.UpdateMode = this.updateModeDeprecated;
skeletonRenderer.updateWhenInvisible = this.updateWhenInvisibleDeprecated;
skeletonRenderer.separatorSlotNames = this.separatorSlotNamesDeprecated;
skeletonRenderer.MeshSettings.zSpacing = this.zSpacingDeprecated;
skeletonRenderer.MeshSettings.useClipping = this.useClippingDeprecated;
skeletonRenderer.MeshSettings.immutableTriangles = this.immutableTrianglesDeprecated;
skeletonRenderer.MeshSettings.pmaVertexColors = this.pmaVertexColorsDeprecated;
skeletonRenderer.MeshSettings.tintBlack = this.tintBlackDeprecated;
skeletonRenderer.MeshSettings.addNormals = this.addNormalsDeprecated;
skeletonRenderer.MeshSettings.calculateTangents = this.calculateTangentsDeprecated;
skeletonRenderer.clearStateOnDisable = this.clearStateOnDisableDeprecated;
skeletonRenderer.singleSubmesh = this.singleSubmeshDeprecated;
skeletonRenderer.maskInteraction = this.maskInteractionDeprecated;
}
[SerializeField] protected bool wasDeprecatedTransferred = false;
// SkeletonRenderer former base class parameters
[FormerlySerializedAs("skeletonDataAsset")] [SerializeField] private SkeletonDataAsset skeletonDataAssetDeprecated;
[FormerlySerializedAs("initialSkinName")] [SpineSkin(defaultAsEmptyString: true)] [SerializeField] private string initialSkinNameDeprecated;
[FormerlySerializedAs("editorSkipSkinSync")] [SerializeField] private bool editorSkipSkinSyncDeprecated = false;
[FormerlySerializedAs("initialFlipX")] [SerializeField] private bool initialFlipXDeprecated = false;
[FormerlySerializedAs("initialFlipY")] [SerializeField] private bool initialFlipYDeprecated = false;
[FormerlySerializedAs("updateMode")] [SerializeField] private UpdateMode updateModeDeprecated = UpdateMode.FullUpdate;
[FormerlySerializedAs("updateWhenInvisible")] [SerializeField] private UpdateMode updateWhenInvisibleDeprecated = UpdateMode.FullUpdate;
[UnityEngine.Serialization.FormerlySerializedAs("submeshSeparators"),
UnityEngine.Serialization.FormerlySerializedAs("separatorSlotNames")]
[SerializeField] private string[] separatorSlotNamesDeprecated = new string[0];
[FormerlySerializedAs("zSpacing")] [SerializeField] private float zSpacingDeprecated = 0f;
[FormerlySerializedAs("useClipping")] [SerializeField] private bool useClippingDeprecated = true;
[FormerlySerializedAs("immutableTriangles")] [SerializeField] private bool immutableTrianglesDeprecated = false;
[FormerlySerializedAs("pmaVertexColors")] [SerializeField] private bool pmaVertexColorsDeprecated = true;
[FormerlySerializedAs("clearStateOnDisable")] [SerializeField] private bool clearStateOnDisableDeprecated = false;
[FormerlySerializedAs("tintBlack")] [SerializeField] private bool tintBlackDeprecated = false;
[FormerlySerializedAs("singleSubmesh")] [SerializeField] private bool singleSubmeshDeprecated = false;
[FormerlySerializedAs("calculateNormals"),
FormerlySerializedAs("addNormals")]
[SerializeField] private bool addNormalsDeprecated = false;
[FormerlySerializedAs("calculateTangents")] [SerializeField] private bool calculateTangentsDeprecated = false;
#if BUILT_IN_SPRITE_MASK_COMPONENT
[FormerlySerializedAs("maskInteraction")] [SerializeField] private SpriteMaskInteraction maskInteractionDeprecated = SpriteMaskInteraction.None;
#endif // BUILT_IN_SPRITE_MASK_COMPONENT
#endif // UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
#endregion
[System.Serializable]
public class MecanimTranslator {
@ -216,6 +223,13 @@ namespace Spine.Unity {
readonly Dictionary<AnimationClip, int> clipNameHashCodeTable = new Dictionary<AnimationClip, int>(AnimationClipEqualityComparer.Instance);
readonly List<Animation> previousAnimations = new List<Animation>();
protected struct ClipInfo {
public Spine.Animation animation;
public float weight;
public float length;
public bool isLooping;
}
protected class ClipInfos {
public bool isInterruptionActive = false;
public bool isLastFrameOfInterruption = false;
@ -223,9 +237,9 @@ namespace Spine.Unity {
public int clipInfoCount = 0;
public int nextClipInfoCount = 0;
public int interruptingClipInfoCount = 0;
public readonly List<AnimatorClipInfo> clipInfos = new List<AnimatorClipInfo>();
public readonly List<AnimatorClipInfo> nextClipInfos = new List<AnimatorClipInfo>();
public readonly List<AnimatorClipInfo> interruptingClipInfos = new List<AnimatorClipInfo>();
public readonly List<ClipInfo> clipInfos = new List<ClipInfo>();
public readonly List<ClipInfo> nextClipInfos = new List<ClipInfo>();
public readonly List<ClipInfo> interruptingClipInfos = new List<ClipInfo>();
public float[] clipResolvedWeights = new float[0];
public float[] nextClipResolvedWeights = new float[0];
public float[] interruptingClipResolvedWeights = new float[0];
@ -235,8 +249,10 @@ namespace Spine.Unity {
public AnimatorStateInfo interruptingStateInfo;
public float interruptingClipTimeAddition = 0;
public float layerWeight = 0;
}
protected ClipInfos[] layerClipInfos = new ClipInfos[0];
protected int layerCount = 0;
Animator animator;
public Animator Animator { get { return this.animator; } }
@ -245,6 +261,8 @@ namespace Spine.Unity {
get {
if (!animator)
return 0;
if (!animator.isInitialized || !animator.isActiveAndEnabled || animator.runtimeAnimatorController == null)
return 0;
return animator.layerCount;
}
}
@ -275,28 +293,28 @@ namespace Spine.Unity {
ClearClipInfosForLayers();
}
private bool ApplyAnimation (Skeleton skeleton, AnimatorClipInfo info, AnimatorStateInfo stateInfo,
private bool ApplyAnimation (Skeleton skeleton, ClipInfo info, AnimatorStateInfo stateInfo,
int layerIndex, float layerWeight, MixBlend layerBlendMode,
bool useCustomClipWeight = false, float customClipWeight = 1.0f) {
float weight = info.weight * layerWeight;
if (weight < WeightEpsilon)
return false;
Animation clip = GetAnimation(info.clip);
if (clip == null)
if (info.animation == null)
return false;
float time = AnimationTime(stateInfo.normalizedTime, info.clip.length,
info.clip.isLooping, stateInfo.speed < 0);
float time = AnimationTime(stateInfo.normalizedTime, info.length,
info.isLooping, stateInfo.speed < 0);
weight = useCustomClipWeight ? layerWeight * customClipWeight : weight;
clip.Apply(skeleton, 0, time, info.clip.isLooping, null,
info.animation.Apply(skeleton, 0, time, info.isLooping, null,
weight, layerBlendMode, MixDirection.In, false);
if (_OnClipApplied != null)
OnClipAppliedCallback(clip, stateInfo, layerIndex, time, info.clip.isLooping, weight);
OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight);
return true;
}
private bool ApplyInterruptionAnimation (Skeleton skeleton,
bool interpolateWeightTo1, AnimatorClipInfo info, AnimatorStateInfo stateInfo,
bool interpolateWeightTo1, ClipInfo info, AnimatorStateInfo stateInfo,
int layerIndex, float layerWeight, MixBlend layerBlendMode, float interruptingClipTimeAddition,
bool useCustomClipWeight = false, float customClipWeight = 1.0f) {
@ -305,45 +323,43 @@ namespace Spine.Unity {
if (weight < WeightEpsilon)
return false;
Animation clip = GetAnimation(info.clip);
if (clip == null)
if (info.animation == null)
return false;
float time = AnimationTime(stateInfo.normalizedTime + interruptingClipTimeAddition,
info.clip.length, info.clip.isLooping, stateInfo.speed < 0);
info.length, stateInfo.speed < 0);
weight = useCustomClipWeight ? layerWeight * customClipWeight : weight;
clip.Apply(skeleton, 0, time, info.clip.isLooping, null,
info.animation.Apply(skeleton, 0, time, info.isLooping, null,
weight, layerBlendMode, MixDirection.In, false);
if (_OnClipApplied != null) {
OnClipAppliedCallback(clip, stateInfo, layerIndex, time, info.clip.isLooping, weight);
OnClipAppliedCallback(info.animation, stateInfo, layerIndex, time, info.isLooping, weight);
}
return true;
}
private void OnClipAppliedCallback (Spine.Animation clip, AnimatorStateInfo stateInfo,
private void OnClipAppliedCallback (Spine.Animation animation, AnimatorStateInfo stateInfo,
int layerIndex, float time, bool isLooping, float weight) {
float speedFactor = stateInfo.speedMultiplier * stateInfo.speed;
float lastTime = time - (Time.deltaTime * speedFactor);
float clipDuration = clip.Duration;
if (isLooping && clipDuration != 0) {
time %= clipDuration;
lastTime %= clipDuration;
if (isLooping && animation.Duration != 0) {
time %= animation.Duration;
lastTime %= animation.Duration;
}
_OnClipApplied(clip, layerIndex, weight, time, lastTime, speedFactor < 0);
_OnClipApplied(animation, layerIndex, weight, time, lastTime, speedFactor < 0);
}
public void Apply (Skeleton skeleton) {
public void GatherAnimatorState () {
#if UNITY_EDITOR
if (!Application.isPlaying) {
if (!ApplicationIsPlaying) {
GetLayerBlendModes();
}
#endif
if (layerMixModes.Length < animator.layerCount) {
layerCount = animator.layerCount;
if (layerMixModes.Length < layerCount) {
int oldSize = layerMixModes.Length;
System.Array.Resize<MixMode>(ref layerMixModes, animator.layerCount);
for (int layer = oldSize; layer < animator.layerCount; ++layer) {
System.Array.Resize<MixMode>(ref layerMixModes, layerCount);
for (int layer = oldSize; layer < layerCount; ++layer) {
bool isAdditiveLayer = false;
if (layer < layerBlendModes.Length)
isAdditiveLayer = layerBlendModes[layer] == MixBlend.Add;
@ -352,10 +368,12 @@ namespace Spine.Unity {
}
InitClipInfosForLayers();
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
for (int layer = 0, n = layerCount; layer < n; layer++) {
GetStateUpdatesFromAnimator(layer);
}
}
public void Apply (Skeleton skeleton) {
// Clear Previous
if (autoReset) {
List<Animation> previousAnimations = this.previousAnimations;
@ -363,54 +381,53 @@ namespace Spine.Unity {
previousAnimations[i].Apply(skeleton, 0, 0, false, null, 0, MixBlend.Setup, MixDirection.Out, false); // SetKeyedItemsToSetupPose
previousAnimations.Clear();
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
for (int layer = 0, n = layerCount; layer < n; layer++) {
ClipInfos layerInfos = layerClipInfos[layer];
float layerWeight = layerInfos.layerWeight;
if (layerWeight <= 0) continue;
AnimatorStateInfo nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
AnimatorStateInfo nextStateInfo = layerInfos.nextStateInfo;
bool hasNext = nextStateInfo.fullPathHash != 0;
int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
IList<AnimatorClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
IList<ClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
bool isInterruptionActive, shallInterpolateWeightTo1;
GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
out clipInfo, out nextClipInfo, out interruptingClipInfo, out shallInterpolateWeightTo1);
for (int c = 0; c < clipInfoCount; c++) {
AnimatorClipInfo info = clipInfo[c];
ClipInfo info = clipInfo[c];
float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue;
Spine.Animation clip = GetAnimation(info.clip);
if (clip != null)
previousAnimations.Add(clip);
if (info.animation != null)
previousAnimations.Add(info.animation);
}
if (hasNext) {
for (int c = 0; c < nextClipInfoCount; c++) {
AnimatorClipInfo info = nextClipInfo[c];
ClipInfo info = nextClipInfo[c];
float weight = info.weight * layerWeight; if (weight < WeightEpsilon) continue;
Spine.Animation clip = GetAnimation(info.clip);
if (clip != null)
previousAnimations.Add(clip);
if (info.animation != null)
previousAnimations.Add(info.animation);
}
}
if (isInterruptionActive) {
for (int c = 0; c < interruptingClipInfoCount; c++) {
AnimatorClipInfo info = interruptingClipInfo[c];
ClipInfo info = interruptingClipInfo[c];
float clipWeight = shallInterpolateWeightTo1 ? (info.weight + 1.0f) * 0.5f : info.weight;
float weight = clipWeight * layerWeight; if (weight < WeightEpsilon) continue;
Spine.Animation clip = GetAnimation(info.clip);
if (clip != null)
previousAnimations.Add(clip);
if (info.animation != null)
previousAnimations.Add(info.animation);
}
}
}
}
// Apply
for (int layer = 0, n = animator.layerCount; layer < n; layer++) {
float layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer); // Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
for (int layer = 0, n = layerCount; layer < n; layer++) {
ClipInfos layerInfos = layerClipInfos[layer];
float layerWeight = layerInfos.layerWeight;
bool isInterruptionActive;
AnimatorStateInfo stateInfo;
@ -422,7 +439,7 @@ namespace Spine.Unity {
bool hasNext = nextStateInfo.fullPathHash != 0;
int clipInfoCount, nextClipInfoCount, interruptingClipInfoCount;
IList<AnimatorClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
IList<ClipInfo> clipInfo, nextClipInfo, interruptingClipInfo;
bool interpolateWeightTo1;
GetAnimatorClipInfos(layer, out isInterruptionActive, out clipInfoCount, out nextClipInfoCount, out interruptingClipInfoCount,
out clipInfo, out nextClipInfo, out interruptingClipInfo, out interpolateWeightTo1);
@ -535,7 +552,7 @@ namespace Spine.Unity {
/// </summary>
protected void MatchWeights (ClipInfos clipInfos, bool hasNext, bool isInterruptionActive,
int clipInfoCount, int nextClipInfoCount, int interruptingClipInfoCount,
IList<AnimatorClipInfo> clipInfo, IList<AnimatorClipInfo> nextClipInfo, IList<AnimatorClipInfo> interruptingClipInfo) {
IList<ClipInfo> clipInfo, IList<ClipInfo> nextClipInfo, IList<ClipInfo> interruptingClipInfo) {
if (clipInfos.clipResolvedWeights.Length < clipInfoCount) {
System.Array.Resize<float>(ref clipInfos.clipResolvedWeights, clipInfoCount);
@ -575,41 +592,43 @@ namespace Spine.Unity {
ClipInfos layerInfos = layerClipInfos[layer];
bool isInterruptionActive = layerInfos.isInterruptionActive;
AnimationClip clip = null;
ClipInfo clipInfo;
Spine.Animation animation = null;
AnimatorStateInfo stateInfo;
if (isInterruptionActive && layerInfos.interruptingClipInfoCount > 0) {
clip = layerInfos.interruptingClipInfos[0].clip;
clipInfo = layerInfos.interruptingClipInfos[0];
stateInfo = layerInfos.interruptingStateInfo;
} else {
clip = layerInfos.clipInfos[0].clip;
} else if (layerInfos.clipInfoCount > 0) {
clipInfo = layerInfos.clipInfos[0];
stateInfo = layerInfos.stateInfo;
} else {
return new KeyValuePair<Spine.Animation, float>(null, 0);
}
animation = GetAnimation(clip);
float time = AnimationTime(stateInfo.normalizedTime, clip.length,
clip.isLooping, stateInfo.speed < 0);
animation = clipInfo.animation;
float time = AnimationTime(stateInfo.normalizedTime, clipInfo.length,
clipInfo.isLooping, stateInfo.speed < 0);
return new KeyValuePair<Animation, float>(animation, time);
}
static float AnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) {
float time = ToSpineAnimationTime(normalizedTime, clipLength, loop, reversed);
float time = AnimationTime(normalizedTime, clipLength, reversed);
if (loop) return time;
const float EndSnapEpsilon = 1f / 30f; // Workaround for end-duration keys not being applied.
return (clipLength - time < EndSnapEpsilon) ? clipLength : time; // return a time snapped to clipLength;
}
static float ToSpineAnimationTime (float normalizedTime, float clipLength, bool loop, bool reversed) {
static float AnimationTime (float normalizedTime, float clipLength, bool reversed) {
if (reversed)
normalizedTime = (1 - normalizedTime);
if (normalizedTime < 0.0f)
normalizedTime = loop ? (normalizedTime % 1.0f) + 1.0f : 0.0f;
normalizedTime = (normalizedTime % 1.0f) + 1.0f;
return normalizedTime * clipLength;
}
void InitClipInfosForLayers () {
if (layerClipInfos.Length < animator.layerCount) {
System.Array.Resize<ClipInfos>(ref layerClipInfos, animator.layerCount);
for (int layer = 0, n = animator.layerCount; layer < n; ++layer) {
if (layerClipInfos.Length < layerCount) {
System.Array.Resize<ClipInfos>(ref layerClipInfos, layerCount);
for (int layer = 0, n = layerCount; layer < n; ++layer) {
if (layerClipInfos[layer] == null)
layerClipInfos[layer] = new ClipInfos();
}
@ -648,15 +667,15 @@ namespace Spine.Unity {
#if UNITY_EDITOR
void GetLayerBlendModes () {
if (layerBlendModes.Length < animator.layerCount) {
System.Array.Resize<MixBlend>(ref layerBlendModes, animator.layerCount);
if (layerBlendModes.Length < layerCount) {
System.Array.Resize<MixBlend>(ref layerBlendModes, layerCount);
}
for (int layer = 0, n = animator.layerCount; layer < n; ++layer) {
AnimatorController controller = animator.runtimeAnimatorController as UnityEditor.Animations.AnimatorController;
for (int layer = 0, n = layerCount; layer < n; ++layer) {
AnimatorController controller = animator.runtimeAnimatorController as AnimatorController;
if (controller != null) {
layerBlendModes[layer] = MixBlend.First;
if (layer > 0) {
layerBlendModes[layer] = controller.layers[layer].blendingMode == UnityEditor.Animations.AnimatorLayerBlendingMode.Additive ?
layerBlendModes[layer] = controller.layers[layer].blendingMode == AnimatorLayerBlendingMode.Additive ?
MixBlend.Add : MixBlend.Replace;
}
}
@ -667,12 +686,16 @@ namespace Spine.Unity {
void GetStateUpdatesFromAnimator (int layer) {
ClipInfos layerInfos = layerClipInfos[layer];
// Note: Animator.GetLayerWeight always returns 0 on the first layer. Should be interpreted as 1.
layerInfos.layerWeight = (layer == 0) ? 1 : animator.GetLayerWeight(layer);
int clipInfoCount = animator.GetCurrentAnimatorClipInfoCount(layer);
int nextClipInfoCount = animator.GetNextAnimatorClipInfoCount(layer);
List<AnimatorClipInfo> clipInfos = layerInfos.clipInfos;
List<AnimatorClipInfo> nextClipInfos = layerInfos.nextClipInfos;
List<AnimatorClipInfo> interruptingClipInfos = layerInfos.interruptingClipInfos;
List<ClipInfo> clipInfos = layerInfos.clipInfos;
List<ClipInfo> nextClipInfos = layerInfos.nextClipInfos;
List<ClipInfo> interruptingClipInfos = layerInfos.interruptingClipInfos;
layerInfos.isInterruptionActive = (clipInfoCount == 0 && clipInfos.Count != 0 &&
nextClipInfoCount == 0 && nextClipInfos.Count != 0);
@ -689,7 +712,20 @@ namespace Spine.Unity {
AnimatorStateInfo interruptingStateInfo = animator.GetNextAnimatorStateInfo(layer);
layerInfos.isLastFrameOfInterruption = interruptingStateInfo.fullPathHash == 0;
if (!layerInfos.isLastFrameOfInterruption) {
animator.GetNextAnimatorClipInfo(layer, interruptingClipInfos);
List<AnimatorClipInfo> tempInterruptingClipInfos = new List<AnimatorClipInfo>();
animator.GetNextAnimatorClipInfo(layer, tempInterruptingClipInfos);
interruptingClipInfos.Clear();
for (int i = 0; i < tempInterruptingClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempInterruptingClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
interruptingClipInfos.Add(info);
}
layerInfos.interruptingClipInfoCount = interruptingClipInfos.Count;
float oldTime = layerInfos.interruptingStateInfo.normalizedTime;
float newTime = interruptingStateInfo.normalizedTime;
@ -705,8 +741,33 @@ namespace Spine.Unity {
if (clipInfos.Capacity < clipInfoCount) clipInfos.Capacity = clipInfoCount;
if (nextClipInfos.Capacity < nextClipInfoCount) nextClipInfos.Capacity = nextClipInfoCount;
animator.GetCurrentAnimatorClipInfo(layer, clipInfos);
animator.GetNextAnimatorClipInfo(layer, nextClipInfos);
// Get current clip infos and extract data
List<AnimatorClipInfo> tempClipInfos = new List<AnimatorClipInfo>();
animator.GetCurrentAnimatorClipInfo(layer, tempClipInfos);
clipInfos.Clear();
for (int i = 0; i < tempClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
clipInfos.Add(info);
}
// Get next clip infos and extract data
List<AnimatorClipInfo> tempNextClipInfos = new List<AnimatorClipInfo>();
animator.GetNextAnimatorClipInfo(layer, tempNextClipInfos);
nextClipInfos.Clear();
for (int i = 0; i < tempNextClipInfos.Count; i++) {
AnimatorClipInfo animatorInfo = tempNextClipInfos[i];
ClipInfo info = new ClipInfo();
info.animation = GetAnimation(animatorInfo.clip);
info.weight = animatorInfo.weight;
info.length = animatorInfo.clip.length;
info.isLooping = animatorInfo.clip.isLooping;
nextClipInfos.Add(info);
}
layerInfos.stateInfo = animator.GetCurrentAnimatorStateInfo(layer);
layerInfos.nextStateInfo = animator.GetNextAnimatorStateInfo(layer);
@ -719,9 +780,9 @@ namespace Spine.Unity {
out int clipInfoCount,
out int nextClipInfoCount,
out int interruptingClipInfoCount,
out IList<AnimatorClipInfo> clipInfo,
out IList<AnimatorClipInfo> nextClipInfo,
out IList<AnimatorClipInfo> interruptingClipInfo,
out IList<ClipInfo> clipInfo,
out IList<ClipInfo> nextClipInfo,
out IList<ClipInfo> interruptingClipInfo,
out bool shallInterpolateWeightTo1) {
ClipInfos layerInfos = layerClipInfos[layer];
@ -777,6 +838,5 @@ namespace Spine.Unity {
public int GetHashCode (int o) { return o; }
}
}
}
}

View File

@ -112,7 +112,8 @@ namespace Spine.Unity {
meshGenerator.BuildMeshWithArrays(currentInstructions, updateTriangles);
}
buffers.UpdateSharedMaterials(currentInstructions.submeshInstructions);
bool materialsChanged;
buffers.GatherMaterialsFromInstructions(currentInstructions.submeshInstructions, out materialsChanged);
// STEP 3: modify mesh.
Mesh mesh = smartMesh.mesh;
@ -124,9 +125,9 @@ namespace Spine.Unity {
meshGenerator.FillVertexData(mesh);
if (updateTriangles) {
meshGenerator.FillTriangles(mesh);
meshRenderer.sharedMaterials = buffers.GetUpdatedSharedMaterialsArray();
} else if (buffers.MaterialsChangedInLastUpdate()) {
meshRenderer.sharedMaterials = buffers.GetUpdatedSharedMaterialsArray();
meshRenderer.sharedMaterials = buffers.UpdateSharedMaterialsArray();
} else if (materialsChanged) {
meshRenderer.sharedMaterials = buffers.UpdateSharedMaterialsArray();
}
meshGenerator.FillLateVertexData(mesh);
}

View File

@ -35,10 +35,13 @@
#define HAS_PROPERTY_BLOCK_QUERY
#endif
#define SPINE_OPTIONAL_RENDEROVERRIDE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -48,7 +51,7 @@ namespace Spine.Unity {
[ExecuteInEditMode]
#endif
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRenderSeparator")]
public class SkeletonRenderSeparator : MonoBehaviour {
public class SkeletonRenderSeparator : MonoBehaviour, IUpgradable {
public const int DefaultSortingOrderIncrement = 5;
#region Inspector
@ -57,10 +60,8 @@ namespace Spine.Unity {
public SkeletonRenderer SkeletonRenderer {
get { return skeletonRenderer; }
set {
#if SPINE_OPTIONAL_RENDEROVERRIDE
if (skeletonRenderer != null)
skeletonRenderer.GenerateMeshOverride -= HandleRender;
#endif
skeletonRenderer = value;
if (value == null)
@ -86,7 +87,7 @@ namespace Spine.Unity {
#region Callback Delegates
/// <summary>OnMeshAndMaterialsUpdated is called at the end of LateUpdate after the Mesh and
/// all materials have been updated.</summary>
public event SkeletonRenderer.SkeletonRendererDelegate OnMeshAndMaterialsUpdated;
public event SkeletonRendererDelegate OnMeshAndMaterialsUpdated;
#endregion
#region Runtime Instantiation
@ -130,7 +131,7 @@ namespace Spine.Unity {
if (!Application.isPlaying) {
skeletonRenderer.enabled = false;
skeletonRenderer.enabled = true;
skeletonRenderer.LateUpdateMesh();
skeletonRenderer.UpdateMesh();
}
#endif
@ -162,15 +163,22 @@ namespace Spine.Unity {
}
#endregion
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
public void OnEnable () {
if (skeletonRenderer == null) return;
if (copiedBlock == null) copiedBlock = new MaterialPropertyBlock();
mainMeshRenderer = skeletonRenderer.GetComponent<MeshRenderer>();
#if SPINE_OPTIONAL_RENDEROVERRIDE
skeletonRenderer.enableSeparatorSlots = true;
skeletonRenderer.GenerateMeshOverride -= HandleRender;
skeletonRenderer.GenerateMeshOverride += HandleRender;
#endif
if (copyMeshRendererFlags) {
var lightProbeUsage = mainMeshRenderer.lightProbeUsage;
@ -181,10 +189,10 @@ namespace Spine.Unity {
var probeAnchor = mainMeshRenderer.probeAnchor;
for (int i = 0; i < partsRenderers.Count; i++) {
var currentRenderer = partsRenderers[i];
SkeletonPartsRenderer currentRenderer = partsRenderers[i];
if (currentRenderer == null) continue; // skip null items.
var mr = currentRenderer.MeshRenderer;
MeshRenderer mr = currentRenderer.MeshRenderer;
mr.lightProbeUsage = lightProbeUsage;
mr.receiveShadows = receiveShadows;
mr.reflectionProbeUsage = reflectionProbeUsage;
@ -195,7 +203,7 @@ namespace Spine.Unity {
}
if (skeletonRenderer.updateWhenInvisible != UpdateMode.FullUpdate)
skeletonRenderer.LateUpdateMesh();
skeletonRenderer.UpdateMesh();
}
public void Update () {
@ -204,10 +212,11 @@ namespace Spine.Unity {
public void OnDisable () {
if (skeletonRenderer == null) return;
#if SPINE_OPTIONAL_RENDEROVERRIDE
skeletonRenderer.enableSeparatorSlots = false;
skeletonRenderer.GenerateMeshOverride -= HandleRender;
#endif
skeletonRenderer.LateUpdateMesh();
skeletonRenderer.UpdateMesh();
ClearPartsRendererMeshes();
}
@ -244,14 +253,15 @@ namespace Spine.Unity {
if (assignPropertyBlock)
mainMeshRenderer.GetPropertyBlock(copiedBlock);
MeshGenerator.Settings originalSettings = skeletonRenderer.MeshSettings;
MeshGenerator.Settings settings = new MeshGenerator.Settings {
addNormals = skeletonRenderer.addNormals,
calculateTangents = skeletonRenderer.calculateTangents,
addNormals = originalSettings.addNormals,
calculateTangents = originalSettings.calculateTangents,
immutableTriangles = false, // parts cannot do immutable triangles.
pmaVertexColors = skeletonRenderer.pmaVertexColors,
tintBlack = skeletonRenderer.tintBlack,
pmaVertexColors = originalSettings.pmaVertexColors,
tintBlack = originalSettings.tintBlack,
useClipping = true,
zSpacing = skeletonRenderer.zSpacing
zSpacing = originalSettings.zSpacing
};
ExposedList<SubmeshInstruction> submeshInstructions = instruction.submeshInstructions;
@ -301,5 +311,21 @@ namespace Spine.Unity {
partsRenderer.ClearMesh();
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonRenderSeparator, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -0,0 +1,731 @@
/******************************************************************************
* 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.
*****************************************************************************/
#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#endif
#if UNITY_2018_1_OR_NEWER
#define PER_MATERIAL_PROPERTY_BLOCKS
#endif
#if UNITY_2017_1_OR_NEWER
#define BUILT_IN_SPRITE_MASK_COMPONENT
#endif
#if UNITY_2019_3_OR_NEWER
#define CONFIGURABLE_ENTER_PLAY_MODE
#endif
#define USE_THREADED_SKELETON_UPDATE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
#define SPINE_OPTIONAL_ON_DEMAND_LOADING
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using UnityEditor.SceneManagement;
#endif
namespace Spine.Unity {
// Partial class: covers common identical attributes, properties and methods shared across all
// ISkeletonRenderer subclasses. This is a workaround for single inheritance limitations and covers code which
// would otherwise be located in a base-class of SkeletonRenderer.
public partial class SkeletonRenderer : MonoBehaviour, ISkeletonRenderer, IHasSkeletonRenderer {
// Identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
#region Identical common ISkeletonRenderer code
#region ISkeletonRenderer Attributes
// Core Attributes
public ISkeletonAnimation skeletonAnimation;
public SkeletonDataAsset skeletonDataAsset;
[System.NonSerialized] public Skeleton skeleton;
[System.NonSerialized] public bool valid;
protected bool wasMeshUpdatedAfterInit = false;
protected bool updateTriangles = true;
// Initialization Settings
/// <summary>Skin name to use when the Skeleton is initialized.</summary>
[SpineSkin(dataField: "skeletonDataAsset", defaultAsEmptyString: true)] public string initialSkinName;
/// <summary>Flip X and Y to use when the Skeleton is initialized.</summary>
public bool initialFlipX, initialFlipY;
// Render Settings
[SerializeField] protected MeshGenerator.Settings meshSettings = MeshGenerator.Settings.Default;
[System.NonSerialized] protected readonly SkeletonRendererInstruction currentInstructions = new SkeletonRendererInstruction();
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
protected UpdateMode updateMode = UpdateMode.FullUpdate;
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode updateWhenInvisible = UpdateMode.FullUpdate;
/// <summary>Clears the state of the render and skeleton when this component or its GameObject is disabled. This prevents previous state from being retained when it is enabled again. When pooling your skeleton, setting this to true can be helpful.</summary>
public bool clearStateOnDisable = false;
// Submesh Separation
/// <summary>Slot names used to populate separatorSlots list when the Skeleton is initialized. Changing this after initialization does nothing.</summary>
[SpineSlot] public string[] separatorSlotNames = new string[0];
/// <summary>Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects.</summary>
[System.NonSerialized] public readonly List<Slot> separatorSlots = new List<Slot>();
public bool enableSeparatorSlots = false;
// Overrides Attributes
// These are API for anything that wants to take over rendering for a SkeletonRenderer.
public bool disableRenderingOnOverride = true;
event InstructionDelegate generateMeshOverride;
[System.NonSerialized] readonly Dictionary<Slot, Material> customSlotMaterials = new Dictionary<Slot, Material>();
[System.NonSerialized] protected bool materialsNeedUpdate = false;
// Physics Attributes
/// <seealso cref="PhysicsPositionInheritanceFactor"/>
[SerializeField] protected Vector2 physicsPositionInheritanceFactor = Vector2.one;
/// <seealso cref="PhysicsRotationInheritanceFactor"/>
[SerializeField] protected float physicsRotationInheritanceFactor = 1.0f;
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
[SerializeField] protected Transform physicsMovementRelativeTo = null;
/// <summary>Used for applying Transform translation to skeleton PhysicsConstraints.</summary>
protected Vector3 lastPosition;
/// <summary>Used for applying Transform rotation to skeleton PhysicsConstraints.</summary>
protected float lastRotation;
/// <summary>Position delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected Vector3 positionDelta;
/// <summary>Rotation delta for threaded processing as Transform access from worker thread is not allowed.</summary>
protected float rotationDelta;
// Threaded Update System Attributes
#if USE_THREADED_SKELETON_UPDATE
protected static int mainThreadID = -1;
[SerializeField] protected SettingsTriState threadedMeshGeneration = SettingsTriState.UseGlobalSetting;
protected bool isUpdatedExternally = false;
protected bool requiresMeshBufferAssignmentMainThread = false;
#if UNITY_EDITOR
static bool applicationIsPlaying = false;
#endif
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
protected bool editorSkipSkinSync = false;
protected bool requiresEditorUpdate = false;
#endif
#endregion ISkeletonRenderer Attributes
#region ISkeletonRenderer Properties
public ISkeletonRenderer Renderer { get { return this; } }
public UnityEngine.MonoBehaviour Component { get { return this; } }
public ISkeletonAnimation Animation { get { return skeletonAnimation; } set { skeletonAnimation = value; } }
public SkeletonDataAsset SkeletonDataAsset {
get { return skeletonDataAsset; }
set { skeletonDataAsset = value; }
}
public Skeleton Skeleton {
get {
Initialize(false);
return skeleton;
}
set {
skeleton = value;
}
}
public SkeletonData SkeletonData {
get {
Initialize(false);
return skeleton == null ? null : skeleton.Data;
}
}
public bool IsValid { get { return valid; } }
public string InitialSkinName { get { return initialSkinName; } set { initialSkinName = value; } }
/// <summary>Flip X to use when the Skeleton is initialized.</summary>
public bool InitialFlipX { get { return initialFlipX; } set { initialFlipX = value; } }
/// <summary>Flip Y to use when the Skeleton is initialized.</summary>
public bool InitialFlipY { get { return initialFlipY; } set { initialFlipY = value; } }
/// <summary>Update mode to optionally limit updates to e.g. only apply animations but not update the mesh.</summary>
public UpdateMode UpdateMode { get { return updateMode; } set { updateMode = value; } }
/// <summary>Update mode used when the MeshRenderer becomes invisible
/// (when <c>OnBecameInvisible()</c> is called). Update mode is automatically
/// reset to <c>UpdateMode.FullUpdate</c> when the mesh becomes visible again.</summary>
public UpdateMode UpdateWhenInvisible { get { return updateWhenInvisible; } set { updateWhenInvisible = value; } }
public MeshGenerator.Settings MeshSettings { get { return meshSettings; } set { meshSettings = value; } }
// Submesh Separation
/// <summary>Slots that determine where the render is split. This is used by components such as SkeletonRenderSeparator so that the skeleton can be rendered by two separate renderers on different GameObjects.</summary>
public List<Slot> SeparatorSlots { get { return separatorSlots; } }
public bool EnableSeparatorSlots { get { return enableSeparatorSlots; } set { enableSeparatorSlots = value; } }
// Overrides Properties
public bool HasGenerateMeshOverride { get { return generateMeshOverride != null; } }
/// <summary>Allows separate code to take over rendering for this SkeletonRenderer component. The subscriber is passed a SkeletonRendererInstruction argument to determine how to render a skeleton.</summary>
public event InstructionDelegate GenerateMeshOverride {
add {
generateMeshOverride += value;
if (disableRenderingOnOverride && generateMeshOverride != null) {
Initialize(false);
DisableRenderers();
updateMode = UpdateMode.FullUpdate;
}
}
remove {
generateMeshOverride -= value;
if (disableRenderingOnOverride && generateMeshOverride == null) {
Initialize(false);
EnableRenderers();
}
}
}
/// <summary>Use this Dictionary to use a different Material to render specific Slots.</summary>
public Dictionary<Slot, Material> CustomSlotMaterials {
get { materialsNeedUpdate = true; return customSlotMaterials; }
}
// Physics Properties
/// <summary>When set to non-zero, Transform position movement in X and Y direction
/// is applied to skeleton PhysicsConstraints, multiplied by this scale factor.
/// Typical values are <c>Vector2.one</c> to apply XY movement 1:1,
/// <c>Vector2(2f, 2f)</c> to apply movement with double intensity,
/// <c>Vector2(1f, 0f)</c> to apply only horizontal movement, or
/// <c>Vector2.zero</c> to not apply any Transform position movement at all.</summary>
public Vector2 PhysicsPositionInheritanceFactor {
get {
return physicsPositionInheritanceFactor;
}
set {
if (physicsPositionInheritanceFactor == Vector2.zero && value != Vector2.zero) ResetLastPosition();
physicsPositionInheritanceFactor = value;
}
}
/// <summary>When set to non-zero, Transform rotation movement is applied to skeleton PhysicsConstraints,
/// multiplied by this scale factor. Typical values are <c>1</c> to apply movement 1:1,
/// <c>2</c> to apply movement with double intensity, or
/// <c>0</c> to not apply any Transform rotation movement at all.</summary>
public float PhysicsRotationInheritanceFactor {
get {
return physicsRotationInheritanceFactor;
}
set {
if (physicsRotationInheritanceFactor == 0f && value != 0f) ResetLastRotation();
physicsRotationInheritanceFactor = value;
}
}
/// <summary>Reference transform relative to which physics movement will be calculated, or null to use world location.</summary>
public Transform PhysicsMovementRelativeTo {
get {
return physicsMovementRelativeTo;
}
set {
physicsMovementRelativeTo = value;
if (physicsPositionInheritanceFactor != Vector2.zero) ResetLastPosition();
if (physicsRotationInheritanceFactor != 0f) ResetLastRotation();
}
}
// Threaded Update System Properties
#if USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return isUpdatedExternally; }
set { isUpdatedExternally = value; }
}
protected bool UsesThreadedMeshGeneration {
get { return threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration; }
}
public bool RequiresMeshBufferAssignmentMainThread {
get { return requiresMeshBufferAssignmentMainThread; }
}
public SettingsTriState ThreadedMeshGeneration {
get { return threadedMeshGeneration; }
set {
if (threadedMeshGeneration == value) return;
threadedMeshGeneration = value;
SkeletonUpdateSystem system = SkeletonUpdateSystem.Instance;
if (system) {
if (threadedMeshGeneration == SettingsTriState.Enable || RuntimeSettings.UseThreadedMeshGeneration)
system.RegisterForUpdate(this);
else
system.UnregisterFromUpdate(this);
}
}
}
#if UNITY_EDITOR
// ApplicationIsPlaying for threaded access. Unfortunately Application.isPlaying throws
// when called from worker thread.
public static bool ApplicationIsPlaying {
get { return applicationIsPlaying; }
set { applicationIsPlaying = value; }
}
#endif
#else // USE_THREADED_SKELETON_UPDATE
public bool IsUpdatedExternally {
get { return false; }
set { }
}
#if UNITY_EDITOR
public static bool ApplicationIsPlaying {
get { return Application.isPlaying; }
set { }
}
#endif
public bool RequiresMeshBufferAssignmentMainThread { get { return true; } }
#endif // USE_THREADED_SKELETON_UPDATE
#if UNITY_EDITOR
/// <summary>Enable this parameter when overwriting the Skeleton's skin from an editor script.
/// Otherwise any changes will be overwritten by the next inspector update.</summary>
public bool EditorSkipSkinSync {
get { return editorSkipSkinSync; }
set { editorSkipSkinSync = value; }
}
#endif
#endregion ISkeletonRenderer Properties
#region ISkeletonRenderer Methods
protected virtual void ClearCommon () {
// Note: do not reset meshFilter.sharedMesh or meshRenderer.sharedMaterial to null,
// otherwise constant reloading will be triggered at prefabs.
currentInstructions.Clear();
rendererBuffers.Clear();
ClearMeshGenerator();
skeleton = null;
valid = false;
if (skeletonAnimation != null)
skeletonAnimation.ClearAnimationState();
}
protected virtual void InitializeCommon (bool overwrite, bool quiet = false) {
if (valid && !overwrite)
return;
if (skeletonDataAsset == null || skeletonDataAsset.skeletonJSON == null) {
Clear();
return;
} else if (skeleton != null && skeletonDataAsset.GetSkeletonData(true) != skeleton.Data) {
Clear();
} else if (this.Freeze) {
return;
} else {
ClearCommon();
}
SkeletonData skeletonData = skeletonDataAsset.GetSkeletonData(quiet);
if (skeletonData == null) return;
rendererBuffers.Initialize();
skeleton = new Skeleton(skeletonData) {
ScaleX = initialFlipX ? -1 : 1,
ScaleY = initialFlipY ? -1 : 1
};
valid = true;
ResetLastPositionAndRotation();
AssignInitialSkin();
separatorSlots.Clear();
for (int i = 0; i < separatorSlotNames.Length; i++)
separatorSlots.Add(skeleton.FindSlot(separatorSlotNames[i]));
AfterAnimationApplied();
wasMeshUpdatedAfterInit = false;
if (OnRebuild != null)
OnRebuild(this);
#if UNITY_EDITOR
if (!Application.isPlaying) {
string errorMessage = null;
if (!quiet && MaterialChecks.IsMaterialSetupProblematic(this, ref errorMessage))
Debug.LogWarningFormat(this, "Problematic material setup at {0}: {1}", this.name, errorMessage);
}
#endif
}
protected virtual void AssignInitialSkin () {
if (string.IsNullOrEmpty(initialSkinName) || string.Equals(initialSkinName, "default", System.StringComparison.Ordinal))
skeleton.SetSkin((Skin)null);
else
skeleton.SetSkin(initialSkinName);
}
public void ResetLastPosition () {
lastPosition = GetPhysicsTransformPosition();
}
public void ResetLastRotation () {
lastRotation = GetPhysicsTransformRotation();
}
public void ResetLastPositionAndRotation () {
lastPosition = GetPhysicsTransformPosition();
lastRotation = GetPhysicsTransformRotation();
}
/// <summary>
/// Gathers Transform movement for later application in <see cref="ApplyTransformMovementToPhysics"/>.
/// Must be called in main thread.
/// </summary>
public virtual void GatherTransformMovementForPhysics () {
#if UNITY_EDITOR
bool isPlaying = ApplicationIsPlaying;
#else
bool isPlaying = true;
#endif
if (isPlaying) {
if (physicsPositionInheritanceFactor != Vector2.zero) {
Vector3 position = GetPhysicsTransformPosition();
positionDelta = (position - lastPosition) / this.MeshScale;
positionDelta = transform.InverseTransformVector(positionDelta);
if (physicsMovementRelativeTo != null) {
positionDelta = physicsMovementRelativeTo.TransformVector(positionDelta);
}
positionDelta.x *= physicsPositionInheritanceFactor.x;
positionDelta.y *= physicsPositionInheritanceFactor.y;
lastPosition = position;
}
if (physicsRotationInheritanceFactor != 0f) {
float rotation = GetPhysicsTransformRotation();
rotationDelta = rotation - lastRotation;
lastRotation = rotation;
}
}
}
/// <summary>
/// Applies position and rotation Transform movement previously gathered via
/// <see cref="GatherTransformMovementForPhysics"/>. May be called in worker thread.
/// </summary>
public virtual void ApplyTransformMovementToPhysics () {
if (physicsPositionInheritanceFactor != Vector2.zero) {
skeleton.PhysicsTranslate(positionDelta.x, positionDelta.y);
}
if (physicsRotationInheritanceFactor != 0f) {
skeleton.PhysicsRotate(0, 0, physicsRotationInheritanceFactor * rotationDelta);
}
}
protected Vector3 GetPhysicsTransformPosition () {
if (physicsMovementRelativeTo == null) {
return transform.position;
} else {
if (physicsMovementRelativeTo == transform.parent)
return transform.localPosition;
else
return physicsMovementRelativeTo.InverseTransformPoint(transform.position);
}
}
protected float GetPhysicsTransformRotation () {
if (physicsMovementRelativeTo == null) {
return this.transform.rotation.eulerAngles.z;
} else {
if (physicsMovementRelativeTo == this.transform.parent)
return this.transform.localRotation.eulerAngles.z;
else {
Quaternion relative = Quaternion.Inverse(physicsMovementRelativeTo.rotation) * this.transform.rotation;
return relative.eulerAngles.z;
}
}
}
public virtual void AfterAnimationApplied (bool calledFromMainThread = true) {
if (_UpdateLocal != null)
_UpdateLocal(this);
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
} else {
UpdateWorldTransform(Physics.Pose);
_UpdateWorld(this);
UpdateWorldTransform(Physics.Update);
}
if (calledFromMainThread && _UpdateComplete != null) {
_UpdateComplete(this);
}
}
public CoroutineIterator AfterAnimationAppliedSplit (CoroutineIterator coroutineIterator) {
if (coroutineIterator.IsDone)
return CoroutineIterator.Done;
/*
0:
if (_UpdateLocal != null) {
yield return true; // continue in main thread
1:
_UpdateLocal(this);
yield return false; // continue in worker thread
}
2:
if (_UpdateWorld == null) {
UpdateWorldTransform(Physics.Update);
// goto 5
} else {
UpdateWorldTransform(Physics.Pose);
yield return true; // continue in main thread
3:
_UpdateWorld(this);
yield return false; // continue in worker thread
4:
UpdateWorldTransform(Physics.Update);
// goto 5
}
5:
if (_UpdateComplete != null) {
yield return true; // continue in main thread
6:
_UpdateComplete(this);
// last call, no need to switch back to worker thread.
}
*/
const int StateBits = 3;
const uint StateMask = (1 << StateBits) - 1;
switch (coroutineIterator.State(StateMask)) {
case 0:
if (_UpdateLocal != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(1, StateMask);
} else {
goto case 2;
}
case 1:
AssertIsMainThread();
_UpdateLocal(this);
return coroutineIterator.YieldReturnAtState(2, StateMask);
case 2:
if (_UpdateWorld == null) {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
} else {
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Pose);
return coroutineIterator.YieldReturnAtState(3, StateMask);
}
case 3:
AssertIsMainThread();
_UpdateWorld(this);
return coroutineIterator.YieldReturnAtState(4, StateMask);
case 4:
AssertIsWorkerThread();
UpdateWorldTransform(Physics.Update);
goto case 5;
case 5:
if (_UpdateComplete != null) {
AssertIsWorkerThread();
return coroutineIterator.YieldReturnAtState(6, StateMask);
} else {
return CoroutineIterator.Done;
}
case 6:
AssertIsMainThread();
_UpdateComplete(this);
return CoroutineIterator.Done;
default:
Debug.LogError(string.Format(
"Internal coroutine logic error: SkeletonRenderer.AfterAnimationAppliedSplit state was {0}.",
coroutineIterator.State(StateMask)), this);
return CoroutineIterator.Done;
}
}
protected void IssueOnPostProcessVertices (MeshGeneratorBuffers buffers) {
if (OnPostProcessVertices != null)
OnPostProcessVertices.Invoke(buffers);
}
protected virtual void UpdateWorldTransform (Physics physics) {
skeleton.UpdateWorldTransform(physics);
}
public virtual void UpdateMesh (bool calledFromMainThread = true) {
#if USE_THREADED_SKELETON_UPDATE
bool canPrepareInstructions = calledFromMainThread || !NeedsMainThreadRendererPreparation;
#else
bool canPrepareInstructions = true;
#endif
if (canPrepareInstructions)
PrepareInstructionsAndRenderers();
updateTriangles = UpdateBuffersToInstructions(calledFromMainThread);
#if USE_THREADED_SKELETON_UPDATE
if (calledFromMainThread) {
requiresMeshBufferAssignmentMainThread = false;
UpdateMeshAndMaterialsToBuffers();
} else {
requiresMeshBufferAssignmentMainThread = true;
}
#else
UpdateMeshAndMaterialsToBuffers();
#endif
}
/// <returns>True if triangles (indices array) need to be updated.</returns>
public virtual bool UpdateBuffersToInstructions (bool calledFromMainThread = true) {
if (!valid || currentInstructions.rawVertexCount < 0) return false;
wasMeshUpdatedAfterInit = true;
if (this.generateMeshOverride != null) {
if (calledFromMainThread)
this.generateMeshOverride(currentInstructions);
if (disableRenderingOnOverride) return false;
}
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetNextMesh(); // Double-buffer for performance.
// Update vertex buffers based on vertices from the attachments and assign buffers to a target UnityEngine.Mesh.
bool updateTriangles = SkeletonRendererInstruction.GeometryNotEqual(currentInstructions, currentSmartMesh.instructionUsed, calledFromMainThread);
FillBuffersFromSubmeshInstructions(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
return updateTriangles;
}
public virtual void UpdateMeshAndMaterialsToBuffers () {
ExposedList<SubmeshInstruction> workingSubmeshInstructions = currentInstructions.submeshInstructions;
MeshRendererBuffers.SmartMesh currentSmartMesh = rendererBuffers.GetCurrentMesh();
UpdateMeshAndMaterialsToBuffers(workingSubmeshInstructions, currentSmartMesh, updateTriangles);
}
protected virtual void UpdateMeshAndMaterialsToBuffers (
ExposedList<SubmeshInstruction> workingSubmeshInstructions, MeshRendererBuffers.SmartMesh currentSmartMesh,
bool updateTriangles) {
FillMeshFromBuffers(currentSmartMesh, updateTriangles);
bool materialsChanged;
rendererBuffers.GatherMaterialsFromInstructions(workingSubmeshInstructions, out materialsChanged);
if (materialsChanged || materialsNeedUpdate) {
UpdateUsedMaterialsForRenderers(workingSubmeshInstructions);
}
#if SPINE_OPTIONAL_ON_DEMAND_LOADING
if (Application.isPlaying)
HandleOnDemandLoading();
#endif
// The UnityEngine.Mesh is ready. Set it as the MeshFilter's mesh. Store the instructions used for that mesh.
AssignMeshAtRenderer(workingSubmeshInstructions, currentSmartMesh);
if (OnMeshAndMaterialsUpdated != null)
OnMeshAndMaterialsUpdated(this);
}
public virtual void UpdateMaterials () {
UpdateUsedMaterialsForRenderers(currentInstructions.submeshInstructions);
}
// Threading Asserts
[System.Diagnostics.Conditional("UNITY_EDITOR")]
protected void InitializeMainThreadID () {
#if USE_THREADED_SKELETON_UPDATE
if (mainThreadID == -1)
mainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsMainThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId != mainThreadID)
Debug.LogError("AssertIsMainThread failed: worker thread calling main thread code. Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private void AssertIsWorkerThread () {
#if USE_THREADED_SKELETON_UPDATE
if (System.Threading.Thread.CurrentThread.ManagedThreadId == mainThreadID)
Debug.LogError("AssertIsWorkerThread failed: main thread calling worker thread code! Thread ID:" + System.Threading.Thread.CurrentThread.ManagedThreadId);
#endif
}
#endregion ISkeletonRenderer Methods
#region ISkeletonRenderer Events
protected event SkeletonRendererDelegate _UpdateLocal;
protected event SkeletonRendererDelegate _UpdateWorld;
protected event SkeletonRendererDelegate _UpdateComplete;
/// <summary>
/// Occurs after the animations are applied and before world space values are resolved.
/// Use this callback when you want to set bone local values.
/// </summary>
public event SkeletonRendererDelegate UpdateLocal { add { _UpdateLocal += value; } remove { _UpdateLocal -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Using this callback will cause the world space values to be solved an extra time.
/// Use this callback if want to use bone world space values, and also set bone local values.</summary>
public event SkeletonRendererDelegate UpdateWorld { add { _UpdateWorld += value; } remove { _UpdateWorld -= value; } }
/// <summary>
/// Occurs after the Skeleton's bone world space values are resolved (including all constraints).
/// Use this callback if you want to use bone world space values, but don't intend to modify bone local values.
/// This callback can also be used when setting world position and the bone matrix.</summary>
public event SkeletonRendererDelegate UpdateComplete { add { _UpdateComplete += value; } remove { _UpdateComplete -= value; } }
/// <summary> Occurs after the vertex data is populated every frame, before the vertices are pushed into the mesh.</summary>
public event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
public event SkeletonRendererDelegate OnRebuild;
/// <summary>OnInstructionsPrepared is raised at the end of <c>LateUpdate</c> after render instructions
/// are done, target renderers are prepared, and the mesh is ready to be generated.</summary>
public event InstructionDelegate OnInstructionsPrepared;
#endregion ISkeletonRenderer Events
#endregion Identical common ISkeletonRenderer code
// End of identical code shared by ISkeletonRenderer subclasses as a workaround for single inheritance limitations.
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9178134ad83033c48a2e3dec43d8d200
timeCreated: 1754676176
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -30,13 +30,17 @@
#if UNITY_2018_3 || UNITY_2019 || UNITY_2018_3_OR_NEWER
#define NEW_PREFAB_SYSTEM
#endif
#define SPINE_OPTIONAL_MATERIALOVERRIDE
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
// Contributed by: Lost Polygon
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
#if NEW_PREFAB_SYSTEM
@ -45,7 +49,7 @@ namespace Spine.Unity {
[ExecuteInEditMode]
#endif
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonRendererCustomMaterials")]
public class SkeletonRendererCustomMaterials : MonoBehaviour {
public class SkeletonRendererCustomMaterials : MonoBehaviour, IUpgradable {
#region Inspector
public SkeletonRenderer skeletonRenderer;
@ -53,6 +57,14 @@ namespace Spine.Unity {
[SerializeField] protected List<AtlasMaterialOverride> customMaterialOverrides = new List<AtlasMaterialOverride>();
#if UNITY_EDITOR
#if AUTO_UPGRADE_TO_43_COMPONENTS
public void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Reset () {
skeletonRenderer = GetComponent<SkeletonRenderer>();
@ -127,7 +139,6 @@ namespace Spine.Unity {
return;
}
#if SPINE_OPTIONAL_MATERIALOVERRIDE
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
if (atlasMaterialOverride.overrideDisabled)
@ -135,7 +146,6 @@ namespace Spine.Unity {
skeletonRenderer.CustomMaterialOverride[atlasMaterialOverride.originalMaterial] = atlasMaterialOverride.replacementMaterial;
}
#endif
}
void RemoveCustomMaterialOverrides () {
@ -144,7 +154,6 @@ namespace Spine.Unity {
return;
}
#if SPINE_OPTIONAL_MATERIALOVERRIDE
for (int i = 0; i < customMaterialOverrides.Count; i++) {
AtlasMaterialOverride atlasMaterialOverride = customMaterialOverrides[i];
Material currentMaterial;
@ -158,7 +167,6 @@ namespace Spine.Unity {
skeletonRenderer.CustomMaterialOverride.Remove(atlasMaterialOverride.originalMaterial);
}
#endif
}
// OnEnable applies the overrides at runtime, and when the editor loads.
@ -210,5 +218,22 @@ namespace Spine.Unity {
return overrideDisabled == other.overrideDisabled && originalMaterial == other.originalMaterial && replacementMaterial == other.replacementMaterial;
}
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonRendererCustomMaterials, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -27,7 +27,12 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -37,7 +42,7 @@ namespace Spine.Unity {
/// Note: This component is automatically attached when calling "Create Hinge Chain 2D" at <see cref="SkeletonUtilityBone"/>,
/// do not attempt to use this component for other purposes.
/// </summary>
public class ActivateBasedOnFlipDirection : MonoBehaviour {
public class ActivateBasedOnFlipDirection : MonoBehaviour, IUpgradable {
public SkeletonRenderer skeletonRenderer;
public SkeletonGraphic skeletonGraphic;
@ -49,6 +54,14 @@ namespace Spine.Unity {
bool wasFlippedXBefore = false;
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
protected void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
private void Start () {
jointsNormalX = activeOnNormalX.GetComponentsInChildren<HingeJoint2D>();
jointsFlippedX = activeOnFlippedX.GetComponentsInChildren<HingeJoint2D>();
@ -88,5 +101,22 @@ namespace Spine.Unity {
Transform currentLocation = toActivate.GetChild(0);
toActivate.position += targetLocation.position - currentLocation.position;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public virtual void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null && skeletonGraphic == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at ActivateBasedOnFlipDirection, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] protected bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -31,8 +31,13 @@
#define NEW_PREFAB_SYSTEM
#endif
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity {
@ -41,9 +46,9 @@ namespace Spine.Unity {
#else
[ExecuteInEditMode]
#endif
[RequireComponent(typeof(ISkeletonAnimation))]
[RequireComponent(typeof(ISkeletonRenderer))]
[HelpURL("https://esotericsoftware.com/spine-unity-utility-components#SkeletonUtility")]
public sealed class SkeletonUtility : MonoBehaviour {
public sealed class SkeletonUtility : MonoBehaviour, IUpgradable {
#region BoundingBoxAttachment
public static PolygonCollider2D AddBoundingBoxGameObject (Skeleton skeleton, string skinName, string slotName, string attachmentName, Transform parent, bool isTrigger = true) {
@ -188,30 +193,33 @@ namespace Spine.Unity {
// Thus to prevent a one-frame-behind offset after a layout change affecting mesh scale,
// we have to re-evaluate the callbacks via the lines below.
if (lastPositionScale != positionScale) {
UpdateLocal(skeletonAnimation);
UpdateWorld(skeletonAnimation);
UpdateComplete(skeletonAnimation);
}
}
UpdateLocal(skeletonGraphic);
UpdateWorld(skeletonGraphic);
UpdateComplete(skeletonGraphic);
}
}
[HideInInspector] public SkeletonRenderer skeletonRenderer;
[HideInInspector] public SkeletonGraphic skeletonGraphic;
[System.NonSerialized] public ISkeletonAnimation skeletonAnimation;
private ISkeletonComponent skeletonComponent;
private ISkeletonRenderer skeletonComponent;
[System.NonSerialized] public List<SkeletonUtilityBone> boneComponents = new List<SkeletonUtilityBone>();
[System.NonSerialized] public List<SkeletonUtilityConstraint> constraintComponents = new List<SkeletonUtilityConstraint>();
public ISkeletonComponent SkeletonComponent {
public ISkeletonComponent SkeletonComponent { get { return this.SkeletonRenderer; } }
public ISkeletonRenderer SkeletonRenderer {
get {
if (skeletonComponent == null) {
skeletonComponent = skeletonRenderer != null ? skeletonRenderer.GetComponent<ISkeletonComponent>() :
skeletonGraphic != null ? skeletonGraphic.GetComponent<ISkeletonComponent>() :
GetComponent<ISkeletonComponent>();
skeletonComponent = skeletonRenderer != null ? skeletonRenderer :
skeletonGraphic != null ? skeletonGraphic :
GetComponent<ISkeletonRenderer>();
}
return skeletonComponent;
}
}
public Skeleton Skeleton {
get {
if (SkeletonComponent == null)
@ -222,8 +230,8 @@ namespace Spine.Unity {
public bool IsValid {
get {
return (skeletonRenderer != null && skeletonRenderer.valid) ||
(skeletonGraphic != null && skeletonGraphic.IsValid);
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
return (skeletonComponent != null && skeletonComponent.IsValid);
}
}
@ -234,7 +242,7 @@ namespace Spine.Unity {
float lastPositionScale = 1.0f;
Vector2 positionOffset = Vector2.zero;
bool hasOverrideBones;
bool hasConstraints;
bool hasConstraintTargetBones;
bool needToReprocessBones;
public void ResubscribeEvents () {
@ -243,31 +251,31 @@ namespace Spine.Unity {
}
void ResubscribeIndependentEvents () {
if (skeletonRenderer != null) {
skeletonRenderer.OnRebuild -= HandleRendererReset;
skeletonRenderer.OnRebuild += HandleRendererReset;
} else if (skeletonGraphic != null) {
skeletonGraphic.OnRebuild -= HandleRendererReset;
skeletonGraphic.OnRebuild += HandleRendererReset;
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.OnRebuild -= HandleRendererReset;
skeletonComponent.OnRebuild += HandleRendererReset;
}
if (skeletonGraphic != null) {
skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
skeletonGraphic.OnPostProcessVertices += UpdateToMeshScaleAndOffset;
}
if (skeletonAnimation != null) {
skeletonAnimation.UpdateLocal -= UpdateLocal;
skeletonAnimation.UpdateLocal += UpdateLocal;
}
}
void ResubscribeDependentEvents () {
if (skeletonAnimation != null) {
skeletonAnimation.UpdateWorld -= UpdateWorld;
skeletonAnimation.UpdateComplete -= UpdateComplete;
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.UpdateLocal -= UpdateLocal;
skeletonComponent.UpdateWorld -= UpdateWorld;
skeletonComponent.UpdateComplete -= UpdateComplete;
if (hasOverrideBones || hasConstraints)
skeletonAnimation.UpdateWorld += UpdateWorld;
if (hasConstraints)
skeletonAnimation.UpdateComplete += UpdateComplete;
bool hasConstraintComponents = constraintComponents.Count > 0;
if (hasOverrideBones || !hasConstraintTargetBones)
skeletonComponent.UpdateLocal += UpdateLocal;
if (hasOverrideBones || hasConstraintComponents)
skeletonComponent.UpdateWorld += UpdateWorld;
if (hasConstraintTargetBones)
skeletonComponent.UpdateComplete += UpdateComplete;
}
}
@ -278,46 +286,37 @@ namespace Spine.Unity {
if (skeletonGraphic == null) {
skeletonGraphic = GetComponent<SkeletonGraphic>();
}
if (skeletonAnimation == null) {
skeletonAnimation = skeletonRenderer != null ? skeletonRenderer.GetComponent<ISkeletonAnimation>() :
skeletonGraphic != null ? skeletonGraphic.GetComponent<ISkeletonAnimation>() :
GetComponent<ISkeletonAnimation>();
}
if (skeletonComponent == null) {
skeletonComponent = skeletonRenderer != null ? skeletonRenderer.GetComponent<ISkeletonComponent>() :
skeletonGraphic != null ? skeletonGraphic.GetComponent<ISkeletonComponent>() :
GetComponent<ISkeletonComponent>();
}
CollectBones();
ResubscribeEvents();
}
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
void Awake () {
if (!Application.isPlaying && !wasUpgradedTo43) {
UpgradeTo43();
}
}
#endif
void Start () {
//recollect because order of operations failure when switching between game mode and edit mode...
CollectBones();
}
void OnDisable () {
if (skeletonRenderer != null)
skeletonRenderer.OnRebuild -= HandleRendererReset;
if (skeletonGraphic != null) {
skeletonGraphic.OnRebuild -= HandleRendererReset;
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent != null) {
skeletonComponent.OnRebuild -= HandleRendererReset;
skeletonComponent.UpdateLocal -= UpdateLocal;
skeletonComponent.UpdateWorld -= UpdateWorld;
skeletonComponent.UpdateComplete -= UpdateComplete;
}
if (skeletonGraphic) {
skeletonGraphic.OnPostProcessVertices -= UpdateToMeshScaleAndOffset;
}
if (skeletonAnimation != null) {
skeletonAnimation.UpdateLocal -= UpdateLocal;
skeletonAnimation.UpdateWorld -= UpdateWorld;
skeletonAnimation.UpdateComplete -= UpdateComplete;
}
}
void HandleRendererReset (SkeletonRenderer r) {
if (OnReset != null) OnReset();
CollectBones();
}
void HandleRendererReset (SkeletonGraphic g) {
void HandleRendererReset (ISkeletonRenderer r) {
if (OnReset != null) OnReset();
CollectBones();
}
@ -349,6 +348,8 @@ namespace Spine.Unity {
}
public void CollectBones () {
ISkeletonRenderer skeletonComponent = this.SkeletonRenderer;
if (skeletonComponent == null) return;
Skeleton skeleton = skeletonComponent.Skeleton;
if (skeleton == null) return;
@ -373,10 +374,9 @@ namespace Spine.Unity {
if (b.bone == null) continue;
}
hasOverrideBones |= (b.mode == SkeletonUtilityBone.Mode.Override);
hasConstraints |= constraintTargets.Contains(b.bone);
hasConstraintTargetBones |= constraintTargets.Contains(b.bone);
}
hasConstraints |= constraintComponents.Count > 0;
needToReprocessBones = false;
} else {
boneComponents.Clear();
@ -385,30 +385,22 @@ namespace Spine.Unity {
ResubscribeDependentEvents();
}
void UpdateLocal (ISkeletonAnimation anim) {
if (needToReprocessBones)
CollectBones();
List<SkeletonUtilityBone> boneComponents = this.boneComponents;
if (boneComponents == null) return;
for (int i = 0, n = boneComponents.Count; i < n; i++)
boneComponents[i].transformLerpComplete = false;
void UpdateLocal (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.Local);
}
void UpdateWorld (ISkeletonAnimation anim) {
void UpdateWorld (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.World);
for (int i = 0, n = constraintComponents.Count; i < n; i++)
constraintComponents[i].DoUpdate();
}
void UpdateComplete (ISkeletonAnimation anim) {
void UpdateComplete (ISkeletonRenderer skeletonRenderer) {
UpdateAllBones(SkeletonUtilityBone.UpdatePhase.Complete);
}
void UpdateAllBones (SkeletonUtilityBone.UpdatePhase phase) {
if (boneRoot == null)
if (boneRoot == null || needToReprocessBones)
CollectBones();
List<SkeletonUtilityBone> boneComponents = this.boneComponents;
@ -502,6 +494,21 @@ namespace Spine.Unity {
return go;
}
#region Transfer of Deprecated Fields
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
public void UpgradeTo43 () {
wasUpgradedTo43 = true;
if (skeletonRenderer == null && skeletonGraphic == null) {
Component previousReference = previousSkeletonRenderer != null ? previousSkeletonRenderer : this;
skeletonRenderer = previousReference.GetComponent<SkeletonRenderer>();
if (skeletonRenderer == null)
Debug.LogError("Please manually re-assign SkeletonRenderer at SkeletonUtility, " +
"automatic upgrade failed.", this);
}
}
[SerializeField, HideInInspector, FormerlySerializedAs("skeletonRenderer")] Component previousSkeletonRenderer;
[SerializeField] bool wasUpgradedTo43 = false;
#endif
#endregion
}
}

View File

@ -54,7 +54,7 @@ namespace Spine.Unity {
Complete
}
#region Inspector
#region Inspector
/// <summary>If a bone isn't set, boneName is used to find the bone.</summary>
public string boneName;
public Transform parentReference;
@ -62,16 +62,22 @@ namespace Spine.Unity {
public bool position, rotation, scale, zPosition = true;
[Range(0f, 1f)]
public float overrideAlpha = 1;
#endregion
#endregion
public SkeletonUtility hierarchy;
[System.NonSerialized] public Bone bone;
[System.NonSerialized] public bool transformLerpComplete;
[System.NonSerialized] public bool valid;
Transform cachedTransform;
Transform skeletonTransform;
Vector3 TransformLocalPosition { get { return cachedTransform.localPosition; } }
Quaternion TransformLocalRotation { get { return cachedTransform.localRotation; } }
Vector3 TransformLocalScale { get { return cachedTransform.localScale; } }
#if UNITY_EDITOR
bool incompatibleTransformMode;
public bool IncompatibleTransformMode { get { return incompatibleTransformMode; } }
#endif
public void Reset () {
bone = null;
@ -145,7 +151,9 @@ namespace Spine.Unity {
if (scale) {
thisTransform.localScale = new Vector3(bonePose.ScaleX, bonePose.ScaleY, 1f);
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
break;
case UpdatePhase.World:
@ -166,38 +174,36 @@ namespace Spine.Unity {
if (scale) {
thisTransform.localScale = new Vector3(appliedPose.ScaleX, appliedPose.ScaleY, 1f);
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
break;
}
} else if (mode == Mode.Override) {
if (transformLerpComplete)
if (phase != UpdatePhase.Local)
return;
var bonePose = bone.Pose;
if (parentReference == null) {
if (position) {
Vector3 clp = thisTransform.localPosition / positionScale;
Vector3 clp = TransformLocalPosition / positionScale;
bonePose.X = Mathf.Lerp(bonePose.X, clp.x, overrideAlpha);
bonePose.Y = Mathf.Lerp(bonePose.Y, clp.y, overrideAlpha);
}
if (rotation) {
float angle = Mathf.LerpAngle(bonePose.Rotation, thisTransform.localRotation.eulerAngles.z, overrideAlpha);
float angle = Mathf.LerpAngle(bonePose.Rotation, TransformLocalRotation.eulerAngles.z, overrideAlpha);
bonePose.Rotation = angle;
bone.AppliedPose.Rotation = angle;
}
if (scale) {
Vector3 cls = thisTransform.localScale;
Vector3 cls = TransformLocalScale;
bonePose.ScaleX = Mathf.Lerp(bonePose.ScaleX, cls.x, overrideAlpha);
bonePose.ScaleY = Mathf.Lerp(bonePose.ScaleY, cls.y, overrideAlpha);
}
} else {
if (transformLerpComplete)
return;
if (position) {
Vector3 pos = parentReference.InverseTransformPoint(thisTransform.position) / positionScale;
bonePose.X = Mathf.Lerp(bonePose.X, pos.x, overrideAlpha);
@ -211,15 +217,14 @@ namespace Spine.Unity {
}
if (scale) {
Vector3 cls = thisTransform.localScale;
Vector3 cls = TransformLocalScale;
bonePose.ScaleX = Mathf.Lerp(bonePose.ScaleX, cls.x, overrideAlpha);
bonePose.ScaleY = Mathf.Lerp(bonePose.ScaleY, cls.y, overrideAlpha);
}
#if UNITY_EDITOR
incompatibleTransformMode = BoneTransformModeIncompatible(bone);
#endif
}
transformLerpComplete = true;
}
}

View File

@ -50,6 +50,7 @@ namespace Spine.Unity {
protected virtual void OnEnable () {
bone = GetComponent<SkeletonUtilityBone>();
hierarchy = transform.GetComponentInParent<SkeletonUtility>();
hierarchy.RegisterBone(bone); // prevent update order issues
hierarchy.RegisterConstraint(this);
}

View File

@ -27,6 +27,14 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#if !SPINE_AUTO_UPGRADE_COMPONENTS_OFF
#define AUTO_UPGRADE_TO_43_COMPONENTS
#endif
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {
public enum UpdateMode {
Nothing = 0,
@ -44,8 +52,9 @@ namespace Spine.Unity {
InLateUpdate
}
public delegate void ISkeletonAnimationDelegate (ISkeletonAnimation animated);
public delegate void UpdateBonesDelegate (ISkeletonAnimation animated);
public delegate void SkeletonAnimationDelegate (ISkeletonAnimation animated);
public delegate void SkeletonRendererDelegate (ISkeletonRenderer skeletonRenderer);
public delegate void InstructionDelegate (SkeletonRendererInstruction instruction);
public interface ISpineComponent { }
public static class ISpineComponentExtensions {
@ -56,13 +65,29 @@ namespace Spine.Unity {
}
/// <summary>A Spine-Unity Component that animates a Skeleton but not necessarily with a Spine.AnimationState.</summary>
public interface ISkeletonAnimation : ISpineComponent {
event ISkeletonAnimationDelegate OnAnimationRebuild;
event UpdateBonesDelegate UpdateLocal;
event UpdateBonesDelegate UpdateWorld;
event UpdateBonesDelegate UpdateComplete;
public interface ISkeletonAnimation : IHasSkeletonRenderer, ISpineComponent {
event SkeletonAnimationDelegate OnAnimationRebuild;
event SkeletonAnimationDelegate BeforeUpdate;
event SkeletonAnimationDelegate BeforeApply;
event SkeletonRendererDelegate UpdateLocal;
event SkeletonRendererDelegate UpdateWorld;
event SkeletonRendererDelegate UpdateComplete;
MonoBehaviour Component { get; }
Skeleton Skeleton { get; }
bool IsValid { get; }
UpdateMode UpdateMode { get; set; }
UpdateTiming UpdateTiming { get; set; }
void EnsureRendererEventsSubscribed ();
void Initialize (bool overwrite, bool quiet = false, bool calledFromRendererCallback = false);
void InitializeAnimationComponent ();
void ClearAnimationState ();
void ApplyAnimation (bool calledFromMainThread = true);
CoroutineIterator ApplyAnimationSplit (CoroutineIterator coroutineIterator);
void UpdateOncePerFrame (float deltaTime);
void Update (float deltaTime);
void OnBecameVisibleFromMode (UpdateMode previousUpdateMode);
}
/// <summary>Holds a reference to a SkeletonDataAsset.</summary>
@ -71,20 +96,26 @@ namespace Spine.Unity {
SkeletonDataAsset SkeletonDataAsset { get; }
}
/// <summary>A Spine-Unity Component that manages a Spine.Skeleton instance, instantiated from a SkeletonDataAsset.</summary>
public interface ISkeletonComponent : ISpineComponent {
/// <summary>Gets the SkeletonDataAsset of the Spine Component.</summary>
//[System.Obsolete]
SkeletonDataAsset SkeletonDataAsset { get; }
public interface IHasModifyableSkeletonDataAsset : IHasSkeletonDataAsset, ISpineComponent {
/// <summary>Set the SkeletonDataAsset of the Spine Component.
/// Initialize(overwrite:true) has to be called at the target component
/// afterwards.</summary>
new SkeletonDataAsset SkeletonDataAsset { get; set; }
}
/// <summary>Gets the Spine.Skeleton instance of the Spine Component. This is equivalent to SkeletonRenderer's .skeleton.</summary>
Skeleton Skeleton { get; }
/// <summary>A Spine-Unity Component that manages a Spine.Skeleton instance, instantiated from a SkeletonDataAsset.</summary>
public interface ISkeletonComponent : IHasModifyableSkeletonDataAsset, ISpineComponent {
/// <summary>Accesses the Spine.Skeleton instance of the Spine Component. This is equivalent to SkeletonRenderer's .skeleton.</summary>
Skeleton Skeleton { get; set; }
MonoBehaviour Component { get; }
}
/// <summary>A Spine-Unity Component that uses a Spine.AnimationState to animate its skeleton.</summary>
public interface IAnimationStateComponent : ISpineComponent {
/// <summary>Gets the Spine.AnimationState of the animated Spine Component. This is equivalent to SkeletonAnimation.state.</summary>
AnimationState AnimationState { get; }
/// <summary>If enabled, AnimationState time is advanced by Unscaled Game Time
/// (<c>Time.unscaledDeltaTime</c> instead of the default Game Time(<c>Time.deltaTime</c>).
/// to animate independent of game <c>Time.timeScale</c>.
@ -92,13 +123,81 @@ namespace Spine.Unity {
bool UnscaledTime { get; set; }
}
/// <summary>A Spine-Unity Component that holds a reference to a SkeletonRenderer.</summary>
/// <summary>A Spine-Unity Component that holds a reference to a skeleton renderer.</summary>
public interface IHasSkeletonRenderer : ISpineComponent {
SkeletonRenderer SkeletonRenderer { get; }
ISkeletonRenderer Renderer { get; }
}
/// <summary>A Spine-Unity Component that holds a reference to an ISkeletonComponent.</summary>
public interface IHasSkeletonComponent : ISpineComponent {
ISkeletonComponent SkeletonComponent { get; }
}
public interface ISkeletonRendererEvents {
event SkeletonRendererDelegate UpdateLocal;
event SkeletonRendererDelegate UpdateWorld;
event SkeletonRendererDelegate UpdateComplete;
/// <summary>OnRebuild is raised after the Skeleton is successfully initialized.</summary>
event SkeletonRendererDelegate OnRebuild;
/// <summary>OnMeshAndMaterialsUpdated is called at the end of LateUpdate after the Mesh and
/// all materials have been updated.</summary>
event SkeletonRendererDelegate OnMeshAndMaterialsUpdated;
}
public interface IThreadedRenderer {
bool IsUpdatedExternally { get; set; }
bool RequiresMeshBufferAssignmentMainThread { get; }
void UpdateMeshAndMaterialsToBuffers ();
void MainThreadPrepareLateUpdateInternal();
void LateUpdateImplementation (bool calledFromMainThread);
}
// combined interfaces
public interface ISkeletonRenderer : ISkeletonComponent, ISkeletonRendererEvents,
IHasModifyableSkeletonDataAsset, IThreadedRenderer {
ISkeletonAnimation Animation { get; set; }
Dictionary<Slot, Material> CustomSlotMaterials { get; }
bool EnableSeparatorSlots { get; set; }
List<Slot> SeparatorSlots { get; }
bool Freeze { get; }
bool HasGenerateMeshOverride { get; }
string InitialSkinName { get; set; }
bool IsValid { get; }
MeshGenerator.Settings MeshSettings { get; set; }
UpdateMode UpdateMode { get; set; }
UpdateMode UpdateWhenInvisible { get; set; }
float MeshScale { get; }
Vector2 PhysicsPositionInheritanceFactor { get; set; }
float PhysicsRotationInheritanceFactor { get; set; }
#if UNITY_EDITOR
bool EditorSkipSkinSync { get; set; }
#endif
event Spine.Unity.InstructionDelegate GenerateMeshOverride;
event Spine.Unity.MeshGeneratorDelegate OnPostProcessVertices;
void ClearSkeletonState ();
void ClearState ();
void Initialize (bool overwrite, bool quiet = false);
void GatherTransformMovementForPhysics ();
void ApplyTransformMovementToPhysics();
void AfterAnimationApplied (bool calledFromMainThread = true);
CoroutineIterator AfterAnimationAppliedSplit (CoroutineIterator coroutineIterator);
void LateUpdate ();
void UpdateMesh (bool calledFromMainThread = true);
void UpdateMaterials ();
void FindAndApplySeparatorSlots (Func<string, bool> slotNamePredicate, bool clearExistingSeparators = true, bool updateStringArray = false);
void FindAndApplySeparatorSlots (string startsWith, bool clearExistingSeparators = true, bool updateStringArray = false);
void ReapplySeparatorSlotNames ();
}
public interface IUpgradable {
#if UNITY_EDITOR && AUTO_UPGRADE_TO_43_COMPONENTS
void UpgradeTo43 ();
#endif
}
}

View File

@ -100,35 +100,35 @@ namespace Spine.Unity {
/// <summary>Holds several methods to prepare and generate a UnityEngine mesh based on a skeleton. Contains buffers needed to perform the operation, and serializes settings for mesh generation.</summary>
[System.Serializable]
public class MeshGenerator {
public Settings settings = Settings.Default;
[NonSerialized] public Settings settings = Settings.Default;
/// <summary>Saved global setting whether linear color space is used. Required because quality settings can't be
/// accessed from worker threads.</summary>
public static bool? linearColorSpaceGlobal = null;
[System.Serializable]
public struct Settings {
public bool useClipping;
[Range(-0.1f, 0f)] public float zSpacing;
public bool tintBlack;
public class Settings {
/// <summary>Use Spine's clipping feature. If false, ClippingAttachments will be ignored.</summary>
public bool useClipping = true;
[Range(-0.1f, 0f)] public float zSpacing = 0f;
/// <summary>If true, second colors on slots will be added to the output Mesh as UV2 and UV3. A special "tint black" shader that interprets UV2 and UV3 as black point colors is required to render this properly.</summary>
public bool tintBlack = false;
[UnityEngine.Serialization.FormerlySerializedAs("canvasGroupTintBlack")]
[Tooltip("Enable when using SkeletonGraphic under a CanvasGroup. " +
"When enabled, PMA Vertex Color alpha value is stored at uv2.g instead of color.a to capture " +
"CanvasGroup modifying color.a. Also helps to detect correct parameter setting combinations.")]
public bool canvasGroupCompatible;
public bool pmaVertexColors;
public bool addNormals;
public bool calculateTangents;
public bool immutableTriangles;
/// <summary>Multiply vertex color RGB with vertex color alpha. Set this to true if the shader used for rendering is a premultiplied alpha shader. Setting this to false disables single-batch additive slots.</summary>
public bool pmaVertexColors = true;
/// <summary>If true, the mesh generator adds normals to the output mesh. For better performance and reduced memory requirements, use a shader that assumes the desired normal.</summary>
public bool addNormals = false;
/// <summary>If true, tangents are calculated every frame and added to the Mesh. Enable this when using a shader that uses lighting that requires tangents.</summary>
public bool calculateTangents = false;
/// <summary>If true, triangles will not be updated. Enable this as an optimization if the skeleton does not make use of attachment swapping or hiding, or draw order keys. Otherwise, setting this to false may cause errors in rendering.</summary>
public bool immutableTriangles = false;
static public Settings Default {
get {
return new Settings {
pmaVertexColors = true,
zSpacing = 0f,
useClipping = true,
tintBlack = false,
calculateTangents = false,
//renderMeshes = true,
addNormals = false,
immutableTriangles = false
};
return new Settings();
}
}
}
@ -190,6 +190,12 @@ namespace Spine.Unity {
submeshes.TrimExcess();
}
public static void InitializeGlobalSettings () {
if (linearColorSpaceGlobal == null) {
linearColorSpaceGlobal = (QualitySettings.activeColorSpace == ColorSpace.Linear);
}
}
#region Step 1 : Generate Instructions
/// <summary>
/// A specialized variant of <see cref="GenerateSkeletonRendererInstruction"/>.
@ -332,7 +338,10 @@ namespace Spine.Unity {
return false;
}
public static void GenerateSkeletonRendererInstruction (SkeletonRendererInstruction instructionOutput, Skeleton skeleton, Dictionary<Slot, Material> customSlotMaterials, List<Slot> separatorSlots, bool generateMeshOverride, bool immutableTriangles = false) {
public static void GenerateSkeletonRendererInstruction (SkeletonRendererInstruction instructionOutput,
Skeleton skeleton, Dictionary<Slot, Material> customSlotMaterials, List<Slot> separatorSlots,
bool enableSeparation, bool immutableTriangles = false) {
// if (skeleton == null) throw new ArgumentNullException("skeleton");
// if (instructionOutput == null) throw new ArgumentNullException("instructionOutput");
@ -432,7 +441,7 @@ namespace Spine.Unity {
}
if (noRender) {
if (current.forceSeparate && generateMeshOverride) { // && current.rawVertexCount > 0) {
if (current.forceSeparate && enableSeparation) { // && current.rawVertexCount > 0) {
{ // Add
current.endSlot = i;
current.preActiveClippingSlotSource = lastPreActiveClipping;
@ -523,21 +532,6 @@ namespace Spine.Unity {
#endif
instructionOutput.immutableTriangles = immutableTriangles;
}
public static void TryReplaceMaterials (ExposedList<SubmeshInstruction> workingSubmeshInstructions, Dictionary<Material, Material> customMaterialOverride) {
// Material overrides are done here so they can be applied per submesh instead of per slot
// but they will still be passed through the GenerateMeshOverride delegate,
// and will still go through the normal material match check step in STEP 3.
SubmeshInstruction[] wsii = workingSubmeshInstructions.Items;
for (int i = 0; i < workingSubmeshInstructions.Count; i++) {
Material material = wsii[i].material;
if (material == null) continue;
Material overrideMaterial;
if (customMaterialOverride.TryGetValue(material, out overrideMaterial))
wsii[i].material = overrideMaterial;
}
}
#endregion
#region Step 2 : Populate vertex data and triangle index buffers.
@ -585,7 +579,7 @@ namespace Spine.Unity {
bool pmaVertexColors = settings.pmaVertexColors;
bool tintBlack = settings.tintBlack;
#if LINEAR_COLOR_SPACE_FIX_ADDITIVE_ALPHA
bool linearColorSpace = QualitySettings.activeColorSpace == ColorSpace.Linear;
bool linearColorSpace = linearColorSpaceGlobal.GetValueOrDefault(false);
#endif
#if SPINE_TRIANGLECHECK
@ -746,11 +740,8 @@ namespace Spine.Unity {
float x = workingVerts[i2];
float y = workingVerts[i2 + 1];
vbi[vi].x = x;
vbi[vi].y = y;
vbi[vi].z = z;
ubi[vi].x = uvs[i2];
ubi[vi].y = uvs[i2 + 1];
vbi[vi] = new Vector3(x, y, z);
ubi[vi] = new Vector2(uvs[i2], uvs[i2 + 1]);
cbi[vi] = color;
// Calculate bounds.
@ -766,11 +757,8 @@ namespace Spine.Unity {
float x = workingVerts[i2];
float y = workingVerts[i2 + 1];
vbi[vi].x = x;
vbi[vi].y = y;
vbi[vi].z = z;
ubi[vi].x = uvs[i2];
ubi[vi].y = uvs[i2 + 1];
vbi[vi] = new Vector3(x, y, z);
ubi[vi] = new Vector2(uvs[i2], uvs[i2 + 1]);
cbi[vi] = color;
// Calculate bounds.
@ -828,7 +816,7 @@ namespace Spine.Unity {
int totalVertexCount = instruction.rawVertexCount;
#if LINEAR_COLOR_SPACE_FIX_ADDITIVE_ALPHA
bool linearColorSpace = QualitySettings.activeColorSpace == ColorSpace.Linear;
bool linearColorSpace = linearColorSpaceGlobal.GetValueOrDefault(false);
#endif
// Add data to vertex buffers
{
@ -964,10 +952,10 @@ namespace Spine.Unity {
float x2 = tempVerts[RegionAttachment.ULX], y2 = tempVerts[RegionAttachment.ULY];
float x3 = tempVerts[RegionAttachment.URX], y3 = tempVerts[RegionAttachment.URY];
float x4 = tempVerts[RegionAttachment.BRX], y4 = tempVerts[RegionAttachment.BRY];
vbi[vertexIndex].x = x1; vbi[vertexIndex].y = y1; vbi[vertexIndex].z = z;
vbi[vertexIndex + 1].x = x4; vbi[vertexIndex + 1].y = y4; vbi[vertexIndex + 1].z = z;
vbi[vertexIndex + 2].x = x2; vbi[vertexIndex + 2].y = y2; vbi[vertexIndex + 2].z = z;
vbi[vertexIndex + 3].x = x3; vbi[vertexIndex + 3].y = y3; vbi[vertexIndex + 3].z = z;
vbi[vertexIndex] = new Vector3(x1, y1, z);
vbi[vertexIndex + 1] = new Vector3(x4, y4, z);
vbi[vertexIndex + 2] = new Vector3(x2, y2, z);
vbi[vertexIndex + 3] = new Vector3(x3, y3, z);
if (settings.pmaVertexColors) {
float alpha = combinedC.a;
@ -993,10 +981,10 @@ namespace Spine.Unity {
cbi[vertexIndex] = color; cbi[vertexIndex + 1] = color; cbi[vertexIndex + 2] = color; cbi[vertexIndex + 3] = color;
float[] regionUVs = regionAttachment.UVs;
ubi[vertexIndex].x = regionUVs[RegionAttachment.BLX]; ubi[vertexIndex].y = regionUVs[RegionAttachment.BLY];
ubi[vertexIndex + 1].x = regionUVs[RegionAttachment.BRX]; ubi[vertexIndex + 1].y = regionUVs[RegionAttachment.BRY];
ubi[vertexIndex + 2].x = regionUVs[RegionAttachment.ULX]; ubi[vertexIndex + 2].y = regionUVs[RegionAttachment.ULY];
ubi[vertexIndex + 3].x = regionUVs[RegionAttachment.URX]; ubi[vertexIndex + 3].y = regionUVs[RegionAttachment.URY];
ubi[vertexIndex] = new Vector2(regionUVs[RegionAttachment.BLX], regionUVs[RegionAttachment.BLY]);
ubi[vertexIndex + 1] = new Vector2(regionUVs[RegionAttachment.BRX], regionUVs[RegionAttachment.BRY]);
ubi[vertexIndex + 2] = new Vector2(regionUVs[RegionAttachment.ULX], regionUVs[RegionAttachment.ULY]);
ubi[vertexIndex + 3] = new Vector2(regionUVs[RegionAttachment.URX], regionUVs[RegionAttachment.URY]);
if (x1 < bmin.x) bmin.x = x1; // Potential first attachment bounds initialization. Initial min should not block initial max. Same for Y below.
if (x1 > bmax.x) bmax.x = x1;
@ -1062,8 +1050,9 @@ namespace Spine.Unity {
for (int iii = 0; iii < verticesArrayLength; iii += 2) {
float x = tempVerts[iii], y = tempVerts[iii + 1];
vbi[vertexIndex].x = x; vbi[vertexIndex].y = y; vbi[vertexIndex].z = z;
cbi[vertexIndex] = color; ubi[vertexIndex].x = attachmentUVs[iii]; ubi[vertexIndex].y = attachmentUVs[iii + 1];
vbi[vertexIndex] = new Vector3(x, y, z);
cbi[vertexIndex] = color;
ubi[vertexIndex] = new Vector2(attachmentUVs[iii], attachmentUVs[iii + 1]);
if (x < bmin.x) bmin.x = x;
else if (x > bmax.x) bmax.x = x;
@ -1329,13 +1318,13 @@ namespace Spine.Unity {
}
#endregion
public void EnsureVertexCapacity (int minimumVertexCount, bool inlcudeTintBlack = false, bool includeTangents = false, bool includeNormals = false) {
public void EnsureVertexCapacity (int minimumVertexCount, bool includeTintBlack = false, bool includeTangents = false, bool includeNormals = false) {
if (minimumVertexCount > vertexBuffer.Items.Length) {
Array.Resize(ref vertexBuffer.Items, minimumVertexCount);
Array.Resize(ref uvBuffer.Items, minimumVertexCount);
Array.Resize(ref colorBuffer.Items, minimumVertexCount);
if (inlcudeTintBlack) {
if (includeTintBlack) {
if (uv2 == null) {
uv2 = new ExposedList<Vector2>(minimumVertexCount);
uv3 = new ExposedList<Vector2>(minimumVertexCount);
@ -1463,119 +1452,5 @@ namespace Spine.Unity {
}
}
#endregion
#region AttachmentRendering
static List<Vector3> AttachmentVerts = new List<Vector3>();
static List<Vector2> AttachmentUVs = new List<Vector2>();
static List<Color32> AttachmentColors32 = new List<Color32>();
static List<int> AttachmentIndices = new List<int>();
/// <summary>Fills mesh vertex data to render a RegionAttachment.</summary>
public static void FillMeshLocal (Mesh mesh, RegionAttachment regionAttachment) {
if (mesh == null) return;
if (regionAttachment == null) return;
AttachmentVerts.Clear();
float[] offsets = regionAttachment.Offset;
AttachmentVerts.Add(new Vector3(offsets[RegionAttachment.BLX], offsets[RegionAttachment.BLY]));
AttachmentVerts.Add(new Vector3(offsets[RegionAttachment.ULX], offsets[RegionAttachment.ULY]));
AttachmentVerts.Add(new Vector3(offsets[RegionAttachment.URX], offsets[RegionAttachment.URY]));
AttachmentVerts.Add(new Vector3(offsets[RegionAttachment.BRX], offsets[RegionAttachment.BRY]));
AttachmentUVs.Clear();
float[] uvs = regionAttachment.UVs;
AttachmentUVs.Add(new Vector2(uvs[RegionAttachment.ULX], uvs[RegionAttachment.ULY]));
AttachmentUVs.Add(new Vector2(uvs[RegionAttachment.URX], uvs[RegionAttachment.URY]));
AttachmentUVs.Add(new Vector2(uvs[RegionAttachment.BRX], uvs[RegionAttachment.BRY]));
AttachmentUVs.Add(new Vector2(uvs[RegionAttachment.BLX], uvs[RegionAttachment.BLY]));
AttachmentColors32.Clear();
Color32 c = (Color32)(regionAttachment.GetColor());
for (int i = 0; i < 4; i++)
AttachmentColors32.Add(c);
AttachmentIndices.Clear();
AttachmentIndices.AddRange(new[] { 0, 2, 1, 0, 3, 2 });
mesh.Clear();
mesh.name = regionAttachment.Name;
mesh.SetVertices(AttachmentVerts);
mesh.SetUVs(0, AttachmentUVs);
mesh.SetColors(AttachmentColors32);
mesh.SetTriangles(AttachmentIndices, 0);
mesh.RecalculateBounds();
AttachmentVerts.Clear();
AttachmentUVs.Clear();
AttachmentColors32.Clear();
AttachmentIndices.Clear();
}
public static void FillMeshLocal (Mesh mesh, MeshAttachment meshAttachment, SkeletonData skeletonData) {
if (mesh == null) return;
if (meshAttachment == null) return;
int vertexCount = meshAttachment.WorldVerticesLength / 2;
AttachmentVerts.Clear();
if (meshAttachment.IsWeighted()) {
int count = meshAttachment.WorldVerticesLength;
int[] meshAttachmentBones = meshAttachment.Bones;
int v = 0;
float[] vertices = meshAttachment.Vertices;
for (int w = 0, b = 0; w < count; w += 2) {
float wx = 0, wy = 0;
int n = meshAttachmentBones[v++];
n += v;
for (; v < n; v++, b += 3) {
BoneMatrix bm = BoneMatrix.CalculateSetupWorld(skeletonData.Bones.Items[meshAttachmentBones[v]]);
float vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
wx += (vx * bm.a + vy * bm.b + bm.x) * weight;
wy += (vx * bm.c + vy * bm.d + bm.y) * weight;
}
AttachmentVerts.Add(new Vector3(wx, wy));
}
} else {
float[] localVerts = meshAttachment.Vertices;
Vector3 pos = default(Vector3);
for (int i = 0; i < vertexCount; i++) {
int ii = i * 2;
pos.x = localVerts[ii];
pos.y = localVerts[ii + 1];
AttachmentVerts.Add(pos);
}
}
float[] uvs = meshAttachment.UVs;
Vector2 uv = default(Vector2);
Color32 c = (Color32)(meshAttachment.GetColor());
AttachmentUVs.Clear();
AttachmentColors32.Clear();
for (int i = 0; i < vertexCount; i++) {
int ii = i * 2;
uv.x = uvs[ii];
uv.y = uvs[ii + 1];
AttachmentUVs.Add(uv);
AttachmentColors32.Add(c);
}
AttachmentIndices.Clear();
AttachmentIndices.AddRange(meshAttachment.Triangles);
mesh.Clear();
mesh.name = meshAttachment.Name;
mesh.SetVertices(AttachmentVerts);
mesh.SetUVs(0, AttachmentUVs);
mesh.SetColors(AttachmentColors32);
mesh.SetTriangles(AttachmentIndices, 0);
mesh.RecalculateBounds();
AttachmentVerts.Clear();
AttachmentUVs.Clear();
AttachmentColors32.Clear();
AttachmentIndices.Clear();
}
#endregion
}
}

View File

@ -42,6 +42,7 @@ namespace Spine.Unity {
DoubleBuffered<SmartMesh> doubleBufferedMesh;
internal readonly ExposedList<Material> submeshMaterials = new ExposedList<Material>();
internal Material[] sharedMaterials = new Material[0];
internal int previousMaterialHash = 0;
public void Initialize () {
if (doubleBufferedMesh != null) {
@ -53,32 +54,9 @@ namespace Spine.Unity {
}
}
/// <summary>Returns a sharedMaterials array for use on a MeshRenderer.</summary>
/// <returns></returns>
public Material[] GetUpdatedSharedMaterialsArray () {
if (submeshMaterials.Count == sharedMaterials.Length)
submeshMaterials.CopyTo(sharedMaterials);
else
sharedMaterials = submeshMaterials.ToArray();
return sharedMaterials;
}
/// <summary>Returns true if the materials were modified since the buffers were last updated.</summary>
public bool MaterialsChangedInLastUpdate () {
int newSubmeshMaterials = submeshMaterials.Count;
Material[] sharedMaterials = this.sharedMaterials;
if (newSubmeshMaterials != sharedMaterials.Length) return true;
Material[] submeshMaterialsItems = submeshMaterials.Items;
for (int i = 0; i < newSubmeshMaterials; i++)
if (!Material.ReferenceEquals(submeshMaterialsItems[i], sharedMaterials[i])) return true; //if (submeshMaterialsItems[i].GetInstanceID() != sharedMaterials[i].GetInstanceID()) return true;
return false;
}
/// <summary>Updates the internal shared materials array with the given instruction list.</summary>
public void UpdateSharedMaterials (ExposedList<SubmeshInstruction> instructions) {
/// <summary>Updates an internal materials list with the given instruction list.</summary>
public void GatherMaterialsFromInstructions (ExposedList<SubmeshInstruction> instructions,
out bool materialsChanged) {
int newSize = instructions.Count;
{ //submeshMaterials.Resize(instructions.Count);
if (newSize > submeshMaterials.Items.Length)
@ -90,6 +68,22 @@ namespace Spine.Unity {
SubmeshInstruction[] instructionsItems = instructions.Items;
for (int i = 0; i < newSize; i++)
submeshMaterialsItems[i] = instructionsItems[i].material;
materialsChanged = EvaluateMaterialsChanged();
}
/// <summary>Returns a sharedMaterials array for use on a MeshRenderer.</summary>
/// <returns></returns>
public Material[] UpdateSharedMaterialsArray () {
if (submeshMaterials.Count == sharedMaterials.Length)
submeshMaterials.CopyTo(sharedMaterials);
else
sharedMaterials = submeshMaterials.ToArray();
return sharedMaterials;
}
public SmartMesh GetCurrentMesh () {
return doubleBufferedMesh.GetCurrent();
}
public SmartMesh GetNextMesh () {
@ -132,5 +126,21 @@ namespace Spine.Unity {
mesh = null;
}
}
/// <summary>Returns true if the materials were modified since the buffers were last updated.</summary>
protected bool EvaluateMaterialsChanged () {
int submeshMaterialsHash = 0;
int newSubmeshMaterials = submeshMaterials.Count;
Material[] submeshMaterialsItems = submeshMaterials.Items;
for (int i = 0; i < newSubmeshMaterials; i++) {
Material material = submeshMaterialsItems[i];
if (material == null) continue;
int hash = material.GetHashCode() * (i + 1);
submeshMaterialsHash += hash;
}
bool isNewHash = previousMaterialHash != submeshMaterialsHash;
previousMaterialHash = submeshMaterialsHash;
return isNewHash || (sharedMaterials.Length != newSubmeshMaterials);
}
}
}

View File

@ -139,10 +139,12 @@ namespace Spine.Unity {
other.submeshInstructions.CopyTo(this.submeshInstructions.Items);
}
public static bool GeometryNotEqual (SkeletonRendererInstruction a, SkeletonRendererInstruction b) {
public static bool GeometryNotEqual (SkeletonRendererInstruction a, SkeletonRendererInstruction b,
bool calledFromMainThread = true) {
#if SPINE_TRIANGLECHECK
#if UNITY_EDITOR
if (!Application.isPlaying)
if (calledFromMainThread && !Application.isPlaying)
return true;
#endif

View File

@ -32,8 +32,6 @@
#define SPINE_TRIANGLECHECK
//#define SPINE_DEBUG
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Spine.Unity {

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 80607c9cb31e99244abf6120607dbb0d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,80 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. Replaces all prior versions.
*
* Copyright (c) 2013-2024, 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.
*****************************************************************************/
using System;
using System.Threading;
/// <summary>
/// A generic lock-free single-producer single-consumer queue.
/// </summary>
public class LockFreeSPSCQueue<T> {
private readonly int capacity;
private readonly T[] circularBuffer;
private int headIndex;
private int tailIndex;
public LockFreeSPSCQueue (int allocatedCapacity) {
capacity = allocatedCapacity;
circularBuffer = new T[allocatedCapacity];
headIndex = tailIndex = 0;
}
/// <summary>Enqueues an item if there is space available.</summary>
/// <returns>True if the item was successfully enqueued, false otherwise.</returns>
public bool Enqueue (T item) {
int head = Thread.VolatileRead(ref headIndex);
int nextHead = (head + 1) % capacity;
if (nextHead == Thread.VolatileRead(ref tailIndex))
return false; // queue is full
circularBuffer[head] = item;
Thread.VolatileWrite(ref headIndex, nextHead);
return true;
}
/// <summary>
/// Dequeues an item unless the queue is empty.
/// </summary>
/// <param name="item">The dequeued item, or the default element if empty.</param>
/// <returns>True if the item was successfully dequeued, false otherwise.</returns>
public bool Dequeue (out T item) {
int tail = Thread.VolatileRead(ref tailIndex);
if (tail == Thread.VolatileRead(ref headIndex)) {
item = default(T); // queue is empty
return false;
}
item = circularBuffer[tail];
int nextTail = (tail + 1) % capacity;
Thread.VolatileWrite(ref tailIndex, nextTail);
return true;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6dd6c3762358e442b1b446b610e959e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,117 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. Replaces all prior versions.
*
* Copyright (c) 2013-2024, 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.
*****************************************************************************/
#if UNITY_2017_3_OR_NEWER
#define ENABLE_THREAD_PROFILING
#endif
using System;
using System.Collections.Generic;
using System.Threading;
#if ENABLE_THREAD_PROFILING
using UnityEngine.Profiling;
#endif
/// Class to distribute work items like ThreadPool.QueueUserWorkItem but keep the same tasks at the same thread
/// across frames, increasing core affinity (and in some scenarios with lower cache pressure on secondary cores,
/// perhaps even reduce cache eviction).
public class LockFreeWorkerPool<T> : IDisposable {
public class Task {
public T parameters;
public Action<T, int> function;
}
private readonly int _threadCount;
private readonly Thread[] _threads;
private readonly LockFreeSPSCQueue<Task>[] _taskQueues;
private readonly AutoResetEvent[] _taskAvailable;
private volatile bool _running = true;
public LockFreeWorkerPool (int threadCount, int queueCapacity = 2) {
_threadCount = threadCount;
_threads = new Thread[_threadCount];
_taskQueues = new LockFreeSPSCQueue<Task>[_threadCount];
_taskAvailable = new AutoResetEvent[_threadCount];
for (int i = 0; i < _threadCount; i++) {
_taskQueues[i] = new LockFreeSPSCQueue<Task>(queueCapacity);
_taskAvailable[i] = new AutoResetEvent(false);
int index = i; // Capture the index for the thread
_threads[i] = new Thread(() => WorkerLoop(index));
_threads[i].Start();
}
}
/// <summary>Enqueues a task item if there is space available.</summary>
/// <returns>True if the item was successfully enqueued, false otherwise.</returns>
public bool EnqueueTask (int threadIndex, Task task) {
if (threadIndex < 0 || threadIndex >= _threadCount)
throw new ArgumentOutOfRangeException("threadIndex");
bool success = _taskQueues[threadIndex].Enqueue(task);
if (!success) {
return false;
}
_taskAvailable[threadIndex].Set();
return true;
}
private void WorkerLoop (int threadIndex) {
#if ENABLE_THREAD_PROFILING
Profiler.BeginThreadProfiling("Spine Threads", "Spine Thread " + threadIndex);
#endif
while (_running) {
Task task = null;
bool success = _taskQueues[threadIndex].Dequeue(out task);
if (success) {
task.function(task.parameters, threadIndex);
} else {
_taskAvailable[threadIndex].WaitOne();
}
}
#if ENABLE_THREAD_PROFILING
Profiler.EndThreadProfiling();
#endif
}
public void Dispose () {
_running = false;
for (int i = 0; i < _threadCount; i++) {
_taskAvailable[i].Set(); // Wake up threads to exit
}
foreach (var thread in _threads) {
thread.Join();
}
for (int i = 0; i < _threadCount; i++) {
_taskAvailable[i].Close();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 013e48636028fc245a5581519017ff3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,794 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated July 28, 2023. Replaces all prior versions.
*
* Copyright (c) 2013-2024, 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.
*****************************************************************************/
#define USE_THREADED_SKELETON_UPDATE
#define USE_THREADED_ANIMATION_UPDATE // requires USE_THREADED_SKELETON_UPDATE enabled
#define READ_VOLATILE_ONCE
#if UNITY_2017_3_OR_NEWER
//#define ENABLE_THREAD_PROFILING
#endif
#define DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS // enabled improves performance a bit.
//#define RUN_ALL_ON_MAIN_THREAD // for profiling comparison only
//#define RUN_NO_ANIMATION_UPDATE_ON_MAIN_THREAD // actual configuration option. depends, measured slightly better when disabled.
#define RUN_NO_SKELETON_LATEUPDATE_ON_MAIN_THREAD // actual configuration option, recommended enabled
#if NET_4_6
#define HAS_MANUAL_RESET_EVENT_SLIM
#endif
#if USE_THREADED_SKELETON_UPDATE
using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
#if ENABLE_THREAD_PROFILING
using UnityEngine.Profiling;
#endif
#if HAS_MANUAL_RESET_EVENT_SLIM
using ResetEvent = System.Threading.ManualResetEventSlim;
#else
using ResetEvent = System.Threading.ManualResetEvent;
#endif
namespace Spine.Unity {
using WorkerPool = LockFreeWorkerPool<SkeletonUpdateSystem.SkeletonUpdateRange>;
using WorkerPoolTask = LockFreeWorkerPool<SkeletonUpdateSystem.SkeletonUpdateRange>.Task;
[DefaultExecutionOrder(0)]
public class SkeletonUpdateSystem : MonoBehaviour {
private static SkeletonUpdateSystem singletonInstance;
const int TimeoutIterationCount = 10000;
public static SkeletonUpdateSystem Instance {
get {
if (singletonInstance == null) {
singletonInstance = FindObjectOfType<SkeletonUpdateSystem>();
if (singletonInstance == null) {
GameObject singletonGameObject = new GameObject("SkeletonUpdateSystem");
singletonInstance = singletonGameObject.AddComponent<SkeletonUpdateSystem>();
DontDestroyOnLoad(singletonGameObject);
singletonGameObject.hideFlags = HideFlags.DontSave;
}
}
return singletonInstance;
}
}
private void Awake () {
if (singletonInstance == null) {
singletonInstance = this;
DontDestroyOnLoad(gameObject);
}
if (singletonInstance != null && singletonInstance != this) {
Debug.LogWarning("Multiple SkeletonUpdateSystem singleton GameObjects found! " +
"Don't manually add SkeletonUpdateSystem to each scene, it is created automatically when needed.");
Destroy(gameObject);
}
}
private void OnDestroy () {
if (singletonInstance == this)
singletonInstance = null;
}
public static int SkeletonSortComparer (ISkeletonRenderer first, ISkeletonRenderer second) {
return first.Skeleton.Data.GetHashCode() - second.Skeleton.Data.GetHashCode();
}
public static int SkeletonSortComparer (SkeletonAnimationBase first, SkeletonAnimationBase second) {
return first.Skeleton.Data.GetHashCode() - second.Skeleton.Data.GetHashCode();
}
public static readonly Comparison<ISkeletonRenderer> SkeletonRendererComparer = SkeletonSortComparer;
public static readonly Comparison<SkeletonAnimationBase> SkeletonAnimationComparer = SkeletonSortComparer;
public struct SkeletonUpdateRange {
public int rangeStart;
public int rangeEndExclusive;
public int frameCount;
public float deltaTime;
public UpdateTiming updateTiming;
}
public List<SkeletonAnimationBase> skeletonAnimationsUpdate = new List<SkeletonAnimationBase>();
public List<SkeletonAnimationBase> skeletonAnimationsFixedUpdate = new List<SkeletonAnimationBase>();
public List<SkeletonAnimationBase> skeletonAnimationsLateUpdate = new List<SkeletonAnimationBase>();
public List<ISkeletonRenderer> skeletonRenderers = new List<ISkeletonRenderer>();
WorkerPoolTask[] genericSkeletonTasks = null;
public WorkerPool workerPool;
public List<ResetEvent> updateDone = new List<ResetEvent>(4);
public List<ResetEvent> lateUpdateDone = new List<ResetEvent>(4);
#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
protected int[] rendererStartIndex;
protected int[] rendererEndIndexExclusive;
volatile protected int[] skeletonsLateUpdatedAtThread;
protected int[] mainThreadProcessed;
public AutoResetEvent lateUpdateWorkAvailable;
#endif
protected Exception[] exceptions;
protected UnityEngine.Object[] exceptionObjects;
volatile protected int numExceptionsSet = 0;
protected int usedThreadCount = -1;
public void DeferredLogException(Exception exc, UnityEngine.Object context, int threadIndex) {
exceptions[threadIndex] = exc;
exceptionObjects[threadIndex] = context;
numExceptionsSet++;
}
protected bool mainThreadUpdateCallbacks = true;
protected CoroutineIterator[] splitUpdateMethod = null;
int UsedThreadCount {
get {
if (usedThreadCount < 0) {
usedThreadCount = Environment.ProcessorCount;
}
return usedThreadCount;
}
set {
usedThreadCount = value;
}
}
/// <summary>
/// Enable to issue update callbacks (e.g. <see cref="SkeletonAnimationBase.UpdateLocal"/>) always from the
/// main thread, at the cost of splitting overhead switching between main and worker thread.
/// Disable to allow update callbacks from worker threads without splitting execution.
/// </summary>
public bool MainThreadUpdateCallbacks {
set { mainThreadUpdateCallbacks = value; }
get { return mainThreadUpdateCallbacks; }
}
#if USE_THREADED_ANIMATION_UPDATE
public void RegisterForUpdate (UpdateTiming updateTiming, SkeletonAnimationBase skeletonAnimation) {
skeletonAnimation.isUpdatedExternally = true;
var skeletonAnimations = skeletonAnimationsUpdate;
if (updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = skeletonAnimationsFixedUpdate;
else if (updateTiming == UpdateTiming.InLateUpdate) skeletonAnimations = skeletonAnimationsLateUpdate;
if (skeletonAnimations.Contains(skeletonAnimation))
return;
skeletonAnimations.Add(skeletonAnimation);
}
public void UnregisterFromUpdate (UpdateTiming updateTiming, SkeletonAnimationBase skeletonAnimation) {
var skeletonAnimations = skeletonAnimationsUpdate;
if (updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = skeletonAnimationsFixedUpdate;
else if (updateTiming == UpdateTiming.InLateUpdate) skeletonAnimations = skeletonAnimationsLateUpdate;
skeletonAnimations.Remove(skeletonAnimation);
skeletonAnimation.isUpdatedExternally = false;
}
#endif
public void RegisterForUpdate (ISkeletonRenderer renderer) {
renderer.IsUpdatedExternally = true;
if (skeletonRenderers.Contains(renderer))
return;
skeletonRenderers.Add(renderer);
}
public void UnregisterFromUpdate (ISkeletonRenderer renderer) {
skeletonRenderers.Remove(renderer);
renderer.IsUpdatedExternally = false;
}
#if USE_THREADED_ANIMATION_UPDATE
public void Update () {
if (skeletonAnimationsUpdate.Count > 0)
UpdateAsync(skeletonAnimationsUpdate, UpdateTiming.InUpdate);
}
public void FixedUpdate () {
if (skeletonAnimationsFixedUpdate.Count > 0)
UpdateAsync(skeletonAnimationsFixedUpdate, UpdateTiming.InFixedUpdate);
}
#endif
public void LateUpdate () {
#if USE_THREADED_ANIMATION_UPDATE
if (skeletonAnimationsLateUpdate.Count > 0)
UpdateAsync(skeletonAnimationsLateUpdate, UpdateTiming.InLateUpdate);
#endif
LateUpdateAsync();
}
public void UpdateAsync (List<SkeletonAnimationBase> skeletons, UpdateTiming updateTiming) {
if (skeletons.Count == 0) return;
// sort by skeleton data to allow for better cache utilization.
skeletons.Sort(SkeletonAnimationComparer);
int numThreads = UsedThreadCount;
#if RUN_ALL_ON_MAIN_THREAD
int numAsyncThreads = 0;
#elif RUN_NO_ANIMATION_UPDATE_ON_MAIN_THREAD
int numAsyncThreads = numThreads;
#else
int numAsyncThreads = numThreads - 1;
#endif
if (workerPool == null)
workerPool = new WorkerPool(numThreads);
if (genericSkeletonTasks == null) {
genericSkeletonTasks = new WorkerPoolTask[numThreads];
for (int t = 0; t < numThreads; ++t) {
genericSkeletonTasks[t] = new WorkerPoolTask();
}
}
for (int t = 0; t < updateDone.Count; ++t) {
updateDone[t].Reset();
}
if (updateDone.Count != numThreads) {
for (int t = updateDone.Count; t < numThreads; ++t) {
updateDone.Add(new ResetEvent(false));
}
}
if (exceptions == null) {
exceptions = new Exception[numThreads];
exceptionObjects = new UnityEngine.Object[numThreads];
}
numExceptionsSet = 0;
int rangePerThread = Mathf.CeilToInt((float)skeletons.Count / (float)numThreads);
int skeletonEnd = skeletons.Count;
int endIndexThreaded = Math.Min(skeletonEnd, rangePerThread * numAsyncThreads);
MainThreadBeforeUpdate(skeletons, endIndexThreaded);
#if RUN_ALL_ON_MAIN_THREAD
for (int r = 0; r < skeletons.Count; ++r) {
skeletons[r].UpdateInternal(Time.deltaTime, Time.frameCount, calledFromOnlyMainThread: true);
}
#else
if (!mainThreadUpdateCallbacks)
UpdateAsyncThreadedCallbacks(skeletons, updateTiming,
numThreads, numAsyncThreads, rangePerThread, skeletonEnd);
else
UpdateAsyncSplitMainThreadCallbacks(skeletons, updateTiming,
numAsyncThreads, rangePerThread, skeletonEnd, endIndexThreaded);
#endif
MainThreadAfterUpdate(skeletons, endIndexThreaded);
}
protected void UpdateAsyncThreadedCallbacks (List<SkeletonAnimationBase> skeletons, UpdateTiming timing,
int numThreads, int numAsyncThreads, int rangePerThread,
int skeletonEnd) {
int start = 0;
int end = Mathf.Min(rangePerThread, skeletonEnd);
for (int t = 0; t < numThreads; ++t) {
var range = new SkeletonUpdateRange() {
rangeStart = start,
rangeEndExclusive = end,
deltaTime = Time.deltaTime,
frameCount = Time.frameCount,
updateTiming = timing
};
if (t < numAsyncThreads) {
UpdateSkeletonsAsync(range, t);
} else {
// this main thread does some work as well, otherwise it's only waiting.
UpdateSkeletonsSynchronous(skeletons, range);
}
start = end;
if (start >= skeletonEnd) {
while (++t < numAsyncThreads)
updateDone[t].Set();
break;
}
end = Mathf.Min(end + rangePerThread, skeletonEnd);
}
WaitForThreadUpdateTasks(numAsyncThreads);
}
protected void UpdateAsyncSplitMainThreadCallbacks (List<SkeletonAnimationBase> skeletons, UpdateTiming timing,
int numAsyncThreads, int rangePerThread,
int skeletonEnd, int endIndexThreaded) {
if (splitUpdateMethod == null) {
splitUpdateMethod = new CoroutineIterator[skeletons.Count];
}
int requiredCount = endIndexThreaded; //skeletonAnimations.Count;
if (splitUpdateMethod.Length < requiredCount) {
Array.Resize(ref splitUpdateMethod, requiredCount);
}
bool isFirstIteration = true;
bool anyWorkLeft;
int timeoutCounter = 0;
do {
int start = 0;
int end = Mathf.Min(rangePerThread, skeletonEnd);
for (int t = 0; t < numAsyncThreads; ++t) {
var range = new SkeletonUpdateRange() {
rangeStart = start,
rangeEndExclusive = end,
deltaTime = Time.deltaTime,
frameCount = Time.frameCount,
updateTiming = timing
};
UpdateSkeletonsAsyncSplit(range, t);
start = end;
if (start >= skeletonEnd) {
while (++t < numAsyncThreads) {
updateDone[t].Set();
}
break;
}
end = Mathf.Min(end + rangePerThread, skeletonEnd);
}
// main thread
if (isFirstIteration && start != skeletonEnd) {
var range = new SkeletonUpdateRange() {
rangeStart = start,
rangeEndExclusive = end,
deltaTime = Time.deltaTime,
frameCount = Time.frameCount,
updateTiming = timing
};
// this main thread does complete update work in the first iteration, otherwise it's only waiting.
UpdateSkeletonsSynchronous(skeletons, range);
}
// wait for all threaded tasks
WaitForThreadUpdateTasks(numAsyncThreads);
for (int t = 0; t < updateDone.Count; ++t) {
updateDone[t].Reset();
}
// Note: the call above contains calls to ResetEvent.WaitOne, creating implicit memory barriers.
// The explicit memory barrier below is added to ensure a memory barrier is in place on the many
// Unity target platforms.
Thread.MemoryBarrier();
// process main thread callback part
anyWorkLeft = UpdateSkeletonsMainThreadSplit(skeletons, endIndexThreaded, Time.deltaTime, Time.frameCount);
isFirstIteration = false;
} while (anyWorkLeft && ++timeoutCounter < TimeoutIterationCount);
if (timeoutCounter >= TimeoutIterationCount) {
Debug.LogError("Internal threading logic error: exited Update loop after timeout!");
}
for (int i = 0; i < endIndexThreaded; ++i) {
splitUpdateMethod[i] = new CoroutineIterator();
}
}
protected void MainThreadBeforeUpdate (List<SkeletonAnimationBase> skeletons, int endIndexThreaded) {
for (int i = 0; i < endIndexThreaded; ++i) {
skeletons[i].MainThreadBeforeUpdateInternal();
}
}
protected void MainThreadAfterUpdate (List<SkeletonAnimationBase> skeletons, int endIndexThreaded) {
for (int i = 0; i < endIndexThreaded; ++i) {
skeletons[i].MainThreadAfterUpdateInternal();
}
}
public void LateUpdateAsync () {
if (skeletonRenderers.Count == 0) return;
// sort by skeleton data to allow for better cache utilization.
skeletonRenderers.Sort(SkeletonRendererComparer);
int numThreads = UsedThreadCount;
#if RUN_ALL_ON_MAIN_THREAD
int numAsyncThreads = 0;
#elif RUN_NO_SKELETON_LATEUPDATE_ON_MAIN_THREAD
int numAsyncThreads = numThreads;
#else
int numAsyncThreads = numThreads - 1;
#endif
if (workerPool == null)
workerPool = new WorkerPool(numThreads);
if (genericSkeletonTasks == null) {
genericSkeletonTasks = new WorkerPoolTask[numThreads];
for (int t = 0; t < numThreads; ++t) {
genericSkeletonTasks[t] = new WorkerPoolTask();
}
}
for (int t = 0; t < lateUpdateDone.Count; ++t) {
lateUpdateDone[t].Reset();
}
if (lateUpdateDone.Count != numThreads) {
for (int t = lateUpdateDone.Count; t < numThreads; ++t) {
lateUpdateDone.Add(new ResetEvent(false));
}
}
int rangePerThread = Mathf.CeilToInt((float)skeletonRenderers.Count / (float)numThreads);
int skeletonEnd = skeletonRenderers.Count;
#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
if (skeletonsLateUpdatedAtThread == null) {
skeletonsLateUpdatedAtThread = new int[numThreads];
mainThreadProcessed = new int[numThreads];
rendererStartIndex = new int[numThreads];
rendererEndIndexExclusive = new int[numThreads];
lateUpdateWorkAvailable = new AutoResetEvent(false);
}
for (int t = 0; t < numThreads; ++t) {
rendererStartIndex[t] = rangePerThread * t;
rendererEndIndexExclusive[t] = Mathf.Min(rangePerThread * (t + 1), skeletonEnd);
}
for (int t = 0; t < numAsyncThreads; ++t) {
skeletonsLateUpdatedAtThread[t] = 0;
}
#endif
int endIndexThreaded = Math.Min(skeletonEnd, rangePerThread * numAsyncThreads);
MainThreadPrepareLateUpdate(endIndexThreaded);
int start = 0;
int end = Mathf.Min(rangePerThread, skeletonEnd);
for (int t = 0; t < numThreads; ++t) {
var range = new SkeletonUpdateRange() {
rangeStart = start,
rangeEndExclusive = end,
deltaTime = Time.deltaTime,
frameCount = Time.frameCount,
updateTiming = UpdateTiming.InLateUpdate
};
if (t < numAsyncThreads) {
LateUpdateSkeletonsAsync(range, t);
} else {
// this main thread does some work as well, otherwise it's only waiting.
LateUpdateSkeletonsSynchronous(range);
}
start = end;
if (start >= skeletonEnd) {
while (++t < numAsyncThreads)
lateUpdateDone[t].Set();
break;
}
end = Mathf.Min(end + rangePerThread, skeletonEnd);
}
#if RUN_ALL_ON_MAIN_THREAD
return; // nothing left to do after all processed as LateUpdateSkeletonsSynchronous
#endif
#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
for (int t = 0; t < numAsyncThreads; ++t) {
mainThreadProcessed[t] = 0;
}
bool anySkeletonsLeft = false;
bool wasWorkAvailable = false;
bool timedOut = false;
do {
anySkeletonsLeft = false;
for (int t = 0; t < numAsyncThreads; ++t) {
int threadEndIndex = rendererEndIndexExclusive[t] - rendererStartIndex[t];
#if READ_VOLATILE_ONCE
int updatedAtWorkerThread = skeletonsLateUpdatedAtThread[t];
while (mainThreadProcessed[t] < updatedAtWorkerThread) {
#else
while (mainThreadProcessed[t] < skeletonsLateUpdatedAtThread[t]) {
#endif
wasWorkAvailable = true;
int r = mainThreadProcessed[t] + rendererStartIndex[t];
var skeletonRenderer = this.skeletonRenderers[r];
if (skeletonRenderer.RequiresMeshBufferAssignmentMainThread)
skeletonRenderer.UpdateMeshAndMaterialsToBuffers();
mainThreadProcessed[t]++;
}
#if READ_VOLATILE_ONCE
if (updatedAtWorkerThread < threadEndIndex) {
#else
if (skeletonsLateUpdatedAtThread[t] < threadEndIndex) {
#endif
anySkeletonsLeft = true;
}
}
LogWorkerThreadExceptions();
if (!wasWorkAvailable) {
int timeoutMilliseconds = 1000;
timedOut = !lateUpdateWorkAvailable.WaitOne(timeoutMilliseconds);
}
} while (anySkeletonsLeft && !timedOut);
if (timedOut) {
Debug.LogError("Internal threading logic error: exited LateUpdate loop after timeout!");
}
#else
// wait for all threaded task, then process all renderers in main thread
WaitForThreadLateUpdateTasks(numAsyncThreads);
// Additional main thread update when the mesh data could not be assigned from worker thread
// and has to be assigned from main thread.
int maxNonUpdatedRenderer = Math.Min(rangePerThread * numAsyncThreads, this.skeletonRenderers.Count);
for (int r = 0; r < maxNonUpdatedRenderer; ++r) {
var skeletonRenderer = this.skeletonRenderers[r];
if (skeletonRenderer.RequiresMeshBufferAssignmentMainThread)
skeletonRenderer.UpdateMeshAndMaterialsToBuffers();
}
#endif
}
protected void MainThreadPrepareLateUpdate (int endIndexThreaded) {
for (int i = 0; i < endIndexThreaded; ++i) {
skeletonRenderers[i].MainThreadPrepareLateUpdateInternal();
}
}
private void WaitForThreadUpdateTasks (int numAsyncThreads) {
for (int t = 0, n = numAsyncThreads; t < n; ++t) {
int timeoutMilliseconds = 1000;
#if HAS_MANUAL_RESET_EVENT_SLIM
updateDone[t].Wait(timeoutMilliseconds);
#else // HAS_MANUAL_RESET_EVENT_SLIM
updateDone[t].WaitOne(timeoutMilliseconds);
#endif // HAS_MANUAL_RESET_EVENT_SLIM
}
LogWorkerThreadExceptions();
}
private void LogWorkerThreadExceptions () {
if (numExceptionsSet > 0) {
for (int t = 0; t < exceptions.Length; ++t) {
if (exceptions[t] == null) continue;
Debug.LogError(string.Format("Exception in worker thread {0}: {1}.\nStackTrace: {2}",
t, exceptions[t].Message, exceptions[t].StackTrace), exceptionObjects[t]);
exceptions[t] = null;
exceptionObjects[t] = null;
}
numExceptionsSet = 0;
}
}
private void WaitForThreadLateUpdateTasks (int numAsyncThreads) {
for (int t = 0, n = numAsyncThreads; t < n; ++t) {
int timeoutMilliseconds = 1000;
#if HAS_MANUAL_RESET_EVENT_SLIM
lateUpdateDone[t].Wait(timeoutMilliseconds);
#else // HAS_MANUAL_RESET_EVENT_SLIM
lateUpdateDone[t].WaitOne(timeoutMilliseconds);
#endif // HAS_MANUAL_RESET_EVENT_SLIM
}
}
#if ENABLE_THREAD_PROFILING
CustomSampler[] profilerSamplerUpdate = new CustomSampler[16];
CustomSampler[] profilerSamplerLateUpdate = new CustomSampler[16];
#endif
/// <summary>Perform Update at all SkeletonRenderers asynchronously.</summary>
void UpdateSkeletonsAsync (SkeletonUpdateRange range, int threadIndex) {
#if ENABLE_THREAD_PROFILING
if (profilerSamplerUpdate[threadIndex] == null) {
profilerSamplerUpdate[threadIndex] = CustomSampler.Create("Spine Update " + threadIndex);
}
#endif
WorkerPoolTask task = genericSkeletonTasks[threadIndex];
task.parameters = range;
task.function = cachedUpdateSkeletonsAsyncImpl;
bool enqueueSucceeded;
do {
enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]);
} while (!enqueueSucceeded);
}
// avoid allocation, unfortunately this is really necessary
static Action<SkeletonUpdateRange, int> cachedUpdateSkeletonsAsyncImpl = UpdateSkeletonsAsyncImpl;
static void UpdateSkeletonsAsyncImpl (SkeletonUpdateRange range, int threadIndex) {
var instance = Instance;
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerUpdate[threadIndex].Begin();
#endif
float deltaTime = range.deltaTime;
int frameCount = range.frameCount;
int start = range.rangeStart;
int end = range.rangeEndExclusive;
var skeletonAnimations = instance.skeletonAnimationsUpdate;
if (range.updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = instance.skeletonAnimationsFixedUpdate;
else if (range.updateTiming == UpdateTiming.InLateUpdate) skeletonAnimations = instance.skeletonAnimationsLateUpdate;
for (int r = start; r < end; ++r) {
try {
skeletonAnimations[r].UpdateInternal(deltaTime, frameCount, calledFromOnlyMainThread: false);
} catch (Exception exc) {
instance.DeferredLogException(exc, skeletonAnimations[r], threadIndex);
}
}
instance.updateDone[threadIndex].Set();
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerUpdate[threadIndex].End();
#endif
}
//------------------------------------------------------------------------------------------
/// <summary>Perform Update at all SkeletonRenderers asynchronously and split off at
/// main-thread callbacks.</summary>
void UpdateSkeletonsAsyncSplit (SkeletonUpdateRange range, int threadIndex) {
#if ENABLE_THREAD_PROFILING
if (profilerSamplerUpdate[threadIndex] == null) {
profilerSamplerUpdate[threadIndex] = CustomSampler.Create("Spine Update " + threadIndex);
}
#endif
bool enqueueSucceeded;
do {
WorkerPoolTask task = genericSkeletonTasks[threadIndex];
task.parameters = range;
task.function = cachedUpdateSkeletonsAsyncSplitImpl;
enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]);
} while (!enqueueSucceeded);
}
// avoid allocation, unfortunately this is really necessary
static Action<SkeletonUpdateRange, int> cachedUpdateSkeletonsAsyncSplitImpl = UpdateSkeletonsAsyncSplitImpl;
static void UpdateSkeletonsAsyncSplitImpl (SkeletonUpdateRange range, int threadIndex) {
float deltaTime = range.deltaTime;
int frameCount = range.frameCount;
int start = range.rangeStart;
int end = range.rangeEndExclusive;
var instance = Instance;
var skeletonAnimations = instance.skeletonAnimationsUpdate;
if (range.updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = instance.skeletonAnimationsFixedUpdate;
else if (range.updateTiming == UpdateTiming.InLateUpdate) skeletonAnimations = instance.skeletonAnimationsLateUpdate;
var splitUpdateMethod = instance.splitUpdateMethod;
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerUpdate[threadIndex].Begin();
#endif
for (int r = start; r < end; ++r) {
try {
var targetSkeletonAnimation = skeletonAnimations[r];
if (!splitUpdateMethod[r].IsDone) {
splitUpdateMethod[r] = targetSkeletonAnimation.UpdateInternalSplit(splitUpdateMethod[r], deltaTime, frameCount);
}
} catch (Exception exc) {
instance.DeferredLogException(exc, skeletonAnimations[r], threadIndex);
}
}
instance.updateDone[threadIndex].Set();
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerUpdate[threadIndex].End();
#endif
}
bool UpdateSkeletonsMainThreadSplit (List<SkeletonAnimationBase> skeletons, int endIndexThreaded,
float deltaTime, int frameCount) {
bool anyWorkLeft = false;
for (int r = 0; r < endIndexThreaded; ++r) {
try {
var targetSkeletonAnimation = skeletons[r];
if (splitUpdateMethod[r].IsInitialState) {
Debug.LogError("Internal threading logic error: skeletonAnimations never called UpdateInternal before!", skeletons[r]);
} else {
if (!splitUpdateMethod[r].IsDone) {
anyWorkLeft = true;
splitUpdateMethod[r] = targetSkeletonAnimation.UpdateInternalSplit(splitUpdateMethod[r], deltaTime, frameCount);
}
}
} catch (Exception exc) {
Debug.LogError(string.Format("Exception in main thread: {0}.\nStackTrace: {1}",
exc.Message, exc.StackTrace));
}
}
return anyWorkLeft;
}
void UpdateSkeletonsSynchronous (List<SkeletonAnimationBase> skeletons, SkeletonUpdateRange range) {
int start = range.rangeStart;
int end = range.rangeEndExclusive;
for (int r = start; r < end; ++r) {
skeletons[r].UpdateInternal(range.deltaTime, range.frameCount, calledFromOnlyMainThread: true);
}
}
/// <summary>Perform LateUpdate at all SkeletonRenderers asynchronously.</summary>
static Action<SkeletonUpdateRange, int> cachedLateUpdateSkeletonsAsyncImpl = LateUpdateSkeletonsAsyncImpl;
void LateUpdateSkeletonsAsync (SkeletonUpdateRange range, int threadIndex) {
#if ENABLE_THREAD_PROFILING
if (profilerSamplerLateUpdate[threadIndex] == null) {
profilerSamplerLateUpdate[threadIndex] = CustomSampler.Create("Spine LateUpdate " + threadIndex);
}
#endif
bool enqueueSucceeded;
WorkerPoolTask task = genericSkeletonTasks[threadIndex];
task.parameters = range;
task.function = cachedLateUpdateSkeletonsAsyncImpl;
do {
enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]);
} while (!enqueueSucceeded);
}
static void LateUpdateSkeletonsAsyncImpl (SkeletonUpdateRange range, int threadIndex) {
int start = range.rangeStart;
int end = range.rangeEndExclusive;
var instance = Instance;
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerLateUpdate[threadIndex].Begin();
#endif
#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
instance.skeletonsLateUpdatedAtThread[threadIndex] = 0;
#endif
for (int r = start; r < end; ++r) {
try {
instance.skeletonRenderers[r].LateUpdateImplementation(calledFromMainThread: false);
} catch (Exception exc) {
instance.DeferredLogException(exc, instance.skeletonRenderers[r].Component, threadIndex);
}
#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
Interlocked.Increment(ref instance.skeletonsLateUpdatedAtThread[threadIndex]);
instance.lateUpdateWorkAvailable.Set(); // signal as soon as it can be processed by main thread
#endif
}
#if !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS
instance.lateUpdateDone[threadIndex].Set(); // signal once after all work is done
#endif
#if ENABLE_THREAD_PROFILING
instance.profilerSamplerLateUpdate[threadIndex].End();
#endif
}
void LateUpdateSkeletonsSynchronous (SkeletonUpdateRange range) {
int start = range.rangeStart;
int end = range.rangeEndExclusive;
for (int r = start; r < end; ++r) {
skeletonRenderers[r].LateUpdateImplementation(calledFromMainThread: true);
}
}
}
}
#endif // USE_THREADED_SKELETON_UPDATE

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9a1427d268719124ab5d9841d5f4ddd7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

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.
*****************************************************************************/
namespace Spine.Unity {
/// <summary>
/// Allocation-free IEnumerator alternative for manual iteration.
/// </summary>
public struct CoroutineIterator {
readonly uint state;
const uint DoneState = uint.MaxValue;
public static readonly CoroutineIterator Done = new CoroutineIterator(DoneState);
public bool IsDone { get { return state == DoneState; } }
public bool IsInitialState { get { return state == 0; } }
public CoroutineIterator (uint state = 0) {
this.state = state;
}
public uint State (uint mask) {
return state & mask;
}
public CoroutineIterator NextState () {
return new CoroutineIterator(state + 1);
}
public CoroutineIterator YieldReturnAtState (uint callerState, uint mask) {
return new CoroutineIterator((state & ~mask) | callerState);
}
public CoroutineIterator ToNestedCall (int numCallerMaskBits) {
return new CoroutineIterator(state >> numCallerMaskBits);
}
public CoroutineIterator FromNestedCall (uint callerState, int numCallerMaskBits) {
return new CoroutineIterator(state << numCallerMaskBits | callerState);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 77d255700a6ba9d4f82263ff498ebba9
timeCreated: 1752007001
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,48 @@
/******************************************************************************
* 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.
*****************************************************************************/
#if UNITY_EDITOR
using System;
using UnityEngine;
namespace Spine.Unity {
public static class EditorBridge {
/// <summary>For editor script to subscribe.</summary>
public static event Action<GameObject> OnRequestMarkDirty;
public static void RequestMarkDirty (GameObject go) {
if (OnRequestMarkDirty != null)
OnRequestMarkDirty.Invoke(go);
}
}
}
#endif

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 5edab187969d1f84d866247775a4e1aa
timeCreated: 1759780987
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -139,7 +139,7 @@ namespace Spine.Unity {
bool isProblematic = false;
if (material) {
isProblematic |= IsMaterialSetupProblematic(material, ref errorMessage);
MeshGenerator.Settings settings = skeletonGraphic.MeshGenerator.settings;
MeshGenerator.Settings settings = skeletonGraphic.MeshSettings;
if (settings.zSpacing == 0) {
isProblematic |= IsZSpacingRequired(material, ref errorMessage);
}
@ -343,7 +343,7 @@ namespace Spine.Unity {
Canvas canvas = skeletonGraphic.canvas;
if (!canvas)
return false;
var requiredChannels =
AdditionalCanvasShaderChannels requiredChannels =
AdditionalCanvasShaderChannels.TexCoord1 |
AdditionalCanvasShaderChannels.TexCoord2;
return (canvas.additionalShaderChannels & requiredChannels) != requiredChannels;

View File

@ -0,0 +1,70 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System.IO;
using UnityEngine;
namespace Spine.Unity {
/// <summary>
/// Project settings stored in a ScriptableObject for use in the build at runtime.
/// Writing the ScriptableObject asset is performed by <see cref="RuntimeSettingsEditor"/>,
/// triggered by preferences setting changes.
/// </summary>
public class RuntimeSettings : ScriptableObject {
public bool useThreadedMeshGeneration = false;
public static bool UseThreadedMeshGeneration {
get { return Instance.useThreadedMeshGeneration; }
set { Instance.useThreadedMeshGeneration = value; }
}
public bool useThreadedAnimation = false;
public static bool UseThreadedAnimation{
get { return Instance.useThreadedAnimation; }
set { Instance.useThreadedAnimation = value; }
}
/// <summary>Path relative to "Assets/Resources".</summary>
public const string ResourcePath = "SpineRuntimeSettings";
private static RuntimeSettings singletonInstance;
public static RuntimeSettings Instance {
get {
if (singletonInstance == null) {
singletonInstance = Resources.Load<RuntimeSettings>(ResourcePath);
}
if (singletonInstance == null) {
singletonInstance = CreateInstance<RuntimeSettings>();
}
return singletonInstance;
}
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4a6a3378dfca7914d8ba2b337350cab8
timeCreated: 1752096299
licenseType: Free
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -34,6 +34,7 @@ namespace Spine.Unity {
/// a third <c>UseGlobalSettings</c> state. Automatically maps serialized
/// bool values to corresponding <c>Disable</c> and <c>Enable</c> states.
/// </summary>
[System.Serializable]
public enum SettingsTriState {
Disable,
Enable,

View File

@ -88,7 +88,7 @@ namespace Spine.Unity {
public WaitForSpineEvent (SkeletonAnimation skeletonAnimation, Spine.EventData eventDataReference, bool unsubscribeAfterFiring = true) {
// If skeletonAnimation is invalid, its state will be null. Subscribe handles null states just fine.
Subscribe(skeletonAnimation.state, eventDataReference, unsubscribeAfterFiring);
Subscribe(skeletonAnimation.AnimationState, eventDataReference, unsubscribeAfterFiring);
}
public WaitForSpineEvent (Spine.AnimationState state, string eventName, bool unsubscribeAfterFiring = true) {
@ -97,7 +97,7 @@ namespace Spine.Unity {
public WaitForSpineEvent (SkeletonAnimation skeletonAnimation, string eventName, bool unsubscribeAfterFiring = true) {
// If skeletonAnimation is invalid, its state will be null. Subscribe handles null states just fine.
SubscribeByName(skeletonAnimation.state, eventName, unsubscribeAfterFiring);
SubscribeByName(skeletonAnimation.AnimationState, eventName, unsubscribeAfterFiring);
}
#endregion

View File

@ -69,7 +69,7 @@ namespace Spine.Unity.Examples {
skeletonAnimation.OnRebuild += Apply;
}
void Apply (SkeletonRenderer skeletonRenderer) {
void Apply (ISkeletonRenderer skeletonRenderer) {
StartCoroutine(Blink());
}

View File

@ -27,6 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#define USE_THREADED_ANIMATION_UPDATE
using Spine.Unity;
using System.Collections;
using UnityEngine;
@ -34,7 +36,7 @@ using UnityEngine;
namespace Spine.Unity.Examples {
public class SpineboyBeginnerView : MonoBehaviour {
#region Inspector
#region Inspector
[Header("Components")]
public SpineboyBeginnerModel model;
public SkeletonAnimation skeletonAnimation;
@ -49,7 +51,7 @@ namespace Spine.Unity.Examples {
[Header("Effects")]
public ParticleSystem gunParticles;
#endregion
#endregion
SpineBeginnerBodyState previousViewState;
@ -58,7 +60,11 @@ namespace Spine.Unity.Examples {
model.ShootEvent += PlayShoot;
model.StartAimEvent += StartPlayingAim;
model.StopAimEvent += StopPlayingAim;
#if USE_THREADED_ANIMATION_UPDATE
skeletonAnimation.MainThreadEvent += HandleEvent;
#else
skeletonAnimation.AnimationState.Event += HandleEvent;
#endif
}
void HandleEvent (Spine.TrackEntry trackEntry, Spine.Event e) {
@ -120,19 +126,19 @@ namespace Spine.Unity.Examples {
Debug.Log(state.GetCurrent(1));
}
#region Transient Actions
#region Transient Actions
public void PlayShoot () {
// Play the shoot animation on track 1.
TrackEntry shootTrack = skeletonAnimation.AnimationState.SetAnimation(1, shoot, false);
shootTrack.MixAttachmentThreshold = 1f;
shootTrack.SetMixDuration(0f, 0f);
skeletonAnimation.state.AddEmptyAnimation(1, 0.5f, 0.1f);
skeletonAnimation.AnimationState.AddEmptyAnimation(1, 0.5f, 0.1f);
// Play the aim animation on track 2 to aim at the mouse target.
TrackEntry aimTrack = skeletonAnimation.AnimationState.SetAnimation(2, aim, false);
aimTrack.MixAttachmentThreshold = 1f;
aimTrack.SetMixDuration(0f, 0f);
skeletonAnimation.state.AddEmptyAnimation(2, 0.5f, 0.1f);
skeletonAnimation.AnimationState.AddEmptyAnimation(2, 0.5f, 0.1f);
gunSource.pitch = GetRandomPitch(gunsoundPitchOffset);
gunSource.Play();
@ -148,20 +154,20 @@ namespace Spine.Unity.Examples {
}
public void StopPlayingAim () {
skeletonAnimation.state.AddEmptyAnimation(2, 0.5f, 0.1f);
skeletonAnimation.AnimationState.AddEmptyAnimation(2, 0.5f, 0.1f);
}
public void Turn (bool facingLeft) {
skeletonAnimation.Skeleton.ScaleX = facingLeft ? -1f : 1f;
// Maybe play a transient turning animation too, then call ChangeStableAnimation.
}
#endregion
#endregion
#region Utility
#region Utility
public float GetRandomPitch (float maxPitchOffset) {
return 1f + Random.Range(-maxPitchOffset, maxPitchOffset);
}
#endregion
#endregion
}
}

View File

@ -1,167 +0,0 @@
/******************************************************************************
* 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.
*****************************************************************************/
using Spine.Unity;
using System.Collections;
using UnityEngine;
namespace Spine.Unity.Examples {
public class SpineboyBeginnerViewGraphic : MonoBehaviour {
#region Inspector
[Header("Components")]
public SpineboyBeginnerModel model;
public SkeletonGraphic skeletonGraphic;
public AnimationReferenceAsset run, idle, aim, shoot, jump;
public EventDataReferenceAsset footstepEvent;
[Header("Audio")]
public float footstepPitchOffset = 0.2f;
public float gunsoundPitchOffset = 0.13f;
public AudioSource footstepSource, gunSource, jumpSource;
[Header("Effects")]
public ParticleSystem gunParticles;
#endregion
SpineBeginnerBodyState previousViewState;
void Start () {
if (skeletonGraphic == null) return;
model.ShootEvent += PlayShoot;
model.StartAimEvent += StartPlayingAim;
model.StopAimEvent += StopPlayingAim;
skeletonGraphic.AnimationState.Event += HandleEvent;
}
void HandleEvent (Spine.TrackEntry trackEntry, Spine.Event e) {
if (e.Data == footstepEvent.EventData)
PlayFootstepSound();
}
void Update () {
if (skeletonGraphic == null) return;
if (model == null) return;
if ((skeletonGraphic.Skeleton.ScaleX < 0) != model.facingLeft) { // Detect changes in model.facingLeft
Turn(model.facingLeft);
}
// Detect changes in model.state
SpineBeginnerBodyState currentModelState = model.state;
if (previousViewState != currentModelState) {
PlayNewStableAnimation();
}
previousViewState = currentModelState;
}
void PlayNewStableAnimation () {
SpineBeginnerBodyState newModelState = model.state;
Animation nextAnimation;
// Add conditionals to not interrupt transient animations.
if (previousViewState == SpineBeginnerBodyState.Jumping && newModelState != SpineBeginnerBodyState.Jumping) {
PlayFootstepSound();
}
if (newModelState == SpineBeginnerBodyState.Jumping) {
jumpSource.Play();
nextAnimation = jump;
} else {
if (newModelState == SpineBeginnerBodyState.Running) {
nextAnimation = run;
} else {
nextAnimation = idle;
}
}
skeletonGraphic.AnimationState.SetAnimation(0, nextAnimation, true);
}
void PlayFootstepSound () {
footstepSource.Play();
footstepSource.pitch = GetRandomPitch(footstepPitchOffset);
}
[ContextMenu("Check Tracks")]
void CheckTracks () {
AnimationState state = skeletonGraphic.AnimationState;
Debug.Log(state.GetCurrent(0));
Debug.Log(state.GetCurrent(1));
}
#region Transient Actions
public void PlayShoot () {
// Play the shoot animation on track 1.
TrackEntry shootTrack = skeletonGraphic.AnimationState.SetAnimation(1, shoot, false);
shootTrack.MixAttachmentThreshold = 1f;
shootTrack.SetMixDuration(0f, 0f);
skeletonGraphic.AnimationState.AddEmptyAnimation(1, 0.5f, 0.1f);
// Play the aim animation on track 2 to aim at the mouse target.
TrackEntry aimTrack = skeletonGraphic.AnimationState.SetAnimation(2, aim, false);
aimTrack.MixAttachmentThreshold = 1f;
aimTrack.SetMixDuration(0f, 0f);
skeletonGraphic.AnimationState.AddEmptyAnimation(2, 0.5f, 0.1f);
gunSource.pitch = GetRandomPitch(gunsoundPitchOffset);
gunSource.Play();
//gunParticles.randomSeed = (uint)Random.Range(0, 100);
gunParticles.Play();
}
public void StartPlayingAim () {
// Play the aim animation on track 2 to aim at the mouse target.
TrackEntry aimTrack = skeletonGraphic.AnimationState.SetAnimation(2, aim, true);
aimTrack.MixAttachmentThreshold = 1f;
aimTrack.SetMixDuration(0f, 0f); // use SetMixDuration(mixDuration, delay) to update delay correctly
}
public void StopPlayingAim () {
skeletonGraphic.AnimationState.AddEmptyAnimation(2, 0.5f, 0.1f);
}
public void Turn (bool facingLeft) {
skeletonGraphic.Skeleton.ScaleX = facingLeft ? -1f : 1f;
// Maybe play a transient turning animation too, then call ChangeStableAnimation.
}
#endregion
#region Utility
public float GetRandomPitch (float maxPitchOffset) {
return 1f + Random.Range(-maxPitchOffset, maxPitchOffset);
}
#endregion
}
}

View File

@ -47,7 +47,7 @@ namespace Spine.Unity.Examples {
}
// This is called after the animation is applied to the skeleton and can be used to adjust the bones dynamically.
public void UpdateLocal (ISkeletonAnimation skeletonRenderer) {
public void UpdateLocal (ISkeletonRenderer skeletonRenderer) {
headBone.Pose.Rotation += extraRotation;
}

View File

@ -27,6 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#define USE_THREADED_ANIMATION_UPDATE
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
@ -58,10 +60,14 @@ namespace Spine.Unity.Examples {
if (audioSource == null) return;
if (skeletonAnimation == null) return;
skeletonAnimation.Initialize(false);
if (!skeletonAnimation.valid) return;
if (!skeletonAnimation.IsValid) return;
eventData = skeletonAnimation.Skeleton.Data.FindEvent(eventName);
#if USE_THREADED_ANIMATION_UPDATE
skeletonAnimation.MainThreadEvent += HandleAnimationStateEvent;
#else
skeletonAnimation.AnimationState.Event += HandleAnimationStateEvent;
#endif
}
private void HandleAnimationStateEvent (TrackEntry trackEntry, Event e) {

View File

@ -30,6 +30,7 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Spine.Unity.Examples {
public class MaterialReplacementExample : MonoBehaviour {
@ -38,6 +39,7 @@ namespace Spine.Unity.Examples {
public Material replacementMaterial;
public bool replacementEnabled = true;
public SkeletonAnimation skeletonAnimation;
public SkeletonRenderer skeletonRenderer;
[Space]
public string phasePropertyName = "_FillPhase";
@ -47,6 +49,8 @@ namespace Spine.Unity.Examples {
MaterialPropertyBlock mpb;
void Start () {
if (skeletonRenderer == null && skeletonAnimation != null)
skeletonRenderer = skeletonAnimation.GetComponent<SkeletonRenderer>();
// Use the code below to programmatically query the original material.
// Note: using MeshRenderer.material will fail since it creates an instance copy of the Material,
// MeshRenderer.sharedMaterial might also fail when called too early or when no Attachments
@ -72,9 +76,9 @@ namespace Spine.Unity.Examples {
void SetReplacementEnabled (bool active) {
if (replacementEnabled) {
skeletonAnimation.CustomMaterialOverride[originalMaterial] = replacementMaterial;
skeletonRenderer.CustomMaterialOverride[originalMaterial] = replacementMaterial;
} else {
skeletonAnimation.CustomMaterialOverride.Remove(originalMaterial);
skeletonRenderer.CustomMaterialOverride.Remove(originalMaterial);
}
}

View File

@ -135,7 +135,7 @@ namespace Spine.Unity.Examples {
//skeleton.SetupPoseSlots();
skeleton.SetupPose();
skeletonGraphic.Update(0);
skeletonGraphic.Animation.Update(0);
skeletonGraphic.OverrideTexture = runtimeAtlas;
// `GetRepackedSkin()` and each call to `GetRemappedClone()` with parameter `premultiplyAlpha` set to `true`

View File

@ -81,7 +81,9 @@ namespace Spine.Unity.Examples {
}
void InstantiateSkeletonAnimation () {
runtimeSkeletonAnimation = SkeletonAnimation.NewSkeletonAnimationGameObject(runtimeSkeletonDataAsset);
SkeletonComponents<SkeletonRenderer, SkeletonAnimation> components
= SkeletonAnimation.NewSkeletonAnimationGameObject(runtimeSkeletonDataAsset);
runtimeSkeletonAnimation = components.skeletonAnimation;
runtimeSkeletonAnimation.transform.parent = transform;
runtimeSkeletonAnimation.name = "SkeletonAnimation Instance";
@ -98,7 +100,9 @@ namespace Spine.Unity.Examples {
Canvas canvas = this.GetComponentInChildren<Canvas>();
Transform parent = canvas.transform;
runtimeSkeletonGraphic = SkeletonGraphic.NewSkeletonGraphicGameObject(runtimeSkeletonDataAsset, parent, skeletonGraphicMaterial);
SkeletonComponents<SkeletonGraphic, SkeletonAnimation> components =
SkeletonGraphic.NewSkeletonGraphicGameObject(runtimeSkeletonDataAsset, parent, skeletonGraphicMaterial);
runtimeSkeletonGraphic = components.skeletonRenderer;
runtimeSkeletonGraphic.name = "SkeletonGraphic Instance";
if (blendModeMaterials) {
@ -114,7 +118,7 @@ namespace Spine.Unity.Examples {
runtimeSkeletonGraphic.Skeleton.SetSkin(skinName);
runtimeSkeletonGraphic.Skeleton.SetupPoseSlots();
if (animationName != "")
runtimeSkeletonGraphic.AnimationState.SetAnimation(0, animationName, true);
components.skeletonAnimation.AnimationState.SetAnimation(0, animationName, true);
}
}
}

View File

@ -47,13 +47,13 @@ namespace Spine.Unity.Examples {
public bool overrideRotation = true;
[Range(0, 360)] public float rotation = 0;
ISkeletonAnimation spineComponent;
ISkeletonRenderer spineComponent;
Bone bone;
#if UNITY_EDITOR
void OnValidate () {
if (Application.isPlaying) return;
if (spineComponent == null) spineComponent = GetComponent<ISkeletonAnimation>();
if (spineComponent == null) spineComponent = GetComponent<ISkeletonRenderer>();
if (spineComponent.IsNullOrDestroyed()) return;
if (bone != null) bone.SetupPose();
OverrideLocal(spineComponent);
@ -61,14 +61,14 @@ namespace Spine.Unity.Examples {
#endif
void Awake () {
spineComponent = GetComponent<ISkeletonAnimation>();
spineComponent = GetComponent<ISkeletonRenderer>();
if (spineComponent == null) { this.enabled = false; return; }
spineComponent.UpdateLocal += OverrideLocal;
if (bone == null) { this.enabled = false; return; }
}
void OverrideLocal (ISkeletonAnimation animated) {
void OverrideLocal (ISkeletonRenderer skeletonRenderer) {
if (bone == null || bone.Data.Name != boneName) {
if (string.IsNullOrEmpty(boneName)) return;
bone = spineComponent.Skeleton.FindBone(boneName);

View File

@ -55,12 +55,12 @@ namespace Spine.Unity.Examples {
if (skeletonRenderer.valid) Apply(skeletonRenderer);
}
void Apply (SkeletonRenderer skeletonRenderer) {
void Apply (ISkeletonRenderer skeletonRenderer) {
if (!this.enabled) return;
atlas = atlasAsset.GetAtlas();
if (atlas == null) return;
float scale = skeletonRenderer.skeletonDataAsset.scale;
float scale = skeletonRenderer.SkeletonDataAsset.scale;
foreach (SlotRegionPair entry in attachments) {
Slot slot = skeletonRenderer.Skeleton.FindSlot(entry.slot);

View File

@ -55,7 +55,7 @@ namespace Spine.Unity.Examples {
applyPMA = skeletonRenderer.pmaVertexColors;
} else {
SkeletonGraphic skeletonGraphic = skeletonComponent as SkeletonGraphic;
applyPMA = skeletonGraphic != null && skeletonGraphic.MeshGenerator.settings.pmaVertexColors;
applyPMA = skeletonGraphic != null && skeletonGraphic.MeshSettings.pmaVertexColors;
}
if (applyPMA) {
@ -97,7 +97,7 @@ namespace Spine.Unity.Examples {
Attach();
}
void AnimationOverrideSpriteAttach (ISkeletonAnimation animated) {
void AnimationOverrideSpriteAttach (ISkeletonRenderer skeletonRenderer) {
if (overrideAnimation && isActiveAndEnabled)
Attach();
}
@ -105,26 +105,16 @@ namespace Spine.Unity.Examples {
public void Initialize (bool overwrite = true) {
if (overwrite || attachment == null) {
// Get the applyPMA value.
ISkeletonComponent skeletonComponent = GetComponent<ISkeletonComponent>();
SkeletonRenderer skeletonRenderer = skeletonComponent as SkeletonRenderer;
if (skeletonRenderer != null)
this.applyPMA = skeletonRenderer.pmaVertexColors;
else {
SkeletonGraphic skeletonGraphic = skeletonComponent as SkeletonGraphic;
if (skeletonGraphic != null)
this.applyPMA = skeletonGraphic.MeshGenerator.settings.pmaVertexColors;
}
// Subscribe to UpdateComplete to override animation keys.
if (overrideAnimation) {
ISkeletonAnimation animatedSkeleton = skeletonComponent as ISkeletonAnimation;
if (animatedSkeleton != null) {
animatedSkeleton.UpdateComplete -= AnimationOverrideSpriteAttach;
animatedSkeleton.UpdateComplete += AnimationOverrideSpriteAttach;
ISkeletonRenderer skeletonRenderer = GetComponent<ISkeletonRenderer>();
if (skeletonRenderer != null) {
this.applyPMA = skeletonRenderer.MeshSettings.pmaVertexColors;
// Subscribe to UpdateComplete to override animation keys.
if (overrideAnimation) {
skeletonRenderer.UpdateComplete -= AnimationOverrideSpriteAttach;
skeletonRenderer.UpdateComplete += AnimationOverrideSpriteAttach;
}
}
spineSlot = spineSlot ?? skeletonComponent.Skeleton.FindSlot(slot);
spineSlot = spineSlot ?? skeletonRenderer.Skeleton.FindSlot(slot);
Shader attachmentShader = applyPMA ? Shader.Find(DefaultPMAShader) : Shader.Find(DefaultStraightAlphaShader);
if (sprite == null)
attachment = null;

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