What's new in Unity 6? Review of innovations and errors in the source code

Unity 6 is finally released! The developers call this version the most stable version for the entire existence of the engine. Why don't we verify this using a static code analyzer? At the same time, we’ll give a brief overview of the main features and improvements introduced by the update.

Introduction

Unity 6 is a global update aimed at improving the performance and stability of the engine, but is far from limited to this. The improvements affected a variety of aspects: multiplayer tools, lighting system, rendering, post-processing, XR tools, some visual effects, etc. I would also like to note that a system for integrating neural networks has been added.

Although the preview version of Unity 6 has already been available since May 15, 2024, perhaps you, like me, have only recently become interested in what is special about this update. Therefore, in the first part of the article I want to give a brief overview of the most significant, in my opinion, improvements, but I will not go into too much detail. Instead, I will share links where you can get more information.

In the second part of the article, you can test your code analysis skills and try to find potential errors in several code fragments found by the static analyzer in the Open Source part of the engine. Such training is a good opportunity to learn from other people’s mistakes and minimize the likelihood of making them in your own code.

Happy reading!

What's new in Unity 6?

So, let's find out what the Unity team has prepared for us this time.

Integration of AI based on artificial neural networks

Framework added Sentis. It allows you to integrate pre-trained neural network models into the runtime and use them to implement functions such as speech and object recognition, creating intelligent NPCs, etc.

Expansion of tools for creating multi-user projects

  • New package Multiplayer Center. Provides a starting point for creating a multiplayer game. It recommends multiplayer packages based on your game settings and provides access to relevant examples and tutorials.

  • Added Multiplayer Play Mode. Allows you to simulate up to 4 players directly from the Unity editor.

  • Added support to Netcode for GameObjects Distributed authority topologies. And a new tool has been added to the Multiplayer Tools package Network scene visualizationwhich, among other things, should help in debugging projects using the new topology.

  • Added plastic bag to support the development of games and applications on a dedicated server platform.

Figure N1 – Multiplayer Play Mode in practice

CPU load optimization

  • New rendering system GPU Resident Drawer. The efficiency of this system directly depends on the average number of object instances in the scene. The more instances of the same objects in a scene (for example, vegetation created using SpeedTree) – the greater the benefit. This system has a number of technical limitations. For example, it optimizes the rendering of only object instances with static meshes. Therefore, it will not work for instances of particle systems and other effects.

  • New feature GPU Occlusion Culling. This function excludes fully shaded objects from rendering on the GPU instead of the CPU.

GPU load optimization

  • New feature Foveated Rendering. Reduces GPU load in XR projects by reducing detail in the user's peripheral vision. Has two modes:

    • Fixed foveated — rendering the central area of ​​the display for each eye with maximum resolution and reducing resolution in the periphery;

    • Gaze-based foveated – This mode uses eye tracking to determine the area of ​​the screen with maximum resolution.

  • Reducing GPU load by implementing automatic merging of rendering stages in the Render Graph. The GPU can also be optimized by creating its own stages and passes between them. It is also worth noting that now Render Graph is available not only in HDRP, but also in URP.

Improved environment rendering quality

  • Improved sky rendering for sunset and sunrise in HDRP. Added ozone layer and atmospheric scattering effects in addition to fog over long distances;

  • Improved water visualization (in HDRP) by adding an underwater volumetric fog effect.

On the left side of the image the effect of the ozone layer is not used, unlike on the right.

Underwater fog effect

Other improvements

  • Important improvements have been made to the XR toolkit;

  • The Unity 6 editor, unlike previous versions, now works on Arm-based Windows devices;

  • The build window has been improved, new build profiles have been added;

  • Lighting improvements have been made. In particular, it was added a new way to create global illumination;

  • Major known issues have been fixed and some improvements have been made to Shader Graph.

The changes listed here seemed to me to be the main ones, but this is by no means a complete list. A more complete list of improvements, as well as their detailed descriptions, can be found Here.

Now that we have become acquainted with the positive changes, it is time to find out whether new errors have appeared in the source code of the engine along with the update.

Analysis of new errors in the engine code

