top of page
Writer's pictureLingheng Tao

Unity Engine #0 C-Sharp


This note is mainly about the C# language. A complete understanding of Unity's features must start with the C# language. Although this part is very stereotyped, it can help us improve the performance of the program and understand the behavior of the program.





Type


Basic concepts


There are two types in C#.

Value type: int, bool, float, char, struct, enum

Reference types: string, object, delegate, interface, class, array


The following differences exist between them:

​value type

Reference type

Storage location

Memory stack

Memory Heap

​Storage speed

Relatively fast

relatively slow

Meaning

Actual data content

Pointers and references to the memory heap

Memory release

Auto-release

GC release

Inherited from

System.ValueType

System.Object

String type


In particular, string has certain specialities as a reference type. Any modification of string actually creates a new string. As long as new is used to create a new reference type, new space will be allocated in the heap. At this time, the copy in the stack will also point to the new object in the heap, so the string will change and become a new object, which is no longer related to the original string.


The solution is that when we will modify a string frequently, we use the StringBuilder class instead of String.


StringBuilder underlying ideas


It is probably a char array that supports expansion. When the space is insufficient, the capacity of the original array size is opened up. The newly created array points to the previous array that has been used up, and the GC itself will not be called.


Garbage Collector / GC


Garbage Collector, referred to as GC, is the garbage collector that comes with C#. They are responsible for managing the contents of memory that will no longer be used. For details, please refer to the Unity Engine #1 Memory section. Here are some concepts briefly explained.


Basic concepts

  1. Memory management pool: As mentioned above, Unity has two internal parts: the memory heap and the memory stack. Stack is used to store smaller, transient data; Heap is used to store larger, more persistent data.

  2. As long as the variable is active, the corresponding memory usage will be Allocated.

  3. Once a variable is no longer active and the memory it occupies is no longer needed, it will be recycled. Recycling on Stack is fast, but garbage on Heap is not recycled in time. Its memory is also marked as used. Memory that is no longer in use will only be reclaimed during the GC phase.

GC operations can significantly affect performance, so we should try to avoid GC.


Packing / Unboxing


Boxing refers to the process of converting a value type to an object type, or to any interface type implemented by this value type.


Practical operation:

- Go to Heap and new an object of class Object.

- Store value type data into the Object class object.

- Returns the address of the object created on the Heap to a reference type variable.


The reverse step is unboxing. Unboxing is an explicit conversion from an object type to a value type (or interface type).


Practical operation:

- Gets the address of a boxed object, checking the object instance to make sure it is a boxed value of the given value type.

- Copies the value from the instance to the value variable type.


Boxing and unboxing will actually generate more GC. During Unity's boxing operation, a System.Object type reference will be allocated on the Heap to encapsulate it, and its corresponding cache will generate memory garbage. Even if there is no direct boxing operation on variables in the code, it is very likely that such a phenomenon will exist in functions or third-party plug-ins. We should try to reduce packing and unpacking operations.


Method:

  1. Use generics.

  2. yield return null instead of yield return 0.

  3. Reduce unnecessary Logs.


Object-oriented programming


As we all know, C# is similar to C++ and is an Object Oriented Programming. There are many important features for object-oriented languages.


Encapsulation


Encapsulation refers to enhancing data security by restricting the extent to which code can modify data. By hiding the internal state and functional implementation details of the object, only the necessary operating interface processes are exposed.


In C#, encapsulation controls the access level of members by using access modifiers, for example:

- public: open to any class and member, no access restrictions.

- private: public only to the current class.

- protected: Public to the current class and its derived classes.

- internal: This class can only be accessed within the assembly that contains it.


Note the behavior of the protected modifier.

public class BaseClass
{
    protected int protectedField;

    public void BaseMethod()
    {
        protectedField = 10; // accessible from the current class
    }
}

public class DerivedClass : BaseClass
{
    public void DerivedMethod()
    {
        protectedField = 20; // accessible from the derived class
    }
}

public class AnotherClass
{
    public void AnotherMethod()
    {
        BaseClass baseClass = new BaseClass();
        // baseClass.protectedField = 30; // will result in a compiler error
    }
}

Here, AnotherClass is not a derived class of BaseClass, so the int value type variable protectedField modified by protected can only be accessed by BaseClass and DerivedClass, but not by AnotherClass.


Inheritance


Inheritance is another important feature. Inheritance allows a newly created class (derived class) to inherit the properties and methods of an existing class (base class).


In C#, only single inheritance is supported, that is, each class can only inherit from one base class, but can implement multiple interfaces.


The simplest example is if we create a new script in Unity, it inherits from the MonoBehavior class. Therefore, new scripts can use the functions already defined in MonoBehavior.

public class Cube : MonoBehavior { 
    void Start(){}
    void Update(){}
}

Here, Cube is the derived class of MonoBehavior. MonoBehavior is the base class of Cube.


