策划项目初始化

This commit is contained in:
zhang hongbo 2026-02-01 15:37:46 +08:00
parent f167beb4bd
commit e7acf9a8b2
608 changed files with 68156 additions and 422 deletions

@ -1 +1 @@
Subproject commit 10480ba573cacd75c1496737614d2d0d26147533 Subproject commit e78462ce1c95bb74a5ee52ea505cd57a73138369

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -0,0 +1,421 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
namespace ThriftPipelineTools
{
/// <summary>
/// Excel 查看器 - 用于学习和测试 EPPlus 的 Excel 读取功能
///
/// 功能:
/// 1. 选择 Excel 文件 (.xlsx)
/// 2. 加载并显示所有工作表 (Sheet)
/// 3. 点击工作表查看数据内容
/// 4. 实时预览 Excel 数据结构
///
/// 学习目的:
/// - 理解 EPPlus 的基本用法
/// - 了解 Excel 文件的结构Workbook、Worksheet、Cells
/// - 掌握单元格数据的读取方法
/// </summary>
public class EPPlusTestEditor : EditorWindow
{
// ========== 文件路径 ==========
private string excelFilePath = ""; // 选中的Excel文件路径
// ========== Excel 数据 ==========
private List<string> sheetNames = new List<string>(); // 所有工作表名称
private int selectedSheetIndex = 0; // 当前选中的工作表索引
private string[,] sheetData = null; // 当前工作表的数据(二维数组)
private int maxRows = 0; // 当前工作表的最大行数
private int maxCols = 0; // 当前工作表的最大列数
// ========== UI 状态 ==========
private Vector2 sheetListScrollPosition; // 工作表列表滚动位置
private Vector2 dataScrollPosition; // 数据表格滚动位置
private bool isFileLoaded = false; // 是否已加载文件
private string statusMessage = "请选择一个 Excel 文件"; // 状态消息
// ========== UI 配置 ==========
private const float CELL_WIDTH = 120f; // 单元格显示宽度
private const float CELL_HEIGHT = 20f; // 单元格显示高度
private const int MAX_DISPLAY_ROWS = 100; // 最多显示行数(避免性能问题)
private const int MAX_DISPLAY_COLS = 50; // 最多显示列数
/// <summary>
/// Unity 菜单项:打开 Excel 查看器窗口
/// </summary>
[MenuItem("蹊径/Thrift/Excel 查看器")]
public static void ShowWindow()
{
var window = GetWindow<EPPlusTestEditor>("Excel 查看器");
window.minSize = new Vector2(800, 600);
window.Show();
}
/// <summary>
/// 绘制 Editor 窗口 UI
/// </summary>
private void OnGUI()
{
EditorGUILayout.Space(10);
// ===== 标题区域 =====
EditorGUILayout.LabelField("EPPlus Excel 读取测试工具", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"此工具用于测试 EPPlus 读取 Excel 文件的功能\n" +
"1. 选择 .xlsx 文件\n" +
"2. 点击「加载文件」查看所有工作表\n" +
"3. 选择工作表查看数据内容",
MessageType.Info
);
EditorGUILayout.Space(10);
// ===== 文件选择区域 =====
DrawFileSelection();
EditorGUILayout.Space(10);
// ===== 状态信息 =====
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("状态:", statusMessage, EditorStyles.miniLabel);
if (isFileLoaded)
{
EditorGUILayout.LabelField($"工作表数量: {sheetNames.Count}", EditorStyles.miniLabel);
if (sheetData != null)
{
EditorGUILayout.LabelField($"当前表格: {maxRows} 行 × {maxCols} 列", EditorStyles.miniLabel);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// ===== 主内容区域 =====
if (isFileLoaded)
{
DrawMainContent();
}
}
/// <summary>
/// 绘制文件选择区域
/// </summary>
private void DrawFileSelection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Excel 文件选择", EditorStyles.boldLabel);
// 文件路径输入框和浏览按钮
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("文件路径:", GUILayout.Width(70));
excelFilePath = EditorGUILayout.TextField(excelFilePath);
if (GUILayout.Button("浏览...", GUILayout.Width(70)))
{
string selected = EditorUtility.OpenFilePanel("选择 Excel 文件", "", "xlsx");
if (!string.IsNullOrEmpty(selected))
{
excelFilePath = selected;
}
}
EditorGUILayout.EndHorizontal();
// 加载按钮
EditorGUILayout.BeginHorizontal();
GUI.enabled = !string.IsNullOrEmpty(excelFilePath) && File.Exists(excelFilePath);
if (GUILayout.Button("🔄 加载文件", GUILayout.Height(30)))
{
LoadExcelFile();
}
// 刷新按钮(仅在已加载时显示)
if (isFileLoaded)
{
if (GUILayout.Button("🔃 刷新数据", GUILayout.Height(30)))
{
LoadExcelFile();
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制主要内容区域(工作表列表 + 数据表格)
/// </summary>
private void DrawMainContent()
{
EditorGUILayout.BeginHorizontal();
// ===== 左侧:工作表列表 =====
EditorGUILayout.BeginVertical("box", GUILayout.Width(200));
EditorGUILayout.LabelField("工作表列表", EditorStyles.boldLabel);
sheetListScrollPosition = EditorGUILayout.BeginScrollView(
sheetListScrollPosition,
GUILayout.ExpandHeight(true)
);
for (int i = 0; i < sheetNames.Count; i++)
{
// 高亮显示选中的工作表
bool isSelected = (i == selectedSheetIndex);
GUI.backgroundColor = isSelected ? Color.cyan : Color.white;
if (GUILayout.Button($"📄 {sheetNames[i]}", GUILayout.Height(25)))
{
selectedSheetIndex = i;
LoadSheetData(i);
}
GUI.backgroundColor = Color.white;
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
// ===== 右侧:数据表格 =====
EditorGUILayout.BeginVertical("box", GUILayout.ExpandWidth(true));
if (sheetData != null)
{
EditorGUILayout.LabelField(
$"工作表: {sheetNames[selectedSheetIndex]}",
EditorStyles.boldLabel
);
DrawDataTable();
}
else
{
EditorGUILayout.LabelField("请选择一个工作表查看数据", EditorStyles.centeredGreyMiniLabel);
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 绘制数据表格
/// </summary>
private void DrawDataTable()
{
if (sheetData == null || maxRows == 0 || maxCols == 0)
return;
dataScrollPosition = EditorGUILayout.BeginScrollView(
dataScrollPosition,
GUILayout.ExpandWidth(true),
GUILayout.ExpandHeight(true)
);
// 限制显示的行列数
int displayRows = Mathf.Min(maxRows, MAX_DISPLAY_ROWS);
int displayCols = Mathf.Min(maxCols, MAX_DISPLAY_COLS);
// 提示信息(如果数据被截断)
if (maxRows > MAX_DISPLAY_ROWS || maxCols > MAX_DISPLAY_COLS)
{
EditorGUILayout.HelpBox(
$"数据过大,仅显示前 {displayRows} 行 × {displayCols} 列",
MessageType.Warning
);
}
// 使用 GUILayout 绘制表格
EditorGUILayout.BeginVertical();
for (int row = 0; row < displayRows; row++)
{
EditorGUILayout.BeginHorizontal();
// 行号
EditorGUILayout.LabelField(
$"{row + 1}",
GUILayout.Width(40)
);
// 每列的数据
for (int col = 0; col < displayCols; col++)
{
string cellValue = sheetData[row, col] ?? "";
// 第一行(表头)使用不同样式
if (row == 0)
{
GUI.backgroundColor = new Color(0.8f, 0.9f, 1f);
EditorGUILayout.LabelField(
cellValue,
EditorStyles.boldLabel,
GUILayout.Width(CELL_WIDTH),
GUILayout.Height(CELL_HEIGHT)
);
GUI.backgroundColor = Color.white;
}
else
{
EditorGUILayout.LabelField(
cellValue,
GUILayout.Width(CELL_WIDTH),
GUILayout.Height(CELL_HEIGHT)
);
}
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 加载 Excel 文件,获取所有工作表名称
/// </summary>
private void LoadExcelFile()
{
try
{
if (!File.Exists(excelFilePath))
{
statusMessage = "❌ 文件不存在!";
Debug.LogError($"文件不存在: {excelFilePath}");
return;
}
// 清空之前的数据
sheetNames.Clear();
sheetData = null;
selectedSheetIndex = 0;
// 使用 EPPlus 读取 Excel
FileInfo fileInfo = new FileInfo(excelFilePath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
// 获取所有工作表名称
// 注意使用名称访问更可靠Worksheets集合的索引可能不是从0开始
Debug.Log($"[EPPlus测试] Worksheets.Count = {package.Workbook.Worksheets.Count}");
foreach (ExcelWorksheet worksheet in package.Workbook.Worksheets)
{
sheetNames.Add(worksheet.Name);
Debug.Log($"[EPPlus测试] 发现工作表: '{worksheet.Name}'");
}
if (sheetNames.Count == 0)
{
statusMessage = "⚠️ Excel 文件中没有工作表";
isFileLoaded = false;
return;
}
isFileLoaded = true;
statusMessage = $"✅ 成功加载文件,共 {sheetNames.Count} 个工作表";
// 自动加载第一个工作表
LoadSheetData(0);
Debug.Log($"[EPPlus测试] 成功加载: {Path.GetFileName(excelFilePath)}");
Debug.Log($"[EPPlus测试] 工作表列表: {string.Join(", ", sheetNames)}");
}
}
catch (Exception ex)
{
statusMessage = $"❌ 加载失败: {ex.Message}";
isFileLoaded = false;
Debug.LogError($"[EPPlus测试] 加载失败: {ex.Message}\n{ex.StackTrace}");
EditorUtility.DisplayDialog("加载失败", ex.Message, "确定");
}
}
/// <summary>
/// 加载指定工作表的数据
/// 使用工作表名称来访问避免索引问题EPPlus的Worksheets集合索引可能不连续
/// </summary>
private void LoadSheetData(int sheetIndex)
{
try
{
if (sheetIndex < 0 || sheetIndex >= sheetNames.Count)
return;
// 获取要加载的工作表名称
string sheetName = sheetNames[sheetIndex];
FileInfo fileInfo = new FileInfo(excelFilePath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
// 使用名称访问工作表(更可靠,避免索引问题)
ExcelWorksheet worksheet = package.Workbook.Worksheets[sheetName];
if (worksheet == null)
{
statusMessage = $"⚠️ 无法读取工作表: {sheetName}";
Debug.LogError($"[EPPlus测试] 工作表 '{sheetName}' 不存在");
return;
}
// 获取有效数据范围
if (worksheet.Dimension == null)
{
statusMessage = $"⚠️ 工作表 '{sheetName}' 为空";
sheetData = null;
maxRows = 0;
maxCols = 0;
Debug.LogWarning($"[EPPlus测试] 工作表 '{sheetName}' 没有数据");
return;
}
maxRows = worksheet.Dimension.End.Row;
maxCols = worksheet.Dimension.End.Column;
// 创建二维数组存储数据
sheetData = new string[maxRows, maxCols];
// 读取所有单元格数据
// 注意EPPlus 的索引从 1 开始,不是 0
for (int row = 1; row <= maxRows; row++)
{
for (int col = 1; col <= maxCols; col++)
{
var cell = worksheet.Cells[row, col];
sheetData[row - 1, col - 1] = cell.Value?.ToString() ?? "";
}
}
statusMessage = $"✅ 已加载工作表: {sheetName} ({maxRows}行 × {maxCols}列)";
// 重置滚动位置
dataScrollPosition = Vector2.zero;
Debug.Log($"[EPPlus测试] 成功加载工作表: '{sheetName}'");
Debug.Log($"[EPPlus测试] 数据范围: {maxRows} 行 × {maxCols} 列");
// 打印前几行数据用于调试
if (maxRows > 0 && maxCols > 0)
{
Debug.Log($"[EPPlus测试] 第1行第1列: {sheetData[0, 0]}");
if (maxRows > 1)
{
Debug.Log($"[EPPlus测试] 第2行第1列: {sheetData[1, 0]}");
}
}
}
}
catch (Exception ex)
{
statusMessage = $"❌ 读取工作表失败: {ex.Message}";
Debug.LogError($"[EPPlus测试] 读取工作表失败: {ex.Message}\n{ex.StackTrace}");
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,5 @@
# 忽略所有日志文件
*
# 但保留 .gitignore 自身
!.gitignore

View File

@ -0,0 +1,715 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OfficeOpenXml;
using Thrift.Protocol;
using Thrift.Transport.Client;
using System.Threading;
namespace ThriftPipelineTools
{
/// <summary>
/// 快速生成 Bytes 工具(简化版)
/// 只生成 AllConfigs.bytes不生成 C# 和 DR
/// 适用于表结构未变化,只需要更新数据的场景
/// </summary>
public class ThriftBytesGeneratorEditor : EditorWindow
{
// ========== 版本信息 ==========
private const string VERSION = "v1.0.0 (Bytes Only)";
// ========== 配置Key使用EditorPrefs ==========
private const string PREF_KEY_DOCS_PATH = "ThriftPipeline_DocsPath";
// ========== 路径配置 ==========
private string docsProjectPath = ""; // Docs 项目路径
// ========== UI 状态 ==========
private Vector2 scrollPosition;
private Vector2 logScrollPosition;
private StringBuilder logBuilder = new StringBuilder();
private bool isExecuting = false;
// ========== 进度控制 ==========
private float progress = 0f;
private string currentStep = "";
[MenuItem("蹊径/Thrift/快速生成 Bytes简化版")]
public static void ShowWindow()
{
var window = GetWindow<ThriftBytesGeneratorEditor>("快速生成 Bytes");
window.minSize = new Vector2(800, 600);
window.Show();
}
private void OnEnable()
{
LoadConfig();
}
private void OnGUI()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.Space(10);
EditorGUILayout.LabelField($"快速生成 Bytes 工具 - {VERSION}", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"此工具只生成 AllConfigs.bytes 文件,不生成 C# 和 DR\n" +
"适用于:表结构未变化,只需要更新配置数据的场景\n" +
"⚡ 执行速度更快,跳过编译和代码生成步骤",
MessageType.Info
);
EditorGUILayout.Space(10);
DrawPathConfiguration();
EditorGUILayout.Space(10);
DrawExecutionSection();
EditorGUILayout.Space(10);
DrawLogSection();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// 绘制路径配置区域
/// </summary>
private void DrawPathConfiguration()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("路径配置", EditorStyles.boldLabel);
EditorGUILayout.HelpBox(
"只需配置 Docs 项目路径(包含 Excel 配置和 cfg_txt.json",
MessageType.None
);
// Docs 项目路径
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Docs 项目路径:", GUILayout.Width(120));
docsProjectPath = EditorGUILayout.TextField(docsProjectPath);
if (GUILayout.Button("选择", GUILayout.Width(60)))
{
string selected = EditorUtility.OpenFolderPanel("选择 Docs 项目路径", docsProjectPath, "");
if (!string.IsNullOrEmpty(selected))
{
docsProjectPath = selected;
SaveConfig();
}
}
EditorGUILayout.EndHorizontal();
// 显示派生路径预览
if (!string.IsNullOrEmpty(docsProjectPath))
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField("派生路径预览:", EditorStyles.miniLabel);
var paths = GetDerivedPaths();
EditorGUILayout.LabelField($" cfg_txt.json: {paths["cfg_json"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" Config 目录: {paths["config_dir"]}", EditorStyles.miniLabel);
EditorGUILayout.LabelField($" 输出到 Unity: {paths["unity_bytes_dir"]}", EditorStyles.miniLabel);
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制执行区域
/// </summary>
private void DrawExecutionSection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("执行控制", EditorStyles.boldLabel);
GUI.enabled = !isExecuting && ValidatePaths();
if (GUILayout.Button("⚡ 快速生成 AllConfigs.bytes", GUILayout.Height(40)))
{
GenerateBytes();
}
GUI.enabled = true;
if (isExecuting)
{
EditorGUILayout.Space(5);
EditorGUILayout.LabelField($"当前步骤: {currentStep}");
EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(GUILayout.Height(20)), progress, "执行中...");
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 绘制日志区域
/// </summary>
private void DrawLogSection()
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("执行日志", EditorStyles.boldLabel);
logScrollPosition = EditorGUILayout.BeginScrollView(
logScrollPosition,
GUILayout.Height(300)
);
EditorGUILayout.TextArea(logBuilder.ToString(), GUILayout.ExpandHeight(true));
EditorGUILayout.EndScrollView();
if (GUILayout.Button("清空日志"))
{
logBuilder.Clear();
}
EditorGUILayout.EndVertical();
}
/// <summary>
/// 加载配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void LoadConfig()
{
try
{
docsProjectPath = EditorPrefs.GetString(PREF_KEY_DOCS_PATH, "");
}
catch (Exception ex)
{
Debug.LogError($"加载配置失败: {ex.Message}");
}
}
/// <summary>
/// 保存配置(使用 EditorPrefs本地保存不会被git提交
/// </summary>
private void SaveConfig()
{
try
{
EditorPrefs.SetString(PREF_KEY_DOCS_PATH, docsProjectPath);
}
catch (Exception ex)
{
Debug.LogError($"保存配置失败: {ex.Message}");
}
}
/// <summary>
/// 获取所有派生路径
/// </summary>
private Dictionary<string, string> GetDerivedPaths()
{
var paths = new Dictionary<string, string>();
// Docs 相关路径
if (!string.IsNullOrEmpty(docsProjectPath))
{
paths["cfg_json"] = Path.Combine(docsProjectPath, "tool", "cfg", "cfg_txt.json");
paths["config_dir"] = Path.Combine(docsProjectPath, "config");
}
// Unity 项目相关路径(当前项目)
string assetsPath = Application.dataPath;
paths["unity_bytes_dir"] = Path.Combine(assetsPath, "Design_SubModule", "ConfigData");
return paths;
}
/// <summary>
/// 验证路径
/// </summary>
private bool ValidatePaths()
{
if (string.IsNullOrEmpty(docsProjectPath))
return false;
var paths = GetDerivedPaths();
if (!File.Exists(paths["cfg_json"]))
return false;
if (!Directory.Exists(paths["config_dir"]))
return false;
return true;
}
/// <summary>
/// 日志输出
/// </summary>
private void Log(string message)
{
logBuilder.AppendLine(message);
Repaint();
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
private async void GenerateBytes()
{
isExecuting = true;
logBuilder.Clear();
progress = 0f;
DateTime startTime = DateTime.Now;
Log("================================================================================");
Log($"开始生成 AllConfigs.bytes ({VERSION})");
Log("================================================================================");
Log("");
try
{
var paths = GetDerivedPaths();
currentStep = "读取配置并生成 bytes";
progress = 0.3f;
Repaint();
if (!await GenerateBytesFile(paths))
{
Log("[FAIL] 生成失败");
EditorUtility.DisplayDialog("失败", "生成 AllConfigs.bytes 失败,请查看日志", "确定");
return;
}
progress = 1f;
currentStep = "完成";
Repaint();
DateTime endTime = DateTime.Now;
TimeSpan duration = endTime - startTime;
Log("");
Log("================================================================================");
Log($"✅ 生成完成!");
Log($" 耗时: {duration.TotalSeconds:F2} 秒");
Log($" 输出: {paths["unity_bytes_dir"]}/AllConfigs.bytes");
Log("================================================================================");
EditorUtility.DisplayDialog("成功", $"AllConfigs.bytes 生成完成!\n耗时: {duration.TotalSeconds:F2} 秒", "确定");
}
catch (Exception ex)
{
Log($"\n[FAIL] 执行失败: {ex.Message}");
Log(ex.StackTrace);
EditorUtility.DisplayDialog("错误", $"执行失败:\n{ex.Message}", "确定");
}
finally
{
isExecuting = false;
AssetDatabase.Refresh();
Repaint();
}
}
/// <summary>
/// 生成 Bytes 文件
/// </summary>
private async Task<bool> GenerateBytesFile(Dictionary<string, string> paths)
{
Log("【生成 AllConfigs.bytes】");
Log("--------------------------------------------------------------------------------");
try
{
string cfgJsonPath = paths["cfg_json"];
string configDir = paths["config_dir"];
string bytesOutputDir = paths["unity_bytes_dir"];
// 创建输出目录
if (!Directory.Exists(bytesOutputDir))
Directory.CreateDirectory(bytesOutputDir);
// 加载程序集
Log(" [1/3] 加载程序集");
System.Reflection.Assembly assembly = null;
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
if (asm.GetType("Byway.Thrift.Data.AllConfigs") != null)
{
assembly = asm;
break;
}
}
if (assembly == null)
{
Log("[FAIL] 找不到 Byway.Thrift.Data.AllConfigs 程序集");
Log(" 请确保已经执行过完整流程,生成了 C# 类");
return false;
}
Log(" [OK] 程序集加载成功");
// 读取配置
Log(" [2/3] 读取配置并填充数据");
string cfgText = File.ReadAllText(cfgJsonPath);
var cfg = JsonConvert.DeserializeObject<JObject>(cfgText);
var fileList = cfg["file_list"] as JArray;
// 创建AllConfigs实例
var allConfigsType = assembly.GetType("Byway.Thrift.Data.AllConfigs");
object allConfigsInstance = Activator.CreateInstance(allConfigsType);
int successCount = 0;
int processedCount = 0;
// 填充每个配置
foreach (JObject configItem in fileList)
{
string inFile = configItem["in_file"]?.ToString();
string outFile = configItem["out_file"]?.ToString();
string sheetName = configItem["sheet_name"]?.ToString();
var columnTypes = configItem["coloum_type"] as JArray;
if (string.IsNullOrEmpty(inFile) || string.IsNullOrEmpty(outFile))
continue;
string structName = outFile.Replace(".txt", "");
string excelPath = Path.Combine(configDir, inFile);
if (!File.Exists(excelPath))
{
Log($" [{++processedCount}] [SKIP] {structName} - Excel文件不存在");
continue;
}
Log($" [{++processedCount}] 处理: {structName}");
if (FillThriftObject(allConfigsInstance, allConfigsType, structName,
excelPath, sheetName, columnTypes, assembly))
{
successCount++;
}
}
// 序列化为bytes
Log($"\n [3/3] 序列化 AllConfigs.bytes (已设置 {successCount} 个配置)");
// 检查有多少配置实际被设置
int setCount = 0;
var issetField = allConfigsType.GetField("__isset",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (issetField != null)
{
var issetObj = issetField.GetValue(allConfigsInstance);
var issetType = issetObj.GetType();
foreach (var prop in allConfigsType.GetProperties())
{
if (prop.Name != "Isset" && prop.PropertyType.Namespace == "Byway.Thrift.Data")
{
var issetProp = issetType.GetProperty(prop.Name);
if (issetProp != null && (bool)issetProp.GetValue(issetObj))
setCount++;
}
}
}
Log($" __isset 标志统计: {setCount} 个配置被标记为已设置");
using (var memoryStream = new MemoryStream())
{
using (var transport = new TStreamTransport(memoryStream, memoryStream, new Thrift.TConfiguration()))
{
using (var protocol = new TBinaryProtocol(transport))
{
var writeMethod = allConfigsType.GetMethod("WriteAsync");
if (writeMethod != null)
{
var task = (Task)writeMethod.Invoke(allConfigsInstance,
new object[] { protocol, CancellationToken.None });
await task;
byte[] bytes = memoryStream.ToArray();
string allConfigsBytesPath = Path.Combine(bytesOutputDir, "AllConfigs.bytes");
File.WriteAllBytes(allConfigsBytesPath, bytes);
Log($" [OK] 生成成功 ({bytes.Length} bytes / {bytes.Length / 1024.0:F2} KB)");
if (bytes.Length < 1000)
{
Log($" [WARNING] 文件太小 ({bytes.Length} bytes),可能大部分配置未序列化!");
}
}
}
}
}
Log($"\n生成完成: 成功 {successCount} 个配置");
return true;
}
catch (Exception ex)
{
Log($"[FAIL] 生成失败: {ex.Message}");
Log(ex.StackTrace);
return false;
}
}
/// <summary>
/// 填充Thrift对象数据
/// </summary>
private bool FillThriftObject(object allConfigsInstance, Type allConfigsType, string structName,
string excelPath, string sheetName, JArray columnTypes, System.Reflection.Assembly assembly)
{
try
{
var configType = assembly.GetType($"Byway.Thrift.Data.{structName}");
var itemType = assembly.GetType($"Byway.Thrift.Data.{structName}Item");
if (configType == null || itemType == null)
{
Log($" [ERROR] 找不到类型: {structName} 或 {structName}Item");
return false;
}
object configInstance = Activator.CreateInstance(configType);
var dictType = typeof(Dictionary<,>).MakeGenericType(typeof(int), itemType);
var dictInstance = Activator.CreateInstance(dictType);
// 读取Excel
FileInfo fileInfo = new FileInfo(excelPath);
using (ExcelPackage package = new ExcelPackage(fileInfo))
{
ExcelWorksheet worksheet = null;
if (!string.IsNullOrEmpty(sheetName) && package.Workbook.Worksheets[sheetName] != null)
worksheet = package.Workbook.Worksheets[sheetName];
else
worksheet = package.Workbook.Worksheets[0];
if (worksheet == null || worksheet.Dimension == null)
{
Log($" [ERROR] 工作表为空");
return false;
}
int maxRows = worksheet.Dimension.End.Row;
int maxCols = worksheet.Dimension.End.Column;
// 读取表头第1行
List<string> headers = new List<string>();
List<int> validColumns = new List<int>();
int maxColumns = columnTypes?.Count ?? maxCols;
for (int col = 1; col <= maxCols && headers.Count < maxColumns; col++)
{
string header = worksheet.Cells[1, col].Value?.ToString() ?? "";
if (!string.IsNullOrWhiteSpace(header))
{
headers.Add(header);
validColumns.Add(col);
}
}
// 读取数据从第3行开始
int filledItemCount = 0;
for (int row = 3; row <= maxRows; row++)
{
bool isEmptyRow = true;
for (int i = 0; i < validColumns.Count; i++)
{
if (worksheet.Cells[row, validColumns[i]].Value != null)
{
isEmptyRow = false;
break;
}
}
if (isEmptyRow)
continue;
object itemInstance = Activator.CreateInstance(itemType);
int? idValue = null;
for (int i = 0; i < headers.Count; i++)
{
string originalHeader = headers[i];
string fieldName = SanitizeFieldName(originalHeader);
int colIndex = validColumns[i];
var cell = worksheet.Cells[row, colIndex];
object cellValue = cell.Value;
var property = itemType.GetProperty(fieldName);
if (property != null)
{
try
{
object convertedValue;
if (cellValue == null)
{
if (property.PropertyType == typeof(string))
{
convertedValue = "";
}
else if (property.PropertyType.IsValueType)
{
convertedValue = Activator.CreateInstance(property.PropertyType);
}
else
{
continue;
}
}
else
{
convertedValue = ConvertCellValue(cellValue, property.PropertyType);
}
property.SetValue(itemInstance, convertedValue);
if (fieldName.ToLower() == "id" && convertedValue is int)
idValue = (int)convertedValue;
}
catch (Exception ex)
{
Log($" [WARN] 行{row} 字段{fieldName}转换失败: {ex.Message}");
}
}
}
if (idValue.HasValue)
{
var addMethod = dictType.GetMethod("Add");
addMethod.Invoke(dictInstance, new object[] { idValue.Value, itemInstance });
filledItemCount++;
}
}
Log($" 读取了 {filledItemCount} 条数据");
}
// 设置字典属性
string dictPropertyName = char.ToUpper(structName[0]) + structName.Substring(1).ToLower() + "s";
var dictProperty = configType.GetProperty(dictPropertyName);
if (dictProperty != null)
{
dictProperty.SetValue(configInstance, dictInstance);
}
else
{
Log($" [ERROR] 未找到字典属性: {dictPropertyName}");
return false;
}
// 设置到AllConfigs
var allConfigProperty = allConfigsType.GetProperty(structName);
if (allConfigProperty != null)
{
allConfigProperty.SetValue(allConfigsInstance, configInstance);
var verifyValue = allConfigProperty.GetValue(allConfigsInstance);
if (verifyValue == null)
{
Log($" [ERROR] 设置到AllConfigs后为null!");
return false;
}
Log($" [OK] 成功");
}
else
{
Log($" [ERROR] AllConfigs中未找到属性: {structName}");
return false;
}
return true;
}
catch (Exception ex)
{
Log($" [ERROR] {structName}: {ex.Message}");
return false;
}
}
/// <summary>
/// 清理并转换字段名为PascalCase
/// </summary>
private string SanitizeFieldName(string fieldName)
{
if (string.IsNullOrWhiteSpace(fieldName))
return "Field_unknown";
StringBuilder sb = new StringBuilder();
foreach (char c in fieldName)
{
if (char.IsLetterOrDigit(c) || c == '_')
sb.Append(c);
else if (c == ' ' || c == '-')
sb.Append('_');
}
string cleaned = sb.ToString();
if (string.IsNullOrEmpty(cleaned))
return "Field_unknown";
if (cleaned.Length > 0)
{
return char.ToUpper(cleaned[0]) + cleaned.Substring(1);
}
return cleaned;
}
/// <summary>
/// 转换单元格值
/// </summary>
private object ConvertCellValue(object cellValue, Type targetType)
{
try
{
if (targetType == typeof(int))
{
if (cellValue is double d)
return (int)d;
return Convert.ToInt32(cellValue);
}
else if (targetType == typeof(long))
{
if (cellValue is double d)
return (long)d;
return Convert.ToInt64(cellValue);
}
else if (targetType == typeof(double))
{
return Convert.ToDouble(cellValue);
}
else if (targetType == typeof(bool))
{
return Convert.ToBoolean(cellValue);
}
else if (targetType == typeof(string))
{
return cellValue.ToString();
}
return cellValue;
}
catch
{
if (targetType == typeof(int)) return 0;
if (targetType == typeof(long)) return 0L;
if (targetType == typeof(double)) return 0.0;
if (targetType == typeof(bool)) return false;
if (targetType == typeof(string)) return "";
return null;
}
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -0,0 +1,937 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 表情配置Editor工具
/// 读取Docs/config/Emoji.xlsx关联Art_SO/Collections/EmojiResource.asset
/// </summary>
public class EmojiConfigEditor : EditorWindow
{
private const string EMOJI_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
private const string EMOJI_SO_NAME = "EmojiResource";
private const string EMOJI_EXCEL_NAME = "Emoji.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string EMOJI_SHEET_NAME = "Emoji";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "EmojiConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<EmojiData> emojiDataList = new List<EmojiData>();
private ArtTableSO emojiTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/表情")]
public static void ShowWindow()
{
var window = GetWindow<EmojiConfigEditor>("表情配置");
window.minSize = new Vector2(1000, 600);
window.Show();
}
private void OnEnable()
{
// 读取上次保存的路径
if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
{
docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
}
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("表情配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
private void LoadData()
{
try
{
// 校验路径
if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
return;
}
// 1. 检查Docs是否为Git仓库并更新
if (!CheckAndUpdateDocsRepository())
{
return;
}
// 2. 检查Design_SubModule分支
if (!CheckAndSwitchDesignSubModuleBranch())
{
return;
}
// 校验Emoji SO是否存在
string emojiSOPath = Path.Combine(EMOJI_SO_PATH, $"{EMOJI_SO_NAME}.asset");
emojiTableSO = AssetDatabase.LoadAssetAtPath<ArtTableSO>(emojiSOPath);
if (emojiTableSO == null)
{
EditorUtility.DisplayDialog("错误",
$"未找到表情资源配置\n路径: {emojiSOPath}\n\n请先在美术资源配置工具中创建",
"确定");
return;
}
if (emojiTableSO.Items == null || emojiTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "表情资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Emoji.xlsx
LoadEmojiExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
success = process.ExitCode == 0;
return success ? output : error;
}
}
catch (Exception e)
{
success = false;
return $"执行Git命令失败: {e.Message}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
private bool CheckAndUpdateDocsRepository()
{
string gitPath = Path.Combine(docsRootPath, ".git");
if (!Directory.Exists(gitPath))
{
EditorUtility.DisplayDialog("错误",
$"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
if (!fetchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Fetch失败",
$"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
if (!statusSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Status失败",
$"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
"确定");
return false;
}
if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
{
EditorUtility.ClearProgressBar();
bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
"Docs仓库中有未提交的更改这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置不推荐",
"继续(不推荐)", "取消,前往处理");
if (!proceed)
{
Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
return false;
}
}
if (statusResult.Contains("Your branch is behind"))
{
EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
EditorUtility.ClearProgressBar();
if (!pullSuccess)
{
if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
{
EditorUtility.DisplayDialog("拉取失败:存在冲突",
$"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
"确定");
}
else
{
EditorUtility.DisplayDialog("拉取失败",
$"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
"确定");
}
return false;
}
Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Docs仓库已是最新版本");
}
return true;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
private bool CheckAndSwitchDesignSubModuleBranch()
{
string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
designSubModulePath = Path.GetFullPath(designSubModulePath);
if (!Directory.Exists(designSubModulePath))
{
EditorUtility.DisplayDialog("错误",
$"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
if (!branchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Branch失败",
$"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
"确定");
return false;
}
string currentBranch = branchResult.Trim();
if (currentBranch != "main")
{
EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
EditorUtility.ClearProgressBar();
if (!checkoutSuccess)
{
EditorUtility.DisplayDialog("切换分支失败",
$"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
"确定");
return false;
}
Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Emoji.xlsx
/// </summary>
private void LoadEmojiExcel()
{
emojiDataList.Clear();
string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
if (!File.Exists(emojiExcelPath))
{
throw new Exception($"未找到Emoji配置文件: {emojiExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
{
var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
{
throw new Exception("Emoji.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1;
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1;
}
}
var emojiData = new EmojiData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId
};
emojiDataList.Add(emojiData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"表情数据列表(共 {emojiDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加表情", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewEmoji();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < emojiDataList.Count; i++)
{
DrawEmojiDataRow(emojiDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawEmojiDataRow(EmojiData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id可编辑
data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表
var iconItems = emojiTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
int currentIndex = 0;
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除表情 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
emojiDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新表情
/// </summary>
private void AddNewEmoji()
{
int newId = 1;
if (emojiDataList.Count > 0)
{
newId = emojiDataList.Max(x => x.Id) + 1;
}
var newEmoji = new EmojiData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1
};
emojiDataList.Add(newEmoji);
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string emojiExcelPath = Path.Combine(docsRootPath, "config", EMOJI_EXCEL_NAME);
if (!File.Exists(emojiExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Emoji配置文件: {emojiExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(emojiExcelPath)))
{
var worksheet = package.Workbook.Worksheets[EMOJI_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Emoji.xlsx中未找到Sheet: {EMOJI_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
var emojiData = emojiDataList.Find(x => x.Id == id);
if (emojiData != null)
{
worksheet.Cells[row, nameKeyCol].Value = emojiData.NameKey;
worksheet.Cells[row, initCol].Value = emojiData.Init;
if (emojiData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = emojiData.IconId;
}
processedIds.Add(id);
}
else
{
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var emojiData in emojiDataList)
{
if (!processedIds.Contains(emojiData.Id))
{
currentRow++;
worksheet.Cells[currentRow, idCol].Value = emojiData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = emojiData.NameKey;
worksheet.Cells[currentRow, initCol].Value = emojiData.Init;
if (emojiData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = emojiData.IconId;
}
}
}
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 表情数据类
/// </summary>
private class EmojiData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
}
}
}

View File

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

View File

@ -0,0 +1,987 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 头像配置Editor工具
/// 读取Docs/config/Face.xlsx关联Art_SO/HeadResources.asset
/// </summary>
public class HeadConfigEditor : EditorWindow
{
private const string HEAD_SO_PATH = "Assets/Art_SubModule/Art_SO";
private const string FACE_EXCEL_NAME = "Face.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string FACE_SHEET_NAME = "Face";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "HeadConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<FaceData> faceDataList = new List<FaceData>();
private ArtTableSO headTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/头像")]
public static void ShowWindow()
{
var window = GetWindow<HeadConfigEditor>("头像配置");
window.minSize = new Vector2(900, 600);
window.Show();
}
private void OnEnable()
{
// 设置EPPlus许可证
// ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
// 读取上次保存的路径
if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
{
docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
}
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("头像配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
private void LoadData()
{
try
{
// 校验路径
if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
return;
}
// 1. 检查Docs是否为Git仓库并更新
if (!CheckAndUpdateDocsRepository())
{
return; // 检查失败,提示已在方法内显示
}
// 2. 检查Design_SubModule分支
if (!CheckAndSwitchDesignSubModuleBranch())
{
return; // 检查失败,提示已在方法内显示
}
// 校验Head SO是否存在
string[] soGuids = AssetDatabase.FindAssets("t:ArtTableSO", new[] { HEAD_SO_PATH });
if (soGuids.Length == 0)
{
EditorUtility.DisplayDialog("错误", $"未在 {HEAD_SO_PATH} 目录下找到头像资源配置ArtTableSO\n请先在美术资源配置工具中创建", "确定");
return;
}
// 查找名称为"HeadResource"的SO
ArtTableSO foundSO = null;
foreach (var guid in soGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var so = AssetDatabase.LoadAssetAtPath<ArtTableSO>(path);
if (so != null && so.TableName == "HeadResource")
{
foundSO = so;
break;
}
}
if (foundSO == null)
{
EditorUtility.DisplayDialog("错误", "无法加载头像资源配置", "确定");
return;
}
headTableSO = foundSO;
if (headTableSO.Items == null || headTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "头像资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Face.xlsx
LoadFaceExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
success = process.ExitCode == 0;
return success ? output : error;
}
}
catch (Exception e)
{
success = false;
return $"执行Git命令失败: {e.Message}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
private bool CheckAndUpdateDocsRepository()
{
// 检查是否是Git仓库
string gitPath = Path.Combine(docsRootPath, ".git");
if (!Directory.Exists(gitPath))
{
EditorUtility.DisplayDialog("错误",
$"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
// 执行git fetch检查远程更新
string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
if (!fetchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Fetch失败",
$"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
// 检查本地和远程的差异
string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
if (!statusSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Status失败",
$"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
"确定");
return false;
}
// 检查是否有未提交的更改
if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
{
EditorUtility.ClearProgressBar();
bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
"Docs仓库中有未提交的更改这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置不推荐",
"继续(不推荐)", "取消,前往处理");
if (!proceed)
{
Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
return false;
}
}
// 检查是否behind远程
if (statusResult.Contains("Your branch is behind"))
{
EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
EditorUtility.ClearProgressBar();
if (!pullSuccess)
{
// 检查是否是合并冲突
if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
{
EditorUtility.DisplayDialog("拉取失败:存在冲突",
$"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
"确定");
}
else
{
EditorUtility.DisplayDialog("拉取失败",
$"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
"确定");
}
return false;
}
Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Docs仓库已是最新版本");
}
return true;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
private bool CheckAndSwitchDesignSubModuleBranch()
{
string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
designSubModulePath = Path.GetFullPath(designSubModulePath);
// 检查Design_SubModule是否存在
if (!Directory.Exists(designSubModulePath))
{
EditorUtility.DisplayDialog("错误",
$"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
// 检查当前分支
string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
if (!branchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Branch失败",
$"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
"确定");
return false;
}
string currentBranch = branchResult.Trim();
if (currentBranch != "main")
{
EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
EditorUtility.ClearProgressBar();
if (!checkoutSuccess)
{
EditorUtility.DisplayDialog("切换分支失败",
$"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
"确定");
return false;
}
Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Face.xlsx
/// </summary>
private void LoadFaceExcel()
{
faceDataList.Clear();
string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
if (!File.Exists(faceExcelPath))
{
throw new Exception($"未找到Face配置文件: {faceExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
{
var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0)
{
throw new Exception("Face.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1; // 默认为未选择
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1; // 解析失败设为未选择
}
}
var faceData = new FaceData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId
};
faceDataList.Add(faceData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"头像数据列表(共 {faceDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加头像", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewFace();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < faceDataList.Count; i++)
{
DrawFaceDataRow(faceDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
// 使用当前鼠标位置,而不是之前记录的位置
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawFaceDataRow(FaceData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id只读
GUI.enabled = false;
EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
GUI.enabled = true;
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容不立即绘制
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表包含未选择选项
var iconItems = headTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
// 查找当前选中的索引
int currentIndex = 0; // 默认为"未选择"
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
// 找到了对应的Item索引需要+1因为第0项是"未选择"
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
// 选择了"未选择"
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
// 选择了具体的Icon索引需要-1
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览和Desc提示
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
// 直接使用Sprite引用
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
// 正确处理图集sprite的预览
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
// 绘制背景
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
// 归一化UV坐标
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
// 计算保持宽高比的显示区域
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
// 添加Tooltip
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
// ID存在但找不到对应的Item
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
// IconId < 0未选择状态
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除头像 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
faceDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新头像
/// </summary>
private void AddNewFace()
{
// 计算新ID当前最大ID + 1
int newId = 1;
if (faceDataList.Count > 0)
{
newId = faceDataList.Max(x => x.Id) + 1;
}
var newFace = new FaceData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1
};
faceDataList.Add(newFace);
// 滚动到底部
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string faceExcelPath = Path.Combine(docsRootPath, "config", FACE_EXCEL_NAME);
if (!File.Exists(faceExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Face配置文件: {faceExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(faceExcelPath)))
{
var worksheet = package.Workbook.Worksheets[FACE_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Face.xlsx中未找到Sheet: {FACE_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或标记删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
// 查找对应的FaceData
var faceData = faceDataList.Find(x => x.Id == id);
if (faceData != null)
{
// 更新数据
worksheet.Cells[row, nameKeyCol].Value = faceData.NameKey;
worksheet.Cells[row, initCol].Value = faceData.Init;
// IconId为-1时写入空字符串否则写入实际值
if (faceData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = faceData.IconId;
}
processedIds.Add(id);
}
else
{
// 删除行
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var faceData in faceDataList)
{
if (!processedIds.Contains(faceData.Id))
{
// 这是新增的数据
currentRow++;
worksheet.Cells[currentRow, idCol].Value = faceData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = faceData.NameKey;
worksheet.Cells[currentRow, initCol].Value = faceData.Init;
if (faceData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = faceData.IconId;
}
}
}
// 保存文件
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 头像数据类
/// </summary>
private class FaceData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
}
}
}

View File

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

View File

@ -0,0 +1,965 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using OfficeOpenXml;
using ArtResource;
using Debug = UnityEngine.Debug;
namespace DesignTools.Collections
{
/// <summary>
/// 头像框配置Editor工具
/// 读取Docs/config/Avatar.xlsx关联Art_SO/Collections/HeadFrameResource.asset
/// </summary>
public class HeadFrameConfigEditor : EditorWindow
{
private const string HEADFRAME_SO_PATH = "Assets/Art_SubModule/Art_SO/Collections";
private const string HEADFRAME_SO_NAME = "HeadFrameResource";
private const string AVATAR_EXCEL_NAME = "Avatar.xlsx";
private const string LANGUAGE_EXCEL_NAME = "AllLanguage.xlsx";
private const string AVATAR_SHEET_NAME = "Avatar";
private const string LANGUAGE_SHEET_NAME = "client";
private const string DOCS_PATH_PREF_KEY = "HeadFrameConfigEditor_DocsPath";
private const string DESIGN_SUBMODULE_PATH = "Assets/Design_SubModule";
private string docsRootPath = "";
private List<HeadFrameData> headFrameDataList = new List<HeadFrameData>();
private ArtTableSO headFrameTableSO;
private Dictionary<string, Dictionary<string, string>> languageDict = new Dictionary<string, Dictionary<string, string>>();
private Vector2 scrollPosition;
private bool isDataLoaded = false;
private string pendingTooltipText = "";
[MenuItem("策划工具/收藏品/头像框")]
public static void ShowWindow()
{
var window = GetWindow<HeadFrameConfigEditor>("头像框配置");
window.minSize = new Vector2(1000, 600);
window.Show();
}
private void OnEnable()
{
// 读取上次保存的路径
if (EditorPrefs.HasKey(DOCS_PATH_PREF_KEY))
{
docsRootPath = EditorPrefs.GetString(DOCS_PATH_PREF_KEY);
}
}
private void OnGUI()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("头像框配置工具", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Docs路径选择
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Docs项目根目录", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
docsRootPath = EditorGUILayout.TextField("路径", docsRootPath);
if (GUILayout.Button("选择文件夹", GUILayout.Width(100)))
{
string selectedPath = EditorUtility.OpenFolderPanel("选择Docs项目根目录", "", "");
if (!string.IsNullOrEmpty(selectedPath))
{
docsRootPath = selectedPath;
EditorPrefs.SetString(DOCS_PATH_PREF_KEY, docsRootPath);
}
}
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("加载配置数据", GUILayout.Height(30)))
{
LoadData();
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(5);
// 数据编辑区域
if (isDataLoaded)
{
DrawDataEditor();
}
else
{
EditorGUILayout.HelpBox("请先选择Docs根目录并加载配置数据", MessageType.Info);
}
}
/// <summary>
/// 加载配置数据
/// </summary>
private void LoadData()
{
try
{
// 校验路径
if (string.IsNullOrEmpty(docsRootPath) || !Directory.Exists(docsRootPath))
{
EditorUtility.DisplayDialog("错误", "请选择有效的Docs根目录", "确定");
return;
}
// 1. 检查Docs是否为Git仓库并更新
if (!CheckAndUpdateDocsRepository())
{
return;
}
// 2. 检查Design_SubModule分支
if (!CheckAndSwitchDesignSubModuleBranch())
{
return;
}
// 校验HeadFrame SO是否存在
string headFrameSOPath = Path.Combine(HEADFRAME_SO_PATH, $"{HEADFRAME_SO_NAME}.asset");
headFrameTableSO = AssetDatabase.LoadAssetAtPath<ArtTableSO>(headFrameSOPath);
if (headFrameTableSO == null)
{
EditorUtility.DisplayDialog("错误",
$"未找到头像框资源配置\n路径: {headFrameSOPath}\n\n请先在美术资源配置工具中创建",
"确定");
return;
}
if (headFrameTableSO.Items == null || headFrameTableSO.Items.Count == 0)
{
EditorUtility.DisplayDialog("错误", "头像框资源配置数据为空", "确定");
return;
}
// 加载语言表
LoadLanguageData();
// 加载Avatar.xlsx
LoadAvatarExcel();
isDataLoaded = true;
EditorUtility.DisplayDialog("成功", "配置数据加载成功!", "确定");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"加载数据失败: {e.Message}\n{e.StackTrace}", "确定");
isDataLoaded = false;
}
}
/// <summary>
/// 执行Git命令
/// </summary>
private string ExecuteGitCommand(string workingDirectory, string arguments, out bool success)
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = workingDirectory,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
success = process.ExitCode == 0;
return success ? output : error;
}
}
catch (Exception e)
{
success = false;
return $"执行Git命令失败: {e.Message}";
}
}
/// <summary>
/// 检查并更新Docs仓库
/// </summary>
private bool CheckAndUpdateDocsRepository()
{
string gitPath = Path.Combine(docsRootPath, ".git");
if (!Directory.Exists(gitPath))
{
EditorUtility.DisplayDialog("错误",
$"Docs目录不是Git仓库\n路径: {docsRootPath}\n\n请确保Docs项目已正确克隆",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查Docs仓库远程更新...", 0.3f);
string fetchResult = ExecuteGitCommand(docsRootPath, "fetch", out bool fetchSuccess);
if (!fetchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Fetch失败",
$"无法检查远程更新:\n{fetchResult}\n\n请在SourceTree或GitHubDesktop中检查网络连接和仓库状态",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查更新", "正在检查是否有新提交...", 0.6f);
string statusResult = ExecuteGitCommand(docsRootPath, "status -uno", out bool statusSuccess);
if (!statusSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Status失败",
$"无法获取仓库状态:\n{statusResult}\n\n请在SourceTree或GitHubDesktop中检查仓库状态",
"确定");
return false;
}
if (statusResult.Contains("Changes not staged") || statusResult.Contains("Changes to be committed"))
{
EditorUtility.ClearProgressBar();
bool proceed = EditorUtility.DisplayDialog("警告:有未提交的更改",
"Docs仓库中有未提交的更改这可能导致拉取时产生冲突。\n\n建议先提交或暂存这些更改。\n\n是否继续加载配置不推荐",
"继续(不推荐)", "取消,前往处理");
if (!proceed)
{
Debug.Log("请在SourceTree或GitHubDesktop中处理未提交的更改");
return false;
}
}
if (statusResult.Contains("Your branch is behind"))
{
EditorUtility.DisplayProgressBar("更新中", "正在从远程拉取最新代码...", 0.8f);
string pullResult = ExecuteGitCommand(docsRootPath, "pull", out bool pullSuccess);
EditorUtility.ClearProgressBar();
if (!pullSuccess)
{
if (pullResult.Contains("CONFLICT") || pullResult.Contains("conflict"))
{
EditorUtility.DisplayDialog("拉取失败:存在冲突",
$"拉取远程更新时发生冲突:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中解决冲突后再操作",
"确定");
}
else
{
EditorUtility.DisplayDialog("拉取失败",
$"无法拉取远程更新:\n{pullResult}\n\n请在SourceTree或GitHubDesktop中检查并解决问题",
"确定");
}
return false;
}
Debug.Log($"Docs仓库已更新到最新版本:\n{pullResult}");
EditorUtility.DisplayDialog("更新成功", "Docs仓库已更新到最新版本", "确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Docs仓库已是最新版本");
}
return true;
}
/// <summary>
/// 检查并切换Design_SubModule到main分支
/// </summary>
private bool CheckAndSwitchDesignSubModuleBranch()
{
string designSubModulePath = Path.Combine(Application.dataPath, "..", DESIGN_SUBMODULE_PATH);
designSubModulePath = Path.GetFullPath(designSubModulePath);
if (!Directory.Exists(designSubModulePath))
{
EditorUtility.DisplayDialog("错误",
$"Design_SubModule目录不存在:\n{designSubModulePath}\n\n请确保子模块已正确初始化",
"确定");
return false;
}
EditorUtility.DisplayProgressBar("检查分支", "正在检查Design_SubModule分支...", 0.5f);
string branchResult = ExecuteGitCommand(designSubModulePath, "branch --show-current", out bool branchSuccess);
if (!branchSuccess)
{
EditorUtility.ClearProgressBar();
EditorUtility.DisplayDialog("Git Branch失败",
$"无法获取当前分支:\n{branchResult}\n\n请在SourceTree或GitHubDesktop中检查Design_SubModule状态",
"确定");
return false;
}
string currentBranch = branchResult.Trim();
if (currentBranch != "main")
{
EditorUtility.DisplayProgressBar("切换分支", "正在切换到main分支...", 0.8f);
string checkoutResult = ExecuteGitCommand(designSubModulePath, "checkout main", out bool checkoutSuccess);
EditorUtility.ClearProgressBar();
if (!checkoutSuccess)
{
EditorUtility.DisplayDialog("切换分支失败",
$"无法切换到main分支:\n{checkoutResult}\n\n当前分支: {currentBranch}\n\n请在SourceTree或GitHubDesktop中手动切换到main分支",
"确定");
return false;
}
Debug.Log($"Design_SubModule已从 {currentBranch} 切换到 main 分支");
EditorUtility.DisplayDialog("分支切换成功",
$"Design_SubModule已从 {currentBranch} 切换到 main 分支",
"确定");
}
else
{
EditorUtility.ClearProgressBar();
Debug.Log("Design_SubModule已在main分支");
}
return true;
}
/// <summary>
/// 加载语言数据
/// </summary>
private void LoadLanguageData()
{
languageDict.Clear();
string languageExcelPath = Path.Combine(docsRootPath, "config", LANGUAGE_EXCEL_NAME);
if (!File.Exists(languageExcelPath))
{
Debug.LogWarning($"未找到语言表: {languageExcelPath}");
return;
}
using (var package = new ExcelPackage(new FileInfo(languageExcelPath)))
{
var worksheet = package.Workbook.Worksheets[LANGUAGE_SHEET_NAME];
if (worksheet == null)
{
Debug.LogWarning($"语言表中未找到Sheet: {LANGUAGE_SHEET_NAME}");
return;
}
// 查找Key列和语言列
int keyColumnIndex = -1;
int zhCNColumnIndex = -1;
int enUSColumnIndex = -1;
int ptBRColumnIndex = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
if (header == "key")
{
keyColumnIndex = col;
}
else if (header == "zh_CN")
{
zhCNColumnIndex = col;
}
else if (header == "en_US")
{
enUSColumnIndex = col;
}
else if (header == "pt_BR")
{
ptBRColumnIndex = col;
}
}
if (keyColumnIndex < 0)
{
Debug.LogWarning("语言表中未找到Key列");
return;
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string key = worksheet.Cells[row, keyColumnIndex].Text;
if (string.IsNullOrEmpty(key)) continue;
if (!languageDict.ContainsKey(key))
{
languageDict[key] = new Dictionary<string, string>();
}
if (zhCNColumnIndex > 0)
{
languageDict[key]["zh_CN"] = worksheet.Cells[row, zhCNColumnIndex].Text;
}
if (enUSColumnIndex > 0)
{
languageDict[key]["en_US"] = worksheet.Cells[row, enUSColumnIndex].Text;
}
if (ptBRColumnIndex > 0)
{
languageDict[key]["pt_BR"] = worksheet.Cells[row, ptBRColumnIndex].Text;
}
}
}
}
/// <summary>
/// 加载Avatar.xlsx
/// </summary>
private void LoadAvatarExcel()
{
headFrameDataList.Clear();
string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
if (!File.Exists(avatarExcelPath))
{
throw new Exception($"未找到Avatar配置文件: {avatarExcelPath}");
}
using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
{
var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
if (worksheet == null)
{
throw new Exception($"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}");
}
// 读取表头第1行
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
case "FrameImageScale":
frameImageScaleCol = col;
break;
}
}
if (idCol < 0 || nameKeyCol < 0 || initCol < 0 || iconCol < 0 || frameImageScaleCol < 0)
{
throw new Exception("Avatar.xlsx表结构不正确缺少必要列Id/NameKey/Init/Icon/FrameImageScale");
}
// 读取数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
for (int row = 3; row <= rowCount; row++)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
string nameKey = worksheet.Cells[row, nameKeyCol].Text;
string initText = worksheet.Cells[row, initCol].Text;
string iconText = worksheet.Cells[row, iconCol].Text;
string frameImageScaleText = worksheet.Cells[row, frameImageScaleCol].Text;
int init = 0;
if (!string.IsNullOrEmpty(initText))
{
int.TryParse(initText, out init);
}
int iconId = -1;
if (!string.IsNullOrEmpty(iconText))
{
if (!int.TryParse(iconText, out iconId))
{
iconId = -1;
}
}
float frameImageScale = 1.0f;
if (!string.IsNullOrEmpty(frameImageScaleText))
{
if (!float.TryParse(frameImageScaleText, out frameImageScale))
{
frameImageScale = 1.0f;
}
}
var frameData = new HeadFrameData
{
Id = id,
NameKey = nameKey,
Init = init,
IconId = iconId,
FrameImageScale = frameImageScale
};
headFrameDataList.Add(frameData);
}
}
}
/// <summary>
/// 绘制数据编辑器
/// </summary>
private void DrawDataEditor()
{
pendingTooltipText = "";
EditorGUILayout.BeginVertical("box");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"头像框数据列表(共 {headFrameDataList.Count} 条)", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.cyan;
if (GUILayout.Button("+ 添加头像框", GUILayout.Height(25), GUILayout.Width(100)))
{
AddNewHeadFrame();
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(3);
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
// 表头
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.LabelField("Id", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("NameKey", EditorStyles.boldLabel, GUILayout.Width(150));
EditorGUILayout.LabelField("中文名称", EditorStyles.boldLabel, GUILayout.Width(120));
EditorGUILayout.LabelField("Init", EditorStyles.boldLabel, GUILayout.Width(50));
EditorGUILayout.LabelField("Icon", EditorStyles.boldLabel, GUILayout.Width(200));
EditorGUILayout.LabelField("预览", EditorStyles.boldLabel, GUILayout.Width(100));
EditorGUILayout.LabelField("缩放", EditorStyles.boldLabel, GUILayout.Width(80));
EditorGUILayout.LabelField("操作", EditorStyles.boldLabel, GUILayout.Width(60));
EditorGUILayout.EndHorizontal();
// 数据行
for (int i = 0; i < headFrameDataList.Count; i++)
{
DrawHeadFrameDataRow(headFrameDataList[i]);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
EditorGUILayout.Space(10);
// 保存按钮
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUI.backgroundColor = Color.green;
if (GUILayout.Button("保存配置到Excel", GUILayout.Height(30), GUILayout.Width(200)))
{
SaveData();
}
GUI.backgroundColor = Color.white;
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
// 在所有内容绘制完后绘制tooltip确保在最上层
if (!string.IsNullOrEmpty(pendingTooltipText))
{
Vector2 tooltipSize = GUI.skin.box.CalcSize(new GUIContent(pendingTooltipText));
tooltipSize.x += 10;
tooltipSize.y += 10;
// 使用当前鼠标位置,而不是之前记录的位置
Vector2 mousePos = Event.current.mousePosition;
Rect tooltipRect = new Rect(
mousePos.x + 1,
mousePos.y + 1,
tooltipSize.x,
tooltipSize.y
);
GUI.Box(tooltipRect, pendingTooltipText);
}
}
/// <summary>
/// 绘制单行数据
/// </summary>
private void DrawHeadFrameDataRow(HeadFrameData data)
{
EditorGUILayout.BeginHorizontal("box");
// Id可编辑
data.Id = EditorGUILayout.IntField(data.Id, GUILayout.Width(50));
// NameKey可编辑
data.NameKey = EditorGUILayout.TextField(data.NameKey, GUILayout.Width(150));
// 中文名称(只读预览,带即时多语言显示)
GUI.enabled = false;
string zhName = "未找到语言Key";
if (languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
if (langs.ContainsKey("zh_CN"))
{
zhName = langs["zh_CN"];
}
}
Rect nameRect = GUILayoutUtility.GetRect(new GUIContent(zhName), GUI.skin.textField, GUILayout.Width(120));
EditorGUI.TextField(nameRect, zhName);
// 检测鼠标悬停并准备tooltip内容不立即绘制
if (nameRect.Contains(Event.current.mousePosition) && languageDict.ContainsKey(data.NameKey))
{
var langs = languageDict[data.NameKey];
List<string> tooltipLines = new List<string>();
if (langs.ContainsKey("zh_CN") && !string.IsNullOrEmpty(langs["zh_CN"]))
{
tooltipLines.Add($"[中文] {langs["zh_CN"]}");
}
if (langs.ContainsKey("en_US") && !string.IsNullOrEmpty(langs["en_US"]))
{
tooltipLines.Add($"[English] {langs["en_US"]}");
}
if (langs.ContainsKey("pt_BR") && !string.IsNullOrEmpty(langs["pt_BR"]))
{
tooltipLines.Add($"[Português] {langs["pt_BR"]}");
}
if (tooltipLines.Count > 0)
{
pendingTooltipText = string.Join("\n", tooltipLines);
Repaint();
}
}
GUI.enabled = true;
// InitCheckbox
bool initChecked = data.Init == 1;
bool newInitChecked = EditorGUILayout.Toggle(initChecked, GUILayout.Width(50));
data.Init = newInitChecked ? 1 : 0;
// Icon下拉列表
var iconItems = headFrameTableSO.Items;
var iconNamesList = new List<string> { "未选择" };
iconNamesList.AddRange(iconItems.Select(x => x.Name));
var iconNames = iconNamesList.ToArray();
int currentIndex = 0;
var currentItem = iconItems.Find(x => x.Id == data.IconId);
if (currentItem != null)
{
int itemIndex = iconItems.IndexOf(currentItem);
if (itemIndex >= 0)
{
currentIndex = itemIndex + 1;
}
}
EditorGUI.BeginChangeCheck();
int newIndex = EditorGUILayout.Popup(currentIndex, iconNames, GUILayout.Width(200));
if (EditorGUI.EndChangeCheck())
{
if (newIndex == 0)
{
data.IconId = -1;
}
else if (newIndex > 0 && newIndex <= iconItems.Count)
{
data.IconId = iconItems[newIndex - 1].Id;
}
}
// 预览
if (data.IconId >= 0)
{
var item = iconItems.Find(x => x.Id == data.IconId);
if (item != null)
{
Sprite sprite = item.Sprite;
if (sprite != null && sprite.texture != null)
{
Rect previewRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
EditorGUI.DrawRect(previewRect, new Color(0.5f, 0.5f, 0.5f, 1f));
Rect texCoords = sprite.textureRect;
Texture2D tex = sprite.texture;
Rect normalizedCoords = new Rect(
texCoords.x / tex.width,
texCoords.y / tex.height,
texCoords.width / tex.width,
texCoords.height / tex.height
);
float aspect = texCoords.width / texCoords.height;
Rect drawRect = previewRect;
if (aspect > 1f)
{
float height = drawRect.width / aspect;
drawRect.y += (drawRect.height - height) * 0.5f;
drawRect.height = height;
}
else
{
float width = drawRect.height * aspect;
drawRect.x += (drawRect.width - width) * 0.5f;
drawRect.width = width;
}
GUI.DrawTextureWithTexCoords(drawRect, tex, normalizedCoords, true);
if (!string.IsNullOrEmpty(item.Desc))
{
GUI.Label(previewRect, new GUIContent("", item.Desc));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("无图片", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
}
else
{
GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
GUILayout.Space(-50);
EditorGUILayout.LabelField("未选择", GUILayout.Width(50));
}
// FrameImageScalefloat输入框
data.FrameImageScale = EditorGUILayout.FloatField(data.FrameImageScale, GUILayout.Width(80));
// 删除按钮
GUI.backgroundColor = Color.red;
if (GUILayout.Button("删除", GUILayout.Width(60)))
{
if (EditorUtility.DisplayDialog("确认删除",
$"确定要删除头像框 {data.Id} ({data.NameKey}) 吗?",
"删除", "取消"))
{
headFrameDataList.Remove(data);
}
}
GUI.backgroundColor = Color.white;
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// 添加新头像框
/// </summary>
private void AddNewHeadFrame()
{
int newId = 1;
if (headFrameDataList.Count > 0)
{
newId = headFrameDataList.Max(x => x.Id) + 1;
}
var newFrame = new HeadFrameData
{
Id = newId,
NameKey = "",
Init = 0,
IconId = -1,
FrameImageScale = 1.0f
};
headFrameDataList.Add(newFrame);
scrollPosition = new Vector2(0, float.MaxValue);
}
/// <summary>
/// 保存数据到Excel
/// </summary>
private void SaveData()
{
try
{
string avatarExcelPath = Path.Combine(docsRootPath, "config", AVATAR_EXCEL_NAME);
if (!File.Exists(avatarExcelPath))
{
EditorUtility.DisplayDialog("错误", $"未找到Avatar配置文件: {avatarExcelPath}", "确定");
return;
}
using (var package = new ExcelPackage(new FileInfo(avatarExcelPath)))
{
var worksheet = package.Workbook.Worksheets[AVATAR_SHEET_NAME];
if (worksheet == null)
{
EditorUtility.DisplayDialog("错误", $"Avatar.xlsx中未找到Sheet: {AVATAR_SHEET_NAME}", "确定");
return;
}
// 查找列索引
int idCol = -1, nameKeyCol = -1, initCol = -1, iconCol = -1, frameImageScaleCol = -1;
int columnCount = worksheet.Dimension.Columns;
for (int col = 1; col <= columnCount; col++)
{
string header = worksheet.Cells[1, col].Text;
switch (header)
{
case "Id":
idCol = col;
break;
case "NameKey":
nameKeyCol = col;
break;
case "Init":
initCol = col;
break;
case "Icon":
iconCol = col;
break;
case "FrameImageScale":
frameImageScaleCol = col;
break;
}
}
// 更新和删除数据从第3行开始
int rowCount = worksheet.Dimension.Rows;
var processedIds = new HashSet<int>();
// 第一遍:更新现有行或删除
for (int row = rowCount; row >= 3; row--)
{
string idText = worksheet.Cells[row, idCol].Text;
if (string.IsNullOrEmpty(idText)) continue;
if (!int.TryParse(idText, out int id)) continue;
var frameData = headFrameDataList.Find(x => x.Id == id);
if (frameData != null)
{
worksheet.Cells[row, nameKeyCol].Value = frameData.NameKey;
worksheet.Cells[row, initCol].Value = frameData.Init;
if (frameData.IconId < 0)
{
worksheet.Cells[row, iconCol].Value = "";
}
else
{
worksheet.Cells[row, iconCol].Value = frameData.IconId;
}
worksheet.Cells[row, frameImageScaleCol].Value = frameData.FrameImageScale;
processedIds.Add(id);
}
else
{
worksheet.DeleteRow(row);
}
}
// 第二遍:添加新行
int currentRow = worksheet.Dimension?.Rows ?? 2;
foreach (var frameData in headFrameDataList)
{
if (!processedIds.Contains(frameData.Id))
{
currentRow++;
worksheet.Cells[currentRow, idCol].Value = frameData.Id;
worksheet.Cells[currentRow, nameKeyCol].Value = frameData.NameKey;
worksheet.Cells[currentRow, initCol].Value = frameData.Init;
if (frameData.IconId < 0)
{
worksheet.Cells[currentRow, iconCol].Value = "";
}
else
{
worksheet.Cells[currentRow, iconCol].Value = frameData.IconId;
}
worksheet.Cells[currentRow, frameImageScaleCol].Value = frameData.FrameImageScale;
}
}
package.Save();
}
// 提示成功并提醒推送
bool understood = EditorUtility.DisplayDialog("保存成功",
"配置已保存到Excel文件\n\n" +
"⚠️ 重要提醒:\n" +
"请及时在SourceTree或GitHubDesktop中\n" +
"1. 提交(Commit)本次修改\n" +
"2. 推送(Push)到远程仓库\n\n" +
"避免与其他策划产生冲突!",
"我知道了");
}
catch (Exception e)
{
EditorUtility.DisplayDialog("错误", $"保存数据失败: {e.Message}\n{e.StackTrace}", "确定");
}
}
/// <summary>
/// 头像框数据类
/// </summary>
private class HeadFrameData
{
public int Id;
public string NameKey;
public int Init;
public int IconId;
public float FrameImageScale;
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,32 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b29e98153ec2fbd44b8f7da1b41194e8, type: 3}
m_Name: SpineSettings
m_EditorClassIdentifier:
defaultScale: 0.01
defaultMix: 0.2
defaultShader: Spine/Skeleton
defaultZSpacing: 0
defaultInstantiateLoop: 1
showHierarchyIcons: 1
setTextureImporterSettings: 1
textureSettingsReference:
blendModeMaterialMultiply: {fileID: 0}
blendModeMaterialScreen: {fileID: 0}
blendModeMaterialAdditive: {fileID: 0}
atlasTxtImportWarning: 1
textureImporterWarning: 1
componentMaterialWarning: 1
autoReloadSceneSkeletons: 1
handleScale: 1
mecanimEventIncludeFolderName: 1
timelineUseBlendDuration: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 73533bdd7e8904b458229a3ad57d6e4f
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

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

8
Assets/Editor/ThirdParty/EPPlus.meta vendored Normal file
View File

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

Binary file not shown.

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 1b0919e5f31535f4daf42348cce924fc
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 844f2c47be54e4448bd722d6b32a8954
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 3cd4be4136000d648aa11abd303e1467
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: 3245b268a22ddce4994a13032b839731
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,33 @@
fileFormatVersion: 2
guid: dc4e68b41881f794086d939cadaa898a
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 1
settings:
DefaultValueInitialized: true
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Scripts.meta Normal file
View File

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

8
Assets/Scripts/Base.meta Normal file
View File

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

View File

@ -0,0 +1,232 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using UnityEngine;
using UnityGameFramework.Runtime;
namespace CrazyMaple
{
/// <summary>
/// 游戏入口。
/// </summary>
public partial class GameEntry : MonoBehaviour
{
/// <summary>
/// 获取游戏基础组件。
/// </summary>
public static BaseComponent Base
{
get;
private set;
}
/// <summary>
/// 获取配置组件。
/// </summary>
public static ConfigComponent Config
{
get;
private set;
}
/// <summary>
/// 获取数据结点组件。
/// </summary>
public static DataNodeComponent DataNode
{
get;
private set;
}
/// <summary>
/// 获取数据表组件。
/// </summary>
public static BywayDataTableComponent DataTable
{
get;
private set;
}
/// <summary>
/// 获取 Byway 数据表组件(新配置系统)。
/// </summary>
// public static BywayDataTableComponent BywayDataTable
// {
// get;
// private set;
// }
/// <summary>
/// 获取调试组件。
/// </summary>
public static DebuggerComponent Debugger
{
get;
private set;
}
/// <summary>
/// 获取下载组件。
/// </summary>
public static DownloadComponent Download
{
get;
private set;
}
/// <summary>
/// 获取实体组件。
/// </summary>
public static EntityComponent Entity
{
get;
private set;
}
/// <summary>
/// 获取事件组件。
/// </summary>
public static EventComponent Event
{
get;
private set;
}
/// <summary>
/// 获取文件系统组件。
/// </summary>
public static FileSystemComponent FileSystem
{
get;
private set;
}
/// <summary>
/// 获取有限状态机组件。
/// </summary>
public static FsmComponent Fsm
{
get;
private set;
}
/// <summary>
/// 获取本地化组件。
/// </summary>
public static LocalizationComponent Localization
{
get;
private set;
}
/// <summary>
/// 获取网络组件。
/// </summary>
public static NetworkComponent Network
{
get;
private set;
}
/// <summary>
/// 获取对象池组件。
/// </summary>
public static ObjectPoolComponent ObjectPool
{
get;
private set;
}
/// <summary>
/// 获取流程组件。
/// </summary>
public static ProcedureComponent Procedure
{
get;
private set;
}
/// <summary>
/// 获取资源组件。
/// </summary>
public static ResourceComponent Resource
{
get;
private set;
}
/// <summary>
/// 获取场景组件。
/// </summary>
public static SceneComponent Scene
{
get;
private set;
}
/// <summary>
/// 获取配置组件。
/// </summary>
public static SettingComponent Setting
{
get;
private set;
}
/// <summary>
/// 获取声音组件。
/// </summary>
public static SoundComponent Sound
{
get;
private set;
}
/// <summary>
/// 获取界面组件。
/// </summary>
public static UIComponent UI
{
get;
private set;
}
/// <summary>
/// 获取网络组件。
/// </summary>
public static WebRequestComponent WebRequest
{
get;
private set;
}
private static void InitBuiltinComponents()
{
Base = UnityGameFramework.Runtime.GameEntry.GetComponent<BaseComponent>();
Config = UnityGameFramework.Runtime.GameEntry.GetComponent<ConfigComponent>();
DataNode = UnityGameFramework.Runtime.GameEntry.GetComponent<DataNodeComponent>();
DataTable = UnityGameFramework.Runtime.GameEntry.GetComponent<BywayDataTableComponent>();
// BywayDataTable = UnityGameFramework.Runtime.GameEntry.GetComponent<BywayDataTableComponent>();
Debugger = UnityGameFramework.Runtime.GameEntry.GetComponent<DebuggerComponent>();
Download = UnityGameFramework.Runtime.GameEntry.GetComponent<DownloadComponent>();
Entity = UnityGameFramework.Runtime.GameEntry.GetComponent<EntityComponent>();
Event = UnityGameFramework.Runtime.GameEntry.GetComponent<EventComponent>();
FileSystem = UnityGameFramework.Runtime.GameEntry.GetComponent<FileSystemComponent>();
Fsm = UnityGameFramework.Runtime.GameEntry.GetComponent<FsmComponent>();
Localization = UnityGameFramework.Runtime.GameEntry.GetComponent<LocalizationComponent>();
Network = UnityGameFramework.Runtime.GameEntry.GetComponent<NetworkComponent>();
ObjectPool = UnityGameFramework.Runtime.GameEntry.GetComponent<ObjectPoolComponent>();
Procedure = UnityGameFramework.Runtime.GameEntry.GetComponent<ProcedureComponent>();
Resource = UnityGameFramework.Runtime.GameEntry.GetComponent<ResourceComponent>();
Scene = UnityGameFramework.Runtime.GameEntry.GetComponent<SceneComponent>();
Setting = UnityGameFramework.Runtime.GameEntry.GetComponent<SettingComponent>();
Sound = UnityGameFramework.Runtime.GameEntry.GetComponent<SoundComponent>();
UI = UnityGameFramework.Runtime.GameEntry.GetComponent<UIComponent>();
WebRequest = UnityGameFramework.Runtime.GameEntry.GetComponent<WebRequestComponent>();
}
}
}

View File

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

View File

@ -0,0 +1,26 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using UnityEngine;
namespace CrazyMaple
{
/// <summary>
/// 游戏入口。
/// </summary>
public partial class GameEntry : MonoBehaviour
{
public static bool ApplicationQuitStatus = false;
private void Start()
{
InitBuiltinComponents();
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,209 @@
using System;
using System.Collections.Generic;
using System.IO;
using CrazyMaple;
using Thrift;
using Thrift.Protocol;
using Thrift.Transport;
using Thrift.Transport.Client;
using UnityEngine;
namespace Byway.Config
{
/// <summary>
/// 配置管理器 - 按需加载模式
/// 自动生成于: 2026-01-13 14:32:35
/// </summary>
public class ConfigManager
{
private static ConfigManager _instance;
public static ConfigManager Instance
{
get
{
if (_instance == null)
{
_instance = new ConfigManager();
}
return _instance;
}
}
// 配置文件根目录AssetBundle 路径)
private const string CONFIG_ROOT = "Assets/Design_SubModule/ConfigData";
// 缓存已加载的配置
private Dictionary<Type, object> _configCache = new Dictionary<Type, object>();
// 缓存AllConfigs实例
private Byway.Thrift.Data.AllConfigs _allConfigs = null;
// 标记AllConfigs是否已经加载完成
private bool _allConfigsLoaded = false;
private ConfigManager() { }
/// <summary>
/// 获取配置数据只从AllConfigs获取必须先调用PreloadAllConfigs
/// </summary>
public T GetConfig<T>() where T : class, TBase, new()
{
Type type = typeof(T);
string typeName = type.Name;
// 先查缓存(缓存命中时不打印日志,减少冗余输出)
if (_configCache.TryGetValue(type, out object cached))
{
return cached as T;
}
// 检查AllConfigs是否已加载
if (!_allConfigsLoaded)
{
Debug.LogError($"[ConfigManager] 配置尚未加载完成,请先调用 PreloadAllConfigs() 并等待完成!配置类型: {typeName}");
return null;
}
// 从 AllConfigs 获取
if (_allConfigs != null)
{
T config = GetConfigFromAllConfigs<T>();
if (config != null)
{
_configCache[type] = config;
Debug.Log($"[ConfigManager] 首次加载配置: {typeName}");
return config;
}
}
else
{
Debug.LogError($"[ConfigManager] AllConfigs 为 null配置类型: {typeName}");
}
Debug.LogError($"[ConfigManager] 无法获取配置: {typeName}");
return null;
}
/// <summary>
/// 从 AllConfigs 中获取指定类型的配置
/// </summary>
private T GetConfigFromAllConfigs<T>() where T : class, TBase, new()
{
if (_allConfigs == null)
return null;
string typeName = typeof(T).Name;
// 使用反射获取 AllConfigs 中对应的属性(而不是字段)
var property = typeof(Byway.Thrift.Data.AllConfigs).GetProperty(typeName);
if (property != null)
{
var value = property.GetValue(_allConfigs) as T;
if (value != null)
{
return value;
}
}
Debug.LogWarning($"[ConfigManager] AllConfigs 中未找到配置: {typeName}");
return null;
}
/// <summary>
/// 预加载所有配置(启动时调用)- 只加载AllConfig.bytes
/// </summary>
public void PreloadAllConfigs(Action onComplete = null)
{
var startTime = System.Diagnostics.Stopwatch.StartNew();
try
{
string fileName = "AllConfigs.bytes";
string assetPath = Path.Combine(CONFIG_ROOT, fileName).Replace("\\", "/");
Debug.Log($"[ConfigManager] 开始加载 AllConfigs.bytes, 路径: {assetPath}");
// 直接使用 GameEntry.Resource.LoadAsset 确保正确加载 TextAsset
var callbacks = new GameFramework.Resource.LoadAssetCallbacks(
(assetName, asset, duration, userData) =>
{
// 加载成功
TextAsset textAsset = asset as TextAsset;
if (textAsset == null)
{
startTime.Stop();
Debug.LogError($"[ConfigManager] AllConfigs 加载失败: 资源类型错误,实际类型: {asset?.GetType().Name}");
_allConfigsLoaded = false;
onComplete?.Invoke();
return;
}
Debug.Log($"[ConfigManager] AllConfigs.bytes 加载成功, 文件大小: {textAsset.bytes.Length} bytes");
try
{
_allConfigs = new Byway.Thrift.Data.AllConfigs();
// 使用 Thrift 反序列化
using (var transport = new TMemoryBufferTransport(textAsset.bytes, new TConfiguration()))
{
using (var protocol = new TBinaryProtocol(transport))
{
_allConfigs.ReadAsync(protocol, System.Threading.CancellationToken.None).GetAwaiter().GetResult();
}
}
startTime.Stop();
Debug.Log($"[ConfigManager] AllConfigs 加载完成!耗时: {startTime.ElapsedMilliseconds} ms ({startTime.Elapsed.TotalSeconds:F3} 秒)");
_allConfigsLoaded = true;
}
catch (Exception ex)
{
startTime.Stop();
Debug.LogError($"[ConfigManager] AllConfigs 反序列化失败,耗时: {startTime.ElapsedMilliseconds} ms错误: {ex.Message}\n{ex.StackTrace}");
_allConfigsLoaded = false;
}
finally
{
onComplete?.Invoke();
}
},
(assetName, status, errorMessage, userData) =>
{
// 加载失败
startTime.Stop();
Debug.LogError($"[ConfigManager] AllConfigs 加载失败, 路径: {assetName}, 状态: {status}, 错误: {errorMessage}");
_allConfigsLoaded = false;
onComplete?.Invoke();
},
(assetName, dependencyAssetName, loadedCount, totalCount, userData) =>
{
// 依赖资源加载
Debug.Log($"[ConfigManager] 加载依赖资源: {dependencyAssetName} ({loadedCount}/{totalCount})");
}
);
GameEntry.Resource.LoadAsset(assetPath, callbacks);
}
catch (Exception ex)
{
startTime.Stop();
Debug.LogError($"[ConfigManager] 加载 AllConfigs 失败,错误: {ex.Message}\n{ex.StackTrace}");
_allConfigsLoaded = false;
onComplete?.Invoke();
}
}
/// <summary>
/// 清空所有缓存
/// </summary>
public void ClearCache()
{
_configCache.Clear();
_allConfigs = null;
_allConfigsLoaded = false;
}
}
}

View File

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

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using CrazyMaple;
using GameFramework.DataTable;
using UnityEngine;
/// <summary>
/// 配置管理器 - 按需加载模式
/// 自动生成于: 2026-01-13 14:32:35
/// </summary>
public class ItemHelper
{
private static ItemHelper _instance;
public static ItemHelper Instance
{
get
{
if (_instance == null)
{
_instance = new ItemHelper();
}
return _instance;
}
}
public IDataTable<DRNetworkItemData> DtNetworkItemData;
public IDataTable<DREmojiData> DtEmojiData;
ItemHelper()
{
DtNetworkItemData = GameEntry.DataTable.GetDataTable<DRNetworkItemData>();
DtEmojiData = GameEntry.DataTable.GetDataTable<DREmojiData>();
}
/// <summary>
/// 判断是否是表情道具
/// </summary>
public bool IsEmojiItemId(int id)
{
if (DtNetworkItemData == null)
{
var _ = Instance; // 确保实例初始化
}
if (DtNetworkItemData == null)
{
return false;
}
var emojiItems = DtNetworkItemData.GetDataRows((x)=> {return x.IType == 109;});
if (emojiItems == null)
{
return false;
}
foreach (var item in emojiItems)
{
if (item.Id == id)
{
return true;
}
}
return false;
}
/// <summary>
/// 判断是否是表情道具
/// </summary>
public bool IsEmojiItemId(string id)
{
if (!int.TryParse(id, out int itemId))
{
return false;
}
return IsEmojiItemId(itemId);
}
// public string GetItemEffect(int Id)
// {
// if (DtNetworkItemData == null)
// {
// var _ = Instance; // 确保实例初始化
// }
// DRNetworkItemData da = PlayerProfileData.DtNetworkItemData.GetDataRow(Id);
// if (da == null)
// {
// return "";
// }
// return da.Effect;
// }
// public string GetEmojiIconPath(int id)
// {
// if(IsEmojiItemId(id))
// {
// int emojiId = int.Parse(GetItemEffect(id).Split(",")[0]);
// return PlayerProfileData.GetEmojiAsset(emojiId);
// }
// else{
// return "";
// }
// }
// public string GetEmojiIconPath(string id)
// {
// if (!int.TryParse(id, out int itemId))
// {
// return "";
// }
// return GetEmojiIconPath(itemId);
// }
}

View File

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

View File

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

View File

@ -0,0 +1,400 @@
//------------------------------------------------------------
// Byway Data Table System
// 基于 GameFramework DataTableComponent 重新实现
// 唯一区别:数据源从 Txt 改为 Thrift bytes
//------------------------------------------------------------
using GameFramework;
using GameFramework.DataTable;
using GameFramework.Resource;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityGameFramework.Runtime
{
/// <summary>
/// Byway 数据表组件 - 完全复刻 DataTableComponent 的功能
/// </summary>
[DisallowMultipleComponent]
[AddComponentMenu("Game Framework/Byway Data Table")]
public sealed class BywayDataTableComponent : GameFrameworkComponent
{
private const int DefaultPriority = 0;
private IDataTableManager m_DataTableManager = null;
private EventComponent m_EventComponent = null;
[SerializeField]
private bool m_EnableLoadDataTableUpdateEvent = false;
[SerializeField]
private bool m_EnableLoadDataTableDependencyAssetEvent = false;
[SerializeField]
private string m_DataTableHelperTypeName = "CrazyMaple.BywayDataTableHelper";
[SerializeField]
private DataTableHelperBase m_CustomDataTableHelper = null;
[SerializeField]
private int m_CachedBytesSize = 0;
/// <summary>
/// 获取数据表数量。
/// </summary>
public int Count
{
get
{
return m_DataTableManager.Count;
}
}
/// <summary>
/// 获取缓冲二进制流的大小。
/// </summary>
public int CachedBytesSize
{
get
{
return m_DataTableManager.CachedBytesSize;
}
}
/// <summary>
/// 游戏框架组件初始化。
/// </summary>
protected override void Awake()
{
base.Awake();
m_DataTableManager = GameFrameworkEntry.GetModule<IDataTableManager>();
if (m_DataTableManager == null)
{
Log.Fatal("Data table manager is invalid.");
return;
}
}
private void Start()
{
BaseComponent baseComponent = UnityGameFramework.Runtime.GameEntry.GetComponent<BaseComponent>();
if (baseComponent == null)
{
Log.Fatal("Base component is invalid.");
return;
}
m_EventComponent = UnityGameFramework.Runtime.GameEntry.GetComponent<EventComponent>();
if (m_EventComponent == null)
{
Log.Fatal("Event component is invalid.");
return;
}
if (baseComponent.EditorResourceMode)
{
m_DataTableManager.SetResourceManager(baseComponent.EditorResourceHelper);
}
else
{
m_DataTableManager.SetResourceManager(GameFrameworkEntry.GetModule<IResourceManager>());
}
DataTableHelperBase dataTableHelper = Helper.CreateHelper(m_DataTableHelperTypeName, m_CustomDataTableHelper);
if (dataTableHelper == null)
{
Log.Error("Can not create data table helper.");
return;
}
dataTableHelper.name = "Byway Data Table Helper";
Transform transform = dataTableHelper.transform;
transform.SetParent(this.transform);
transform.localScale = Vector3.one;
m_DataTableManager.SetDataProviderHelper(dataTableHelper);
m_DataTableManager.SetDataTableHelper(dataTableHelper);
if (m_CachedBytesSize > 0)
{
EnsureCachedBytesSize(m_CachedBytesSize);
}
Debug.Log("[BywayDataTableComponent] 数据表组件初始化完成");
}
/// <summary>
/// 确保二进制流缓存分配足够大小的内存并缓存。
/// </summary>
/// <param name="ensureSize">要确保二进制流缓存分配内存的大小。</param>
public void EnsureCachedBytesSize(int ensureSize)
{
m_DataTableManager.EnsureCachedBytesSize(ensureSize);
}
/// <summary>
/// 释放缓存的二进制流。
/// </summary>
public void FreeCachedBytes()
{
m_DataTableManager.FreeCachedBytes();
}
/// <summary>
/// 是否存在数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <returns>是否存在数据表。</returns>
public bool HasDataTable<T>() where T : IDataRow
{
return m_DataTableManager.HasDataTable<T>();
}
/// <summary>
/// 是否存在数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <returns>是否存在数据表。</returns>
public bool HasDataTable(Type dataRowType)
{
return m_DataTableManager.HasDataTable(dataRowType);
}
/// <summary>
/// 是否存在数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <param name="name">数据表名称。</param>
/// <returns>是否存在数据表。</returns>
public bool HasDataTable<T>(string name) where T : IDataRow
{
return m_DataTableManager.HasDataTable<T>(name);
}
/// <summary>
/// 是否存在数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <param name="name">数据表名称。</param>
/// <returns>是否存在数据表。</returns>
public bool HasDataTable(Type dataRowType, string name)
{
return m_DataTableManager.HasDataTable(dataRowType, name);
}
/// <summary>
/// 获取数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <returns>要获取的数据表。</returns>
public IDataTable<T> GetDataTable<T>() where T : IDataRow
{
return m_DataTableManager.GetDataTable<T>();
}
/// <summary>
/// 获取数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <returns>要获取的数据表。</returns>
public DataTableBase GetDataTable(Type dataRowType)
{
return m_DataTableManager.GetDataTable(dataRowType);
}
/// <summary>
/// 获取数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <param name="name">数据表名称。</param>
/// <returns>要获取的数据表。</returns>
public IDataTable<T> GetDataTable<T>(string name) where T : IDataRow
{
return m_DataTableManager.GetDataTable<T>(name);
}
/// <summary>
/// 获取数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <param name="name">数据表名称。</param>
/// <returns>要获取的数据表。</returns>
public DataTableBase GetDataTable(Type dataRowType, string name)
{
return m_DataTableManager.GetDataTable(dataRowType, name);
}
/// <summary>
/// 获取所有数据表。
/// </summary>
public DataTableBase[] GetAllDataTables()
{
return m_DataTableManager.GetAllDataTables();
}
/// <summary>
/// 获取所有数据表。
/// </summary>
/// <param name="results">所有数据表。</param>
public void GetAllDataTables(List<DataTableBase> results)
{
m_DataTableManager.GetAllDataTables(results);
}
/// <summary>
/// 创建数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <returns>要创建的数据表。</returns>
public IDataTable<T> CreateDataTable<T>() where T : class, IDataRow, new()
{
return CreateDataTable<T>(null);
}
/// <summary>
/// 创建数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <returns>要创建的数据表。</returns>
public DataTableBase CreateDataTable(Type dataRowType)
{
return CreateDataTable(dataRowType, null);
}
/// <summary>
/// 创建数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <param name="name">数据表名称。</param>
/// <returns>要创建的数据表。</returns>
public IDataTable<T> CreateDataTable<T>(string name) where T : class, IDataRow, new()
{
IDataTable<T> dataTable = m_DataTableManager.CreateDataTable<T>(name);
DataTableBase dataTableBase = (DataTableBase)dataTable;
dataTableBase.ReadDataSuccess += OnReadDataSuccess;
dataTableBase.ReadDataFailure += OnReadDataFailure;
if (m_EnableLoadDataTableUpdateEvent)
{
dataTableBase.ReadDataUpdate += OnReadDataUpdate;
}
if (m_EnableLoadDataTableDependencyAssetEvent)
{
dataTableBase.ReadDataDependencyAsset += OnReadDataDependencyAsset;
}
return dataTable;
}
/// <summary>
/// 创建数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <param name="name">数据表名称。</param>
/// <returns>要创建的数据表。</returns>
public DataTableBase CreateDataTable(Type dataRowType, string name)
{
DataTableBase dataTable = m_DataTableManager.CreateDataTable(dataRowType, name);
dataTable.ReadDataSuccess += OnReadDataSuccess;
dataTable.ReadDataFailure += OnReadDataFailure;
if (m_EnableLoadDataTableUpdateEvent)
{
dataTable.ReadDataUpdate += OnReadDataUpdate;
}
if (m_EnableLoadDataTableDependencyAssetEvent)
{
dataTable.ReadDataDependencyAsset += OnReadDataDependencyAsset;
}
return dataTable;
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable<T>() where T : IDataRow, new()
{
return m_DataTableManager.DestroyDataTable<T>();
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable(Type dataRowType)
{
return m_DataTableManager.DestroyDataTable(dataRowType);
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <param name="name">数据表名称。</param>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable<T>(string name) where T : IDataRow
{
return m_DataTableManager.DestroyDataTable<T>(name);
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <param name="dataRowType">数据表行的类型。</param>
/// <param name="name">数据表名称。</param>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable(Type dataRowType, string name)
{
return m_DataTableManager.DestroyDataTable(dataRowType, name);
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <typeparam name="T">数据表行的类型。</typeparam>
/// <param name="dataTable">要销毁的数据表。</param>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable<T>(IDataTable<T> dataTable) where T : IDataRow
{
return m_DataTableManager.DestroyDataTable(dataTable);
}
/// <summary>
/// 销毁数据表。
/// </summary>
/// <param name="dataTable">要销毁的数据表。</param>
/// <returns>是否销毁数据表成功。</returns>
public bool DestroyDataTable(DataTableBase dataTable)
{
return m_DataTableManager.DestroyDataTable(dataTable);
}
private void OnReadDataSuccess(object sender, ReadDataSuccessEventArgs e)
{
m_EventComponent.Fire(this, LoadDataTableSuccessEventArgs.Create(e));
}
private void OnReadDataFailure(object sender, ReadDataFailureEventArgs e)
{
Log.Warning("Load data table failure, asset name '{0}', error message '{1}'.", e.DataAssetName, e.ErrorMessage);
m_EventComponent.Fire(this, LoadDataTableFailureEventArgs.Create(e));
}
private void OnReadDataUpdate(object sender, ReadDataUpdateEventArgs e)
{
m_EventComponent.Fire(this, LoadDataTableUpdateEventArgs.Create(e));
}
private void OnReadDataDependencyAsset(object sender, ReadDataDependencyAssetEventArgs e)
{
m_EventComponent.Fire(this, LoadDataTableDependencyAssetEventArgs.Create(e));
}
}
}

View File

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

View File

@ -0,0 +1,331 @@
//------------------------------------------------------------
// Byway Data Table Helper
// 基于 DefaultDataTableHelper 重新实现
// 核心改动ParseData 方法从 Thrift 配置加载数据
//------------------------------------------------------------
using GameFramework;
using GameFramework.DataTable;
using System;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace CrazyMaple
{
/// <summary>
/// Byway 数据表辅助器 - 从 Thrift 配置加载数据
/// </summary>
public class BywayDataTableHelper : DataTableHelperBase
{
private static readonly string BytesAssetExtension = ".bytes";
private ResourceComponent m_ResourceComponent = null;
/// <summary>
/// 读取数据表。
/// </summary>
public override bool ReadData(DataTableBase dataTable, string dataTableAssetName, object dataTableAsset, object userData)
{
TextAsset dataTableTextAsset = dataTableAsset as TextAsset;
if (dataTableTextAsset != null)
{
// Byway 系统始终使用 bytes 格式Thrift 二进制)
return dataTable.ParseData(dataTableTextAsset.bytes, userData);
}
Log.Warning("Data table asset '{0}' is invalid.", dataTableAssetName);
return false;
}
/// <summary>
/// 读取数据表。
/// </summary>
public override bool ReadData(DataTableBase dataTable, string dataTableAssetName, byte[] dataTableBytes, int startIndex, int length, object userData)
{
// Byway 系统始终使用 bytes 格式Thrift 二进制)
return dataTable.ParseData(dataTableBytes, startIndex, length, userData);
}
/// <summary>
/// 解析数据表(字符串格式 - Byway 不支持)
/// </summary>
public override bool ParseData(DataTableBase dataTable, string dataTableString, object userData)
{
Log.Error("Byway data table system does not support string format. Use Thrift bytes instead.");
return false;
}
/// <summary>
/// 解析数据表(核心方法 - 从 Thrift 配置加载)
/// </summary>
public override bool ParseData(DataTableBase dataTable, byte[] dataTableBytes, int startIndex, int length, object userData)
{
var timer = System.Diagnostics.Stopwatch.StartNew();
try
{
// 获取 DataRow 类型
Type dataRowType = dataTable.Type;
string dataRowTypeName = dataRowType.Name;
// 推断 Thrift 配置类型名(去除 DR 前缀)
// 例如DRMergeData -> MergeData
string configTypeName = dataRowTypeName.StartsWith("DR")
? dataRowTypeName.Substring(2)
: dataRowTypeName;
// 通过反射获取 Thrift 配置类型
Type configType = FindThriftConfigType(configTypeName);
if (configType == null)
{
Log.Error("Can not find Thrift config type '{0}' for data row type '{1}'.", configTypeName, dataRowTypeName);
return false;
}
// 调用 ConfigManager.GetConfig<T>() 获取配置对象
var getConfigMethod = typeof(Byway.Config.ConfigManager)
.GetMethod("GetConfig")
.MakeGenericMethod(configType);
object configInstance = getConfigMethod.Invoke(Byway.Config.ConfigManager.Instance, null);
if (configInstance == null)
{
Log.Error("Can not get config instance for type '{0}'.", configTypeName);
return false;
}
// 获取配置字典
var dictData = GetConfigDictionary(configInstance, configTypeName);
if (dictData == null)
{
Log.Error("Can not get config dictionary for type '{0}'.", configTypeName);
return false;
}
// 性能优化:不预先创建所有 DataRow而是缓存配置字典供按需查询
// 对于几千行的大表,这样可以避免启动时创建几千个 DataRow 对象
var dict = dictData as System.Collections.IDictionary;
if (dict == null)
{
Log.Error("Config dictionary is not IDictionary type.");
return false;
}
// 将配置实例缓存到 DataTable 的 userData 中,供后续按需获取
// 格式:{ "ConfigInstance": configInstance, "ConfigDict": dict }
var cacheData = new System.Collections.Generic.Dictionary<string, object>
{
{ "ConfigInstance", configInstance },
{ "ConfigDict", dictData },
{ "OriginalUserData", userData }
};
// 性能优化选项:反射路径实测反而更慢,禁用
// 标准路径ParseDataRow + ConfigDict已经是最优解
bool useDirectConstruction = false; // 禁用反射优化路径
int successCount = 0;
int failCount = 0;
// Debug.LogError($"[性能测试] 表 {dataRowTypeName} 开始加载,行数: {dict.Count}, 模式: 标准路径(ConfigDict直接获取)");
var loadTimer = System.Diagnostics.Stopwatch.StartNew();
if (useDirectConstruction)
{
// 【性能最优路径】直接创建 DataRow 并设置数据,完全跳过 ParseDataRow
// Log.Info("Large table detected ({0} rows), using direct construction mode.", dict.Count);
foreach (System.Collections.DictionaryEntry entry in dict)
{
try
{
int id = (int)entry.Key;
object itemData = entry.Value;
// 直接创建 DataRow 实例(不走 AddDataRow
var dataRow = Activator.CreateInstance(dataTable.Type) as DataRowBase;
if (dataRow == null)
{
failCount++;
continue;
}
// 调用 SetConfigData 方法直接设置数据(跳过所有查询)
var setDataMethod = dataTable.Type.GetMethod("SetConfigData");
if (setDataMethod != null)
{
setDataMethod.Invoke(dataRow, new object[] { itemData });
// 使用反射调用 DataTable 的内部 AddDataRow 方法
var addMethod = dataTable.GetType().GetMethod("InternalAddDataRow",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (addMethod != null)
{
addMethod.Invoke(dataTable, new object[] { dataRow });
successCount++;
}
else
{
// 兜底:使用 AddDataRow会触发 ParseDataRow但数据已设置好
if (dataTable.AddDataRow(id.ToString(), null))
{
successCount++;
}
else
{
failCount++;
}
}
}
else
{
Log.Warning("SetConfigData method not found, fallback to AddDataRow");
// 降级到标准模式
useDirectConstruction = false;
break;
}
}
catch (Exception ex)
{
Log.Error("Direct construction failed for entry: {0}", ex.Message);
failCount++;
}
}
}
// 【标准路径】通过 AddDataRow + ParseDataRow
if (!useDirectConstruction || (successCount == 0 && dict.Count > 0))
{
Log.Info("Using standard AddDataRow mode ({0} rows).", dict.Count);
successCount = 0;
failCount = 0;
foreach (System.Collections.DictionaryEntry entry in dict)
{
try
{
int id = (int)entry.Key;
// 调用 DataTable.AddDataRow(int id)
// DataRow 的 ParseDataRow 方法会被调用,传入 id 和配置实例
if (!dataTable.AddDataRow(id.ToString(), cacheData))
{
Log.Warning("Add data row failed for id '{0}'.", id);
failCount++;
}
else
{
successCount++;
}
}
catch (Exception ex)
{
Log.Error("Add data row exception: {0}", ex.Message);
failCount++;
}
}
}
loadTimer.Stop();
// Debug.LogError($"[性能测试] 表 {dataRowTypeName} 加载完成,成功: {successCount}, 失败: {failCount}, 耗时: {loadTimer.ElapsedMilliseconds}ms ({loadTimer.Elapsed.TotalSeconds:F3}秒)");
timer.Stop();
// Debug.LogError($"[性能测试] 表 {dataRowTypeName} 总耗时(含配置获取): {timer.ElapsedMilliseconds}ms ({timer.Elapsed.TotalSeconds:F3}秒)");
return failCount == 0;
}
catch (Exception exception)
{
Log.Error("Parse Thrift data table failed with exception: {0}\n{1}", exception.Message, exception.StackTrace);
return false;
}
}
/// <summary>
/// 查找 Thrift 配置类型
/// </summary>
private Type FindThriftConfigType(string typeName)
{
// 在 Byway.Thrift.Data 命名空间中查找
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
Type type = assembly.GetType($"Byway.Thrift.Data.{typeName}");
if (type != null)
{
return type;
}
}
return null;
}
/// <summary>
/// 获取配置字典
/// </summary>
private object GetConfigDictionary(object config, string configTypeName)
{
if (config == null) return null;
Type configType = config.GetType();
// 尝试多种可能的字典属性名
string[] possibleNames = new string[]
{
configTypeName + "s", // MergeData -> MergeDatas
configTypeName.ToLower() + "s", // MergeData -> mergedatas
char.ToLower(configTypeName[0]) + configTypeName.Substring(1) + "s", // MergeData -> mergeDatas
"Items",
"Data",
"Configs"
};
foreach (string name in possibleNames)
{
var property = configType.GetProperty(name);
if (property != null)
{
object value = property.GetValue(config);
if (value != null)
{
return value;
}
}
}
// 查找第一个 Dictionary 类型的属性
foreach (var property in configType.GetProperties())
{
Type propType = property.PropertyType;
if (propType.IsGenericType &&
propType.GetGenericTypeDefinition() == typeof(System.Collections.Generic.Dictionary<,>))
{
object value = property.GetValue(config);
if (value != null)
{
return value;
}
}
}
return null;
}
/// <summary>
/// 释放数据表资源
/// </summary>
public override void ReleaseDataAsset(DataTableBase dataTable, object dataTableAsset)
{
m_ResourceComponent.UnloadAsset(dataTableAsset);
}
private void Start()
{
m_ResourceComponent = UnityGameFramework.Runtime.GameEntry.GetComponent<ResourceComponent>();
if (m_ResourceComponent == null)
{
Log.Fatal("Resource component is invalid.");
return;
}
}
}
}

View File

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

8
Assets/Spine.meta Normal file
View File

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

919
Assets/Spine/CHANGELOG.md Normal file
View File

@ -0,0 +1,919 @@
# 3.8
## AS3
* **Breaking changes**
* Renamed `Slot#getAttachmentVertices()` to `Slot#getDeform()`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin#addAttachment()` to `Skin#setAttachment()`.
* Removed `VertexAttachment#applyDeform()` and replaced it with `VertexAttachment#deformAttachment`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* Switched projects from FDT to Visual Studio Code. See updated `README.md` files for instructions.
* **Additions**
* Added `SkeletonBinary` to load binary `.skel` files. See `MixAndMatchExample.as` in `spine-startling-example`.
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin#getAttachments()`. Returns all attachments in the skin.
* Added `Skin#getAttachments(int slotIndex)`. Returns all attachements in the skin for the given slot index.
* Added `Skin#addSkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin#copySkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* Added `Attachment#copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
### Starling
* Added `MixAndMatchExample.as` to demonstrate the new Skin API additions and how to load binary `.skel` files.
* Switched projects from FDT to Visual Studio Code. See updated `README.md` files for instructions.
## C
* **Breaking changes**
* Renamed `spSlot#attachmentVertices` to `spSlot#deform`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `spSkin_addAttachment()` to `Skin#spSkin_addAttachment()`.
* Removed `spVertexAttachment_applyDeform()` and replaced it with `VertexAttachment#deformAttachment`. The attachment set on this field is used to decide if a `spDeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `spMeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* **Additions**
* Added `x` and `y` coordinates for setup pose AABB in `spSkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `spSkin_getAttachments()`. Returns all attachments in the skin.
* Added `spSkin_getAttachments(int slotIndex)`. Returns all attachements in the skin for the given slot index.
* Added `spSkin_addSkin(spSkin* skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `spSkin_copySkin(spSkin* skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `spVertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* All attachments inserted into skins are reference counted. When the last skin referencing an attachment is disposed, the attachment will also be disposed.
* Added `spAttachment_copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `spMeshAttachment_newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
### Cocos2d-Objc
* Added mix-and-match example to demonstrate the new Skin API.
* Added `IKExample`.
* Added `SkeletonAnimation preUpdateWorldTransformsListener` and `SkeletonAnimation postUpdateWorldTransformsListener`. When set, these callbacks will be invokved before and after the skeleton's `updateWorldTransforms()` method is called. See the `IKExample` how it can be used.
### SFML
* Added mix-and-match example to demonstrate the new Skin API.
* Added `IKExample`.
## C++
* **Breaking Changes**
* Renamed `Slot::getAttachmentVertices()` to `Slot::getDeform()`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin::addAttachment()` to `Skin::setAttachment()`.
* Removed `VertexAttachment::applyDeform()` and replaced it with `VertexAttachment::getDeformAttachment()`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `_inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* **Additions**
* `AnimationState` and `TrackEntry` now also accept a subclass of `AnimationStateListenerObject` as a listener for animation events in the overloaded `setListener()` method.
* `SkeletonBinary` and `SkeletonJson` now parse and set all non-essential data like audio path.
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin#getAttachments()`. Returns all attachments in the skin.
* Added `Skin#getAttachments(int slotIndex)`. Returns all attachements in the skin for the given slot index.
* Added `Skin#addSkin(Skin &skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin#copySkin(Skin &skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* All attachments inserted into skins are reference counted. When the last skin referencing an attachment is disposed, the attachment will also be disposed.
* Added `Attachment#copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
* Exposed `x` and `y` on `SkeletonData` through getters and setters.
### Cocos2d-x
* Updated to cocos2d-x 3.17.1
* Added mix-and-match example to demonstrate the new Skin API.
* Exmaple project requires Visual Studio 2019 on Windows
* Added `IKExample`.
* Added `SkeletonAnimation::setPreUpdateWorldTransformsListener()` and `SkeletonAnimation::setPreUpdateWorldTransformsListener()`. When set, these callbacks will be invokved before and after the skeleton's `updateWorldTransforms()` method is called. See the `IKExample` how it can be used.
### SFML
* Added mix-and-match example to demonstrate the new Skin API.
### UE4
* Added `bAutoPlaying` flag to `USpineSkeletonAnimationComponent`. When `false`, the component will not update the internal animation state and skeleton.
* Updated example project to UE 4.22.
* (Re-)Importing Spine assets will perform a version compatibility check and alert users about mismatches in editor mode.
* `USpineSkeletonRendererComponent` allows passing a `USpineSkeletonComponent` to update it. This way, the renderer component can be used without a skeleton component on the same actor.
* Added blueprint-callable methods to `SpineSkeletonComponent` and `SpineSkeletonAnimationComponent` to query and set skins, and enumerate bones, slots, and animations.
* Extended skeleton data editor preview. The preview now shows bones, slots, animations, and skins found in the skeleton data. See this [blog post](http://esotericsoftware.com/blog/Unreal-Engine-4-quality-of-life-improvements).
* Added preview animation and skin fields, allowing you to preview animations and skins right in the editor. See this [blog post](http://esotericsoftware.com/blog/Unreal-Engine-4-quality-of-life-improvements).
* Removed dependency on `RHI`, `RenderCore`, and `ShaderCore`.
* Re-importing atlases and their textures now works consistently in all situations.
* Added mix-and-match example to demonstrate the new Skin API.
* Materials on `SkeletonRendererComponent` are now blueprint read and writeable. This allows setting dynamic material instances at runtime.
* Added `InitialSkin` property to `USpineWidget`. This allows previewing different skins in the UMG Designer. Initial skins can still be overridden via blueprint events such as `On Initialized`.
## C# ##
* **Breaking changes**
* **Changed `IkConstraintData.Bones` type from `List<BoneData>` to `ExposedList<BoneData>`** for unification reasons. *Note: this modification will most likely not affect user code.*
* Renamed `Slot.AttachmentVertices` to `Slot.Deform`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin.AddAttachment()` to `Skin.SetAttachment()`.
* Removed `FindAttachmentsForSlot(int slotIndex, List<Attachment> attachments)` and `FindNamesForSlot (int slotIndex, List<string> names)` and replaced it with `Skin.GetAttachments(int slotIndex, List<SkinEntry> attachments)` which returns the combined `SkinEntry` object holding both name and attachment.
* Removed `VertexAttachment.ApplyDeform()` and replaced it with `VertexAttachment.DeformAttachment`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* **Additions**
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin.GetAttachments()`. Returns all attachments in the skin.
* Added `Skin.GetAttachments(int slotIndex, List<SkinEntry> attachments)`. Returns all attachements in the skin for the given slot index. This method replaces `FindAttachmentsForSlot` and `FindNamesForSlot`.
* Added `Skin.AddSkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin.CopySkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* Added `Attachment.Copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment.NewLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
### Unity
* **Breaking changes**
* **Officially supported Unity versions are 2017.1-2020.2**.
* **Spine `.asmdef` files are again active by default**. They have previously been deactivated to `.txt` extension which is now no longer necessary.
* **Removed PoseSkeleton() and PoseWithAnimation()** extension methods to prevent issues where animations are not mixed out. Problem was that these methods did not set AnimationState, leaving incorrect state at e.g. attachments enabled at slots when starting subsequent animations. As a replacement you can use `AnimationState.ClearTrack(0);` followed by `var entry = AnimationState.SetAnimation(0, animation, loop); entry.TrackTime = time` to achieve similar behaviour.
* **The `Shadow alpha cutoff` shader parameter is now respecting slot-color alpha** values at all Spine shaders. A fragment's texture color alpha is multiplied with slot-color alpha before the result is tested against the `Shadow alpha cutoff` threshold.
* **Removed redundant `Attachment.GetClone()` and `MeshAttachment.GetLinkedClone()` extension methods**. Use methods `Attachment.Copy` and `MeshAttachment.NewLinkedMesh()` instead.
* **Renamed extension method `Attachment.GetClone(bool cloneMeshesAsLinked)` to `Attachment.GetCopy(bool cloneMeshesAsLinked)`** to follow the naming scheme of the Spine API.
* `SkeletonDataAsset.atlasAssets` is now an array of the base class `AtlasAssetBase` instead of `SpineAtlasAsset`, which provides `IEnumerable<> Materials` instead of `List<> materials`. Replace any access via `atlasAsset.materials[0]` with `atlasAsset.Materials.First()` and add a `using System.Linq;` statement.
* **Changed `MeshAttachment.GetLinkedMesh()` method signatures:** removed optional parameters `bool inheritDeform = true, bool copyOriginalProperties = false`.
* Changed namespace `Spine.Unity.Modules` to `Spine.Unity` and `Spine.Unity.Examples` after restructuring (see section below) in respective classes:
* When receiving namespace related errors, replace using statements of `using Spine.Unity.Modules.AttachmentTools;` with `using Spine.Unity.AttachmentTools;`. You can remove `using Spine.Unity.Modules;` statements when a `using Spine.Unity` statement is already present in the file.
* `AttachmentTools`, `SkeletonPartsRenderer`, `SkeletonRenderSeparator`, `SkeletonRendererCustomMaterials` changed to namespace `Spine.Unity`.
* `SkeletonGhost`, `SkeletonGhostRenderer`, `AtlasRegionAttacher`, `SkeletonGraphicMirror`, `SkeletonRagdoll`, `SkeletonRagdoll2D`, `SkeletonUtilityEyeConstraint`, `SkeletonUtilityGroundConstraint`, `SkeletonUtilityKinematicShadow` changed to namespace `Spine.Unity.Examples`.
* Split `Editor/Utility/SpineEditorUtilities` class into multiple files with partial class qualifier.
* Nested classes `SpineEditorUtilities.AssetUtility` and `SpineEditorUtilities.EditorInstantiation` are now no longer nested. If you receive namespace related errors, replace any occurrance of
* `SpineEditorUtilities.AssetUtility` with `AssetUtility` and
* `SpineEditorUtilities.EditorInstantiation` with `EditorInstantiation`.
* **Timeline Support has been moved to a separate UPM Package** Previously the Spine Timeline integration was located in the `Modules/Timeline` directory and was deactivated by default, making it necessary to activate it via the Spine Preferences. Now the Timeline integration has been moved to an additional UPM package which can be found under `Modules/com.esotericsoftware.spine.timeline`.
* **Installation:** You can download the Unity Package Manager (UPM) package via the [download page](http://esotericsoftware.com/spine-unity-download) or find it in the [spine-runtimes/spine-unity/Modules](https://github.com/EsotericSoftware/spine-runtimes/tree/3.8-beta/spine-unity/Modules) subdirectory on the git repository. You can then either unzip (copy if using git) the package to
a) the `Packages` directory in your project where it will automatically be loaded, or
b) to an arbitrary directory outside the Assets directory and then open Package Manager in Unity, select the `+` icon, choose `Add package from disk..` and point it to the package.json file.
The Project panel should now show an entry `Spine Timeline Extensions` under `Packages`. If the directory is not yet listed, you will need to close and re-open Unity to have it display the directory and its contents.
* `SkeletonMecanim`'s `Layer Mix Mode` enum name `MixMode.SpineStyle` has been renamed to `MixMode.Hard`. This is most likely not set via code and thus unlikely to be a problem. Serialized scenes and prefabs are unaffected.
* `SkeletonRootMotion` and `SkeletonMecanimRootMotion` components now support arbitrary bones in the hierarchy as `Root Motion Bone`. Previously there were problems when selecting a non-root bone as `Root Motion Bone`. `Skeleton.ScaleX` and `.ScaleY` and parent bone scale is now respected as well.
* **Additions**
* **Spine Preferences stored in Assets/Editor/SpineSettings.asset** Now Spine uses the new `SettingsProvider` API, storing settings in a SpineSettings.asset file which can be shared with team members. Your old preferences are automatically migrated to the new system.
* Added support for Unity's SpriteMask to `SkeletonAnimation` and `SkeletonMecanim`. All mask interaction modes are supported. See this [blog post](http://esotericsoftware.com/blog/Unity-SpriteMask-and-RectMask2D-support).
* Added support for Unity's RectMask2D to SkeletonGraphics. See this [blog post](http://esotericsoftware.com/blog/Unity-SpriteMask-and-RectMask2D-support).
* Added `Create 2D Hinge Chain` button at `SkeletonUtilityBone` inspector, previously only `Create 3D Hinge Chain` was available.
* **Now supporting Lightweight Render Pipeline (LWRP) through an additional UPM package.**
* **Installation:** You can download the Unity Package Manager (UPM) package via the [download page](http://esotericsoftware.com/spine-unity-download) or find it in the [spine-runtimes/spine-unity/Modules](https://github.com/EsotericSoftware/spine-runtimes/tree/3.8-beta/spine-unity/Modules) subdirectory on the git repository. You can then either unzip (copy if using git) the package to
* a) the `Packages` directory in your project where it will automatically be loaded, or
* b) to an arbitrary directory outside the Assets directory and then open Package Manager in Unity, select the `+` icon, choose `Add package from disk..` and point it to the package.json file.
> If you are using git and Unity 2019.2 or newer versions and receive an error that dependencies could not be resolved by the package manager (only higher versions of Unity's `Lightweight RP` package are available, e.g. `6.9.0` and up), please copy the prepared package-UNITYVERSION.json file for your Unity version (e.g. `package-2019.2.json`) over the existing package.json file to change the dependency accordingly. Unfortunately Unity's Package Manager does not provide a way to specify a version range for a dependency like "5.7.2 - 6.9.0" yet, so this manual step is necessary for git users.
The Project panel should now show an entry `Spine Lightweight RP Shaders` under `Packages`. If the directory is not yet listed, you will need to close and re-open Unity to have it display the directory and its contents.
* **Usage:** The package provides two shaders specifically built for the lightweight render pipeline:
* `Lightweight Render Pipeline/Spine/Skeleton`, as a lightweight variant of the `Spine/Skeleton` shader,
* `Lightweight Render Pipeline/Spine/Skeleton Lit`, as a lightweight variant of the `Spine/Skeleton Lit` shader and
* `Lightweight Render Pipeline/Spine/Sprite`, as a lightweight variant of the `Spine/Sprite/Vertex Lit` and `Pixel Lit` shaders, which were not functioning in the lightweight render pipeline. The shaders can be assigned to materials as usual and will respect your settings of the assigned `LightweightRenderPipelineAsset` under `Project Settings - Graphics`.
* **Restrictions** As all Spine shaders, the LWRP shaders **do not support `Premultiply alpha` (PMA) atlas textures in Linear color space**. Please export your atlas textures as `straight alpha` textures with disabled `Premultiply alpha` setting when using Linear color space. You can check the current color space via `Project Settings - Player - Other Settings - Color Space.`.
* **Example:** You can find an example scene in the package under `com.esotericsoftware.spine.lwrp-shaders-3.8/Examples/LWRP Shaders.unity` that demonstrates usage of the LWRP shaders.
* Added `Spine/Skeleton Lit ZWrite` shader. This variant of the `Spine/Skeleton Lit` shader writes to the depth buffer with configurable depth alpha threshold. Apart from that it is identical to `Spine/Skeleton Lit`.
* Additional yield instructions to wait for animation track events `End`, `Complete` and `Interrupt`.
* `WaitForSpineAnimationComplete` now proves an additional `bool includeEndEvent` parameter, defaults to `false` (previous behaviour).
* Added a new `WaitForSpineAnimationEnd` yield instruction.
* Added a new generic `WaitForSpineAnimation` yield instruction which can be configured to wait for any combination of animation track events. It is now used as base class for `WaitForSpineAnimationComplete` and `WaitForSpineAnimationEnd`.
* Additional **Fix Draw Order** parameter at SkeletonRenderer, defaults to `disabled` (previous behaviour).
Applies only when 3+ submeshes are used (2+ materials with alternating order, e.g. "A B A").
If true, 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 leave this parameter disabled when everything is drawn correctly to save the additional performance cost.
* **Additional Timeline features.** SpineAnimationStateClip now provides a `Speed Multiplier`, a start time offset parameter `Clip In`, support for blending successive animations by overlapping tracks. An additional `Use Blend Duration` parameter *(defaults to true)* allows for automatic synchronisation of MixDuration with the current overlap blend duration. An additional Spine preferences parameter `Use Blend Duration` has been added which can be disabled to default to the previous behaviour before this update.
* Additional `SpriteMask and RectMask2D` example scene added for demonstration of mask setup and interaction.
* `Real physics hinge chains` for both 2D and 3D physics. The [SkeletonUtilityBone](http://esotericsoftware.com/spine-unity#SkeletonUtilityBone) Inspector provides an interface to create 2D and 3D hinge chains. Previously created chains have only been respecting gravity, but not momentum of the skeleton or parent bones. The new physics rig created when pressing `Create 3D Hinge Chain` and `Create 2D Hinge Chain` creates a more complex setup that also works when flipping the skeleton. Note that the chain root node is no longer parented to bones of the skeleton. This is a requirement in Unity to have momentum applied properly - do not reparent the chain root to bones of your skeleton, or you will loose any momentum applied by the skeleton's movement.
* `Outline rendering functionality for all shaders.` Every shader now provides an additional set of `Outline` parameters to enable custom outline rendering. When outline rendering is enabled via the `Material` inspector, it automatically switches the shader to the respective `Spine/Outline` shader variant. Outlines are generated by sampling neighbour pixels, so be sure to add enough transparent padding when exporting your atlas textures to fit the desired outline width. In order to enable outline rendering at a skeleton, it is recommended to first prepare an additional outline material copy and then switch the material of the target skeleton to this material. This prevents unnecessary additional runtime material copies and drawcalls. Material switching can be prepared via a [SkeletonRendererCustomMaterials](http://esotericsoftware.com/spine-unity#SkeletonRendererCustomMaterials) component and then enabled or disabled at runtime. Alternatively, you can also directly modify the `SkeletonRenderer.CustomMaterialOverride` property.
Outline rendering is fully supported on `SkeletonGraphic` shaders as well.
* Added `SkeletonRenderer.EditorSkipSkinSync` scripting API property to be able to set custom skins in editor scripts. Enable this property when overwriting the Skeleton's skin from an editor script. Without setting this parameter, changes will be overwritten by the next inspector update. Only affects Inspector synchronisation of skin with `initialSkinName`, not startup initialization.
* All `Spine/SkeletonGraphic` shaders now provide a parameter `CanvasGroup Compatible` which can be enabled to support `CanvasGroup` alpha blending. For correct results, you should then disable `Pma Vertex Colors` in the `SkeletonGraphic` Inspector, in section `Advanced` (otherwise Slot alpha will be applied twice).
* **Now supporting Universal Render Pipeline (URP), including the 2D Renderer pipeline, through an additional UPM package.**
* **Installation:** You can download the Unity Package Manager (UPM) package via the [download page](http://esotericsoftware.com/spine-unity-download) or find it in the [spine-runtimes/spine-unity/Modules](https://github.com/EsotericSoftware/spine-runtimes/tree/3.8-beta/spine-unity/Modules) subdirectory on the git repository. You can then either unzip (copy if using git) the package to
* a) the `Packages` directory in your project where it will automatically be loaded, or
* b) to an arbitrary directory outside the Assets directory and then open Package Manager in Unity, select the `+` icon, choose `Add package from disk..` and point it to the package.json file.
The Project panel should now show an entry `Spine Universal RP Shaders` under `Packages`. If the directory is not yet listed, you will need to close and re-open Unity to have it display the directory and its contents.
* **Usage:** The package provides two shaders specifically built for the universal render pipeline:
* `Universal Render Pipeline/Spine/Skeleton`, as a universal variant of the `Spine/Skeleton` shader,
* `Universal Render Pipeline/Spine/Skeleton Lit`, as a universal variant of the `Spine/Skeleton Lit` shader,
* `Universal Render Pipeline/Spine/Sprite`, as a universal variant of the `Spine/Sprite/Vertex Lit` and `Pixel Lit` shaders, which were not functioning in the universal render pipeline,
* `Universal Render Pipeline/2D/Spine/Skeleton Lit`, as a universal 2D Renderer variant of the `Spine/Skeleton Lit` shader, and
* `Universal Render Pipeline/2D/Spine/Sprite`, as a universal 2D Renderer variant of the `Spine/Sprite/Vertex Lit` and `Pixel Lit` shaders.
The shaders can be assigned to materials as usual and will respect your settings of the assigned `UniversalRenderPipelineAsset` under `Project Settings - Graphics`.
* **Restrictions** As all Spine shaders, the URP shaders **do not support `Premultiply alpha` (PMA) atlas textures in Linear color space**. Please export your atlas textures as `straight alpha` textures with disabled `Premultiply alpha` setting when using Linear color space. You can check the current color space via `Project Settings - Player - Other Settings - Color Space.`.
* **Example:** You can find an example scene in the package under `com.esotericsoftware.spine.urp-shaders-3.8/Examples/URP Shaders.unity` that demonstrates usage of the URP shaders.
* Spine Preferences now provide an **`Atlas Texture Settings`** parameter for applying customizable texture import settings at all newly imported Spine atlas textures.
When exporting atlas textures from Spine with `Premultiply alpha` enabled (the default), you can leave it at `PMATexturePreset`. If you have disabled `Premultiply alpha`, set it to the included `StraightAlphaTexturePreset` asset. You can also create your own `TextureImporter` `Preset` asset and assign it here (include `PMA` or `Straight` in the name). In Unity versions before 2018.3 you can use `Texture2D` template assets instead of the newer `Preset` assets. Materials created for imported textures will also have the `Straight Alpha Texture` parameter configured accordingly.
* All `Sprite` shaders (including URP and LWRP extension packages) now provide an additional `Fixed Normal Space` option `World-Space`. PReviously options were limited to `View-Space` and `Model-Space`.
* `SkeletonGraphic` now fully supports [`SkeletonUtility`](http://esotericsoftware.com/spine-unity#SkeletonUtility) for generating a hierarchy of [`SkeletonUtilityBones`](http://esotericsoftware.com/spine-unity#SkeletonUtilityBone) in both modes `Follow` and `Override`. This also enables creating hinge chain physics rigs and using `SkeletonUtilityConstraints` such as `SkeletonUtilityGroundConstraint` and `SkeletonUtilityEyeConstraint` on `SkeletonGraphic`.
* Added `OnMeshAndMaterialsUpdated` callback event to `SkeletonRenderer` and `SkeletonGraphic`. It is issued at the end of `LateUpdate`, before rendering.
* Added `Skeleton-OutlineOnly` single pass shader to LWRP and URP extension modules. It can be assigned to materials as `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly`. This allows for separate outline child *GameObjects* that reference the existing Mesh of their parent, and re-draw the mesh using this outline shader.
* Added example component `RenderExistingMesh` to render a mesh again with different materials, as required by the new `Skeleton-OutlineOnly` shaders.
In URP the outline has to be rendered via a separate GameObject as URP does not allow multiple render passes. To add an outline to your SkeletenRenderer:
1) Add a child GameObject and move it a bit back (e.g. position Z = 0.01).
2) Add a `RenderExistingMesh` component, provided in the `Spine Examples/Scripts/Sample Components` directory.
3) Copy the original material, add *_Outline* to its name and set the shader to `Universal Render Pipeline/Spine/Outline/Skeleton-OutlineOnly`.
4) Assign this *_Outline* material at the `RenderExistingMesh` component under *Replacement Materials*.
* Added `Outline Shaders URP` example scene to URP extension module to demonstrate the above additions.
* Added support for Unity's [`SpriteAtlas`](https://docs.unity3d.com/Manual/class-SpriteAtlas.html) as atlas provider (as an alternative to `.atlas.txt` and `.png` files) alongside a skeleton data file. There is now an additional `Spine SpriteAtlas Import` tool window accessible via `Window - Spine - SpriteAtlas Import`. Additional information can be found in a new section on the [spine-unity documentation page](http://esotericsoftware.com/spine-unity#Advanced---Using-Unity-SpriteAtlas-as-Atlas-Provider).
* Added support for **multiple atlas textures at `SkeletonGraphic`**. You can enable this feature by enabling the parameter `Multiple CanvasRenders` in the `Advanced` section of the `SkeletonGraphic` Inspector. This automatically creates the required number of child `CanvasRenderer` GameObjects for each required draw call (submesh).
* Added support for **Render Separator Slots** at `SkeletonGraphic`. Render separation can be enabled directly in the `Advanced` section of the `SkeletonGraphic` Inspector, it does not require any additional components (like `SkeletonRenderSeparator` or `SkeletonPartsRenderer` for `SkeletonRenderer` components). When enabled, additional separator GameObjects will be created automatically for each separation part, and `CanvasRenderer` GameObjects re-parented to them accordingly. The separator GameObjects can be moved around and re-parented in the hierarchy according to your requirements to achieve the desired draw order within your `Canvas`. A usage example can be found in the updated `Spine Examples/Other Examples/SkeletonRenderSeparator` scene.
* Added `SkeletonGraphicCustomMaterials` component, providing functionality to override materials and textures of a `SkeletonGraphic`, similar to `SkeletonRendererCustomMaterials`. Note: overriding materials or textures per slot is not provided due to structural limitations.
* Added **Root Motion support** for `SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic` via new components `SkeletonRootMotion` and `SkeletonMecanimRootMotion`. The `SkeletonAnimation` and `SkeletonGraphic` component Inspector now provides a line `Root Motion` with `Add Component` and `Remove Component` buttons to add/remove the new `SkeletonRootMotion` component to your GameObject. The `SkeletonMecanim` Inspector detects whether root motion is enabled at the `Animator` component and adds a `SkeletonMecanimRootMotion` component automatically.
* `SkeletonMecanim` now provides an additional `Custom MixMode` parameter under `Mecanim Translator`. It is enabled by default in version 3.8 to maintain current behaviour, using the set `Mix Mode` for each Mecanim layer. When disabled, `SkeletonMecanim` will use the recommended `MixMode` according to the layer blend mode. Additional information can be found in the [Mecanim Translator section](http://esotericsoftware.com/spine-unity#Parameters-for-animation-blending-control) on the spine-unity documentation pages.
* Added **SkeletonGraphic Timeline support**. Added supprot for multi-track Timeline preview in the Editor outside of play mode (multi-track scrubbing). See the [Timeline-Extension-UPM-Package](http://esotericsoftware.com/spine-unity#Timeline-Extension-UPM-Package) section of the spine-unity documentation for more information.
* Added support for double-sided lighting at all `SkeletonLit` shaders (including URP and LWRP packages).
* Added frustum culling update mode parameters `Update When Invisible` (Inspector parameter) and `UpdateMode` (available via code) to all Skeleton components. This provides a simple way to disable certain updates when the `Renderer` is no longer visible (outside all cameras, culled in frustum culling). The new `UpdateMode` property allows disabling updates at a finer granularity level than disabling the whole component. Available modes are: `Nothing`, `OnlyAnimationStatus`, `EverythingExceptMesh` and `FullUpdate`.
* Added a new `Spine/Outline/OutlineOnly-ZWrite` shader to provide correct outline-only rendering. Note: the shader requires two render passes and is therefore not compatible with URP. The `Spine Examples/Other Examples/Outline Shaders` example scene has been updated to demonstrate the new shader.
* Added `OnMeshAndMaterialsUpdated` callback event to `SkeletonRenderSeparator` and `SkeletonPartsRenderer`. It is issued at the end of `LateUpdate`, before rendering.
* Added `Root Motion Scale X/Y` parameters to `SkeletonRootMotionBase` subclasses (`SkeletonRootMotion` and `SkeletonMecanimRootMotion`). Also providing `AdjustRootMotionToDistance()` and other methods to allow for easy delta compensation. Delta compensation can be used to e.g. stretch a jump to a given distance. Root motion can be adjusted at the start of an animation or every frame via `skeletonRootMotion.AdjustRootMotionToDistance(targetPosition - transform.position, trackIndex);`.
* Now providing a `Canvas Group Tint Black` parameter at the `SkeletonGraphic` Inspector in the `Advanced` section. When using the `Spine/SkeletonGraphic Tint Black` shader you can enable this parameter to receive proper blending results when using `Additive` blend mode under a `CanvasGroup`. Be sure to also have the parameter `CanvasGroup Compatible` enabled at the shader. Note that the normal `Spine/SkeletonGraphic` does not support `Additive` blend mode at a `CanvasGroup`, as it requires additional shader channels to work.
* Added `Mix and Match Skins` example scene to demonstrate how the 3.8 Skin API and combining skins can be used for a wardrobe and equipment use case.
* Spine Timeline Extensions: Added `Hold Previous` parameter at `SpineAnimationStateClip`.
* Added more warning messages at incompatible SkeletonRenderer/SkeletonGraphic Component vs Material settings. They appear both as an info box in the Inspector as well as upon initialization in the Console log window. The Inspector box warnings can be disabled via `Edit - Preferences - Spine`.
* Now providing `BeforeApply` update callbacks at all skeleton animation components (`SkeletonAnimation`, `SkeletonMecanim` and `SkeletonGraphic`).
* Added `BoundingBoxFollowerGraphic` component. This class is a counterpart of `BoundingBoxFollower` that can be used with `SkeletonGraphic`.
* Added Inspector context menu functions `SkeletonRenderer - Add all BoundingBoxFollower GameObjects` and `SkeletonGraphic - Add all BoundingBoxFollowerGraphic GameObjects` that automatically generate bounding box follower GameObjects for every `BoundingBoxAttachment` for all skins of a skeleton.
* `GetRemappedClone()` now provides an additional parameter `pivotShiftsMeshUVCoords` for `MeshAttachment` to prevent uv shifts at a non-central Sprite pivot. This parameter defaults to `true` to maintain previous behaviour.
* `SkeletonRenderer` components now provide an additional update mode `Only Event Timelines` at the `Update When Invisible` property. This mode saves additional timeline updates compared to update mode `Everything Except Mesh`.
* Now all URP (Universal Render Pipeline) and LWRP (Lightweight Render Pipeline) shaders support SRP (Scriptable Render Pipeline) batching. See [Unity SRPBatcher documentation pages](https://docs.unity3d.com/Manual/SRPBatcher.html) for additional information.
* Sprite shaders now provide four `Diffuse Ramp` modes as an Inspector Material parameter: `Hard`, `Soft`, `Old Hard` and `Old Soft`. In spine-unity 3.8 it defaults to `Old Hard` to keep the behaviour of existing projects unchanged. Note that `Old Hard` and `Old Soft` ramp versions were using only the right half of the ramp texture, and additionally multiplying the light intensity by 2, both leading to brighter lighting than without a ramp texture active. The new ramp modes `Hard` and `Soft` use the full ramp texture and do not modify light intensity, being consistent with lighting without a ramp texture active.
* Added **native support for slot blend modes** `Additive`, `Multiply` and `Screen` with automatic assignment at newly imported skeleton assets. `BlendModeMaterialAssets` are now obsolete and replaced by the native properties at `SkeletonDataAsset`. The `SkeletonDataAsset` Inspector provides a new `Blend Modes - Upgrade` button to upgrade an obsolete `BlendModeMaterialAsset` to the native blend modes properties. This upgrade will be performed automatically on imported and re-imported assets in Unity 2020.1 and newer to prevent reported `BlendModeMaterialAsset` issues in these Unity versions. spine-unity 4.0 and newer will automatically perform this upgrade regardless of the Unity version.
* `BoneFollower` and `BoneFollowerGraphic` components now provide better support for following bones when the skeleton's Transform is not the parent of the follower's Transform. Previously e.g. rotating a common parent Transform did not lead to the desired result, as well as negatively scaling a skeleton's Transform when it is not a parent of the follower's Transform.
* URP and LWRP `Sprite` and `SkeletonLit` shaders no longer require `Advanced - Add Normals` enabled to properly cast and receive shadows. It is recommended to disable `Add Normals` if normals are otherwise not needed.
* Added an example component `RootMotionDeltaCompensation` located in `Spine Examples/Scripts/Sample Components` which can be used for applying simple delta compensation. You can enable and disable the component to toggle delta compensation of the currently playing animation on and off.
* Root motion delta compensation now allows to only adjust X or Y components instead of both. Adds two parameters to `SkeletonRootMotionBase.AdjustRootMotionToDistance()` which default to adjusting both X and Y as before. The `RootMotionDeltaCompensation` example component exposes these parameters as public attributes.
* Root motion delta compensation now allows to also add translation root motion to e.g. adjust a horizontal jump upwards or downwards over time. This is necessary because a Y root motion of zero cannot be scaled to become non-zero.
* `Attachment.GetRemappedClone(Sprite)` method now provides an additional optional parameter `useOriginalRegionScale`. When set to `true`, the replaced attachment's scale is used instead of the Sprite's `Pixel per Unity` setting, allowing for more consistent scaling. *Note:* When remapping Sprites, be sure to set the Sprite's `Mesh Type` to `Full Rect` and not `Tight`, otherwise the scale will be wrong.
* **Changes of default values**
* `SkeletonMecanim`'s `Layer Mix Mode` now defaults to `MixMode.MixNext` instead of `MixMode.MixAlways`.
* `BlendModeMaterialAsset` and it's instance `Default BlendModeMaterials.asset` now have `Apply Additive Material` set to `true` by default in order to apply all blend modes by default.
* **Deprecated**
* Deprecated `Modules/SlotBlendModes/SlotBlendModes` component. Changed namespace from `Spine.Unity.Modules` to `Spine.Unity.Deprecated`. Moved to `Deprecated/SlotBlendModes`.
* **Restructuring (Non-Breaking)**
Note: The following changes will most likely not affect users of the Spine-Unity runtime as the API remains unchanged and no references are invalidated.
* Removed duplicates of `.cginc` files in `Modules/Shaders/Sprite` that were also present in the `Modules/Shaders/Sprite/CGIncludes` directory.
* Moved shaders from `Modules/Shaders` to `Shaders` directory.
* Moved shaders from `Modules/SkeletonGraphic/Shaders` to `Shaders/SkeletonGraphic`.
* Renamed shader `Shaders/Spine-SkeletonLit.shader` to `Shaders/Spine-Skeleton-Lit.shader`.
* Moved components from `SkeletonGraphic` to `Components` and `Components/Following` except for `SkeletonGraphicMirror` which was moved to `Spine Examples/Scripts/Sample Components`.
* Moved `BoneFollower`, `BoneFollowerGraphic` and `PointFollower` from `Components` directory to `Components/Following`.
* Moved `BoundingBoxFollower` component from `Modules/BoundingBoxFollower` to `Components/Following`.
* Moved `Modules/SkeletonRenderSeparator` directory to `Components/SkeletonRenderSeparator`.
* Moved `Modules/CustomMaterials` directory to `Components/SkeletonRendererCustomMaterials`.
* Moved `Asset Types/BlendModeMaterialsAsset.cs` class, `Shaders/BlendModes/Default BlendModeMaterials.asset` and materials from `Shaders/BlendModes` to `SkeletonDataModifierAssets/BlendModeMaterials` directory.
* Moved `Modules/Ghost` directory to `Spine Examples/Scripts/Sample Components/Ghost`.
* Moved `Modules/SkeletonUtility Modules` directory to `Spine Examples/Scripts/Sample Components/SkeletonUtility Modules`.
* Moved `Modules/AnimationMatchModifier` directory to `Spine Examples/Scripts/MecanimAnimationMatchModifier`.
* Moved `SkeletonRagdoll` and `SkeletonRagdoll2D` components from `Modules/Ragdoll` directory to `Spine Examples/Scripts/Sample Components/SkeletonUtility Modules`.
* Moved `AttachmentTools.cs` to `Utility` directory.
* Split the file `AttachmentTools` into 5 separate files for each contained class. No namespace or other API changes performed.
* Split the file `Mesh Generation/SpineMesh` into 4 separate files for each contained class. No namespace or other API changes performed.
* Moved `SkeletonExtensions.cs` to `Utility` directory.
* Moved `Modules/YieldInstructions` directory to `Utility/YieldInstructions`.
* Moved corresponding editor scripts of the above components to restructured directories as well.
* Renamed inspector editor class `PointFollowerEditor` to `PointFollowerInspector` for consistency reasons.
### XNA/MonoGame
* Updated to latest MonoGame version 3.7.1
* Rewrote example project to be cleaner and better demonstrate basic Spine features.
* Added mix-and-match example to demonstrate the new Skin API.
* Added normalmap support via `SpineEffectNormalmap` and support for loading multiple texture layers following a suffix-pattern. Please see the example code on how to use them.
## Java
* **Breaking changes**
* Renamed `Slot#getAttachmentVertices()` to `Slot#getDeform()`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin#addAttachment()` to `Skin#setAttachment()`.
* Removed `VertexAttachment#applyDeform()` and replaced it with `VertexAttachment#deformAttachment`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* `JsonRollback` tool now converts from 3.8 JSON to 3.7.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* **Additions**
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin#getAttachments()`. Returns all attachments in the skin.
* Added `Skin#getAttachments(int slotIndex)`. Returns all attachements in the skin for the given slot index.
* Added `Skin#addSkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin#copySkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* Added `Attachment#copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
### libGDX
* `SkeletonViewer` can load a skeleton by specifying it as the first argument on the command line.
* Added mix-and-match example to demonstrate the new Skin API.
## Lua
* **Breaking changes**
* Renamed `Slot:getAttachmentVertices()` to `Slot#deform`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin:addAttachment()` to `Skin#setAttachment()`.
* Removed `VertexAttachment:applyDeform()` and replaced it with `VertexAttachment#deformAttachment`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed the `.json` file format to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* **Additions**
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin:getAttachments()`. Returns all attachments in the skin.
* Added `Skin:getAttachments(slotIndex)`. Returns all attachements in the skin for the given slot index.
* Added `Skin:addSkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin:copySkin(Skin skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* Added `Attachment:copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment:newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
### Love2D
* Added support for 0-1 RGBA color component range change in Löve 0.11+. Older Löve versions using the 0-255 range are still supported!
* Added mix-and-match example to demonstrate the new Skin API.
### Corona
* Added mix-and-match example to demonstrate the new Skin API.
## Typescript/Javascript
* **Breaking changes**
* Renamed `MixDirection.in/out` to `MixDirection.mixIn/mixOut` as it was crashing a JS compressor.
* Renamed `Slot#getAttachmentVertices()` to `Slot#getDeform()`.
* Changed the `.json` curve format and added more assumptions for omitted values, reducing the average size of JSON exports.
* Renamed `Skin#addAttachment()` to `Skin#setAttachment()`.
* Removed `VertexAttachment#applyDeform()` and replaced it with `VertexAttachment#deformAttachment`. The attachment set on this field is used to decide if a `DeformTimeline` should be applied to the attachment active on the slot to which the timeline is applied.
* Removed `inheritDeform` field, getter, and setter from `MeshAttachment`.
* Changed `.skel` binary format, added a string table. References to strings in the data resolve to this string table, reducing storage size of binary files considerably.
* Changed the `.json` and `.skel` file formats to accomodate the new feature and file size optimiations. Old projects must be exported with Spine 3.8.20+ to be compatible with the 3.8 Spine runtimes.
* Updated runtime to be compatible with TypeScript 3.6.3.
* **Additions**
* Added support for loading binary data via `AssetManager#loadBinary()`. `AssetManager#get()` will return a `Uint8Array` for such assets.
* Added support for loading binaries via new `SkeletonBinary`. Parses a `Uint8Array`.
* Added `x` and `y` coordinates for setup pose AABB in `SkeletonData`.
* Added support for rotated mesh region UVs.
* Added skin-specific bones and constraints which are only updated if the skeleton's current skin contains them.
* Improved Skin API to make it easier to handle mix-and-match use cases.
* Added `Skin#getAttachments()`. Returns all attachments in the skin.
* Added `Skin#getAttachments(slotIndex: number)`. Returns all attachements in the skin for the given slot index.
* Added `Skin#addSkin(skin: Skin)`. Adds all attachments, bones, and skins from the specified skin to this skin.
* Added `Skin#copySkin(skin: Skin)`. Adds all attachments, bones, and skins from the specified skin to this skin. `VertexAttachment` are shallowly copied and will retain any parent mesh relationship. All other attachment types are deep copied.
* Added `Attachment#copy()` to all attachment type implementations. This lets you deep copy an attachment to modify it independently from the original, i.e. when programmatically changing texture coordinates or mesh vertices.
* Added `MeshAttachment#newLinkedMesh()`, creates a linked mesh linkted to either the original mesh, or the parent of the original mesh.
* Added IK softness.
* Added `AssetManager.setRawDataURI(path, data)`. Allows to embed data URIs for skeletons, atlases and atlas page images directly in the HTML/JS without needing to load it from a separate file.
### WebGL backend
* `Input` can now take a partially defined implementation of `InputListener`.
* Added mix-and-match example to demonstrate the new Skin API.
### Canvas backend
### Three.js backend
* `SkeletonMesh` now takes an optional `SkeletonMeshMaterialParametersCustomizer` function that allows you to modify the `ShaderMaterialParameters` before the material is finalized. Use it to modify things like THREEJS' `Material.depthTest` etc. See #1590.
### Player
* `SpinePlayer#setAnimation()` can now be called directly to set the animation being displayed.
* The player supports loading `.skel` binary skeleton files by setting the `SpinePlayerConfig#skelUrl` field instead of `SpinePlayerConfig#jsonUrl`.
* Added `SpinePlayerConfig#rawDataURIs`. Allows to embed data URIs for skeletons, atlases and atlas page images directly in the HTML/JS without needing to load it from a separate file. See the example for a demonstration.
# 3.7
## AS3
* **Breaking changes**
* The completion event will fire for looped 0 duration animations every frame.
* `MixPose` is now called `MixBlend`
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* **Additions**
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `TrackEntry#setMixBlend(MixBlend.add)` on each track. To specify the blend percentage, set `TrackEntry#alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion.
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `TrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
### Starling
* Added support for vertex effects. See `RaptorExample.as`
* Added 'getTexture()' method to 'StarlingTextureAtlasAttachmentLoader'
* Breaking change: if a skeleton requires two color tinting, you have to enable it via `SkeletonSprite.twoColorTint = true`. In this case the skeleton will use the `TwoColorMeshStyle`, which internally uses a different vertex layout and shader. This means that skeletons with two color tinting enabled will break batching and hence increase the number of draw calls in your app.
* Added `VertexEffect` and implementations `JitterEffect` and `SwirlEffect`. Allows you to modify vertices before they are submitted for drawing. See Starling changes.
* Fix issues with StarlingAtlasAttachmentLoader, see https://github.com/EsotericSoftware/spine-runtimes/issues/939
* Fix issues with region trimming support, see https://github.com/EsotericSoftware/spine-runtimes/commit/262bc26c64d4111002d80e201cb1a3345e6727df
* Added support for overriding `StarlingAtlasAttachmentLoader#getTexture()`, see https://github.com/EsotericSoftware/spine-runtimes/commit/ea7dbecb98edc74e439aa9ef90dcf6eed865f718
* Texture atlas operations are no longer handled in `Starling#newRegionAttachment` and `Starling#newMeshAttachment` but delegated to the atlas.
* Added sample for additive animation blending, see https://github.com/EsotericSoftware/spine-runtimes/blob/6a556de01429878df47bb276a97959a8bdbbe32f/spine-starling/spine-starling-example/src/spine/examples/OwlExample.as
* Added sample on how to use bounding box attachment vertices https://github.com/EsotericSoftware/spine-runtimes/commit/e20428b02699226164fa73ba4b12f7d029ae6f4d
* Fully transparent meshes are not submitted for rendering.
* No hit-tests are performed when a skeleton is invisible.
## C
* **Breaking changes**
* Listeners on `spAnimationState` and `spTrackEntry` will now also be called if a track entry gets disposed as part of disposing an animation state.
* The completion event will fire for looped 0 duration animations every frame.
* The spine-cocos2dx and spine-ue4 runtimes are now based on spine-cpp. See below for changes.
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* `spMeshAttachment` has two new fields `regionTextureWith` and `regionTextureHeight`. These must be set in custom attachment loader. See `AtlasAttachmentLoader`.
* **Additions**
* Added support for local and relative transform constraint calculation, including additional fields in `spTransformConstraintData`.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
* Added `spVertexEffect` and corresponding implementations `spJitterVertexEffect` and `spSwirlVertexEffect`. Create/dispose through the corresponding `spXXXVertexEffect_create()/dispose()` functions. Set on framework/engine specific renderer.
* Functions in `extension.h` are not prefixed with `_sp` instead of just `_` to avoid interference with other libraries.
* Introduced `SP_API` macro. Every spine-c function is prefixed with this macro. By default, it is an empty string. Can be used to markup spine-c functions with e.g. ``__declspec` when compiling to a dll or linking to that dll.
* Added `void *userData` to `spAnimationState`to be consumed in callbacks.
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `spTrackEntry->mixBlend = SP_MIXBLEND_ADD)` on each track. To specify the blend percentage, set `spTrackEntry->alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion.
* Optimized attachment lookup to give a 40x speed-up. See https://github.com/EsotericSoftware/spine-runtimes/commit/cab81276263890b65d07fa2329ace16db1e365ff
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `spTrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
### Cocos2d-Objc
* Added vertex effect support to modify vertices of skeletons on the CPU. See `RaptorExample.m`.
* Explanation how to handle ARC, see https://github.com/EsotericSoftware/spine-runtimes/commit/a4f122b08c5e2a51d6aad6fc5a947f7ec31f2eb8
* The super class `::update()` method of `SkeletonRenderer` is now called, see https://github.com/EsotericSoftware/spine-runtimes/commit/f7bb98185236a6d8f35bfefc70afe4f31e9ec9d2
* Added improved tint-black shader.
### SFML
* `spine-sfml.h` no longer defines `SPINE_SHORT_NAMES` to avoid collisions with other APIs. See #1058.
* Added support for vertex effects. See raptor example.
* Added premultiplied alpha support to `SkeletonDrawable`. Use `SkeletonDrawable::setUsePremultipliedAlpha()`, see https://github.com/EsotericSoftware/spine-runtimes/commit/34086c1f41415309b2ecce86055f6656fcba2950
* Added additive animation blending sample, see https://github.com/EsotericSoftware/spine-runtimes/blob/b7e712d3ca1d6be3ebcfe3254dc2cea9c44dda71/spine-sfml/example/main.cpp#L369
## C++
* ** Additions **
* Added C++ Spine runtime. See the [spine-cpp Runtime Guide](https://esotericsoftware.com/spine-cpp) for more information on spine-cpp.
* Added parsing of non-essential data (fps, images path, audio path) to for `.json`/`.skel` parsers.
### Cocos2d-x
* Added ETC1 alpha support, thanks @halx99! Does not work when two color tint is enabled.
* Added `spAtlasPage_setCustomTextureLoader()` which let's you do texture loading manually. Thanks @jareguo.
* Added `SkeletonRenderer:setSlotsRange()` and `SkeletonRenderer::createWithSkeleton()`. This allows you to split rendering of a skeleton up into multiple parts, and render other nodes in between. See `SkeletonRendererSeparatorExample.cpp` for an example.
* Fully transparent attachments will not be rendered, improving rendering performance.
* Added improved tint-black shader.
* Updated to cocos2d-x 3.16
* The skeleton setup pose and world transform are now calculated on initialization to avoid flickering on start-up.
* Updated to cocos2d-x 3.17.1
* **Breaking change**: Switched from [spine-c](spine-c) to [spine-cpp](spine-cpp) as the underlying Spine runtime. See the [spine-cpp Runtime Guide](https://esotericsoftware.com/spine-cpp) for more information on spine-cpp.
* Added `Cocos2dAttachmentLoader` to be used when constructing an `Atlas`. Used by default by `SkeletonAnimation` and `SkeletonRenderer` when creating instances via the `createXXX` methods.
* All C structs and enums `spXXX` have been replaced with their C++ equivalents `spine::XXX` in all public interfaces.
* All instantiations via `new` of C++ classes from spine-cpp should contain `(__FILE__, __LINE__)`. This allows the tracking of instantations and detection of memory leaks via the `spine::DebugExtension`.
### SFML
* Create a second SFML backend using [spine-cpp](spine-cpp/). See the [spine-cpp Runtime Guide](https://esotericsoftware.com/spine-cpp) for more information on spine-cpp.
* Added support for vertex effects. See raptor example.
* Added premultiplied alpha support to `SkeletonDrawable`. Use `SkeletonDrawable::setUsePremultipliedAlpha()`, see https://github.com/EsotericSoftware/spine-runtimes/commit/34086c1f41415309b2ecce86055f6656fcba2950
* Added additive animation blending sample, see https://github.com/EsotericSoftware/spine-runtimes/blob/b7e712d3ca1d6be3ebcfe3254dc2cea9c44dda71/spine-sfml/example/main.cpp#L369
### UE4
* spine-c is now exposed from the plugin shared library on Windows via __declspec.
* Updated to Unreal Engine 4.18
* Added C++ example, see https://github.com/EsotericSoftware/spine-runtimes/commit/15011e81b7061495dba45e28b4d3f4efb10d7f40
* `SkeletonRendererComponent` generates collision meshes by default.
* Disabled generation of collision meshes by `SkeletonRendererComponent`. Both `ProceduralMeshComponent` and `RuntimeMeshComponent` have a bug that generates a new PhysiX file every frame per component. Users are advised to add a separate collision shape to the root scene component of an actor instead.
* Using UE4 `FMemory` allocator by default. This should fix issues on some consoles.
* **Breaking change** moved away from `RuntimeMeshComponent`, as its maintainance has seized, back to `ProceduralMeshComponent`. Existing projects should just work. However, if you run into issues, you may have to remove the old `SpineSkeletonRendererComponent` and add a new one to your existing actors.
* **Breaking change** due to the removal of `RuntimeMeshComponent` and reversal to `ProceduralMeshComponent`, two color tinting is currently not supported. `ProceduralMeshComponent` does not support enough vertex attributes for us to encode the second color in the vertex stream. You can remove the `RuntimeMeshComponent/` directory from your plugins directory and remove the component from any `build.cs` files that may reference it.
* **Breaking change**: Switched from [spine-c](spine-c) to [spine-cpp](spine-cpp) as the underlying Spine runtime. See the [spine-cpp Runtime Guide](https://esotericsoftware.com/spine-cpp) for more information on spine-cpp.
* All C structs and enums `spXXX` have been replaced with their C++ equivalents `spine::XXX` in all public interfaces.
* All instantiations via `new` of C++ classes from spine-cpp should contain `(__FILE__, __LINE__)`. This allows the tracking of instantations and detection of memory leaks via the `spine::DebugExtension`.
* Updated to Unreal Engine 4.20 (samples require 4.17+), see the `spine-ue4/Plugins/SpinePlugin/Source/SpinePlugin/SpinePlugin.build.cs` file on how to compile in 4.20 with the latest UBT API changes.
* Updated to Unreal Engine 4.21 (samples require 4.21).
* **Breaking change**: `UBoneDriverComponent` and `UBoneFollowerComponent` are now `USceneComponent` instead of `UActorComponent`. They either update only themselves, or also the owning `UActor`, depending on whether the new flag `UseComponentTransform` is set. See https://github.com/EsotericSoftware/spine-runtimes/pull/1175
* Added query methods for slots, bones, skins and animations to `SpineSkeletonComponent` and `UTrackEntry`. These allow you to query these objects by name in both C++ and blueprints.
* Added `Preview Animation` and `Preview Skin` properties to `SpineSkeletonAnimationComponent`. Enter an animation or skin name to live-preview it in the editor. Enter an empty string to reset the animation or skin.
## C# ##
* **Breaking changes**
* The completion event will fire for looped 0 duration animations every frame.
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* **Additions**
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `TrackEntry#MixBlend = MixBlend.add` on each track. To specify the blend percentage, set `TrackEntry#Alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion.
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `TrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
### Unity
* **Runtime and Editor, and Assembly Definition** Files and folders have been reorganized into "Runtime" and "Editor". Each of these have an `.asmdef` file that defines these separately as their own assembly in Unity *(Note: Spine `.asmdef` files are currently deactivated to `.txt` extension, see below)*. For projects not using assembly definition, you may delete the `.asmdef` files. These assembly definitions will be ignored by older versions of Unity that don't support it.
* In this scheme, the entirety of the base spine-csharp runtime is inside the "Runtime" folder, to be compiled in the same assembly as spine-unity so they can continue to share internal members.
* **Spine `.asmdef` files are now deactivated (using `.txt` extension) by default** This prevents problems when updating Spine through unitypackages, overwriting the Timeline reference entry in `spine-unity.asmdef` (added automatically when enabling Unity 2019 Timeline support, see `Timeline Support for Unity 2019`), causing compile errors. In case you want to enable the `.asmdef` files, rename the files:
`Spine/Runtime/spine-unity.txt` to `Spine/Runtime/spine-unity.asmdef` and
`Spine/Editor/spine-unity-editor.txt` to `Spine/Editor/spine-unity-editor.asmdef`.
* **SkeletonAnimator is now SkeletonMecanim** The Spine-Unity Mecanim-driven component `SkeletonAnimator` has been renamed `SkeletonMecanim` to make it more autocomplete-friendly and more obvious at human-glance. The .meta files and guids should remain intact so existing projects and prefabs should not break. However, user code needs to be updated to use `SkeletonMecanim`.
* **SpineAtlasAsset** The existing `AtlasAsset` type has been renamed to `SpineAtlasAsset` to signify that it specifically uses a Spine/libGDX atlas as its source. Serialization should be intact but user code will need to be updated to refer to existing atlases as `SpineAtlasAsset`.
* **AtlasAssetBase** `SpineAtlasAsset` now has an abstract base class called `SpineAtlasAsset`. This is the base class to derive when using alternate atlas sources. Existing SkeletonDataAsset field "atlasAssets" now have the "AtlasAssetBase" type. Serialization should be intact, but user code will need to be updated to refer to the atlas assets accordingly.
* This change is in preparation for alternate atlas options such as Unity's SpriteAtlas.
* **Optional Straight Alpha for shaders** Spine-Unity's included Unity shaders now have a `_STRAIGHT_ALPHA_INPUT` shader_feature, toggled as a checkbox in the Material's inspector. This allows the Material to use a non-premultiplied alpha/straight alpha input texture.
* The following shaders now have the "Straight Alpha Texture" checkbox when used on a material:
* `Spine/Skeleton`
* `Spine/Skeleton Tint Black`
* `Spine/Skeleton Lit`
* `Spine/Skeleton Tint`
* `Spine/Skeleton Fill`
* `Spine/SkeletonGraphic (Premultiply Alpha)` was renamed to `Spine/SkeletonGraphic`
* `Spine/SkeletonGraphic Tint Black (Premultiply Alpha)` was renamed to `Spine/SkeletonGraphic Tint Black`
* `Spine/Skeleton PMA Multiply`
* `Spine/Skeleton PMA Screen`
* Dedicated straight alpha shaders were removed from the runtime.
* `Spine/Straight Alpha/Skeleton Fill`
* `Spine/Straight Alpha/Skeleton Tint`
* **Detection of Incorrect Texture Settings** Especially when atlas textures are exported with setting `Premultiply alpha` enabled, it is important to configure Unity's texture import settings correctly. By default, you will now receive warnings where texture settings are expected to cause incorrect rendering.
* The following rules apply:
* `sRGB (Color Texture)` shall be disabled when `Generate Mip Maps` is enabled, otherwise you will receive white border outlines.
* `Alpha Is Transparency` shall be disabled on `Premultiply alpha` textures, otherwise you will receive light ghosting artifacts in transparent areas.
* These warnings can be disabled in `Edit - Preferences - Spine`.
* **Sprite Mask Support for all Included Shaders** The `Skeleton Animation` and `Skeleton Mecanim` components now provide an additional `Mask Interaction` field in the Inspector, covering identical functionality as Unity's built in `Sprite Renderer` component:
* `Mask Interaction` modes:
* `None` - The sprite will not interact with the masking system. Default behavior.
* `Visible Inside Mask` - The sprite will be visible only in areas where a mask is present.
* `Visible Outside Mask` - The sprite will be visible only in areas where no mask is present.
* `Automatically Generated Materials` When switching `Mask Interaction` modes in the Inspector outside of Play mode, the required additional material assets are generated for the respective `Stencil Compare` parameters - with file suffixes `'_InsideMask'` and `'_OutsideMask'`, placed in the same folder as the original materials. By default all generated materials are kept as references by the `Skeleton Animation` component for switching at runtime.
These materials can be managed and optimized via the `SkeletonAnimation`'s `Advanced` section:
* Using the `Clear` button you can clear the reference to unneeded materials,
* Using the `Delete` button the respective assets are deleted as well as references cleared. Note that other `Skeleton Animation` GameObjects might still reference the materials, so use with caution!
* With the `Set` button you can again assign a link to the respective materials to prepare them for runtime use. If the materials were not present or have been deleted, they are generated again based on the default materials.
* When switching `Mask Interaction` mode at runtime, the previously prepared materials are switched active automatically. When the respective materials have not been prepared, material copies of the default materials are created on the fly. Note that these materials are not shared between similar `Skeleton Animation` GameObjects, so it is recommended to use the generated material assets where possible.
* **Every shader now exposes the `Stencil Compare` parameter** for further customization. This way you have maximum flexibility to use custom mechanisms to switch materials at runtime if you should ever need more than the three materials generated by `Skeleton Animation`'s `Mask Interaction` parameter. Reference `Stencil Compare` values are:
* `CompareFunction.Disabled` for `Mask Interaction - None`
* `CompareFunction.LessEqual` for `Mask Interaction - Visible Inside Mask`
* `CompareFunction.Greater` for `Mask Interaction - Visible Outside Mask`
* **RectMask2D Support for SkeletonGraphic** Both `SkeletonGraphic` shaders '`Spine/SkeletonGraphic`' and '`Spine/SkeletonGraphic Tint Black`' now respect masking areas defined via Unity's `RectMask2D` component.
* **Timeline Support for Unity 2019** using the existing Timeline components. By default, all Spine Timeline components are deactivated in Unity 2019 and **can be activated via the Spine Preferences menu**. This step became necessary because in Unity 2019, Timeline has been moved to a separate Package and is no longer included in the Unity core. Please visit `Edit - Preferences - Spine` and at `Timeline Package Support` hit `Enable` to automatically perform all necessary steps to activate the Timeline components.
This will automatically:
1. download the Unity Timeline package
2. activate the Spine Timeline components by setting the compile definition `SPINE_TIMELINE_PACKAGE_DOWNLOADED` for all platforms
3. modify the `spine-unity.asmdef` file by adding the reference to the Unity Timeline library.
* Added `Create 2D Hinge Chain` functionality at `SkeletonUtilityBone` inspector, previously only `Create 3D Hinge Chain` was available.
### XNA/MonoGame
* Added support for any `Effect` to be used by `SkeletonRenderer`
* Added support for `IVertexEffect` to modify vertices of skeletons on the CPU. `IVertexEffect` instances can be set on the `SkeletonRenderer`. See example project.
* Added `SkeletonDebugRenderer`
* Made `MeshBatcher` of SkeletonRenderer accessible via a getter. Allows user to batch their own geometry together with skeleton meshes for maximum batching instead of using XNA SpriteBatcher.
## Java
* **Breaking changes**
* Skeleton attachments: Moved update of attached skeleton out of libGDX `SkeletonRenderer`, added overloaded method `Skeleton#updateWorldTransform(Bone)`, used for `SkeletonAttachment`. You now MUST call this new method with the bone of the parent skeleton to which the child skeleton is attached. See `SkeletonAttachmentTest` for and example.
* The completion event will fire for looped 0 duration animations every frame.
* `MixPose` is now called `MixBlend`.
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* **Additions**
* Added `EventData#audioPath` field. This field contains the file name of the audio file used for the event.
* Added convenience method to add all attachments from one skin to another, see https://github.com/EsotericSoftware/spine-runtimes/commit/a0b7bb6c445efdfac12b0cdee2057afa3eff3ead
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `TrackEntry#setMixBlend(MixBlend.add)` on each track. To specify the blend percentage, set `TrackEntry#alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion.
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `TrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
### libGDX
* Added `VertexEffect` interface, instances of which can be set on `SkeletonRenderer`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect` and `VertexEffectTest`.
* Added improved tint-black shader.
* Improved performance by avoiding batch flush when not switching between normal and additive rendering with PMA
* Improvements to skeleton viewer.
* `TwoColorPolygonBatch` implements the `Batch` interface, allowing to the be used with other libGDX classes that require a batcher for drawing, potentially improving performance. See https://github.com/EsotericSoftware/spine-runtimes/commit/a46b3d1d0c135d51f9bef9ca17a5f8e5dda69927
* Added `SkeletonDrawable` to render skeletons in scene2d UI https://github.com/EsotericSoftware/spine-runtimes/commit/b93686c185e2c9d5466969a8e07eee573ebe4b97
## Lua
* **Breaking changes**
* The completion event will fire for looped 0 duration animations every frame.
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* **Additions**
* Added `JitterEffect` and `SwirlEffect` and support for vertex effects in Corona and Love
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `TrackEntry:setMixBlend(MixBlend.add)` on each track. To specify the blend percentage, set `TrackEntry.alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion.
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `TrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
### Love2D
* Added support for vertex effects. Set an implementation like "JitterEffect" on `Skeleton.vertexEffect`. See `main.lua` for an example.
### Corona
* Added support for vertex effects. Set an implementation like "JitterEffect" on `SkeletonRenderer.vertexEffect`. See `main.lua` for an example
## Typescript/Javascript
* **Breaking changes**
* The completion event will fire for looped 0 duration animations every frame.
* Skeleton `flipX/flipY` has been replaced with `scaleX/scaleY`. This cleans up applying transforms and is more powerful. Allows scaling a whole skeleton which has bones that disallow scale inheritance
* Mix time is no longer affected by `TrackEntry#timeScale`. See https://github.com/EsotericSoftware/spine-runtimes/issues/1194
* **Additions**
* Added `AssetManager.loadTextureAtlas`. Instead of loading the `.atlas` and corresponding image files manually, you can simply specify the location of the `.atlas` file and AssetManager will load the atlas and all its images automatically. `AssetManager.get("atlasname.atlas")` will then return an instance of `spine.TextureAtlas`.
* Added additive animation blending. When playing back multiple animations on different tracks, where each animation modifies the same skeleton property, the results of tracks with lower indices are discarded, and only the result from the track with the highest index is used. With animation blending, the results of all tracks are mixed together. This allows effects like mixing multiple facial expressions (angry, happy, sad) with percentage mixes. By default the old behaviour is retained (results from lower tracks are discarded). To enable additive blending across animation tracks, call `TrackEntry#setMixBlend(MixBlend.add)` on each track. To specify the blend percentage, set `TrackEntry#alpha`. See http://esotericsoftware.com/forum/morph-target-track-animation-mix-mode-9459 for a discussion. See https://github.com/EsotericSoftware/spine-runtimes/blob/f045d221836fa56191ccda73dd42ae884d4731b8/spine-ts/webgl/tests/test-additive-animation-blending.html for an example.
* Added work-around for iOS WebKit JIT bug, see https://github.com/EsotericSoftware/spine-runtimes/commit/c28bbebf804980f55cdd773fed9ff145e0e7e76c
* Support for stretchy IK
* Support for audio events, see `audioPath`, `volume` and `balance` fields on event (data).
* `TrackEntry` has an additional field called `holdPrevious`. It can be used to counter act a limitation of `AnimationState` resulting in "dipping" of parts of the animation. For a full discussion of the problem and the solution we've implemented, see this [forum thread](http://esotericsoftware.com/forum/Probably-Easy-Animation-mixing-with-multiple-tracks-10682?p=48130&hilit=holdprevious#p48130).
* Added `AssetManager#setRawDataURI(path, data)`. Allows to set raw data URIs for a specific path, which in turn enables embedding assets into JavaScript/HTML.
### WebGL backend
* Added `VertexEffect` interface, instances of which can be set on `SkeletonRenderer`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect`, and the example which allows to set effects.
* Added `slotRangeStart` and `slotRangeEnd` parameters to `SkeletonRenderer#draw` and `SceneRenderer#drawSkeleton`. This allows you to render only a range of slots in the draw order. See `spine-ts/webgl/tests/test-slot-range.html` for an example.
* Added improved tint-black shader.
* Added `SceneRenderer#drawTextureUV()`, allowing to draw a texture with manually specified texture coordinates.
* Exposed all renderers in `SceneRenderer`.
### Canvas backend
* Added support for shearing and non-uniform scaling inherited from parent bones.
* Added support for alpha tinting.
### Three.js backend
* Added `VertexEffect` interface, instances of which can be set on `SkeletonMesh`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect`.
* Added support for multi-page atlases
### Widget backend
* Added fields `atlasContent`, `atlasPagesContent`, and `jsonContent` to `WidgetConfiguration` allowing you to directly pass the contents of the `.atlas`, atlas page `.png` files, and the `.json` file without having to do a request. See `README.md` and the example for details.
* `SpineWidget.setAnimation()` now takes an additional optional parameter for callbacks when animations are completed/interrupted/etc.
# 3.6
## AS3
* **Breaking changes**
* Removed `Bone.worldToLocalRotationX` and `Bone.worldToLocalRotationY`. Replaced by `Bone.worldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* Made `Bone` fields `_a`, `_b`, `_c`, `_d`, `_worldX` and `_worldY` public, removed underscore prefix.
* Removed `VertexAttachment.computeWorldVertices` overload, changed `VertexAttachment.computeWorldVertices2` to `VertexAttachment.computeWorldVertices`, added `stride` parameter.
* Removed `RegionAttachment.vertices` field. The vertices array is provided to `RegionAttachment.computeWorldVertices` by the API user now.
* Removed `RegionAttachment.updateWorldVertices`, added `RegionAttachment.computeWorldVertices`. The new method now computes the x/y positions of the 4 vertices of the corner and places them in the provided `worldVertices` array, starting at `offset`, then moving by `stride` array elements when advancing to the next vertex. This allows to directly compose the vertex buffer and avoids a copy. The computation of the full vertices, including vertex colors and texture coordinates, is now done by the backend's respective renderer.
* Replaced `r`, `g`, `b`, `a` fields with instances of new `Color` class in `RegionAttachment`, `MeshAttachment`, `Skeleton`, `SkeletonData`, `Slot` and `SlotData`.
* The completion event will fire for looped 0 duration animations every frame.
* **Additions**
* Added `Skeleton.getBounds` from reference implementation.
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `Bone.localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `TwoColorTimeline` and additional fields on `Slot` and `SlotData`.
* Added `PointAttachment`, additional method `newPointAttachment` in `AttachmentLoader` interface.
* Added `ClippingAttachment`, additional method `newClippingAttachment` in `AttachmentLoader` interface.
* `AnimationState#apply` returns boolean indicating if any timeline was applied or not.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
* Added `VertexEffect` and implementations `JitterEffect` and `SwirlEffect`. Allows you to modify vertices before they are submitted for drawing. See Starling changes.
### Starling
* Fixed renderer to work with 3.6 changes.
* Added support for two color tinting.
* Added support for clipping.
* Added support for rotated regions in texture atlas loaded via StarlingAtlasAttachmentLoader.
* Added support for vertex effects. See `RaptorExample.as`
* Added 'getTexture()' method to 'StarlingTextureAtlasAttachmentLoader'
* Breaking change: if a skeleton requires two color tinting, you have to enable it via `SkeletonSprite.twoColorTint = true`. In this case the skeleton will use the `TwoColorMeshStyle`, which internally uses a different vertex layout and shader. This means that skeletons with two color tinting enabled will break batching and hence increase the number of draw calls in your app.
## C
* **Breaking changes**
* `spVertexAttachment_computeWorldVertices` and `spRegionAttachment_computeWorldVerticeS` now take new parameters to make it possible to directly output the calculated vertex positions to a vertex buffer. Removes the need for additional copies in the backends' respective renderers.
* Removed `spBoundingBoxAttachment_computeWorldVertices`, superseded by `spVertexAttachment_computeWorldVertices`.
* Removed `spPathAttachment_computeWorldVertices` and `spPathAttachment_computeWorldVertices1`, superseded by `spVertexAttachment_computeWorldVertices`.
* Removed `sp_MeshAttachment_computeWorldVertices`, superseded by `spVertexAttachment_computeWorldVertices`.
* Removed `spBone_worldToLocalRotationX` and `spBone_worldToLocalRotationY`. Replaced by `spBone_worldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* Replaced `r`, `g`, `b`, `a` fields with instances of new `spColor` struct in `spRegionAttachment`, `spMeshAttachment`, `spSkeleton`, `spSkeletonData`, `spSlot` and `spSlotData`.
* Removed `spVertexIndex`from public API.
* Listeners on `spAnimationState` or `spTrackEntry` will now be also called in case a track entry is disposed as part of dispoing the `spAnimationState`.
* The completion event will fire for looped 0 duration animations every frame.
* **Additions**
* Added support for local and relative transform constraint calculation, including additional fields in `spTransformConstraintData`.
* Added `spPointAttachment`, additional method `spAtlasAttachmentLoadeR_newPointAttachment`.
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `spBone_localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `spTwoColorTimeline` and additional fields on `spSlot` and `spSlotData`.
* Added `userData` field to `spTrackEntry`, so users can expose data in `spAnimationState` callbacks.
* Modified kvec.h used by SkeletonBinary.c to use Spine's MALLOC/FREE macros. That way there's only one place to inject custom allocators ([extension.h](https://github.com/EsotericSoftware/spine-runtimes/blob/master/spine-c/spine-c/include/spine/extension.h)) [commit](https://github.com/EsotericSoftware/spine-runtimes/commit/c2cfbc6cb8709daa082726222d558188d75a004f)
* Added macros to define typed dynamic arrays, see `Array.h/.c`
* Added `spClippingAttachment` and respective enum.
* Added `spSkeletonClipper` and `spTriangulator`, used to implement software clipping of attachments.
* `AnimationState#apply` returns boolean indicating if any timeline was applied or not.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
* Added `spVertexEffect` and corresponding implementations `spJitterVertexEffect` and `spSwirlVertexEffect`. Create/dispose through the corresponding `spXXXVertexEffect_create()/dispose()` functions. Set on framework/engine specific renderer. See changes for spine-c based frameworks/engines below.
* Functions in `extension.h` are not prefixed with `_sp` instead of just `_` to avoid interference with other libraries.
* Introduced `SP_API` macro. Every spine-c function is prefixed with this macro. By default, it is an empty string. Can be used to markup spine-c functions with e.g. ``__declspec` when compiling to a dll or linking to that dll.
### Cocos2d-X
* Fixed renderer to work with 3.6 changes
* Optimized rendering by removing all per-frame allocation in `SkeletonRenderer`, resulting in 15% performance increase for large numbers of skeletons being rendered per frame.
* Added support for two color tinting. Tinting is enabled/disabled per `SkeletonRenderer`/`SkeletonAnimation` instance. Use `SkeletonRenderer::setTwoColorTint()`. Note that two color tinting requires the use of a non-standard shader and vertex format. This means that skeletons rendered with two color tinting will break batching. However, skeletons with two color tinting enabled and rendered after each other will be batched.
* Updated example to use Cocos2d-x 3.14.1.
* Added mesh debug rendering. Enable/Disable via `SkeletonRenderer::setDebugMeshesEnabled()`.
* Added support for clipping.
* SkeletonRenderer now combines the displayed color of the Node (cascaded from all parents) with the skeleton color for tinting.
* Added support for vertex effects. See `RaptorExample.cpp`.
* Added ETC1 alpha support, thanks @halx99! Does not work when two color tint is enabled.
* Added `spAtlasPage_setCustomTextureLoader()` which let's you do texture loading manually. Thanks @jareguo.
* Added `SkeletonRenderer:setSlotsRange()` and `SkeletonRenderer::createWithSkeleton()`. This allows you to split rendering of a skeleton up into multiple parts, and render other nodes in between. See `SkeletonRendererSeparatorExample.cpp` for an example.
### Cocos2d-Objc
* Fixed renderer to work with 3.6 changes
* Added support for two color tinting. Tinting is enabled/disabled per `SkeletonRenderer/SkeletonAnimation.twoColorTint = true`. Note that two color tinted skeletons do not batch with other nodes.
* Added support for clipping.
### SFML
* Fixed renderer to work with 3.6 changes. Sadly, two color tinting does not work, as the vertex format in SFML is fixed.
* Added support for clipping.
* Added support for vertex effects. See raptor example.
* Added premultiplied alpha support to `SkeletonDrawable`.
### Unreal Engine 4
* Fixed renderer to work with 3.6 changes
* Added new UPROPERTY to SpineSkeletonRendererComponent called `Color`. This allows to set the tint color of the skeleton in the editor, C++ and Blueprints. Under the hood, the `spSkeleton->color` will be set on every tick of the renderer component.
* Added support for clipping.
* Switched from built-in ProceduralMeshComponent to RuntimeMeshComponent by Koderz (https://github.com/Koderz/UE4RuntimeMeshComponent, MIT). Needed for more flexibility regarding vertex format, should not have an impact on existing code/assets. You need to copy the RuntimeMeshComponentPlugin from our repository in `spine-ue4\Plugins\` to your project as well!
* Added support for two color tinting. All base materials, e.g. SpineUnlitNormalMaterial, now do proper two color tinting. No material parameters have changed.
* Updated to Unreal Engine 4.16.1. Note that 4.16 has a regression which will make it impossible to compile plain .c files!
* spine-c is now exposed from the plugin shared library on Windows via __declspec.
## C#
* **Breaking changes**
* `MeshAttachment.parentMesh` is now a private field to enforce using the `.ParentMesh` setter property in external code. The `MeshAttachment.ParentMesh` property is an appropriate replacement wherever `.parentMesh` was used.
* `Skeleton.GetBounds` takes a scratch array as input so it doesn't have to allocate a new array on each invocation itself. Reduces GC activity.
* Removed `Bone.WorldToLocalRotationX` and `Bone.WorldToLocalRotationY`. Replaced by `Bone.WorldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* Added `stride` parameter to `VertexAttachment.ComputeWorldVertices`.
* Removed `RegionAttachment.Vertices` field. The vertices array is provided to `RegionAttachment.ComputeWorldVertices` by the API user now.
* Removed `RegionAttachment.UpdateWorldVertices`, added `RegionAttachment.ComputeWorldVertices`. The new method now computes the x/y positions of the 4 vertices of the corner and places them in the provided `worldVertices` array, starting at `offset`, then moving by `stride` array elements when advancing to the next vertex. This allows to directly compose the vertex buffer and avoids a copy. The computation of the full vertices, including vertex colors and texture coordinates, is now done by the backend's respective renderer.
* The completion event will fire for looped 0 duration animations every frame.
* **Additions**
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `Bone.localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `TwoColorTimeline` and additional fields on `Slot` and `SlotData`.
* Added `PointAttachment`, additional method `NewPointAttachment` in `AttachmentLoader` interface.
* Added `ClippingAttachment`, additional method `NewClippingAttachment` in `AttachmentLoader` interface.
* Added `SkeletonClipper` and `Triangulator`, used to implement software clipping of attachments.
* `AnimationState.Apply` returns a bool indicating if any timeline was applied or not.
* `Animation.Apply` and `Timeline.Apply`` now take enums `MixPose` and `MixDirection` instead of bools.
### Unity
* Refactored renderer to work with new 3.6 features.
* **Two color tinting** is currently supported via extra UV2 and UV3 mesh vertex streams. To use Two color tinting, you need to:
* switch on "Tint Black" under "Advanced...",
* use the new `Spine/Skeleton Tint Black` shader, or your own shader that treats the UV2 and UV3 streams similarly.
* Additionally, for SkeletonGraphic, you can use `Spine/SkeletonGraphic Tint Black` (or the bundled SkeletonGraphicTintBlack material) or your own shader that uses UV2 and UV3 streams similarly. **Additional Shader Channels** TexCoord1 and TexCoord2 will need to be enabled from the Canvas component's inspector. These correspond to UV2 and UV3.
* **Clipping** is now supported. Caution: The SkeletonAnimation switches to slightly slower mesh generation code when clipping so limit your use of `ClippingAttachment`s when using on large numbers of skeletons.
* **SkeletonRenderer.initialFlip** Spine components such as SkeletonRenderer, SkeletonAnimation, SkeletonAnimator now has `initialFlipX` and `initialFlipY` fields which are also visible in the inspector under "Advanced...". It will allow you to set and preview a starting flip value for your skeleton component. This is applied immediately when the internal skeleton object is instantiated.
* **[SpineAttribute] Improvements**
* **Icons have been added to SpineAttributeDrawers**. This should make your default inspectors easier to understand at a glance.
* **Added Constraint Attributes** You can now use `[SpineIkConstraint]` `[SpineTransformConstraint]` `[SpinePathConstraint]`
* **SpineAttribute dataField** parameter can also now detect sibling fields within arrays and serializable structs/classes.
* **[SpineAttribute(includeNone:false)]** SpineAttributes now have an `includeNone` optional parameter to specify if you want to include or exclude a none ("") value option in the dropdown menu. Default is `includeNone:true`.
* **[SpineAttachment(skinField:"mySkin")]** The SpineAttachment attribute now has a skinField optional parameter to limit the dropdown items to attachments in a specific skin instead of the just default skin or all the skins in SkeletonData.
* **SkeletonDebugWindow**. Debugging tools have been moved from the SkeletonAnimation and SkeletonUtility component inspectors into its own utility window. You can access "Skeleton Debug" under the `Advanced...` foldout in the SkeletonAnimation inspector, or in SkeletonAnimation's right-click/context menu.
* **Skeleton Baking Window** The old Skeleton Baking feature is also now accessible through the SkeletonDataAsset's right-click/context menu.
* **AttachmentTools source material**. `AttachmentTools` methods can now accept a `sourceMaterial` argument to copy material properties from.
* **AttachmentTools Skin Extensions**. Using AttachmentTools, you can now add entries by slot name by also providing a skeleton argument. Also `Append(Skin)`, `RemoveAttachment` and `Clear` have been added.
* **BoneFollower and SkeletonUtilityBone Add RigidBody Button**. The BoneFollower and SkeletonUtilityBone component inspectors will now offer to add a `Rigidbody` or `Rigidbody2D` if it detects a collider of the appropriate type. Having a rigidbody on a moving transform with a collider fits better with the Unity physics systems and prevents excess calculations. It will not detect colliders on child objects so you have to add Rigidbody components manually accordingly.
* **SkeletonRenderer.OnPostProcessVertices** is a new callback that gives you a reference to the MeshGenerator after it has generated a mesh from the current skeleton pose. You can access `meshGenerator.VertexBuffer` or `meshGenerator.ColorBuffer` to modify these before they get pushed into the UnityEngine.Mesh for rendering. This can be useful for non-shader vertex effects.
* **Examples**
* **Examples now use properties**. The code in the example scripts have been switched over to using properties instead of fields to encourage their use for consistency. This is in anticipation of both users who want to move the Spine folders to the Unity Plugins folder (compiled as a different assembly), and of Unity 2017's ability to manually define different assemblies for shorter compilation times.
* **Mix And Match**. The mix-and-match example scene, code and data have been updated to reflect the current recommended setup for animation-compatible custom equip systems The underlying API has changed since 3.5 and the new API calls in MixAndMatch.cs is recommended. Documentation is in progress.
* **Sample Components**. `AtasRegionAttacher` and `SpriteAttacher` are now part of `Sample Components`, to reflect that they are meant to be used as sample code rather than production. A few other sample components have also been added. New imports of the unitypackage Examples folder will see a "Legacy" folder comprised of old sample components that no longer contain the most up-to-date and recommended workflows, but are kept in case old setups used them for production.
* **Spine folder**. In the unitypackage, the "spine-csharp" and "spine-unity" folders are now inside a "Spine" folder. This change will only affect fresh imports. Importing the unitypackage to update Spine-Unity in your existing project will update the appropriate files however you chose to arrange them, as long as the meta files are intact.
* **Breaking changes**
* The Sprite shaders module was updated to the latest version from the [source](https://github.com/traggett/UnitySpriteShaders/commits/master). Some changes were made to the underlying keyword structure. You may need to review the settings of your lit materials. Particularly, your Fixed Normals settings.
* The `Spine/Skeleton Lit` shader was switched over to non-fixed-function code. It now no longer requires mesh normals and has fixed normals at the shader level.
* The old MeshGenerator classes, interfaces and code in `Spine.Unity.MeshGeneration` are now deprecated. All mesh-generating components now share the class `Spine.Unity.MeshGenerator` defined in `SpineMesh.cs`. MeshGenerator is a serializable class.
* The `SkeletonRenderer.renderMeshes` optimization is currently non-functional.
* Old triangle-winding code has been removed from `SkeletonRenderer`. Please use shaders that have backface culling off.
* Render settings in `SkeletonGraphic` can now be accessed under `SkeletonGraphic.MeshGenerator.settings`. This is visible in the SkeletonGraphic inspector as `Advanced...`
* We will continue to bundle the unitypackage with the empty .cs files of deprecated classes until Spine 3.7 to ensure the upgrade process does not break.
* The [SpineAttachment(slotField:)] optional parameter found property value now acts as a Find(slotName) argument rather than Contains(slotName).
* `SkeletonAnimator` now uses a `SkeletonAnimator.MecanimTranslator` class to translate an Animator's Mecanim State Machine into skeleton poses. This makes code reuse possible for a Mecanim version of SkeletonGraphic.
* `SkeletonAnimator` `autoreset` and the `mixModes` array are now a part of SkeletonAnimator's MecanimTranslator `.Translator`. `autoReset` is set to true by default. Old prefabs and scene objects with Skeleton Animator may no longer have correct values set.
* Warnings and conditionals checking for specific Unity 5.2-and-below incompatibility have been removed.
## XNA/MonoGame
* Added support for clipping
* Removed `RegionBatcher` and `SkeletonRegionRenderer`, renamed `SkeletonMeshRenderer` to `SkeletonRenderer`
* Added support for two color tint. For it to work, you need to add the `SpineEffect.fx` file to your content project, then load it via `var effect = Content.Load<Effect>("SpineEffect");`, and set it on the `SkeletonRenderer`. See the example project for code.
* Added support for any `Effect` to be used by `SkeletonRenderer`
* Added support for `IVertexEffect` to modify vertices of skeletons on the CPU. `IVertexEffect` instances can be set on the `SkeletonRenderer`. See example project.
* Added `SkeletonDebugRenderer`
* Made `MeshBatcher` of SkeletonRenderer accessible via a getter. Allows user to batch their own geometry together with skeleton meshes for maximum batching instead of using XNA SpriteBatcher.
## Java
* **Breaking changes**
* `Skeleton.getBounds` takes a scratch array as input so it doesn't have to allocate a new array on each invocation itself. Reduces GC activity.
* Removed `Bone.worldToLocalRotationX` and `Bone.worldToLocalRotationY`. Replaced by `Bone.worldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* Added `stride` parameter to `VertexAttachment.computeWorldVertices`.
* Removed `RegionAttachment.vertices` field. The vertices array is provided to `RegionAttachment.computeWorldVertices` by the API user now.
* Removed `RegionAttachment.updateWorldVertices`, added `RegionAttachment.computeWorldVertices`. The new method now computes the x/y positions of the 4 vertices of the corner and places them in the provided `worldVertices` array, starting at `offset`, then moving by `stride` array elements when advancing to the next vertex. This allows to directly compose the vertex buffer and avoids a copy. The computation of the full vertices, including vertex colors and texture coordinates, is now done by the backend's respective renderer.
* Skeleton attachments: Moved update of attached skeleton out of libGDX `SkeletonRenderer`, added overloaded method `Skeleton#updateWorldTransform(Bone), used for `SkeletonAttachment`. You now MUST call this new method
with the bone of the parent skeleton to which the child skeleton is attached. See `SkeletonAttachmentTest` for and example.
* The completion event will fire for looped 0 duration animations every frame.
* **Additions**
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `Bone.localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `TwoColorTimeline` and additional fields on `Slot` and `SlotData`.
* Added `PointAttachment`, additional method `newPointAttachment` in `AttachmentLoader` interface.
* Added `ClippingAttachment`, additional method `newClippingAttachment` in `AttachmentLoader` interface.
* Added `SkeletonClipper` and `Triangulator`, used to implement software clipping of attachments.
* `AnimationState#apply` returns boolean indicating if any timeline was applied or not.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
### libGDX
* Fixed renderer to work with 3.6 changes
* Added support for two color tinting. Use the new `TwoColorPolygonBatch` together with `SkeletonRenderer`
* Added support for clipping. See `SkeletonClipper`. Used automatically by `SkeletonRenderer`. Does not work when using a `SpriteBatch` with `SkeletonRenderer`. Use `PolygonSpriteBatch` or `TwoColorPolygonBatch` instead.
* Added `VertexEffect` interface, instances of which can be set on `SkeletonRenderer`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect` and `VertexEffectTest`.
## Lua
* **Breaking changes**
* Removed `Bone:worldToLocalRotationX` and `Bone:worldToLocalRotationY`. Replaced by `Bone:worldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* `VertexAttachment:computeWorldVertices` now takes offsets and stride to allow compositing vertices directly in a vertex buffer to be send to the GPU. The compositing is now performed in the backends' respective renderers. This also affects the subclasses `MeshAttachment`, `BoundingBoxAttachment` and `PathAttachment`.
* Removed `RegionAttachment:updateWorldVertices`, added `RegionAttachment:computeWorldVertices`, which takes offsets and stride to allow compositing vertices directly in a vertex buffer to be send to the GPU. The compositing is now performed in the backends' respective renderers.
* Removed `MeshAttachment.worldVertices` field. Computation is now performed in each backends' respective renderer. The `uv` coordinates are now stored in `MeshAttachment.uvs`.
* Removed `RegionAttachment.vertices` field. Computation is now performed in each backends respective renderer. The `uv` coordinates for each vertex are now stored in the `RegionAttachment.uvs` field.
* The completion event will fire for looped 0 duration animations every frame.
* **Additions**
* Added `Bone:localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `TwoColorTimeline` and additional fields on `Slot` and `SlotData`.
* Added `PointAttachment`, additional method `newPointAttachment` in `AttachmentLoader` interface.
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `ClippingAttachment`, additional method `newClippingAttachment` in `AttachmentLoader` interface.
* Added `SkeletonClipper` and `Triangulator`, used to implement software clipping of attachments.
* `AnimationState#apply` returns boolean indicating if any timeline was applied or not.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
* Added `JitterEffect` and `SwirlEffect` and support for vertex effects in Corona and Love
### Love2D
* Fixed renderer to work with 3.6 changes
* Added support for two color tinting. Enable it via `SkeletonRenderer.new(true)`.
* Added clipping support.
* Added support for vertex effects. Set an implementation like "JitterEffect" on `Skeleton.vertexEffect`. See `main.lua` for an example.
### Corona
* Fixed renderer to work with 3.6 changes. Sadly, two color tinting is not supported, as Corona doesn't let us change the vertex format needed and its doesn't allow to modify shaders in the way needed for two color tinting
* Added clipping support.
* Added support for vertex effects. Set an implementation like "JitterEffect" on `SkeletonRenderer.vertexEffect`. See `main.lua` for an example
## Typescript/Javascript
* **Breaking changes**
* `Skeleton.getBounds` takes a scratch array as input so it doesn't have to allocate a new array on each invocation itself. Reduces GC activity.
* Removed `Bone.worldToLocalRotationX` and `Bone.worldToLocalRotationY`. Replaced by `Bone.worldToLocalRotation` (rotation given relative to x-axis, counter-clockwise, in degrees).
* Removed `VertexAttachment.computeWorldVertices` overload, changed `VertexAttachment.computeWorldVerticesWith` to `VertexAttachment.computeWorldVertices`, added `stride` parameter.
* Removed `RegionAttachment.vertices` field. The vertices array is provided to `RegionAttachment.computeWorldVertices` by the API user now.
* Removed `RegionAttachment.updateWorldVertices`, added `RegionAttachment.computeWorldVertices`. The new method now computes the x/y positions of the 4 vertices of the corner and places them in the provided `worldVertices` array, starting at `offset`, then moving by `stride` array elements when advancing to the next vertex. This allows to directly compose the vertex buffer and avoids a copy. The computation of the full vertices, including vertex colors and texture coordinates, is now done by the backend's respective renderer.
* The completion event will fire for looped 0 duration animations every frame.
* Removed the Spine Widget in favor of [Spine Web Player](https://esotericsoftware.com/spine-player).
* **Additions**
* Added support for local and relative transform constraint calculation, including additional fields in `TransformConstraintData`
* Added `Bone.localToWorldRotation`(rotation given relative to x-axis, counter-clockwise, in degrees).
* Added two color tinting support, including `TwoColorTimeline` and additional fields on `Slot` and `SlotData`.
* Added `PointAttachment`, additional method `newPointAttachment` in `AttachmentLoader` interface.
* Added `ClippingAttachment`, additional method `newClippingAttachment` in `AttachmentLoader` interface.
* Added `SkeletonClipper` and `Triangulator`, used to implement software clipping of attachments.
* `AnimationState#apply` returns boolean indicating if any timeline was applied or not.
* `Animation#apply` and `Timeline#apply`` now take enums `MixPose` and `MixDirection` instead of booleans
* Added `AssetManager.loadTextureAtlas`. Instead of loading the `.atlas` and corresponding image files manually, you can simply specify the location of the `.atlas` file and AssetManager will load the atlas and all its images automatically. `AssetManager.get("atlasname.atlas")` will then return an instance of `spine.TextureAtlas`.
* Added the [Spine Web Player](https://esotericsoftware.com/spine-player)
### WebGL backend
* Fixed WebGL context loss
* Added `Restorable` interface, implemented by any WebGL resource that needs restoration after a context loss. All WebGL resource classes (`Shader`, `Mesh`, `GLTexture`) implement this interface.
* Added `ManagedWebGLRenderingContext`. Handles setup of a `WebGLRenderingContext` given a canvas element and restoration of WebGL resources (`Shader`, `Mesh`, `GLTexture`) on WebGL context loss. WebGL resources register themselves with the `ManagedWebGLRenderingContext`. If the context is informed of a context loss and restoration, the registered WebGL resources' `restore()` method is called. The `restore()` method implementation on each resource type will recreate the GPU side objects.
* All classes that previously took a `WebGLRenderingContext` in the constructor now also allow a `ManagedWebGLRenderingContext`. This ensures existing applications do not break.
* To use automatic context restauration:
1. Create or fetch a canvas element from the DOM
2. Instantiate a `ManagedWebGLRenderingContext`, passing the canvas to the constructor. This will setup a `WebGLRenderingContext` internally and manage context loss/restoration.
3. Pass the `ManagedWebGLRenderingContext` to the constructors of classes that you previously passed a `WebGLRenderingContext` to (`AssetManager`, `GLTexture`, `Mesh`, `Shader`, `PolygonBatcher`, `SceneRenderer`, `ShapeRenderer`, `SkeletonRenderer`, `SkeletonDebugRenderer`).
* Fixed renderer to work with 3.6 changes.
* Added support for two color tinting.
* Improved performance by using `DYNAMIC_DRAW` for vertex buffer objects and fixing bug that copied to much data to the GPU each frame in `PolygonBatcher`/`Mesh`.
* Added two color tinting support, enabled by default. You can disable it via the constructors of `SceneRenderer`, `SkeletonRenderer`and `PolygonBatcher`. Note that you will need to use a shader created via `Shader.newTwoColoredTexturedShader` shader with `SkeletonRenderer` and `PolygonBatcher` if two color tinting is enabled.
* Added clipping support
* Added `VertexEffect` interface, instances of which can be set on `SkeletonRenderer`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect`, and the example which allows to set effects.
* Added `slotRangeStart` and `slotRangeEnd` parameters to `SkeletonRenderer#draw` and `SceneRenderer#drawSkeleton`. This allows you to render only a range of slots in the draw order. See `spine-ts/webgl/tests/test-slot-range.html` for an example.
### Canvas backend
* Fixed renderer to work for 3.6 changes. Sadly, we can't support two color tinting via the Canvas API.
* Added support for shearing and non-uniform scaling inherited from parent bones.
* Added support for alpha tinting.
### Three.js backend
* Fixed renderer to work with 3.6 changes. Two color tinting is not supported.
* Added clipping support
* Added `VertexEffect` interface, instances of which can be set on `SkeletonMesh`. Allows to modify vertices before submitting them to GPU. See `SwirlEffect`, `JitterEffect`.
* Added support for multi-page atlases
### Widget backend
* Fixed WebGL context loss (see WebGL backend changes). Enabled automatically.
* Fixed renderer to work for 3.6 changes. Supports two color tinting & clipping (see WebGL backend changes for details).
* Added fields `atlasContent`, `atlasPagesContent`, and `jsonContent` to `WidgetConfiguration` allowing you to directly pass the contents of the `.atlas`, atlas page `.png` files, and the `.json` file without having to do a request. See `README.md` and the example for details.
* `SpineWidget.setAnimation()` now takes an additional optional parameter for callbacks when animations are completed/interrupted/etc.

View File

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

9
Assets/Spine/Editor.meta Normal file
View File

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

View File

@ -0,0 +1,12 @@
{
"name": "spine-unity-editor",
"references": [
"spine-unity"
],
"optionalUnityReferences": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 173464ddf4cdb6640a4dfa8a9281ad69
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 83fbec88df35fe34bab43a5dde6788af
folderAsset: yes
timeCreated: 1527569675
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,5 @@
fileFormatVersion: 2
guid: f0e95036e72b08544a9d295dd4366f40
folderAsset: yes
DefaultImporter:
userData:

View File

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

View File

@ -0,0 +1,185 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Reflection;
using System;
namespace Spine.Unity.Editor {
using Editor = UnityEditor.Editor;
[CustomEditor(typeof(AnimationReferenceAsset))]
public class AnimationReferenceAssetEditor : Editor {
const string InspectorHelpText = "This is a Spine-Unity Animation Reference Asset. It serializes a reference to a SkeletonDataAsset and an animationName. It does not contain actual animation data. At runtime, it stores a reference to a Spine.Animation.\n\n" +
"You can use this in your AnimationState calls instead of a string animation name or a Spine.Animation reference. Use its implicit conversion into Spine.Animation or its .Animation property.\n\n" +
"Use AnimationReferenceAssets as an alternative to storing strings or finding animations and caching per component. This only does the lookup by string once, and allows you to store and manage animations via asset references.";
readonly SkeletonInspectorPreview preview = new SkeletonInspectorPreview();
FieldInfo skeletonDataAssetField = typeof(AnimationReferenceAsset).GetField("skeletonDataAsset", BindingFlags.NonPublic | BindingFlags.Instance);
FieldInfo nameField = typeof(AnimationReferenceAsset).GetField("animationName", BindingFlags.NonPublic | BindingFlags.Instance);
AnimationReferenceAsset ThisAnimationReferenceAsset { get { return target as AnimationReferenceAsset; } }
SkeletonDataAsset ThisSkeletonDataAsset { get { return skeletonDataAssetField.GetValue(ThisAnimationReferenceAsset) as SkeletonDataAsset; } }
string ThisAnimationName { get { return nameField.GetValue(ThisAnimationReferenceAsset) as string; } }
bool changeNextFrame = false;
SerializedProperty animationNameProperty;
SkeletonDataAsset lastSkeletonDataAsset;
SkeletonData lastSkeletonData;
void OnEnable () { HandleOnEnablePreview(); }
void OnDestroy () {
HandleOnDestroyPreview();
AppDomain.CurrentDomain.DomainUnload -= OnDomainUnload;
EditorApplication.update -= preview.HandleEditorUpdate;
}
public override void OnInspectorGUI () {
animationNameProperty = animationNameProperty ?? serializedObject.FindProperty("animationName");
string animationName = animationNameProperty.stringValue;
Animation animation = null;
if (ThisSkeletonDataAsset != null) {
var skeletonData = ThisSkeletonDataAsset.GetSkeletonData(true);
if (skeletonData != null) {
animation = skeletonData.FindAnimation(animationName);
}
}
bool animationNotFound = (animation == null);
if (changeNextFrame) {
changeNextFrame = false;
if (ThisSkeletonDataAsset != lastSkeletonDataAsset || ThisSkeletonDataAsset.GetSkeletonData(true) != lastSkeletonData) {
preview.Clear();
preview.Initialize(Repaint, ThisSkeletonDataAsset, LastSkinName);
if (animationNotFound) {
animationNameProperty.stringValue = "";
preview.ClearAnimationSetupPose();
}
}
preview.ClearAnimationSetupPose();
if (!string.IsNullOrEmpty(animationNameProperty.stringValue))
preview.PlayPauseAnimation(animationNameProperty.stringValue, true);
}
lastSkeletonDataAsset = ThisSkeletonDataAsset;
lastSkeletonData = ThisSkeletonDataAsset.GetSkeletonData(true);
//EditorGUILayout.HelpBox(AnimationReferenceAssetEditor.InspectorHelpText, MessageType.Info, true);
EditorGUILayout.Space();
EditorGUI.BeginChangeCheck();
DrawDefaultInspector();
if (EditorGUI.EndChangeCheck()) {
changeNextFrame = true;
}
// Draw extra info below default inspector.
EditorGUILayout.Space();
if (ThisSkeletonDataAsset == null) {
EditorGUILayout.HelpBox("SkeletonDataAsset is missing.", MessageType.Error);
} else if (string.IsNullOrEmpty(animationName)) {
EditorGUILayout.HelpBox("No animation selected.", MessageType.Warning);
} else if (animationNotFound) {
EditorGUILayout.HelpBox(string.Format("Animation named {0} was not found for this Skeleton.", animationNameProperty.stringValue), MessageType.Warning);
} else {
using (new SpineInspectorUtility.BoxScope()) {
if (!string.Equals(AssetUtility.GetPathSafeName(animationName), ThisAnimationReferenceAsset.name, System.StringComparison.OrdinalIgnoreCase))
EditorGUILayout.HelpBox("Animation name value does not match this asset's name. Inspectors using this asset may be misleading.", MessageType.None);
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(animationName, SpineEditorUtilities.Icons.animation));
if (animation != null) {
EditorGUILayout.LabelField(string.Format("Timelines: {0}", animation.Timelines.Count));
EditorGUILayout.LabelField(string.Format("Duration: {0} sec", animation.Duration));
}
}
}
}
#region Preview Handlers
string TargetAssetGUID { get { return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(ThisSkeletonDataAsset)); } }
string LastSkinKey { get { return TargetAssetGUID + "_lastSkin"; } }
string LastSkinName { get { return EditorPrefs.GetString(LastSkinKey, ""); } }
void HandleOnEnablePreview () {
if (ThisSkeletonDataAsset != null && ThisSkeletonDataAsset.skeletonJSON == null)
return;
SpineEditorUtilities.ConfirmInitialization();
// This handles the case where the managed editor assembly is unloaded before recompilation when code changes.
AppDomain.CurrentDomain.DomainUnload -= OnDomainUnload;
AppDomain.CurrentDomain.DomainUnload += OnDomainUnload;
preview.Initialize(this.Repaint, ThisSkeletonDataAsset, LastSkinName);
preview.PlayPauseAnimation(ThisAnimationName, true);
preview.OnSkinChanged -= HandleOnSkinChanged;
preview.OnSkinChanged += HandleOnSkinChanged;
EditorApplication.update -= preview.HandleEditorUpdate;
EditorApplication.update += preview.HandleEditorUpdate;
}
private void OnDomainUnload (object sender, EventArgs e) {
OnDestroy();
}
private void HandleOnSkinChanged (string skinName) {
EditorPrefs.SetString(LastSkinKey, skinName);
preview.PlayPauseAnimation(ThisAnimationName, true);
}
void HandleOnDestroyPreview () {
EditorApplication.update -= preview.HandleEditorUpdate;
preview.OnDestroy();
}
override public bool HasPreviewGUI () {
if (serializedObject.isEditingMultipleObjects) return false;
return ThisSkeletonDataAsset != null && ThisSkeletonDataAsset.GetSkeletonData(true) != null;
}
override public void OnInteractivePreviewGUI (Rect r, GUIStyle background) {
preview.Initialize(this.Repaint, ThisSkeletonDataAsset);
preview.HandleInteractivePreviewGUI(r, background);
}
public override GUIContent GetPreviewTitle () { return SpineInspectorUtility.TempContent("Preview"); }
public override void OnPreviewSettings () { preview.HandleDrawSettings(); }
public override Texture2D RenderStaticPreview (string assetPath, UnityEngine.Object[] subAssets, int width, int height) { return preview.GetStaticPreview(width, height); }
#endregion
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 01cbef8f24d105f4bafa9668d669e040
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@ -0,0 +1,383 @@
/******************************************************************************
* 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.
*****************************************************************************/
//#define BAKE_ALL_BUTTON
//#define REGION_BAKING_MESH
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Spine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
[CustomEditor(typeof(SpineAtlasAsset)), CanEditMultipleObjects]
public class SpineAtlasAssetInspector : UnityEditor.Editor {
SerializedProperty atlasFile, materials;
SpineAtlasAsset atlasAsset;
GUIContent spriteSlicesLabel;
GUIContent SpriteSlicesLabel {
get {
if (spriteSlicesLabel == null) {
spriteSlicesLabel = new GUIContent(
"Apply Regions as Texture Sprite Slices",
SpineEditorUtilities.Icons.unity,
"Adds Sprite slices to atlas texture(s). " +
"Updates existing slices if ones with matching names exist. \n\n" +
"If your atlas was exported with Premultiply Alpha, " +
"your SpriteRenderer should use the generated Spine _Material asset (or any Material with a PMA shader) instead of Sprites-Default.");
}
return spriteSlicesLabel;
}
}
static List<AtlasRegion> GetRegions (Atlas atlas) {
FieldInfo regionsField = SpineInspectorUtility.GetNonPublicField(typeof(Atlas), "regions");
return (List<AtlasRegion>)regionsField.GetValue(atlas);
}
void OnEnable () {
SpineEditorUtilities.ConfirmInitialization();
atlasFile = serializedObject.FindProperty("atlasFile");
materials = serializedObject.FindProperty("materials");
materials.isExpanded = true;
atlasAsset = (SpineAtlasAsset)target;
#if REGION_BAKING_MESH
UpdateBakedList();
#endif
}
#if REGION_BAKING_MESH
private List<bool> baked;
private List<GameObject> bakedObjects;
void UpdateBakedList () {
AtlasAsset asset = (AtlasAsset)target;
baked = new List<bool>();
bakedObjects = new List<GameObject>();
if (atlasFile.objectReferenceValue != null) {
List<AtlasRegion> regions = this.Regions;
string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset);
string atlasAssetDirPath = Path.GetDirectoryName(atlasAssetPath);
string bakedDirPath = Path.Combine(atlasAssetDirPath, atlasAsset.name);
for (int i = 0; i < regions.Count; i++) {
AtlasRegion region = regions[i];
string bakedPrefabPath = Path.Combine(bakedDirPath, AssetUtility.GetPathSafeRegionName(region) + ".prefab").Replace("\\", "/");
GameObject prefab = (GameObject)AssetDatabase.LoadAssetAtPath(bakedPrefabPath, typeof(GameObject));
baked.Add(prefab != null);
bakedObjects.Add(prefab);
}
}
}
#endif
override public void OnInspectorGUI () {
if (serializedObject.isEditingMultipleObjects) {
DrawDefaultInspector();
return;
}
serializedObject.Update();
atlasAsset = (atlasAsset == null) ? (SpineAtlasAsset)target : atlasAsset;
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(atlasFile);
EditorGUILayout.PropertyField(materials, true);
if (EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
atlasAsset.Clear();
atlasAsset.GetAtlas();
}
if (materials.arraySize == 0) {
EditorGUILayout.HelpBox("No materials", MessageType.Error);
return;
}
for (int i = 0; i < materials.arraySize; i++) {
SerializedProperty prop = materials.GetArrayElementAtIndex(i);
var material = (Material)prop.objectReferenceValue;
if (material == null) {
EditorGUILayout.HelpBox("Materials cannot be null.", MessageType.Error);
return;
}
}
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 (var m in atlasAsset.materials) {
var texture = m.mainTexture;
string texturePath = AssetDatabase.GetAssetPath(texture.GetInstanceID());
var importer = (TextureImporter)TextureImporter.GetAtPath(texturePath);
importer.mipMapBias = SpinePreferences.DEFAULT_MIPMAPBIAS;
EditorUtility.SetDirty(texture);
}
Debug.Log("Texture mipmap bias set to " + SpinePreferences.DEFAULT_MIPMAPBIAS);
}
EditorGUILayout.Space();
if (atlasFile.objectReferenceValue != null) {
if (SpineInspectorUtility.LargeCenteredButton(SpriteSlicesLabel)) {
var atlas = atlasAsset.GetAtlas();
foreach (var m in atlasAsset.materials)
UpdateSpriteSlices(m.mainTexture, atlas);
}
}
EditorGUILayout.Space();
#if REGION_BAKING_MESH
if (atlasFile.objectReferenceValue != null) {
Atlas atlas = asset.GetAtlas();
FieldInfo field = typeof(Atlas).GetField("regions", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.NonPublic);
List<AtlasRegion> regions = (List<AtlasRegion>)field.GetValue(atlas);
EditorGUILayout.LabelField(new GUIContent("Region Baking", SpineEditorUtilities.Icons.unityIcon));
EditorGUI.indentLevel++;
AtlasPage lastPage = null;
for (int i = 0; i < regions.Count; i++) {
if (lastPage != regions[i].page) {
if (lastPage != null) {
EditorGUILayout.Separator();
EditorGUILayout.Separator();
}
lastPage = regions[i].page;
Material mat = ((Material)lastPage.rendererObject);
if (mat != null) {
GUILayout.BeginHorizontal();
{
EditorGUI.BeginDisabledGroup(true);
EditorGUILayout.ObjectField(mat, typeof(Material), false, GUILayout.Width(250));
EditorGUI.EndDisabledGroup();
}
GUILayout.EndHorizontal();
} else {
EditorGUILayout.LabelField(new GUIContent("Page missing material!", SpineEditorUtilities.Icons.warning));
}
}
GUILayout.BeginHorizontal();
{
//EditorGUILayout.ToggleLeft(baked[i] ? "" : regions[i].name, baked[i]);
bool result = baked[i] ? EditorGUILayout.ToggleLeft("", baked[i], GUILayout.Width(24)) : EditorGUILayout.ToggleLeft(" " + regions[i].name, baked[i]);
if(baked[i]){
EditorGUILayout.ObjectField(bakedObjects[i], typeof(GameObject), false, GUILayout.Width(250));
}
if (result && !baked[i]) {
//bake
baked[i] = true;
bakedObjects[i] = SpineEditorUtilities.BakeRegion(atlasAsset, regions[i]);
EditorGUIUtility.PingObject(bakedObjects[i]);
} else if (!result && baked[i]) {
//unbake
bool unbakeResult = EditorUtility.DisplayDialog("Delete Baked Region", "Do you want to delete the prefab for " + regions[i].name, "Yes", "Cancel");
switch (unbakeResult) {
case true:
//delete
string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset);
string atlasAssetDirPath = Path.GetDirectoryName(atlasAssetPath);
string bakedDirPath = Path.Combine(atlasAssetDirPath, atlasAsset.name);
string bakedPrefabPath = Path.Combine(bakedDirPath, SpineEditorUtilities.GetPathSafeRegionName(regions[i]) + ".prefab").Replace("\\", "/");
AssetDatabase.DeleteAsset(bakedPrefabPath);
baked[i] = false;
break;
case false:
//do nothing
break;
}
}
}
GUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
#if BAKE_ALL_BUTTON
// Check state
bool allBaked = true;
bool allUnbaked = true;
for (int i = 0; i < regions.Count; i++) {
allBaked &= baked[i];
allUnbaked &= !baked[i];
}
if (!allBaked && GUILayout.Button("Bake All")) {
for (int i = 0; i < regions.Count; i++) {
if (!baked[i]) {
baked[i] = true;
bakedObjects[i] = SpineEditorUtilities.BakeRegion(atlasAsset, regions[i]);
}
}
} else if (!allUnbaked && GUILayout.Button("Unbake All")) {
bool unbakeResult = EditorUtility.DisplayDialog("Delete All Baked Regions", "Are you sure you want to unbake all region prefabs? This cannot be undone.", "Yes", "Cancel");
switch (unbakeResult) {
case true:
//delete
for (int i = 0; i < regions.Count; i++) {
if (baked[i]) {
string atlasAssetPath = AssetDatabase.GetAssetPath(atlasAsset);
string atlasAssetDirPath = Path.GetDirectoryName(atlasAssetPath);
string bakedDirPath = Path.Combine(atlasAssetDirPath, atlasAsset.name);
string bakedPrefabPath = Path.Combine(bakedDirPath, SpineEditorUtilities.GetPathSafeRegionName(regions[i]) + ".prefab").Replace("\\", "/");
AssetDatabase.DeleteAsset(bakedPrefabPath);
baked[i] = false;
}
}
break;
case false:
//do nothing
break;
}
}
#endif
}
#else
if (atlasFile.objectReferenceValue != null) {
int baseIndent = EditorGUI.indentLevel;
var regions = SpineAtlasAssetInspector.GetRegions(atlasAsset.GetAtlas());
int regionsCount = regions.Count;
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel);
EditorGUILayout.LabelField(string.Format("{0} regions total", regionsCount));
}
AtlasPage lastPage = null;
for (int i = 0; i < regionsCount; i++) {
if (lastPage != regions[i].page) {
if (lastPage != null) {
EditorGUILayout.Separator();
EditorGUILayout.Separator();
}
lastPage = regions[i].page;
Material mat = ((Material)lastPage.rendererObject);
if (mat != null) {
EditorGUI.indentLevel = baseIndent;
using (new GUILayout.HorizontalScope())
using (new EditorGUI.DisabledGroupScope(true))
EditorGUILayout.ObjectField(mat, typeof(Material), false, GUILayout.Width(250));
EditorGUI.indentLevel = baseIndent + 1;
} else {
EditorGUILayout.HelpBox("Page missing material!", MessageType.Warning);
}
}
string regionName = regions[i].name;
Texture2D icon = SpineEditorUtilities.Icons.image;
if (regionName.EndsWith(" ")) {
regionName = string.Format("'{0}'", regions[i].name);
icon = SpineEditorUtilities.Icons.warning;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon, "Region name ends with whitespace. This may cause errors. Please check your source image filenames."));
} else {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon));
}
}
EditorGUI.indentLevel = baseIndent;
}
#endif
if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current))
atlasAsset.Clear();
}
static public void UpdateSpriteSlices (Texture texture, Atlas atlas) {
string texturePath = AssetDatabase.GetAssetPath(texture.GetInstanceID());
var t = (TextureImporter)TextureImporter.GetAtPath(texturePath);
t.spriteImportMode = SpriteImportMode.Multiple;
var spriteSheet = t.spritesheet;
var sprites = new List<SpriteMetaData>(spriteSheet);
var regions = SpineAtlasAssetInspector.GetRegions(atlas);
char[] FilenameDelimiter = {'.'};
int updatedCount = 0;
int addedCount = 0;
foreach (var r in regions) {
string pageName = r.page.name.Split(FilenameDelimiter, StringSplitOptions.RemoveEmptyEntries)[0];
string textureName = texture.name;
bool pageMatch = string.Equals(pageName, textureName, StringComparison.Ordinal);
// if (pageMatch) {
// int pw = r.page.width;
// int ph = r.page.height;
// bool mismatchSize = pw != texture.width || pw > t.maxTextureSize || ph != texture.height || ph > t.maxTextureSize;
// if (mismatchSize)
// Debug.LogWarningFormat("Size mismatch found.\nExpected atlas size is {0}x{1}. Texture Import Max Size of texture '{2}'({4}x{5}) is currently set to {3}.", pw, ph, texture.name, t.maxTextureSize, texture.width, texture.height);
// }
int spriteIndex = pageMatch ? sprites.FindIndex(
(s) => string.Equals(s.name, r.name, StringComparison.Ordinal)
) : -1;
bool spriteNameMatchExists = spriteIndex >= 0;
if (pageMatch) {
Rect spriteRect = new Rect();
if (r.rotate) {
spriteRect.width = r.height;
spriteRect.height = r.width;
} else {
spriteRect.width = r.width;
spriteRect.height = r.height;
}
spriteRect.x = r.x;
spriteRect.y = r.page.height - spriteRect.height - r.y;
if (spriteNameMatchExists) {
var s = sprites[spriteIndex];
s.rect = spriteRect;
sprites[spriteIndex] = s;
updatedCount++;
} else {
sprites.Add(new SpriteMetaData {
name = r.name,
pivot = new Vector2(0.5f, 0.5f),
rect = spriteRect
});
addedCount++;
}
}
}
t.spritesheet = sprites.ToArray();
EditorUtility.SetDirty(t);
AssetDatabase.ImportAsset(texturePath, ImportAssetOptions.ForceUpdate);
EditorGUIUtility.PingObject(texture);
Debug.Log(string.Format("Applied sprite slices to {2}. {0} added. {1} updated.", addedCount, updatedCount, texture.name));
}
}
}

View File

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

View File

@ -0,0 +1,153 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Spine;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
[CustomEditor(typeof(SpineSpriteAtlasAsset)), CanEditMultipleObjects]
public class SpineSpriteAtlasAssetInspector : UnityEditor.Editor {
SerializedProperty atlasFile, materials;
SpineSpriteAtlasAsset atlasAsset;
static List<AtlasRegion> GetRegions (Atlas atlas) {
FieldInfo regionsField = SpineInspectorUtility.GetNonPublicField(typeof(Atlas), "regions");
return (List<AtlasRegion>)regionsField.GetValue(atlas);
}
void OnEnable () {
SpineEditorUtilities.ConfirmInitialization();
atlasFile = serializedObject.FindProperty("spriteAtlasFile");
materials = serializedObject.FindProperty("materials");
materials.isExpanded = true;
atlasAsset = (SpineSpriteAtlasAsset)target;
if (!SpineSpriteAtlasAsset.AnySpriteAtlasNeedsRegionsLoaded())
return;
EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
EditorApplication.update += SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
}
void OnDisable () {
EditorApplication.update -= SpineSpriteAtlasAsset.UpdateWhenEditorPlayModeStarted;
}
override public void OnInspectorGUI () {
if (serializedObject.isEditingMultipleObjects) {
DrawDefaultInspector();
return;
}
serializedObject.Update();
atlasAsset = (atlasAsset == null) ? (SpineSpriteAtlasAsset)target : atlasAsset;
if (atlasAsset.RegionsNeedLoading) {
if (GUILayout.Button(SpineInspectorUtility.TempContent("Load regions by entering Play mode"), GUILayout.Height(20))) {
EditorApplication.isPlaying = true;
}
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(atlasFile);
EditorGUILayout.PropertyField(materials, true);
if (EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
atlasAsset.Clear();
atlasAsset.GetAtlas();
atlasAsset.updateRegionsInPlayMode = true;
}
if (materials.arraySize == 0) {
EditorGUILayout.HelpBox("No materials", MessageType.Error);
return;
}
for (int i = 0; i < materials.arraySize; i++) {
SerializedProperty prop = materials.GetArrayElementAtIndex(i);
var material = (Material)prop.objectReferenceValue;
if (material == null) {
EditorGUILayout.HelpBox("Materials cannot be null.", MessageType.Error);
return;
}
}
if (atlasFile.objectReferenceValue != null) {
int baseIndent = EditorGUI.indentLevel;
var regions = SpineSpriteAtlasAssetInspector.GetRegions(atlasAsset.GetAtlas());
int regionsCount = regions.Count;
using (new EditorGUILayout.HorizontalScope()) {
EditorGUILayout.LabelField("Atlas Regions", EditorStyles.boldLabel);
EditorGUILayout.LabelField(string.Format("{0} regions total", regionsCount));
}
AtlasPage lastPage = null;
for (int i = 0; i < regionsCount; i++) {
if (lastPage != regions[i].page) {
if (lastPage != null) {
EditorGUILayout.Separator();
EditorGUILayout.Separator();
}
lastPage = regions[i].page;
Material mat = ((Material)lastPage.rendererObject);
if (mat != null) {
EditorGUI.indentLevel = baseIndent;
using (new GUILayout.HorizontalScope())
using (new EditorGUI.DisabledGroupScope(true))
EditorGUILayout.ObjectField(mat, typeof(Material), false, GUILayout.Width(250));
EditorGUI.indentLevel = baseIndent + 1;
} else {
EditorGUILayout.HelpBox("Page missing material!", MessageType.Warning);
}
}
string regionName = regions[i].name;
Texture2D icon = SpineEditorUtilities.Icons.image;
if (regionName.EndsWith(" ")) {
regionName = string.Format("'{0}'", regions[i].name);
icon = SpineEditorUtilities.Icons.warning;
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon, "Region name ends with whitespace. This may cause errors. Please check your source image filenames."));
} else {
EditorGUILayout.LabelField(SpineInspectorUtility.TempContent(regionName, icon));
}
}
EditorGUI.indentLevel = baseIndent;
}
if (serializedObject.ApplyModifiedProperties() || SpineInspectorUtility.UndoRedoPerformed(Event.current))
atlasAsset.Clear();
}
}
}

View File

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

View File

@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 0134640f881c8d24d812a6f9af9d0761
folderAsset: yes
timeCreated: 1563304704
licenseType: Free
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,207 @@
/******************************************************************************
* 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.
*****************************************************************************/
using UnityEngine;
using UnityEditor;
using Spine.Unity;
namespace Spine.Unity.Editor {
using Editor = UnityEditor.Editor;
using Event = UnityEngine.Event;
[CustomEditor(typeof(BoneFollowerGraphic)), CanEditMultipleObjects]
public class BoneFollowerGraphicInspector : Editor {
SerializedProperty boneName, skeletonGraphic, followXYPosition, followZPosition, followBoneRotation,
followLocalScale, followSkeletonFlip, maintainedAxisOrientation;
BoneFollowerGraphic targetBoneFollower;
bool needsReset;
#region Context Menu Item
[MenuItem ("CONTEXT/SkeletonGraphic/Add BoneFollower GameObject")]
static void AddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonGraphic = cmd.context as SkeletonGraphic;
var go = EditorInstantiation.NewGameObject("BoneFollower", true, typeof(RectTransform));
var t = go.transform;
t.SetParent(skeletonGraphic.transform);
t.localPosition = Vector3.zero;
var f = go.AddComponent<BoneFollowerGraphic>();
f.skeletonGraphic = skeletonGraphic;
f.SetBone(skeletonGraphic.Skeleton.RootBone.Data.Name);
EditorGUIUtility.PingObject(t);
Undo.RegisterCreatedObjectUndo(go, "Add BoneFollowerGraphic");
}
// Validate
[MenuItem ("CONTEXT/SkeletonGraphic/Add BoneFollower GameObject", true)]
static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonGraphic = cmd.context as SkeletonGraphic;
return skeletonGraphic.IsValid;
}
#endregion
void OnEnable () {
skeletonGraphic = serializedObject.FindProperty("skeletonGraphic");
boneName = serializedObject.FindProperty("boneName");
followBoneRotation = serializedObject.FindProperty("followBoneRotation");
followXYPosition = serializedObject.FindProperty("followXYPosition");
followZPosition = serializedObject.FindProperty("followZPosition");
followLocalScale = serializedObject.FindProperty("followLocalScale");
followSkeletonFlip = serializedObject.FindProperty("followSkeletonFlip");
maintainedAxisOrientation = serializedObject.FindProperty("maintainedAxisOrientation");
targetBoneFollower = (BoneFollowerGraphic)target;
if (targetBoneFollower.SkeletonGraphic != null)
targetBoneFollower.SkeletonGraphic.Initialize(false);
if (!targetBoneFollower.valid || needsReset) {
targetBoneFollower.Initialize();
targetBoneFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
}
public void OnSceneGUI () {
var tbf = target as BoneFollowerGraphic;
var skeletonGraphicComponent = tbf.SkeletonGraphic;
if (skeletonGraphicComponent == null) return;
var transform = skeletonGraphicComponent.transform;
var skeleton = skeletonGraphicComponent.Skeleton;
var canvas = skeletonGraphicComponent.canvas;
float positionScale = canvas == null ? 1f : skeletonGraphicComponent.canvas.referencePixelsPerUnit;
if (string.IsNullOrEmpty(boneName.stringValue)) {
SpineHandles.DrawBones(transform, skeleton, positionScale);
SpineHandles.DrawBoneNames(transform, skeleton, positionScale);
Handles.Label(tbf.transform.position, "No bone selected", EditorStyles.helpBox);
} else {
var targetBone = tbf.bone;
if (targetBone == null) return;
SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor, positionScale);
Handles.Label(targetBone.GetWorldPosition(transform, positionScale), targetBone.Data.Name, SpineHandles.BoneNameStyle);
}
}
override public void OnInspectorGUI () {
if (serializedObject.isEditingMultipleObjects) {
if (needsReset) {
needsReset = false;
foreach (var o in targets) {
var bf = (BoneFollower)o;
bf.Initialize();
bf.LateUpdate();
}
SceneView.RepaintAll();
}
EditorGUI.BeginChangeCheck();
DrawDefaultInspector();
needsReset |= EditorGUI.EndChangeCheck();
return;
}
if (needsReset && Event.current.type == EventType.Layout) {
targetBoneFollower.Initialize();
targetBoneFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
serializedObject.Update();
// Find Renderer
if (skeletonGraphic.objectReferenceValue == null) {
SkeletonGraphic parentRenderer = targetBoneFollower.GetComponentInParent<SkeletonGraphic>();
if (parentRenderer != null && parentRenderer.gameObject != targetBoneFollower.gameObject) {
skeletonGraphic.objectReferenceValue = parentRenderer;
Debug.Log("Inspector automatically assigned BoneFollowerGraphic.SkeletonGraphic");
}
}
EditorGUILayout.PropertyField(skeletonGraphic);
var skeletonGraphicComponent = skeletonGraphic.objectReferenceValue as SkeletonGraphic;
if (skeletonGraphicComponent != null) {
if (skeletonGraphicComponent.gameObject == targetBoneFollower.gameObject) {
skeletonGraphic.objectReferenceValue = null;
EditorUtility.DisplayDialog("Invalid assignment.", "BoneFollowerGraphic can only follow a skeleton on a separate GameObject.\n\nCreate a new GameObject for your BoneFollower, or choose a SkeletonGraphic from a different GameObject.", "Ok");
}
}
if (!targetBoneFollower.valid) {
needsReset = true;
}
if (targetBoneFollower.valid) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(boneName);
needsReset |= EditorGUI.EndChangeCheck();
EditorGUILayout.PropertyField(followBoneRotation);
EditorGUILayout.PropertyField(followXYPosition);
EditorGUILayout.PropertyField(followZPosition);
EditorGUILayout.PropertyField(followLocalScale);
EditorGUILayout.PropertyField(followSkeletonFlip);
if ((followSkeletonFlip.hasMultipleDifferentValues || followSkeletonFlip.boolValue == false) &&
(followBoneRotation.hasMultipleDifferentValues || followBoneRotation.boolValue == true)) {
using (new SpineInspectorUtility.IndentScope())
EditorGUILayout.PropertyField(maintainedAxisOrientation);
}
//BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower);
} else {
var boneFollowerSkeletonGraphic = targetBoneFollower.skeletonGraphic;
if (boneFollowerSkeletonGraphic == null) {
EditorGUILayout.HelpBox("SkeletonGraphic is unassigned. Please assign a SkeletonRenderer (SkeletonAnimation or SkeletonMecanim).", MessageType.Warning);
} else {
boneFollowerSkeletonGraphic.Initialize(false);
if (boneFollowerSkeletonGraphic.skeletonDataAsset == null)
EditorGUILayout.HelpBox("Assigned SkeletonGraphic does not have SkeletonData assigned to it.", MessageType.Warning);
if (!boneFollowerSkeletonGraphic.IsValid)
EditorGUILayout.HelpBox("Assigned SkeletonGraphic is invalid. Check target SkeletonGraphic, its SkeletonDataAsset or the console for other errors.", MessageType.Warning);
}
}
var current = Event.current;
bool wasUndo = (current.type == EventType.ValidateCommand && current.commandName == "UndoRedoPerformed");
if (wasUndo)
targetBoneFollower.Initialize();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@ -0,0 +1,228 @@
/******************************************************************************
* 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.
*****************************************************************************/
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Editor = UnityEditor.Editor;
using Event = UnityEngine.Event;
[CustomEditor(typeof(BoneFollower)), CanEditMultipleObjects]
public class BoneFollowerInspector : Editor {
SerializedProperty boneName, skeletonRenderer, followXYPosition, followZPosition, followBoneRotation,
followLocalScale, followSkeletonFlip, maintainedAxisOrientation;
BoneFollower targetBoneFollower;
bool needsReset;
#region Context Menu Item
[MenuItem ("CONTEXT/SkeletonRenderer/Add BoneFollower GameObject")]
static void AddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonRenderer = cmd.context as SkeletonRenderer;
var go = EditorInstantiation.NewGameObject("New BoneFollower", true);
var t = go.transform;
t.SetParent(skeletonRenderer.transform);
t.localPosition = Vector3.zero;
var f = go.AddComponent<BoneFollower>();
f.skeletonRenderer = skeletonRenderer;
EditorGUIUtility.PingObject(t);
Undo.RegisterCreatedObjectUndo(go, "Add BoneFollower");
}
// Validate
[MenuItem ("CONTEXT/SkeletonRenderer/Add BoneFollower GameObject", true)]
static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonRenderer = cmd.context as SkeletonRenderer;
return skeletonRenderer.valid;
}
[MenuItem("CONTEXT/BoneFollower/Rename BoneFollower GameObject")]
static void RenameGameObject (MenuCommand cmd) {
AutonameGameObject(cmd.context as BoneFollower);
}
#endregion
static void AutonameGameObject (BoneFollower boneFollower) {
if (boneFollower == null) return;
string boneName = boneFollower.boneName;
boneFollower.gameObject.name = string.IsNullOrEmpty(boneName) ? "BoneFollower" : string.Format("{0} (BoneFollower)", boneName);
}
void OnEnable () {
skeletonRenderer = serializedObject.FindProperty("skeletonRenderer");
boneName = serializedObject.FindProperty("boneName");
followBoneRotation = serializedObject.FindProperty("followBoneRotation");
followXYPosition = serializedObject.FindProperty("followXYPosition");
followZPosition = serializedObject.FindProperty("followZPosition");
followLocalScale = serializedObject.FindProperty("followLocalScale");
followSkeletonFlip = serializedObject.FindProperty("followSkeletonFlip");
maintainedAxisOrientation = serializedObject.FindProperty("maintainedAxisOrientation");
targetBoneFollower = (BoneFollower)target;
if (targetBoneFollower.SkeletonRenderer != null)
targetBoneFollower.SkeletonRenderer.Initialize(false);
if (!targetBoneFollower.valid || needsReset) {
targetBoneFollower.Initialize();
targetBoneFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
}
public void OnSceneGUI () {
var tbf = target as BoneFollower;
var skeletonRendererComponent = tbf.skeletonRenderer;
if (skeletonRendererComponent == null) return;
var transform = skeletonRendererComponent.transform;
var skeleton = skeletonRendererComponent.skeleton;
if (string.IsNullOrEmpty(boneName.stringValue)) {
SpineHandles.DrawBones(transform, skeleton);
SpineHandles.DrawBoneNames(transform, skeleton);
Handles.Label(tbf.transform.position, "No bone selected", EditorStyles.helpBox);
} else {
var targetBone = tbf.bone;
if (targetBone == null) return;
SpineHandles.DrawBoneWireframe(transform, targetBone, SpineHandles.TransformContraintColor);
Handles.Label(targetBone.GetWorldPosition(transform), targetBone.Data.Name, SpineHandles.BoneNameStyle);
}
}
override public void OnInspectorGUI () {
if (serializedObject.isEditingMultipleObjects) {
if (needsReset) {
needsReset = false;
foreach (var o in targets) {
var bf = (BoneFollower)o;
bf.Initialize();
bf.LateUpdate();
}
SceneView.RepaintAll();
}
EditorGUI.BeginChangeCheck();
DrawDefaultInspector();
needsReset |= EditorGUI.EndChangeCheck();
return;
}
if (needsReset && Event.current.type == EventType.Layout) {
targetBoneFollower.Initialize();
targetBoneFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
serializedObject.Update();
// Find Renderer
if (skeletonRenderer.objectReferenceValue == null) {
SkeletonRenderer parentRenderer = targetBoneFollower.GetComponentInParent<SkeletonRenderer>();
if (parentRenderer != null && parentRenderer.gameObject != targetBoneFollower.gameObject) {
skeletonRenderer.objectReferenceValue = parentRenderer;
Debug.Log("Inspector automatically assigned BoneFollower.SkeletonRenderer");
}
}
EditorGUILayout.PropertyField(skeletonRenderer);
var skeletonRendererReference = skeletonRenderer.objectReferenceValue as SkeletonRenderer;
if (skeletonRendererReference != null) {
if (skeletonRendererReference.gameObject == targetBoneFollower.gameObject) {
skeletonRenderer.objectReferenceValue = null;
EditorUtility.DisplayDialog("Invalid assignment.", "BoneFollower can only follow a skeleton on a separate GameObject.\n\nCreate a new GameObject for your BoneFollower, or choose a SkeletonRenderer from a different GameObject.", "Ok");
}
}
if (!targetBoneFollower.valid) {
needsReset = true;
}
if (targetBoneFollower.valid) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(boneName);
needsReset |= EditorGUI.EndChangeCheck();
EditorGUILayout.PropertyField(followBoneRotation);
EditorGUILayout.PropertyField(followXYPosition);
EditorGUILayout.PropertyField(followZPosition);
EditorGUILayout.PropertyField(followLocalScale);
EditorGUILayout.PropertyField(followSkeletonFlip);
if ((followSkeletonFlip.hasMultipleDifferentValues || followSkeletonFlip.boolValue == false) &&
(followBoneRotation.hasMultipleDifferentValues || followBoneRotation.boolValue == true)) {
using (new SpineInspectorUtility.IndentScope())
EditorGUILayout.PropertyField(maintainedAxisOrientation);
}
BoneFollowerInspector.RecommendRigidbodyButton(targetBoneFollower);
} else {
var boneFollowerSkeletonRenderer = targetBoneFollower.skeletonRenderer;
if (boneFollowerSkeletonRenderer == null) {
EditorGUILayout.HelpBox("SkeletonRenderer is unassigned. Please assign a SkeletonRenderer (SkeletonAnimation or SkeletonMecanim).", MessageType.Warning);
} else {
boneFollowerSkeletonRenderer.Initialize(false);
if (boneFollowerSkeletonRenderer.skeletonDataAsset == null)
EditorGUILayout.HelpBox("Assigned SkeletonRenderer does not have SkeletonData assigned to it.", MessageType.Warning);
if (!boneFollowerSkeletonRenderer.valid)
EditorGUILayout.HelpBox("Assigned SkeletonRenderer is invalid. Check target SkeletonRenderer, its SkeletonDataAsset or the console for other errors.", MessageType.Warning);
}
}
var current = Event.current;
bool wasUndo = (current.type == EventType.ValidateCommand && current.commandName == "UndoRedoPerformed");
if (wasUndo)
targetBoneFollower.Initialize();
serializedObject.ApplyModifiedProperties();
}
internal static void RecommendRigidbodyButton (Component component) {
bool hasCollider2D = component.GetComponent<Collider2D>() != null || component.GetComponent<BoundingBoxFollower>() != null;
bool hasCollider3D = !hasCollider2D && component.GetComponent<Collider>();
bool missingRigidBody = (hasCollider2D && component.GetComponent<Rigidbody2D>() == null) || (hasCollider3D && component.GetComponent<Rigidbody>() == null);
if (missingRigidBody) {
using (new SpineInspectorUtility.BoxScope()) {
EditorGUILayout.HelpBox("Collider detected. Unity recommends adding a Rigidbody to the Transforms of any colliders that are intended to be dynamically repositioned and rotated.", MessageType.Warning);
var rbType = hasCollider2D ? typeof(Rigidbody2D) : typeof(Rigidbody);
string rbLabel = string.Format("Add {0}", rbType.Name);
var rbContent = SpineInspectorUtility.TempContent(rbLabel, SpineInspectorUtility.UnityIcon(rbType), "Add a rigidbody to this GameObject to be the Physics body parent of the attached collider.");
if (SpineInspectorUtility.CenteredButton(rbContent)) component.gameObject.AddComponent(rbType);
}
}
}
}
}

View File

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

View File

@ -0,0 +1,261 @@
/******************************************************************************
* 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
#endif
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
using Icons = SpineEditorUtilities.Icons;
[CustomEditor(typeof(BoundingBoxFollowerGraphic))]
public class BoundingBoxFollowerGraphicInspector : UnityEditor.Editor {
SerializedProperty skeletonGraphic, slotName, isTrigger, clearStateOnDisable;
BoundingBoxFollowerGraphic follower;
bool rebuildRequired = false;
bool addBoneFollower = false;
bool sceneRepaintRequired = false;
bool debugIsExpanded;
GUIContent addBoneFollowerLabel;
GUIContent AddBoneFollowerLabel {
get {
if (addBoneFollowerLabel == null) addBoneFollowerLabel = new GUIContent("Add Bone Follower", Icons.bone);
return addBoneFollowerLabel;
}
}
void InitializeEditor () {
skeletonGraphic = serializedObject.FindProperty("skeletonGraphic");
slotName = serializedObject.FindProperty("slotName");
isTrigger = serializedObject.FindProperty("isTrigger");
clearStateOnDisable = serializedObject.FindProperty("clearStateOnDisable");
follower = (BoundingBoxFollowerGraphic)target;
}
public override void OnInspectorGUI () {
#if !NEW_PREFAB_SYSTEM
bool isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#else
bool isInspectingPrefab = false;
#endif
// Note: when calling InitializeEditor() in OnEnable, it throws exception
// "SerializedObjectNotCreatableException: Object at index 0 is null".
InitializeEditor();
// Try to auto-assign SkeletonGraphic field.
if (skeletonGraphic.objectReferenceValue == null) {
var foundSkeletonGraphic = follower.GetComponentInParent<SkeletonGraphic>();
if (foundSkeletonGraphic != null)
Debug.Log("BoundingBoxFollowerGraphic automatically assigned: " + foundSkeletonGraphic.gameObject.name);
else if (Event.current.type == EventType.Repaint)
Debug.Log("No Spine GameObject detected. Make sure to set this GameObject as a child of the Spine GameObject; or set BoundingBoxFollowerGraphic's 'Skeleton Graphic' field in the inspector.");
skeletonGraphic.objectReferenceValue = foundSkeletonGraphic;
serializedObject.ApplyModifiedProperties();
InitializeEditor();
}
var skeletonGraphicValue = skeletonGraphic.objectReferenceValue as SkeletonGraphic;
if (skeletonGraphicValue != null && skeletonGraphicValue.gameObject == follower.gameObject) {
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
EditorGUILayout.HelpBox("It's ideal to add BoundingBoxFollowerGraphic to a separate child GameObject of the Spine GameObject.", MessageType.Warning);
if (GUILayout.Button(new GUIContent("Move BoundingBoxFollowerGraphic to new GameObject", Icons.boundingBox), GUILayout.Height(30f))) {
AddBoundingBoxFollowerGraphicChild(skeletonGraphicValue, follower);
DestroyImmediate(follower);
return;
}
}
EditorGUILayout.Space();
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(skeletonGraphic);
EditorGUILayout.PropertyField(slotName, new GUIContent("Slot"));
if (EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
InitializeEditor();
#if !NEW_PREFAB_SYSTEM
if (!isInspectingPrefab)
rebuildRequired = true;
#endif
}
using (new SpineInspectorUtility.LabelWidthScope(150f)) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(isTrigger);
bool triggerChanged = EditorGUI.EndChangeCheck();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(clearStateOnDisable, new GUIContent(clearStateOnDisable.displayName, "Enable this if you are pooling your Spine GameObject"));
bool clearStateChanged = EditorGUI.EndChangeCheck();
if (clearStateChanged || triggerChanged) {
serializedObject.ApplyModifiedProperties();
InitializeEditor();
if (triggerChanged)
foreach (var col in follower.colliderTable.Values)
col.isTrigger = isTrigger.boolValue;
}
}
if (isInspectingPrefab) {
follower.colliderTable.Clear();
follower.nameTable.Clear();
EditorGUILayout.HelpBox("BoundingBoxAttachments cannot be previewed in prefabs.", MessageType.Info);
// How do you prevent components from being saved into the prefab? No such HideFlag. DontSaveInEditor | DontSaveInBuild does not work. DestroyImmediate does not work.
var collider = follower.GetComponent<PolygonCollider2D>();
if (collider != null) Debug.LogWarning("Found BoundingBoxFollowerGraphic collider components in prefab. These are disposed and regenerated at runtime.");
} else {
using (new SpineInspectorUtility.BoxScope()) {
if (debugIsExpanded = EditorGUILayout.Foldout(debugIsExpanded, "Debug Colliders")) {
EditorGUI.indentLevel++;
EditorGUILayout.LabelField(string.Format("Attachment Names ({0} PolygonCollider2D)", follower.colliderTable.Count));
EditorGUI.BeginChangeCheck();
foreach (var kp in follower.nameTable) {
string attachmentName = kp.Value;
var collider = follower.colliderTable[kp.Key];
bool isPlaceholder = attachmentName != kp.Key.Name;
collider.enabled = EditorGUILayout.ToggleLeft(new GUIContent(!isPlaceholder ? attachmentName : string.Format("{0} [{1}]", attachmentName, kp.Key.Name), isPlaceholder ? Icons.skinPlaceholder : Icons.boundingBox), collider.enabled);
}
sceneRepaintRequired |= EditorGUI.EndChangeCheck();
EditorGUI.indentLevel--;
}
}
}
if (follower.Slot == null)
follower.Initialize(false);
bool hasBoneFollower = follower.GetComponent<BoneFollowerGraphic>() != null;
if (!hasBoneFollower) {
bool buttonDisabled = follower.Slot == null;
using (new EditorGUI.DisabledGroupScope(buttonDisabled)) {
addBoneFollower |= SpineInspectorUtility.LargeCenteredButton(AddBoneFollowerLabel, true);
EditorGUILayout.Space();
}
}
if (Event.current.type == EventType.Repaint) {
if (addBoneFollower) {
var boneFollower = follower.gameObject.AddComponent<BoneFollowerGraphic>();
boneFollower.skeletonGraphic = skeletonGraphicValue;
boneFollower.SetBone(follower.Slot.Data.BoneData.Name);
addBoneFollower = false;
}
if (sceneRepaintRequired) {
SceneView.RepaintAll();
sceneRepaintRequired = false;
}
if (rebuildRequired) {
follower.Initialize();
rebuildRequired = false;
}
}
}
#region Menus
[MenuItem("CONTEXT/SkeletonGraphic/Add BoundingBoxFollowerGraphic GameObject")]
static void AddBoundingBoxFollowerGraphicChild (MenuCommand command) {
var go = AddBoundingBoxFollowerGraphicChild((SkeletonGraphic)command.context);
Undo.RegisterCreatedObjectUndo(go, "Add BoundingBoxFollowerGraphic");
}
[MenuItem("CONTEXT/SkeletonGraphic/Add all BoundingBoxFollowerGraphic GameObjects")]
static void AddAllBoundingBoxFollowerGraphicChildren (MenuCommand command) {
var objects = AddAllBoundingBoxFollowerGraphicChildren((SkeletonGraphic)command.context);
foreach (var go in objects)
Undo.RegisterCreatedObjectUndo(go, "Add BoundingBoxFollowerGraphic");
}
#endregion
public static GameObject AddBoundingBoxFollowerGraphicChild (SkeletonGraphic skeletonGraphic,
BoundingBoxFollowerGraphic original = null, string name = "BoundingBoxFollowerGraphic",
string slotName = null) {
var go = EditorInstantiation.NewGameObject(name, true);
go.transform.SetParent(skeletonGraphic.transform, false);
go.AddComponent<RectTransform>();
var newFollower = go.AddComponent<BoundingBoxFollowerGraphic>();
if (original != null) {
newFollower.slotName = original.slotName;
newFollower.isTrigger = original.isTrigger;
newFollower.clearStateOnDisable = original.clearStateOnDisable;
}
if (slotName != null)
newFollower.slotName = slotName;
newFollower.skeletonGraphic = skeletonGraphic;
newFollower.Initialize();
Selection.activeGameObject = go;
EditorGUIUtility.PingObject(go);
return go;
}
public static List<GameObject> AddAllBoundingBoxFollowerGraphicChildren (
SkeletonGraphic skeletonGraphic, BoundingBoxFollowerGraphic original = null) {
List<GameObject> createdGameObjects = new List<GameObject>();
foreach (var skin in skeletonGraphic.Skeleton.Data.Skins) {
var attachments = skin.Attachments;
foreach (var entry in attachments) {
var boundingBoxAttachment = entry.Value as BoundingBoxAttachment;
if (boundingBoxAttachment == null)
continue;
int slotIndex = entry.Key.SlotIndex;
var slot = skeletonGraphic.Skeleton.Slots.Items[slotIndex];
string slotName = slot.Data.Name;
GameObject go = AddBoundingBoxFollowerGraphicChild(skeletonGraphic,
original, boundingBoxAttachment.Name, slotName);
var boneFollower = go.AddComponent<BoneFollowerGraphic>();
boneFollower.skeletonGraphic = skeletonGraphic;
boneFollower.SetBone(slot.Data.BoneData.Name);
createdGameObjects.Add(go);
}
}
return createdGameObjects;
}
}
}

View File

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

View File

@ -0,0 +1,260 @@
/******************************************************************************
* 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
#endif
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
namespace Spine.Unity.Editor {
using Event = UnityEngine.Event;
using Icons = SpineEditorUtilities.Icons;
[CustomEditor(typeof(BoundingBoxFollower))]
public class BoundingBoxFollowerInspector : UnityEditor.Editor {
SerializedProperty skeletonRenderer, slotName, isTrigger, clearStateOnDisable;
BoundingBoxFollower follower;
bool rebuildRequired = false;
bool addBoneFollower = false;
bool sceneRepaintRequired = false;
bool debugIsExpanded;
GUIContent addBoneFollowerLabel;
GUIContent AddBoneFollowerLabel {
get {
if (addBoneFollowerLabel == null) addBoneFollowerLabel = new GUIContent("Add Bone Follower", Icons.bone);
return addBoneFollowerLabel;
}
}
void InitializeEditor () {
skeletonRenderer = serializedObject.FindProperty("skeletonRenderer");
slotName = serializedObject.FindProperty("slotName");
isTrigger = serializedObject.FindProperty("isTrigger");
clearStateOnDisable = serializedObject.FindProperty("clearStateOnDisable");
follower = (BoundingBoxFollower)target;
}
public override void OnInspectorGUI () {
#if !NEW_PREFAB_SYSTEM
bool isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#else
bool isInspectingPrefab = false;
#endif
// Note: when calling InitializeEditor() in OnEnable, it throws exception
// "SerializedObjectNotCreatableException: Object at index 0 is null".
InitializeEditor();
// Try to auto-assign SkeletonRenderer field.
if (skeletonRenderer.objectReferenceValue == null) {
var foundSkeletonRenderer = follower.GetComponentInParent<SkeletonRenderer>();
if (foundSkeletonRenderer != null)
Debug.Log("BoundingBoxFollower automatically assigned: " + foundSkeletonRenderer.gameObject.name);
else if (Event.current.type == EventType.Repaint)
Debug.Log("No Spine GameObject detected. Make sure to set this GameObject as a child of the Spine GameObject; or set BoundingBoxFollower's 'Skeleton Renderer' field in the inspector.");
skeletonRenderer.objectReferenceValue = foundSkeletonRenderer;
serializedObject.ApplyModifiedProperties();
InitializeEditor();
}
var skeletonRendererValue = skeletonRenderer.objectReferenceValue as SkeletonRenderer;
if (skeletonRendererValue != null && skeletonRendererValue.gameObject == follower.gameObject) {
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) {
EditorGUILayout.HelpBox("It's ideal to add BoundingBoxFollower to a separate child GameObject of the Spine GameObject.", MessageType.Warning);
if (GUILayout.Button(new GUIContent("Move BoundingBoxFollower to new GameObject", Icons.boundingBox), GUILayout.Height(30f))) {
AddBoundingBoxFollowerChild(skeletonRendererValue, follower);
DestroyImmediate(follower);
return;
}
}
EditorGUILayout.Space();
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(skeletonRenderer);
EditorGUILayout.PropertyField(slotName, new GUIContent("Slot"));
if (EditorGUI.EndChangeCheck()) {
serializedObject.ApplyModifiedProperties();
InitializeEditor();
#if !NEW_PREFAB_SYSTEM
if (!isInspectingPrefab)
rebuildRequired = true;
#endif
}
using (new SpineInspectorUtility.LabelWidthScope(150f)) {
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(isTrigger);
bool triggerChanged = EditorGUI.EndChangeCheck();
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(clearStateOnDisable, new GUIContent(clearStateOnDisable.displayName, "Enable this if you are pooling your Spine GameObject"));
bool clearStateChanged = EditorGUI.EndChangeCheck();
if (clearStateChanged || triggerChanged) {
serializedObject.ApplyModifiedProperties();
InitializeEditor();
if (triggerChanged)
foreach (var col in follower.colliderTable.Values)
col.isTrigger = isTrigger.boolValue;
}
}
if (isInspectingPrefab) {
follower.colliderTable.Clear();
follower.nameTable.Clear();
EditorGUILayout.HelpBox("BoundingBoxAttachments cannot be previewed in prefabs.", MessageType.Info);
// How do you prevent components from being saved into the prefab? No such HideFlag. DontSaveInEditor | DontSaveInBuild does not work. DestroyImmediate does not work.
var collider = follower.GetComponent<PolygonCollider2D>();
if (collider != null) Debug.LogWarning("Found BoundingBoxFollower collider components in prefab. These are disposed and regenerated at runtime.");
} else {
using (new SpineInspectorUtility.BoxScope()) {
if (debugIsExpanded = EditorGUILayout.Foldout(debugIsExpanded, "Debug Colliders")) {
EditorGUI.indentLevel++;
EditorGUILayout.LabelField(string.Format("Attachment Names ({0} PolygonCollider2D)", follower.colliderTable.Count));
EditorGUI.BeginChangeCheck();
foreach (var kp in follower.nameTable) {
string attachmentName = kp.Value;
var collider = follower.colliderTable[kp.Key];
bool isPlaceholder = attachmentName != kp.Key.Name;
collider.enabled = EditorGUILayout.ToggleLeft(new GUIContent(!isPlaceholder ? attachmentName : string.Format("{0} [{1}]", attachmentName, kp.Key.Name), isPlaceholder ? Icons.skinPlaceholder : Icons.boundingBox), collider.enabled);
}
sceneRepaintRequired |= EditorGUI.EndChangeCheck();
EditorGUI.indentLevel--;
}
}
}
if (follower.Slot == null)
follower.Initialize(false);
bool hasBoneFollower = follower.GetComponent<BoneFollower>() != null;
if (!hasBoneFollower) {
bool buttonDisabled = follower.Slot == null;
using (new EditorGUI.DisabledGroupScope(buttonDisabled)) {
addBoneFollower |= SpineInspectorUtility.LargeCenteredButton(AddBoneFollowerLabel, true);
EditorGUILayout.Space();
}
}
if (Event.current.type == EventType.Repaint) {
if (addBoneFollower) {
var boneFollower = follower.gameObject.AddComponent<BoneFollower>();
boneFollower.skeletonRenderer = skeletonRendererValue;
boneFollower.SetBone(follower.Slot.Data.BoneData.Name);
addBoneFollower = false;
}
if (sceneRepaintRequired) {
SceneView.RepaintAll();
sceneRepaintRequired = false;
}
if (rebuildRequired) {
follower.Initialize();
rebuildRequired = false;
}
}
}
#region Menus
[MenuItem("CONTEXT/SkeletonRenderer/Add BoundingBoxFollower GameObject")]
static void AddBoundingBoxFollowerChild (MenuCommand command) {
var go = AddBoundingBoxFollowerChild((SkeletonRenderer)command.context);
Undo.RegisterCreatedObjectUndo(go, "Add BoundingBoxFollower");
}
[MenuItem("CONTEXT/SkeletonRenderer/Add all BoundingBoxFollower GameObjects")]
static void AddAllBoundingBoxFollowerChildren (MenuCommand command) {
var objects = AddAllBoundingBoxFollowerChildren((SkeletonRenderer)command.context);
foreach (var go in objects)
Undo.RegisterCreatedObjectUndo(go, "Add BoundingBoxFollower");
}
#endregion
public static GameObject AddBoundingBoxFollowerChild (SkeletonRenderer skeletonRenderer,
BoundingBoxFollower original = null, string name = "BoundingBoxFollower",
string slotName = null) {
var go = EditorInstantiation.NewGameObject(name, true);
go.transform.SetParent(skeletonRenderer.transform, false);
var newFollower = go.AddComponent<BoundingBoxFollower>();
if (original != null) {
newFollower.slotName = original.slotName;
newFollower.isTrigger = original.isTrigger;
newFollower.clearStateOnDisable = original.clearStateOnDisable;
}
if (slotName != null)
newFollower.slotName = slotName;
newFollower.skeletonRenderer = skeletonRenderer;
newFollower.Initialize();
Selection.activeGameObject = go;
EditorGUIUtility.PingObject(go);
return go;
}
public static List<GameObject> AddAllBoundingBoxFollowerChildren (
SkeletonRenderer skeletonRenderer, BoundingBoxFollower original = null) {
List<GameObject> createdGameObjects = new List<GameObject>();
foreach (var skin in skeletonRenderer.Skeleton.Data.Skins) {
var attachments = skin.Attachments;
foreach (var entry in attachments) {
var boundingBoxAttachment = entry.Value as BoundingBoxAttachment;
if (boundingBoxAttachment == null)
continue;
int slotIndex = entry.Key.SlotIndex;
var slot = skeletonRenderer.Skeleton.Slots.Items[slotIndex];
string slotName = slot.Data.Name;
GameObject go = AddBoundingBoxFollowerChild(skeletonRenderer,
original, boundingBoxAttachment.Name, slotName);
var boneFollower = go.AddComponent<BoneFollower>();
boneFollower.skeletonRenderer = skeletonRenderer;
boneFollower.SetBone(slot.Data.BoneData.Name);
createdGameObjects.Add(go);
}
}
return createdGameObjects;
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 670a3cefa3853bd48b5da53a424fd542
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@ -0,0 +1,188 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System.Collections;
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
using Editor = UnityEditor.Editor;
using Event = UnityEngine.Event;
[CustomEditor(typeof(PointFollower)), CanEditMultipleObjects]
public class PointFollowerInspector : Editor {
SerializedProperty slotName, pointAttachmentName, skeletonRenderer, followZPosition, followBoneRotation, followSkeletonFlip;
PointFollower targetPointFollower;
bool needsReset;
#region Context Menu Item
[MenuItem("CONTEXT/SkeletonRenderer/Add PointFollower GameObject")]
static void AddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonRenderer = cmd.context as SkeletonRenderer;
var go = EditorInstantiation.NewGameObject("PointFollower", true);
var t = go.transform;
t.SetParent(skeletonRenderer.transform);
t.localPosition = Vector3.zero;
var f = go.AddComponent<PointFollower>();
f.skeletonRenderer = skeletonRenderer;
EditorGUIUtility.PingObject(t);
Undo.RegisterCreatedObjectUndo(go, "Add PointFollower");
}
// Validate
[MenuItem("CONTEXT/SkeletonRenderer/Add PointFollower GameObject", true)]
static bool ValidateAddBoneFollowerGameObject (MenuCommand cmd) {
var skeletonRenderer = cmd.context as SkeletonRenderer;
return skeletonRenderer.valid;
}
#endregion
void OnEnable () {
skeletonRenderer = serializedObject.FindProperty("skeletonRenderer");
slotName = serializedObject.FindProperty("slotName");
pointAttachmentName = serializedObject.FindProperty("pointAttachmentName");
targetPointFollower = (PointFollower)target;
if (targetPointFollower.skeletonRenderer != null)
targetPointFollower.skeletonRenderer.Initialize(false);
if (!targetPointFollower.IsValid || needsReset) {
targetPointFollower.Initialize();
targetPointFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
}
public void OnSceneGUI () {
var tbf = target as PointFollower;
var skeletonRendererComponent = tbf.skeletonRenderer;
if (skeletonRendererComponent == null)
return;
var skeleton = skeletonRendererComponent.skeleton;
var skeletonTransform = skeletonRendererComponent.transform;
if (string.IsNullOrEmpty(pointAttachmentName.stringValue)) {
// Draw all active PointAttachments in the current skin
var currentSkin = skeleton.Skin;
if (currentSkin != skeleton.Data.DefaultSkin) DrawPointsInSkin(skeleton.Data.DefaultSkin, skeleton, skeletonTransform);
if (currentSkin != null) DrawPointsInSkin(currentSkin, skeleton, skeletonTransform);
} else {
int slotIndex = skeleton.FindSlotIndex(slotName.stringValue);
if (slotIndex >= 0) {
var slot = skeleton.Slots.Items[slotIndex];
var point = skeleton.GetAttachment(slotIndex, pointAttachmentName.stringValue) as PointAttachment;
if (point != null) {
DrawPointAttachmentWithLabel(point, slot.Bone, skeletonTransform);
}
}
}
}
static void DrawPointsInSkin (Skin skin, Skeleton skeleton, Transform transform) {
foreach (var skinEntry in skin.Attachments) {
var attachment = skinEntry.Value as PointAttachment;
if (attachment != null) {
var skinKey = (Skin.SkinEntry)skinEntry.Key;
var slot = skeleton.Slots.Items[skinKey.SlotIndex];
DrawPointAttachmentWithLabel(attachment, slot.Bone, transform);
}
}
}
static void DrawPointAttachmentWithLabel (PointAttachment point, Bone bone, Transform transform) {
Vector3 labelOffset = new Vector3(0f, -0.2f, 0f);
SpineHandles.DrawPointAttachment(bone, point, transform);
Handles.Label(labelOffset + point.GetWorldPosition(bone, transform), point.Name, SpineHandles.PointNameStyle);
}
override public void OnInspectorGUI () {
if (serializedObject.isEditingMultipleObjects) {
if (needsReset) {
needsReset = false;
foreach (var o in targets) {
var bf = (BoneFollower)o;
bf.Initialize();
bf.LateUpdate();
}
SceneView.RepaintAll();
}
EditorGUI.BeginChangeCheck();
DrawDefaultInspector();
needsReset |= EditorGUI.EndChangeCheck();
return;
}
if (needsReset && Event.current.type == EventType.Layout) {
targetPointFollower.Initialize();
targetPointFollower.LateUpdate();
needsReset = false;
SceneView.RepaintAll();
}
serializedObject.Update();
DrawDefaultInspector();
// Find Renderer
if (skeletonRenderer.objectReferenceValue == null) {
SkeletonRenderer parentRenderer = targetPointFollower.GetComponentInParent<SkeletonRenderer>();
if (parentRenderer != null && parentRenderer.gameObject != targetPointFollower.gameObject) {
skeletonRenderer.objectReferenceValue = parentRenderer;
Debug.Log("Inspector automatically assigned PointFollower.SkeletonRenderer");
}
}
var skeletonRendererReference = skeletonRenderer.objectReferenceValue as SkeletonRenderer;
if (skeletonRendererReference != null) {
if (skeletonRendererReference.gameObject == targetPointFollower.gameObject) {
skeletonRenderer.objectReferenceValue = null;
EditorUtility.DisplayDialog("Invalid assignment.", "PointFollower can only follow a skeleton on a separate GameObject.\n\nCreate a new GameObject for your PointFollower, or choose a SkeletonRenderer from a different GameObject.", "Ok");
}
}
if (!targetPointFollower.IsValid) {
needsReset = true;
}
var current = Event.current;
bool wasUndo = (current.type == EventType.ValidateCommand && current.commandName == "UndoRedoPerformed");
if (wasUndo)
targetPointFollower.Initialize();
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@ -0,0 +1,139 @@
/******************************************************************************
* 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.
*****************************************************************************/
using UnityEditor;
using UnityEngine;
using Spine;
namespace Spine.Unity.Editor {
[CustomEditor(typeof(SkeletonAnimation))]
[CanEditMultipleObjects]
public class SkeletonAnimationInspector : SkeletonRendererInspector {
protected SerializedProperty animationName, loop, timeScale, autoReset;
protected bool wasAnimationParameterChanged = false;
protected bool requireRepaint;
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.");
protected override void OnEnable () {
base.OnEnable();
animationName = serializedObject.FindProperty("_animationName");
loop = serializedObject.FindProperty("loop");
timeScale = serializedObject.FindProperty("timeScale");
}
protected override void DrawInspectorGUI (bool multi) {
base.DrawInspectorGUI(multi);
if (!TargetIsValid) return;
bool sameData = SpineInspectorUtility.TargetsUseSameData(serializedObject);
foreach (var 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 (var o in targets) {
var component = o as SkeletonAnimation;
component.timeScale = Mathf.Max(component.timeScale, 0);
}
EditorGUILayout.Space();
SkeletonRootMotionParameter();
serializedObject.ApplyModifiedProperties();
if (!isInspectingPrefab) {
if (requireRepaint) {
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
requireRepaint = false;
}
}
}
protected void TrySetAnimation (SkeletonAnimation skeletonAnimation) {
if (skeletonAnimation == null) return;
if (!skeletonAnimation.valid || skeletonAnimation.AnimationState == null)
return;
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;
var skeleton = skeletonAnimation.Skeleton;
var state = skeletonAnimation.AnimationState;
if (!Application.isPlaying) {
if (state != null) state.ClearTrack(0);
skeleton.SetToSetupPose();
}
Spine.Animation animationToUse = skeleton.Data.FindAnimation(animationName.stringValue);
if (!Application.isPlaying) {
if (animationToUse != null) {
skeletonAnimation.AnimationState.SetAnimation(0, animationToUse, loop.boolValue);
}
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;
}
}
}
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 39fbfef61034ca045b5aa80088e1e8a4
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,159 @@
/******************************************************************************
* 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.
*****************************************************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Spine.Unity.Examples;
namespace Spine.Unity.Editor {
// This script is not intended for use with code. See spine-unity documentation page for additional information.
[CustomEditor(typeof(SkeletonGraphicCustomMaterials))]
public class SkeletonGraphicCustomMaterialsInspector : UnityEditor.Editor {
List<SkeletonGraphicCustomMaterials.AtlasMaterialOverride> componentCustomMaterialOverrides, _customMaterialOverridesPrev;
List<SkeletonGraphicCustomMaterials.AtlasTextureOverride> componentCustomTextureOverrides, _customTextureOverridesPrev;
SkeletonGraphicCustomMaterials component;
const BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
MethodInfo RemoveCustomMaterialOverrides, RemoveCustomTextureOverrides, SetCustomMaterialOverrides, SetCustomTextureOverrides;
#region SkeletonGraphic context menu
[MenuItem("CONTEXT/SkeletonGraphic/Add Basic Serialized Custom Materials")]
static void AddSkeletonGraphicCustomMaterials (MenuCommand menuCommand) {
var skeletonGraphic = (SkeletonGraphic)menuCommand.context;
var newComponent = skeletonGraphic.gameObject.AddComponent<SkeletonGraphicCustomMaterials>();
Undo.RegisterCreatedObjectUndo(newComponent, "Add Basic Serialized Custom Materials");
}
[MenuItem("CONTEXT/SkeletonGraphic/Add Basic Serialized Custom Materials", true)]
static bool AddSkeletonGraphicCustomMaterials_Validate (MenuCommand menuCommand) {
var skeletonGraphic = (SkeletonGraphic)menuCommand.context;
return (skeletonGraphic.GetComponent<SkeletonGraphicCustomMaterials>() == null);
}
#endregion
void OnEnable () {
Type cm = typeof(SkeletonGraphicCustomMaterials);
RemoveCustomMaterialOverrides = cm.GetMethod("RemoveCustomMaterialOverrides", PrivateInstance);
RemoveCustomTextureOverrides = cm.GetMethod("RemoveCustomTextureOverrides", PrivateInstance);
SetCustomMaterialOverrides = cm.GetMethod("SetCustomMaterialOverrides", PrivateInstance);
SetCustomTextureOverrides = cm.GetMethod("SetCustomTextureOverrides", PrivateInstance);
}
public override void OnInspectorGUI () {
component = (SkeletonGraphicCustomMaterials)target;
var skeletonGraphic = component.skeletonGraphic;
// Draw the default inspector
DrawDefaultInspector();
if (serializedObject.isEditingMultipleObjects)
return;
if (componentCustomMaterialOverrides == null) {
Type cm = typeof(SkeletonGraphicCustomMaterials);
componentCustomMaterialOverrides = cm.GetField("customMaterialOverrides", PrivateInstance).GetValue(component) as List<SkeletonGraphicCustomMaterials.AtlasMaterialOverride>;
componentCustomTextureOverrides = cm.GetField("customTextureOverrides", PrivateInstance).GetValue(component) as List<SkeletonGraphicCustomMaterials.AtlasTextureOverride>;
if (componentCustomMaterialOverrides == null) {
Debug.Log("Reflection failed.");
return;
}
}
// Fill with current values at start
if (_customMaterialOverridesPrev == null || _customTextureOverridesPrev == null) {
_customMaterialOverridesPrev = CopyList(componentCustomMaterialOverrides);
_customTextureOverridesPrev = CopyList(componentCustomTextureOverrides);
}
// Compare new values with saved. If change is detected:
// store new values, restore old values, remove overrides, restore new values, restore overrides.
// 1. Store new values
var customMaterialOverridesNew = CopyList(componentCustomMaterialOverrides);
var customTextureOverridesNew = CopyList(componentCustomTextureOverrides);
// Detect changes
if (!_customMaterialOverridesPrev.SequenceEqual(customMaterialOverridesNew) ||
!_customTextureOverridesPrev.SequenceEqual(customTextureOverridesNew)) {
// 2. Restore old values
componentCustomMaterialOverrides.Clear();
componentCustomTextureOverrides.Clear();
componentCustomMaterialOverrides.AddRange(_customMaterialOverridesPrev);
componentCustomTextureOverrides.AddRange(_customTextureOverridesPrev);
// 3. Remove overrides
RemoveCustomMaterials();
// 4. Restore new values
componentCustomMaterialOverrides.Clear();
componentCustomTextureOverrides.Clear();
componentCustomMaterialOverrides.AddRange(customMaterialOverridesNew);
componentCustomTextureOverrides.AddRange(customTextureOverridesNew);
// 5. Restore overrides
SetCustomMaterials();
if (skeletonGraphic != null)
skeletonGraphic.LateUpdate();
}
_customMaterialOverridesPrev = CopyList(componentCustomMaterialOverrides);
_customTextureOverridesPrev = CopyList(componentCustomTextureOverrides);
if (SpineInspectorUtility.LargeCenteredButton(SpineInspectorUtility.TempContent("Clear and Reapply Changes", tooltip: "Removes all non-serialized overrides in the SkeletonGraphic and reapplies the overrides on this component."))) {
if (skeletonGraphic != null) {
skeletonGraphic.CustomMaterialOverride.Clear();
skeletonGraphic.CustomTextureOverride.Clear();
RemoveCustomMaterials();
SetCustomMaterials();
skeletonGraphic.LateUpdate();
}
}
}
void RemoveCustomMaterials () {
RemoveCustomMaterialOverrides.Invoke(component, null);
RemoveCustomTextureOverrides.Invoke(component, null);
}
void SetCustomMaterials () {
SetCustomMaterialOverrides.Invoke(component, null);
SetCustomTextureOverrides.Invoke(component, null);
}
static List<T> CopyList<T> (List<T> list) {
return list.GetRange(0, list.Count);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 349bf125947e3aa4bb78690fec69ea17
timeCreated: 1588789940
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,436 @@
/******************************************************************************
* 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
#endif
using UnityEngine;
using UnityEditor;
namespace Spine.Unity.Editor {
using Icons = SpineEditorUtilities.Icons;
[CustomEditor(typeof(SkeletonGraphic))]
[CanEditMultipleObjects]
public class SkeletonGraphicInspector : UnityEditor.Editor {
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
const string ReloadButtonString = "Reload";
protected GUIContent SkeletonDataAssetLabel;
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; } }
SerializedProperty material, color;
SerializedProperty skeletonDataAsset, initialSkinName;
SerializedProperty startingAnimation, startingLoop, timeScale, freeze, updateWhenInvisible, unscaledTime, tintBlack;
SerializedProperty initialFlipX, initialFlipY;
SerializedProperty meshGeneratorSettings;
SerializedProperty allowMultipleCanvasRenderers, separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation;
SerializedProperty raycastTarget;
SkeletonGraphic thisSkeletonGraphic;
protected bool isInspectingPrefab;
protected bool slotsReapplyRequired = false;
protected bool forceReloadQueued = false;
protected bool TargetIsValid {
get {
if (serializedObject.isEditingMultipleObjects) {
foreach (var o in targets) {
var component = (SkeletonGraphic)o;
if (!component.IsValid)
return false;
}
return true;
}
else {
var component = (SkeletonGraphic)target;
return component.IsValid;
}
}
}
void OnEnable () {
#if NEW_PREFAB_SYSTEM
isInspectingPrefab = false;
#else
isInspectingPrefab = (PrefabUtility.GetPrefabType(target) == PrefabType.Prefab);
#endif
SpineEditorUtilities.ConfirmInitialization();
// Labels
SkeletonDataAssetLabel = new GUIContent("SkeletonData Asset", Icons.spine);
var so = this.serializedObject;
thisSkeletonGraphic = target as SkeletonGraphic;
// MaskableGraphic
material = so.FindProperty("m_Material");
color = so.FindProperty("m_Color");
raycastTarget = so.FindProperty("m_RaycastTarget");
// SkeletonRenderer
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");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
meshGeneratorSettings = so.FindProperty("meshGenerator").FindPropertyRelative("settings");
meshGeneratorSettings.isExpanded = SkeletonRendererInspector.advancedFoldout;
allowMultipleCanvasRenderers = so.FindProperty("allowMultipleCanvasRenderers");
updateSeparatorPartLocation = so.FindProperty("updateSeparatorPartLocation");
enableSeparatorSlots = so.FindProperty("enableSeparatorSlots");
separatorSlotNames = so.FindProperty("separatorSlotNames");
separatorSlotNames.isExpanded = true;
}
public override void OnInspectorGUI () {
if (UnityEngine.Event.current.type == EventType.Layout) {
if (forceReloadQueued) {
forceReloadQueued = false;
foreach (var c in targets) {
SpineEditorUtilities.ReloadSkeletonDataAssetAndComponent(c as SkeletonGraphic);
}
}
else {
foreach (var c in targets) {
var 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;
}
EditorGUILayout.PropertyField(material);
EditorGUILayout.PropertyField(color);
if (thisSkeletonGraphic.skeletonDataAsset == null) {
EditorGUILayout.HelpBox("You need to assign a SkeletonDataAsset first.", MessageType.Info);
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
return;
}
string errorMessage = null;
if (SpineEditorUtilities.Preferences.componentMaterialWarning &&
MaterialChecks.IsMaterialSetupProblematic(thisSkeletonGraphic, ref errorMessage)) {
EditorGUILayout.HelpBox(errorMessage, MessageType.Error, true);
}
bool isSingleRendererOnly = (!allowMultipleCanvasRenderers.hasMultipleDifferentValues && allowMultipleCanvasRenderers.boolValue == false);
bool isSeparationEnabledButNotMultipleRenderers =
isSingleRendererOnly && (!enableSeparatorSlots.hasMultipleDifferentValues && enableSeparatorSlots.boolValue == true);
bool meshRendersIncorrectlyWithSingleRenderer =
isSingleRendererOnly && SkeletonHasMultipleSubmeshes();
if (isSeparationEnabledButNotMultipleRenderers || meshRendersIncorrectlyWithSingleRenderer)
meshGeneratorSettings.isExpanded = true;
using (new SpineInspectorUtility.BoxScope()) {
EditorGUILayout.PropertyField(meshGeneratorSettings, SpineInspectorUtility.TempContent("Advanced..."), includeChildren: true);
SkeletonRendererInspector.advancedFoldout = meshGeneratorSettings.isExpanded;
if (meshGeneratorSettings.isExpanded) {
EditorGUILayout.Space();
using (new SpineInspectorUtility.IndentScope()) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PropertyField(allowMultipleCanvasRenderers, SpineInspectorUtility.TempContent("Multiple CanvasRenderers"));
if (GUILayout.Button(new GUIContent("Trim Renderers", "Remove currently unused CanvasRenderer GameObjects. These will be regenerated whenever needed."),
EditorStyles.miniButton, GUILayout.Width(100f))) {
foreach (var skeletonGraphic in targets) {
((SkeletonGraphic)skeletonGraphic).TrimRenderers();
}
}
EditorGUILayout.EndHorizontal();
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. You\n" +
"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.Space();
SeparatorsField(separatorSlotNames, enableSeparatorSlots, updateSeparatorPartLocation);
}
}
EditorGUILayout.Space();
EditorGUILayout.PropertyField(initialSkinName);
{
var 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"));
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Animation", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(startingAnimation);
EditorGUILayout.PropertyField(startingLoop);
EditorGUILayout.PropertyField(timeScale);
EditorGUILayout.PropertyField(unscaledTime, SpineInspectorUtility.TempContent(unscaledTime.displayName, tooltip: "If checked, this will use Time.unscaledDeltaTime to make this update independent of game Time.timeScale. Instance SkeletonGraphic.timeScale will still be applied."));
EditorGUILayout.Space();
EditorGUILayout.PropertyField(freeze);
EditorGUILayout.Space();
SkeletonRendererInspector.SkeletonRootMotionParameter(targets);
EditorGUILayout.Space();
EditorGUILayout.LabelField("UI", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(raycastTarget);
EditorGUILayout.BeginHorizontal(GUILayout.Height(EditorGUIUtility.singleLineHeight + 5));
EditorGUILayout.PrefixLabel("Match RectTransform with Mesh");
if (GUILayout.Button("Match", EditorStyles.miniButton, GUILayout.Width(65f))) {
foreach (var skeletonGraphic in targets) {
MatchRectTransformWithBounds((SkeletonGraphic)skeletonGraphic);
}
}
EditorGUILayout.EndHorizontal();
if (TargetIsValid && !isInspectingPrefab) {
EditorGUILayout.Space();
if (SpineInspectorUtility.CenteredButton(new GUIContent("Add Skeleton Utility", Icons.skeletonUtility), 21, true, 200f))
foreach (var t in targets) {
var 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 (var target in targets) {
var skeletonGraphic = (SkeletonGraphic)target;
skeletonGraphic.ReapplySeparatorSlotNames();
skeletonGraphic.LateUpdate();
SceneView.RepaintAll();
}
slotsReapplyRequired = false;
}
}
protected bool SkeletonHasMultipleSubmeshes () {
foreach (var target in targets) {
var skeletonGraphic = (SkeletonGraphic)target;
if (skeletonGraphic.HasMultipleSubmeshInstructions())
return true;
}
return false;
}
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 void SeparatorsField (SerializedProperty separatorSlotNames, SerializedProperty enableSeparatorSlots,
SerializedProperty updateSeparatorPartLocation) {
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++) {
int index = skeleton.FindSlotIndex(separatorSlotNames.GetArrayElementAtIndex(i).stringValue);
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);
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."));
}
}
#region Menus
[MenuItem("CONTEXT/SkeletonGraphic/Match RectTransform with Mesh Bounds")]
static void MatchRectTransformWithBounds (MenuCommand command) {
var skeletonGraphic = (SkeletonGraphic)command.context;
MatchRectTransformWithBounds(skeletonGraphic);
}
static void MatchRectTransformWithBounds (SkeletonGraphic skeletonGraphic) {
if (!skeletonGraphic.MatchRectTransformWithBounds())
Debug.Log("Mesh was not previously generated.");
}
[MenuItem("GameObject/Spine/SkeletonGraphic (UnityUI)", false, 15)]
static public void SkeletonGraphicCreateMenuItem () {
var parentGameObject = Selection.activeObject as GameObject;
var parentTransform = parentGameObject == null ? null : parentGameObject.GetComponent<RectTransform>();
if (parentTransform == null)
Debug.LogWarning("Your new SkeletonGraphic will not be visible until it is placed under a Canvas");
var gameObject = NewSkeletonGraphicGameObject("New SkeletonGraphic");
gameObject.transform.SetParent(parentTransform, false);
EditorUtility.FocusProjectWindow();
Selection.activeObject = gameObject;
EditorGUIUtility.PingObject(Selection.activeObject);
}
// SpineEditorUtilities.InstantiateDelegate. Used by drag and drop.
public static Component SpawnSkeletonGraphicFromDrop (SkeletonDataAsset data) {
return InstantiateSkeletonGraphic(data);
}
public static SkeletonGraphic InstantiateSkeletonGraphic (SkeletonDataAsset skeletonDataAsset, string skinName) {
return InstantiateSkeletonGraphic(skeletonDataAsset, skeletonDataAsset.GetSkeletonData(true).FindSkin(skinName));
}
public static SkeletonGraphic InstantiateSkeletonGraphic (SkeletonDataAsset skeletonDataAsset, Skin skin = null) {
string spineGameObjectName = string.Format("SkeletonGraphic ({0})", skeletonDataAsset.name.Replace("_SkeletonData", ""));
var go = NewSkeletonGraphicGameObject(spineGameObjectName);
var graphic = go.GetComponent<SkeletonGraphic>();
graphic.skeletonDataAsset = skeletonDataAsset;
SkeletonData data = skeletonDataAsset.GetSkeletonData(true);
if (data == null) {
for (int i = 0; i < skeletonDataAsset.atlasAssets.Length; i++) {
string reloadAtlasPath = AssetDatabase.GetAssetPath(skeletonDataAsset.atlasAssets[i]);
skeletonDataAsset.atlasAssets[i] = (AtlasAssetBase)AssetDatabase.LoadAssetAtPath(reloadAtlasPath, typeof(AtlasAssetBase));
}
data = skeletonDataAsset.GetSkeletonData(true);
}
skin = skin ?? data.DefaultSkin ?? data.Skins.Items[0];
graphic.MeshGenerator.settings.zSpacing = SpineEditorUtilities.Preferences.defaultZSpacing;
graphic.startingLoop = SpineEditorUtilities.Preferences.defaultInstantiateLoop;
graphic.Initialize(false);
if (skin != null) graphic.Skeleton.SetSkin(skin);
graphic.initialSkinName = skin.Name;
graphic.Skeleton.UpdateWorldTransform();
graphic.UpdateMesh();
return graphic;
}
static GameObject NewSkeletonGraphicGameObject (string gameObjectName) {
var go = EditorInstantiation.NewGameObject(gameObjectName, true, typeof(RectTransform), typeof(CanvasRenderer), typeof(SkeletonGraphic));
var graphic = go.GetComponent<SkeletonGraphic>();
graphic.material = SkeletonGraphicInspector.DefaultSkeletonGraphicMaterial;
return go;
}
public static Material DefaultSkeletonGraphicMaterial {
get {
var guids = AssetDatabase.FindAssets("SkeletonGraphicDefault t:material");
if (guids.Length <= 0) return null;
var firstAssetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
if (string.IsNullOrEmpty(firstAssetPath)) return null;
var firstMaterial = AssetDatabase.LoadAssetAtPath<Material>(firstAssetPath);
return firstMaterial;
}
}
#endregion
}
}

View File

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

View File

@ -0,0 +1,153 @@
/******************************************************************************
* 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.
*****************************************************************************/
// Contributed by: Mitch Thompson
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
[CustomEditor(typeof(SkeletonMecanim))]
[CanEditMultipleObjects]
public class SkeletonMecanimInspector : SkeletonRendererInspector {
public static bool mecanimSettingsFoldout;
protected SerializedProperty autoReset;
protected SerializedProperty useCustomMixMode;
protected SerializedProperty layerMixModes;
protected SerializedProperty layerBlendModes;
protected override void OnEnable () {
base.OnEnable();
SerializedProperty mecanimTranslator = serializedObject.FindProperty("translator");
autoReset = mecanimTranslator.FindPropertyRelative("autoReset");
useCustomMixMode = mecanimTranslator.FindPropertyRelative("useCustomMixMode");
layerMixModes = mecanimTranslator.FindPropertyRelative("layerMixModes");
layerBlendModes = mecanimTranslator.FindPropertyRelative("layerBlendModes");
}
protected override void DrawInspectorGUI (bool multi) {
AddRootMotionComponentIfEnabled();
base.DrawInspectorGUI(multi);
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(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."));
if (useCustomMixMode.hasMultipleDifferentValues || useCustomMixMode.boolValue == true) {
DrawLayerSettings();
EditorGUILayout.Space();
}
}
}
}
protected void AddRootMotionComponentIfEnabled () {
foreach (var t in targets) {
var component = t as Component;
var animator = component.GetComponent<Animator>();
if (animator != null && animator.applyRootMotion) {
if (component.GetComponent<SkeletonMecanimRootMotion>() == null) {
component.gameObject.AddComponent<SkeletonMecanimRootMotion>();
}
}
}
}
protected void DrawLayerSettings () {
string[] layerNames = GetLayerNames();
float widthLayerColumn = 140;
float widthMixColumn = 84;
using (new GUILayout.HorizontalScope()) {
var rect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, EditorGUIUtility.singleLineHeight);
rect.width = widthLayerColumn;
EditorGUI.LabelField(rect, SpineInspectorUtility.TempContent("Mecanim Layer"), EditorStyles.boldLabel);
var savedIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
rect.position += new Vector2(rect.width, 0);
rect.width = widthMixColumn;
EditorGUI.LabelField(rect, SpineInspectorUtility.TempContent("Mix Mode"), EditorStyles.boldLabel);
EditorGUI.indentLevel = savedIndent;
}
using (new SpineInspectorUtility.IndentScope()) {
int layerCount = layerMixModes.arraySize;
for (int i = 0; i < layerCount; ++i) {
using (new GUILayout.HorizontalScope()) {
string layerName = i < layerNames.Length ? layerNames[i] : ("Layer " + i);
var rect = GUILayoutUtility.GetRect(EditorGUIUtility.currentViewWidth, EditorGUIUtility.singleLineHeight);
rect.width = widthLayerColumn;
EditorGUI.PrefixLabel(rect, SpineInspectorUtility.TempContent(layerName));
var savedIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
var mixMode = layerMixModes.GetArrayElementAtIndex(i);
rect.position += new Vector2(rect.width, 0);
rect.width = widthMixColumn;
EditorGUI.PropertyField(rect, mixMode, GUIContent.none);
EditorGUI.indentLevel = savedIndent;
}
}
}
}
protected string[] GetLayerNames () {
int maxLayerCount = 0;
int maxIndex = 0;
for (int i = 0; i < targets.Length; ++i) {
var skeletonMecanim = ((SkeletonMecanim)targets[i]);
int count = skeletonMecanim.Translator.MecanimLayerCount;
if (count > maxLayerCount) {
maxLayerCount = count;
maxIndex = i;
}
}
if (maxLayerCount == 0)
return new string[0];
var skeletonMecanimMaxLayers = ((SkeletonMecanim)targets[maxIndex]);
return skeletonMecanimMaxLayers.Translator.MecanimLayerNames;
}
}
}

View File

@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 6a9ca5213a3a4614c9a9f2e60909bc33
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,81 @@
/******************************************************************************
* 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.
*****************************************************************************/
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
[CustomEditor(typeof(SkeletonMecanimRootMotion))]
[CanEditMultipleObjects]
public class SkeletonMecanimRootMotionInspector : SkeletonRootMotionBaseInspector {
protected SerializedProperty mecanimLayerFlags;
protected GUIContent mecanimLayersLabel;
protected override void OnEnable () {
base.OnEnable();
mecanimLayerFlags = serializedObject.FindProperty("mecanimLayerFlags");
mecanimLayersLabel = new UnityEngine.GUIContent("Mecanim Layers", "Mecanim layers to apply root motion at. Defaults to the first Mecanim layer.");
}
override public void OnInspectorGUI () {
base.MainPropertyFields();
MecanimLayerMaskPropertyField();
base.OptionalPropertyFields();
serializedObject.ApplyModifiedProperties();
}
protected string[] GetLayerNames () {
int maxLayerCount = 0;
int maxIndex = 0;
for (int i = 0; i < targets.Length; ++i) {
var skeletonMecanim = ((SkeletonMecanimRootMotion)targets[i]).SkeletonMecanim;
int count = skeletonMecanim.Translator.MecanimLayerCount;
if (count > maxLayerCount) {
maxLayerCount = count;
maxIndex = i;
}
}
if (maxLayerCount == 0)
return new string[0];
var skeletonMecanimMaxLayers = ((SkeletonMecanimRootMotion)targets[maxIndex]).SkeletonMecanim;
return skeletonMecanimMaxLayers.Translator.MecanimLayerNames;
}
protected void MecanimLayerMaskPropertyField () {
string[] layerNames = GetLayerNames();
if (layerNames.Length > 0)
mecanimLayerFlags.intValue = EditorGUILayout.MaskField(
mecanimLayersLabel, mecanimLayerFlags.intValue, layerNames);
}
}
}

View File

@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4613924c50d66cf458f0db803776dd2f
timeCreated: 1593175106
licenseType: Pro
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,165 @@
/******************************************************************************
* 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.
*****************************************************************************/
#define SPINE_OPTIONAL_MATERIALOVERRIDE
// Contributed by: Lost Polygon
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using Spine.Unity.Examples;
namespace Spine.Unity.Editor {
// This script is not intended for use with code. See the readme.txt file in SkeletonRendererCustomMaterials folder to learn more.
[CustomEditor(typeof(SkeletonRendererCustomMaterials))]
public class SkeletonRendererCustomMaterialsInspector : UnityEditor.Editor {
List<SkeletonRendererCustomMaterials.AtlasMaterialOverride> componentCustomMaterialOverrides, _customMaterialOverridesPrev;
List<SkeletonRendererCustomMaterials.SlotMaterialOverride> componentCustomSlotMaterials, _customSlotMaterialsPrev;
SkeletonRendererCustomMaterials component;
const BindingFlags PrivateInstance = BindingFlags.Instance | BindingFlags.NonPublic;
MethodInfo RemoveCustomMaterialOverrides, RemoveCustomSlotMaterials, SetCustomMaterialOverrides, SetCustomSlotMaterials;
#region SkeletonRenderer context menu
[MenuItem("CONTEXT/SkeletonRenderer/Add Basic Serialized Custom Materials")]
static void AddSkeletonRendererCustomMaterials (MenuCommand menuCommand) {
var skeletonRenderer = (SkeletonRenderer)menuCommand.context;
var newComponent = skeletonRenderer.gameObject.AddComponent<SkeletonRendererCustomMaterials>();
Undo.RegisterCreatedObjectUndo(newComponent, "Add Basic Serialized Custom Materials");
}
[MenuItem("CONTEXT/SkeletonRenderer/Add Basic Serialized Custom Materials", true)]
static bool AddSkeletonRendererCustomMaterials_Validate (MenuCommand menuCommand) {
var skeletonRenderer = (SkeletonRenderer)menuCommand.context;
return (skeletonRenderer.GetComponent<SkeletonRendererCustomMaterials>() == null);
}
#endregion
void OnEnable () {
Type cm = typeof(SkeletonRendererCustomMaterials);
RemoveCustomMaterialOverrides = cm.GetMethod("RemoveCustomMaterialOverrides", PrivateInstance);
RemoveCustomSlotMaterials = cm.GetMethod("RemoveCustomSlotMaterials", PrivateInstance);
SetCustomMaterialOverrides = cm.GetMethod("SetCustomMaterialOverrides", PrivateInstance);
SetCustomSlotMaterials = cm.GetMethod("SetCustomSlotMaterials", PrivateInstance);
}
public override void OnInspectorGUI () {
component = (SkeletonRendererCustomMaterials)target;
var skeletonRenderer = component.skeletonRenderer;
// Draw the default inspector
DrawDefaultInspector();
if (serializedObject.isEditingMultipleObjects)
return;
if (componentCustomMaterialOverrides == null) {
Type cm = typeof(SkeletonRendererCustomMaterials);
componentCustomMaterialOverrides = cm.GetField("customMaterialOverrides", PrivateInstance).GetValue(component) as List<SkeletonRendererCustomMaterials.AtlasMaterialOverride>;
componentCustomSlotMaterials = cm.GetField("customSlotMaterials", PrivateInstance).GetValue(component) as List<SkeletonRendererCustomMaterials.SlotMaterialOverride>;
if (componentCustomMaterialOverrides == null) {
Debug.Log("Reflection failed.");
return;
}
}
// Fill with current values at start
if (_customMaterialOverridesPrev == null || _customSlotMaterialsPrev == null) {
_customMaterialOverridesPrev = CopyList(componentCustomMaterialOverrides);
_customSlotMaterialsPrev = CopyList(componentCustomSlotMaterials);
}
// Compare new values with saved. If change is detected:
// store new values, restore old values, remove overrides, restore new values, restore overrides.
// 1. Store new values
var customMaterialOverridesNew = CopyList(componentCustomMaterialOverrides);
var customSlotMaterialsNew = CopyList(componentCustomSlotMaterials);
// Detect changes
if (!_customMaterialOverridesPrev.SequenceEqual(customMaterialOverridesNew) ||
!_customSlotMaterialsPrev.SequenceEqual(customSlotMaterialsNew)) {
// 2. Restore old values
componentCustomMaterialOverrides.Clear();
componentCustomSlotMaterials.Clear();
componentCustomMaterialOverrides.AddRange(_customMaterialOverridesPrev);
componentCustomSlotMaterials.AddRange(_customSlotMaterialsPrev);
// 3. Remove overrides
RemoveCustomMaterials();
// 4. Restore new values
componentCustomMaterialOverrides.Clear();
componentCustomSlotMaterials.Clear();
componentCustomMaterialOverrides.AddRange(customMaterialOverridesNew);
componentCustomSlotMaterials.AddRange(customSlotMaterialsNew);
// 5. Restore overrides
SetCustomMaterials();
if (skeletonRenderer != null)
skeletonRenderer.LateUpdate();
}
_customMaterialOverridesPrev = CopyList(componentCustomMaterialOverrides);
_customSlotMaterialsPrev = CopyList(componentCustomSlotMaterials);
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();
skeletonRenderer.LateUpdate();
}
}
}
void RemoveCustomMaterials () {
RemoveCustomMaterialOverrides.Invoke(component, null);
RemoveCustomSlotMaterials.Invoke(component, null);
}
void SetCustomMaterials () {
SetCustomMaterialOverrides.Invoke(component, null);
SetCustomSlotMaterials.Invoke(component, null);
}
static List<T> CopyList<T> (List<T> list) {
return list.GetRange(0, list.Count);
}
}
}

View File

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

View File

@ -0,0 +1,593 @@
/******************************************************************************
* 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 System.Collections.Generic;
using UnityEngine;
using System.Reflection;
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;
const string SeparatorSlotNamesFieldName = "separatorSlotNames";
protected SerializedProperty skeletonDataAsset, initialSkinName;
protected SerializedProperty initialFlipX, initialFlipY;
protected SerializedProperty updateWhenInvisible, singleSubmesh, separatorSlotNames, clearStateOnDisable, immutableTriangles, fixDrawOrder;
protected SerializedProperty normals, tangents, zSpacing, pmaVertexColors, tintBlack; // MeshGenerator settings
protected SerializedProperty maskInteraction;
protected SerializedProperty maskMaterialsNone, maskMaterialsInside, maskMaterialsOutside;
protected SpineInspectorUtility.SerializedSortingProperties sortingProperties;
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, UpdateWhenInvisibleLabel, SingleSubmeshLabel, FixDrawOrderLabel;
protected GUIContent NormalsLabel, TangentsLabel, MaskInteractionLabel;
protected GUIContent MaskMaterialsHeadingLabel, MaskMaterialsNoneLabel, MaskMaterialsInsideLabel, MaskMaterialsOutsideLabel;
protected GUIContent SetMaterialButtonLabel, ClearMaterialButtonLabel, DeleteMaterialButtonLabel;
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 (var o in targets) {
var component = (SkeletonRenderer)o;
if (!component.valid)
return false;
}
return true;
} else {
var 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();
// 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.");
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.");
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.");
var 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");
updateWhenInvisible = so.FindProperty("updateWhenInvisible");
singleSubmesh = so.FindProperty("singleSubmesh");
fixDrawOrder = so.FindProperty("fixDrawOrder");
maskInteraction = so.FindProperty("maskInteraction");
maskMaterialsNone = so.FindProperty("maskMaterials.materialsMaskDisabled");
maskMaterialsInside = so.FindProperty("maskMaterials.materialsInsideMask");
maskMaterialsOutside = so.FindProperty("maskMaterials.materialsOutsideMask");
separatorSlotNames = so.FindProperty("separatorSlotNames");
separatorSlotNames.isExpanded = true;
zSpacing = so.FindProperty("zSpacing");
SerializedObject renderersSerializedObject = SpineInspectorUtility.GetRenderersSerializedObject(serializedObject); // Allows proper multi-edit behavior.
sortingProperties = new SpineInspectorUtility.SerializedSortingProperties(renderersSerializedObject);
}
public void OnSceneGUI () {
var skeletonRenderer = (SkeletonRenderer)target;
var skeleton = skeletonRenderer.Skeleton;
var 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 (var o in targets)
SpineEditorUtilities.ReinitializeComponent((SkeletonRenderer)o);
SceneView.RepaintAll();
}
}
}
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 SkeletonRenderer);
}
} else {
foreach (var c in targets) {
var component = c as SkeletonRenderer;
if (!component.valid) {
SpineEditorUtilities.ReinitializeComponent(component);
if (!component.valid) continue;
}
}
}
#if BUILT_IN_SPRITE_MASK_COMPONENT
if (setMaskNoneMaterialsQueued) {
setMaskNoneMaterialsQueued = false;
foreach (var c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.None);
}
if (setInsideMaskMaterialsQueued) {
setInsideMaskMaterialsQueued = false;
foreach (var c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleInsideMask);
}
if (setOutsideMaskMaterialsQueued) {
setOutsideMaskMaterialsQueued = false;
foreach (var c in targets)
EditorSetMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleOutsideMask);
}
if (deleteInsideMaskMaterialsQueued) {
deleteInsideMaskMaterialsQueued = false;
foreach (var c in targets)
EditorDeleteMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleInsideMask);
}
if (deleteOutsideMaskMaterialsQueued) {
deleteOutsideMaskMaterialsQueued = false;
foreach (var c in targets)
EditorDeleteMaskMaterials(c as SkeletonRenderer, SpriteMaskInteraction.VisibleOutsideMask);
}
#endif
#if NO_PREFAB_MESH
if (isInspectingPrefab) {
foreach (var c in targets) {
var component = (SkeletonRenderer)c;
MeshFilter meshFilter = component.GetComponent<MeshFilter>();
if (meshFilter != null && meshFilter.sharedMesh != null)
meshFilter.sharedMesh = null;
}
}
#endif
}
bool valid = TargetIsValid;
// 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 {
var 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("Skeleton Data Asset required", MessageType.Warning);
return;
}
if (!SpineEditorUtilities.SkeletonDataAssetIsValid(component.skeletonDataAsset)) {
EditorGUILayout.HelpBox("Skeleton Data Asset error. Please check Skeleton Data Asset.", MessageType.Error);
return;
}
if (valid)
EditorGUILayout.PropertyField(initialSkinName, SpineInspectorUtility.TempContent("Initial Skin"));
}
EditorGUILayout.Space();
// Sorting Layers
SpineInspectorUtility.SortingPropertyFields(sortingProperties, applyModifiedProperties: true);
if (maskInteraction != null) EditorGUILayout.PropertyField(maskInteraction, MaskInteractionLabel);
if (!valid)
return;
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()) {
SpineInspectorUtility.ToggleLeftLayout(initialFlipX);
SpineInspectorUtility.ToggleLeftLayout(initialFlipY);
EditorGUILayout.Space();
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("Renderer Settings", EditorStyles.boldLabel);
using (new SpineInspectorUtility.LabelWidthScope()) {
// Optimization options
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();
}
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
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();
}
}
if (EditorGUI.EndChangeCheck())
SceneView.RepaintAll();
}
}
protected void SkeletonRootMotionParameter() {
SkeletonRootMotionParameter(targets);
}
public static void SkeletonRootMotionParameter(Object[] targets) {
int rootMotionComponentCount = 0;
foreach (var t in targets) {
var 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 (var t in targets) {
var 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 (var t in targets) {
var component = t as Component;
var rootMotionComponent = component.GetComponent<SkeletonRootMotion>();
if (rootMotionComponent != null) {
DestroyImmediate(rootMotionComponent);
}
}
}
}
}
}
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 void SeparatorsField (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++) {
int index = skeleton.FindSlotIndex(separatorSlotNames.GetArrayElementAtIndex(i).stringValue);
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 (var o in targets) {
mismatchDetected |= UpdateIfSkinMismatch((SkeletonRenderer)o, newSkinName);
}
if (mismatchDetected) {
mismatchDetected = false;
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
}
}
}
static bool UpdateIfSkinMismatch (SkeletonRenderer skeletonRenderer, string componentSkinName) {
if (!skeletonRenderer.valid || skeletonRenderer.EditorSkipSkinSync) return false;
var 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.SetSlotsToSetupPose();
// 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 (var o in targets) {
var 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,10 @@
fileFormatVersion: 2
guid: d0fc5db9788bce4418ad3252d43faa8a
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,113 @@
/******************************************************************************
* 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.
*****************************************************************************/
using UnityEditor;
using UnityEngine;
namespace Spine.Unity.Editor {
[CustomEditor(typeof(SkeletonRootMotionBase))]
[CanEditMultipleObjects]
public class SkeletonRootMotionBaseInspector : UnityEditor.Editor {
protected SerializedProperty rootMotionBoneName;
protected SerializedProperty transformPositionX;
protected SerializedProperty transformPositionY;
protected SerializedProperty rootMotionScaleX;
protected SerializedProperty rootMotionScaleY;
protected SerializedProperty rootMotionTranslateXPerY;
protected SerializedProperty rootMotionTranslateYPerX;
protected SerializedProperty rigidBody2D;
protected SerializedProperty rigidBody;
protected GUIContent rootMotionBoneNameLabel;
protected GUIContent transformPositionXLabel;
protected GUIContent transformPositionYLabel;
protected GUIContent rootMotionScaleXLabel;
protected GUIContent rootMotionScaleYLabel;
protected GUIContent rootMotionTranslateXPerYLabel;
protected GUIContent rootMotionTranslateYPerXLabel;
protected GUIContent rigidBody2DLabel;
protected GUIContent rigidBodyLabel;
protected virtual void OnEnable () {
rootMotionBoneName = serializedObject.FindProperty("rootMotionBoneName");
transformPositionX = serializedObject.FindProperty("transformPositionX");
transformPositionY = serializedObject.FindProperty("transformPositionY");
rootMotionScaleX = serializedObject.FindProperty("rootMotionScaleX");
rootMotionScaleY = serializedObject.FindProperty("rootMotionScaleY");
rootMotionTranslateXPerY = serializedObject.FindProperty("rootMotionTranslateXPerY");
rootMotionTranslateYPerX = serializedObject.FindProperty("rootMotionTranslateYPerX");
rigidBody2D = serializedObject.FindProperty("rigidBody2D");
rigidBody = serializedObject.FindProperty("rigidBody");
rootMotionBoneNameLabel = new UnityEngine.GUIContent("Root Motion Bone", "The bone to take the motion from.");
transformPositionXLabel = new UnityEngine.GUIContent("X", "Root transform position (X)");
transformPositionYLabel = new UnityEngine.GUIContent("Y", "Use the Y-movement of the bone.");
rootMotionScaleXLabel = new UnityEngine.GUIContent("Root Motion Scale (X)", "Scale applied to the horizontal root motion delta. Can be used for delta compensation to e.g. stretch a jump to the desired distance.");
rootMotionScaleYLabel = new UnityEngine.GUIContent("Root Motion Scale (Y)", "Scale applied to the vertical root motion delta. Can be used for delta compensation to e.g. stretch a jump to the desired distance.");
rootMotionTranslateXPerYLabel = new UnityEngine.GUIContent("Root Motion Translate (X)", "Added X translation per root motion Y delta. Can be used for delta compensation when scaling is not enough, to e.g. offset a horizontal jump to a vertically different goal.");
rootMotionTranslateYPerXLabel = new UnityEngine.GUIContent("Root Motion Translate (Y)", "Added Y translation per root motion X delta. Can be used for delta compensation when scaling is not enough, to e.g. offset a horizontal jump to a vertically different goal.");
rigidBody2DLabel = new UnityEngine.GUIContent("Rigidbody2D",
"Optional Rigidbody2D: Assign a Rigidbody2D here if you want " +
" to apply the root motion to the rigidbody instead of the Transform." +
"\n\n" +
"Note that animation and physics updates are not always in sync." +
"Some jitter may result at certain framerates.");
rigidBodyLabel = new UnityEngine.GUIContent("Rigidbody",
"Optional Rigidbody: Assign a Rigidbody here if you want " +
" to apply the root motion to the rigidbody instead of the Transform." +
"\n\n" +
"Note that animation and physics updates are not always in sync." +
"Some jitter may result at certain framerates.");
}
public override void OnInspectorGUI () {
MainPropertyFields();
OptionalPropertyFields();
serializedObject.ApplyModifiedProperties();
}
protected virtual void MainPropertyFields () {
EditorGUILayout.PropertyField(rootMotionBoneName, rootMotionBoneNameLabel);
EditorGUILayout.PropertyField(transformPositionX, transformPositionXLabel);
EditorGUILayout.PropertyField(transformPositionY, transformPositionYLabel);
EditorGUILayout.PropertyField(rootMotionScaleX, rootMotionScaleXLabel);
EditorGUILayout.PropertyField(rootMotionScaleY, rootMotionScaleYLabel);
EditorGUILayout.PropertyField(rootMotionTranslateXPerY, rootMotionTranslateXPerYLabel);
EditorGUILayout.PropertyField(rootMotionTranslateYPerX, rootMotionTranslateYPerXLabel);
}
protected virtual void OptionalPropertyFields () {
EditorGUILayout.PropertyField(rigidBody2D, rigidBody2DLabel);
EditorGUILayout.PropertyField(rigidBody, rigidBodyLabel);
}
}
}

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