So, armed with the PVS-Studio code analyzer, I examined the C# part of the most current version of the engine at the time of writing (6000.0.21f1) and found several interesting both potential and obvious errors. However, instead of simply describing them to you, I would like to invite you to try your hand at being a static code analyzer. Try to find one error in the given code fragments and come up with ways to fix them. Under each of the fragments there is a complete answer with which you can check your guess.

Let it begin hunger games tests and may your programming ingenuity always be with you! By the way, it will be cool if you share your results in the comments 🙂

First test

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 answer is hidden here

Analyzer message:

V3005. The 'this.defaultHasBeenPrevented' variable is assigned to itself. EventDebuggerTrace.cs 42.

For some reason, when initializing the property defaultHasBeenPrevented assigned to itself. Since the property does not have a setter, its value will never change again and will always be equal to the default value – false.

Possible fix: Add a new parameter to the constructor to initialize the property.

Too easy? Let's consider this a warm-up and see how you do further!

Second test

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 answer is hidden here

Analyzer message:

V3095. The 'sprite' object was used before it was verified against null. Check lines: 310, 312. TextGeneratorParsing.cs 310.

Variable sprite is dereferenced right before checking for null:

m_CurrentSpriteAsset = sprite.textAsset as SpriteAsset;
m_SpriteIndex = (int)sprite.glyphIndex;

if (sprite == null)
  continue;

In case sprite will actually be equal nullthis will inevitably lead to an exception. Probably, when adding dereferencing strings to the method, they did not pay attention to the importance of the order of operations. Thus, you can avoid the problem if you transfer dereferences to be checked on null:

if (sprite == null)
  continue;

m_CurrentSpriteAsset = sprite.textAsset as SpriteAsset;
m_SpriteIndex = (int)sprite.glyphIndex;

Third test

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 answer is hidden here

Analyzer message:

V3001. There are identical sub-expressions to the left and to the right of the '&&' operator. StyleSheetApplicator.cs 169.

The analyzer suggests that in one of the conditions, through the operator && two identical expressions are executed:

if (((val1.handle.valueType == StyleValueType.Dimension) ||
     (val1.handle.valueType == StyleValueType.Float)) &&

    ((val1.handle.valueType == StyleValueType.Dimension) || 
     (val1.handle.valueType == StyleValueType.Float)))

There was clearly a mistake made here. Moreover, its correction is obvious. Pay attention to the rest of the code. It performs similar operations everywhere, first using val1and then using val2. For example, the condition following the one under consideration consists of two similar checks:

else if ((val1.handle.valueType == StyleValueType.Enum)) &&
         (val2.handle.valueType == StyleValueType.Enum)

The only difference between them is that the first uses the value val1and in the second – val2.

Thus, fixing the error will most likely involve replacing all val1 on val2 in a repeated expression of an incorrect condition:

if (((val1.handle.valueType == StyleValueType.Dimension) ||
     (val1.handle.valueType == StyleValueType.Float)) &&

    ((val2.handle.valueType == StyleValueType.Dimension) || 
     (val2.handle.valueType == StyleValueType.Float)))

Fourth test

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 answer is hidden here

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.

A simple but common mistake. Here, anonymous functions are used to subscribe to and unsubscribe from an event. No matter how visually similar they are, these functions will still be different objects. As a result, subscription to the event will be completed correctly, but unsubscription will no longer work.

One solution is to implement a full-fledged method instead of an anonymous function and use it to subscribe/unsubscribe to/from an event.

Fifth test

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 answer is hidden here

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.

Condition of the first if-operator in the method DoRenderPreview consists of two identical subexpressions. This could be either an error or simply redundant code left inadvertently.

The implementation of the method hints at the presence of an error here SetupPreviewLightingAndFxwhich uses not only previewUtility.lights[0]but also previewUtility.lights[1].

So the bug fix might look like this:


if (previewUtility.lights[0].intensity != kDefaultIntensity ||  
    previewUtility.lights[1].intensity != kDefaultIntensity)
{
  ....
}

Sixth test

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 answer is hidden here

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.

Pay attention to the second one string.Format(….). Its format string (first argument) has 4 insertion slots. The values ​​to be inserted (arguments 2-5) are also passed 4. The problem is that the slots only contain the numbers 0 and 1. As a result, only the first and second values ​​will be inserted into them, while the remaining two will not be used at all.

The corrected format string looks like this:

