Unity ScriptedImporter报错:无法添加空对象问题求助
问题排查:Unity ScriptedImporter 导入 Heightmap 时抛出 Null 错误
错误信息
InvalidOperationException: Cannot add a null object: filename
UnityEditor.AssetImporters.AssetImportContext.AddObjectToAsset(System.String identifier, UnityEngine.Object obj)
相关代码
Heightmap 类
using System.IO; using System; using UnityEngine; namespace Aurora.TerrainTools { /// <summary> /// Represents a heightmap for terrain generation and manipulation. /// </summary> [Serializable] public class Heightmap : UnityEngine.Object { #region Constants private const ushort SOI = 0x0315; private const ushort EOI = 0x0804; #endregion #region Fields private readonly int m_Width; private readonly int m_Height; private readonly float [] m_Data; #endregion #region Properties /// <summary> /// Gets the width of the heightmap. /// </summary> public int Width => m_Width; /// <summary> /// Gets the height of the heightmap. /// </summary> public int Height => m_Height; /// <summary> /// Gets or sets the heightmap value at the specified coordinates. /// </summary> /// <param name="x">The x-coordinate.</param> /// <param name="y">The y-coordinate.</param> /// <returns>The heightmap value at the specified coordinates.</returns> /// <exception cref="ArgumentOutOfRangeException">Thrown when coordinates are out of map range.</exception> public float this [ int x, int y ] { get { if ( !InMapRange ( x, y ) ) throw new ArgumentOutOfRangeException ( "coordinates" ); return m_Data [ x + ( y * m_Width ) ]; } set { if ( !InMapRange ( x, y ) ) throw new ArgumentOutOfRangeException ( "coordinates" ); m_Data [ x + ( y * m_Width ) ] = value; } } #endregion #region Constructors /// <summary> /// Initializes a new instance of the Heightmap class with the specified dimensions. /// </summary> /// <param name="width">The width of the heightmap.</param> /// <param name="height">The height of the heightmap.</param> public Heightmap ( int width, int height ) { m_Width = width; m_Height = height; m_Data = new float [ width * height ]; } /// <summary> /// Initializes a new instance of the Heightmap class by loading data from a stream. /// </summary> /// <param name="stream">The stream containing heightmap data.</param> /// <exception cref="IOException">Thrown if the heightmap data is invalid or loading fails.</exception> public Heightmap ( Stream stream ) { using ( BinaryReader reader = new ( stream ) ) { try { if ( reader.ReadUInt16 () == SOI ) { m_Width = reader.ReadInt32 (); m_Height = reader.ReadInt32 (); m_Data = new float [ m_Width * m_Height ]; for ( int i = 0; i < m_Data.Length; i++ ) { m_Data [ i ] = reader.ReadSingle (); } if ( reader.ReadUInt16 () != EOI ) { throw new IOException ( "Invalid heightmap" ); } } else { throw new IOException ( "Invalid heightmap" ); } } catch ( Exception e ) { string message = $"Error loading heightmap: {e.Message}"; Debug.LogError ( message ); throw new IOException ( message ); } Debug.Log ( "Success in loading, why is it null" ); } } #endregion #region Methods /// <summary> /// Checks if the given coordinates are within the map range. /// </summary> /// <param name="x">The x-coordinate.</param> /// <param name="y">The y-coordinate.</param> /// <returns><c>true</c> if the coordinates are within the map range, otherwise <c>false</c>.</returns> public bool InMapRange ( int x, int y ) => x >= 0 && x < m_Width && y >= 0 && y < m_Height; /// <summary> /// Saves the heightmap data to a stream. /// </summary> /// <param name="stream">The stream to save the heightmap data to.</param> public void Save ( Stream stream ) { using ( BinaryWriter writer = new ( stream ) ) { try { writer.Write ( SOI ); // Start of Image (SOI) writer.Write ( m_Width ); writer.Write ( m_Height ); foreach ( float i in m_Data ) { writer.Write ( i ); } writer.Write ( EOI ); // End of Image (EOI) } catch ( Exception e ) { Debug.LogError ( "Error saving heightmap: " + e.Message ); } } } public void Normalize () => Normalize ( 0, 1 ); public void Normalize ( float minHeight, float maxHeight ) { float minValue = float.MaxValue; float maxValue = float.MinValue; for ( int i = 0; i < m_Data.Length; i++ ) { minValue = Mathf.Min ( minValue, m_Data [ i ] ); maxValue = Mathf.Max ( maxValue, m_Data [ i ] ); } for ( int i = 0; i < m_Data.Length; i++ ) { m_Data [ i ] = Mathf.Lerp ( minHeight, maxHeight, Mathf.InverseLerp ( minValue, maxValue, m_Data [ i ] ) ); } } public void NormalizeToInt ( int minHeight, int maxHeight ) { float minValue = float.MaxValue; float maxValue = float.MinValue; for ( int i = 0; i < m_Data.Length; i++ ) { minValue = Mathf.Min ( minValue, m_Data [ i ] ); maxValue = Mathf.Max ( maxValue, m_Data [ i ] ); } for ( int i = 0; i < m_Data.Length; i++ ) { m_Data [ i ] = Mathf.Round ( Mathf.Lerp ( minHeight, maxHeight, Mathf.InverseLerp ( minValue, maxValue, m_Data [ i ] ) ) ); } } #endregion } }
HeightmapImporter 类
using System; using System.IO; using UnityEditor.AssetImporters; using UnityEngine; namespace Aurora.TerrainTools.Editor { [ScriptedImporter ( 1, "heightmap", AllowCaching = true )] public class HeightmapImporter : ScriptedImporter { [SerializeField, HideInInspector] private Heightmap m_Heightmap; public override void OnImportAsset ( AssetImportContext ctx ) { using ( FileStream stream = new FileStream ( ctx.assetPath, FileMode.Open, FileAccess.Read ) ) { m_Heightmap = new Heightmap ( stream ); ctx.AddObjectToAsset ( Path.GetFileNameWithoutExtension ( ctx.assetPath ), m_Heightmap ); ctx.SetMainObject ( m_Heightmap ); } } } }
问题原因与解决方案
核心问题
Unity中继承自UnityEngine.Object的类不能直接用new关键字实例化,必须通过ScriptableObject.CreateInstance<T>()这类Unity提供的对象创建方法生成。直接new Heightmap()会导致对象未被正确初始化,在Editor的Asset导入流程中会被判定为null,从而触发Cannot add a null object错误。
修复步骤
- 重构Heightmap类:
- 将类改为继承
ScriptableObject(更适合作为可序列化的Asset对象)。 - 移除公开构造函数,改用静态工厂方法创建实例,内部调用
ScriptableObject.CreateInstance。 - 将原本的
readonly字段改为[SerializeField],确保Unity能正确序列化数据。
- 将类改为继承
修改后的Heightmap类关键部分:
[Serializable] [CreateAssetMenu(fileName = "New Heightmap", menuName = "Terrain Tools/Heightmap")] public class Heightmap : ScriptableObject { [SerializeField] private int m_Width; [SerializeField] private int m_Height; [SerializeField] private float[] m_Data; private const ushort SOI = 0x0315; private const ushort EOI = 0x0804; // 私有构造函数,禁止外部直接new private Heightmap() {} public static Heightmap Create(int width, int height) { var heightmap = CreateInstance<Heightmap>(); heightmap.m_Width = width; heightmap.m_Height = height; heightmap.m_Data = new float[width * height]; return heightmap; } public static Heightmap LoadFromStream(Stream stream) { var heightmap = CreateInstance<Heightmap>(); using (BinaryReader reader = new BinaryReader(stream)) { try { if (reader.ReadUInt16() == SOI) { heightmap.m_Width = reader.ReadInt32(); heightmap.m_Height = reader.ReadInt32(); heightmap.m_Data = new float[heightmap.m_Width * heightmap.m_Height]; for (int i = 0; i < heightmap.m_Data.Length; i++) { heightmap.m_Data[i] = reader.ReadSingle(); } if (reader.ReadUInt16() != EOI) { throw new IOException("Invalid heightmap"); } } else { throw new IOException("Invalid heightmap"); } } catch (Exception e) { string message = $"Error loading heightmap: {e.Message}"; Debug.LogError(message); DestroyImmediate(heightmap); // 创建失败时销毁无效对象 throw new IOException(message); } } return heightmap; } // 原有属性和方法保持不变... }
- 更新Importer代码:
将实例化方式改为调用静态工厂方法,并添加空值检查:
public override void OnImportAsset(AssetImportContext ctx) { using (FileStream stream = new FileStream(ctx.assetPath, FileMode.Open, FileAccess.Read)) { try { m_Heightmap = Heightmap.LoadFromStream(stream); if (m_Heightmap != null) { ctx.AddObjectToAsset(Path.GetFileNameWithoutExtension(ctx.assetPath), m_Heightmap); ctx.SetMainObject(m_Heightmap); } else { ctx.LogImportError("Failed to load valid heightmap data"); } } catch (IOException e) { ctx.LogImportError($"Heightmap import failed: {e.Message}"); } } }
额外说明
ScriptableObject是Unity专门用于存储可序列化数据的类,适合作为自定义Asset类型的基类。- 直接
new继承自UnityEngine.Object的对象会绕过Unity的对象管理系统,导致对象无法被Editor正确识别。 - 添加空值检查和异常捕获,能让导入流程更健壮,便于定位问题。
内容的提问来源于stack exchange,提问作者RWolfe
相关产品推荐
相关产品推荐

