Organization

Types

C# is an Object Oriented language, meaning that everything is an object with a particular type and all variables/functions/etc. must be part of a type. Here are some of the most common object types you will use in Unity:

Type Description
bool A binary value which can be either true or false.
int A whole number such as 0 or 6.
float A decimal number such as 0.1 or 10.5. Note that the default type for decimal numbers in C# is double (a higher precision float), so whenever you write a decimal number to use as a float you need to give it an f suffix like so: value = 0.5f;.
Vector3 A group of 3 floats. They are used for many purposes, such as storing the position of an object in 3D space.
string A series of characters representing some text. Strings in code must be surrounded by "Quotation Marks".
GameObject An object in the Unity scene which can have various components attached to it such as a Collider, Rigidbody, Renderer, or any MonoBehaviour script you write.
MonoBehaviour A type of component that can be attached to a GameObject. Most of the scripts you write will likely Inherit from MonoBehaviour.
ScriptableObject A script that can be saved as an asset. These are less common than MonoBehaviours, but still quite common.

Types can contain Fields, Methods, and Properties which are collectively referred to as Members. There are various other kinds of Members, but those are the main ones.

Values and References

There are two general categories which all types in C# fall into:

Value Types Reference Types
Must always have a value unless you specifically use Nullable Value Types, which causes them to behave like Reference Types. Can be null, meaning that no reference has currently been assigned.
Copied by value so any modifications to a particular copy will not affect any other copies. Copied by reference so any number of things can reference the same object and any modifications will affect everything referencing that object.
// Vector2 is a struct.
Vector2 a = new Vector2(3, 7);
Vector2 b = a;

// a and b are both (3, 7),
// but they are not linked in any way.

a.x = 0;

// a is now (0, 7).
// b is still (3, 7).
// GameObject is a class.
GameObject a = new GameObject();
a.name = "Hello World";

GameObject b = a;

// a and b both refer to the same object.

a.name = "Goodbye World";

// a.name is now "Goodbye World".
// b still refers to the same object,
// so b.name is also "Goodbye World" now.
Note that Reference Types get Garbage Collected.

The Microsoft documentation has a more detailed explanation of Types and Variables.

Namespaces

Multiple classes cannot exist with the same name unless they are each different namespaces. This is not so important for something like AnimancerComponent since it is unlikely that any other class will exist with that name, but for other classes with more common names like ObjectPool it is very important so that importing Animancer does not cause compiler errors if the project already has a class with that name. Even within Animancer, several of the examples use their own Character class so each of those examples has its own namespace.

All of the scripts in Animancer are located in the Animancer namespace (or a sub-namespace such as Animancer.Examples for the example scripts), meaning that they are declared like this:

namespace Animancer
{
    public class AnimancerComponent : MonoBehaviour
    {
        // Lots of cool stuff goes here.
    }
}

This means that in order to refer to AnimancerComponent in another script, you need to do one of the following:

  • Put using Animancer; at the top of the script.
  • Use the full name of the class which includes its namespace: Animancer.AnimancerComponent whenever you refer to it.
  • Put your class in the Animancer namespace as well. You would not generally do this unless you intend for your script to be part of Animancer (rather than simply using it).

Note that if you type AnimancerComponent in a script without doing any of the above, Visual Studio will highlight it as a compiler error (meaning the script cannot be compiled until you fix it). You can select it and press Ctrl + . and it will give you the option to quickly do either of the first two options. Other IDEs will likely have similar features.

Nested Types

Types can also be nested inside other types. For example:

  • An AnimancerEvent is a normalizedTime value paired with a Callback Delegate.
  • They are generally managed by a sequence class that holds multiple events, but rather than call the sequence class AnimancerEventSequence it is actually just called Sequence but is nested inside AnimancerEvent so you access it using AnimancerEvent.Sequence.
  • This makes it clearer that the two types are connected to each other rather than just coincidentally having similar names.

Access Modifiers

Access modifiers allow you to control which parts of the program can access each other, often referred to as encapsulation. This is particularly important for preventing bugs and ensuring proper use of a system.

