user3280790 user3280790 - 2 months ago 23
C# Question

Transform.Find sends null value

I am making a simple deck/card project. My project has a card prefab, with a Canvas child which would contain the UI version of the suit/card information. The information can be displayed with one card, but has problems when it comes to multiple cards. My theory on the matter is due to GameObject.FindGameObjectWithTag(string) finds the first instance of the gameObject that holds the tag. So, when I draw multiple cards, it will just rewrite the first card, rather than draw on the other cards.

I have tried Transform.Find(string), but it has turned up null, despite having a game object set in the Editor. One solution would be to use multiple tags with numbers after the name, ala topNum1, topNum2, etc., but doing that for 52 different numbers 3 times each sounds very repetitive, and frustrating. Is there a better way to do this?

Card code below:

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

/// <summary>
/// This class sets up the card object, which can be used in a variety of games.
/// </summary>

public class Card: MonoBehaviour
{

int cardNum;//The Value of the card.
int cardType;//The ‘Suit’ of the card. Hearts, Spades, Diamonds, Clubs
//Text topText;
Text botText;
Text faceText;
Text topText;

//Creates a default card.
public Card()
{
cardNum = -1;
cardType = -1;
topText = GameObject.FindGameObjectWithTag("topText").GetComponent<Text>();
//topText = gameObject.transform.FindChild("Canvas").gameObject.transform.FindChild("TopCardValue").GetComponent<Text>();
botText = GameObject.FindGameObjectWithTag("botText").GetComponent<Text>();
faceText = GameObject.FindGameObjectWithTag("faceText").GetComponent<Text>();
}
//Creates a custom card, with the provided values.
public Card(int cN, int cT)
{
cardNum = cN;
cardType = cT;
topText = GameObject.FindGameObjectWithTag("topText").GetComponent<Text>();
//topText = gameObject.transform.FindChild("Canvas").gameObject.transform.FindChild("TopCardValue").GetComponent<Text>();
botText = GameObject.FindGameObjectWithTag("botText").GetComponent<Text>();
faceText = GameObject.FindGameObjectWithTag("faceText").GetComponent<Text>();
}

//returns the card’s value.
public int getCardNum()
{
return cardNum;
}

//returns the card’s suit.
public int getCardType()
{
return cardType;
}

//Sets the card’s value.
public void setCardNum(int newNum)
{
cardNum = newNum;
}

//Sets the card’s suit.
public void setCardType(int newType)
{
cardType = newType;
}

//Checks if the card’s value is a face card (Jack, Queen, King, or Ace)
public bool checkIfFace()
{
if (getCardNum() > 10 && getCardNum() < 15 || getCardNum() == 0)
return true;
else
return false;
}

//Checks if the card is a valid card.
public bool checkifValid()
{
if (getCardType() < -1 || getCardType() > 4)
{
Debug.LogError("Error: Card Type not valid. Card type is: " + getCardType());
return false;
}
if (getCardNum() < 0 || getCardNum() > 15)
{
Debug.LogError("Error: Card Value not valid. Card value is : " + getCardNum());
return false;
}
return true;
}
//Prints out the card information.
public void printOutCardInfo()
{
string value = "";
string suit = "";


if (getCardNum() == 1)
value = (" Ace");
else if (getCardNum() > 1 && getCardNum() < 11)
value = (getCardNum().ToString());
else if (getCardNum() == 11)
value = ("Jack");
else if (getCardNum() == 12)
value = ("Queen");
else if (getCardNum() == 13)
value = ("King");
else
Debug.LogError("Error: No Num Found! The number in question is: " + getCardNum());
switch(getCardType())
{
case 0:
suit = ("Hearts");
break;
case 1:
suit = ("Spades");
break;
case 2:
suit = ("Diamonds");
break;
case 3:
suit = ("Clubs");
break;
default:
Debug.LogError("Error: Suit not found.");
break;
}
topText.text = value;
botText.text = value;
faceText.text = suit;
}
}


Any and all help would be greatly appreciated. Thank you for your time.

EDIT: Since a few people asked for it, I have edited this question to include the code that this class is called in:

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

public class ListDeck : MonoBehaviour
{

//Card[] deckOfCards;
List<Card> deckOfCards;

// public GameObject spawner;
//public GameObject card;

//The default constructor.
public ListDeck()
{
deckOfCards = new List<Card>();
setUpDeck(4, 14);
randomizeDeck();
}

//Sets up the deck
public void setUpDeck(int numSuits, int numValues)
{
int counter = 0;

for (int i = 1; i < numValues; i++)//the thirteen values.
{
for (int j = 0; j < numSuits; j++)//The four suits.
{
Debug.Log("I value: " + i + " j value: " + j);
deckOfCards.Add(new Card(i, j));
counter++;//Increments the counter.
}
}
}
//Randomizes the deck so that the card dealout is random.
//http://answers.unity3d.com/questions/486626/how-can-i-shuffle-alist.html
public void randomizeDeck()
{
for (int i = 0; i < deckOfCards.Count; i++)
{
Debug.Log(i);
Card temp = deckOfCards[i];
int randomIndex = Random.Range(i, deckOfCards.Count);
deckOfCards[i] = deckOfCards[randomIndex];
deckOfCards[randomIndex] = temp;
}
}
//Prints out the deck for the game.
public void printOutDeck()
{
for (int i = 0; i < deckOfCards.Count; i++)
{
Debug.Log("Card " + i + ": ");
deckOfCards[i].printOutCardInfo();
}
}

public List<Card> getDeck()
{
return deckOfCards;
}

public void transferCards(List<Card> deckTo, int numCards)
{

for (int i = 0; i < numCards; i++)
{
deckTo.Add(deckOfCards[0]);
deckOfCards.RemoveAt(0);
}
}
}

