/r/unity_tutorials
Tutorials for the Unity game engine! Share a tutorial that’s helped you, or that you’ve created and think will help others!
/r/unity_tutorials
Hola, estoy creando un nuevo canal de YouTube sobre Unity! donde pienso subir videos tutoriales de como crear juegos si quieren pueden suscribirse gracias!
https://www.youtube.com/channel/UCdzxBQfPH1gdDqZQUe0th7A
"This tool is designed to be used in grid-based 2D puzzle games. It has a user-friendly interface where users can view, edit, and reset the current state of the level in the editor."
source code: https://github.com/berkencami/grid-based-level-editor
unity basics | Learn unity beginner to advanced https://youtu.be/6NF8SLq6bHE
Rendering plays a critical role in creating visually appealing and interactive game scenes. However, inefficient utilization of rendering resources can lead to poor performance and limitations on target devices. Unity, one of the most popular game engines, offers various methods and tools to optimize rendering.
Last time we considered optimizing C# code from the viewpoint of memory and CPU. In this article, we will review the basic principles of rendering optimization in Unity, provide code examples, and discuss practical strategies for improving game performance.
This article has examples of how you can optimize a particular aspect of rendering, but these examples are written only for understanding the basics, not for use in production
Before we move on to optimization, let's briefly recap the basics of rendering in Unity. You can read more about the rendering process in my past article.
Unity uses a graphics pipeline to convert three-dimensional models and scenes into two-dimensional images. The main stages of the pipeline include:
The main components of rendering in Unity include:
Meshes: Geometric shapes of objects.
Materials: Parameters that determine the appearance of an object, including color, textures, and lighting properties.
Shaders: Programs that determine how objects are rendered on the screen.
Optimizing rendering in Unity aims to improve performance by efficiently using CPU and graphics card resources. Below we'll look at a few key optimization strategies:
Let's get started!
Depending on which rendering engine you have chosen and the goals you are pursuing - you should make some adjustments to that engine. Below we will look in detail at the most necessary options using HDRP as an example (but some of them are valid for URP and Built-In as well).
Graphics Setup (Project Settings -> Graphics)
Optimal Settings for Graphics Setup:
Depending on how you use shaders, you may need to configure Forward or Deferred Rendering. The default setting in Unity is mostly Forward Rendering, but you can change it to Forward and in some cases it will speed up the rendering process by several times.
Quality Settings (Project Settings -> Quality)
Optimal Settings for Quality Setup:
Additional Rendering Settings (Project Settings -> Player)
Optimal Settings for Quality Setup:
Render Pipeline Setup (HDRP Asset)
Now let's look at Settings in HDRP Asset:
Camera Optimization
Now let's look at Camera Setup:
Use lower Clipping Planes for low-end devices;
Allow Dynamic Resolution with Performance Setup at low-end devices;
Use Culling masks and Occlusion Culling;
The fewer triangles in a scene, the faster Unity can render it. Use simple shapes where possible and avoid excessive detail. Use tools like LOD (levels of detail) and Impostors to automatically reduce the detail of objects at a distance.
LOD (level of detail) is a system that allows you to use less detailed objects at different distances.
Impostors is a system that bakes a highly polygonal object to display as sprites, which can also be useful on the course. Unlike regular Billboards, Impostors look different from different angles, just like a regular 3D model should.
You can also reduce the number of triangles on the fly if you want to create your own clipping conditions. For example you can use this component for runtime mesh processing.
Culling objects involves making objects invisible. This is an effective way to reduce both the CPU and GPU load.
In many games, a quick and effective way to do this without compromising the player experience is to cull small objects more aggressively than large ones. For example, small rocks and debris could be made invisible at long distances, while large buildings would still be visible.
Occlusion culling is a process which prevents Unity from performing rendering calculations for GameObjects that are completely hidden from view (occluded) by other GameObjects. When rendering rather large polygonal objects (for example, in-door or out-door scenes) not all vertices are actually visible on the screen. By not sending these vertices for rendering, you can save a lot on rendering speed with Frustrum Culling.
In Unity has its own system for Occlusion Culling, it works based on cutoff areas.
To determine whether occlusion culling is likely to improve the runtime performance of your Project, consider the following:
For an improved Frustrum Culling experience, I suggest taking a library that handles it using Jobs.
Materials and Shaders can have a significant impact on performance. The following things should be considered when working with materials:
Write LOD-based shaders for your project:
Shader "Examples/ExampleLOD"
{
SubShader
{
LOD 200
Pass
{
// The rest of the code that defines the Pass goes here.
}
}
SubShader
{
LOD 100
Pass
{
// The rest of the code that defines the Pass goes here.
}
}
}
Switching Shader LOD at Runtime:
Material material = GetComponent<Renderer>().material;
material.shader.maximumLOD = 100;
Complex mathematical operations
Transcendental mathematical functions (such as pow, exp, log, cos, sin, tan) are quite resource-intensive, so avoid using them where possible. Consider using lookup textures as an alternative to complex math calculations if applicable.
Avoid writing your own operations (such as normalize, dot, inversesqrt). Unity’s built-in options ensure that the driver can generate much better code. Remember that the Alpha Test (discard) operation often makes your fragment shader slower.
Floating point precision
While the precision (float vs half vs fixed) of floating point variables is largely ignored on desktop GPUs, it is quite important to get a good performance on mobile GPUs.
Bundling textures and models reduces the number of calls to the disk and reduces resource utilization. There are several options for packaging resources in the way that is right for you:
// Runtime Texture Packing Example
Texture2D[] textures = Resources.LoadAll<Texture2D>("Textures");
Texture2DArray textureArray = new Texture2DArray(512, 512, textures.Length, TextureFormat.RGBA32, true);
for (int i = 0; i < textures.Length; i++)
{
Graphics.CopyTexture(textures[i], 0, textureArray, i);
}
Resources.UnloadUnusedAssets();
Also, don't forget about choosing the right texture compression. If possible, also use Crunched compression. And of course disable unnecessary MipMaps levels to save space.
Disabling rendering of objects behind the camera or behind other objects can significantly improve performance. You can use culling or runtime disabling:
// Runtime invisible renderers disabling example
Renderer renderer = GetComponent<Renderer>();
if (renderer != null && !renderer.isVisible)
{
renderer.enabled = false;
}
All Lights can be rendered using either of two methods:
Lights have a big impact on rendering speed, so lighting quality must be traded off against frame rate. Since pixel lights have a much higher rendering overhead than vertex lights, Unity will only render the brightest lights at per-pixel quality and render the rest as vertex lights.
Realtime shadows have quite a high rendering overhead, so you should use them sparingly. Any objects that might cast shadows must first be rendered into the shadow map and then that map will be used to render objects that might receive shadows. Enabling shadows has an even bigger impact on performance than the pixel/vertex trade-off mentioned above.
So, let's look at general tips for lighting performance:
Simple example of realtime lights disabling at runtime:
Light[] lights = FindObjectsOfType<Light>();
foreach (Light light in lights)
{
if (!light.gameObject.isStatic)
{
light.enabled = false;
}
}
Try to use asynchronous functions and coroutines for heavy in-frame operations. Also try to take calculations out of Update() method, because they will block the main rendering thread and increase micro-frizz between frames, reducing your FPS.
// Bad Example
void Update() {
// Heavy calculations here
}
// Good Example
void LateUpdate(){
if(!runnedOperationWorker){
RunHeavyOperationHere();
}
}
void RunHeavyOperationHere() {
// Create Async Calculations Here
}
Bad Example of Heavy Operations:
// Our Upscaling Method
public void Upscale() {
if(isUpscaled) return;
// Heavy Method Execution
UpscaleTextures(() => {
Resources.UnloadUnusedAssets();
OnUpscaled?.Invoke();
Debug.Log($"Complete Upscale for {gameObject.name} (Materials Pool): {materialPool.Count} textures upscaled.");
});
isUpscaled = true;
}
private void UpscaleTextures(){
if(!isUpscaled) Upscale();
}
Good Example of Heavy Operation:
// Our Upscaling Method
public void Upscale() {
if(isUpscaled) return;
// Run Heavy method on Coroutine (can be used async instead)
StopCoroutine(UpscaleTextures());
StartCoroutine(UpscaleTextures(() => {
Resources.UnloadUnusedAssets();
OnUpscaled?.Invoke();
Debug.Log($"Complete Upscale for {gameObject.name} (Materials Pool): {materialPool.Count} textures upscaled.");
}));
isUpscaled = true;
}
private void UpscaleTextures(){
if(!isUpscaled) Upscale();
}
If you using ECS for your games - you can speed-up your entities rendering process using Entities Graphics. This package provides systems and components for rendering ECS Entities. Entities Graphics is not a render pipeline: it is a system that collects the data necessary for rendering ECS entities, and sends this data to Unity's existing rendering architecture.
The Universal Render Pipeline (URP) and High Definition Render Pipeline (HDRP) are responsible for authoring the content and defining the rendering passes.
https://docs.unity3d.com/Packages/com.unity.entities.graphics@1.0/manual/index.html
Simple Usage Example:
public class AddComponentsExample : MonoBehaviour
{
public Mesh Mesh;
public Material Material;
public int EntityCount;
// Example Burst job that creates many entities
[GenerateTestsForBurstCompatibility]
public struct SpawnJob : IJobParallelFor
{
public Entity Prototype;
public int EntityCount;
public EntityCommandBuffer.ParallelWriter Ecb;
public void Execute(int index)
{
// Clone the Prototype entity to create a new entity.
var e = Ecb.Instantiate(index, Prototype);
// Prototype has all correct components up front, can use SetComponent to
// set values unique to the newly created entity, such as the transform.
Ecb.SetComponent(index, e, new LocalToWorld {Value = ComputeTransform(index)});
}
public float4x4 ComputeTransform(int index)
{
return float4x4.Translate(new float3(index, 0, 0));
}
}
void Start()
{
var world = World.DefaultGameObjectInjectionWorld;
var entityManager = world.EntityManager;
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.TempJob);
// Create a RenderMeshDescription using the convenience constructor
// with named parameters.
var desc = new RenderMeshDescription(
shadowCastingMode: ShadowCastingMode.Off,
receiveShadows: false);
// Create an array of mesh and material required for runtime rendering.
var renderMeshArray = new RenderMeshArray(new Material[] { Material }, new Mesh[] { Mesh });
// Create empty base entity
var prototype = entityManager.CreateEntity();
// Call AddComponents to populate base entity with the components required
// by Entities Graphics
RenderMeshUtility.AddComponents(
prototype,
entityManager,
desc,
renderMeshArray,
MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));
entityManager.AddComponentData(prototype, new LocalToWorld());
// Spawn most of the entities in a Burst job by cloning a pre-created prototype entity,
// which can be either a Prefab or an entity created at run time like in this sample.
// This is the fastest and most efficient way to create entities at run time.
var spawnJob = new SpawnJob
{
Prototype = prototype,
Ecb = ecb.AsParallelWriter(),
EntityCount = EntityCount,
};
var spawnHandle = spawnJob.Schedule(EntityCount, 128);
spawnHandle.Complete();
ecb.Playback(entityManager);
ecb.Dispose();
entityManager.DestroyEntity(prototype);
}
}
And of course, don't optimize graphics blindly. Use Unity profiling tools like Profiler to identify rendering bottlenecks and optimize performance.
For example - create your profiler metrics for heavy calculations:
Profiler.BeginSample("MyUpdate");
// Calculations here
Profiler.EndSample();
So, let's take a look at an additional checklist for optimizing your graphics after you've learned the basic techniques above:
Optimizing rendering is a rather painstaking process. Some basic things - such as lighting settings, texture and model compression, preparing objects for Culling and Batching, or UI optimization - should be done already during the first work on your project to form your optimization-focused work pipeline. However, you can optimize most other things on demand by profiling.
And of course thank you for reading the article, I would be happy to discuss various aspects of optimization with you.
You can also support writing tutorials, articles and see ready-made solutions for your projects:
My Discord | My Blog | My GitHub | Buy me a Beer
BTC: bc1qef2d34r4xkrm48zknjdjt7c0ea92ay9m2a7q55
ETH: 0x1112a2Ef850711DF4dE9c432376F255f416ef5d0
Hi guys, I’m trying to make a 2d platformer roguelike with art by penusbmic and I need to finish a big part of it by mid june. My primary goals are making the character movement and actions feel decent, decent enemies and one boss at the end of the game play. I don’t want to make it big yet. The problem is I’m not sure which tutorials to follow, I’ve watched a couple but none of them seem to be the ones I need. Could you guys maybe lend me a hand if you know some good tutorial and direct me to them? Also, do you think it’s a realistic goal, in the time given? This game is basically all I need to do for the next two months so I can work on it 5-6 hours a day.
Check out my latest video in the series about Importance of Math in game development.
This video gives a brief overview of how math plays a important role in 3D graphics.
https://youtu.be/q7oYF3pl7jk?si=jtTQpeD0DvgxdrEK
Hi everyone, last time we already touched upon the topic of optimizing code in C# from the point of view of RAM usage. In general, efficient use of computer resources such as the central processing unit (CPU) is one of the main aspects of software development. This time we will talk about optimizing CPU load when writing code in C#, which can significantly improve application performance and reduce power consumption, which is especially critical on mobile platforms and the web. In this article, we will consider several key approaches and strategies for optimizing CPU load in the C# programming language.
One of the most important aspects of CPU load optimization is choosing efficient algorithms. When writing C# code, make sure that you use algorithms with minimal runtime complexity. For example, when searching for an element in a large array, use algorithms with O(log n) or O(1) time complexity, such as binary search, instead of algorithms with O(n) time complexity, such as sequential search.
Linear Search - also known as the sequential search algorithm. A simple search algorithm checks each element in a collection until the desired value is found. Linear search can be used for sorted and unsorted collections, but it is useful for small collections.
public static int LinearSearch(int[] arr, int target) {
for (int i = 0; i < arr.Length; i++)
if (arr[i] == target)
return i;
return -1;
}
Binary Search - is a more efficient search algorithm that divides the collection in half at each iteration. Binary search requires the collection to be sorted in ascending or descending order.
public static int BinarySearch(int[] arr, int target) {
int left = 0;
int right = arr.Length - 1;
while (left <= right){
int mid = (left + right) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target)
left = mid + 1;
else
right = mid - 1;
}
return -1; // target not found
}
Interpolation search - is a variant of binary search that works best for uniformly distributed collections. It uses an interpolation formula to estimate the position of the target element.
public static int InterpolationSearch(int[] arr, int target) {
int left = 0;
int right = arr.Length - 1;
while (left <= right && target >= arr[left] && target <= arr[right]) {
int pos = left + ((target - arr[left]) * (right - left)) / (arr[right] - arr[left]);
if (arr[pos] == target)
return pos;
else if (arr[pos] < target)
left = pos + 1;
else
right = pos - 1;
}
return -1; // target not found
}
Jump search - is another variant of binary search that works by jumping ahead by a fixed number of steps instead of dividing the interval in half.
public static int JumpSearch(int[] arr, int target) {
int n = arr.Length;
int step = (int)Math.Sqrt(n);
int prev = 0;
while (arr[Math.Min(step, n) - 1] < target) {
prev = step;
step += (int)Math.Sqrt(n);
if (prev >= n)
return -1; // target not found
}
while (arr[prev] < target) {
prev++;
if (prev == Math.Min(step, n))
return -1; // target not found
}
if (arr[prev] == target)
return prev;
return -1; // target not found
}
As you can see, there can be a large number of search algorithms. Some of them are suitable for some purposes, others for others. The fast binary search algorithm is most often used as a well-established algorithm, but this does not mean that you are obliged to use it only, because it has its own purposes as well.
Bubble sort - a straightforward sorting algorithm that iterates through a list, comparing adjacent elements and swapping them if they are in the incorrect order. This process is repeated until the list is completely sorted. Below is the C# code implementation for bubble sort:
public static void BubbleSort(int[] arr) {
int n = arr.Length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
Selection sort - a comparison-based sorting algorithm that operates in place. It partitions the input list into two sections: the left end represents the sorted portion, initially empty, while the right end denotes the unsorted portion of the entire list. The algorithm works by locating the smallest element within the unsorted section and swapping it with the leftmost unsorted element, progressively expanding the sorted region by one element.
public static void SelectionSort(int[] arr) {
int n = arr.Length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex])
minIndex = j;
}
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
Insertion sort - a basic sorting algorithm that constructs the sorted array gradually, one item at a time. It is less efficient than more advanced algorithms like quicksort, heapsort, or merge sort, especially for large lists. The algorithm operates by sequentially traversing an array from left to right, comparing adjacent elements, and performing swaps if they are out of order.
public static void InsertionSort(int[] arr) {
int n = arr.Length;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
Quicksort - a sorting algorithm based on the divide-and-conquer approach. It begins by choosing a pivot element from the array and divides the remaining elements into two sub-arrays based on whether they are smaller or larger than the pivot. These sub-arrays are then recursively sorted.
public static void QuickSort(int[] arr, int left, int right){
if (left < right) {
int pivotIndex = Partition(arr, left, right);
QuickSort(arr, left, pivotIndex - 1);
QuickSort(arr, pivotIndex + 1, right);
}
}
private static int Partition(int[] arr, int left, int right){
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp2 = arr[i + 1];
arr[i + 1] = arr[right];
arr[right] = temp2;
return i + 1;
}
Merge sort - a sorting algorithm based on the divide-and-conquer principle. It begins by dividing an array into two halves, recursively applying itself to each half, and then merging the two sorted halves back together. The merge operation plays a crucial role in this algorithm.
public static void MergeSort(int[] arr, int left, int right){
if (left < right) {
int middle = (left + right) / 2;
MergeSort(arr, left, middle);
MergeSort(arr, middle + 1, right);
Merge(arr, left, middle, right);
}
}
private static void Merge(int[] arr, int left, int middle, int right) {
int[] temp = new int[arr.Length];
for (int i = left; i <= right; i++){
temp[i] = arr[i];
}
int j = left;
int k = middle + 1;
int l = left;
while (j <= middle && k <= right){
if (temp[j] <= temp[k]) {
arr[l] = temp[j];
j++;
} else {
arr[l] = temp[k];
k++;
}
l++;
}
while (j <= middle) {
arr[l] = temp[j];
l++;
j++;
}
}
Like search algorithms, there are many different algorithms used for sorting. Each of them serves a different purpose and you should choose the one you need for a particular purpose.
Loops are one of the most common places where CPU load occurs. When writing loops in C# code, try to minimize the number of operations inside a loop and avoid redundant iterations. Also, pay attention to the order of nested loops, as improper management of them can lead to exponential growth of execution time, as well as lead to memory leaks, which I wrote about in the last article.
Suppose we have a loop in which we perform some calculations on array elements. We can optimize this loop if we avoid unnecessary calls to properties and methods of objects inside the loop:
// Our Arrays for Cycle
int[] numbers = { 1, 2, 3, 4, 5 };
int sum = 0;
// Bad Cycle
for (int i = 0; i < numbers.Length; i++) {
sum += numbers[i] * numbers[i];
}
// Good Cycle
for (int i = 0, len = numbers.Length; i < len; i++) {
int num = numbers[i];
sum += num * num;
}
This example demonstrates how you can avoid repeated calls to object properties and methods within a loop, and how you can avoid calling the Length property of an array at each iteration of the loop by using the local variable len. These optimizations can significantly improve code performance, especially when dealing with large amounts of data.
C# has powerful tools to deal with parallelism, such as multithreading and parallel collections. By parallelizing computations, you can efficiently use the resources of multiprocessor systems and reduce CPU load. However, be careful when using parallelism, as improper thread management can lead to race conditions and other synchronization problems and memory leaks.
So, let's look at bad example of parallelism in C#:
long sum = 0;
int[] numbers = new int[1000000];
Random random = new Random();
// Just fill random numbers for example
for (int i = 0; i < numbers.Length; i++) {
numbers[i] = random.Next(100);
}
// Bad example with each iteration in separated thread
Parallel.For(0, numbers.Length, i => {
sum += numbers[i] * numbers[i];
});
And Impoved Example:
long sum = 0;
int[] numbers = new int[1000000];
Random random = new Random();
// Just fill random numbers for example
for (int i = 0; i < numbers.Length; i++) {
numbers[i] = random.Next(100);
}
// Sync our parallel computions
Parallel.For(0, numbers.Length, () => 0L, (i, state, partialSum) => {
partialSum += numbers[i] * numbers[i];
return partialSum;
}, partialSum => {
lock (locker) {
sum += partialSum;
}
});
In this good example, we use the Parallel.For construct to parallelize the calculations. However, instead of directly modifying the shared variable sum, we pass each thread a local variable partialSum, which is the partial sum of the computations for each thread. After each thread completes, we sum these partial sums into the shared variable sum, using monitoring and locking to secure access to the shared variable from different threads. Thus, we avoid race conditions and ensure correct operation of the parallel program.
Don't forget that there is still work to be done with stopping and clearing threads. You should use IDisposable and use using to avoid memory leaks.
If you develop projects in Unity - i really recommend to see at UniTaks.
Efficient use of the CPU cache can significantly improve the performance of your application. When working with large amounts of data, try to minimize memory accesses and maximize data locality. This can be achieved by caching frequently used data and optimizing access to it.
Let's look at example:
// Our Cache Dictionary
static Dictionary<int, int> cache = new Dictionary<int, int>();
// Example of Expensive operation with cache
static int ExpensiveOperation(int input) {
if (cache.ContainsKey(input)) {
// We found a result in cache
return cache[input];
}
// Example of expensive operation here (it may be webrequest or something else)
int result = input * input;
// Save Result to cache
cache[input] = result;
return result;
}
In this example, we use a cache dictionary to store the results of expensive operations. Before executing an operation, we check if there is already a result for the given input value in the cache. If there is already a result, we load it from the cache, which avoids re-executing the operation and reduces CPU load. If there is no result in the cache, we perform the operation, store the result in the cache, and then return it.
This example demonstrates how data caching can reduce CPU overhead by avoiding repeated computations for the same input data. For the faster and unique cache use HashSet structure.
Of course, you should not forget that if you work with Unity - you need to take into account both the rendering process and the game engine itself. I advise you to pay attention first of all to the following aspects when optimizing CPU in Unity:
Finally, don't forget to profile your application and look for bottlenecks where the most CPU usage is occurring. There are many profiling tools for C#, such as dotTrace and ANTS Performance Profiler or Unity Profiler, that can help you identify and fix performance problems.
Optimizing CPU load when writing C# code is an art that requires balancing performance, readability, and maintainability of the code. By choosing the right algorithms, optimizing loops, using parallelism, data caching, and profiling, you can create high-performance applications on the .NET platform or at Unity.
And of course thank you for reading the article, I would be happy to discuss various aspects of optimization and code with you.
In the world of modern programming, efficient utilization of resources, including memory, is a key aspect of application development. Today we will talk about how you can optimize the resources available to you during development.
The C# programming language, although it provides automatic memory management through the Garbage Collection (GC) mechanism, requires special knowledge and skills from developers to optimize memory handling.
So, let's explore various memory optimization strategies and practices in C# that help in creating efficient and fast applications.
Before we begin - I would like to point out that this article is not a panacea and can only be considered as a support for your further research.
Before we dive into the details of memory optimization in C#, it's important to understand the distinction between managed and unmanaged memory.
This is memory whose management rests entirely on the shoulders of the CLR (Common Language Runtime). In C#, all objects are created in the managed heap and are automatically destroyed by the garbage collector when they are no longer needed.
This is memory that is managed by the developer. In C#, you can handle unmanaged memory through interoperability with low-level APIs (Application Programming Interface) or by using the unsafe
and fixed
keywords. Unmanaged memory can be used to optimize performance in critical code sections, but requires careful handling to avoid memory leaks or errors.
Unity has basically no unmanaged memory and also the garbage collector works a bit differently, so you should just rely on yourself and understand how managed memory works on a basic level to know under what conditions it will be cleared and under what conditions it won't.
Choosing an appropriate data structure is a key aspect of memory optimization. Instead of using complex objects and collections, which may consume more memory due to additional metadata and management information, you should prefer simple data structures such as arrays, lists, and structs.
Let's look at an example:
// Uses more memory
List<string> names = new List<string>();
names.Add("John");
names.Add("Doe");
// Uses less memory
string[] names = new string[2];
names[0] = "John";
names[1] = "Doe";
In this example, the string[]
array requires less memory compared to List<string>
because it has no additional data structure to manage dynamic resizing.
However, that doesn't mean you should always use arrays instead of lists. You should realize that if you often have to add new elements and rebuild the array, or perform heavy searches that are already provided in the list, it is better to choose the second option.
In my understanding, classes and structures are quite similar to each other, albeit with some differences (but that's not what this article will be about), they still have quite a big difference about how they are arranged in our application's memory. And understanding this can save you a huge amount of execution time and RAM, especially on large amounts of data. So let's look at some examples.
So, suppose we have a class with arrays and a structure with arrays. In the first case, the arrays will be stored in the RAM of our application, and in the second case, in the processor cache (taking into account some peculiarities of garbage collection, which we will discuss below). If we store data in the CPU cache, we speed up access to the data we need, in some cases from 10 to 100 times (of course, everything depends on the peculiarities of the CPU and RAM, and these days CPUs have become much smarter friends with compilers, providing a more efficient approach to memory management).
So, over time, as we populate or organize our class, the data will no longer be placed with each other in memory due to the heap handling features, because our class is a reference type and it is arranged more chaotically in memory locations. Over time, memory fragmentation makes it more difficult for the CPU to move data into the cache, which creates some performance and access speed issues with that very data.
// Class Array Data
internal class ClassArrayData
{
public int value;
}
// Struct Array Data
internal struct StructArrayData
{
public int value;
}
Let's look at the options of when we should use classes and when we should use structures.
When you shouldn't replace classes with structures:
When it's still worth replacing a class with a structure:
A 90% boost is a lot, so if it sounds like something for you, I highly recommend doing some tests yourself. I would also like to point out that we can only make assumptions based on the industry norms because we are down at the hardware level.
I also want to give an example of benchmarks with mixed elements of arrays based on classes and structures (done on Intel Core i5-11260H 2.6 HHz, iteratively on 100 million operations with 5 attempts):
Yes, we are talking about huge amounts of data here, but what I wanted to emphasize here is that the compiler cannot guess how you want to use this data, unlike you - and it is up to you to decide how you want to access it first.
Memory leaks can occur due to careless handling of objects and object references. In C#, the garbage collector automatically frees memory when an object is no longer used, but if there are references to objects that remain in memory, they will not be removed.
When working with managed resources such as files, network connections, or databases, make sure that they are properly released after use. Otherwise, this may result in memory leaks or exhaustion of system resources.
So, let's look at example of Memory Leak Code in C#:
public class MemoryLeakSample
{
public static void Main()
{
while (true)
{
Thread thread = new Thread(new ThreadStart(StartThread));
thread.Start();
}
}
public static void StartThread()
{
Thread.CurrentThread.Join();
}
}
And Memory Leak Code in Unity:
int frameNumber = 0;
WebCamTexture wct;
Texture2D frame;
void Start()
{
frameNumber = 0;
wct = new WebCamTexture(WebCamTexture.devices[0].name, 1280, 720, 30);
Renderer renderer = GetComponent<Renderer>();
renderer.material.mainTexture = wct;
wct.Play();
frame = new Texture2D(wct.width, wct.height);
}
// Update is called once per frame
// This code in update() also leaks memory
void Update()
{
if (wct.didUpdateThisFrame == false)
return;
++frameNumber;
//Check when camera texture size changes then resize your frame too
if (frame.width != wct.width || frame.height != wct.height)
{
frame.Resize(wct.width, wct.height);
}
frame.SetPixels(wct.GetPixels());
frame.Apply();
}
There are many ways to avoid memory leak in C#. We can avoid memory leak while working with unmanaged resources with the help of the ‘using’ statement, which internally calls Dispose() method. The syntax for the ‘using’ statement is as follows:
// Variant with Disposable Classes
using(var ourObject = new OurDisposableClass)
{
//user code
}
When using managed resources, such as databases or network connections, it is also recommended to use connection pools to reduce the overhead of creating and destroying resources.
When working with large amounts of data, it is important to avoid unnecessary copying and use efficient data structures. For example, if you need to manipulate large strings of text, use StringBuilder instead of regular strings to avoid unnecessary memory allocations.
// Bad Variant
string result = "";
for (int i = 0; i < 10000; i++) {
result += i.ToString();
}
// Good Variant
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.Append(i);
}
string result = sb.ToString();
You should also avoid unnecessary memory allocations when working with collections. For example, if you use LINQ to filter a list, you can convert the result to an array using the
ToArray()
method to avoid creating an unnecessary list.
// Bad Example
List<int> numbers = Enumerable.Range(1, 10000).ToList();
List<int> evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// Good Example
int[] numbers = Enumerable.Range(1, 10000).ToArray();
int[] evenNumbers = numbers.Where(n => n % 2 == 0).ToArray();
Code profiling allows you to identify bottlenecks and optimize them to improve performance and memory efficiency. There are many profiling tools for C#, such as dotTrace, ANTS Performance Profiler and Visual Studio Profiler.
Unity has own Memory Profiler. You can read more about them here.
Profiling allows you to:
Identify code sections that consume the most memory.
Identify memory leaks and unnecessary allocations.
Optimize algorithms and data structures to reduce memory consumption.
Depending on the specific usage scenarios of your application, some optimization strategies may be more or less appropriate. For example, if your application runs in real time (like games), you may encounter performance issues due to garbage collection, and you may need to use specialized data structures or algorithms to deal with this problem (for example Unity DOTS and Burst Compiler).
Although the use of unsafe
memory in C# should be cautious and limited, there are scenarios where using unsafe
code can significantly improve performance. This can be particularly useful when working with large amounts of data or when writing low-level algorithms where the overhead of garbage collection becomes significant.
// Unsafe Code Example
unsafe
{
int x = 10;
int* ptr;
ptr = &x;
// displaying value of x using pointer
Console.WriteLine("Inside the unsafe code block");
Console.WriteLine("The value of x is " + *ptr);
} // end unsafe block
Console.WriteLine("\nOutside the unsafe code block");
However, using
unsafe
code requires a serious understanding of the inner workings of memory and multithreading in .NET, and requires extra precautions such as checking array bounds and handling pointers with care.
Memory optimization in C# is a critical aspect of developing efficient and fast applications. Understanding the basic principles of memory management, choosing the right data structures and algorithms, and using profiling tools will help you create an efficient application that utilizes system resources efficiently and provides high performance.
However, don't forget that in addition to code optimization, you should also optimize application resources (for example, this is very true for games, where you need to work with texture compression, frame rendering optimization, dynamic loading and unloading of resources using Bundles, etc.).
And of course thank you for reading the article, I would be happy to discuss various aspects of optimization and code with you.
You can also support writing tutorials, articles and see ready-made solutions for your projects:
Anybody buy a recent humble bundle that came with "Unreal Engine 5: Creating a Car Racing Game," and they want to trade for the Godot or Unity tutorials (Awesome Tuts) that im not going to use from the current GameMasters Toolkit up on the site? I bought the wrong bundle, and got the wrong car racing tut. Will trade all unity or Godot tuts (so they don't go to waste) for the one unreal tut. Kay, let me know