To give a practical example:

  • The AnimancerEvent.Sequence class contains an Array of AnimancerEvents.
  • It automatically keeps all of the events sorted by their time value so that when checking which events need to be triggered, if it sees that it is not yet time for the next event it knows that none of the others will need to be triggered either because they all come later, which allows it to save some processing time since it doesn't need to check every event every frame.
  • If it allowed other scripts to modify that array directly then it could not be sure that the events are still sorted so it would need to waste processing time checking and re-sorting all the time.
  • Instead, it keeps the array private and has public methods and properties for accessing it in specific ways that allow it to enforce its required sorting.

There are six different access modifiers in C#:

Modifier Effect
public Can be accessed from anywhere.
private Can only be accessed within the same type.
protected Can only be accessed within the same type or any type that Inherits from it.
internal Can only be accessed within any type in the same Assembly.
protected internal Can only be accessed within the same type or any type that Inherits from it or within any type in the same Assembly.
private protected Can only be accessed within the same type or any type that Inherits from it as long as that type is also in the same Assembly.

If you do not specify an access modifier, it will use a default depending on the context:

  • Non-nested types will be internal.
  • Members inside a class will be private.
  • Members inside a struct will be public.

Many Coding Standards require you to always specify access modifiers to be clear about the intent of your code.

Static

The static keyword modifies a particular Member so that it belongs to the type itself rather than to individual instances of that type.

public class UniqueID
{
    // Static Auto-Property.
    // Static means there is only one of this value in the entire program.
    public static int NextID { get; private set; }

    // Non-static Auto-Property.
    // Non-static means that every UniqueID has its own value for this property.
    public int ID { get; private set; }

    // Default Constructor.
    public UniqueID()
    {
        // Set the ID of this instance.
        ID = NextID;

        // Increase the NextID by 1 so that the next instance to be created gets a different ID.
        NextID++;
    }
}

public class ExampleProgram
{
    public void Execute()
    {
        // Since NextID is static, we access it using the class name: UniqueID.NextID.
        // UniqueID.NextID is currently 0.

        // Create an instance of the UniqueID class by calling its constructor.
        var firstID = new UniqueID();

        // firstID.ID is 0.
        // UniqueID.NextID is now 1.

        // Create another UniqueID object.
        var secondID = new UniqueID();

        // Every UniqueID has its own ID (because it is non-static):
        // firstID.ID is still 0.
        // secondID.ID is 1.
        // And there is only one NextID (because it is static):
        // UniqueID.NextID is now 2.

        // Also note that since both of the properties in UniqueID have private setters,
        // we cannot change their values from outside that class.
        // So we cannot do "firstID.ID = 6" or "UniqueID.NextID = 0".
        // This is good because allowing that would mean the IDs are no longer unique.
    }
}

Classes can also be marked as static, which simply means that all their members must be static.

The C# documentation goes into more detail about the Static Keyword.

Generics

Generics allow types and methods to leave certain types up to the user's code. For example, Lists are a generic type which knows it will contain an array which it can add and remove items from but it does not specifically know which type of items it will contain so its declaration looks something like this:

public class List<T>
{
    // An array of whatever T happens to be.
    private T[] _Array;

    public void Add(T item)
    {
        ...
    }
}

// In some other method:
var myFloats = new List<float>();// The list has an array of floats.
myFloats.Add(0.5f);// And we can add floats to it.
myFloats.Add("Some String");// But we get an error if we try to add a string to it.

Individual methods can also be generic such as Unity's GameObject.AddComponent which has three Overloads:

Method Description
// Declaration:
Component AddComponent(string className)

// Usage:
gameObject.AddComponent("Rigidbody");
This method is Obsolete because it requires a Magic String to specify the type of component to add, meaning the developers of Unity have decided that it should no longer be used and will be removed in some future version.
// Declaration:
Component AddComponent(Type componentType)

// Usage:
gameObject.AddComponent(typeof(Rigidbody));
This is much better than the string method because the typeof operator is type-safe, meaning that it will immediately give you a compiler error if you misspell Rigidbody instead of pretending the string is fine until you run the game and that method actually gets called.
// Declaration:
T AddComponent<T>()

// Usage:
gameObject.AddComponent<Rigidbody>();
This is better than both other methods (in most cases) because the usage is shorter and it actually returns a Rigidbody instead of a Component (even though we know that the Component will be a Rigidbody, the compiler does not know that fact).