Note that although the specific implementation of these MonoBehavior functions we wrote in the new script looks like rewriting, in fact they are not clearly defined as void methods or abstract methods in the MonoBehavior class. They are actually called specifically by the Unity engine through a specific mechanism. When the Unity engine is running, it will check whether the derived class of MonoBehavior has specific functions like Start() and Update(). If it exists, these new functions will be automatically called at the specified time; because this is a A special reflection mechanism, not object-oriented inheritance, so we do not need the override keyword mentioned in polymorphism below.


In C#, we can also use the sealed keyword to prevent other classes from inheriting the class; if you use the sealed keyword on a method, you can prevent other classes from overriding the method. This mechanism is called Sealing.


Polymorphism


Polymorphism means that the same operation can have different implementations on different class instances.


In C#, polymorphism is achieved through the method override modifier override and interface implementation. It allows a derived class object to be treated as an object of its base class type while retaining its unique behavior.


This example can be used to explain polymorphism.

// Base Class
public class Animal
{
    public virtual void MakeSound()
    {
        Console.WriteLine("Some sound");
    }
}

// Derived Class Dog
public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark");
    }
}

// Derived Class Cat
public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        myDog.MakeSound(); // Output "Bark"
        myCat.MakeSound(); // Output "Meow"

        Animal[] animals = new Animal[2];
        animals[0] = new Dog();
        animals[1] = new Cat();

        foreach (var animal in animals)
        {
            animal.MakeSound(); // Output "Bark" and "Meow" respectively
        }
    }
}

Our base class is Animal, and Dog and Cat are its derived classes. The base class defines the function MakeSound, and then in different derived classes, we can rewrite this function through override so that they each have their own definition. Called directly through the base class, you can even automatically call the MakeSound method of the derived class.


Abstract


In C#, we can achieve through abstract class (a class that cannot be instantiated) and interface (which defines a set of methods that are not implemented).


Among them, abstract classes can contain abstract methods (methods without implementation) and concrete methods (methods with implementation). Can contain members such as constructors, fields, methods, properties, etc. Abstract methods can only be declared in abstract classes, and they must be overridden in non-abstract derived classes.


The following example is a code example of an abstract class. You can compare it with the code in polymorphism above. In fact, the focus is on the abstraction of functions (abstract classes can also include non-abstract methods).

public abstract class Animal
{
    public abstract void MakeSound();

    // Abstract Class may have non-abstract functions
    public void Eat()
    {
        Console.WriteLine("Animal is eating.");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Bark");
    }
}

public class Cat : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Meow");
    }
}

In this example, MakeSound is the only abstract method (requires abstract modification). Eat is a specific method. Abstract methods require concrete implementation in derived classes Dog and Cat.


Interface is a completely abstract structure that only defines the signature of the members and does not contain any implementation. However, the interface cannot contain fields, constructors and destructors. All members are public by default and cannot contain access modifiers.


The following example is a code example of the interface.

public interface IVehicle
{
    void StartEngine(); 
    void StopEngine();  
    void Drive();       
}

public class Car : IVehicle
{
    public void StartEngine()
    {
        Console.WriteLine("Car engine started.");
    }

    public void StopEngine()
    {
        Console.WriteLine("Car engine stopped.");
    }

    public void Drive()
    {
        Console.WriteLine("Car is driving.");
    }
}

public class Boat : IVehicle
{
    public void StartEngine()
    {
        Console.WriteLine("Boat engine started.");
    }

    public void StopEngine()
    {
        Console.WriteLine("Boat engine stopped.");
    }

    public void Drive()
    {
        Console.WriteLine("Boat is sailing.");
    }
}

Concrete classes implement specific functions in the interface. However, the interface itself cannot have any fields, only abstract methods.


In contrast, interfaces are implemented by classes, while abstract classes are inherited by classes. Interfaces can only declare methods but not implementations, while abstract classes can have concrete implementations. A class can implement multiple interfaces, but can only inherit from one class. Abstract classes can have different access levels, but all members of the interface are public by default.


Struct


Struct is very similar to the structure of a class, but there are many essential differences.


Differences from classes

  1. Struct is a value type and is stored on the stack, while class is a reference type and is stored on the heap. Therefore, Struct has fast access speed and small capacity, and is only suitable for lightweight objects, such as point coordinates, rectangles, circles, colors, etc.

  2. Struct cannot declare parameterless constructors, but classes can.

  3. Members cannot be initialized when a Struct is defined. To define a structure, all members must be assigned and initialized by themselves.

  4. After declaring the structure, you can use new to create a constructed object, or you can use new without new. Without new, fields will remain unassigned and the object will not be available until all fields are initialized.

  5. Struct cannot be inherited.

  6. Struct cannot be static modified, but classes can.


Void function


 

Reference materials:

  1. Unity Game Development Client Interview - C# (Elementary): https://blog.csdn.net/Sea3752/article/details/127354146?spm=1001.2014.3001.5501


9 views0 comments

Recent Posts

See All

Comentarios


bottom of page