Luxel Operation | DevBlog

Development Blog for Luxel Operation's upcoming games.

Archive for the ‘ Unity ’ Category

I have been working on implementing Ads into LuxelOperation’s current project for the past week or so. Having finally wrapped up all of the loose ends I turned it over to one of my routine play testers. He immediately found what was not only a game breaking bug, but a show stopping bug and I didn’t have an easy answer (after looking at the log output) of why.

Our project has a UI overlay that closes two doors over the scene when we change scenes. This covers up the scene change and allows us to give an obvious loading screen if it’s going to be more than an instantaneous change. We decided to trigger the overlay to “close” the doors, and then display an ad, and after the user returns from watching the ad the doors would open back up. This gave some notification that an Ad was about to happen and really helps on older devices where the change in Android Activity’s is a sluggish and jarring process (something which is almost painless on modern devices fortunately).

The Problem:

What was happening in play tests was that the first time you began playing the game in a session, everything would be peachy. The ads would show up fine, the doors would open and close and everything went exactly as planned. However, if you returned to the main menu and then went back into the Gameplay scene as soon as the first ad finished playing the doors would never open and the game would be unplayable.

A short time looking through the logs later showed that the script that moved the UI components was suddenly throwing NullReferenceExceptions because it couldn’t find the doors… Which had worked fine up until we returned from the ad, and worked fine the first time the scene was loaded. This was very confusing because the doors are not persistent through scenes, so by reloading the Gameplay scene a second time it should have been exactly like the first time. (The doors do not use Unity’s DontDestroyOnLoad).

Many hours of printf-style debugging later, I finally came to the conclusion that the MonoBehaviour that was calling the shots was null. Except, the other MonoBehaviours it referenced weren’t null, but things they referenced were null! Confused and frustrated, I called it quits for the night only to have something tickle the back of my brain… C# Events.

As near as I can tell, the final score is this:

  1. GameObject “A” registered a delegate to an Event that takes place on GameObject “B” (which was marked as DontDestroyOnLoad (and thus was persistent)).
  2. The Scene was loaded, the Delegate was registered, and everything was fine.
  3. The Scene was unloaded, and everything was destroyed. Except the GameObject still had a reference to it the form of that Delegate (as far as the C# Garbage Collector is concerned), so the GameObject persisted in C# memory but not in the Unity Hierarchy.
  4. The Scene was reloaded a short time later and a new copy of it was made and a second event was registered.
  5. The callback was called when an ad finished and this event was broadcast to both copies of the GameObject, the real one (from the second scene load), and the ‘ghost’ one (which only persisted in C# memory).
  6. The ‘ghost’ GameObject tried to reference other GameObjects from the first time the scene was loaded which no longer existed and threw a NullReferenceException.

The Fix:

void OnDisable()
{
	LuxelAds.OnAdClosed -= OnAdFinishedCallback;
}

Yup, just one line.

Best Practices for C# Events in Unity:

When using Events on Unity GameObjects, they should register their callbacks in the OnEnable() function, and unregister their callbacks in the OnDisable() function. The reason we do this in OnEnable and OnDisable (instead of Awake() and Destroy()) is so that code does not get executed on an Inactive GameObject (which probably leads to more debugging nightmares).

Earlier today I had a situation where I wanted to be able to place an element outside of the users screen while using a UIAnchor set to Center.
The reason for this was that the element needed to line up with a duplicate element in the center of the screen without overlapping. The two elements in this example are two halves of a full screen overlay (a “panel”) designed to hide the scene for menu transitions.

the_goal

The easiest method to get the two UI panels to get to the center and line up pixel perfect is to set the Pivot point on the left panel to be on the right side, and on the right panel do the opposite, setting the pivot to be the left center. This meant that when the localPosition of both panels was set to 0,0,0 they would line up perfectly in the middle of the screen without overlapping and it’d work on all resolutions/aspect ratios. (A UI Stretch component was added that set their Relative Size’s X to .5, so they each take up half of the width. The UI Root in this case is set to FixedSize)

Now came other half of the equation: Ensuring that each of these panels would start off screen and non-visible to the player so they could smoothly close without starting halfway onscreen, etc. A simple bit of math tells us that if we take Unity’s Screen.width and divide that by two then we could simply set the localPosition of each panel to be that far and they’d show up off screen. However, upon testing this I noticed a problem: If the code was run on anything but the ‘native’ resolution (1280×800 in this case), the panels would either show up too far off screen, or still be onscreen!

whats_happening

The reason for this is that when NGUI uses the FixedSize and it scales everything up/down from the 800 pixel height, it also impacts the horizontal positioning of elements. Why this specifically happens is unclear, but the fix is easy enough.

Instead of:

int panelOffset = Screen.width/2;
LeftPanel.transform.localPosition = new Vector3(-panelOffset, 0, 0);
RightPanel.transform.localPosition = new Vector3(panelOffset, 0, 0);

We use this neat little function, UIRoot.GetPixelSizeAdjustment. While the docs are unfortunately fairly vague, it’s pretty simple to use in practice:

int paneloffset = Mathf.FloorToInt(UIRoot.GetPixelSizeAdjustment(LeftPanel)*(Screen.width/2f))+1;
LeftPanel.transform.localPosition = new Vector3(-paneloffset, 0, 0);
RightPanel.transform.localPosition = new Vector3(paneloffset, 0, 0);

The reason for the Mathf.FloorToInt(…)+1 is to ensure that the elements definitely stay off screen. Due to floating point calculations the panels will often fall on a half or third pixel on the inside of the screen. By flooring it and adding one we ensure that we stay on a whole pixel and we stay off screen.

Tada!