Basic

June 30 2020

7 comments

Unity C#: Local Functions for Higher Safety (C# 7.0)

By Rubén Torres Bonet

June 30, 2020


__CONFIG_colors_palette__{"active_palette":0,"config":{"colors":{"6c4de":{"name":"Main Accent","parent":-1},"67ed2":{"name":"Accent Dark","parent":"6c4de","lock":{"saturation":1}}},"gradients":[]},"palettes":[{"name":"Default","value":{"colors":{"6c4de":{"val":"var(--tcb-skin-color-0)"},"67ed2":{"val":"rgb(59, 65, 63)","hsl_parent_dependency":{"h":160,"l":-0.05,"s":0.04}}},"gradients":[]},"original":{"colors":{"6c4de":{"val":"rgb(51, 190, 127)","hsl":{"h":152,"s":0.57,"l":0.47}},"67ed2":{"val":"rgb(59, 65, 63)","hsl_parent_dependency":{"h":160,"s":0.04,"l":0.24}}},"gradients":[]}}]}__CONFIG_colors_palette__

Have you ever crafted Unity C# code like this?

void ForceUpdateBackend()
{
StartCoroutine(ForceUpdateBackend_Internal(path: Url));
}
IEnumerator ForceUpdateBackend_Internal(string path)
{
var request = UnityWebRequest.Get(path);
yield return request.SendWebRequest();
Debug.Log(request.downloadHandler.text);
}
void ForceUpdateBackend() { StartCoroutine(ForceUpdateBackend_Internal(path: Url)); } IEnumerator ForceUpdateBackend_Internal(string path) { var request = UnityWebRequest.Get(path); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); // ... }
void ForceUpdateBackend()
{
  StartCoroutine(ForceUpdateBackend_Internal(path: Url));
}

IEnumerator ForceUpdateBackend_Internal(string path)
{
  var request = UnityWebRequest.Get(path);
  yield return request.SendWebRequest();
  Debug.Log(request.downloadHandler.text);
}

Somehow, it feels uncomfortable to have two different functions that do kind of the same thing.

Wait, you haven't written such code?

Okay, what about this pattern?

void AvoidCollision()
{
var penaltyLeft = CalculateCollisionPenalty(-transform.right);
var penaltyForward = CalculateCollisionPenalty(transform.forward);
var penaltyRight = CalculateCollisionPenalty(transform.right);
// drive towards the safest direction
// ...
}
int CalculateCollisionPenalty(Vector3 direction)
{
var didHit = Physics.Raycast(transform.position, direction, out RaycastHit hit);
if (didHit == false)
return 0;
if (hit.collider.CompareTag("Wall"))
return 10;
if (hit.collider.CompareTag("Car"))
return 5;
return 1;
}
void AvoidCollision() { var penaltyLeft = CalculateCollisionPenalty(-transform.right); var penaltyForward = CalculateCollisionPenalty(transform.forward); var penaltyRight = CalculateCollisionPenalty(transform.right); // drive towards the safest direction // ... } int CalculateCollisionPenalty(Vector3 direction) { var didHit = Physics.Raycast(transform.position, direction, out RaycastHit hit); if (didHit == false) return 0; if (hit.collider.CompareTag("Wall")) return 10; if (hit.collider.CompareTag("Car")) return 5; return 1; }
void AvoidCollision()
{
  var penaltyLeft = CalculateCollisionPenalty(-transform.right);
  var penaltyForward = CalculateCollisionPenalty(transform.forward);
  var penaltyRight = CalculateCollisionPenalty(transform.right);
  // drive towards the safest direction
  // ...
}

int CalculateCollisionPenalty(Vector3 direction)
{
  var didHit = Physics.Raycast(transform.position, direction, out RaycastHit hit);
  if (didHit == false)
    return 0;
  if (hit.collider.CompareTag("Wall"))
    return 10;
  if (hit.collider.CompareTag("Car"))
    return 5;
  return 1;
}

The same thing.

  • We create a dummy, simpler function that calls a more complex function.
  • Or we create a support function to avoid repetition in our main function.

It feels as if these extra functions should only be part of our original function

Is another developer going to call these functions by mistake?

There's no happy ending if sneaky invocations cause side effects in your game.

Can we do something about that?

Hell yeah!

C# 7.0 brought some candy to us, ambitious game developers

Let's have a look at something new...

Welcome to Unity, C# 7.0 local functions.

Does This Mean Trouble to Me?

We started with a few (simplified) examples of C# patterns I see often in Unity games.

And another one:

void GenerateProceduralUniverse(int randomSeed)
{
for (var levelId = 0; levelId < 10; levelId++)
{
var level = GenerateProceduralLevel(randomSeed, levelId);
// ...
}
}

Level GenerateProceduralLevel(int randomSeed, int levelId)
{
var newLevel = new Level(randomSeed, levelId);
// ....
return newLevel;
}

Here's the thing:

No one cares about these extra supporting methods but you.

And I don't say that to hurt your feelings (I've heard we, programmers, don't have those).

Actually, it's not even about protecting them from the curious eye.

It's about protecting our game from someone causing undesirable side effects by calling these "extra" functions.

These side effects can get you canned. Not cool.

The common line is the same:

We write supporting functions to support our original function...

But, at the same time, we don't want to expose these extra functions to the outside world.

The solution is pretty simple since C# 7.0...

Let's declare them as local functions.

Upgrading These Common Programming Patterns

The idea is simple: we declare these functions inside the main function.

Let's revisit the three dummy examples.

Pattern 1: StartCoroutine + IEnumerator

Often enough, a user won't care about how your function is implemented.

All a user wants is to call it without messing with weird invocation styles such starting coroutines and such.

We can upgrade the previous example to use Unity C# local functions:

void ForceUpdateBackend_Local()
{
StartCoroutine(ForceUpdateBackend_Internal_Local(path: Url));
IEnumerator ForceUpdateBackend_Internal_Local(string path)
{
var request = UnityWebRequest.Get(path);
yield return request.SendWebRequest();
Debug.Log(request.downloadHandler.text);
}
}
void ForceUpdateBackend_Local() { StartCoroutine(ForceUpdateBackend_Internal_Local(path: Url)); IEnumerator ForceUpdateBackend_Internal_Local(string path) { var request = UnityWebRequest.Get(path); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); } }
void ForceUpdateBackend_Local()
{
    StartCoroutine(ForceUpdateBackend_Internal_Local(path: Url));
    
    IEnumerator ForceUpdateBackend_Internal_Local(string path)
    {
        var request = UnityWebRequest.Get(path);
        yield return request.SendWebRequest();
        Debug.Log(request.downloadHandler.text);
    }
}

