Search code examples
c#unity-game-enginegame-development

How to correctly pass information between newly instantiated prefabs in Unity?


I am making snake game. Got the head of the snake moving, first body segment(a prefab gameObject) spawning when colliding with food "eating". First body segment following the head, by accessing heads last position before it moves and setting the body segments part to that.

The problem arose when I tried to make consecutive body segments to follow previously instantiated body segment.

In my approach, when I instantiate a new snake body segment I give it a reference to the segment that was spawned before it, using the reference I access the previously spawned segments last known transform.position and move the new body part there. For some reasson instead of moving one after another, all of the body segemtns follow the snake head part directly. Meaning all body segments get their position set to the lastPosition of snakeHead.

Snake Head script:

using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;

public class snakeHeadController : MonoBehaviour
{
    private string directionOfMovement;
    private float timer;
    public float delay = 0;
    public float movementIncrement = 1;
    public Vector3 lastPosition;
    public GameObject snakeBodyPart;
    GameObject newBodyPart;
    public GameObject lastCreatedBodyPart;
    int bodyPartNumber = 0;
    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        changeDirectionOfSnake();
        moveHead(directionOfMovement);
    }

    void moveHead(string directionOfMovement)
    {
        timer += Time.deltaTime;
        if (timer > delay)
        {
            lastPosition = transform.position;
            if (directionOfMovement == "Right")
            {
                transform.position += new Vector3(movementIncrement, 0, 0);
            }
            else if (directionOfMovement == "Left")
            {
                transform.position += new Vector3(-movementIncrement, 0, 0);
            }
            else if (directionOfMovement == "Up")
            {
                transform.position += new Vector3(0, movementIncrement, 0);
            }
            else if (directionOfMovement == "Down")
            {
                transform.position += new Vector3(0, -movementIncrement, 0);
            }
            timer -= delay;
        }
        
    }

    //function for determening direction of movement of the head
    void changeDirectionOfSnake()
    {
        if (Input.GetKeyDown(KeyCode.D) && directionOfMovement != "Left")
        {
            directionOfMovement = "Right";
        } else if(Input.GetKeyDown(KeyCode.A) && directionOfMovement != "Right")
        {
            directionOfMovement = "Left";
        } else if( Input.GetKeyDown(KeyCode.W) && directionOfMovement != "Down")
        {
            directionOfMovement = "Up";
        } else if (Input.GetKeyDown(KeyCode.S) && directionOfMovement != "Up")
        {
            directionOfMovement = "Down";
        }
    }

    //this function spawns a new body segment and passes a reference of the last created body segment to the newly created one
    public void spawnBodyPart() 
    {

        newBodyPart = Instantiate(snakeBodyPart, transform);
        newBodyPart.name = "snakeBodySegment" + bodyPartNumber.ToString();
        if (lastCreatedBodyPart  != null)
        {
            newBodyPart.GetComponent<snakeBodyScript>().getReferenceToBodyBeforeMe(lastCreatedBodyPart);
        }
        bodyPartNumber++;
        lastCreatedBodyPart = newBodyPart;
    }
}

Body Part(segment) script:

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

public class snakeBodyScript : MonoBehaviour
{
    private GameObject head;
    private snakeHeadController headScript;
    public GameObject bodyBeforeMe;
    public Vector3 myLastPostion;

    // Start is called before the first frame update
    void Start()
    {
        head = GameObject.Find("snakeHead");
        headScript = head.GetComponent<snakeHeadController>();
    }

    // Update is called once per frame
    void LateUpdate()
    {
        moveBody();
    }

    void moveBody()
    {
        if (bodyBeforeMe == null)
        {
            myLastPostion = transform.position;
            transform.position = headScript.lastPosition;
        } 
        else
        {
            myLastPostion = transform.position;
            transform.position = bodyBeforeMe.GetComponent<snakeBodyScript>().myLastPostion;
        }
    }

    public void getReferenceToBodyBeforeMe(GameObject bodyPart)
    {
        bodyBeforeMe = bodyPart;
    }
}

New body part gets spawned through the snakeFood script (when snake head collides with food) with this code: snakeHeadScript.spawnBodyPart();

To my eye the solution that I implemented seems like it should be working although it is convoluted with the passing of references and all that (I am sure there is a more elegant) way of doing this, but I just cant seem to understand why my approach does not work.


Solution

  • The problems in your solution

    1. You instatiate the new body part as a child of the head object:

      Instantiate(snakeBodyPart, transform);
      

      Therefore, every time the head object moves, all the body.position will also change accordingly.

    2. You move the head object with a delay, but in the body scripts, they are updated without a delay, so the bodies will always be moved to the head's position if delay is large enough.

    3. Unity does not guarantee the execution order of event methods for different objects.

      In general, you should not rely on the order in which the same event function is invoked for different GameObjects — except when the order is explicitly documented or settable.

      Your solution is based on the fact that new bodies must be updated after old ones. To avoid this situation, you must consider manually controlling this order.

    A better solution

    As derHugo suggested, you should control the snake in one script. Here are some modifications I made based on snakeHeadController, use a List to collect all body parts, so that you can use a loop to set their positions one after another.

    List<Transform> bodies = new List<Transform>();
    
    void MoveHead(string directionOfMovement)
    {
        timer += Time.deltaTime;
        if (timer > delay)
        {
            lastPosition = transform.position;
            ....
            if (bodies.Count > 0)
                MoveBodies();
        }
    }
    
    void MoveBodies()
    {
        var lastBodyPosition = bodies[0].position;
        bodies[0].position = lastPosition;
        for (int i = 1; i < bodies.Count; i++)
        {
            var currentPosition = bodies[i].position;
            bodies[i].position = lastBodyPosition;
            lastBodyPosition = currentPosition;
        }
    }
    
    public void SpawnBodyPart()
    {
        newBodyPart = Instantiate(snakeBodyPart); //remove the transform arg
        newBodyPart.name = "snakeBodySegment" + bodyPartNumber.ToString();
        bodyPartNumber++;
        bodies.Add(newBodyPart.transform);
    }