psst
This commit is contained in:
514
Editor/PSXObjectExporterEditor.cs
Normal file
514
Editor/PSXObjectExporterEditor.cs
Normal file
@@ -0,0 +1,514 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using SplashEdit.RuntimeCode;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SplashEdit.EditorCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom inspector for PSXObjectExporter with enhanced UX.
|
||||
/// Shows mesh info, texture preview, collision visualization, and validation.
|
||||
/// </summary>
|
||||
[CustomEditor(typeof(PSXObjectExporter))]
|
||||
[CanEditMultipleObjects]
|
||||
public class PSXObjectExporterEditor : UnityEditor.Editor
|
||||
{
|
||||
// Serialized properties
|
||||
private SerializedProperty isActiveProp;
|
||||
private SerializedProperty bitDepthProp;
|
||||
private SerializedProperty luaFileProp;
|
||||
private SerializedProperty objectFlagsProp;
|
||||
private SerializedProperty collisionTypeProp;
|
||||
private SerializedProperty exportCollisionMeshProp;
|
||||
private SerializedProperty customCollisionMeshProp;
|
||||
private SerializedProperty collisionLayerProp;
|
||||
private SerializedProperty previewNormalsProp;
|
||||
private SerializedProperty normalPreviewLengthProp;
|
||||
private SerializedProperty showCollisionBoundsProp;
|
||||
private SerializedProperty textureProp;
|
||||
|
||||
// UI State
|
||||
private bool showMeshInfo = true;
|
||||
private bool showTextureInfo = true;
|
||||
private bool showExportSettings = true;
|
||||
private bool showCollisionSettings = true;
|
||||
private bool showGizmoSettings = false;
|
||||
private bool showValidation = true;
|
||||
|
||||
// Cached data
|
||||
private MeshFilter meshFilter;
|
||||
private MeshRenderer meshRenderer;
|
||||
private int triangleCount;
|
||||
private int vertexCount;
|
||||
private Bounds meshBounds;
|
||||
private List<string> validationErrors = new List<string>();
|
||||
private List<string> validationWarnings = new List<string>();
|
||||
|
||||
// Styles
|
||||
private GUIStyle headerStyle;
|
||||
private GUIStyle errorStyle;
|
||||
private GUIStyle warningStyle;
|
||||
|
||||
// Validation
|
||||
private bool _validationDirty = true;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Get serialized properties
|
||||
isActiveProp = serializedObject.FindProperty("isActive");
|
||||
bitDepthProp = serializedObject.FindProperty("bitDepth");
|
||||
luaFileProp = serializedObject.FindProperty("luaFile");
|
||||
objectFlagsProp = serializedObject.FindProperty("objectFlags");
|
||||
collisionTypeProp = serializedObject.FindProperty("collisionType");
|
||||
exportCollisionMeshProp = serializedObject.FindProperty("exportCollisionMesh");
|
||||
customCollisionMeshProp = serializedObject.FindProperty("customCollisionMesh");
|
||||
collisionLayerProp = serializedObject.FindProperty("collisionLayer");
|
||||
previewNormalsProp = serializedObject.FindProperty("previewNormals");
|
||||
normalPreviewLengthProp = serializedObject.FindProperty("normalPreviewLength");
|
||||
showCollisionBoundsProp = serializedObject.FindProperty("showCollisionBounds");
|
||||
textureProp = serializedObject.FindProperty("texture");
|
||||
|
||||
// Cache mesh info
|
||||
CacheMeshInfo();
|
||||
|
||||
// Defer validation to first inspector draw
|
||||
_validationDirty = true;
|
||||
}
|
||||
|
||||
private void CacheMeshInfo()
|
||||
{
|
||||
var exporter = target as PSXObjectExporter;
|
||||
if (exporter == null) return;
|
||||
|
||||
meshFilter = exporter.GetComponent<MeshFilter>();
|
||||
meshRenderer = exporter.GetComponent<MeshRenderer>();
|
||||
|
||||
if (meshFilter != null && meshFilter.sharedMesh != null)
|
||||
{
|
||||
var mesh = meshFilter.sharedMesh;
|
||||
triangleCount = mesh.triangles.Length / 3;
|
||||
vertexCount = mesh.vertexCount;
|
||||
meshBounds = mesh.bounds;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunValidation()
|
||||
{
|
||||
validationErrors.Clear();
|
||||
validationWarnings.Clear();
|
||||
|
||||
var exporter = target as PSXObjectExporter;
|
||||
if (exporter == null) return;
|
||||
|
||||
// Check mesh
|
||||
if (meshFilter == null || meshFilter.sharedMesh == null)
|
||||
{
|
||||
validationErrors.Add("No mesh assigned to MeshFilter");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (triangleCount > 100)
|
||||
{
|
||||
validationWarnings.Add($"High triangle count ({triangleCount}). PS1 recommended: <100 per object");
|
||||
}
|
||||
|
||||
// Check vertex bounds
|
||||
var mesh = meshFilter.sharedMesh;
|
||||
var verts = mesh.vertices;
|
||||
bool hasOutOfBounds = false;
|
||||
|
||||
foreach (var v in verts)
|
||||
{
|
||||
var world = exporter.transform.TransformPoint(v);
|
||||
float scaled = Mathf.Max(Mathf.Abs(world.x), Mathf.Abs(world.y), Mathf.Abs(world.z)) * 4096f;
|
||||
if (scaled > 32767f)
|
||||
{
|
||||
hasOutOfBounds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOutOfBounds)
|
||||
{
|
||||
validationErrors.Add("Vertices exceed PS1 coordinate limits (±8 units from origin)");
|
||||
}
|
||||
}
|
||||
|
||||
// Check renderer
|
||||
if (meshRenderer == null)
|
||||
{
|
||||
validationWarnings.Add("No MeshRenderer - object will not be visible");
|
||||
}
|
||||
else if (meshRenderer.sharedMaterial == null)
|
||||
{
|
||||
validationWarnings.Add("No material assigned - will use default colors");
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnInspectorGUI()
|
||||
{
|
||||
serializedObject.Update();
|
||||
|
||||
// Run deferred validation
|
||||
if (_validationDirty)
|
||||
{
|
||||
RunValidation();
|
||||
_validationDirty = false;
|
||||
}
|
||||
|
||||
InitStyles();
|
||||
|
||||
// Active toggle at top
|
||||
EditorGUILayout.PropertyField(isActiveProp, new GUIContent("Export This Object"));
|
||||
|
||||
if (!isActiveProp.boolValue)
|
||||
{
|
||||
EditorGUILayout.HelpBox("This object will be skipped during export.", MessageType.Info);
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Mesh Info Section
|
||||
DrawMeshInfoSection();
|
||||
|
||||
// Texture Section
|
||||
DrawTextureSection();
|
||||
|
||||
// Export Settings Section
|
||||
DrawExportSettingsSection();
|
||||
|
||||
// Collision Settings Section
|
||||
DrawCollisionSettingsSection();
|
||||
|
||||
// Gizmo Settings Section
|
||||
DrawGizmoSettingsSection();
|
||||
|
||||
// Validation Section
|
||||
DrawValidationSection();
|
||||
|
||||
// Action Buttons
|
||||
DrawActionButtons();
|
||||
|
||||
if (serializedObject.ApplyModifiedProperties())
|
||||
{
|
||||
_validationDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitStyles()
|
||||
{
|
||||
if (headerStyle == null)
|
||||
{
|
||||
headerStyle = new GUIStyle(EditorStyles.foldoutHeader);
|
||||
}
|
||||
|
||||
if (errorStyle == null)
|
||||
{
|
||||
errorStyle = new GUIStyle(EditorStyles.label);
|
||||
errorStyle.normal.textColor = Color.red;
|
||||
}
|
||||
|
||||
if (warningStyle == null)
|
||||
{
|
||||
warningStyle = new GUIStyle(EditorStyles.label);
|
||||
warningStyle.normal.textColor = new Color(1f, 0.7f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMeshInfoSection()
|
||||
{
|
||||
showMeshInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showMeshInfo, "Mesh Information");
|
||||
if (showMeshInfo)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
if (meshFilter != null && meshFilter.sharedMesh != null)
|
||||
{
|
||||
EditorGUILayout.LabelField("Mesh", meshFilter.sharedMesh.name);
|
||||
EditorGUILayout.LabelField("Triangles", triangleCount.ToString());
|
||||
EditorGUILayout.LabelField("Vertices", vertexCount.ToString());
|
||||
EditorGUILayout.LabelField("Bounds Size", meshBounds.size.ToString("F2"));
|
||||
|
||||
// Triangle budget bar
|
||||
float budgetPercent = triangleCount / 100f;
|
||||
Rect rect = EditorGUILayout.GetControlRect(false, 20);
|
||||
EditorGUI.ProgressBar(rect, Mathf.Clamp01(budgetPercent), $"Triangle Budget: {triangleCount}/100");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUILayout.HelpBox("No mesh assigned", MessageType.Warning);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawTextureSection()
|
||||
{
|
||||
showTextureInfo = EditorGUILayout.BeginFoldoutHeaderGroup(showTextureInfo, "Texture Settings");
|
||||
if (showTextureInfo)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.PropertyField(textureProp, new GUIContent("Override Texture"));
|
||||
EditorGUILayout.PropertyField(bitDepthProp, new GUIContent("Bit Depth"));
|
||||
|
||||
// Show texture preview if assigned
|
||||
var tex = textureProp.objectReferenceValue as Texture2D;
|
||||
if (tex != null)
|
||||
{
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
GUILayout.FlexibleSpace();
|
||||
Rect previewRect = GUILayoutUtility.GetRect(64, 64, GUILayout.Width(64));
|
||||
EditorGUI.DrawPreviewTexture(previewRect, tex);
|
||||
GUILayout.FlexibleSpace();
|
||||
}
|
||||
|
||||
EditorGUILayout.LabelField($"Size: {tex.width}x{tex.height}");
|
||||
|
||||
// VRAM estimate
|
||||
int bpp = bitDepthProp.enumValueIndex == 0 ? 4 : (bitDepthProp.enumValueIndex == 1 ? 8 : 16);
|
||||
int vramBytes = (tex.width * tex.height * bpp) / 8;
|
||||
EditorGUILayout.LabelField($"Est. VRAM: {vramBytes} bytes ({bpp}bpp)");
|
||||
}
|
||||
else if (meshRenderer != null && meshRenderer.sharedMaterial != null)
|
||||
{
|
||||
var matTex = meshRenderer.sharedMaterial.mainTexture;
|
||||
if (matTex != null)
|
||||
{
|
||||
EditorGUILayout.HelpBox($"Using material texture: {matTex.name}", MessageType.Info);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawExportSettingsSection()
|
||||
{
|
||||
showExportSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showExportSettings, "Export Settings");
|
||||
if (showExportSettings)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.PropertyField(objectFlagsProp, new GUIContent("Object Flags"));
|
||||
EditorGUILayout.PropertyField(luaFileProp, new GUIContent("Lua Script"));
|
||||
|
||||
// Quick Lua file buttons
|
||||
if (luaFileProp.objectReferenceValue != null)
|
||||
{
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Edit Lua", GUILayout.Width(80)))
|
||||
{
|
||||
AssetDatabase.OpenAsset(luaFileProp.objectReferenceValue);
|
||||
}
|
||||
if (GUILayout.Button("Clear", GUILayout.Width(60)))
|
||||
{
|
||||
luaFileProp.objectReferenceValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GUILayout.Button("Create New Lua Script"))
|
||||
{
|
||||
CreateNewLuaScript();
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawCollisionSettingsSection()
|
||||
{
|
||||
showCollisionSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showCollisionSettings, "Collision Settings");
|
||||
if (showCollisionSettings)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.PropertyField(collisionTypeProp, new GUIContent("Collision Type"));
|
||||
|
||||
var collType = (PSXCollisionType)collisionTypeProp.enumValueIndex;
|
||||
if (collType != PSXCollisionType.None)
|
||||
{
|
||||
EditorGUILayout.PropertyField(exportCollisionMeshProp, new GUIContent("Export Collision Mesh"));
|
||||
EditorGUILayout.PropertyField(customCollisionMeshProp, new GUIContent("Custom Collision Mesh"));
|
||||
EditorGUILayout.PropertyField(collisionLayerProp, new GUIContent("Collision Layer"));
|
||||
|
||||
// Collision info
|
||||
EditorGUILayout.Space(5);
|
||||
string collisionInfo = collType switch
|
||||
{
|
||||
PSXCollisionType.Solid => "Solid: Blocks movement, fires onCollision",
|
||||
PSXCollisionType.Trigger => "Trigger: Fires onTriggerEnter/Exit, doesn't block",
|
||||
PSXCollisionType.Platform => "Platform: Solid from above only",
|
||||
_ => ""
|
||||
};
|
||||
EditorGUILayout.HelpBox(collisionInfo, MessageType.Info);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawGizmoSettingsSection()
|
||||
{
|
||||
showGizmoSettings = EditorGUILayout.BeginFoldoutHeaderGroup(showGizmoSettings, "Gizmo Settings");
|
||||
if (showGizmoSettings)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.PropertyField(previewNormalsProp, new GUIContent("Preview Normals"));
|
||||
if (previewNormalsProp.boolValue)
|
||||
{
|
||||
EditorGUILayout.PropertyField(normalPreviewLengthProp, new GUIContent("Normal Length"));
|
||||
}
|
||||
|
||||
EditorGUILayout.PropertyField(showCollisionBoundsProp, new GUIContent("Show Collision Bounds"));
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawValidationSection()
|
||||
{
|
||||
if (validationErrors.Count == 0 && validationWarnings.Count == 0)
|
||||
return;
|
||||
|
||||
showValidation = EditorGUILayout.BeginFoldoutHeaderGroup(showValidation, "Validation");
|
||||
if (showValidation)
|
||||
{
|
||||
foreach (var error in validationErrors)
|
||||
{
|
||||
EditorGUILayout.HelpBox(error, MessageType.Error);
|
||||
}
|
||||
|
||||
foreach (var warning in validationWarnings)
|
||||
{
|
||||
EditorGUILayout.HelpBox(warning, MessageType.Warning);
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Refresh Validation"))
|
||||
{
|
||||
CacheMeshInfo();
|
||||
RunValidation();
|
||||
}
|
||||
}
|
||||
EditorGUILayout.EndFoldoutHeaderGroup();
|
||||
}
|
||||
|
||||
private void DrawActionButtons()
|
||||
{
|
||||
EditorGUILayout.Space(10);
|
||||
|
||||
using (new EditorGUILayout.HorizontalScope())
|
||||
{
|
||||
if (GUILayout.Button("Select Scene Exporter"))
|
||||
{
|
||||
var exporter = FindObjectOfType<PSXSceneExporter>();
|
||||
if (exporter != null)
|
||||
{
|
||||
Selection.activeGameObject = exporter.gameObject;
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("Not Found", "No PSXSceneExporter in scene.", "OK");
|
||||
}
|
||||
}
|
||||
|
||||
if (GUILayout.Button("Open Scene Validator"))
|
||||
{
|
||||
PSXSceneValidatorWindow.ShowWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateNewLuaScript()
|
||||
{
|
||||
var exporter = target as PSXObjectExporter;
|
||||
string defaultName = exporter.gameObject.name.ToLower().Replace(" ", "_");
|
||||
string path = EditorUtility.SaveFilePanelInProject(
|
||||
"Create Lua Script",
|
||||
defaultName + ".lua",
|
||||
"lua",
|
||||
"Create a new Lua script for this object");
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
string template = $@"-- Lua script for {exporter.gameObject.name}
|
||||
--
|
||||
-- Available globals: Entity, Vec3, Input, Timer, Camera, Audio,
|
||||
-- Debug, Math, Scene, Persist
|
||||
--
|
||||
-- Available events:
|
||||
-- onCreate(self) — called once when the object is registered
|
||||
-- onUpdate(self, dt) — called every frame (dt = delta frames, usually 1)
|
||||
-- onEnable(self) — called when the object becomes active
|
||||
-- onDisable(self) — called when the object becomes inactive
|
||||
-- onCollision(self, other) — called on collision with another object
|
||||
-- onTriggerEnter(self, other)
|
||||
-- onTriggerStay(self, other)
|
||||
-- onTriggerExit(self, other)
|
||||
-- onInteract(self) — called when the player interacts
|
||||
-- onButtonPress(self, btn) — called on button press (btn = Input.CROSS etc.)
|
||||
-- onButtonRelease(self, btn)
|
||||
-- onDestroy(self) — called before the object is destroyed
|
||||
--
|
||||
-- Properties: self.position (Vec3), self.rotationY (pi-units), self.active (bool)
|
||||
|
||||
function onCreate(self)
|
||||
-- Called once when this object is registered in the scene
|
||||
end
|
||||
|
||||
function onUpdate(self, dt)
|
||||
-- Called every frame. dt = number of elapsed frames (usually 1).
|
||||
end
|
||||
|
||||
function onInteract(self)
|
||||
-- Called when the player interacts with this object
|
||||
end
|
||||
";
|
||||
System.IO.File.WriteAllText(path, template);
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var luaFile = AssetDatabase.LoadAssetAtPath<LuaFile>(path);
|
||||
if (luaFile != null)
|
||||
{
|
||||
luaFileProp.objectReferenceValue = luaFile;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MenuItem("CONTEXT/PSXObjectExporter/Copy Settings to Selected")]
|
||||
private static void CopySettingsToSelected(MenuCommand command)
|
||||
{
|
||||
var source = command.context as PSXObjectExporter;
|
||||
if (source == null) return;
|
||||
|
||||
foreach (var go in Selection.gameObjects)
|
||||
{
|
||||
var target = go.GetComponent<PSXObjectExporter>();
|
||||
if (target != null && target != source)
|
||||
{
|
||||
Undo.RecordObject(target, "Copy PSX Settings");
|
||||
// Copy via serialized object
|
||||
EditorUtility.CopySerialized(source, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user