You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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错误。

修复步骤

  1. 重构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;
    }

    // 原有属性和方法保持不变...
}
  1. 更新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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 06:19:52