-3

Creating a basic snake game.

Trying to get a gameobject to spawn within the limits of a grid, with a sprite. However, when attmpting to call the function that spawns the object, I am getting a NULL REFERENCE error. However, I am confident I have clearly created a reference to the object?

Snake.cs script (Error occurs at line: 'levelGrid.SnakeMoved(gridPosition)')

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Snake : MonoBehaviour
{
    private Vector2Int gridMoveDirection;
    private Vector2Int gridPosition;
    private float gridMoveTimer;
    private float gridMoveTimerMax;
    private LevelGrid levelGrid;

    public void Setup(LevelGrid levelGrid)
    {
        this.levelGrid = levelGrid;
    }


    private void Awake ()
    {
        gridPosition = new Vector2Int(10, 10);
        gridMoveTimerMax = .5f;
        gridMoveTimer = gridMoveTimerMax;
        gridMoveDirection = new Vector2Int(1, 0);
    }

    // Update is called once per frame
    private void Update()
    {
        HandleInput();
        HandleGridMovement();
    }

    private void HandleInput()
    {
        if (Input.GetKeyDown(KeyCode.UpArrow) && gridMoveDirection.y != -1)
        {
            gridMoveDirection.x = 0;
            gridMoveDirection.y = +1;
        }
        if (Input.GetKeyDown(KeyCode.DownArrow) && gridMoveDirection.y != +1)
        {
            gridMoveDirection.x = 0;
            gridMoveDirection.y = -1;
        }
        if (Input.GetKeyDown(KeyCode.LeftArrow) && gridMoveDirection.x != +1)
        {
            gridMoveDirection.x = -1;
            gridMoveDirection.y = 0;
        }
        if (Input.GetKeyDown(KeyCode.RightArrow) && gridMoveDirection.x != -1)
        {
            gridMoveDirection.x = +1;
            gridMoveDirection.y = 0;
        }
    }

    /* Explanation: HandleGridMovement()
     * gridMoveTimer += Time.deltaTime causes the variable of gridMove to incrediment in REAL TIME.
     * gridMoveMaxTime is initalized as the starting value of gridMoveTimer, every 0.5 seconds the if statement will take place
     * the if statement transform both the position of the head of the snake, alongside rotating it accordingly.
     */
    private void HandleGridMovement()
    {

        gridMoveTimer += Time.deltaTime;
        if (gridMoveTimer >= gridMoveTimerMax)
        {
            gridMoveTimer -= gridMoveTimerMax;
            gridPosition += gridMoveDirection;

            transform.position = new Vector3(gridPosition.x, gridPosition.y);
            transform.eulerAngles = new Vector3(0, 0, GetAngleFromVector(gridMoveDirection) - 90);//eurlerAngles r just the rotation angles.

            levelGrid.SnakeMoved(gridPosition);
        }

    }

    private float GetAngleFromVector(Vector2Int dir)
    {
        float n = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
        if (n < 0) n += 360;
        return n;
    }
}

LevelGrid.cs Script: (script that is not working)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LevelGrid
{

    private Vector2Int foodGridPosition;
    private GameObject foodGameObject;
    private int width;
    private int height;
    private Snake snake;

    public LevelGrid(int width, int height)
    {
        this.width = width;
        this.height = height;

        Debug.Log("here!");
        SpawnFood();
    }

    public void Setup(Snake snake)
    {
        this.snake = snake;
    }

    private void SpawnFood()
    {
        foodGridPosition = new Vector2Int(4,4);

        foodGameObject = new GameObject("FoodApple", typeof(SpriteRenderer));
        foodGameObject.GetComponent<SpriteRenderer>().sprite = GameAssets.i.foodSprite;
        foodGameObject.transform.position = new Vector3(foodGridPosition.x, foodGridPosition.y);
    }

    public void SnakeMoved(Vector2Int snakeGridPosition)
    {
        if (snakeGridPosition == foodGridPosition)
        {
            Object.Destroy(foodGameObject);
            SpawnFood();
        }
    }

}

Other two scripts for clarity sake:

