Unity's .SetActive vs .SetActiveRecursively
A story of how fixing warnings broke all my code. It's all very well obsoleting this stuff, but you have to make sure it's still compatible...
At some point during the 4.x cycle, Unity switched the way that a GameObject
is activated and deactivated in a scene at runtime. Previously, there was a boolean .active
property, and a method called SetActiveRecursively()
that could be used. This leads to some confusion: if I set a.active = false
then what is the state of a child of a
? Assuming that all children of a
are automatically disabled too, what's the purpose of SetActiveRecursively()
?
The recent change obsoletes the original members and adds a single place to change state: the SetActive()
method. This new method sets the state only on the current GameObject but, as expected, all descendant objects are automatically disabled. Two readonly properties can be queried to examine the active state of a GameObject: activeSelf
will return the value set through SetActive()
, where activeInHierarchy
will return true
only if the GameObject and all of its parents are enabled. It's possible therefore for activeSelf
to be true
even though activeInHierarchy
is false
- when a parent object has been disabled - and in this case the GameObject is disabled in the scene.
This is a Good Thing: it makes the API much clearer and more intuitive. However, it's not quite backward compatible, as I found out after tidying up some code.
I'd addressed some compiler warnings by replacing some obsolete SetActiveRecursively()
calls with SetActive()
. Suddenly, things went awry: runtime errors spewing through the console. My "quick tidy-up" had broken everything! What had happened is that SetActive()
isn't a drop-in replacement for SetActiveRecursively()
, even though the compiler warning suggests the switch.
When you call SetActive()
, the change in value doesn't take place until the end of the current frame. With the old methods, the change would happen immediately. This coul be a hugely important distinction because, in my case, the following line of code was a call to GetComponentInChildren
- which explicitly only returns components from active GameObjects. The call was returning nothing, because the object wasn't going to be active until the end of the frame.
The available solutions were to introduce a frame's delay (which gives a whole slew of new problems), switch the call to GetComponentsInChildren
(which has an override that will include results from inactive objects, but that can get messy in a complex scene), or continue using the obsolete call. For the sake of not breaking existing code, I had to stick with the obsolete call.
This highlights why backward compatibility in an API can be massively important - and why it's doubly important to have the behaviours of a public API well-documented: subtle changes can have huge effects!