C-Sharp — the main programming language used in Unity — has been in development
since the year 2000.
That’s right: it’s over two decades old.
And in the 22 years of its existence, C# has matured into one of the most popular programming
languages in the world.
In fact, according to the TIOBE index (which is used to measure the popularity of programming
languages) C# is currently ranked number 5.
And yet Unity doesn’t support the latest version.
As of today, C# is up to version 10, with version 11 under active development.
But for the past few years, Unity has only had support for C# version 7.3.
Now there’s actually a very good reason for this and if you’d like to learn more
about it, you can check out this video here.
But the good news is that the Tech Stream release of Unity 2021.2 introduced full support
of C# version 8.
Of course, it isn’t the latest version but it still includes a bunch of features that
many Unity developers have been asking for for quite some time.
And in this video, I’m gonna cover a handful of those new features that are now available
in Unity thanks C# version 8.
But first if you want more content just like this, sign up for my Level 2 Game Dev newsletter.
Level 2 is all about helping you develop the skills needed to take your game dev hobby
or career to the next level.
Once a month we’ll send you a curated list of content that’ll help take you to the
next step on your game dev journey.
If you’re interested, visit the link in the description to sign up now.
Alright, let’s check out some of the new features in C-Sharp 8!
Now there are a bunch of great features that we could cover — too many for just one video.
So I’ve chosen a handful of the ones that I think are the most interesting.
Your mileage with each of these may vary and if you find yourself uninterested in a feature
I’ve chosen to cover, feel free to use the chapter markers to skip ahead.
We’ll start with Readonly members.
C# 8 introduces the ability to mark struct members with the readonly modifier.
Previously, you could only mark the struct declaration as readonly, effectively making
those objects immutable.
But now you have granular control over which elements can actually be changed.
More importantly, the compiler can now expose your intent in the form of build warnings
and errors so you or anyone else on your team can’t accidentally circumvent your readonly
members.
Take this struct, called “Point”, for example.
It has two fields, X and Y, and a calculated property that returns the point’s distance
from the origin.
It also overrides the ToString method to return a more human-readable description of the object.
This seems like a great candidate for the readonly modifier since ToString should never
modify the state of the object that it’s describing.
However when we add the readonly modifier, the compiler produces a warning.
“Call to non-readonly property 'Distance' from a 'readonly' member results in an implicit
copy of 'this'”.
The compiler warns us that our readonly method references a non-readonly property because
that property’s getter could be changing the state of the point object without us (or
the compiler) knowing it.
To show you what I mean, let’s pretend we wanted to keep track of the last time that
distance was checked.
We’ll need to add a DateTime field called “LastDistanceCheck” and update it when
the Distance property’s getter is invoked.
Now let’s add some logic to this example MonoBehaviour so we can see what happens to
LastDistanceCheck if we call ToString every frame.
In the Update method, we’ll start by logging the value of LastDistanceCheck.
Then we’ll invoke the point’s ToString method.
The expected behavior here is that LastDistanceCheck will continue to change because ToString calls
the Distance property’s getter, and that getter updates LastDistanceCheck.
However…
It doesn’t.
LastDistanceCheck never changes, even though it technically should.
That’s because of the implicit copy of “this” that was mentioned in the compiler warning.
What’s happening behind the scenes is that the invisible “this” variable that exists
within the scope of the ToString method call, and is shared with nested calls, gets implicitly
replaced with a copy.
So when the Distance getter is called from our readonly ToString method, it updates the
LastDistanceCheck field of a copy of the point.
In other words, we’ll need to approach this logic in a different way if we want to continue
to mark ToString as “readonly”.
Now to get rid of the original warning, all we have to do is add the readonly modifier
to the Distance property.
Of course, if we haven't removed the update to LastDistanceCheck, we’ll now get an error
message telling us that our immutable property is trying to change the state of the point.
The compiler is enforcing our readonly rule.
So we’ll remove that logic to fix the issue.
Perfect.
While I don’t use structs that often, I can see this as being a valuable feature for
defensive programming.
As a developer, one of your main concerns is making sure that your intent shines through
your code.
That way other developers, or even a future version of yourself that has long since forgotten
your own code, can read your logic and fully understand what you were trying to accomplish
so they don’t mess that up.
Readonly members, and the compiler messages that accompany them, help to do just that.
If you’ve ever worked on a library, you’ll know how frustrating it can be to introduce
new functionality to an existing interface.
Because doing so breaks all of the types that implement that interface.
It’s a huge headache that used to require a lot of coordination.
But not any more.
C# 8 includes the Default Interface Methods feature which introduces a way to add new
methods to existing interfaces without breaking their implementers.
On the surface, it’s pretty straightforward.
But there’s one small gotcha that you should definitely be aware of.
Take this IWeapon interface for example.
It’s current API is very simple, it has a couple of floats to encapsulate the weapon’s
damage range and a method for calculating it’s damage output.
Implementers can calculate damage however they see fit like this MeleeWeapon class does
by picking a random value within the damage range.
However, what if - after many weapons had already been implemented - I decided that
I wanted to add a new feature to the IWeapon interface.
I’d like to calculate a damage modifier based on the weapon's current durability.
Well before C# 8, I’d have to add the method to the interface and then make sure that it
was implemented by each IWeapon subclass.
But now it’s much simpler.
All I have to do is implement the method directly on the interface.
Let’s do that now.
We’ll say that as long as your weapon’s durability is above 75%, we’ll grant you
a 10% damage bonus.
Voila.
Our new method, ‘CalculateDamageModifer’, is a default interface method that has its
own default implementation.
Now here’s where the gotcha comes in.
Over in the Example MonoBehaviour, we’ll go ahead and create an instance of MeleeWeapon.
Now, we should be able to call ‘CalculateDamageModifer’ on ‘meleeWeapon’, but it isn’t there
and we get an error saying that the method can’t be resolved.
That’s because while each implementor can override a default interface method they don’t
inherit access.
So we’ll need to cast ‘meleeWeapon’ to IWeapon.
Again, this is only necessary because our MeleeWeapon implementation doesn’t override
‘CalculateDamageModifer’.
Default Interface Methods make working with C# much more flexible.
As humans, we try our best to model the world around us in code, but we rarely get it right
on the first try.
Having the ability to continuously evolve our interfaces is an incredibly powerful feature
and I’m thankful we finally have access to it in Unity.
One thing that C# has been really good at improving over the span of its life is syntactic
sugar.
Syntactic sugar is syntax within a programming language that is designed to make things easier
to read or to express.
It reminds me of a quote from Martin Fowler, “Any fool can write code that a computer
can understand.
Good programmers write code that humans can understand.”
The goal of syntactic sugar is to help you write code that reads more intuitively.
One example of this in C# is the ternary operator.
Instead of having to write a verbose, multi-line expression to essentially say, “if not this,
then that”, you can use a ternary operator to express the concept on a single line.
C# 8 introduces a new bit of syntactic sugar in the form of indices and ranges.
Indices and ranges provide a succinct syntax for accessing single elements or ranges in
a sequence.
In C# 8, there are two new operators that improve how you access the data in collections.
Take this array of ten strings, for example.
If we wanted to access the fifth element, we’d use the index notation.
Pretty standard stuff.
But what if we wanted to access the fifth element from the end?
Normally you’d have to subtract the index from the length of the array, but that isn’t
very sweet.
Enter the end operator, which is denoted by a caret symbol.
By placing a caret ahead of the index, we’re specifying an index that is relative to the
end of the array.
A caret followed by zero would be equivalent to the length of the array - and would throw
an exception.
If you want to access the last item in an array, you could index a caret followed by
one, which is a neat little trick.
The other operator added in C# 8 is the range operator, which is denoted by a number followed
by two dots and another number.
The numbers specify the start and end of the range where the start index is inclusive and
the end index is exclusive.
That means that zero followed by two dots and caret-zero represents the entire range
of the array.
More realistically, however, you’d probably want to grab an arbitrary sequence in the
middle.
You can also express ranges as variables.
This would have come in handy in our IWeapon interface to describe a weapon’s damage
range.
There are a few other bits of syntactic sugar peppered throughout C# 8, like more simplified
switch expressions, so I encourage you to research them and add each one to your toolbelt
so you can continue to get better at writing code that expresses your true intentions in
the simplest way possible.
Those were just a handful of features that were introduced in C# version 8, which is
now supported by Unity 2021.2 and on.
Like I mentioned earlier, there were a bunch of other features that I didn’t get a chance
to cover.
If you’d like to learn more about those, check out the C# 8 feature page on Microsoft’s
official documentation website (which I’ve linked in the description below).
And if you’d like me to cover some of them in a video, please let me know by leaving
a comment below.
If there’s enough interest, I’d be happy to make a part-two.
If you enjoyed this video, please leave a like and subscribe to the Infallible Code
channel.
And for more content just like this sign for the Level 2 Game Dev Newsletter.
Thanks for watching.
As always, I’ll catch you in the next video.