GameHandler.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameHandler : MonoBehaviour
{

    [SerializeField] private Snake snake;
    
    private LevelGrid levelGrid;


    private void Start()
    {
        Debug.Log("GameHandler Called!");

        levelGrid = new LevelGrid(20, 20);

        snake.Setup(levelGrid);
        levelGrid.Setup(snake);
    }

}

GameAssets.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameAssets : MonoBehaviour
{

    public static GameAssets i;

    private void Awake()
    {
        i = this;
    }
    
    public Sprite snakeHeadSprite;
    public Sprite foodSprite;
}
raining
  • 9
  • 4
  • Do you attempt to use the `levelGrid` variable before running the Setup method? – gunr2171 Apr 24 '23 at 22:27
  • You dont make gameobjects with new ..... – BugFinder Apr 24 '23 at 22:30
  • @BugFinder structs and non-Unity classes can be instantiated with `new()`. Some Unity classes can be instantiated with `new()` as well. You're probably thinking of MBs and SOs. – TheNomad Apr 25 '23 at 02:57
  • @TheNomad yes but "GameObject" isnt one. You should instantiate it. – BugFinder Apr 25 '23 at 08:14
  • What do you mean? Of course it is! `Instantiate()` creates a clone, but you need an existing object to do it: a reference (set by the Inspector, `Find*()` or `GetComponent().gameObject`), a Resource or a Prefab. If you want to create a **new** one one, you use `new()`. See the documentation. https://docs.unity3d.com/ScriptReference/GameObject-ctor.html . True, both, cloning with creating, are instantiated and live as instances of the `GameObject` class internally, but the way to **create** is using `new()`. As proved by the public constructor and seen in the docs I linked. – TheNomad Apr 25 '23 at 09:00

1 Answers1

0

The problem is in Update(), but the "cause" is here: snake.Setup(levelGrid).

Unity has no guarantee in what order it will instantiate its objects. Also, Awake() gets called instantly upon creation, followed by Update(), so there is no guarantee that Setup() will be faster.

The best practice approach is doing this in Snake.Update():

private void Update()
{
    if (levelGrid == null)
        return;

    HandleInput();
    HandleGridMovement();
}

When you have variables that depend on some sort of flow logic and are dependent on timings or some external setup, never assume they are assigned. Periodic calls such as frame loops, rendering, physics updates and even repeating invoking has an advantage here as you can "skip" code execution if the code is not ready.

For single-fire functions, that's where you'll need proper design to not get loopholes. Luckily for you, you're in the previous category.

As derHugo said, you should also avoid cross-dependencies from a third dependency, like you have here: snake.Setup(levelGrid); levelGrid.Setup(snake);

While it might work in certain situations, remember that Unity has its own way of instantiating and its own idea of how you should do internal setup (such as Awake(), Start() or OnEnable() depending on the class and situation).

TheNomad
  • 892
  • 5
  • 6
  • that's no entirely true ... Unity calls `Awake` immediately followed by `OnEnable`. This is done for all objects that are active and enabled in the first phase. Then it calls `Start` for all. And only after those are all done the first `Update` pass goes. See https://docs.unity3d.com/Manual/ExecutionOrder.html .. in general you should not rely on `Awake` etc at all but rather have a central initializer (aca dependency injection handler) for your applications – derHugo Apr 25 '23 at 07:26
  • The main issue in my eyes already lies in the design of `snake.Setup(levelGrid); levelGrid.Setup(snake);` -> there shouldn't be this kind of criss-cross dependencies ever. Either one should know the other but not the other way round - or they shouldn't know each other at all but only the `GameHandler` and access the values from it when they need them (single source of truth) – derHugo Apr 25 '23 at 07:29
  • Of course. I know about the execution flow, but I was simplifying here. Awake and Start are almost the same, except for the exception of not being called when disabled. Without seeing the project, there can be a lot of things there, including even not having the object in the scene and it being loaded using `Resources`. A lot of factors can influence his flow. I just kept it to the point without going into more details. If he needs more info or I need info, I'll just edit the answer. Other than that, my answer should be fine from most POV. As for the cross-setup, I agree and will include it. – TheNomad Apr 25 '23 at 07:52