Search code examples
c#visual-studiounity-game-enginegame-development

Method is Unexpectedly Called in Unity C#


I'm working on an optimization for a learning project where I'm recreating the basic functionality of Minecraft.

I've successfully implemented optimizations for the border and corner cubes of chunks. When the player triggers the chunk generation process, cubes on the borders and corners that are fully covered on all sides are either disabled (if they already exist) or not instantiated (if they are new cubes for a new chunk).

To make the code cleaner, I decided to move the border and corner cube optimizations into separate classes, each inheriting from a MapOptimization script. I started with the BorderOptimization script and used an onIsBorderCube action to trigger the BorderCubeOptimizationSequence. I've subscribed to the onIsBorderCube action in the Start() method of BorderOptimization. While the action is invoked correctly, I'm encountering a NullReferenceException after finishing each valid iterations.

The exception occurs right after the ProcessAllCubeDataOfUpcomingChunk method finishes. For some reason, the ProcessAllCubeDataOfUpcomingChunk method is being called again, but this time the onIsBorderCube action is null, causing the crash.

MapOptimization script bellow ⬇️

The script is not full, I've removed other things to keep preview brief. If you miss anything let me know and I will add it.

public class MapOptimization : MonoBehaviour
{
    protected Action<Dictionary<Vector3, CubeData>, CubeData, Border> onIsBorderCube;

    private void ProcessAllCubeDataOfUpcommingChunk(Dictionary<Vector3, CubeData> newChunkFieldData, Vector3 centerOfNewChunk)
    {
        centerOfXNegativeNeighbourChunk = new Vector3(centerOfNewChunk.x - mapGenerator.gridSize.x, centerOfNewChunk.y, centerOfNewChunk.z);
        centerOfXPositiveNeighbourChunk = new Vector3(centerOfNewChunk.x + mapGenerator.gridSize.x, centerOfNewChunk.y, centerOfNewChunk.z);
        centerOfZNegativeNeighbourChunk = new Vector3(centerOfNewChunk.x, centerOfNewChunk.y, centerOfNewChunk.z - mapGenerator.gridSize.z);
        centerOfZPositiveNeighbourChunk = new Vector3(centerOfNewChunk.x, centerOfNewChunk.y, centerOfNewChunk.z + mapGenerator.gridSize.z);

        XNegativeCorner = centerOfNewChunk.x - Mathf.Ceil((float)mapGenerator.gridSize.x / 2.0f) + 1.0f;
        XPositiveCorner = centerOfNewChunk.x + Mathf.Ceil((float)mapGenerator.gridSize.x / 2.0f) - 1.0f;
        ZNegativeCorner = centerOfNewChunk.z - Mathf.Ceil((float)mapGenerator.gridSize.x / 2.0f) + 1.0f;
        ZPositiveCorner = centerOfNewChunk.z + Mathf.Ceil((float)mapGenerator.gridSize.x / 2.0f) - 1.0f;

        foreach (KeyValuePair<Vector3, CubeData> newCubeData in newChunkFieldData)
        {
            OptimizeDataOfNewChunk(newCubeData.Value, centerOfNewChunk, newChunkFieldData);
        }
    }

    private void OptimizeDataOfNewChunk(CubeData newCubeData, Vector3 centerOfNewChunk, Dictionary<Vector3, CubeData> newChunkFieldData)
    {
        Border newChunkBorder = Border.Null;
        if (isCubeAtBorder(newCubeData.position, ref newChunkBorder))
        {
            Corner newCubeCorner = Corner.Null;
            if (isNewCubeAtCorner(newCubeData, ref newCubeCorner))
            {
                CornerCubeOptimizationSequence(newCubeData, centerOfNewChunk, newChunkBorder, newCubeCorner);
            }
            else
            {
                onIsBorderCube(newChunkFieldData, newCubeData, newChunkBorder);
            }
        }
        else
        {
            DeactiavateSurroundedCubeData(newCubeData, newChunkFieldData);
        }
    }
}

BorderOptimization script bellow ⬇️

The script is not full, I've removed other things to keep it preview brief. If you miss anything let me know and I will add it.

public class BorderOptimization : MapOptimization 
{
    void Start()
    {
        Debug.Log("onIsBorder is subscribbed");
        onIsBorderCube += BorderCubeOptimizationSequence;
    }

    private void BorderCubeOptimizationSequence(Dictionary<Vector3, CubeData> newChunkFieldData, CubeData newCubeData, Border newChunkBorder)
    {
        Border neighbourChunkBorder = Border.Null;
        Vector3 neighbourCubePosition = Vector3.zero;
        Vector3 neighbourChunkCenter = Vector3.zero;

        SetNeighborChunkValues(newChunkBorder, newCubeData.position, ref neighbourChunkBorder, ref neighbourCubePosition, ref neighbourChunkCenter);

        if (!DoesNeighborChunkExist(neighbourChunkCenter))
        {
            return;
        }

        Dictionary<Vector3, CubeParameters> neighbourChunkField = mapGenerator.dictionaryOfCentersWithItsChunkField[neighbourChunkCenter];
        // Return if New Cube in New Chunk isn't surrounded with cubes from each sides
        if (!IsBorderCubeSurrounded<CubeData, CubeParameters>(newChunkFieldData, newCubeData.position, neighbourChunkField, neighbourCubePosition, newChunkBorder))
        {
            return;
        }
        newCubeData.isCubeDataSurrounded = true;

        // Return if Neighbor Cube in Neighbor Chunk isn't surrounded with cubes from each sides
        if (!IsBorderCubeSurrounded<CubeParameters, CubeData>(neighbourChunkField, neighbourCubePosition, newChunkFieldData, newCubeData.position, neighbourChunkBorder))
        {
            return;
        }
        neighbourChunkField[neighbourCubePosition].cubeInstance.gameObject.SetActive(false);
    }
}

The issue I’m facing isn't that the onIsBorderCube action is throwing a NullReferenceException, but that the ProcessAllCubeDataOfUpcommingChunk method is being called an extra time, which it shouldn't be.

Steps I've Taken:

  1. I reverted to a previous implementation, and in that version, the extra call to ProcessAllCubeDataOfUpcommingChunk doesn't occur. This suggests that the problem might not lie in the onIsBorderCube action itself, but elsewhere in the code.

  2. I’ve reviewed all references to ProcessAllCubeDataOfUpcommingChunk and confirmed it should only be called once within the corresponding script.

  3. While debugging, I noticed that when the method is called that extra time, the debugger doesn't return to the place where ProcessAllCubeDataOfUpcommingChunk has been been called.

  4. I’ve also checked the call stack for both valid and invalid iterations of this method call. Oddly, the call stacks look identical for both cases, despite one resulting in an extra invocation.

Valid iteration enter image description here

Invalid iteration enter image description here

I suppose that it might be occurring due to Order of Execution for Event Functions, but struggling to find the reason why.

Here I'm sending a video how it looks like when I'm debugging the issue.


Solution

  • The issue was in my misunderstanding how inheritance works.

    If I put Base script and Derived script on the same gameobject, then there will be created two instances of Base script and ProcessAllCubeDataOfUpcomingChunk will be called twice (once per instance).