Unity 6 is finally here! According to the developers, this is the most stable version in the game engine history. Why don't we check it using a static code analyzer? We'll also take a brief look at the key features and enhancements the update brings.
Unity 6 is a global update that aims at improving engine performance and stability but goes far beyond that. The enhancements cover a variety of aspects: multiplayer tools, lighting system, rendering, post-processing, the XR-toolkit, some visual effects, and more. I'd also like to highlight a system for integrating AI models.
While the preview of Unity 6 has already been available since May 15, 2024, it seems that many people's curiosity about what makes this update special has only recently arisen. So, in the first part of the article, I'll give a brief overview of what I think are the most important enhancements. I won't go into too much detail, though. Instead, I'll provide links where you can find more information.
In the second part, you may want to test your code analysis skills by looking for potential errors found by the static analyzer in several code fragments of the open-source part of the engine. This exercise is a good way to learn from other people's mistakes and minimize the chance of making them in your own code.
Enjoy reading the article!
Okay, let's see what the Unity team has in store for us this time around.
The Sentis framework has been introduced. It enables developers to integrate pre-trained neural network models into the runtime environment and use them to implement features such as speech and object recognition, creation of intelligent NPCs, etc.
Figure N1 — Multiplayer Play Mode in action
The left side of the image doesn't have the ozone layer effect unlike the right one.
Underwater Volumetric fog
I found the mentioned changes to be the most significant, but this is by no means a complete list. You can find a more comprehensive catalog of improvements, as well as their detailed descriptions, here.
Now that we've learned about the enhancements, it's time to see whether the update has brought new bugs to the engine source code.
So, I've equipped myself with the PVS-Studio code analyzer and investigated the C# part of the current engine version (6000.0.21f1), uncovering some interesting potential and explicit errors. However, instead of simply describing them, I'd like to invite you to test your code-analysis skills. Challenge yourself to find errors in the given code fragments and figure out how to fix it. Each snippet has a full answer below it, so you can check your guesses.
Let the Hunger Games tests begin, and may the programmer's wits be ever in your favor! By the way, it'd be awesome if you could share your findings in the comments :)
public bool propagationHasStopped { get; }
public bool immediatePropagationHasStopped { get; }
public bool defaultHasBeenPrevented { get; }
public EventDebuggerCallTrace(IPanel panel, EventBase evt,
int cbHashCode, string cbName,
bool propagationHasStopped,
bool immediatePropagationHasStopped,
long duration,
IEventHandler mouseCapture): base(....)
{
this.callbackHashCode = cbHashCode;
this.callbackName = cbName;
this.propagationHasStopped = propagationHasStopped;
this.immediatePropagationHasStopped = immediatePropagationHasStopped;
this.defaultHasBeenPrevented = defaultHasBeenPrevented;
}
The analyzer message:
V3005. The 'this.defaultHasBeenPrevented' variable is assigned to itself. EventDebuggerTrace.cs 42.
For some reason, the defaultHasBeenPrevented property is assigned to itself during initialization. Since the property has no setter, its value will never change again and will always have the default value of false.
You can fix the issue by adding a new parameter to the constructor to initialize the property.
Too easy for you? Let's call this one a warm-up and see how you do next!
public void ParsingPhase(....)
{
....
SpriteCharacter sprite =
(SpriteCharacter)textInfo.textElementInfo[m_CharacterCount]
.textElement;
m_CurrentSpriteAsset = sprite.textAsset as SpriteAsset;
m_SpriteIndex = (int)sprite.glyphIndex;
if (sprite == null)
continue;
if (charCode == '<')
charCode = 57344 + (uint)m_SpriteIndex;
else
m_SpriteColor = Color.white;
}
The analyzer message:
V3095. The 'sprite' object was used before it was verified against null. Check lines: 310, 312. TextGeneratorParsing.cs 310.
The sprite variable is dereferenced before being checked for null:
m_CurrentSpriteAsset = sprite.textAsset as SpriteAsset;
m_SpriteIndex = (int)sprite.glyphIndex;
if (sprite == null)
continue;
If sprite is indeed null, this inevitably leads to an exception. Perhaps the developers didn't consider the importance of operation order when adding string dereferencing to the method. To avoid the issue, you can move the dereferences under the null check:
if (sprite == null)
continue;
m_CurrentSpriteAsset = sprite.textAsset as SpriteAsset;
m_SpriteIndex = (int)sprite.glyphIndex;
private static void CompileBackgroundPosition(....)
{
....
else if (valCount == 2)
{
if (((val1.handle.valueType == StyleValueType.Dimension) ||
(val1.handle.valueType == StyleValueType.Float)) &&
((val1.handle.valueType == StyleValueType.Dimension) ||
(val1.handle.valueType == StyleValueType.Float)))
{
.... = new BackgroundPosition(...., val1.sheet
.ReadDimension(val1.handle)
.ToLength());
.... = new BackgroundPosition(...., val2.sheet
.ReadDimension(val2.handle)
.ToLength());
}
else if ((val1.handle.valueType == StyleValueType.Enum)) &&
(val2.handle.valueType == StyleValueType.Enum)
....
{
}
The analyzer message:
V3001. There are identical sub-expressions to the left and to the right of the '&&' operator. StyleSheetApplicator.cs 169.
The analyzer prompts that two identical expressions are executed using the && operator in one of the conditions:
if (((val1.handle.valueType == StyleValueType.Dimension) ||
(val1.handle.valueType == StyleValueType.Float)) &&
((val1.handle.valueType == StyleValueType.Dimension) ||
(val1.handle.valueType == StyleValueType.Float)))
Obviously, this is an error, and the fix is fairly straightforward. Look at the rest of the code—it performs similar operations consistently, first using val1 and then val2. For example, the condition that follows the one in question, consists of two similar checks:
else if ((val1.handle.valueType == StyleValueType.Enum)) &&
(val2.handle.valueType == StyleValueType.Enum)
The only difference between the two is that the first one uses the val1 value, and the second one uses val2.
So, to fix the error, most likely, the devs need to replace all val1 with val2 in the repeated expression of the incorrect condition:
if (((val1.handle.valueType == StyleValueType.Dimension) ||
(val1.handle.valueType == StyleValueType.Float)) &&
((val2.handle.valueType == StyleValueType.Dimension) ||
(val2.handle.valueType == StyleValueType.Float)))
public partial class BuildPlayerWindow : EditorWindow
{
....
internal static event Action<BuildProfile>
drawingMultiplayerBuildOptions;
....
}
internal static class EditorMultiplayerManager
{
....
public static event Action<NamedBuildTarget> drawingMultiplayerBuildOptions
{
add => BuildPlayerWindow.drawingMultiplayerBuildOptions +=
(profile) => ....;
remove => BuildPlayerWindow.drawingMultiplayerBuildOptions -=
(profile) => ....;
}
....
}
The analyzer message:
V3084. Anonymous function is used to unsubscribe from 'drawingMultiplayerBuildOptions' event. No handlers will be unsubscribed, as a separate delegate instance is created for each anonymous function declaration. EditorMultiplayerManager.bindings.cs 48.
This is a simple yet common error. Anonymous functions are used here for subscribing to and unsubscribing from an event. No matter how visually similar they are, these functions are still different objects. As a result, subscribing to an event is performed correctly, but unsubscribing fails.
One solution is to implement a full-fledged method instead of an anonymous function and use for both subscribing to/unsubscribing from the event.
public void DoRenderPreview(Rect previewRect, GUIStyle background)
{
....
Matrix4x4 shadowMatrix;
RenderTexture shadowMap = RenderPreviewShadowmap(....);
if (previewUtility.lights[0].intensity != kDefaultIntensity ||
previewUtility.lights[0].intensity != kDefaultIntensity)
{
SetupPreviewLightingAndFx(probe);
}
float tempZoomFactor = (is2D ? 1.0f : m_ZoomFactor);
previewUtility.camera.orthographic = is2D;
if (is2D)
previewUtility.camera.orthographicSize = 2.0f * m_ZoomFactor;
....
}
private void SetupPreviewLightingAndFx(SphericalHarmonicsL2 probe)
{
previewUtility.lights[0].intensity = kDefaultIntensity;
previewUtility.lights[0].transform.rotation = ....;
previewUtility.lights[1].intensity = kDefaultIntensity;
....
}
The analyzer message:
V3001. There are identical sub-expressions 'previewUtility.lights[0].intensity != kDefaultIntensity' to the left and to the right of the '||' operator. AvatarPreview.cs 721.
The condition of the first if statement in the DoRenderPreview method consists of two identical subexpressions. This could be either a bug or simply redundant code left out due to oversight.
The implementation of the SetupPreviewLightingAndFx method, which uses not only previewUtility.lights[0] but also previewUtility.lights[1], hints at an error here.
So, this is how you can fix the error:
if (previewUtility.lights[0].intensity != kDefaultIntensity ||
previewUtility.lights[1].intensity != kDefaultIntensity)
{
....
}
void UpdateInfo()
{
....
var infoLine3_format = "<color=\"white\">CurrentElement:" +
" Visible:{0}" +
" Enable:{1}" +
" EnableInHierarchy:{2}" +
" YogaNodeDirty:{3}";
m_InfoLine3.text = string.Format(infoLine3_format,
m_LastDrawElement.visible,
m_LastDrawElement.enable,
m_LastDrawElement.enabledInHierarchy,
m_LastDrawElement.isDirty);
var infoLine4_format = "<color=\"white\">" +
"Count of ZeroSize Element:{0} {1}%" +
" Count of Out of Root Element:{0} {1}%";
m_InfoLine4.text = string.Format(infoLine4_format,
countOfZeroSizeElement,
100.0f * countOfZeroSizeElement / count,
outOfRootVE,
100.0f * outOfRootVE / count);
....
}
The analyzer message:
V3025. Incorrect format. A different number of format items is expected while calling 'Format' function. Arguments not used: 3rd, 4th. UILayoutDebugger.cs 179.
Take a look at the second string.Format(....). Its format string (the first argument) has four slots for insertion. There are also four values passed for insertion (from the second to the fifth argument). The issue is that the slots contain only 0 and 1. As a result, only the first and second values are inserted, while the other two aren't used at all.
The corrected format string looks as follows:
var infoLine4_format = "<color=\"white\">" +
"Count of ZeroSize Element:{0} {1}%" +
" Count of Out of Root Element:{2} {3}%";
protected static bool IsFinite(float f)
{
if ( f == Mathf.Infinity
|| f == Mathf.NegativeInfinity
|| f == float.NaN)
{
return false;
}
return true;
}
The analyzer message:
V3076. Comparison of 'f' with 'float.NaN' is meaningless. Use 'float.IsNaN()' method instead. PhysicsDebugWindowQueries.cs 87.
The error here relates to a not-so-obvious behavior. A comparison of two values that are equal to NaN is always false. So, as the analyzer prompts, we need to use float.IsNaN(f) instead of the f == float.NaN expression here.
public readonly struct SearchField : IEquatable<SearchField>
{
....
public override bool Equals(object other)
{
return other is SearchIndexEntry l && Equals(l);
}
public bool Equals(SearchField other)
{
return string.Equals(name, other.name, StringComparison.Ordinal);
}
}
The analyzer message:
V3197. The compared value inside the 'Object.Equals' override is converted to the 'SearchIndexEntry' type instead of 'SearchField' that contains the override. SearchItem.cs 634.
As the analyzer message states, in the first Equals method, the other parameter is incorrectly converted to the SearchIndexEntry type instead of SearchField. This results in the same method overload being invoked when Equals(l) is called later. However, if other is really of the SearchIndexEntry type, the code loops. So, the StackOverflowException will be thrown.
private void DrawRenderTargetToolbar()
{
float blackMinLevel = ....;
float blackLevel = ....;
float whiteLevel = ....;
EditorGUILayout.MinMaxSlider(....);
float whiteMaxLevel = ....;
if (blackMinLevel < whiteMaxLevel && whiteMaxLevel > blackMinLevel)
{
m_RTBlackMinLevel = blackMinLevel;
m_RTWhiteMaxLevel = whiteMaxLevel;
m_RTBlackLevel = Mathf.Clamp(blackLevel,
m_RTBlackMinLevel,
whiteLevel);
m_RTWhiteLevel = Mathf.Clamp(whiteLevel,
blackLevel,
m_RTWhiteMaxLevel);
}
}
The analyzer message:
V3001. There are identical sub-expressions 'blackMinLevel < whiteMaxLevel' to the left and to the right of the '&&' operator. FrameDebuggerEventDetailsView.cs 364.
Again, we have the if statement with the condition consisting of two essentially identical expressions, which differ from each other only in the operand order. It should probably be something else instead of blackMinLevel in the second expression. A review of the surrounding code suggests that whiteLevel would be the most logical option. So, the fixed condition could look as follows:
if (blackMinLevel < whiteMaxLevel && whiteMaxLevel > whiteLevel)
{
....
}
internal static IEnumerable<Sample> FindByPackage(PackageInfo package, ....)
{
if (string.IsNullOrEmpty(package?.upmReserved) &&
string.IsNullOrEmpty(package.resolvedPath))
{
return Enumerable.Empty<Sample>();
}
try
{
IEnumerable<IDictionary<string, object>> samples = null;
var upmReserved = upmCache.ParseUpmReserved(package);
if (upmReserved != null)
samples = upmReserved.GetList<....>("samples");
....
}
....
}
The analyzer message:
V3042. Possible NullReferenceException. The '?.' and '.' operators are used for accessing members of the 'package' object. PackageSample.cs 102.
The check at the beginning of the method aimed to handle several cases at once:
However, it doesn't work correctly in most cases:
The developers may have used the operator && instead of || by mistake. So, the fixed check can look like this:
if (string.IsNullOrEmpty(package?.upmReserved) ||
string.IsNullOrEmpty(package.resolvedPath))
{
return Enumerable.Empty<Sample>();
}
[RequiredByNativeCode]
internal static void InvalidateAll()
{
lock (s_Instances)
{
foreach (var kvp in s_Instances)
{
WeakReference wr = kvp.Value;
if (wr.IsAlive)
(wr.Target as TextGenerator).Invalidate();
}
}
}
The analyzer message:
V3145. Unsafe dereference of a WeakReference target. The object could have been garbage collected between checking 'IsAlive' and accessing the 'Target' property. TextGenerator.cs 140.
Despite the small amount of code, this is probably the most complex error in the article. Here are the reasons why:
How can you protect yourself from the NullReferenceException, then? To avoid the NullReferenceException for sure, one needs to create a strong reference to the object. Then, the garbage collector will no longer delete it as long as the reference is relevant. In other words, create a simple local variable referring to the object and work with it. The safe method can look like this:
[RequiredByNativeCode]
internal static void InvalidateAll()
{
lock (s_Instances)
{
foreach (var kvp in s_Instances)
{
WeakReference wr = kvp.Value;
var target = wr.Target;
If (target != null)
(target as TextGenerator).Invalidate();
}
}
}
So, how'd you do? Aren't you tired yet? Okay, now it's time to take a break, as the article is coming to an end. I hope you found it not only useful but also enjoyable.
Looking for errors even in such small code fragments can be challenging, but it's almost impossible to detect them in the code of such a large project as the Unity Editor. Unless, of course, you use special software tools.
You can try the tool I used to detect the potential issues described for free: just request a trial version on the PVS-Studio website.
By the way, you can use the PVS-Studio code analyzer to check not only the Unity engine but also games developed with it. More information on this you can find in the documentation.
See you in the next articles!