Now you have protected your inner function from the sneaky hands of evil programmers.

Pattern 2: Function + Supporting Function

Here, we want to avoid code repetition by extracting code into a secondary function.

Let's upgrade our complicated procedural universe generator:

void GenerateProceduralUniverse_Local(int randomSeed)
{
for (var levelId = 0; levelId < 10; levelId++)
{
var level = GenerateProceduralLevel_Local(levelId);
// ...
}
Level GenerateProceduralLevel_Local(int levelId)
{
var newLevel = new Level(randomSeed, levelId);
// ....
return newLevel;
}
}
void GenerateProceduralUniverse_Local(int randomSeed) { for (var levelId = 0; levelId < 10; levelId++) { var level = GenerateProceduralLevel_Local(levelId); // ... } Level GenerateProceduralLevel_Local(int levelId) { var newLevel = new Level(randomSeed, levelId); // .... return newLevel; } }
void GenerateProceduralUniverse_Local(int randomSeed)
{
  for (var levelId = 0; levelId < 10; levelId++)
  {
    var level = GenerateProceduralLevel_Local(levelId);
    // ...
  }

  Level GenerateProceduralLevel_Local(int levelId)
  {
    var newLevel = new Level(randomSeed, levelId);
    // ....
    return newLevel;
  }
}

Ding! Another level up

Advantages of Unity C# Local Functions

Let's keep it simple.

Here are the advantages of using local functions:

  • Your code is safer: professional code saboteurs can't call your inner function.
  • You make your intentions clear: that function is only for internal use.
  • Local functions work as well with async and IEnumerator

A Note on Lambdas vs. Local Functions

There are significant differences between using C# lambdas and local functions.

Yes, it's not only a matter of aesthetic preference (that also).

Here are a few:

  • Local functions throw exceptions earlier than lambdas.
  • Local functions have explicit names and return/argument types.
  • Local function execution may avoid heap allocations by storing its data on the stack. Lambdas always generate heap allocations.
The Gamedev Guru Logo

Conclusion

If you want to read more on local functions (after all, I just served you the appetizer), have a look here*.

* Careful, it's a Microsoft link. It might set Edge as your default browser.

For the average user, there's no performance boost by using local functions. If you use lambdas, switching to local functions might help you reduce memory allocations. And that will make your garbage collector happy.

You know what they say...

Happy GC, happy holidays.

What about you?

Have you found any uses for local functions in your game?

~Ruben

  • Great! I have not seen local functions it from this angle yet.

    I usually put blocks of code in functions to make the calling function more readable. But often I get the problem that the supporting function does not make much sense out of context and muddles the list of functions in a class.

    Like this the “shorthand” for a block is still in the scope of the function and does not get confused for something else.

    The local function increases the length of the function though and I guess it requires discipline when using several local functions.

  • On my first project, when I was learning Unity and C# (initial learning, we are always learning), I was typing a lot of code with this pattern, then thought – what a mess, there isn’t a way to create a method inside another? I searched for it and, unfortunately, you couldn’t. I’m so happy that it is a possibility now.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

    Rubén Torres Bonet

    About the author

    Born Game Developer, now ready to help you develop better games. Primary programmer on Star Trek Bridge Crew (Oculus Quest), Diamond Dash. Programmer on Time Stall, Catan Universe, Anne Frank House VR, Jelly Splash, Blackguards Definitive Edition. I also worked in minor XR experiences for HoloLens and Vive for clients such as Audi and Volkswagen.

    You might also like

    >