Answer

This is because you are calling the Unity API functions from a constructor function. Basically, you are doing this during deserialization and in another Thread.

In Unity 5.3.4f1 and below, the Find function will silently fail when called from a constructor and you won't know. This is one of the mistakes that is complicated to track in Unity.

In Unity 5.4 and above, Unity decided to add error message to alert you about this problem. You won't see it now because you are still using 5.3. The error is as fellow:

FindGameObjectWithTag is not allowed to be called from a MonoBehaviour constructor (or instance field initializer), call it in Awake or Start instead. Called from MonoBehaviour 'Card' on game object 'Cube'.

Similar error message will appear when the Find function is called in a constructor function.

Continue reading for more descriptive information and solution:

Inheriting from MonoBehaviour vs not inheriting from MonoBehaviour

Inheriting from MonoBehaviour:

1.You can attach the script to a GameObject.

2.You can't use the new keyword to create a new instance of a script that inherits from MonoBehaviour. Your deckOfCards.Add(new Card(i, j)); is wrong in this case since Card inherits from MonoBehaviour.

3.You use gameobject.AddComponent<Card>() or the Instantiate(clone prefab) function to create new instance of script. There is an example at the end of this.

Rules for using a constructor function in Unity:

1.Do not use a constructor in a script that inherits from MonoBehaviour unless you understand what's going on under the hood in Unity.

2.If you are going to use a constructor, do not inherit the script from MonoBehaviour.

3.If you break Rule #2, do not use any Unity API in a constructor function of a class that inherits from MonoBehaviour.

Why?

You cannot call Unity API from another Thread. It will fail. You will either get an exception or it will silently fail.

What does this have to do with Threads?

A constructor function is called from another Thread in Unity.

When a script is attached to a GameObject and that script inherits from MonoBehaviour and has a constructor, that constructor is first called from Unity's main Thread(which is fine) then it is called again from another Thread (non Unity's main Thread). This breaks rule #3. You cannot use Unity API from another function.

You can prove this by running the code below:

using UnityEngine;
using System.Threading;

public class Card : MonoBehaviour
{
    public Card()
    {
        Debug.Log("Constructor Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    }

    void Start()
    {
        Debug.Log("Start() function Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    }
    // Update is called once per frame
    void Update()
    {
        Debug.Log("Update() function Thread ID: " + Thread.CurrentThread.ManagedThreadId);
    }
}

Output when attached to a GameObject:

Constructor Thread ID: 1

Constructor Thread ID: 20

Start() function Thread ID 1

Update() function Thread ID: 1

As you can see, the Start() and Update() functions are called from the-same Thread (ID 1)which is the main Thread. The Constructor function is also called from the main Thread but then called again from another Thread (ID 20).

Example of BAD code: Because there is a constructor in a script that inherits from MonoBehaviour. Also bad because new instance is created with the new keyword.

public class Card : MonoBehaviour
{
    Text topText;

   //Bad, because  `MonoBehaviour` is inherited
    public Card()
    {
        topText = GameObject.FindGameObjectWithTag("topText").GetComponent<Text>();
    }
}

then creating new instance with the new keyword:

Card card = new Card(); //Bad, because MonoBehaviour is inherited

Example of Good code:

public class Card : MonoBehaviour
{
    Text topText;

    public Awake()
    {
        topText = GameObject.FindGameObjectWithTag("topText").GetComponent<Text>();
    }
}

then creating new instance with the AddComponent function:

Card card = gameObject.AddComponent<Card>()

OR clone from prefab with the Instantiate function:

public Card cardPrfab;
Card card = (Card)Instantiate(cardPrfab);

Not inheriting from MonoBehaviour:

1.You can't attach the script to a GameObject but you can use it from another script.

2.You can simply use the new keyword to create new instance of the script when it doesn't inherit from MonoBehaviour.

public class Card
{
    Text topText;
    //Constructor

   //Correct, because no `MonoBehaviour` inherited
    public Card()
    {
        topText = GameObject.FindGameObjectWithTag("topText").GetComponent<Text>();
    }
}

Then you can create new instance with the new keyword like:

Card card = new Card(); //Correct, because no MonoBehaviour inherited

Solution:

1.If you decide to inherit from MonoBehaviour and have to attach the script to a GameObject , you must remove all your constructor functions and put code inside them into Awake() or Start() function. The Awake() and Start() functions are automatically called by Unity once and you use them initialize your variables. You don't have to call them manually. Do not use the new keyword to create instance of scripts that inherit from MonoBehaviour.

2.If you decide not to inherit from MonoBehaviour and you are not required to attach the script to a GameObject, you can have a constructor function like you did in your current code and you can use Unity's API in those constructor functions.You can now use new keyword to create instance of the script since it doesn't inherit from MonoBehaviour.

Comments