- Updated AddressReferenceGraphWindow to scan for AddressKeys in the _Game directory and added a warning for missing directories. - Enhanced AddressableBatchTool with new filters for asset types (Prefab, Scene, ScriptableObject, Texture, Audio) and improved UI layout for better usability. - Introduced automatic application of grouping and labeling rules during registration in AddressableBatchTool. - Added functionality to quickly scan the _Game folder and improved address building logic. - Updated AddressableRuleSyncWindow to include handling for custom labels and improved reporting of issues. - Enhanced AddressableRules with a whitelist for known labels and refined grouping and labeling logic based on asset prefixes.
528 lines
24 KiB
C#
528 lines
24 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEditor.AddressableAssets;
|
||
using UnityEditor.AddressableAssets.Settings;
|
||
using UnityEngine;
|
||
|
||
namespace BaseGames.Editor
|
||
{
|
||
/// <summary>
|
||
/// Addressable 规则同步窗口。
|
||
///
|
||
/// 功能:
|
||
/// 1. 扫描所有已注册的 Addressable 资产
|
||
/// 2. 根据 <see cref="AddressableRules"/> 中的规则计算期望分组与期望标签
|
||
/// 3. 对比实际值,显示所有不符合规范的条目(分组错误 / 标签缺失 / 标签多余)
|
||
/// 4. 一键自动修复全部问题
|
||
/// 5. 导出 CSV 报告供存档或 Code Review
|
||
///
|
||
/// 菜单:BaseGames → Addressables → Rule Sync
|
||
/// </summary>
|
||
public class AddressableRuleSyncWindow : EditorWindow
|
||
{
|
||
// ── 内部数据结构 ───────────────────────────────────────────────────────
|
||
|
||
private enum IssueKind { None, WrongGroup, MissingLabel, ExtraLabel }
|
||
|
||
private class EntryReport
|
||
{
|
||
public string Address;
|
||
public string AssetPath;
|
||
public string CurrentGroup;
|
||
public string ExpectedGroup; // null = 规则未覆盖,维持现状
|
||
public string[] CurrentLabels;
|
||
public string[] ExpectedLabels;
|
||
public string[] MissingLabels; // 应有但没有(规则要求),红色错误
|
||
public string[] ExtraLabels; // 规则不要求且在 KnownLabels 中(多余规则标签),红色错误
|
||
public string[] UnknownLabels; // 规则不要求且不在 KnownLabels 中(自定义标签),黄色警告,不自动删除
|
||
public bool GroupOk => ExpectedGroup == null || CurrentGroup == ExpectedGroup;
|
||
public bool LabelsOk => MissingLabels.Length == 0 && ExtraLabels.Length == 0;
|
||
public bool IsOk => GroupOk && LabelsOk;
|
||
public bool HasWarnings => UnknownLabels.Length > 0;
|
||
}
|
||
|
||
// ── 状态 ──────────────────────────────────────────────────────────────
|
||
|
||
private List<EntryReport> _reports = new();
|
||
private Vector2 _scrollPos;
|
||
private bool _showOk = false;
|
||
private bool _scanned = false;
|
||
private string _searchFilter = "";
|
||
|
||
// ── 样式(惰性初始化)────────────────────────────────────────────────
|
||
|
||
private GUIStyle _okStyle;
|
||
private GUIStyle _warnStyle;
|
||
private GUIStyle _errorStyle;
|
||
private GUIStyle _boldStyle;
|
||
private GUIStyle _rowEven;
|
||
private GUIStyle _rowOdd;
|
||
private bool _stylesReady;
|
||
|
||
// ── 颜色 ─────────────────────────────────────────────────────────────
|
||
|
||
private static readonly Color ColOk = new(0.20f, 0.78f, 0.35f, 1f);
|
||
private static readonly Color ColWarn = new(0.95f, 0.75f, 0.10f, 1f);
|
||
private static readonly Color ColError = new(0.90f, 0.25f, 0.20f, 1f);
|
||
private static readonly Color ColRowEven = new(0.22f, 0.22f, 0.22f, 0.4f);
|
||
|
||
// ── 菜单入口 ──────────────────────────────────────────────────────────
|
||
|
||
[MenuItem("BaseGames/Addressables/Rule Sync", priority = 110)]
|
||
public static void OpenWindow()
|
||
{
|
||
var win = GetWindow<AddressableRuleSyncWindow>("Addressable Rule Sync");
|
||
win.minSize = new Vector2(1040, 540);
|
||
win.Show();
|
||
}
|
||
|
||
// ── GUI ───────────────────────────────────────────────────────────────
|
||
|
||
private void OnGUI()
|
||
{
|
||
EnsureStyles();
|
||
|
||
if (AddressableAssetSettingsDefaultObject.Settings == null)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
"Addressable Settings 未初始化。\n" +
|
||
"请先执行 Window → Asset Management → Addressables → Groups → Create Addressables Settings。",
|
||
MessageType.Error);
|
||
return;
|
||
}
|
||
|
||
DrawToolbar();
|
||
DrawStats();
|
||
DrawTable();
|
||
DrawFooter();
|
||
}
|
||
|
||
// ── 工具栏 ────────────────────────────────────────────────────────────
|
||
|
||
private void DrawToolbar()
|
||
{
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
if (GUILayout.Button("扫描", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||
Scan();
|
||
|
||
if (GUILayout.Button("🔄 刷新", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||
Scan();
|
||
|
||
GUILayout.Space(8);
|
||
_showOk = GUILayout.Toggle(_showOk, "显示正常项", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||
GUILayout.Space(8);
|
||
|
||
EditorGUILayout.LabelField("搜索:", GUILayout.Width(42));
|
||
_searchFilter = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField,
|
||
GUILayout.Width(200));
|
||
|
||
GUILayout.FlexibleSpace();
|
||
|
||
GUI.enabled = _scanned && _reports.Any(r => !r.IsOk);
|
||
if (GUILayout.Button("✦ 修复所有问题", EditorStyles.toolbarButton, GUILayout.Width(120)))
|
||
FixAll();
|
||
GUI.enabled = _scanned;
|
||
if (GUILayout.Button("导出 CSV", EditorStyles.toolbarButton, GUILayout.Width(80)))
|
||
ExportCsv();
|
||
GUI.enabled = true;
|
||
}
|
||
}
|
||
|
||
// ── 统计行 ────────────────────────────────────────────────────────────
|
||
|
||
private void DrawStats()
|
||
{
|
||
if (!_scanned) return;
|
||
|
||
int total = _reports.Count;
|
||
int ok = _reports.Count(r => r.IsOk);
|
||
int issues = _reports.Count(r => !r.IsOk);
|
||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||
int wrongGrp = _reports.Count(r => !r.GroupOk);
|
||
int misLabel = _reports.Count(r => r.MissingLabels.Length > 0);
|
||
int extLabel = _reports.Count(r => r.ExtraLabels.Length > 0);
|
||
int unkLabel = _reports.Count(r => r.UnknownLabels.Length > 0);
|
||
|
||
EditorGUILayout.Space(2);
|
||
using (new EditorGUILayout.HorizontalScope())
|
||
{
|
||
GUILayout.Label($"共 {total} 条目", EditorStyles.miniLabel);
|
||
GUILayout.Space(12);
|
||
DrawColoredLabel($"✅ 正常 {ok}", ColOk);
|
||
GUILayout.Space(12);
|
||
DrawColoredLabel($"❌ 问题 {issues}", issues > 0 ? ColError : ColOk);
|
||
GUILayout.Space(8);
|
||
DrawColoredLabel($"⚠ 自定义标签 {unkLabel}", unkLabel > 0 ? ColWarn : ColOk);
|
||
GUILayout.Space(20);
|
||
GUILayout.Label($"分组错误 {wrongGrp} | 标签缺失 {misLabel} | 多余规则标签 {extLabel}",
|
||
EditorStyles.miniLabel);
|
||
GUILayout.FlexibleSpace();
|
||
}
|
||
EditorGUILayout.Space(2);
|
||
}
|
||
|
||
// ── 主表格 ────────────────────────────────────────────────────────────
|
||
|
||
private void DrawTable()
|
||
{
|
||
// 表头
|
||
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
||
{
|
||
GUILayout.Label("Address", _boldStyle, GUILayout.Width(200));
|
||
GUILayout.Label("当前分组", _boldStyle, GUILayout.Width(120));
|
||
GUILayout.Label("期望分组", _boldStyle, GUILayout.Width(120));
|
||
GUILayout.Label("缺失标签", _boldStyle, GUILayout.Width(130));
|
||
GUILayout.Label("多余规则标签", _boldStyle, GUILayout.Width(110));
|
||
GUILayout.Label("自定义标签", _boldStyle, GUILayout.Width(110));
|
||
GUILayout.Label("状态", _boldStyle, GUILayout.Width(80));
|
||
}
|
||
|
||
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos, GUILayout.ExpandHeight(true));
|
||
|
||
if (!_scanned)
|
||
{
|
||
EditorGUILayout.HelpBox("点击「扫描」按钮开始分析已注册的 Addressable 资产。", MessageType.Info);
|
||
}
|
||
else
|
||
{
|
||
var display = _reports
|
||
.Where(r => _showOk || !r.IsOk)
|
||
.Where(r => string.IsNullOrEmpty(_searchFilter)
|
||
|| r.Address.IndexOf(_searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)
|
||
.ToList();
|
||
|
||
if (display.Count == 0)
|
||
{
|
||
EditorGUILayout.HelpBox(
|
||
_showOk ? "没有匹配搜索条件的条目。" : "✅ 所有资产均符合规范!",
|
||
MessageType.Info);
|
||
}
|
||
|
||
for (int i = 0; i < display.Count; i++)
|
||
DrawRow(display[i], i);
|
||
}
|
||
|
||
EditorGUILayout.EndScrollView();
|
||
}
|
||
|
||
private void DrawRow(EntryReport r, int idx)
|
||
{
|
||
var bg = idx % 2 == 0 ? _rowEven : GUIStyle.none;
|
||
using (new EditorGUILayout.HorizontalScope(bg, GUILayout.Height(20)))
|
||
{
|
||
// Address(点击可 Ping)
|
||
if (GUILayout.Button(r.Address, EditorStyles.linkLabel, GUILayout.Width(200)))
|
||
PingAsset(r.AssetPath);
|
||
|
||
// 当前分组
|
||
var grpColor = r.GroupOk ? ColOk : ColError;
|
||
DrawColoredLabel(r.CurrentGroup ?? "—", grpColor, GUILayout.Width(120));
|
||
|
||
// 期望分组
|
||
var expGrpText = r.ExpectedGroup ?? "(规则未覆盖)";
|
||
var expGrpColor = r.GroupOk ? ColOk : ColWarn;
|
||
DrawColoredLabel(expGrpText, expGrpColor, GUILayout.Width(120));
|
||
|
||
// 缺失标签(红色,须补齐)
|
||
var missingText = r.MissingLabels.Length > 0 ? string.Join(", ", r.MissingLabels) : "—";
|
||
DrawColoredLabel(missingText, r.MissingLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(130));
|
||
|
||
// 多余规则标签(红色,将被 FixEntry 移除)
|
||
var extraText = r.ExtraLabels.Length > 0 ? string.Join(", ", r.ExtraLabels) : "—";
|
||
DrawColoredLabel(extraText, r.ExtraLabels.Length > 0 ? ColError : ColOk, GUILayout.Width(110));
|
||
|
||
// 自定义标签(黄色警告,不会被自动删除,建议写入规范)
|
||
var unknownText = r.UnknownLabels.Length > 0 ? string.Join(", ", r.UnknownLabels) : "—";
|
||
DrawColoredLabel(unknownText, r.UnknownLabels.Length > 0 ? ColWarn : ColOk, GUILayout.Width(110));
|
||
|
||
// 状态 + 单条修复按钮
|
||
if (r.IsOk)
|
||
{
|
||
var statusColor = r.HasWarnings ? ColWarn : ColOk;
|
||
var statusText = r.HasWarnings ? "⚠ 自定义标签" : "✅ 正常";
|
||
DrawColoredLabel(statusText, statusColor, GUILayout.Width(80));
|
||
}
|
||
else
|
||
{
|
||
DrawColoredLabel("❌ 需修复", ColError, GUILayout.Width(60));
|
||
if (GUILayout.Button("修复", EditorStyles.miniButton, GUILayout.Width(40)))
|
||
FixEntry(r);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 底栏 ──────────────────────────────────────────────────────────────
|
||
|
||
private void DrawFooter()
|
||
{
|
||
EditorGUILayout.Space(4);
|
||
EditorGUILayout.HelpBox(
|
||
"规则来源:Docs/Standards/AddressablesLabelSpec.md §3 分组规则:AssetFolderSpec.md §8.1\n" +
|
||
"「修复所有问题」仅修改已注册资产的分组/标签,不注册新资产,不删除自定义标签(黄色警告项)。\n" +
|
||
"新增资产工作流:① Addressable Batch Tool → ⚡ 全量扫描 _Game/ → 注册所有 ② 返回此窗口 → 扫描 → 修复所有问题",
|
||
MessageType.None);
|
||
}
|
||
|
||
// ── 扫描逻辑 ──────────────────────────────────────────────────────────
|
||
|
||
private void Scan()
|
||
{
|
||
_reports.Clear();
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) return;
|
||
|
||
foreach (var group in settings.groups)
|
||
{
|
||
if (group == null) continue;
|
||
foreach (var entry in group.entries)
|
||
{
|
||
if (entry == null) continue;
|
||
|
||
var address = entry.address;
|
||
var expectedGroup = AddressableRules.GetExpectedGroup(address);
|
||
var expectedLbls = AddressableRules.GetExpectedLabels(address);
|
||
var currentLbls = entry.labels.ToArray();
|
||
|
||
var missing = expectedLbls.Except(currentLbls, StringComparer.Ordinal).ToArray();
|
||
|
||
// 区分两类"多余标签":
|
||
// extra = 规则已知标签(KnownLabels)中规则不要求的 → 红色,FixEntry 会移除
|
||
// unknown = 不在 KnownLabels 中的自定义标签 → 黄色警告,FixEntry 保留,建议写入规范
|
||
var notExpected = currentLbls.Except(expectedLbls, StringComparer.Ordinal);
|
||
var extra = notExpected.Where(l => AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||
var unknown = notExpected.Where(l => !AddressableRules.KnownLabels.Contains(l)).ToArray();
|
||
|
||
_reports.Add(new EntryReport
|
||
{
|
||
Address = address,
|
||
AssetPath = entry.AssetPath,
|
||
CurrentGroup = group.name,
|
||
ExpectedGroup = expectedGroup,
|
||
CurrentLabels = currentLbls,
|
||
ExpectedLabels = expectedLbls,
|
||
MissingLabels = missing,
|
||
ExtraLabels = extra,
|
||
UnknownLabels = unknown,
|
||
});
|
||
}
|
||
}
|
||
|
||
// 问题项排前面,仅有警告的次之,正常项排最后;同类按 Address 字母序
|
||
_reports = _reports
|
||
.OrderBy(r => r.IsOk ? (r.HasWarnings ? 1 : 2) : 0)
|
||
.ThenBy(r => r.Address, StringComparer.Ordinal)
|
||
.ToList();
|
||
|
||
_scanned = true;
|
||
Repaint();
|
||
|
||
int issues = _reports.Count(r => !r.IsOk);
|
||
int warnings = _reports.Count(r => r.IsOk && r.HasWarnings);
|
||
Debug.Log($"[AddressableRuleSync] 扫描完成:{_reports.Count} 个条目," +
|
||
$"{issues} 个需要修复,{warnings} 个含自定义标签警告。");
|
||
}
|
||
|
||
// ── 修复逻辑 ──────────────────────────────────────────────────────────
|
||
|
||
private void FixAll()
|
||
{
|
||
var issues = _reports.Where(r => !r.IsOk).ToList();
|
||
if (issues.Count == 0) return;
|
||
|
||
int moveCount = issues.Count(r => !r.GroupOk);
|
||
int addCount = issues.Sum(r => r.MissingLabels.Length);
|
||
int removeCount = issues.Sum(r => r.ExtraLabels.Length);
|
||
|
||
// 干跑预览对话框
|
||
bool confirmed = EditorUtility.DisplayDialog(
|
||
"确认修复所有问题",
|
||
$"将对 {issues.Count} 个条目执行以下操作:\n\n" +
|
||
$" • 移动分组:{moveCount} 个\n" +
|
||
$" • 添加标签:{addCount} 个\n" +
|
||
$" • 移除多余规则标签:{removeCount} 个\n\n" +
|
||
"⚠ 自定义标签(黄色警告项)不会被删除。\n" +
|
||
"此操作不可撤销,请确认后继续。",
|
||
"确认修复", "取消");
|
||
if (!confirmed) return;
|
||
|
||
int fixedCount = 0;
|
||
foreach (var r in issues)
|
||
{
|
||
if (FixEntry(r)) fixedCount++;
|
||
}
|
||
|
||
SaveSettings();
|
||
Scan(); // 修复后重新扫描以更新结果
|
||
Debug.Log($"[AddressableRuleSync] 修复完成:共处理 {fixedCount} 个条目。");
|
||
}
|
||
|
||
private bool FixEntry(EntryReport r)
|
||
{
|
||
var settings = AddressableAssetSettingsDefaultObject.Settings;
|
||
if (settings == null) return false;
|
||
|
||
var entry = FindEntry(settings, r.Address);
|
||
if (entry == null)
|
||
{
|
||
Debug.LogWarning($"[AddressableRuleSync] 找不到条目:{r.Address}");
|
||
return false;
|
||
}
|
||
|
||
bool changed = false;
|
||
|
||
// 修复分组
|
||
if (!r.GroupOk && r.ExpectedGroup != null)
|
||
{
|
||
var targetGroup = GetOrCreateGroup(settings, r.ExpectedGroup);
|
||
if (targetGroup != null && entry.parentGroup != targetGroup)
|
||
{
|
||
settings.MoveEntry(entry, targetGroup, false, false);
|
||
r.CurrentGroup = r.ExpectedGroup;
|
||
changed = true;
|
||
}
|
||
}
|
||
|
||
// 添加缺失标签
|
||
foreach (var lbl in r.MissingLabels)
|
||
{
|
||
EnsureLabelExists(settings, lbl);
|
||
entry.SetLabel(lbl, true, true);
|
||
changed = true;
|
||
}
|
||
|
||
// 移除多余规则标签(ExtraLabels 只包含 KnownLabels 中规则不要求的标签;
|
||
// UnknownLabels 是用户自定义标签,刻意保留,不做删除)
|
||
foreach (var lbl in r.ExtraLabels)
|
||
{
|
||
entry.SetLabel(lbl, false, true);
|
||
changed = true;
|
||
}
|
||
|
||
return changed;
|
||
}
|
||
|
||
// ── 导出 CSV ──────────────────────────────────────────────────────────
|
||
|
||
private void ExportCsv()
|
||
{
|
||
if (_reports.Count == 0) return;
|
||
|
||
var path = EditorUtility.SaveFilePanel(
|
||
"导出 Addressable Rule 报告", "", "AddressableRuleReport.csv", "csv");
|
||
if (string.IsNullOrEmpty(path)) return;
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine("Address,CurrentGroup,ExpectedGroup,GroupOk,MissingLabels,ExtraLabels,Status");
|
||
|
||
foreach (var r in _reports)
|
||
{
|
||
var status = r.IsOk ? "OK" : "ISSUE";
|
||
sb.AppendLine(
|
||
$"\"{r.Address}\"," +
|
||
$"\"{r.CurrentGroup}\"," +
|
||
$"\"{r.ExpectedGroup ?? "(uncovered)"}\"," +
|
||
$"{r.GroupOk}," +
|
||
$"\"{string.Join(";", r.MissingLabels)}\"," +
|
||
$"\"{string.Join(";", r.ExtraLabels)}\"," +
|
||
$"{status}");
|
||
}
|
||
|
||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||
Debug.Log($"[AddressableRuleSync] CSV 报告已导出:{path}");
|
||
}
|
||
|
||
// ── 辅助方法 ──────────────────────────────────────────────────────────
|
||
|
||
private static AddressableAssetEntry FindEntry(AddressableAssetSettings settings, string address)
|
||
{
|
||
foreach (var group in settings.groups)
|
||
{
|
||
if (group == null) continue;
|
||
foreach (var e in group.entries)
|
||
if (e != null && e.address == address) return e;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private static AddressableAssetGroup GetOrCreateGroup(AddressableAssetSettings settings, string groupName)
|
||
{
|
||
var existing = settings.groups.FirstOrDefault(g => g != null && g.name == groupName);
|
||
if (existing != null) return existing;
|
||
|
||
var template = settings.GroupTemplateObjects.FirstOrDefault()
|
||
as AddressableAssetGroupTemplate;
|
||
var newGroup = settings.CreateGroup(groupName, false, false, true,
|
||
template != null
|
||
? new List<AddressableAssetGroupSchema>(template.SchemaObjects)
|
||
: null);
|
||
|
||
if (newGroup != null)
|
||
Debug.Log($"[AddressableRuleSync] 已自动创建分组:{groupName}");
|
||
|
||
return newGroup ?? settings.DefaultGroup;
|
||
}
|
||
|
||
private static void EnsureLabelExists(AddressableAssetSettings settings, string label)
|
||
{
|
||
var labels = settings.GetLabels();
|
||
if (!labels.Contains(label))
|
||
{
|
||
settings.AddLabel(label, true);
|
||
Debug.Log($"[AddressableRuleSync] 已创建标签:{label}");
|
||
}
|
||
}
|
||
|
||
private static void SaveSettings()
|
||
{
|
||
AssetDatabase.SaveAssets();
|
||
AddressableAssetSettingsDefaultObject.Settings?.SetDirty(
|
||
AddressableAssetSettings.ModificationEvent.EntryModified, null, true);
|
||
}
|
||
|
||
private static void PingAsset(string assetPath)
|
||
{
|
||
if (string.IsNullOrEmpty(assetPath)) return;
|
||
var obj = AssetDatabase.LoadMainAssetAtPath(assetPath);
|
||
if (obj != null) EditorGUIUtility.PingObject(obj);
|
||
}
|
||
|
||
// ── 样式初始化 ────────────────────────────────────────────────────────
|
||
|
||
private void EnsureStyles()
|
||
{
|
||
if (_stylesReady) return;
|
||
|
||
_boldStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 11 };
|
||
_okStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColOk } };
|
||
_warnStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColWarn } };
|
||
_errorStyle = new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = ColError } };
|
||
|
||
_rowEven = new GUIStyle();
|
||
_rowEven.normal.background = MakeTexture(1, 1, ColRowEven);
|
||
|
||
_stylesReady = true;
|
||
}
|
||
|
||
private void DrawColoredLabel(string text, Color color, params GUILayoutOption[] options)
|
||
{
|
||
var prev = GUI.color;
|
||
GUI.color = color;
|
||
GUILayout.Label(text, EditorStyles.miniLabel, options);
|
||
GUI.color = prev;
|
||
}
|
||
|
||
private static Texture2D MakeTexture(int width, int height, Color color)
|
||
{
|
||
var tex = new Texture2D(width, height);
|
||
tex.SetPixel(0, 0, color);
|
||
tex.Apply();
|
||
return tex;
|
||
}
|
||
}
|
||
}
|