var infoLine4_format = "<color=\"white\">" +
                       "Count of ZeroSize Element:{0} {1}%" +
                       " Count of Out of Root Element:{2} {3}%";

Seventh test

protected static bool IsFinite(float f)
{
  if (   f == Mathf.Infinity 
      || f == Mathf.NegativeInfinity 
      || f == float.NaN)
  {
    return false;
  }

  return true;
}
The answer is hidden here

Analyzer message:

V3076. Comparison of 'f' with 'float.NaN' is meaningless. Use 'float.IsNaN()' method instead. PhysicsDebugWindowQueries.cs 87.

The error here is due to some not very obvious behavior. Comparison of two values ​​equal NaN always false. Therefore, as the analyzer advises, instead of the expression f == float.NaN should be used here float.IsNaN(f).

Eighth test

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 answer is hidden here

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 says, in the first method Equals parameter other mistakenly cast to type SearchIndexEntry instead of SearchField. Because of what, on a subsequent call Equals(l) the same method overload will be called. If it suddenly turns out that other really has a type SearchIndexEntrythe code will loop. This will lead to StackOverflowException.

Ninth test

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 answer is hidden here

Analyzer message:

V3001. There are identical sub-expressions 'blackMinLevel < whiteMaxLevel' to the left and to the right of the '&&' operator. FrameDebuggerEventDetailsView.cs 364.

Again before us if-an operator whose condition consists of two essentially identical expressions that differ from each other only in the order of the operands. Probably instead blackMinLevel there must be something different in the second expression. Having looked at the surrounding code, the most logical option seems to be whiteLevel. So the corrected condition might look like this:

if (blackMinLevel < whiteMaxLevel && whiteMaxLevel > whiteLevel)
{
  ....
}

Tenth test

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 answer is hidden here

Analyzer message:

V3042. Possible NullReferenceException. The '?.' and '.' operators are used for accessing members of the 'package' object. PackageSample.cs 102.

By checking at the beginning of the method, the developer tried to handle several cases at once:

  • When package.upmReserved equals null or an empty line;

  • When package.resolvedPath equals null or an empty line;

  • When package equals null.

However, in most cases it will not work correctly:

  • If package equals nullThat NullReferenceException will be thrown away already during dereferencing in the second subexpression of the check itself;

  • if either package.upmReservedor package.resolvedPath will be equal null or an empty string (but not both at once), the method will not exit, contrary to expectations.

Perhaps the developer inadvertently used the operator && instead of ||. So the corrected version of the check might look like this:

if (string.IsNullOrEmpty(package?.upmReserved) || 
    string.IsNullOrEmpty(package.resolvedPath))
{
  return Enumerable.Empty<Sample>();
}

Eleventh test

[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 answer is hidden here

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 perhaps the most complex case in the article. There are several reasons:

  1. Not everyone is familiar with the concept of a weak link (WeakReference). The object referenced by such a link can be garbage collected at any time, despite the existence of a weak link.

  2. Property WeakReference.IsAlive allows you to check whether the object referenced by a weak link still exists. But this code does not take into account that this same object can be cleaned up after passing the check, before dereferencing. As a result, it may still be thrown out NullReferenceException.

  3. One might assume that the operator lock can protect an object from garbage collection. However, after reproducing such a case, it turned out that this was not the case.

So how to protect yourself in this case? To be sure to avoid an exception NullReferenceExceptionyou should create a strong reference to the object. In this case, the garbage collector will no longer be able to remove it while the link remains valid. Simply put, you need to create a simple local variable that refers to this object, and then work with it. So a safe version of the method might 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();     
     }
   }
}

Conclusion

Well, did you manage? Aren't you tired? Well, it's time to rest, because the article is coming to an end. I hope you found it not only useful, but also exciting.

Finding errors even in such small fragments of code is not easy, but finding them in the code base of such a large project as an editor Unity almost impossible. Of course, if you don’t use special software tools.

You can try the tool that was used to find the potential problems described in the article for free by requesting a trial for PVS-Studio website.

By the way, using the PVS-Studio code analyzer you can analyze not only Unity itself, but also games implemented using Unity. You can find more information about this in documentation.

See you in the next articles!

If you want to share this article with an English-speaking audience, please use the translation link: Andrey Moskalev. What's new in Unity 6? Overview of release updates and source code issues.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *