Obi Official Forum

Full Version: NullReferenceException in ObiRopeCursor (build)
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hi,

I am changing rope length in OnBlueprintLoaded callback. This works well in Editor, but in build I get
NullReferenceException in ObiRopeCursor script, line 131

Code:
NullReferenceException: Object reference not set to an instance of an object
at Obi.ObiRopeCursor.ChangeLength (System.Single newLength) [0x00001]
in C:\myProject\Assets\Obi\Scripts\RopeAndRod\Actors\ObiRopeCursor.cs:131

I guess that the rope reference in cursor is somehow null. How can this happen if it worked fine in Editor?

Thanks in advance for help
Hi!

Cursors initialize their reference to the rope component in OnEnable(). Actors -such as ropes- are initialized in OnEnable() too, and get their OnBlueprintLoaded called.

Since the order in which Unity calls events for different components is undefined by default (the specific order might change between editor/runtime, and even between different runs of the same scene) chances are the cursor is attempting to change the length of the rope before its own OnEnable() event has been called, so the reference to the rope is still null.

A solution is to change the length of the rope in Start, instead of OnBlueprintLoaded. Start() is guaranteed to be called after OnEnable().

let me know if you need further help,

kind regards
Thanks for your reply!

I tried moving that part of code to the Start method. Now I get NullReferenceException in this line in my code (in Editor):

Code:
int solverIndex = actor.solverIndices[i];

In debugger it shows that actor is good, but solverIndices is null. Seems like actor is not initialized all the way when I try to work with it inside my Start() method.

This is the whole codeblock I use -  it loads positions and velocities from json file and should set them to the rope:

Code:
var positions = reader.Read<List<Vector3>>("tether_positions");
var velocities = reader.Read<List<Vector3>>("tether_velocities");
for (int i = 0; i < positions.Count; ++i)
{
   int solverIndex = actor.solverIndices[i];
   actor.solver.positions[solverIndex] = positions[i];
   actor.solver.velocities[solverIndex] = velocities[i];
}
cursor.ChangeLength(reader.Read<float>("tether_length"));


Actually, while debugging - drom actor I see that solver's initialized flag is false
Could you share the entire script you're using?

(15-05-2023, 01:20 PM)natko1412 Wrote: [ -> ]In debugger it shows that actor is good, but solverIndices is null. Seems like actor is not initialized all the way when I try to work with it inside my Start() method.

Actors are initialized in OnEnable(), and that's called before Start() so the actor should be completely initialized by that point.

kind regards,
(16-05-2023, 08:40 AM)josemendez Wrote: [ -> ]Could you share the entire script you're using?


Actors are initialized in OnEnable(), and that's called before Start() so the actor should be completely initialized by that point.

kind regards,

Yes, here is the script

Code:
using System.Collections.Generic;
using UnityEngine;
using CI.QuickSave;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using Obi;

public class SaveLoadManager : MonoBehaviour
{

    public InputField inputField;
    public ObiActor actor;
    public ObiRope tether;
    public ObiRopeCursor cursor;
    public List<GameObject> ObjectsToSave;
    public GameObject TMS_ROV;
    QuickSaveWriter writer;
    QuickSaveReader reader;
    SettingsManager sm;

    void Start()
    {
        var settings = GameObject.Find("Settings");
        sm = settings.GetComponent<SettingsManager>();

        if (sm.saveFile != "")
        {
            Load(sm.saveFile);
        }
        sm.scenarioLoaded = true;
    }

    public void Save()
    {
        if (inputField.text == "") return;
        writer = QuickSaveWriter.Create($"{SceneManager.GetActiveScene().name} - {inputField.text}");
        int idx = 1;
        foreach (var obj in ObjectsToSave)
        {
            writer.Write($"{idx}_{obj.name}_position", obj.transform.position);
            writer.Write($"{idx}_{obj.name}_rotation", obj.transform.rotation);
            idx++;

            if (obj.name == "TMS")
            {
                var ctrl = obj.GetComponent<HcuTmsController>();
                writer.Write("latched", ctrl.latched);
                writer.Write("latch_button", ctrl.latchedButton);
            }

            if (obj.name == "a-frame_edit")
            {
                var ctrl = obj.GetComponent<UmbilicalController>();
                writer.Write("frame_angle", ctrl.angle);
            }
            if (obj.name == "BULLET")
            {
                writer.Write("bullet_minAngle", obj.GetComponent<HingeJoint>().limits.min);
                writer.Write("bullet_maxAngle", obj.GetComponent<HingeJoint>().limits.max);
                continue;
            }
        }
        List<Vector3> positions = new();
        List<Vector3> velocities = new();
        for (int i = 0; i < actor.solverIndices.Length; ++i)
        {
            int solverIndex = actor.solverIndices[i];

            positions.Add(actor.solver.positions[solverIndex]);
            velocities.Add(actor.solver.velocities[solverIndex]);
        }
        writer.Write("tether_positions", positions);
        writer.Write("tether_velocities", velocities);
        writer.Write("tether_length", tether.restLength);

        writer.Commit();
        Debug.Log("Save complete!");

    }

    public void Load(string saveFile="test")
    {
        HcuTmsController ctrl = null;

        reader = QuickSaveReader.Create(saveFile);
        int idx = 0;
        foreach (var obj in ObjectsToSave)
        {
            idx++;

            if (obj.name == "TMS")
            {
                ctrl = obj.GetComponent<HcuTmsController>();
                ctrl.latched = reader.Read<bool>("latched");
                ctrl.latchedButton = reader.Read<bool>("latch_button");
            }


            if (obj.name == "a-frame_edit")
            {
                obj.transform.position = reader.Read<Vector3>($"{idx}_{obj.name}_position");
                obj.transform.rotation = reader.Read<Quaternion>($"{idx}_{obj.name}_rotation");
                var ctrl2 = obj.GetComponent<UmbilicalController>();
                ctrl2.Init();
                ctrl2.startAngle = reader.Read<Quaternion>($"{idx}_{obj.name}_rotation").eulerAngles.x;
                ctrl2.Rotate();
                continue;
            }
            if (obj.name == "BULLET")
            {
                continue;
            }

            obj.transform.position = reader.Read<Vector3>($"{idx}_{obj.name}_position");
            obj.transform.rotation = reader.Read<Quaternion>($"{idx}_{obj.name}_rotation");
        }

        // rope loading
        var positions = reader.Read<List<Vector3>>("tether_positions");
        var velocities = reader.Read<List<Vector3>>("tether_velocities");
        for (int i = 0; i < positions.Count; ++i)
        {
            int solverIndex = actor.solverIndices[i];
            actor.solver.positions[solverIndex] = positions[i];
            actor.solver.velocities[solverIndex] = velocities[i];
        }
        cursor.ChangeLength(reader.Read<float>("tether_length"));

        Debug.Log("Load complete!");
    }
}

I found the culprit!

I am using the Mirror networking package and it is enabling networked objects just after the complete scene load.
Since my rope is treated by Mirror as networked object, it was actually executing OnEnable and other methods a bit later than the normal offline gameObjects would.

So, my settings manager would initialize before the rope.

Thank you for your help, it went over my head that Mirror could be messing with execution order.