<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>The Knights of U</title>
	<atom:link href="https://theknightsofu.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://theknightsofu.com/</link>
	<description></description>
	<lastBuildDate>Wed, 17 Jun 2026 13:24:43 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://theknightsofu.com/wp-content/uploads/2024/08/cropped-favicon-32x32.png</url>
	<title>The Knights of U</title>
	<link>https://theknightsofu.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Photon Fusion 2 &#8211; Game Tutorial &#8211; Part 4/4</title>
		<link>https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/</link>
					<comments>https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/#respond</comments>
		
		<dc:creator><![CDATA[Sir Maciej and Sir Elias]]></dc:creator>
		<pubDate>Wed, 17 Jun 2026 11:55:00 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Basics]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Multiplayer]]></category>
		<category><![CDATA[Photon Fusion 2]]></category>
		<category><![CDATA[Tips]]></category>
		<category><![CDATA[TKoU]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[fusion 2]]></category>
		<category><![CDATA[multiplayer]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[photon]]></category>
		<category><![CDATA[reconnect]]></category>
		<category><![CDATA[Scripting]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5439</guid>

					<description><![CDATA[<p>Previous part: Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4. Ready to put Photon Fusion 2 into practice? In this final part of our series, you’ll learn how to implement real multiplayer features step by step in a simple Unity game. We’ll skip the non-networking stuff and focus on essential multiplayer systems. Session setup, &#8230; <a href="https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/">Photon Fusion 2 &#8211; Game Tutorial &#8211; Part 4/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Previous part: <a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/" type="link" id="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4</a>.</p>



<p class="wp-block-paragraph">Ready to put Photon Fusion 2 into practice? In this <strong>final part</strong> of our series, you’ll learn how to implement real multiplayer features step by step in a simple Unity game. We’ll skip the non-networking stuff and focus on essential multiplayer systems. Session setup, player management, object synchronisation, reconnect logic, and more. By following this guide, you’ll build a solid foundation for scalable, production-ready multiplayer games in Unity.</p>



<h2 class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">Welcome to the final article in our Photon Fusion 2 beginner series! Here, we’ll take a practical approach &#8211; step by step, you’ll learn how to build the <strong>multiplayer part </strong>of a simple Unity game using Photon Fusion 2. This article won’t give you a complete game, but you’ll focus only on features directly tied to Fusion 2 networking. To keep things clear, we’ll skip unrelated mechanics like UI, loading screens, or single-player logic. Every code sample below delivers a real networking function you can use as a foundation for your own projects. We’ll work with Unity’s basic primitives &#8211; planes, cubes and spheres &#8211; and supplement them with free assets from the Unity Asset Store.</p>



<h2 class="wp-block-heading">Game Description</h2>



<p class="wp-block-paragraph">This project focuses on a small part of a larger game to help you learn core multiplayer mechanics you can expand later. You won’t see a complete game loop, but you’ll get a <strong>solid foundation</strong> for your own projects.</p>



<p class="wp-block-paragraph">The game is a 3D top-down shooter where you work with up to 4 players in classic co-op mode. Each player drives a unique vehicle using the W, S, A, D keys and the mouse. At the start of each session, players choose from three predefined vehicles. Every vehicle has a weapon mounted on its roof &#8211; you aim and shoot using the mouse. Missions include red objects as required targets &#8211; you must destroy all of them to win. Yellow objects are optional &#8211; you can destroy them, but they aren’t needed to finish the mission. Automated turrets on the map shoot at player vehicles and deal damage.</p>



<figure class="wp-block-image size-large is-resized"><img fetchpriority="high" decoding="async" width="1024" height="833" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-1024x833.png" alt="Demo game sample UI." class="wp-image-5463" style="width:536px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-1024x833.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-300x244.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-768x625.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-400x325.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-600x488.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-800x651.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6-1000x813.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-6.png 1162w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="1024" height="507" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1024x507.png" alt="Demo game sample UI." class="wp-image-5464" style="width:539px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1024x507.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-300x149.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-768x380.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1536x761.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-2048x1014.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-400x198.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-600x297.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-800x396.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1000x495.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1280x634.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1520x753.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-7-1900x941.png 1900w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large is-resized"><img decoding="async" width="1024" height="645" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-1024x645.png" alt="Fusion 2 demo game sample gameplay view." class="wp-image-5471" style="width:538px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-1024x645.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-300x189.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-768x484.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-400x252.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-600x378.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-800x504.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12-1000x630.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-12.png 1393w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Project Setup</h2>



<p class="wp-block-paragraph">Before you start with Photon Fusion 2, make sure you have everything set up. We explained all the details in the first article of this series, so <strong>if you need a refresher, <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">click here</a></strong>.</p>



<h3 class="wp-block-heading">Photon Dashboard</h3>



<p class="wp-block-paragraph">First, create a Photon Engine account. If you don’t have one yet, go to the Photon website and sign up.</p>



<p class="wp-block-paragraph">Next, open the <a href="https://dashboard.photonengine.com/">Photon Dashboard</a> and create a new Fusion 2 application. This will generate a dedicated <strong>App ID</strong> for your game. You can reuse the same <strong>App ID</strong> from previous articles if you want, but remember: network traffic from both projects will mix. This is fine for personal test projects.</p>



<p class="wp-block-paragraph">Once you’ve set up your “My Awesome Game”, copy its <strong>App ID</strong> from the location shown in the image below. Now you’re ready to configure your Unity project.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="509" src="https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-1024x509.png" alt="Photon Fusion 2 App Dashboard." class="wp-image-5445" style="width:563px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-1024x509.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-300x149.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-768x382.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-400x199.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-600x298.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-800x398.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-1000x497.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1-1280x636.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1.png 1348w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">Unity Project</h3>



<p class="wp-block-paragraph">At the time of writing this post, the latest Photon Fusion 2 SDK is <strong>2.0.12 Stable Build 1861</strong>, released <strong>March 19, 2026</strong>. The minimum Unity version required for Photon Fusion 2 is <strong>2021.3.<strong>45</strong>/2022.3.x/6.0.x </strong>or <strong>newer</strong>.</p>



<p class="wp-block-paragraph">Create a new Unity project and import the Fusion 2 SDK. Download the latest Fusion 2 <code><em>.unitypackage</em></code> from <a href="https://doc.photonengine.com/fusion/current/getting-started/sdk-download">this link</a>, then import it using <strong>Assets > Import Package > Custom Package</strong>. After the import, you’ll see the Photon Fusion 2 Hub &#8211; your main management panel. If it doesn’t appear automatically, find it in the top menu under <strong>Tools > Fusion > Fusion Hub.</strong></p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="738" src="https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1024x738.png" alt="Photon Fusion 2 Hub view in Unity." class="wp-image-5444" style="width:440px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1024x738.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-300x216.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-768x554.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-400x288.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-600x433.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-800x577.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed-1000x721.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/unnamed.png 1218w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Paste the <strong>App ID</strong> you copied earlier from the Dashboard into the appropriate field. If the <strong>App ID</strong> is valid, the icon will turn green. Now your setup is complete &#8211; you’re ready to start building with Fusion 2!</p>



<h2 class="wp-block-heading">Scene Management</h2>



<p class="wp-block-paragraph">In this section, you’ll create a SceneLoader class to manage scene transitions during the game. Your project uses three main scenes: Splash_Screen, Main_Menu, and Mission_x. You can add more mission scenes later if you want to expand the project, but for this tutorial, you’ll work with just one. Make sure to include every scene you plan to use in the Build Settings window.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="999" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1024x999.png" alt="Unity Build Settings." class="wp-image-5450" style="width:420px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1024x999.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-300x293.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-768x749.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-400x390.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-600x585.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-800x781.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1000x976.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz.png 1109w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">The <strong>Splash_Screen</strong> scene initialises core game systems. It loads only once when the app starts and creates singleton objects that remain active throughout the session. This scene has its own controller, which moves the game to Main_Menu automatically once all required systems are ready. On Splash_Screen, you’ll set up the Network Manager (explained in the next section), the Missions Manager (handles available mission info), and the SceneLoader itself. You’ll also initialise shared UI elements here, such as pop-up controllers and the loading screen shown when switching scenes.</p>



<p class="wp-block-paragraph">The <strong>Main_Menu</strong> scene is where players choose the game mode &#8211; for example, Singleplayer or Multiplayer &#8211; and adjust settings. Here, players can host a game, join existing sessions, or configure the match before starting. The Host can launch the mission only after every player has picked a vehicle and set their status to Ready. At that point, the SceneLoader switches to the Mission_x scene.</p>



<p class="wp-block-paragraph">The <strong>Mission_x</strong> scene is the specific mission chosen by the Host. In this scene, players control their vehicles and work together to complete mission objectives.</p>



<p class="wp-block-paragraph">Below is the SceneLoader class, which extends the default NetworkSceneManagerDefault controller from Photon Fusion 2. This custom SceneLoader adds support for loading screens, lets you respond to scene loading progress, and gives you a place to add any extra logic your game needs.</p>



<p class="wp-block-paragraph">In the code below, you’ll find a tutorial-only section that introduces a fake loading time. This part simulates large data loads and longer system initialisation; you can safely remove it from your own project.</p>



<p class="wp-block-paragraph">This code assumes Splash_Screen and Main_Menu have scene indexes 0 and 1, and that mission scenes start from index 2. That’s why the LoadMission method uses GetMissionSceneIndex, which calculates the correct scene index for the chosen mission ID. In this tutorial, every scene loads in Single mode, but check the documentation for info on additive scene loading and how to write a custom scene controller using the Fusion 2 INetworkSceneManager interface.</p>



<p class="wp-block-paragraph"><strong>SceneLoader.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using ProtoGDM.Networking;
using ProtoGDM.UI;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace ProtoGDM.ScenesManagement
{
    public class SceneLoader : NetworkSceneManagerDefault
    {
        #region Variables

        public const int SPLASH_SCREEN_SCENE = 0;
        public const int MAIN_MENU_SCENE = 1;

        &#x5B;SerializeField]
        private float minimumLoadingTime = 2f;

        #endregion Variables

        #region Public methods

        public void LoadScene(int index, bool noRunner = false)
        {
            if (!noRunner &amp;&amp; Runner)
            {
                Runner.LoadScene(SceneRef.FromIndex(index));
            }
            else
            {
                StartCoroutine(LoadSceneAsync(index));
            }
        }

        public void LoadMission(Mission mission)
        {
            if (Runner)
            {
                Runner.LoadScene(SceneRef.FromIndex(GetMissionSceneIndex(mission.ID)), LoadSceneMode.Single);
            }
        }

        #endregion Public methods

        #region Private methods

        protected override IEnumerator LoadSceneCoroutine(SceneRef sceneRef, NetworkLoadSceneParameters sceneParams)
        {
            PreLoadScene(sceneRef.AsIndex);

            yield return base.LoadSceneCoroutine(sceneRef, sceneParams);

            // Delay one frame, so we&#039;re sure level objects has spawned locally
            yield return null;

            if (sceneRef.AsIndex &gt; MAIN_MENU_SCENE)
            {
                if (Runner.IsServer &amp;&amp; (Runner.GameMode == GameMode.Single || Runner.GameMode == GameMode.Host))
                {
                    if (Runner.CanSpawn)
                    {
                        foreach (RoomPlayer roomPlayer in RoomPlayer.RoomPlayers)
                        {
                            MissionController.Instance.SpawnPlayerVehicle(Runner, roomPlayer);
                        }
                    }
                }
            }

            PostLoadScene();
        }

        protected override void OnLoadSceneProgress(SceneRef sceneRef, float progress)
        {
            base.OnLoadSceneProgress(sceneRef, progress);

            // TODO: Loading Progress logic here.
        }

        protected override IEnumerator OnSceneLoaded(SceneRef sceneRef, Scene scene, NetworkLoadSceneParameters sceneParams)
        {
            // TODO: Scene Loaded logic here.

            yield return base.OnSceneLoaded(sceneRef, scene, sceneParams);
        }

        private void PreLoadScene(int index)
        {
            LoadingScreenUIManager.Instance.ShowLoadingScreen(1.5f);
        }

        private void PostLoadScene()
        {
            LoadingScreenUIManager.Instance.HideLoadingScreen();

            PopupsUIManager.Instance.HideLastPopup();
        }

        private int GetMissionSceneIndex(int missionIndex)
        {
            int offset = 2;

            return missionIndex + offset;
        }

        private IEnumerator LoadSceneAsync(int index)
        {
            PreLoadScene(index);

            float startTime = Time.time;

            AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(index);

            asyncOperation.allowSceneActivation = false;

            while (asyncOperation.progress &lt; 0.9f)
            {
                yield return null;
            }

            float elapsedTime = Time.time - startTime;

            //Simulated loading delay (minimumLoadingTime) to mimic the loading of larger data assets.
            if (elapsedTime &lt; minimumLoadingTime)
            {
                yield return new WaitForSeconds(minimumLoadingTime - elapsedTime);
            }

            asyncOperation.allowSceneActivation = true;

            while (!asyncOperation.isDone)
            {
                yield return null;
            }

            PostLoadScene();
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">Below is the ready-to-use code for the MissionManager and Mission classes. Add a MissionManager component to an object in the Splash_Screen scene. Be sure to set up at least one mission. The first mission should have an ID of 0 and a valid scene name, such as Mission_1.</p>



<p class="wp-block-paragraph"><strong>MissionsManager.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using ProtoGDM.Utilities;
using System.Collections.Generic;
using UnityEngine;

namespace ProtoGDM
{
    public class MissionsManager : Singleton&lt;MissionsManager&gt;
    {
        #region Properties

        &#x5B;SerializeField]
        private List&lt;Mission&gt; missions;

        #endregion

        #region Unity methods

        protected override void Awake()
        {
            base.Awake();

            DontDestroyOnLoad(gameObject);
        }

        #endregion Unity methods

        #region Public methods

        public Mission GetMission(int id)
        {
            Mission mission = null;

            foreach (var item in missions)
            {
                if (item.ID == id)
                {
                    mission = item;

                    break;
                }
            }

            return mission;
        }

        public List&lt;Mission&gt; GetMissions()
        {
            if (missions != null)
            {
                return new List&lt;Mission&gt;(missions);
            }
            else
            {
                return new List&lt;Mission&gt;();
            }
        }

        #endregion Public methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>Mission.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using System;
using UnityEngine;

namespace ProtoGDM
{
    &#x5B;Serializable]
    public class Mission
    {
        #region Properties

        &#x5B;SerializeField]
        private int id;

        public int ID
        {
            get
            {
                return id;
            }
        }

        &#x5B;SerializeField]
        private string title;

        public string Title
        {
            get
            {
                return title;
            }
        }

        &#x5B;SerializeField]
        private string description;

        public string Description
        {
            get
            {
                return description;
            }
        }

        &#x5B;SerializeField]
        private Sprite sprite;

        public Sprite Sprite
        {
            get
            {
                return sprite;
            }
        }

        &#x5B;SerializeField]
        private string scene;

        public string Scene
        {
            get
            {
                return scene;
            }
        }

        #endregion
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">Here’s an example of a correct test mission definition in the Splash_Screen scene.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="639" height="853" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1.png" alt="Mission Manager prefab inspector." class="wp-image-5452" style="width:246px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1.png 639w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1-225x300.png 225w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1-300x400.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1-400x534.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-1-600x801.png 600w" sizes="auto, (max-width: 639px) 100vw, 639px" /></figure>



<h2 class="wp-block-heading">Network Management</h2>



<p class="wp-block-paragraph">You need to implement the <strong>NetworkManager </strong>class to manage player sessions and handle network events. <strong>NetworkManager </strong>implements the INetworkRunnerCallbacks interface, which gives you callbacks for events such as player connections and disconnections, gathering player input, session data updates, data transmission, and host migration. The class lets you start sessions for a single player or host and join multiplayer games as a Host or Client. It also includes a basic matchmaking mechanic.</p>



<p class="wp-block-paragraph">We <strong>already covered session management </strong>in detail in a previous article of this series. If you need a refresher or want more details, check out the article <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management</a>. In this article, we’ll focus on the basic methods for creating and joining sessions. The next section will cover the mechanics of reconnect.</p>



<p class="wp-block-paragraph"><strong>NetworkManager </strong>is a singleton placed in the Splash_Screen scene. The application creates it at startup and manages all sessions throughout the app&#8217;s lifetime. You need to assign four key references in the Inspector.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="634" height="920" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13.png" alt="Photon Fusion 2 Network Manager prefab inspector." class="wp-image-5475" style="width:261px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13.png 634w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13-207x300.png 207w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13-300x435.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13-400x580.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-13-600x871.png 600w" sizes="auto, (max-width: 634px) 100vw, 634px" /></figure>



<p class="wp-block-paragraph">Reference to <strong>sceneLoader</strong> – the controller from the previous chapter, responsible for scene loading.</p>



<p class="wp-block-paragraph">Prefab <strong>networkRunnerPrefab</strong> – the core Photon Fusion 2 prefab for network communication. Since this demo includes physics, your prefab should have both NetworkRunner and RunnerSimulatePhysics3D components.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="632" height="769" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2.png" alt="Fusion 2 Network Runner prefab inspector." class="wp-image-5457" style="width:253px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2.png 632w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2-247x300.png 247w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2-300x365.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2-400x487.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-2-600x730.png 600w" sizes="auto, (max-width: 632px) 100vw, 632px" /></figure>



<p class="wp-block-paragraph">Prefab <strong>missionSettingsPrefab</strong> – synchronises mission data selected by the Host.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="635" height="620" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-4.png" alt="Mission Settings prefab inspector." class="wp-image-5459" style="width:254px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-4.png 635w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-4-300x293.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-4-400x391.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-4-600x586.png 600w" sizes="auto, (max-width: 635px) 100vw, 635px" /></figure>



<p class="wp-block-paragraph"><strong>MissionSettings.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using System;

namespace ProtoGDM.Networking
{
    public class MissionSettings : NetworkBehaviour
    {
        #region Events

        public static event Action&lt;MissionSettings&gt; OnMissionSettingsUpdated;

        #endregion Events

        #region Variables

        private ChangeDetector changeDetector;

        #endregion Variables

        #region Properties

        public static MissionSettings Instance
        {
            get;
            private set;
        }

        &#x5B;Networked]
        public int MissionIndex
        {
            get;
            set;
        }

        &#x5B;Networked]
        public int DifficultyIndex
        {
            get;
            set;
        }

        #endregion Properties

        #region Unity methods

        private void Awake()
        {
            if (Instance)
            {
                Destroy(gameObject);

                return;
            }

            Instance = this;

            DontDestroyOnLoad(gameObject);
        }

        #endregion Unity methods

        #region Public methods

        public override void Spawned()
        {
            changeDetector = GetChangeDetector(ChangeDetector.Source.SimulationState);

            if (Object.HasStateAuthority)
            {
                MissionIndex = 0;

                DifficultyIndex = 1;
            }
        }

        public float GetDifficultyFactor()
        {
            switch (DifficultyIndex)
            {
                case 0:
                    return 0.5f;
                case 1:
                    return 1f;
                case 2:
                    return 1.5f;
                default:
                    return 1f;
            }
        }

        public override void Render()
        {
            foreach (var change in changeDetector.DetectChanges(this))
            {
                switch (change)
                {
                    case nameof(MissionIndex):
                    case nameof(DifficultyIndex):
                        OnMissionSettingsChangedCallback(this);
                        break;
                }
            }
        }

        #endregion Public methods

        #region Private methods

        private void OnMissionSettingsChangedCallback(MissionSettings gameManager)
        {
            OnMissionSettingsUpdated?.Invoke(gameManager);
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">Prefab <strong>roomPlayerPrefab</strong> – represents each player in a session, synchronises player data, and supports the reconnect mechanic. You’ll learn more about RoomPlayer in later chapters.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="632" height="907" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3.png" alt="Room Player prefab inspector." class="wp-image-5458" style="width:245px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3.png 632w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3-209x300.png 209w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3-300x431.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3-400x574.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-3-600x861.png 600w" sizes="auto, (max-width: 632px) 100vw, 632px" /></figure>



<p class="wp-block-paragraph">This tutorial uses a Host-Client topology. One of the Clients acts as the Server and has authority over all other players. Players can create their own sessions, set a custom name, choose the number of players (2, 3, or 4), and protect the session with a password if needed.</p>



<p class="wp-block-paragraph">Below you’ll find the full code for the NetworkManager class. The code is long, but understanding each method will help you write and modify your own multiplayer logic. In this example, you won’t see every callback from the INetworkRunnerCallbacks interface. The unused callbacks are grouped in a separate region.</p>



<p class="wp-block-paragraph"><strong>NetworkManager.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using Fusion.Photon.Realtime;
using Fusion.Sockets;
using ProtoGDM.ScenesManagement;
using ProtoGDM.Utilities;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;

namespace ProtoGDM.Networking
{
    public class NetworkManager : Singleton&lt;NetworkManager&gt;, INetworkRunnerCallbacks
    {
        #region Events

        public event Action&lt;List&lt;SessionInfo&gt;&gt; OnNetworkRunnerSessionListUpdated;

        public event Action&lt;ShutdownReason&gt; OnNetworkRunnerShutdown;

        #endregion Events

        #region Consts

        public const string SESSION_NAME_KEY = &quot;sessionName&quot;;
        public const string SESSION_PASSWORD_REQUIRED_KEY = &quot;sessionPasswordRequired&quot;;

        #endregion Consts

        #region Variables

        &#x5B;SerializeField]
        private SceneLoader sceneLoader;

        &#x5B;SerializeField]
        private NetworkRunner networkRunnerPrefab;

        &#x5B;SerializeField]
        private MissionSettings missionSettingsPrefab;

        &#x5B;SerializeField]
        private RoomPlayer roomPlayerPrefab;

        private MissionSettings missionSettings;

        private bool currentSessionPasswordRequired;

        private string currentSessionPassword;

        private Dictionary&lt;string, RoomPlayer&gt; roomPlayerMap;

        #endregion Variables

        #region Properties

        private NetworkRunner networkRunner;

        public NetworkRunner NetworkRunner
        {
            get
            {
                return networkRunner;
            }

            private set
            {
                networkRunner = value;
            }
        }

        &#x5B;SerializeField]
        private GameMode gameMode;

        public GameMode GameMode
        {
            get
            {
                return gameMode;
            }

            private set
            {
                gameMode = value;
            }
        }

        #endregion

        #region Unity methods

        protected override void Awake()
        {
            base.Awake();

            roomPlayerMap = new Dictionary&lt;string, RoomPlayer&gt;();

            DontDestroyOnLoad(gameObject);
        }

        private void Start()
        {
            //RoomPlayer.RemovePlayerData();

            if (!RoomPlayer.LoadPlayerData())
            {
                RoomPlayer.CreatePlayerData(&quot;Player_&quot; + UnityEngine.Random.Range(0, 1000), null);
            }
        }

        #endregion Unity methods

        #region Public methods

        #region INetworkRunnerCallbacks

        public void OnSessionListUpdated(NetworkRunner networkRunner, List&lt;SessionInfo&gt; sessionList)
        {
            OnNetworkRunnerSessionListUpdated?.Invoke(sessionList);
        }

        public void OnConnectRequest(NetworkRunner networkRunner, NetworkRunnerCallbackArgs.ConnectRequest request, byte&#x5B;] token)
        {
            string password = string.Empty;

            if (token != null)
            {
                ConnectionToken connectionToken = ConnectionToken.FromBytes(token);

                if (connectionToken != null)
                {
                    password = connectionToken.password;

                    string reconnectGuid = connectionToken.reconnectGuid;

                    if (roomPlayerMap.ContainsKey(reconnectGuid))
                    {
                        RoomPlayer roomPlayer = roomPlayerMap&#x5B;reconnectGuid];

                        if (roomPlayer == null)
                        {
                            roomPlayerMap.Remove(reconnectGuid);

                            request.Refuse();

                            return;
                        }
                    }
                }
            }

            if (currentSessionPasswordRequired)
            {
                if (currentSessionPassword.Equals(password))
                {
                    request.Accept();
                }
                else
                {
                    request.Refuse();
                }
            }
            else
            {
                request.Accept();
            }
        }

        public void OnPlayerJoined(NetworkRunner networkRunner, PlayerRef player)
        {
            if (networkRunner.CanSpawn)
            {
                byte&#x5B;] playerConnectionToken = networkRunner.GetPlayerConnectionToken(player);

                ConnectionToken connectionToken = ConnectionToken.FromBytes(playerConnectionToken);

                string reconnectGuid = string.Empty;

                if (connectionToken != null)
                {
                    reconnectGuid = connectionToken.reconnectGuid;
                }

                if (!missionSettings)
                {
                    missionSettings = networkRunner.Spawn(missionSettingsPrefab, Vector3.zero, Quaternion.identity);
                }

                RoomPlayer roomPlayer = null;

                if (roomPlayerMap.TryGetValue(reconnectGuid, out roomPlayer) == false || roomPlayer == null)
                {
                    roomPlayer = networkRunner.Spawn(roomPlayerPrefab, Vector3.zero, Quaternion.identity, player, (networkRunner, networkObject) =&gt;
                    {
                        networkObject.GetBehaviour&lt;RoomPlayer&gt;().ReconnectGuid = reconnectGuid;
                    });
                }

                if (connectionToken != null)
                {
                    roomPlayerMap&#x5B;reconnectGuid] = roomPlayer;
                }

                roomPlayer.AssignInputAuthority(player);
                roomPlayer.IsDisconnected = false;
                roomPlayer.DisconnectTick = 0;

                networkRunner.SetPlayerObject(player, roomPlayer.Object);
            }
        }

        public void OnPlayerLeft(NetworkRunner networkRunner, PlayerRef player)
        {
            RoomPlayer roomPlayer = RoomPlayer.GetPlayer(player);

            if (roomPlayer != null)
            {
                roomPlayer.IsDisconnected = true;

                roomPlayer.DisconnectTick = networkRunner.Tick;
            }
        }

        public void OnInput(NetworkRunner networkRunner, NetworkInput networkInput)
        {
            NetworkInputData networkedInputData = new NetworkInputData();

            networkedInputData.acceleration = PlayerInput.GetAxis(&quot;Vertical&quot;);
            networkedInputData.braking = PlayerInput.GetKey(KeyCode.Space) ? 1f : 0f;
            networkedInputData.steering = PlayerInput.GetAxis(&quot;Horizontal&quot;);
            networkedInputData.shooting = PlayerInput.GetMouseButton(0);

            networkedInputData.networkButtons.Set(NetworkInputData.ButtonTypes.Reload, PlayerInput.GetKey(KeyCode.R));
            networkedInputData.networkButtons.Set(NetworkInputData.ButtonTypes.Reset, PlayerInput.GetKey(KeyCode.P));

            Ray ray = Camera.main.ScreenPointToRay(PlayerInput.MousePosition);

            Plane groundPlane = new Plane(Vector3.up, 0.5f * Vector3.up);

            if (groundPlane.Raycast(ray, out float rayDistance))
            {
                Vector3 targetPoint = ray.GetPoint(rayDistance);

                networkedInputData.targetPoint = targetPoint;
            }

            networkInput.Set(networkedInputData);
        }

        public void OnShutdown(NetworkRunner networkRunner, ShutdownReason shutdownReason)
        {
            if (SceneManager.GetActiveScene().buildIndex &gt; SceneLoader.MAIN_MENU_SCENE)
            {
                sceneLoader.LoadScene(SceneLoader.MAIN_MENU_SCENE, true);
            }

            OnNetworkRunnerShutdown?.Invoke(shutdownReason);
        }

        public void OnDisconnectedFromServer(NetworkRunner networkRunner, NetDisconnectReason reason)
        {
            ShutdownSession();
        }

        #region NOT USED
        public void OnConnectedToServer(NetworkRunner networkRunner) { }
        public void OnObjectExitAOI(NetworkRunner networkRunner, NetworkObject obj, PlayerRef player) { }
        public void OnObjectEnterAOI(NetworkRunner networkRunner, NetworkObject obj, PlayerRef player) { }
        public void OnInputMissing(NetworkRunner networkRunner, PlayerRef player, NetworkInput input) { }
        public void OnConnectFailed(NetworkRunner networkRunner, NetAddress remoteAddress, NetConnectFailedReason reason) { }
        public void OnUserSimulationMessage(NetworkRunner networkRunner, SimulationMessagePtr message) { }
        public void OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary&lt;string, object&gt; data) { }
        public void OnHostMigration(NetworkRunner networkRunner, HostMigrationToken hostMigrationToken) { }
        public void OnReliableDataProgress(NetworkRunner networkRunner, PlayerRef player, ReliableKey reliableKey, float progress) { }
        public void OnReliableDataReceived(NetworkRunner networkRunner, PlayerRef player, ReliableKey reliableKey, ArraySegment&lt;byte&gt; data) { }
        public void OnSceneLoadStart(NetworkRunner networkRunner) { }
        public void OnSceneLoadDone(NetworkRunner networkRunner) { }
        #endregion NOT USED

        #endregion INetworkRunnerCallbacks

        public async Task StartSingle(string sessionName, Action onSuccess, Action&lt;string&gt; onFailure)
        {
            GameMode = GameMode.Single;

            if (NetworkRunner == null)
            {
                CreateNetworkRunner();
            }

            currentSessionPasswordRequired = false;
            currentSessionPassword = null;

            Dictionary&lt;string, SessionProperty&gt; sessionProperties = new Dictionary&lt;string, SessionProperty&gt;
            {
                { SESSION_NAME_KEY, sessionName },
                { SESSION_PASSWORD_REQUIRED_KEY, currentSessionPasswordRequired },
            };

            ConnectionToken connectionToken = new ConnectionToken(null, SessionID.GuidString);

            StartGameArgs startGameArgs = new StartGameArgs()
            {
                GameMode = GameMode,
                PlayerCount = 1,
                SessionProperties = sessionProperties,
                SceneManager = sceneLoader,
                ConnectionToken = ConnectionToken.ToBytes(connectionToken)
            };

            StartGameResult result = await NetworkRunner.StartGame(startGameArgs);

            if (result.Ok)
            {
                Debug.Log($&quot;StartSingle Mode {NetworkRunner.Mode} {NetworkRunner.IsServer} {NetworkRunner.IsClient}&quot;);

                onSuccess?.Invoke();
            }
            else
            {
                Debug.LogError($&quot;StartHost Failure {result.ShutdownReason}&quot;);

                currentSessionPassword = string.Empty;

                onFailure?.Invoke(result.ShutdownReason.ToString());
            }
        }

        public async Task StartHost(string sessionName, string password, int maxPlayers, Action onSuccess, Action&lt;string&gt; onFailure)
        {
            GameMode = GameMode.Host;

            if (NetworkRunner == null)
            {
                CreateNetworkRunner();
            }

            currentSessionPasswordRequired = !string.IsNullOrEmpty(password);
            currentSessionPassword = password;

            Dictionary&lt;string, SessionProperty&gt; sessionProperties = new Dictionary&lt;string, SessionProperty&gt;
            {
                { SESSION_NAME_KEY, sessionName },
                { SESSION_PASSWORD_REQUIRED_KEY, currentSessionPasswordRequired },
            };

            var sceneRef = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
            NetworkSceneInfo info = new NetworkSceneInfo();
            info.AddSceneRef(sceneRef, LoadSceneMode.Single);

            ConnectionToken connectionToken = new ConnectionToken(password, SessionID.GuidString);

            StartGameArgs startGameArgs = new StartGameArgs()
            {
                Scene = info,
                GameMode = GameMode,
                SessionName = sessionName,
                PlayerCount = maxPlayers,
                SessionProperties = sessionProperties,
                SceneManager = sceneLoader
            };

            StartGameResult result = await NetworkRunner.StartGame(startGameArgs);

            if (result.Ok)
            {
                Debug.Log($&quot;StartHost Mode {NetworkRunner.Mode} {NetworkRunner.IsServer} {NetworkRunner.IsClient}&quot;);

                onSuccess?.Invoke();
            }
            else
            {
                Debug.LogError($&quot;StartHost Failure {result.ShutdownReason}&quot;);

                currentSessionPassword = string.Empty;

                onFailure?.Invoke(result.ShutdownReason.ToString());
            }
        }

        public async Task JoinLobby(Action onSuccess, Action&lt;string&gt; onFailure)
        {
            if (NetworkRunner == null)
            {
                CreateNetworkRunner();
            }

            StartGameResult result = await NetworkRunner.JoinSessionLobby(SessionLobby.ClientServer);

            if (result.Ok)
            {
                // all ok
                onSuccess?.Invoke();
            }
            else
            {
                // log error
                Debug.LogError($&quot;JoinLobby failure {result.ShutdownReason}&quot;);

                onFailure?.Invoke(result.ShutdownReason.ToString());
            }
        }

        public async Task JoinSession(string sessionName, string password, Action onSuccess, Action&lt;string&gt; onFailure)
        {
            GameMode = GameMode.Client;

            if (NetworkRunner == null)
            {
                CreateNetworkRunner();
            }

            var sceneRef = SceneRef.FromIndex(SceneManager.GetActiveScene().buildIndex);
            NetworkSceneInfo info = new NetworkSceneInfo();
            info.AddSceneRef(sceneRef, LoadSceneMode.Single);

            ConnectionToken connectionToken = new ConnectionToken(password, SessionID.GuidString);

            StartGameArgs startGameArgs = new StartGameArgs
            {
                Scene = info,
                GameMode = GameMode,
                SessionName = sessionName,
                SceneManager = sceneLoader,
                ConnectionToken = ConnectionToken.ToBytes(connectionToken)
            };

            StartGameResult result = await NetworkRunner.StartGame(startGameArgs);

            if (result.Ok)
            {
                Debug.Log($&quot;JoinSession Mode {NetworkRunner.Mode} {NetworkRunner.IsServer} {NetworkRunner.IsClient}&quot;);

                PlayerPrefs.SetString(&quot;CURRENT_SESSION_NAME&quot;, sessionName);
                PlayerPrefs.SetString(&quot;CURRENT_CONNECTION_TOKEN&quot;, Convert.ToBase64String(ConnectionToken.ToBytes(connectionToken)));
                PlayerPrefs.Save();

                onSuccess?.Invoke();
            }
            else
            {
                Debug.LogError($&quot;JoinSession Failure {result.ShutdownReason}&quot;);

                onFailure?.Invoke(result.ShutdownReason.ToString());
            }
        }

        public async Task StartMatchmake(string sessionName, int maxPlayers, Action onSuccess, Action&lt;string&gt; onFailure)
        {
            GameMode = GameMode.AutoHostOrClient;

            if (NetworkRunner == null)
            {
                CreateNetworkRunner();
            }

            currentSessionPasswordRequired = false;
            currentSessionPassword = string.Empty;

            Dictionary&lt;string, SessionProperty&gt; sessionProperties = new Dictionary&lt;string, SessionProperty&gt;
            {
                { SESSION_NAME_KEY, sessionName },
                { SESSION_PASSWORD_REQUIRED_KEY, currentSessionPasswordRequired },
            };

            ConnectionToken connectionToken = new ConnectionToken(string.Empty, SessionID.GuidString);

            StartGameArgs startGameArgs = new StartGameArgs
            {
                GameMode = GameMode,
                PlayerCount = maxPlayers,
                SessionProperties = sessionProperties,
                SceneManager = sceneLoader,
                MatchmakingMode = MatchmakingMode.RandomMatching,
                ConnectionToken = ConnectionToken.ToBytes(connectionToken)
            };

            StartGameResult result = await NetworkRunner.StartGame(startGameArgs);

            if (result.Ok)
            {
                Debug.Log($&quot;JoinSessionMatchmake Mode {NetworkRunner.Mode} {NetworkRunner.IsServer} {NetworkRunner.IsClient}&quot;);

                onSuccess?.Invoke();
            }
            else
            {
                Debug.LogError($&quot;JoinSessionMatchmake Failure {result.ShutdownReason}&quot;);

                onFailure?.Invoke(result.ShutdownReason.ToString());
            }
        }

        public void ShowSession()
        {
            if (!NetworkRunner.IsServer)
            {
                return;
            }

            NetworkRunner.SessionInfo.IsVisible = true;
        }

        public void HideSession()
        {
            if (!NetworkRunner.IsServer)
            {
                return;
            }

            NetworkRunner.SessionInfo.IsVisible = false;
        }

        public void ShutdownSession()
        {
            if (NetworkRunner != null)
            {
                NetworkRunner.Shutdown();
            }

            GameMode = 0;
        }

        #endregion Public methods

        #region Private methods

        private void CreateNetworkRunner()
        {
            NetworkRunner = Instantiate(networkRunnerPrefab);

            NetworkRunner.AddCallbacks(this);
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>Singleton.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using UnityEngine;

namespace ProtoGDM.Utilities
{
    public abstract class Singleton&lt;T&gt; : MonoBehaviour where T : Singleton&lt;T&gt;
    {
        &#x5B;Header(&quot;Singleton&quot;)]

        &#x5B;SerializeField]
        &#x5B;Tooltip(&quot;Automatically sets this script as the instance&quot;)]
        private bool automaticallyAssignAsInstance = true;

        &#x5B;SerializeField]
        &#x5B;Tooltip(&quot;Debug this singleton&quot;)]
        private bool singletonDebug = false;

        private static T instance;

        public static T Instance
        {
            get
            {
                if (instance == null)
                {
                    Debug.LogError($&quot;Inst doesn&#039;t exist!&quot;);
                    return null;
                }

                if (instance.singletonDebug)
                {
                    Debug.Log($&quot;Retreiving singleton.&quot;);
                }

                return instance;
            }

            set
            {
                if (instance != null &amp;&amp; instance.singletonDebug)
                {
                    Debug.Log($&quot;Changing singleton instance to {instance?.GetInstanceID()}&quot;);
                }

                instance = value;

                if (instance != null &amp;&amp; instance.singletonDebug)
                {
                    Debug.Log($&quot;Changed singleton instance&quot;);
                }
            }
        }

        public static bool InstExists =&gt; (instance != null);

        protected virtual void Awake()
        {
            if (automaticallyAssignAsInstance)
            {
                Instance = (T)this;
            }
        }

        protected virtual void OnDestroy()
        {
            Instance = null;
        }
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">NetworkManager works as a singleton. This approach makes implementation easier, but be mindful not to overuse global access. If you prefer, you can use explicit references instead of the singleton pattern.<br><br>The class exposes a NetworkRunner property. The whole application uses just one NetworkRunner to manage network communication, so NetworkManager serves as a central reference point for this component.</p>



<p class="wp-block-paragraph">NetworkManager offers several key public methods:</p>



<p class="wp-block-paragraph"><strong>StartSingle</strong> – Starts a single-player session. You still get all Fusion 2 features, but without any internet connection. This is great for small projects that share logic between single and multiplayer modes.</p>



<p class="wp-block-paragraph"><strong>StartHost</strong> – Creates a new session as the Host. You set the session name, player limit, and password protection if needed. You join as the first player and act as the Host.</p>



<p class="wp-block-paragraph"><strong>JoinLobby</strong> – Joins a Fusion 2 Lobby. A Lobby is tied to a specific region and lists all active sessions in that region.</p>



<p class="wp-block-paragraph"><strong>JoinSession</strong> – Lets you join a session by name and password (if required). You join as a Client in the selected session.</p>



<p class="wp-block-paragraph"><strong>ShowSession</strong> and <strong>HideSession</strong> – These methods control session visibility in the region’s Lobby. Use them as the Host to hide the session during gameplay or show it when waiting for players for the next round.</p>



<p class="wp-block-paragraph"><strong>ShutdownSession</strong> – Shuts down the NetworkRunner and closes the network connection.</p>



<h2 class="wp-block-heading">Network Objects</h2>



<p class="wp-block-paragraph">In this section, you’ll learn about the core networked objects that enable multiplayer gameplay. First, you’ll see how the RoomPlayer object represents each player who joins a session and gets ready for the game. Every player has a unique RoomPlayer instance that keeps their data synchronised across all peers.</p>



<p class="wp-block-paragraph">After that, you’ll explore the main objects present in the gameplay scene. The MissionController class handles spawning player vehicles at the start of a mission and manages mission objectives and rewards. You’ll also discover how destructible elements work, starting with the base DestructibleObject class, followed by TurretWeaponController, VehicleController, and PlayerVehicleController.</p>



<h3 class="wp-block-heading">Room Player</h3>



<p class="wp-block-paragraph">When a player joins a session as a Client, the Host triggers the OnConnectRequest callback. The Host decides in this method whether to let the player join the session. You’ll learn more about this method when we discuss reconnecting players later in the tutorial. If the Host accepts the join request, Fusion 2 assigns the player to the session and then calls the OnPlayerJoined callback. In this callback, the Host spawns a RoomPlayer object for the new player and gives them InputAuthority.</p>



<p class="wp-block-paragraph">RoomPlayer stores and synchronises key player data, including the player’s name, current status (showing their current location), resources such as Coins and Titanium, the selected vehicle (VehiclePresetID), and the player’s ready status (IsReady).</p>



<p class="wp-block-paragraph">You can also use RoomPlayer to save and load player data. In this demo, the system uses PlayerPrefs to store data locally. For real-world projects, you should replace this approach with a cloud-based service that securely saves data and protects against cheating.</p>



<p class="wp-block-paragraph"><strong>RoomPlayer.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using MemoryPack;
using ProtoGDM.Data;
using ProtoGDM.Data.Presets;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace ProtoGDM.Networking
{
    public class RoomPlayer : NetworkBehaviour
    {
        #region Events

        public static event Action&lt;RoomPlayer&gt; OnPlayerJoined;
        public static event Action&lt;RoomPlayer&gt; OnPlayerLeft;

        #endregion Events

        #region Variables

        private const string PLAYER_DATA_KEY = &quot;playerData&quot;;

        public static readonly List&lt;RoomPlayer&gt; RoomPlayers = new List&lt;RoomPlayer&gt;();

        public static RoomPlayer LocalRoomPlayer;

        public static PlayerData LocalPlayerData;

        public PlayerVehicleController PlayerVehicleController
        {
            get;
            set;
        }

        private ChangeDetector changeDetector;

        #endregion Variables

        #region Properties

        &#x5B;Networked]
        public NetworkBool IsInitialized
        {
            get;
            set;
        }

        &#x5B;Networked]
        public NetworkBool IsDisconnected
        {
            get;
            set;
        }

        &#x5B;Networked]
        public NetworkString&lt;_64&gt; ReconnectGuid
        {
            get;
            set;
        }

        &#x5B;Networked]
        public float DisconnectTick
        {
            get;
            set;
        }

        &#x5B;Networked]
        public NetworkString&lt;_32&gt; Name
        {
            get;
            set;
        }

        &#x5B;Networked]
        public int Coins
        {
            get;
            set;
        }

        &#x5B;Networked]
        public int Titanium
        {
            get;
            set;
        }

        &#x5B;Networked]
        public int VehiclePresetID
        {
            get;
            set;
        }

        &#x5B;Networked]
        public NetworkString&lt;_32&gt; Status
        {
            get;
            set;
        }

        &#x5B;Networked]
        public NetworkBool IsReady
        {
            get;
            set;
        }

        #endregion Properties

        #region Unity methods

        private void OnDestroy()
        {
            OnPlayerLeft?.Invoke(this);

            RoomPlayers.Remove(this);
        }

        public override void FixedUpdateNetwork()
        {
            if (IsDisconnected &amp;&amp; Runner.IsServer)
            {
                float secondsDisconnected = (Runner.Tick - DisconnectTick) * Runner.DeltaTime;

                if (secondsDisconnected &gt; 30f)
                {
                    if (PlayerVehicleController)
                    {
                        Runner.Despawn(PlayerVehicleController.Object);
                    }

                    Runner.Despawn(Object);
                }
            }
        }

        #endregion Unity methods

        #region Public methods

        #region RPC

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority, HostMode = RpcHostMode.SourceIsHostPlayer)]
        public void RPC_ChangeIsInitialized(NetworkBool isInitialized, RpcInfo info = default)
        {
            IsInitialized = isInitialized;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
        public void RPC_SetPlayerName(NetworkString&lt;_32&gt; name, RpcInfo info = default)
        {
            Name = name;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
        public void RPC_SetPlayerStatus(NetworkString&lt;_32&gt; status, RpcInfo info = default)
        {
            Status = status;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
        public void RPC_ChangeCoins(int coins, RpcInfo info = default)
        {
            Coins = coins;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
        public void RPC_ChangeTitanium(int titanium, RpcInfo info = default)
        {
            Titanium = titanium;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority)]
        public void RPC_ChangeVehiclePresetID(int vehiclePresetID, RpcInfo info = default)
        {
            VehiclePresetID = vehiclePresetID;
        }

        &#x5B;Rpc(RpcSources.InputAuthority, RpcTargets.StateAuthority, HostMode = RpcHostMode.SourceIsHostPlayer)]
        public void RPC_ChangeIsReady(NetworkBool isReady, RpcInfo info = default)
        {
            IsReady = isReady;
        }

        #endregion RPC

        public override void Spawned()
        {
            changeDetector = GetChangeDetector(ChangeDetector.Source.SimulationState);

            if (Object.HasInputAuthority)
            {
                LocalRoomPlayer = this;

                if (!IsInitialized &amp;&amp; LocalPlayerData != null)
                {
                    LocalRoomPlayer.RPC_SetPlayerName(LocalPlayerData.name);
                    LocalRoomPlayer.RPC_SetPlayerStatus(&quot;Lobby&quot;);
                    LocalRoomPlayer.RPC_ChangeCoins(LocalPlayerData.coins);
                    LocalRoomPlayer.RPC_ChangeTitanium(LocalPlayerData.titanium);

                    if (LocalPlayerData.vehicle != null)
                    {
                        LocalRoomPlayer.RPC_ChangeVehiclePresetID(LocalPlayerData.vehicle.presetId);

                        if (Runner.GameMode == GameMode.Single)
                        {
                            LocalRoomPlayer.RPC_ChangeIsReady(true);
                        }
                    }
                    else
                    {
                        LocalRoomPlayer.RPC_ChangeVehiclePresetID(-1);
                        LocalRoomPlayer.RPC_ChangeIsReady(false);
                    }

                    LocalRoomPlayer.RPC_ChangeIsInitialized(true);
                }
            }

            RoomPlayers.Add(this);

            OnPlayerJoined?.Invoke(this);

            DontDestroyOnLoad(gameObject);
        }

        public override void Render()
        {
            foreach (var change in changeDetector.DetectChanges(this))
            {
                switch (change)
                {
                    case nameof(Name):
                    case nameof(Coins):
                    case nameof(Titanium):
                    case nameof(VehiclePresetID):
                    case nameof(IsReady):

                        if (Object.HasInputAuthority)
                        {
                            LocalPlayerData.name = Name.ToString();
                            LocalPlayerData.coins = Coins;
                            LocalPlayerData.titanium = Titanium;

                            SavePlayerData();
                        }

                        break;
                }
            }
        }

        public void AssignInputAuthority(PlayerRef playerRef)
        {
            Object.AssignInputAuthority(playerRef);

            if (PlayerVehicleController)
            {
                PlayerVehicleController.AssignInputAuthority(playerRef);
            }
        }

        public static RoomPlayer GetPlayer(PlayerRef playerRef)
        {
            return RoomPlayers.FirstOrDefault(x =&gt; x.Object.InputAuthority == playerRef);
        }

        public static void RemovePlayer(NetworkRunner networkRunner, PlayerRef playerRef)
        {
            RoomPlayer roomPlayer = GetPlayer(playerRef);

            if (roomPlayer != null)
            {
                RoomPlayers.Remove(roomPlayer);

                networkRunner.Despawn(roomPlayer.Object);
            }
        }

        public static bool CreatePlayerData(string name, VehicleSO vehicleSO)
        {
            VehicleData vehicleData = null;

            if (vehicleSO != null)
            {
                vehicleData = new VehicleData(vehicleSO);
            }

            LocalPlayerData = new PlayerData(name, 0, 0, vehicleData);

            LocalPlayerData.AddToInventory(new VehicleData(PresetsManager.Instance.GetVehicleById(0)));
            LocalPlayerData.AddToInventory(new VehicleData(PresetsManager.Instance.GetVehicleById(1)));

            return SavePlayerData();
        }

        public static bool SavePlayerData()
        {
            if (LocalPlayerData != null)
            {
                byte&#x5B;] data = MemoryPackSerializer.Serialize(LocalPlayerData);

                string base64Data = Convert.ToBase64String(data);

                PlayerPrefs.SetString(PLAYER_DATA_KEY, base64Data);

                PlayerPrefs.Save();

                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool LoadPlayerData()
        {
            if (PlayerPrefs.HasKey(PLAYER_DATA_KEY))
            {
                string base64Data = PlayerPrefs.GetString(PLAYER_DATA_KEY);

                byte&#x5B;] data = Convert.FromBase64String(base64Data);

                LocalPlayerData = MemoryPackSerializer.Deserialize&lt;PlayerData&gt;(data);

                return LocalPlayerData != null;
            }
            else
            {
                return false;
            }
        }

        public static bool RemovePlayerData()
        {
            if (PlayerPrefs.HasKey(PLAYER_DATA_KEY))
            {
                PlayerPrefs.DeleteKey(PLAYER_DATA_KEY);

                return true;
            }
            else
            {
                return false;
            }
        }

        #endregion Public methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<h3 class="wp-block-heading">Mission Controller</h3>



<p class="wp-block-paragraph">MissionController manages victory conditions in each mission. The class also handles creating and destroying player vehicles. For demo purposes, it provides basic UI control during missions, but in a real project, you should move this logic to a dedicated UI controller. In this demo, victory conditions are a simple list of objects that players must destroy to win. You can expand or customise these conditions for each mission as your game evolves.</p>



<p class="wp-block-paragraph">Pay special attention to the <strong>SpawnPlayerVehicle</strong> method. This method selects a predefined spawn point using the player&#8217;s index, then chooses the right vehicle prefab using the VehiclePresetID from RoomPlayer. It creates the vehicle instance in the scene and assigns it to the roomPlayer.PlayerVehicleController. <strong>This reference is important</strong> for implementing reconnect features later on.</p>



<p class="wp-block-paragraph">MissionController listens for events when a player’s vehicle is destroyed. The class tracks the number of active vehicles and displays a message when the last player is eliminated. The DestructibleObject_OnDestructibleDestroy callback checks if all mission targets are destroyed. If so, the Host sees a message about mission completion and rewards, and has the option to return everyone to the Session Lobby. Clients see a similar message about mission completion and rewards, but can only leave the session. Only the Host can bring the whole team back to the Lobby. Individual Clients can quit the session if they choose.</p>



<p class="wp-block-paragraph"><strong>MissionController.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using ProtoGDM.Data;
using ProtoGDM.Networking;
using ProtoGDM.ScenesManagement;
using ProtoGDM.UI;
using ProtoGDM.Utilities;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace ProtoGDM
{
    public class MissionController : NetworkBehaviour
    {
        #region Variables

        &#x5B;Tooltip(&quot;Check in Inspector when scene is not ready and is during development.&quot;)]
        &#x5B;SerializeField]
        private bool notReady = false;

        &#x5B;SerializeField]
        private List&lt;Transform&gt; spawnPoints;

        private Dictionary&lt;RoomPlayer, (PlayerVehicleController, NetworkObject)&gt; players = new Dictionary&lt;RoomPlayer, (PlayerVehicleController, NetworkObject)&gt;();

        &#x5B;Header(&quot;Objectives&quot;)]

        &#x5B;SerializeField]
        private List&lt;DestructibleObject&gt; objectsToDestroy;

        &#x5B;Header(&quot;Rewards&quot;)]

        &#x5B;SerializeField]
        private int coins;

        &#x5B;SerializeField]
        private int titanium;

        #endregion Variables

        #region Properties

        public static MissionController Instance
        {
            get;
            private set;
        }

        public bool ObjectivesFulfilled
        {
            get
            {
                return objectsToDestroy.Count == 0;
            }
        }

        #endregion Properties

        #region Unity methods 

        private void Awake()
        {
            if (Instance)
            {
                Destroy(gameObject);

                return;
            }

            Instance = this;
        }

        private void Update()
        {
            if (PlayerInput.Enabled &amp;&amp; Input.GetKeyUp(KeyCode.Escape))
            {
                PlayerInput.Enabled = false;

                PopupsUIManager.Instance.ShowDecisionPopup(
                    &quot;Surrender?&quot;,
                    &quot;Yes&quot;,
                    () =&gt;
                    {
                        PopupsUIManager.Instance.HideLastPopup();

                        PopupsUIManager.Instance.ShowInfoPopup(
                            &quot;Mission Failed!&quot;,
                            &quot;Ok&quot;,
                            () =&gt;
                            {
                                PopupsUIManager.Instance.HideLastPopup();

                                EndMission();
                            });
                    },
                    &quot;No&quot;,
                    () =&gt;
                    {
                        PlayerInput.Enabled = true;

                        PopupsUIManager.Instance.HideLastPopup();
                    });
            }
        }

        private void OnDestroy()
        {
            if (notReady)
            {
                return;
            }

            Instance = null;
        }

        #endregion Unity methods

        #region Public methods

        public override void Spawned()
        {
            RegisterObjectives();

            PlayerInput.Enabled = true;
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            UnregisterObjectives();

            PlayerInput.Enabled = false;
        }

        public void SpawnPlayerVehicle(NetworkRunner networkRunner, RoomPlayer roomPlayer)
        {
            if (!networkRunner)
            {
                throw new NullReferenceException(&quot;NetworkRunner is null!&quot;);
            }

            if (!roomPlayer)
            {
                throw new NullReferenceException(&quot;RoomPlayer is null!&quot;);
            }

            int index = RoomPlayer.RoomPlayers.IndexOf(roomPlayer);

            if (index &gt;= 0 &amp;&amp; index &lt; spawnPoints.Count)
            {
                Transform spawnPointTransform = spawnPoints&#x5B;index];

                NetworkObject vehicleNetworkObject = networkRunner.Spawn(PresetsManager.Instance.GetVehicleById(roomPlayer.VehiclePresetID).Prefab, spawnPointTransform.position, spawnPointTransform.rotation, roomPlayer.Object.InputAuthority);

                PlayerVehicleController playerVehicleController = vehicleNetworkObject.GetComponent&lt;PlayerVehicleController&gt;();

                if (playerVehicleController)
                {
                    roomPlayer.PlayerVehicleController = playerVehicleController;

                    playerVehicleController.OnDestructibleDestroy += PlayerVehicleController_OnDestructibleDestroy;

                    if (!players.ContainsKey(roomPlayer))
                    {
                        players.Add(roomPlayer, (playerVehicleController, vehicleNetworkObject));
                    }
                }
            }
        }

        public void DespawnPlayerVehicle(NetworkRunner networkRunner, RoomPlayer roomPlayer)
        {
            if (players.ContainsKey(roomPlayer))
            {
                PlayerVehicleController playerVehicleController = players&#x5B;roomPlayer].Item1;

                NetworkObject networkObject = players&#x5B;roomPlayer].Item2;

                if (playerVehicleController)
                {
                    playerVehicleController.OnDestructibleDestroy -= PlayerVehicleController_OnDestructibleDestroy;
                }

                players.Remove(roomPlayer);

                networkRunner.Despawn(networkObject);
            }
        }

        public void EndMission()
        {
            SceneLoader sceneLoader = FindObjectOfType&lt;SceneLoader&gt;();

            if (sceneLoader.Runner)
            {
                if (HasStateAuthority)
                {
                    sceneLoader.LoadScene(SceneLoader.MAIN_MENU_SCENE);
                }
                else
                {
                    NetworkManager.Instance.ShutdownSession();
                }
            }
            else
            {
                sceneLoader.LoadScene(SceneLoader.MAIN_MENU_SCENE);
            }
        }

        #endregion Public methods

        #region Private methods

        #region Event callbacks

        private void PlayerVehicleController_OnDestructibleDestroy(DestructibleObject destructibleObject)
        {
            RoomPlayer roomPlayer = null;

            int livingPlayers = 0;

            foreach (var item in players)
            {
                if (item.Value.Item1)
                {
                    if (item.Value.Item1 == destructibleObject)
                    {
                        roomPlayer = item.Key;
                    }
                    else
                    {
                        livingPlayers++;
                    }
                }
            }

            players&#x5B;roomPlayer] = (null, null);

            if (livingPlayers == 0)
            {
                UnregisterObjectives();

                PlayerInput.Enabled = false;

                PopupsUIManager.Instance.ShowInfoPopup(
                &quot;Mission Failed!&quot;,
                &quot;Ok&quot;,
                () =&gt;
                {
                    PopupsUIManager.Instance.HideLastPopup();

                    PlayerInput.Enabled = true;

                    EndMission();
                });
            }
        }

        private void DestructibleObject_OnDestructibleDestroy(DestructibleObject destructibleObject)
        {
            if (destructibleObject != null)
            {
                if (objectsToDestroy.Contains(destructibleObject))
                {
                    objectsToDestroy.Remove(destructibleObject);
                }

                if (ObjectivesFulfilled)
                {
                    UnregisterObjectives();

                    PlayerInput.Enabled = false;

                    ClaimRewards();

                    if (HasStateAuthority)
                    {
                        PopupsUIManager.Instance.ShowRewardsPopup(
                        &quot;Mission accomplished successfully!&quot;,
                        &quot;Go to Lobby&quot;,
                        () =&gt;
                        {
                            PopupsUIManager.Instance.HideLastPopup();

                            PlayerInput.Enabled = true;

                            EndMission();
                        });
                    }
                    else
                    {
                        PopupsUIManager.Instance.ShowRewardsPopup(
                        &quot;Mission accomplished successfully! Please wait for host action or exit session.&quot;,
                        &quot;Exit Session&quot;,
                        () =&gt;
                        {
                            PopupsUIManager.Instance.HideLastPopup();

                            PlayerInput.Enabled = true;

                            EndMission();
                        });
                    }
                }
            }
        }

        #endregion Event callbacks

        private void RegisterObjectives()
        {
            for (int i = 0; i &lt; objectsToDestroy.Count; i++)
            {
                DestructibleObject destructibleObject = objectsToDestroy&#x5B;i];

                if (destructibleObject != null)
                {
                    destructibleObject.OnDestructibleDestroy += DestructibleObject_OnDestructibleDestroy;
                }
            }
        }

        private void UnregisterObjectives()
        {
            for (int i = 0; i &lt; objectsToDestroy.Count; i++)
            {
                DestructibleObject destructibleObject = objectsToDestroy&#x5B;i];

                if (destructibleObject != null)
                {
                    destructibleObject.OnDestructibleDestroy -= DestructibleObject_OnDestructibleDestroy;
                }
            }
        }

        private void ClaimRewards()
        {
            RoomPlayer.LocalPlayerData.coins += coins;
            RoomPlayer.LocalPlayerData.titanium += titanium;
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<h3 class="wp-block-heading">Destructible</h3>



<p class="wp-block-paragraph">The DestructibleObject class handles all networked objects that can take damage and be destroyed during missions. It defines network-synced properties like CurrentHP, MaxHP, Regeneration, and Armor to set each object’s stats. The class uses OnChangedRender to react immediately whenever a property changes. For example, you can update the UI when HP changes or despawn the object when its HP reaches zero.</p>



<p class="wp-block-paragraph">DestructibleObject acts as the base class for all destructible objects. To keep things simple in the tutorial demo, it also manages UI elements, such as health bars attached above player vehicles. The class automatically updates the UI every time the HP value changes.</p>



<p class="wp-block-paragraph"><strong>DestructibleObject.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using ProtoGDM.Networking;
using ProtoGDM.UI;
using System;
using UnityEngine;

namespace ProtoGDM
{
    public class DestructibleObject : NetworkBehaviour
    {
        #region Events

        public event Action&lt;float&gt; OnCurrentHPChange;

        public event Action&lt;float&gt; OnMaxHPChange;

        public event Action&lt;float&gt; OnCurrentRegenerationChange;

        public event Action&lt;float&gt; OnCurrentArmorChange;

        public event Action&lt;DestructibleObject&gt; OnDestructibleDestroy;

        #endregion Events

        #region Variables

        &#x5B;SerializeField]
        protected UnitWorldUI unitWorldUI;

        #endregion Variables

        #region Properties

        &#x5B;Networked, OnChangedRender(nameof(CurrentHPChanged))]
        public float CurrentHP
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(MaxHPChanged))]
        public float MaxHP
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(RegenerationChanged))]
        public float Regeneration
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(ArmorChanged))]
        public int Armor
        {
            get;
            set;
        }

        #endregion Properties

        #region Unity methods

        protected virtual void OnDestroy()
        {
            if (unitWorldUI)
            {
                unitWorldUI.UnregisterEvents(this);

                Destroy(unitWorldUI.gameObject);
            }
        }

        #endregion Unity methods

        #region Photon methods

        public override void FixedUpdateNetwork()
        {
            if (!HasStateAuthority)
            {
                return;
            }

            if (CurrentHP &gt; 0f &amp;&amp; Regeneration &gt; 0f)
            {
                HandleRegeneration(Runner.DeltaTime);
            }
        }

        public override void Spawned()
        {
            if (!HasStateAuthority)
            {
                Runner.SetIsSimulated(Object, false);
            }

            if (unitWorldUI)
            {
                string name = gameObject.name;

                RoomPlayer roomPlayer = RoomPlayer.GetPlayer(Object.InputAuthority);

                if (roomPlayer != null &amp;&amp; this is PlayerVehicleController)
                {
                    name = roomPlayer.Name.ToString();
                }

                unitWorldUI.RegisterEvents(this, name);
            }
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            if (CurrentHP == 0f)
            {
                OnDestructibleDestroy?.Invoke(this);
            }

            if (unitWorldUI)
            {
                unitWorldUI.UnregisterEvents(this);
            }
        }

        #endregion Photon methods

        #region Public methods

        public void SetCurrentHP(float hp)
        {
            if (hp &gt; MaxHP)
            {
                hp = MaxHP;
            }

            if (hp &lt; 0f)
            {
                hp = 0f;
            }

            if (hp != CurrentHP)
            {
                CurrentHP = hp;
            }
        }

        public void SetMaxHP(float hp)
        {
            if (hp != MaxHP)
            {
                MaxHP = hp;

                if (MaxHP &lt; CurrentHP)
                {
                    SetCurrentHP(MaxHP);
                }
            }
        }

        public void TakeDamage(float damage)
        {
            damage -= Armor;

            if (damage &lt; 0)
            {
                damage = 0;
            }

            SetCurrentHP(CurrentHP - damage);
        }

        #endregion Public methods

        #region Protected methods

        protected virtual void HandleRegeneration(float deltaTime)
        {
            if (CurrentHP &lt; MaxHP)
            {
                SetCurrentHP(CurrentHP + Regeneration * deltaTime);
            }
        }

        #endregion Protected methods

        #region Private methods

        private void CurrentHPChanged(NetworkBehaviourBuffer previous)
        {
            float prevValue = GetPropertyReader&lt;float&gt;(nameof(CurrentHP)).Read(previous);

            if (HasStateAuthority)
            {
                if (CurrentHP == 0f)
                {
                    NetworkManager.Instance.NetworkRunner.Despawn(Object);
                }
            }

            OnCurrentHPChange?.Invoke(CurrentHP);
        }

        private void MaxHPChanged()
        {
            OnMaxHPChange?.Invoke(MaxHP);
        }

        private void RegenerationChanged()
        {
            OnCurrentRegenerationChange?.Invoke(Regeneration);
        }

        private void ArmorChanged()
        {
            OnCurrentArmorChange?.Invoke(Armor);
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<h3 class="wp-block-heading">Player Vehicle</h3>



<p class="wp-block-paragraph">The player vehicle in this demo uses free assets from the Unity Asset Store (<a href="https://assetstore.unity.com/packages/3d/vehicles/land/tiny-low-poly-cars-180034">link</a>). It moves with real physics, relying on Rigidbody and Wheel Colliders. The VehicleController class provides the core driving logic for all vehicles. Each vehicle also features a mounted weapon that you control with the mouse. You’ll find more about the weapon controller later in this article.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="781" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-1024x781.png" alt="Demo game vehicle." class="wp-image-5511" style="width:544px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-1024x781.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-300x229.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-768x586.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-400x305.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-600x458.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-800x611.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14-1000x763.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-14.png 1233w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">From the networking side, the vehicle uses standard Photon Fusion 2 components. The main vehicle object, Sport Car 1, includes a NetworkObject, which enables networking. NetworkRigidbody3D synchronises all Rigidbody and physics data. The Player component handles player input. PlayerVehicleController extends VehicleController, adding the following camera and letting you control the car with your input.</p>



<p class="wp-block-paragraph">The prefab’s Wheels section splits into Meshes and Colliders. Meshes show the visual rotation and turning of each wheel. NetworkTransform components replicate their movement across the network.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="538" height="584" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-15.png" alt="Demo game vehicle structure." class="wp-image-5512" style="width:293px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-15.png 538w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-15-276x300.png 276w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-15-300x326.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-15-400x434.png 400w" sizes="auto, (max-width: 538px) 100vw, 538px" /></figure>



<p class="wp-block-paragraph">In this demo, only the Host calculates physics. The Host computes wheel movement and sends the result to the Clients. Clients do not run any local wheel physics; they just display the networked position and rotation for each wheel.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="691" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-1024x691.png" alt="Demo vehicle wheel colliders." class="wp-image-5513" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-1024x691.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-300x203.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-768x519.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-400x270.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-600x405.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-800x540.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-1000x675.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16-1280x864.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-16.png 1410w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>VehicleController.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using Fusion.Addons.Physics;
using UnityEngine;

namespace ProtoGDM
{
    public class VehicleController : DestructibleObject
    {
        #region Variables

        &#x5B;Header(&quot;Configuration&quot;)]

        &#x5B;SerializeField]
        private NetworkRigidbody3D networkRigidbody3D;

        &#x5B;SerializeField]
        private WheelCollider frontLeftWheelCollider;

        &#x5B;SerializeField]
        private WheelCollider frontRightWheelCollider;

        &#x5B;SerializeField]
        private WheelCollider rearLeftWheelCollider;

        &#x5B;SerializeField]
        private WheelCollider rearRightWheelCollider;

        &#x5B;SerializeField]
        private Transform frontLeftTransform;

        &#x5B;SerializeField]
        private Transform frontRightTransform;

        &#x5B;SerializeField]
        private Transform rearLeftTransform;

        &#x5B;SerializeField]
        private Transform rearRightTransform;

        &#x5B;Header(&quot;Settings&quot;)]

        &#x5B;SerializeField]
        private DriveTypes driveType = DriveTypes.FWD;

        &#x5B;SerializeField]
        private bool frontSteering = true;

        &#x5B;SerializeField]
        private bool rearSteering = false;

        &#x5B;SerializeField]
        private float rearSteeringFactor = 0.15f;

        &#x5B;SerializeField]
        protected float maxAccelerationForce = 500f;

        &#x5B;SerializeField]
        protected float maxBreakingForce = 500f;

        &#x5B;SerializeField]
        protected float maxSteerAngle = 20f;

        &#x5B;SerializeField]
        protected float minSteerAngle = 20f;

        &#x5B;SerializeField]
        protected Vector3 centerOfMass;

        #endregion Variables

        #region Properties

        &#x5B;SerializeField]
        private WeaponController weaponController;

        public WeaponController WeaponController
        {
            get
            {
                return weaponController;
            }

            set
            {
                weaponController = value;
            }
        }

        &#x5B;SerializeField]
        private Collider hitCollider;

        public Collider HitCollider
        {
            get
            {
                return hitCollider;
            }
        }

        #endregion Properties

        #region Unity methods

        protected virtual void Start()
        {
            Rigidbody rigidbody = GetComponent&lt;Rigidbody&gt;();

            if (rigidbody)
            {
                rigidbody.automaticCenterOfMass = false;

                rigidbody.centerOfMass = centerOfMass;
            }
        }

        #endregion Unity methods

        #region Public methods

        public override void FixedUpdateNetwork()
        {
            base.FixedUpdateNetwork();

            if (!HasStateAuthority)
            {
                return;
            }

            AnimateWheel(frontLeftWheelCollider, frontLeftTransform);
            AnimateWheel(frontRightWheelCollider, frontRightTransform);

            AnimateWheel(rearLeftWheelCollider, rearLeftTransform);
            AnimateWheel(rearRightWheelCollider, rearRightTransform);
        }

        public void AssignInputAuthority(PlayerRef playerRef)
        {
            Object.AssignInputAuthority(playerRef);

            if (WeaponController)
            {
                WeaponController.Object.AssignInputAuthority(playerRef);
            }
        }

        public virtual void HandleAcceleration(float value)
        {
            switch (driveType)
            {
                case DriveTypes.FWD:

                    frontLeftWheelCollider.motorTorque = value;
                    frontRightWheelCollider.motorTorque = value;

                    break;
                case DriveTypes.RWD:

                    rearLeftWheelCollider.motorTorque = value;
                    rearRightWheelCollider.motorTorque = value;

                    break;
                case DriveTypes.AWD:

                    frontLeftWheelCollider.motorTorque = value;
                    frontRightWheelCollider.motorTorque = value;
                    rearLeftWheelCollider.motorTorque = value;
                    rearRightWheelCollider.motorTorque = value;

                    break;
            }
        }

        public virtual void HandleBraking(float value)
        {
            frontLeftWheelCollider.brakeTorque = value;
            frontRightWheelCollider.brakeTorque = value;
            rearLeftWheelCollider.brakeTorque = value;
            rearRightWheelCollider.brakeTorque = value;
        }

        public virtual void HandleSteering(float value)
        {
            if (frontSteering)
            {
                frontLeftWheelCollider.steerAngle = value;
                frontRightWheelCollider.steerAngle = value;
            }

            if (rearSteering)
            {
                rearLeftWheelCollider.steerAngle = -value * rearSteeringFactor;
                rearRightWheelCollider.steerAngle = -value * rearSteeringFactor;
            }
        }

        #endregion Public methods

        #region Private methods

        private void AnimateWheel(WheelCollider wheelCollider, Transform transform)
        {
            Vector3 position;
            Quaternion quaternion;

            wheelCollider.GetWorldPose(out position, out quaternion);

            transform.position = position;
            transform.rotation = quaternion;
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>PlayerVehicleController.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using ProtoGDM.CameraManagement;
using ProtoGDM.UI;

namespace ProtoGDM
{
    public class PlayerVehicleController : VehicleController
    {
        public override void Spawned()
        {
            base.Spawned();

            CamerasManager.Instance.AddTarget(this);

            if (HasInputAuthority)
            {
                CamerasManager.Instance.SetFollow(this);

                GameplayUIManager.Instance.RegisterVehicleControllerEvents(this);
            }
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            base.Despawned(runner, hasState);

            if (CamerasManager.InstExists)
            {
                CamerasManager.Instance.RemoveTarget(this);
            }

            if (HasInputAuthority)
            {
                if (CamerasManager.InstExists)
                {
                    CamerasManager.Instance.SetFollow(null);

                    CamerasManager.Instance.SetCameraMode(CamerasManager.CameraModeTypes.Free);
                }

                if (GameplayUIManager.InstExists)
                {
                    GameplayUIManager.Instance.UnregisterVehicleControllersEvents(this);
                }
            }
        }

        public override void HandleAcceleration(float value)
        {
            float finalAcceleration = maxAccelerationForce * value;

            base.HandleAcceleration(finalAcceleration);
        }

        public override void HandleBraking(float value)
        {
            float finalBraking = maxBreakingForce * value;

            base.HandleBraking(finalBraking);
        }

        public override void HandleSteering(float value)
        {
            float finalSteering = maxSteerAngle * value;

            base.HandleSteering(finalSteering);
        }
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>Player.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using ProtoGDM.UI;
using UnityEngine;

namespace ProtoGDM.Networking
{
    public class Player : NetworkBehaviour
    {
        #region Variables

        &#x5B;SerializeField]
        private PlayerVehicleController playerVehicleController;

        &#x5B;Networked]
        public NetworkButtons NetworkButtonsPrevious
        {
            get;
            set;
        }

        #endregion Variables

        #region Photon methods

        public override void FixedUpdateNetwork()
        {
            if (!HasStateAuthority)
            {
                return;
            }

            if (playerVehicleController)
            {
                if (GetInput(out NetworkInputData input))
                {
                    // Acceleration
                    playerVehicleController.HandleAcceleration(input.acceleration);

                    // Braking
                    playerVehicleController.HandleBraking(input.braking);

                    // Steering
                    playerVehicleController.HandleSteering(input.steering);

                    // Weapon
                    var pressed = input.networkButtons.GetPressed(NetworkButtonsPrevious);

                    NetworkButtonsPrevious = input.networkButtons;

                    // Shooting
                    if (playerVehicleController.WeaponController)
                    {
                        playerVehicleController.WeaponController.HandleWeaponAimingWorldPoint(input.targetPoint);

                        playerVehicleController.WeaponController.HandleWeaponShooting(Runner, input.shooting);

                        if (pressed.IsSet(NetworkInputData.ButtonTypes.Reload))
                        {
                            playerVehicleController.WeaponController.Reload();
                        }
                    }
                }
            }
        }

        public override void Spawned()
        {
            if (HasInputAuthority)
            {
                GameplayUIManager.Instance.ShowGameplayInfo();
                GameplayUIManager.Instance.HideFreeCameraInfo();
            }
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            if (HasInputAuthority)
            {
                if (GameplayUIManager.InstExists)
                {
                    GameplayUIManager.Instance.HideGameplayInfo();
                    GameplayUIManager.Instance.ShowFreeCameraInfo();
                }
            }
        }

        #endregion Photon methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">Now let’s talk about the vehicle’s weapon. The weapon uses its own prefab and acts as an independent networked object, which you nest in the vehicle’s hierarchy. Correctly replicating these nested objects is essential for multiplayer games &#8211; review this process in the previous article <strong>if you need a refresher (<a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">link</a>)</strong>.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="544" height="592" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-17.png" alt="" class="wp-image-5517" style="width:265px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-17.png 544w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-17-276x300.png 276w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-17-300x326.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-17-400x435.png 400w" sizes="auto, (max-width: 544px) 100vw, 544px" /></figure>



<p class="wp-block-paragraph">The roof-mounted weapon uses the <strong>WeaponController</strong> component. This component acts as a networked object with a state. It controls all fired projectiles and maintains a collection of projectiles currently flying toward their targets. Each projectile has a lifetime, a flight speed, and its own visual prefab. Unlike vehicles and weapons, each projectile exists only locally &#8211; it is not a networked object. Only the local player sees its visuals. The weapon keeps a list of projectiles and simulates their state on the Host, which owns StateAuthority. The Host then replicates this simulation to all Clients. Both Host and Clients use the synchronised state to visualise the projectiles with the same prefabs.</p>



<p class="wp-block-paragraph">The base class <strong>NetworkBehaviourWithState&lt;T></strong> simplifies synchronisation. It stores all networked properties in a dedicated structure, NetworkState. Here, NetworkState holds a NetworkArray&lt;ShotState>, which describes all active projectiles. Each ShotState implements ISparseState, allowing local prediction (extrapolation) of projectile paths without sending every update over the network.</p>



<p class="wp-block-paragraph">This approach perfectly separates the visual and logical layers. Only the Host, which has StateAuthority, runs gameplay logic such as collision detection, damage calculation, and projectile destruction. The BulletProcessor method, running in FixedUpdateNetwork(), handles this logic on the Host. Clients do not run logic or have authority over projectiles. Instead, they visualise the state they receive by rendering projectiles with their local prefabs. The Projectile class, which implements ISparseVisual, handles this rendering.</p>



<p class="wp-block-paragraph">SparseCollection connects logic to visuals. It links ShotState data with Projectile prefabs and manages their lifecycle, updates, and synchronisation. This approach allows you to create and synchronise hundreds of projectiles efficiently, using <strong>very little</strong> network bandwidth. Even better, you can completely decouple visuals from logic. For example, you can run the game in headless mode without any visuals, or add advanced effects like muzzle flashes and hit particles &#8211; none of this affects the game logic or network synchronisation.</p>



<p class="wp-block-paragraph"><strong>WeaponController.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using FusionHelpers;
using ProtoGDM.Networking;
using System;
using UnityEngine;

namespace ProtoGDM
{
    public abstract class WeaponController : NetworkBehaviourWithState&lt;NetworkState&gt;
    {
        &#x5B;Networked]
        public override ref NetworkState State =&gt; ref MakeRef&lt;NetworkState&gt;();

        #region Events

        public event Action&lt;int&gt; OnCurrentAmmoChange;

        public event Action&lt;int&gt; OnMaxAmmoChange;

        public event Action&lt;float&gt; OnCooldownStart;

        public event Action OnCooldownComplete;

        public event Action&lt;float&gt; OnCooldownChange;

        public event Action&lt;float&gt; OnReloadStart;

        public event Action OnReloadComplete;

        public event Action&lt;float&gt; OnReloadChange;

        #endregion Events

        #region Variables

        &#x5B;Header(&quot;Weapon Configuration&quot;)]

        &#x5B;SerializeField]
        protected Transform baseTransform;

        &#x5B;SerializeField]
        protected Projectile projectilePrefab;

        &#x5B;SerializeField]
        protected float damageFactor = 1f;

        protected SparseCollection&lt;ShotState, Projectile&gt; bullets;

        protected float cooldownTime;

        protected float reloadTime;

        #endregion Variables

        #region Properties

        public bool IsReady
        {
            get
            {
                return !IsReloading &amp;&amp; !IsCooldown &amp;&amp; CurrentAmmo &gt; 0;
            }
        }

        &#x5B;Networked, OnChangedRender(nameof(IsReloadingChanged))]
        public bool IsReloading
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(ReloadProgressChanged))]
        public float ReloadProgress
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(IsCooldownChanged))]
        public bool IsCooldown
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(CooldownProgressChanged))]
        public float CooldownProgress
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(CurrentAmmoChanged))]
        public int CurrentAmmo
        {
            get;
            set;
        }

        &#x5B;Networked, OnChangedRender(nameof(MaxAmmoChanged))]
        public int MaxAmmo
        {
            get;
            set;
        }

        &#x5B;SerializeField]
        private float reloadInterval = 1f;

        public float ReloadInterval
        {
            get
            {
                return reloadInterval;
            }

            protected set
            {
                if (reloadInterval != value)
                {
                    reloadInterval = value;
                }
            }
        }

        &#x5B;SerializeField]
        private float fireInterval = 1f;

        public float FireInterval
        {
            get
            {
                return fireInterval;
            }

            protected set
            {
                if (fireInterval != value)
                {
                    fireInterval = value;
                }
            }
        }

        &#x5B;SerializeField]
        private bool autoReload = true;

        public bool AutoReload
        {
            get
            {
                return autoReload;
            }

            protected set
            {
                if (autoReload != value)
                {
                    autoReload = value;
                }
            }
        }

        #endregion Properties

        #region Abstract methods

        public abstract void HandleWeaponAimingWorldPoint(Vector3 target);

        public abstract void HandleWeaponShooting(NetworkRunner networkRunner, bool input);

        #endregion Abstract methods

        #region Public methods

        public override void Spawned()
        {
            CurrentAmmo = MaxAmmo;

            bullets = new SparseCollection&lt;ShotState, Projectile&gt;(State.bulletStates, projectilePrefab);
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            if (bullets != null)
            {
                bullets.Clear();
            }
        }

        public override void FixedUpdateNetwork()
        {
            if (!HasStateAuthority)
            {
                return;
            }

            if (bullets != null)
            {
                bullets.Process(this, BulletProcessor);
            }

            if (IsReloading)
            {
                reloadTime -= Runner.DeltaTime;

                if (reloadTime &lt; 0f)
                {
                    reloadTime = 0f;
                }

                ReloadProgress = 1f - reloadTime / ReloadInterval;

                if (ReloadProgress == 1f)
                {
                    CurrentAmmo = MaxAmmo;

                    IsReloading = false;
                }
            }

            if (IsCooldown)
            {
                cooldownTime -= Runner.DeltaTime;

                if (cooldownTime &lt; 0f)
                {
                    cooldownTime = 0f;
                }

                CooldownProgress = 1f - cooldownTime / FireInterval;

                if (CooldownProgress == 1f)
                {
                    IsCooldown = false;
                }
            }
        }

        public override void Render()
        {
            NetworkState from;
            NetworkState to;

            if (TryGetStateChanges(out from, out to))
            {
                int len = Mathf.Min(from.bulletStates.Length, to.bulletStates.Length);

                for (int i = 0; i &lt; len; i++)
                {
                    ShotState oldState = from.bulletStates&#x5B;i];

                    ShotState newState = to.bulletStates&#x5B;i];

                    if (oldState.StartTick != newState.StartTick)
                    {
                        // TODO: Shot sound effect &amp; visual effect.
                    }
                    else if (oldState.EndTick != newState.EndTick)
                    {
                        if (newState.Position.y &lt; -0.15f)
                        {
                            // TODO: Miss sound effect &amp; visual effect.
                        }
                        else
                        {
                            // TODO: Hit sound effect &amp; visual effect.
                        }
                    }
                }
            }
            else
            {
                Tick fromTick;
                Tick toTick;
                float alpha;

                TryGetStateSnapshots(out from, out fromTick, out to, out toTick, out alpha);
            }

            if (bullets != null)
            {
                bullets.Render(this, from.bulletStates);
            }
        }

        public void SetDamageFactor(float value)
        {
            damageFactor = value;
        }

        public void Cooldown(float progress = 0f)
        {
            cooldownTime = FireInterval;

            CooldownProgress = progress;

            IsCooldown = true;
        }

        public void Reload(float progress = 0f)
        {
            reloadTime = ReloadInterval;

            ReloadProgress = progress;

            IsReloading = true;
        }

        #endregion Public methods

        #region Private methods

        private void IsCooldownChanged()
        {
            if (IsCooldown)
            {
                OnCooldownStart?.Invoke(0f);
            }
            else
            {
                OnCooldownComplete?.Invoke();
            }
        }

        private void CooldownProgressChanged()
        {
            OnCooldownChange?.Invoke(CooldownProgress);
        }

        private void IsReloadingChanged()
        {
            if (IsReloading)
            {
                OnReloadStart?.Invoke(0f);
            }
            else
            {
                OnReloadComplete?.Invoke();
            }
        }

        private void ReloadProgressChanged()
        {
            OnReloadChange?.Invoke(ReloadProgress);
        }

        private void CurrentAmmoChanged()
        {
            OnCurrentAmmoChange?.Invoke(CurrentAmmo);
        }

        private void MaxAmmoChanged()
        {
            OnMaxAmmoChange?.Invoke(MaxAmmo);
        }

        /// &lt;summary&gt;
        /// Method processing single &lt;see cref=&quot;ShotState&quot;/&gt;.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;shotState&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;tick&quot;&gt;&lt;/param&gt;
        /// &lt;returns&gt;True if state was modified.&lt;/returns&gt;
        private bool BulletProcessor(ref ShotState shotState, int tick)
        {
            if (shotState.Position.y &lt; -0.15f)
            {
                shotState.EndTick = Runner.Tick;

                return true;
            }

            if (shotState.EndTick &gt; Runner.Tick)
            {
                Vector3 normalizedDirection = shotState.Direction.normalized;

                float step = projectilePrefab.Speed * Runner.DeltaTime;

                if (Physics.Raycast(
                        shotState.Position - normalizedDirection * step,
                        normalizedDirection,
                        out var hit,
                        step,
                        projectilePrefab.HitMask.value,
                        QueryTriggerInteraction.Ignore))
                {
                    shotState.Position = hit.point;
                    shotState.EndTick = Runner.Tick;

                    if (hit.collider)
                    {
                        DestructibleObject destructibleObject = hit.collider.GetComponentInParent&lt;DestructibleObject&gt;();

                        if (destructibleObject != null)
                        {
                            destructibleObject.TakeDamage(projectilePrefab.Damage * damageFactor);
                        }
                    }

                    return true;
                }
            }

            return false;
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>NetworkBehaviourWithState.cs</strong> (Photon Fusion 2)</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;

namespace FusionHelpers
{
    /// &lt;summary&gt;
    /// Baseclass for Network Behaviours that follow a pattern of keeping all networked properties in its own struct.
    /// This reduces the amount of boilerplate needed to manage snapshots and change detection, at the cost of some flexibility.
    /// For example, you can check for changes and get both previous and current states with a single call, but you won&#039;t know exactly which properties changed.
    /// You can also acquire previous and current snapshots without dealing with property readers.
    /// Networked data is accessed via the `State` property which can be a useful visual hint when reading (and writing) code.
    /// (Note that while you *can* add networked state outside of the provided struct,
    /// it is not recommended since it most likely leads to extra calls to change detectors and is just generally confusing)
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;T&quot;&gt;The struct that declares the networked state of this behaviour&lt;/typeparam&gt;

    public abstract class NetworkBehaviourWithState&lt;T&gt; : NetworkBehaviour where T : unmanaged, INetworkStruct
    {
        public abstract ref T State { get; }

        private ChangeDetector _changesSimulation;
        private ChangeDetector _changesFrom;
        private ChangeDetector _changesTo;

        protected bool TryGetStateChanges(out T previous, out T current, ChangeDetector.Source source = ChangeDetector.Source.SimulationState)
        {
            switch (source)
            {
                default:
                case ChangeDetector.Source.SimulationState:
                    return TryGetStateChanges(source, ref _changesSimulation, out previous, out current);
                case ChangeDetector.Source.SnapshotFrom:
                    return TryGetStateChanges(source, ref _changesFrom, out previous, out current);
                case ChangeDetector.Source.SnapshotTo:
                    return TryGetStateChanges(source, ref _changesTo, out previous, out current);
            }
        }

        private bool TryGetStateChanges(ChangeDetector.Source source, ref ChangeDetector changes, out T previous, out T current)
        {
            if (changes == null)
                changes = GetChangeDetector(source);

            if (changes != null)
            {
                foreach (var change in changes.DetectChanges(this, out var previousBuffer, out var currentBuffer))
                {
                    switch (change)
                    {
                        case nameof(State):
                            var reader = GetPropertyReader&lt;T&gt;(change);
                            current = currentBuffer.Read(reader);
                            previous = previousBuffer.Read(reader);
                            return true;
                    }
                }
            }
            current = default;
            previous = default;
            return false;
        }

        protected bool TryGetStateSnapshots(out T from, out Tick fromTick, out T to, out Tick toTick, out float alpha)
        {
            if (TryGetSnapshotsBuffers(out var fromBuffer, out var toBuffer, out alpha))
            {
                var reader = GetPropertyReader&lt;T&gt;(nameof(State));
                from = fromBuffer.Read(reader);
                to = toBuffer.Read(reader);
                fromTick = fromBuffer.Tick;
                toTick = toBuffer.Tick;
                return true;
            }

            from = default;
            to = default;
            fromTick = default;
            toTick = default;
            return false;
        }
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>SparseCollection.cs</strong> (Photon Fusion 2)</p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using UnityEngine;
using Object = UnityEngine.Object;

namespace FusionHelpers
{
    /// &lt;summary&gt;
    /// ISparseState represents the networked part of a simple object that require infrequent (network) updates,
    /// but still needs a visual game object rendered every frame on all clients.
    /// For example, a bullet following a straight line can be rendered just knowing its start location and velocity.
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;P&quot;&gt;The MonoBehaviour that represents this state visually&lt;/typeparam&gt;
    public interface ISparseState&lt;P&gt; : INetworkStruct where P : MonoBehaviour
    {
        public int StartTick { get; set; }
        public int EndTick { get; set; }

        /// &lt;summary&gt;
        /// The extrapolate method should update properties on the state struct using the given local time t
        /// (local time is the offset in seconds from StartTick).
        /// Note that these changes are just local predictions/extrapolations - they will not automatically be networked.
        /// (See the SparseCollection.Process method if you need to update state on State Authority)
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;t&quot;&gt;Local time in seconds to extrapolate to (t is 0 at Runner.Tick==StartTick)&lt;/param&gt;
        /// &lt;param name=&quot;prefab&quot;&gt;Prefab used to create the visual (handy for accessing visual object configuration -
        /// Keep in mind that this is the prefab, it is *NOT* (guaranteed to be) the actual game object used to visualize this state)&lt;/param&gt;
        public void Extrapolate(float t, P prefab);
    }

    /// &lt;summary&gt;
    /// ISparseVisual is implemented by local game objects that &quot;visualize&quot; an ISparseState.
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;T&quot;&gt;The actual type of the sparse state&lt;/typeparam&gt;
    /// &lt;typeparam name=&quot;P&quot;&gt;The MonoBehaviour used to visualise the sparse state&lt;/typeparam&gt;
    public interface ISparseVisual&lt;T, P&gt; where T : unmanaged, ISparseState&lt;P&gt; where P : MonoBehaviour, ISparseVisual&lt;T, P&gt;
    {
        /// &lt;summary&gt;
        /// This method is called every frame to update the visual game object to match the current render state.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;owner&quot;&gt;The NB that owns the collection of sparse states&lt;/param&gt;
        /// &lt;param name=&quot;state&quot;&gt;The current render state of this particular visual&lt;/param&gt;
        /// &lt;param name=&quot;t&quot;&gt;The current local time that the state represents&lt;/param&gt;
        /// &lt;param name=&quot;isFirstRender&quot;&gt;True the first time this is called for a new visual&lt;/param&gt;
        /// &lt;param name=&quot;isLastRender&quot;&gt;True the last time this is called for a given visual&lt;/param&gt;
        public void ApplyStateToVisual(NetworkBehaviour owner, T state, float t, bool isFirstRender, bool isLastRender);
    }

    /// &lt;summary&gt;
    /// The sparse collection maps sparse states to sparse visuals and keeps track of the somewhat complex timing involved
    /// &lt;/summary&gt;
    /// &lt;typeparam name=&quot;T&quot;&gt;The actual type of the sparse state&lt;/typeparam&gt;
    /// &lt;typeparam name=&quot;P&quot;&gt;The MonoBehaviour used to visualise the sparse state&lt;/typeparam&gt;
    public class SparseCollection&lt;T, P&gt; where T : unmanaged, ISparseState&lt;P&gt; where P : MonoBehaviour, ISparseVisual&lt;T, P&gt;
    {
        // Internal struct for keeping track of matching states and visuals
        private struct Entry
        {
            public P visual;
            public bool enabled;
        }

        // To give proxies a chance to disable the local GO before it&#039;s re-used, we wait a few additional ticks before re-using a state.
        private const int REUSE_DELAY_TICKS = 24;

        // Reference to the raw networked state data
        private NetworkArray&lt;T&gt; _states;

        // State to visual map
        private Entry&#x5B;] _entries;

        // Prefab used for visuals
        private readonly P _prefab;

        // Last time render was called - used to make sure we don&#039;t miss updates if framerate is low
        private float _nextRenderTime;

        /// &lt;summary&gt;
        /// The sparse collection itself is not a networked object, so you need to create its backing data elsewhere (in a NB)
        /// and pass it to the constructor along with a reference to the prefab to use for the associated visuals.
        /// Once created, call
        /// * Render() from the NetworkBehaviour&#039;s Render() method, passing in the relevant snapshot
        /// * Process() from the NetworkBehaviour&#039;s FixedUpdateNetwork() method if you want to alter state, and
        /// * Add() from Input or State auth to &quot;spawn&quot; a new object.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;states&quot;&gt;Networked array of sparse state structs&lt;/param&gt;
        /// &lt;param name=&quot;prefab&quot;&gt;Prefab to use for visuals&lt;/param&gt;
        public SparseCollection(NetworkArray&lt;T&gt; states, P prefab)
        {
            _entries = new Entry&#x5B;states.Length];
            _states = states;
            _prefab = prefab;
        }

        /// &lt;summary&gt;
        /// Call Render() every frame to update visuals to their associated sparse state.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;owner&quot;&gt;The &lt;see cref=&quot;NetworkBehaviour&quot;/&gt; that contains the networked state objects&lt;/param&gt;
        public void Render(NetworkBehaviour owner, NetworkArray&lt;T&gt; states)
        {
            NetworkRunner runner = owner.Runner;

            float renderTime = owner.Object.RenderTime;

            for (int i = 0; i &lt; _entries.Length; i++)
            {
                Entry entry = _entries&#x5B;i];

                T state = states&#x5B;i];

                if (!entry.enabled &amp;&amp; entry.visual &amp;&amp; entry.visual.gameObject.activeSelf)
                {
                    entry.visual.gameObject.SetActive(false);
                }

                // Note: t may be less than zero if we&#039;re rendering across several ticks and StartTick is somewhere in-between.
                // (E.g. from=100, to=102 with start=101 and alpha=0.25 will place us ahead of the start tick)
                float t = renderTime - state.StartTick * runner.DeltaTime;
                float t1 = (state.EndTick - state.StartTick) * runner.DeltaTime;

                bool isLastRender = t &gt;= t1 &amp;&amp; entry.enabled;
                bool isFirstRender = false;

                // We delay disabling of the object one frame since &quot;last render&quot; isn&#039;t really a last render if the object is immediately hidden.
                entry.enabled = t &gt;= 0 &amp;&amp; t &lt; t1;

                // Make sure we have a valid enabled GameObject if this state represents an active instance
                if (entry.enabled || isLastRender)
                {
                    if (!entry.visual)
                    {
                        entry.visual = Object.Instantiate(_prefab);
                        isFirstRender = true;
                    }

                    if (!entry.visual.gameObject.activeSelf)
                    {
                        entry.visual.gameObject.SetActive(true);
                        isFirstRender = true;
                    }

                    if (isFirstRender)
                    {
                        ApplyState(state, entry, 0, true, false);
                    }

                    if (!isFirstRender &amp;&amp; !isLastRender)
                    {
                        ApplyState(state, entry, t, false, false);
                    }

                    if (isLastRender)
                    {
                        ApplyState(state, entry, t1, false, true);
                    }
                }
                // Done modifying the entry struct, copy it back to the array
                _entries&#x5B;i] = entry;
            }

            void ApplyState(T state, Entry e, float t, bool isFirstRender, bool isLastRender)
            {
                // Update state to t
                state.Extrapolate(t, _prefab);

                // Update visual to match the state
                e.visual.ApplyStateToVisual(owner, state, t, isFirstRender, isLastRender);
            }
        }

        public delegate bool Processor(ref T state, int tick);

        /// &lt;summary&gt;
        /// Call process every tick if you want to adjust *networked* properties on the sparse state.
        /// As the name suggests, these updates should be infrequent, so if you *do* change the state
        /// you must return true from the delegate to update the backing array.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;owner&quot;&gt;The &lt;see cref=&quot;NetworkBehaviour&quot;/&gt; that owns the sparse state list&lt;/param&gt;
        /// &lt;param name=&quot;processor&quot;&gt;A delegate that will process each (active) sparse state&lt;/param&gt;
        public void Process(NetworkBehaviour owner, Processor processor)
        {
            if (owner.IsProxy)
                return;

            NetworkRunner runner = owner.Runner;

            int simTtick = runner.Tick;

            for (int i = 0; i &lt; _states.Length; i++)
            {
                T state = _states&#x5B;i];

                float t = (simTtick - state.StartTick) * runner.DeltaTime;

                if (simTtick &lt;= state.EndTick)
                {
                    state.Extrapolate(t, _prefab);

                    if (processor(ref state, simTtick))
                    {
                        // Since we&#039;re storing the extrapolated state, we must also update the start tick, as this is our new starting point going forward.
                        state.StartTick = simTtick;
                        // Update the networked backing storage so the change is propagated
                        _states&#x5B;i] = state;
                    }
                }
            }
        }

        /// &lt;summary&gt;
        /// Call Add to &quot;instantiate&quot; (or rather, &quot;activate&quot;) a new sparse state. Note that this will not allocate
        /// but simply select the next in-active sparse state in the array. It will do nothing if none is found.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;runner&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;state&quot;&gt;The initial state to add&lt;/param&gt;
        /// &lt;param name=&quot;secondsToLive&quot;&gt;Initial number of ticks for the sparse state to be alive&lt;/param&gt;
        public void Add(NetworkRunner runner, T state, float secondsToLive)
        {
            state.StartTick = runner.Tick;
            state.EndTick = state.StartTick + Mathf.Max(1, (int)(secondsToLive / runner.DeltaTime));

            for (int i = 0; i &lt; _states.Length; i++)
            {
                if (runner.Tick &gt; _states&#x5B;i].EndTick + REUSE_DELAY_TICKS)
                {
                    _states&#x5B;i] = state;

                    return;
                }
            }

            Debug.LogWarning(&quot;No free slots in state array!&quot;);
        }

        /// &lt;summary&gt;
        /// Call Clear to destroy all visuals for this sparse set.
        /// &lt;/summary&gt;
        public void Clear()
        {
            for (int i = 0; i &lt; _entries.Length; i++)
            {
                Entry entry = _entries&#x5B;i];

                if (entry.visual)
                {
                    Object.Destroy(entry.visual.gameObject);
                }

                _entries&#x5B;i] = default;
            }
        }
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>NetworkState.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;

namespace ProtoGDM.Networking
{
    public struct NetworkState : INetworkStruct
    {
        #region Properties

        &#x5B;Networked, Capacity(64)]
        public NetworkArray&lt;ShotState&gt; bulletStates
        {
            get
            {
                return default;
            }
        }

        #endregion Properties
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>ShotState.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using FusionHelpers;
using UnityEngine;

namespace ProtoGDM.Networking
{
    public struct ShotState : ISparseState&lt;Projectile&gt;
    {
        #region Properties

        public Vector3 Position
        {
            get;
            set;
        }

        public Vector3 Direction
        {
            get;
            set;
        }

        public int StartTick
        {
            get;
            set;
        }

        public int EndTick
        {
            get;
            set;
        }

        #endregion Properties

        #region Constructors

        public ShotState(Vector3 startPosition, Vector3 direction)
        {
            StartTick = 0;
            EndTick = 0;
            Position = startPosition;
            Direction = direction;
        }

        #endregion Constructors

        #region Public methods

        public void Extrapolate(float time, Projectile projectile)
        {
            Position = GetPositionAt(time, projectile);
            Direction = GetDirectionAt(time, projectile);
        }

        public Vector3 GetTargetPosition(Projectile projectile)
        {
            float a = 0.5f * projectile.Gravity.y;
            float b = projectile.Speed * Direction.y;
            float c = Position.y;
            float d = b * b - 4 * a * c;
            float t = (-b - Mathf.Sqrt(d)) / (2 * a);
            Vector3 p = GetPositionAt(t, projectile);
            p.y = 0.05f; // Return the position with a slight y offset to avoid placing target where it will end up z-fighting with the ground;

            return p;
        }

        #endregion Public methods

        #region Private methods

        private Vector3 GetPositionAt(float time, Projectile projectile)
        {
            return Position + time * (projectile.Speed * Direction + 0.5f * time * projectile.Gravity);
        }

        private Vector3 GetDirectionAt(float time, Projectile projectile)
        {
            return projectile.Speed == 0 ? Direction : (projectile.Speed * Direction + time * projectile.Gravity).normalized;
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph"><strong>Projectile.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using FusionHelpers;
using ProtoGDM.Networking;
using UnityEngine;

namespace ProtoGDM
{
    public class Projectile : MonoBehaviour, ISparseVisual&lt;ShotState, Projectile&gt;
    {
        private Transform locTransform;

        #region Properties

        &#x5B;SerializeField]
        private Vector3 gravity;

        public Vector3 Gravity
        {
            get
            {
                return gravity;
            }
        }

        &#x5B;SerializeField]
        private float timeOfLife = 2f;

        public float TimeOfLife
        {
            get
            {
                return timeOfLife;
            }
        }

        &#x5B;SerializeField]
        private LayerMask hitMask;

        public LayerMask HitMask
        {
            get
            {
                return hitMask;
            }
        }

        &#x5B;SerializeField]
        private float speed = 50f;

        public float Speed
        {
            get
            {
                return speed;
            }
        }

        &#x5B;SerializeField]
        private float damage = 10f;

        public float Damage
        {
            get
            {
                return damage;
            }
        }

        #endregion Properties

        private void Awake()
        {
            locTransform = transform;
        }

        #region Public methods

        public void ApplyStateToVisual(NetworkBehaviour owner, ShotState shotState, float time, bool isFirstRender, bool isLastRender)
        {
            locTransform.forward = shotState.Direction;
            locTransform.position = shotState.Position;
        }

        #endregion Public methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<h3 class="wp-block-heading">Turret</h3>



<p class="wp-block-paragraph">Turrets are another key part of this demo. Each turret is a stationary object with its own health and specific shooting parameters. A trigger collider defines the turret’s detection area. When a player vehicle enters this area, the turret rotates to face it and begins firing. The turret checks if an obstacle blocks its target. If it can’t get a clear shot, it looks for another player&#8217;s vehicle.</p>



<p class="wp-block-paragraph">All logic for aiming, targeting, and controlling the turret runs on the Host &#8211; the side with State Authority. The turret’s rotation and barrel angles are synchronised with all Clients. When no player vehicles are in range, the turret enters search mode and spins on its axis to find a new target.</p>



<p class="wp-block-paragraph">Projectile shooting, synchronisation, and visualisation work just like they do for the player vehicle. The system synchronises only the projectile states. Both the Host and Clients handle projectile visualisation locally whenever the state changes.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="929" height="696" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18.png" alt="Demo game turret object." class="wp-image-5519" style="width:412px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18.png 929w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18-300x225.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18-768x575.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18-400x300.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18-600x450.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-18-800x599.png 800w" sizes="auto, (max-width: 929px) 100vw, 929px" /></figure>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="434" height="1024" src="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-434x1024.png" alt="Turret prefab inspector view. Photon Fusion 2 components. " class="wp-image-5520" style="width:298px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-434x1024.png 434w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-127x300.png 127w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-300x707.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-400x943.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19-600x1414.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/obraz-19.png 635w" sizes="auto, (max-width: 434px) 100vw, 434px" /></figure>



<p class="wp-block-paragraph"><strong>TurretController.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using Fusion;
using FusionHelpers;
using ProtoGDM.Networking;
using System.Collections.Generic;
using UnityEngine;

namespace ProtoGDM
{
    public class TurretController : NetworkBehaviourWithState&lt;NetworkState&gt;
    {
        &#x5B;Networked]
        public override ref NetworkState State =&gt; ref MakeRef&lt;NetworkState&gt;();

        #region Variables

        &#x5B;Header(&quot;Configuration&quot;)]

        &#x5B;SerializeField]
        protected Transform baseTransform;

        &#x5B;SerializeField]
        protected Transform barrelTransform;

        &#x5B;SerializeField]
        private Transform raycastPoint;

        &#x5B;SerializeField]
        private Transform projectileSpawnPoint;

        &#x5B;Header(&quot;Settings&quot;)]

        &#x5B;SerializeField]
        private Projectile projectilePrefab;

        &#x5B;SerializeField]
        private float agroRange = 10f;

        &#x5B;SerializeField]
        private LayerMask layerMask;

        &#x5B;SerializeField]
        protected float rotationSpeed = 5f;

        &#x5B;SerializeField]
        protected float minPitchAngle = 0f;

        &#x5B;SerializeField]
        protected float maxPitchAngle = 20f;

        &#x5B;SerializeField]
        private SphereCollider detectionCollider;

        &#x5B;SerializeField]
        private float searchRotationSpeed = 10f;

        &#x5B;SerializeField]
        private float searchPitchSpeed = 1f;

        &#x5B;SerializeField]
        private float fireInterval = 2f;

        &#x5B;SerializeField]
        private List&lt;PlayerVehicleController&gt; targetsInRange = new List&lt;PlayerVehicleController&gt;();

        private float searchTime = 0f;

        protected SparseCollection&lt;ShotState, Projectile&gt; bullets;

        #endregion Variables

        #region Properties

        &#x5B;Networked]
        public TurretStateTypes CurrentState
        {
            get;
            set;
        }

        &#x5B;Networked]
        public float Cooldown
        {
            get;
            set;
        }

        #endregion Properties

        #region Unity methods

        public override void Spawned()
        {
            bullets = new SparseCollection&lt;ShotState, Projectile&gt;(State.bulletStates, projectilePrefab);

            if (detectionCollider != null)
            {
                detectionCollider.isTrigger = true;

                detectionCollider.radius = agroRange;
            }
            else
            {
                Debug.LogWarning(&quot;Detection Collider is not set on &quot; + gameObject.name);
            }
        }

        public override void Despawned(NetworkRunner runner, bool hasState)
        {
            bullets.Clear();

            base.Despawned(runner, hasState);
        }

        public override void FixedUpdateNetwork()
        {
            if (!HasStateAuthority)
            {
                return;
            }

            bullets.Process(this, BulletProcessor);

            float deltaTime = Runner.DeltaTime;

            if (Cooldown &gt; 0f)
            {
                Cooldown -= deltaTime;

                if (Cooldown &lt; 0f)
                {
                    Cooldown = 0f;
                }
            }

            PlayerVehicleController currentTarget = null;

            for (int i = 0; i &lt; targetsInRange.Count; i++)
            {
                if (targetsInRange&#x5B;i])
                {
                    if (IsTargetVisible(targetsInRange&#x5B;i]))
                    {
                        currentTarget = targetsInRange&#x5B;i];

                        break;
                    }
                }
            }

            if (currentTarget)
            {
                CurrentState = TurretStateTypes.Tracking;

                AimAtTarget(currentTarget.transform.position);

                if (Cooldown == 0f)
                {
                    Shoot(Runner);

                    Cooldown = fireInterval;
                }
            }
            else
            {
                CurrentState = TurretStateTypes.Searching;

                SearchForTarget(deltaTime);
            }
        }

        public override void Render()
        {
            if (TryGetStateChanges(out var from, out var to))
            {

            }
            else
            {
                TryGetStateSnapshots(out from, out _, out _, out _, out _);
            }

            bullets.Render(this, from.bulletStates);
        }

        private void OnTriggerEnter(Collider other)
        {
            PlayerVehicleController vehicle = other.GetComponentInParent&lt;PlayerVehicleController&gt;();

            if (vehicle != null &amp;&amp; !targetsInRange.Contains(vehicle))
            {
                targetsInRange.Add(vehicle);
            }
        }

        private void OnTriggerExit(Collider other)
        {
            PlayerVehicleController vehicle = other.GetComponentInParent&lt;PlayerVehicleController&gt;();

            if (vehicle != null)
            {
                targetsInRange.Remove(vehicle);
            }
        }

        private void OnDrawGizmosSelected()
        {
            if (targetsInRange == null)
            {
                return;
            }

            foreach (PlayerVehicleController vehicleController in targetsInRange)
            {
                if (vehicleController == null)
                {
                    continue;
                }

                Vector3 direction = (vehicleController.transform.position + Vector3.up) - raycastPoint.position;

                Ray ray = new Ray(raycastPoint.position, direction);

                RaycastHit raycastHit;

                if (Physics.Raycast(ray, out raycastHit, agroRange, layerMask, QueryTriggerInteraction.Ignore))
                {
                    if (vehicleController.HitCollider == raycastHit.collider)
                    {
                        Gizmos.color = Color.green;
                    }
                    else
                    {
                        Gizmos.color = Color.red;
                    }

                    Gizmos.DrawLine(raycastPoint.position, raycastHit.point);
                }
                else
                {
                    Gizmos.color = Color.red;
                    Gizmos.DrawLine(raycastPoint.position, raycastPoint.position + direction.normalized * agroRange);
                }
            }
        }

        #endregion Unity methods

        #region Private methods

        private bool IsTargetVisible(PlayerVehicleController vehicleController)
        {
            Vector3 direction = (vehicleController.transform.position + Vector3.up) - raycastPoint.position;

            Ray ray = new Ray(raycastPoint.position, direction);

            RaycastHit raycastHit;

            if (Physics.Raycast(ray, out raycastHit, agroRange, layerMask, QueryTriggerInteraction.Ignore))
            {
                if (vehicleController.HitCollider == raycastHit.collider)
                {
                    return true;
                }
            }

            return false;
        }

        private void SearchForTarget(float deltaTime)
        {
            baseTransform.Rotate(Vector3.up, searchRotationSpeed * deltaTime);

            searchTime += deltaTime * searchPitchSpeed;

            float pitchAngle = Mathf.Lerp(minPitchAngle, maxPitchAngle, (Mathf.Sin(searchTime) + 1f) * 0.5f);

            barrelTransform.localRotation = Quaternion.Euler(-pitchAngle, 0, 0);
        }

        private void AimAtTarget(Vector3 targetPosition)
        {
            Vector3 directionToTarget = targetPosition - baseTransform.position;

            Vector3 flatDirection = new Vector3(directionToTarget.x, 0, directionToTarget.z);

            if (flatDirection.sqrMagnitude &gt; 0.001f)
            {
                Quaternion baseRotation = Quaternion.LookRotation(flatDirection);

                baseTransform.rotation = Quaternion.Euler(0, baseRotation.eulerAngles.y, 0);
            }

            Vector3 localTargetPosition = baseTransform.InverseTransformPoint(targetPosition);

            float distance = Mathf.Sqrt(localTargetPosition.z * localTargetPosition.z + localTargetPosition.x * localTargetPosition.x);

            float pitchAngle = Mathf.Atan2(localTargetPosition.y, distance) * Mathf.Rad2Deg;

            pitchAngle = Mathf.Clamp(pitchAngle, minPitchAngle, maxPitchAngle);

            barrelTransform.localRotation = Quaternion.Euler(-pitchAngle, 0, 0);
        }

        private void Shoot(NetworkRunner networkRunner)
        {
            if (Object.HasStateAuthority)
            {
                bullets.Add(networkRunner, new ShotState(projectileSpawnPoint.position, projectileSpawnPoint.forward), projectilePrefab.TimeOfLife);
            }
        }

        /// &lt;summary&gt;
        /// Method processing single &lt;see cref=&quot;ShotState&quot;/&gt;.
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;shotState&quot;&gt;&lt;/param&gt;
        /// &lt;param name=&quot;tick&quot;&gt;&lt;/param&gt;
        /// &lt;returns&gt;True if state was modified.&lt;/returns&gt;
        private bool BulletProcessor(ref ShotState shotState, int tick)
        {
            if (shotState.Position.y &lt; -0.15f)
            {
                shotState.EndTick = Runner.Tick;

                return true;
            }

            if (shotState.EndTick &gt; Runner.Tick)
            {
                Vector3 normalizedDirection = shotState.Direction.normalized;

                float step = projectilePrefab.Speed * Runner.DeltaTime;

                if (Physics.Raycast(
                        shotState.Position - normalizedDirection * step,
                        normalizedDirection,
                        out var hit,
                        step,
                        projectilePrefab.HitMask.value,
                        QueryTriggerInteraction.Ignore))
                {
                    shotState.Position = hit.point;
                    shotState.EndTick = Runner.Tick;

                    if (hit.collider)
                    {
                        DestructibleObject destructibleObject = hit.collider.GetComponentInParent&lt;DestructibleObject&gt;();

                        if (destructibleObject != null)
                        {
                            destructibleObject.TakeDamage(projectilePrefab.Damage * MissionSettings.Instance.GetDifficultyFactor());
                        }
                    }

                    return true;
                }
            }

            return false;
        }

        #endregion Private methods
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<h2 class="wp-block-heading">Reconnect</h2>



<p class="wp-block-paragraph">Every real-time multiplayer game is vulnerable to connection issues. At any moment, a Client can lose the connection to the Server or Host.</p>



<p class="wp-block-paragraph">To ensure a good experience, your game should include <strong>reconnection mechanisms</strong>. These systems should automatically try to restore the connection if the problem is temporary. It’s also useful to let players return to a session after events like a PC restart, sudden crash, or power outage.</p>



<p class="wp-block-paragraph">In our demo, we focus on the Host-Client topology. One player acts as the Host and knows the current state of the game. Only the Host can help a reconnecting Client recover the latest game state.</p>



<p class="wp-block-paragraph"><strong>What is reconnect?</strong><br>Reconnect means the player restores a stable connection after losing it. The game also has to rebuild the player’s knowledge of the current world state. While the player was offline, other participants kept playing and changed the world. Only the Host can decide if the player can rejoin and is responsible for sending the most up-to-date state.</p>



<p class="wp-block-paragraph">If your game does not support reconnect, the Host assumes every disconnect is intentional. The game despawns the player’s objects and forgets their state. Supporting reconnect makes things more complex. The Host must decide whether the disconnect was intentional or not. They also need to save the player’s data for a while and identify if a reconnecting player is really the same person.</p>



<p class="wp-block-paragraph">As described in the <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">previous article</a>, joining a Fusion 2 session is simple if you know the session name. The Host receives a callback and chooses to accept or reject the request. Fusion 2 does not have a built-in reconnect system. For the Host, a reconnecting player looks like a new player. The Host must check if the player is new or someone who lost connection earlier. To do this, use the ConnectionToken from StartGameArgs. See the NetworkManager class and JoinSession method in this article. The ConnectionToken allows the Host to recognize returning players. Here’s how the flow works:</p>



<ol class="wp-block-list">
<li>The Host creates a session.</li>



<li>Player A joins and sends a unique ConnectionToken.</li>



<li>The Host accepts Player A and saves their ConnectionToken.</li>



<li>Player B joins and sends a unique ConnectionToken.</li>



<li>The Host accepts Player B and saves their ConnectionToken.</li>



<li>The Host starts the game. All players are in the session. Suddenly, Player A’s computer freezes. After restarting, they want to return.</li>



<li>Player A remembers the session name and their ConnectionToken. They request to rejoin using NetworkManager.JoinSession.</li>



<li>The Host receives OnConnectRequest and checks if the ConnectionToken matches a known player.</li>



<li>If it matches, the Host knows it’s Player A.</li>



<li>The Host accepts the player. Since this is not a new player, the Host does not spawn new objects. Instead, the Host restores Input Authority to Player A’s previous objects. This lets Player A regain control and synchronizes the current state.</li>
</ol>



<p class="wp-block-paragraph">This is the basic reconnect workflow. Here are a few <strong>common problems</strong> you may face.</p>



<p class="wp-block-paragraph"><strong>Session slot reservation</strong><br>Fusion 2 does not reserve session slots for reconnecting players. If a player disconnects unexpectedly, the Fusion backend eventually removes them from the session. Meanwhile, another player might see a free slot and try to join. Only your Host can check if the slot is really free, or if you’re waiting for a reconnect. There’s also another problem. The Fusion backend may be slower to detect disconnects than the Host. The Host checks client status more often. As a result, a reconnecting player might see a “session full” error even if the Host already knows they left. The backend still thinks the slot is occupied.</p>



<p class="wp-block-paragraph"><strong>Password-protected sessions</strong><br>You might use ConnectionToken to send a session password, but now you also need to use it to identify the player. Fusion 2 does not offer separate fields for passwords and reconnects, so you have to combine them. In this example, create a structure with both the player’s ID and password, serialize it to JSON, convert it to byte[], and use that as the ConnectionToken.</p>



<p class="wp-block-paragraph"><strong>ConnectionToken.cs</strong></p>



<details class="wp-block-details is-layout-flow wp-block-details-is-layout-flow"><summary>Expand/Collapse Code</summary><div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
namespace ProtoGDM.Networking
{
    &#x5B;Serializable]
    public class ConnectionToken
    {
        public string password;
        public string reconnectGuid;

        public ConnectionToken(string password, string reconnectGuid)
        {
            this.password = password;
            this.reconnectGuid = reconnectGuid;
        }

        public static byte&#x5B;] ToBytes(ConnectionToken token)
        {
            string json = JsonUtility.ToJson(token);

            return Encoding.UTF8.GetBytes(json);
        }

        public static ConnectionToken FromBytes(byte&#x5B;] data)
        {
            if (data == null || data.Length == 0)
            {
                return null;
            }

            string json = Encoding.UTF8.GetString(data);

            return JsonUtility.FromJson&lt;ConnectionToken&gt;(json);
        }
    }
}
</pre></div>


<p class="wp-block-paragraph"></p>
</details>



<p class="wp-block-paragraph">The Host should not wait indefinitely for a disconnected player. Most games set a <strong>reconnect window</strong> of several seconds or a few minutes. After this time, the Host considers the player gone, removes their state, and deletes their ConnectionToken. If the player tries to reconnect after this period, Host treats them as a new participant and does not restore their previous state.</p>



<p class="wp-block-paragraph">In our demo, you’ll find the roomPlayerMap dictionary in the NetworkManager class. This dictionary links each RoomPlayer object to the player’s unique identifier (string). When a player joins the game, the Host checks this dictionary in both OnConnectRequest and OnPlayerJoined.</p>



<ul class="wp-block-list">
<li>In <strong>OnConnectRequest</strong>, the Host verifies whether the player’s identifier is still mapped to a RoomPlayer. If not, the reconnect window has expired and the connection cannot be restored.</li>



<li>In <strong>OnPlayerJoined</strong>, the Host tries to fetch the RoomPlayer object. If successful, the Host assigns InputAuthority and returns control to the player without creating new objects.</li>
</ul>



<p class="wp-block-paragraph">The RoomPlayer class also contains a networked property called IsDisconnected. This property lets other players know when someone has connection issues. For example, you can display a &#8220;Disconnected&#8221; message above the player’s vehicle.</p>



<h2 class="wp-block-heading">Summary</h2>



<p class="wp-block-paragraph">This article wrapped up our Photon Fusion 2 series with a complete but very simple project. You learned how to structure a Unity project for multiplayer, set up and manage sessions, handle player data, synchronize gameplay objects, and implement reconnection logic for real-time games. We explored practical code examples, explained core networking concepts, and showed you how to combine all these systems in a working demo.</p>



<p class="wp-block-paragraph">With this knowledge, you’re ready to design your own multiplayer mechanics, expand on the sample game, and take full advantage of Photon Fusion 2 in future projects. If you missed any previous articles, check out the earlier parts of the series. Good luck, and happy coding!</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/">Photon Fusion 2 &#8211; Game Tutorial &#8211; Part 4/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4</title>
		<link>https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/</link>
					<comments>https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/#respond</comments>
		
		<dc:creator><![CDATA[Sir Maciej and Sir Elias]]></dc:creator>
		<pubDate>Wed, 03 Jun 2026 09:41:22 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Basics]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Multiplayer]]></category>
		<category><![CDATA[Photon Fusion 2]]></category>
		<category><![CDATA[Tips]]></category>
		<category><![CDATA[TKoU]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[Authority]]></category>
		<category><![CDATA[fusion 2]]></category>
		<category><![CDATA[multiplayer]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[photon]]></category>
		<category><![CDATA[Scripting]]></category>
		<category><![CDATA[synchronization]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5240</guid>

					<description><![CDATA[<p>Previous part: Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4. Data synchronisation is the most important part of every multiplayer game. In Photon Fusion 2, you have all the tools you need to keep players, objects, and game logic in perfect sync across the network. In this article, you’ll learn how authority works, how &#8230; <a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Previous part: <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</a>.</p>



<p class="wp-block-paragraph">Data synchronisation is the most important part of every multiplayer game. In Photon Fusion 2, you have all the tools you need to keep players, objects, and game logic in perfect sync across the network. In this article, you’ll learn how authority works, how to use networked properties and RPCs, how to synchronise object transforms and physics, and how to handle player input. Whether you’re syncing <strong>basic values</strong> or building <strong>advanced mechanics</strong>, this guide gives you the essentials to make your multiplayer game feel seamless.</p>



<h2 class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">You already understand the basics of Photon Fusion 2 and know how to choose the best network topology for your game. You can manage sessions, create new ones, join as a Client, and set up private or password-protected sessions.</p>



<p class="wp-block-paragraph">Now you’re ready for the most exciting part &#8211; <strong>syncing game data in real time</strong>. This article teaches you the core methods of data synchronisation in Photon Fusion 2. With these skills, you can finally start building your game’s main mechanics.</p>



<p class="wp-block-paragraph">You will learn how to synchronise property values, use built-in components to sync the position, rotation, and scale of objects, and manage physics across the network. You’ll handle player input, respond to parameter changes, build your first RPCs, and spawn networked objects.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">Synchronization</h2>



<p class="wp-block-paragraph">When you play a single-player game, you can freely modify the world without any communication required, because you are the only entity that is affected by it. With multiplayer games, when you want to modify the world, you have to notify other players so they can also modify their world accordingly. Otherwise, the changes would be local only, and other players wouldn&#8217;t know about it. There are even cases when your attempt at modification is straight-up denied because of a conflict with other changes you haven&#8217;t registered yet. This is a very complex process called&nbsp;<strong>data synchronisation</strong>,&nbsp;and getting it right is the key to a good player experience.</p>



<p class="wp-block-paragraph">Not everything needs to be synchronised. For instance, in a game that allows you to customise the HUD, there is no point in synchronising it. There is also no point in synchronising static level elements. The GameObjects that are synchronised are called <strong>Networked Objects</strong>.</p>



<h2 class="wp-block-heading">Authority</h2>



<p class="wp-block-paragraph">In multiplayer games &#8211; especially those built with the Host-Client topology &#8211; it’s crucial to define which peer can modify the in-game object. Photon Fusion 2 handles this with a clear authority system. Understanding how authority works helps you prevent conflicting updates, cheating, and synchronisation errors.</p>



<p class="wp-block-paragraph"><strong>Authority</strong> determines which player or device controls a specific object or game system. In Photon Fusion 2, there are two types of authority: <strong>State Authority</strong> and <strong>Input Authority</strong>.</p>



<h3 class="wp-block-heading">State Authority</h3>



<p class="wp-block-paragraph">State Authority gives a peer the right to update and synchronise the state of a networked object. In the Host-Client model, the <strong>Host always has State Authority</strong> over all networked objects. Only the Host can make decisions that affect the state of the game world &#8211; such as player positions, health values, spawning objects, and resolving gameplay actions.</p>



<p class="wp-block-paragraph">Other clients receive updates from the Host about the state of every networked object, but they cannot change the state themselves. If a client tries to update a networked property of an object without State Authority, Fusion ignores the change or overwrites it with the Host’s data on the next tick.</p>



<p class="wp-block-paragraph">State Authority is critical for:</p>



<ul class="wp-block-list">
<li>Preventing cheating by ensuring only the trusted Host can modify critical data.</li>



<li>Providing a single source of truth for the game world, thus eliminating the risk of conflicting changes.</li>



<li>Ensuring all clients see the same state as determined by the Host.</li>
</ul>



<h3 class="wp-block-heading">Input Authority</h3>



<p class="wp-block-paragraph">Input Authority controls which Client can provide input &#8211; usually the player’s own character. In the Host-Client topology, each <strong>Client holds Input Authority over its own player object</strong>. This allows the client to send movement, action, or control inputs to the Host for their character, and for the Host to react to the Client&#8217;s input.</p>



<p class="wp-block-paragraph">The Host reads received input from the clients, processes the data, and updates the authoritative game state. Fusion then replicates the updated state to all clients.</p>



<p class="wp-block-paragraph">Input Authority is important for:</p>



<ul class="wp-block-list">
<li>Letting each player control their character.</li>



<li>Keeping input collection (client-side) separate from state validation (host-side).</li>



<li>Supporting <strong>client-side prediction</strong>, so Clients can simulate their actions instantly.</li>
</ul>



<h2 class="wp-block-heading">Replication</h2>



<p class="wp-block-paragraph">Photon Fusion 2 synchronises game state between Clients through the Networked Simulation Loop. The engine splits gameplay into simulation ticks. A <strong>simulation tick</strong> is a fixed, regular step in the simulation. Every action, update, and network event occurs on these ticks, ensuring all Clients process the same events in the same order. This approach helps to keep every Client in sync, regardless of frame rate or hardware differences. You can set the tick rate in your project settings and adjust it for your game’s needs.</p>



<h3 class="wp-block-heading">Replication Mechanisms in Photon Fusion 2</h3>



<p class="wp-block-paragraph"><strong>Networked Properties</strong> &#8211; You synchronise object properties, like health or gold, by using the [Networked] attribute. The client or server with State Authority sends property changes to other clients. When a client changes a property without authority, the update is <strong>local</strong>. On the next tick, Fusion overwrites it with the value from the State Authority.</p>



<p class="wp-block-paragraph"><strong>Remote Procedure Calls (RPCs)</strong> &#8211; You mark methods with the [Rpc] attribute to call functions on other clients or the server. You call the method like any other local method, but behind the scenes, Photon synchronises the call to the method according to the attribute parameters.</p>



<p class="wp-block-paragraph"><strong>NetworkTransform</strong> &#8211; this component synchronises the position, rotation, scale, and parent object of a game object. Fusion uses this component to deliver smooth and efficient synchronisation across all clients.</p>



<p class="wp-block-paragraph"><strong>Player Input</strong> &#8211; In Host-Client Mode, the player with Input Authority collects input every tick. The Host synchronises and buffers this data, then uses it in each simulation step to simulate movement and actions.</p>



<h3 class="wp-block-heading">Replication Control Mechanisms in Photon Fusion 2</h3>



<p class="wp-block-paragraph"><strong>Interest Management (Area of Interest, AoI)</strong> &#8211; Fusion decides which objects to synchronise for each player. It only sends relevant data to each client, reducing network load and improving performance.</p>



<p class="wp-block-paragraph"><strong>FixedUpdateNetwork method</strong> &#8211; Fusion calls this method every simulation tick, so you can update object states. The system then synchronises these changes to all clients.</p>



<p class="wp-block-paragraph"><strong>Render method</strong> &#8211; Fusion calls the Render method after each simulation step to interpolate and display the current game state on the player’s screen.</p>



<p class="wp-block-paragraph"><strong>Change Detection</strong> &#8211; Fusion tracks property changes in game objects. You can react to these changes by updating the visuals or other aspects of your game.</p>



<p class="wp-block-paragraph">These tools give you full control over replication and help you keep your multiplayer game synchronised.</p>



<h2 class="wp-block-heading">Networked Properties (NP)</h2>



<p class="wp-block-paragraph">The first mechanism for state synchronisation in Photon Fusion 2 is Networked Properties. The peer with State Authority sets the values, and Fusion replicates them to all other peers in the session. This approach lets you continuously synchronise changes to the game state.</p>



<p class="wp-block-paragraph">To start replicating a property in Photon Fusion 2, follow these three steps. For more details, see the official documentation <a href="https://doc.photonengine.com/fusion/current/manual/data-transfer/networked-properties"><em>here</em></a>.</p>



<ul class="wp-block-list">
<li>Declare the property in a class that inherits from <strong>NetworkBehaviour</strong>.</li>



<li>Use an <strong>auto-property</strong> because Fusion 2 supports only auto-properties. Fusion’s IL-Weaver injects code for serialisation, compression, validation, and network transport.</li>



<li>Add the [Networked] attribute to your property.</li>
</ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="152" height="122" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-27.png" alt="" class="wp-image-5995"/></figure>



<p class="wp-block-paragraph">Photon Fusion 2 can replicate some types directly, such as byte, int, and float. For others, you must use special network types, like NetworkBool instead of the standard bool. Always keep your networked properties as auto-properties and don’t implement custom logic inside them. If you need additional logic, wrap the auto-property in another property or use <strong>Change Detection</strong>.</p>



<p class="wp-block-paragraph">Networked Properties run inside the Fusion 2 simulation loop, which works on fixed ticks. On every tick, Fusion calls the FixedUpdateNetwork method, just like Unity’s FixedUpdate. Fusion collects the values of all properties with the [Networked] attribute and creates a snapshot on the side with State Authority. Fusion then synchronises this snapshot with other peers.</p>



<p class="wp-block-paragraph">Keep in mind that Networked Properties synchronisation also connects with the Area of Interest (AoI) system, although this article does not cover AoI in detail. With AoI, you can reduce network traffic and focus on syncing only the data your players really need &#8211; for example, objects within their view or interaction range. With AoI enabled, objects outside a client’s area of interest are simulated only on the authoritative side and their state is not sent to clients unless they become relevant. AoI is disabled by default.</p>



<h2 class="wp-block-heading">Change Detection</h2>



<p class="wp-block-paragraph">You need to detect changes in Networked Properties in almost every game. For example, when the HP value changes, you might want to update the UI or destroy the object if HP reaches zero.</p>



<p class="wp-block-paragraph">The <strong>OnChangedRender</strong> attribute provides the easiest and most popular way to respond to property changes. You use this attribute to specify the name of the method that Photon Fusion 2 should call when the property changes. Fusion automatically generates code to call your chosen method whenever the value updates.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="461" height="117" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-29.png" alt="" class="wp-image-5998" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-29.png 461w, https://theknightsofu.com/wp-content/uploads/2026/04/image-29-300x76.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-29-400x102.png 400w" sizes="auto, (max-width: 461px) 100vw, 461px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="689" height="304" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-30.png" alt="" class="wp-image-5999" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-30.png 689w, https://theknightsofu.com/wp-content/uploads/2026/04/image-30-300x132.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-30-400x176.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-30-600x265.png 600w" sizes="auto, (max-width: 689px) 100vw, 689px" /></figure>



<p class="wp-block-paragraph">Fusion always calls your method after updating the property value. In the example, you can also read the previous value from the buffer.</p>



<h2 class="wp-block-heading">Remote Procedure Call (RPC)</h2>



<p class="wp-block-paragraph">Almost every game needs systems for triggering single or complex actions on demand. For example, players may set a nickname, pick up an item, or open a door. These one-time events do not need constant synchronisation, so you can reduce network traffic. By default, Photon Fusion delivers RPC calls reliably, but you can also use unreliable mode. Photon ensures the packets arrive and runs the procedure on the receiver.</p>



<p class="wp-block-paragraph">Here’s how to declare a custom RPC to set the player’s ready status. The Host can only start the match when all players are ready.<br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="872" height="230" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-34.png" alt="" class="wp-image-6003" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-34.png 872w, https://theknightsofu.com/wp-content/uploads/2026/04/image-34-300x79.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-34-768x203.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-34-400x106.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-34-600x158.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-34-800x211.png 800w" sizes="auto, (max-width: 872px) 100vw, 872px" /></figure>



<p class="wp-block-paragraph"><strong>How to define an RPC?</strong></p>



<ul class="wp-block-list">
<li>Declare the RPC in a class that inherits from NetworkBehaviour.</li>



<li>Use a &#8216;rpc&#8217; prefix or postfix.</li>



<li>Add the [Rpc] attribute.</li>



<li>Set RpcSources to control who can call the RPC. For example, every player with InputAuthority.</li>



<li>Set RpcTargets to control who receives the RPC. In most cases, only the Host, since only the Host with StateAuthority can update the networked state.</li>



<li>Optionally, set HostMode to control how Photon treats the Host. By default, Photon treats the Host as a server, so RpcInfo.Source will be None. To treat the Host like any other client, set SourceIsHostPlayer.</li>
</ul>



<p class="wp-block-paragraph"><strong>How does this work in practice?</strong></p>



<ul class="wp-block-list">
<li>Player B (Client) calls RPC_ChangeIsReady(true) to confirm readiness.</li>



<li>Photon checks if Player B can receive the RPC. Since B lacks StateAuthority, Photon only sends the call to valid targets (the Host, Player A). The method is not called on the Client.</li>



<li>Player A receives the RPC and runs the method RPC_ChangeIsReady(true). Player A as a Host updates the IsReady property.</li>



<li>The IsReady property synchronises to all clients using the [Networked] attribute.</li>



<li>Both players react to the property change and update the status on screen.</li>
</ul>



<p class="wp-block-paragraph"><strong>Important note</strong></p>



<p class="wp-block-paragraph">RPCs are <strong>one-time events</strong>. They only reach players at the moment of execution. If your game logic depends on RPCs, remember to handle new players joining or reconnecting. For example, if you open a door via an RPC and don’t continuously synchronise its state, send the door&#8217;s status to new clients when they join. Otherwise, the door state will be out of sync.<br></p>



<h2 class="wp-block-heading">Data Streaming</h2>



<p class="wp-block-paragraph">Sometimes you need to transfer much more data than a simple variable or an RPC parameter. Some games allow players to create custom maps, share images and videos, or exchange configuration and save data.</p>



<p class="wp-block-paragraph">Photon Fusion 2 offers an easy-to-use data streaming system. You can send large byte arrays from the client to the server or host, and in the other direction as well. The high-level API lets you send and receive data without managing packet fragmentation or order. As the sender, you call a method to reliably send data. The receiver listens for events that report transfer progress and confirms when the whole package arrives. Fusion 2 does not inform the sender about delivery status or completion. If you need this feedback, use RPCs to notify the sender.</p>



<p class="wp-block-paragraph">Data streaming does not depend on State Authority or Input Authority. To send data to a player, use a NetworkRunner in Host or Server mode. When you want to send data to the server, use the NetworkRunner in Client mode. To receive data or progress info, implement the methods OnReliableDataProgress and OnReliableDataReceived from the INetworkRunnerCallbacks interface.</p>



<p class="wp-block-paragraph"><strong>Sending</strong></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="604" height="123" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-40.png" alt="" class="wp-image-6012" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-40.png 604w, https://theknightsofu.com/wp-content/uploads/2026/04/image-40-300x61.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-40-400x81.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-40-600x122.png 600w" sizes="auto, (max-width: 604px) 100vw, 604px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="474" height="119" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-41.png" alt="" class="wp-image-6014" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-41.png 474w, https://theknightsofu.com/wp-content/uploads/2026/04/image-41-300x75.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-41-400x100.png 400w" sizes="auto, (max-width: 474px) 100vw, 474px" /></figure>



<p class="wp-block-paragraph"><strong>Receiving</strong></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="229" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-42-1024x229.png" alt="" class="wp-image-6015" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-42-1024x229.png 1024w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-300x67.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-768x172.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-400x90.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-600x134.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-800x179.png 800w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42-1000x224.png 1000w, https://theknightsofu.com/wp-content/uploads/2026/04/image-42.png 1121w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Sync Components</h2>



<p class="wp-block-paragraph">Now you’ll learn about the main components that let you easily synchronise basic properties of scene objects &#8211; such as position, rotation, scale, and even object hierarchy, meaning parent-child relationships. I’ll also introduce you to the component for syncing Rigidbodies, so you can use physics in your multiplayer game.</p>



<h3 class="wp-block-heading">NetworkTransform</h3>



<p class="wp-block-paragraph">The NetworkTransform component allows you to synchronise an object’s position and rotation by default. You can place it either at the root of your object hierarchy, next to the NetworkObject component, or on any nested child object. You don’t need to do anything else to start using this basic functionality.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="469" height="314" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-35.png" alt="" class="wp-image-6004" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-35.png 469w, https://theknightsofu.com/wp-content/uploads/2026/04/image-35-300x201.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-35-400x268.png 400w" sizes="auto, (max-width: 469px) 100vw, 469px" /></figure>



<p class="wp-block-paragraph">For objects with a hierarchy, add a NetworkTransform to every object whose position and rotation relative to its parent should be synchronised. If your game requires it, you can also enable localScale synchronisation by checking the Sync Scale option.</p>



<p class="wp-block-paragraph">A very important aspect of synchronising objects and their hierarchies in games is managing parenting and changing the parent of an object. Some of the most common examples include a player character picking up an item and holding it in their hand, or a character entering a vehicle and moving with it. Usually, you handle such operations by changing the object hierarchy &#8211; nesting one object inside another. For instance, a seat in a vehicle becomes the parent of a player character’s object when you call playerTransform.SetParent(vehicleSeatTransform) and set the appropriate local position and rotation. Photon Fusion 2 also needs to correctly handle changes to the hierarchy so that the scene state on the Client matches that on the Host.</p>



<p class="wp-block-paragraph">To explain how this works, let’s look at an example where you mount a weapon on a vehicle in a sample game. Below, you can see the hierarchy of a vehicle controlled by the player. The vehicle contains a nested structure of objects, and one of them is called Weapon Slot, marking the place on the roof where you can attach a weapon.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="538" height="584" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-43.png" alt="" class="wp-image-6055" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-43.png 538w, https://theknightsofu.com/wp-content/uploads/2026/04/image-43-276x300.png 276w, https://theknightsofu.com/wp-content/uploads/2026/04/image-43-300x326.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-43-400x434.png 400w" sizes="auto, (max-width: 538px) 100vw, 538px" /></figure>



<p class="wp-block-paragraph">The weapon object is defined as a prefab consisting of a gun base and a barrel. Once mounted on the vehicle, the gun base can rotate relative to the vehicle, and the barrel can move up and down relative to the base.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="450" height="262" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-45.png" alt="" class="wp-image-6059" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-45.png 450w, https://theknightsofu.com/wp-content/uploads/2026/04/image-45-300x175.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-45-400x233.png 400w" sizes="auto, (max-width: 450px) 100vw, 450px" /></figure>



<p class="wp-block-paragraph">How do you set up the hierarchy so that a call to basicGunTransform.SetParent(sportCar1Transform) works and replicates across all peers while still allowing the weapon to perform the movements described above? The key is the <strong>Sync Parent </strong>option, which you enable on the NetworkTransform component. Sync Parent works upwards &#8211; the component with this option enabled will synchronise the identifier of its parent object.</p>



<ul class="wp-block-list">
<li><strong>Basic Gun</strong>: Add NetworkObject (as the root) and NetworkTransform with Sync Parent enabled. This object will be nested inside the Weapon Slot.</li>



<li><strong>Barrel Mount</strong>: Add NetworkTransform, but don’t enable Sync Parent. The hierarchy doesn’t change here, but the barrel still moves relative to the base.</li>



<li><strong>Sport Car 1</strong>: Add NetworkObject (root) and NetworkRigidbody3D (explained in the next section).</li>



<li><strong>Weapon Slot</strong>: Add NetworkTransform (without Sync Parent), even if its position and rotation won’t change relative to the vehicle. Each NetworkTransform has a unique NetworkTransformId, which Fusion 2 uses to identify the parent during synchronisation.</li>
</ul>



<p class="wp-block-paragraph">After this simple setup, with no code required, your objects are ready for logic like mounting weapons on vehicles. You can now focus on your game mechanics and let Fusion 2 handle the network replication.</p>



<h3 class="wp-block-heading">NetworkRigidbody3D</h3>



<p class="wp-block-paragraph">NetworkRigidbody3D is part of the <strong>Physics add-on</strong> for Photon Fusion 2. You need to download and import this package separately (see the <em><a href="https://doc.photonengine.com/fusion/current/addons/physics-addon-2.0">link</a></em>). This component lets you synchronise Unity’s Rigidbody over the network. To control networked physics, you also need to add the RunnerSimulatePhysics3D component to the object with NetworkRunner. If you need a reminder, check the <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">earlier article</a> in this series &#8211; especially the part about the NetworkRunner component. RunnerSimulatePhysics3D lets you decide how Fusion 2 should work with Unity’s physics engine. Fusion takes Physics Authority, managing all calls to Physics.SyncTransforms() and Physics.Simulate(). The Physics Timing setting lets you choose if these calls run inside Fusion’s FixedUpdateNetwork loop or in Unity’s own Update or FixedUpdate methods.<br></p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="460" height="201" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-36.png" alt="" class="wp-image-6005" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-36.png 460w, https://theknightsofu.com/wp-content/uploads/2026/04/image-36-300x131.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-36-400x175.png 400w" sizes="auto, (max-width: 460px) 100vw, 460px" /></figure>



<p class="wp-block-paragraph">You can configure exactly how the physics simulation runs on each client.</p>



<p class="wp-block-paragraph"><strong>Key Client Physics Simulation Settings</strong></p>



<ul class="wp-block-list">
<li><strong>Disabled</strong> &#8211; By default, client-side simulation is turned off. Only the Host or Server simulates physics. Clients simply replicate object positions and rotations after the simulation. In this mode, Fusion does not call SyncTransforms(), so any interaction with physics objects won’t work on the client.</li>



<li><strong>Sync Transforms</strong> &#8211; In this mode, clients do not run a full physics simulation. However, Fusion calls SyncTransforms() every tick. This allows physics interactions to work as expected, because the physics engine always has the correct object positions, rotations and proper Rigidbody configurations.</li>



<li><strong>Simulate Forward</strong> &#8211; The client runs Simulate() for each forward tick. Players immediately see feedback from their actions, without waiting for the server. When the server’s authoritative state arrives, Fusion 2 corrects the local predicted state to match the server. If the difference is minor, you can interpolate smoothly to prevent sudden jumps.</li>



<li><strong>Simulate Always</strong> &#8211; This is the most accurate, but also the most performance-intensive mode. Fusion runs a local physics simulation for every forward tick and every resimulation. The Client’s state always stays as close as possible to the server.</li>
</ul>



<h2 class="wp-block-heading">Player Input</h2>



<p class="wp-block-paragraph">Players interact with the game by sending input. They move characters, drive vehicles, shoot, and use items. Each action generates input data, which players use to affect the game world. In a Host-Client topology, the Client cannot modify the game state directly. Clients only have InputAuthority; only Host holds StateAuthority. You need a system where Clients collect their input, send it to the Host, and the Host updates the game world. Then Photon Fusion 2 replicates these changes to all Clients.</p>



<p class="wp-block-paragraph">Fusion 2 collects player input every simulation tick and sends it to the Host. Fusion supports client-side prediction. This feature lets the client instantly simulate the results of its own inputs, without waiting for the authoritative Host’s response. If the local prediction does not match the authoritative state from the Host, Fusion automatically rewinds the simulation and replays it with the correct data &#8211; a process called rollback and reconciliation. As a result, players experience smooth and responsive controls, even if there is a network delay.</p>



<h3 class="wp-block-heading">NetworkInputData structure</h3>



<p class="wp-block-paragraph">Define a structure to store player input by implementing the INetworkInput interface. In the example shown below, the code uses the NetworkButtons structure to track which buttons the player presses. NetworkButtons makes it easy to check if a button is currently pressed, but it does not store previous states. If you need to compare the current and previous input values &#8211; such as detecting when a button is pressed, released, or held &#8211; you should store the previous state yourself.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="405" height="228" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-37.png" alt="" class="wp-image-6008" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-37.png 405w, https://theknightsofu.com/wp-content/uploads/2026/04/image-37-300x169.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-37-400x225.png 400w" sizes="auto, (max-width: 405px) 100vw, 405px" /></figure>



<h3 class="wp-block-heading">How to read and use Input?</h3>



<p class="wp-block-paragraph">Use the <strong>OnInput</strong> method, part of the INetworkRunnerCallbacks interface, to collect input each simulation tick. Fusion calls this method on every tick. Avoid spreading input collection across many components. If you do, multiple OnInput calls can overwrite data. The best practice is to use a single component to aggregate all player input, so only one OnInput call runs per tick.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="967" height="358" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-38.png" alt="" class="wp-image-6009" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-38.png 967w, https://theknightsofu.com/wp-content/uploads/2026/04/image-38-300x111.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-38-768x284.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-38-400x148.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-38-600x222.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-38-800x296.png 800w" sizes="auto, (max-width: 967px) 100vw, 967px" /></figure>



<p class="wp-block-paragraph">In the example above, OnInput collects vehicle controls, key presses, and mouse position for aiming. Store all collected input in NetworkInput with the <strong>Set method</strong>. If you call Set from multiple places without aggregating, input can be overwritten.</p>



<p class="wp-block-paragraph">After Fusion 2 collects and synchronises player input, the Host uses it to update the game world. Unlike input collection, which should happen in only one place, you can process player input in as many systems as your game requires. For example, different gameplay systems &#8211; such as character control, weapon handling, or UI &#8211; can all read and use the same input data. You typically retrieve player input using the GetInput(…) method, which you call inside FixedUpdateNetwork().</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="665" height="456" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-39.png" alt="" class="wp-image-6010" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-39.png 665w, https://theknightsofu.com/wp-content/uploads/2026/04/image-39-300x206.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-39-400x274.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-39-600x411.png 600w" sizes="auto, (max-width: 665px) 100vw, 665px" /></figure>



<p class="wp-block-paragraph">For <strong>local co-op games</strong>, Photon Fusion 2 treats multiple local players as one peer. If two players share a device in split-screen, handle their inputs in your own structure. Design your input structure to separate each player’s data.</p>



<h2 class="wp-block-heading">Object Spawning</h2>



<p class="wp-block-paragraph">Let’s look at a key multiplayer feature: spawning new networked objects. You’ll almost always need to <strong>create objects dynamically</strong> during gameplay. For example, when a character drops an item, throws a grenade, or crafts a potion and sets the flask on a table. All these objects need to be replicated across all clients in a multiplayer environment.</p>



<p class="wp-block-paragraph">To make an object networked, you <strong>must </strong>register it with Photon Fusion 2. Never create these objects using GameObject.Instantiate(). That method only creates a local instance without a network connection. Always use NetworkRunner.Spawn(). This method creates a networked instance, assigns a unique ID, and ensures Fusion 2 replicates the object correctly for all Clients, even those that join the session after the creation.</p>



<p class="wp-block-paragraph">When you call NetworkRunner.Spawn(), you must provide a prefab of type NetworkObject. You can also specify a position, rotation, or a PlayerRef to decide which player gets InputAuthority over the object. </p>



<p class="wp-block-paragraph"><strong>Remember:</strong> the position and rotation you set affect only the local state on the side responsible for spawning the object &#8211; usually the Host or Server. To synchronise positions <strong>across all clients</strong>, use data synchronisation mechanisms such as NetworkTransform, as covered in the previous sections. The same principle applies if you want to parent the new object under another object.</p>



<p class="wp-block-paragraph">NetworkRunner.Spawn() also accepts an OnBeforeSpawned callback, which runs just before the object is replicated. Use this callback to handle custom configuration.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="32" src="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-1024x32.png" alt="" class="wp-image-5351" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-1024x32.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-300x9.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-768x24.png 768w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-400x13.png 400w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-600x19.png 600w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-800x25.png 800w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-1000x31.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2-1280x40.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-2.png 1398w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="797" height="158" src="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3.png" alt="Photon Fusion 2 OnBeforeSpawned method example." class="wp-image-5352" style="width:393px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3.png 797w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3-300x59.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3-768x152.png 768w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3-400x79.png 400w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-3-600x119.png 600w" sizes="auto, (max-width: 797px) 100vw, 797px" /></figure>



<p class="wp-block-paragraph">Every NetworkBehaviour implements the ISpawned and IDespawned interfaces, which provide Spawned and Despawned methods. Override these methods to handle initialisation and cleanup, just like Unity’s Awake, Start, or Destroy.</p>



<p class="wp-block-paragraph">If you need extra control, you can implement the IAfterSpawned interface. This gives you an AfterSpawned method, called immediately after the networked object is created. Fusion 2 calls these interface methods both for objects created at runtime and for any objects that already exist in the scene and have a networked representation.</p>



<p class="wp-block-paragraph">Fusion calls the Spawned and Despawned methods on both the Host and Client. Both sides can execute logic as needed for your game. To find out if code is running on the Host or Client, check NetworkRunner.IsServer or NetworkRunner.IsClient, or use Object.HasStateAuthority and Object.HasInputAuthority to verify the object’s authority.</p>



<p class="wp-block-paragraph">Keep in mind that Fusion calls OnBeforeSpawned <strong>exclusively on the Host.</strong> On the Client, Fusion only executes the Spawned and AfterSpawned methods. This distinction is important when you need to run pre-spawn configuration or custom logic. Make sure to implement it on the Host side.</p>



<p class="wp-block-paragraph"><strong>Host</strong></p>



<ul class="wp-block-list">
<li>OnBeforeSpawned callback from NetworkManager.Spawn(&#8230;).</li>



<li>Spawned.</li>



<li>AfterSpawned.</li>
</ul>



<p class="wp-block-paragraph"><strong>Client</strong></p>



<ul class="wp-block-list">
<li>Spawned.</li>



<li>AfterSpawned.</li>
</ul>



<h2 class="wp-block-heading">Summary and what’s next?</h2>



<p class="wp-block-paragraph">In previous articles, you created accounts, discovered the basics of networking topologies, the main use cases for Photon Fusion 2, and learned how to manage user sessions. If you missed any of these topics, be sure to check out the earlier posts in this series &#8211; they provide the foundation for everything covered here:<br><a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4</a><br><a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/" type="link" id="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</a></p>



<p class="wp-block-paragraph">You’ve just learned the core techniques for data synchronisation in Photon Fusion 2: how authority works, how to use networked properties and RPCs, how to sync object transforms and physics, and how to handle player input for smooth gameplay. With these skills, you can now build reliable multiplayer features and keep your game state consistent for all players.</p>



<p class="wp-block-paragraph">Next, you’ll take your project further by exploring <strong>advanced synchronisation strategies, optimising network performance, and managing session state and players.</strong> Stay tuned to unlock the full potential of Photon Fusion 2 and create seamless multiplayer experiences.</p>



<p class="wp-block-paragraph">Continue reading: <a href="https://theknightsofu.com/photon-fusion-2-game-tutorial-part-4-4/">Photon Fusion 2 &#8211; Game Tutorial &#8211; Part 4/4</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</title>
		<link>https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/</link>
					<comments>https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/#respond</comments>
		
		<dc:creator><![CDATA[Sir Maciej and Sir Elias]]></dc:creator>
		<pubDate>Fri, 22 May 2026 07:39:53 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Basics]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Multiplayer]]></category>
		<category><![CDATA[Photon Fusion 2]]></category>
		<category><![CDATA[Tips]]></category>
		<category><![CDATA[TKoU]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[fusion 2]]></category>
		<category><![CDATA[multiplayer]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[photon]]></category>
		<category><![CDATA[Scripting]]></category>
		<category><![CDATA[Sessions Management]]></category>
		<category><![CDATA[topologies]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5237</guid>

					<description><![CDATA[<p>Previous part: Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4. Want to learn how to create, join, and manage multiplayer sessions in Photon Fusion 2? In this article, we’ll walk you through everything you need to know about working with sessions using the Host-Client topology. You’ll discover how to set up public, private, and password-protected &#8230; <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Previous part: <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4.</a></p>



<p class="wp-block-paragraph">Want to learn how to create, join, and manage multiplayer sessions in Photon Fusion 2? In this article, we’ll walk you through everything you need to know about <strong>working with sessions using the Host-Client topology</strong>. You’ll discover how to set up public, private, and password-protected sessions, use matchmaking effectively, and handle key callbacks such as OnConnectedToServer, OnSessionListUpdated, and OnConnectRequest. Whether you&#8217;re just starting out or looking to improve your session flow, this guide will give you a clear, practical foundation.</p>



<h2 class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">In the <strong>previous post</strong>, we explored what Photon Fusion 2 is, what network topologies it supports, and what it’s generally capable of. You’ve already created your first project, configured the plugin inside Unity, and now you’re ready to start working on your first real multiplayer logic.</p>



<p class="wp-block-paragraph">In this post, you’ll learn how to create new Photon Fusion 2 sessions, find and join existing ones, and even set up a private session protected by a password. You’ll <span style="box-sizing: border-box; margin: 0px; padding: 0px;">become familiar with&nbsp;<strong>NetworkRunner</strong>, learn about its core configuration options, and explore essential session-management callbacks</span>. We’ll be using the <strong>Host-Client topology</strong> throughout this post to keep things simple and cost-effective during the learning process. No need to worry about dedicated server infrastructure just yet.</p>



<h2 class="wp-block-heading">Network Runner</h2>



<p class="wp-block-paragraph">Photon Engine documentation here: <a href="https://doc.photonengine.com/fusion/current/manual/network-runner">Network Runner</a>.</p>



<p class="wp-block-paragraph">Before diving into multiplayer logic with Photon Fusion 2 Sessions, you should first understand what the <strong>NetworkRunner</strong> actually is. At the heart of Fusion 2 is a mission to let developers focus on building gameplay, not on rewriting the same networking logic for every project.</p>



<p class="wp-block-paragraph"><strong>NetworkRunner</strong> is the central component that represents a player-controlled peer in the game. It wraps most of the core multiplayer functionality, like session connection, matchmaking, input handling or spawning and despawning of objects into one user-friendly component. Simply put, it’s the most important part of the Fusion 2 system.</p>



<p class="wp-block-paragraph">Most games use a single NetworkRunner in one app, but Fusion 2 also offers a more advanced <strong>multi-peer mode</strong> where multiple NetworkRunners can coexist in one app. We’ll stick to the single runner setup in this article.</p>



<p class="wp-block-paragraph">The <strong>NetworkRunner</strong> is also responsible for regularly executing the FixedUpdateNetwork() and Render() methods, as well as invoking <strong>callbacks</strong> that notify about key events during connection setup, player joining, and general network communication.</p>



<p class="wp-block-paragraph">Upon creation, the NetworkRunner automatically scans its child components in the hierarchy and registers any that inherit from SimulationBehaviour or implement the INetworkRunnerCallbacks interface.</p>



<p class="wp-block-paragraph">If you want to register an INetworkRunnerCallbacks component that isn’t part of the NetworkRunner’s hierarchy at the time it’s created, you’ll need to call NetworkRunner.AddCallbacks(&#8230;) manually and pass in a reference to the target callback instance.</p>



<h3 class="wp-block-heading">Creating NetworkRunner</h3>



<p class="wp-block-paragraph">There are three ways to create a NetworkRunner instance.</p>



<ul class="wp-block-list">
<li><strong>Using a prefab</strong>. This approach allows you to preconfigure the NetworkRunner, add components for callback handling, or display debug data.</li>



<li><strong>Placing it directly in the scene</strong>. If your scene structure allows it, you can place a runner directly in the Unity Editor and reference it when needed.</li>



<li><strong>Creating it dynamically in code</strong>. You can instantiate a GameObject at runtime and add the NetworkRunner component via AddComponent().</li>
</ul>



<p class="wp-block-paragraph">Once you’ve got your NetworkRunner reference, you’re ready to start creating or joining sessions. But before we move on to that, let’s talk about <strong>GameMode</strong> and <strong>Callbacks</strong> Fusion 2 provides. These are essential for understanding how sessions work and for monitoring key multiplayer processes.</p>



<h2 class="wp-block-heading">GameMode</h2>



<p class="wp-block-paragraph">In this section, we’ll revisit some familiar concepts from the <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">previous post</a> &#8211; but this time from a new perspective. While the previous article focused on Photon Fusion 2 network topologies and their pros and cons, here we’ll examine the same topic from the perspective of a single application. Photon needs to know whether the application is supposed to run as a <strong>Dedicated Server or a Client</strong>. This is what the <strong>GameMode</strong> is used for. What is important to point out is that topology limits the GameModes you can use. Let’s take a closer look at available options for each topology.</p>



<h3 class="wp-block-heading">Host-Client</h3>



<p class="wp-block-paragraph"><strong>GameMode.Host</strong> is used for a player acting simultaneously as both the Server and a Client. <strong>GameMode.Client</strong> is used for players joining the host. Fusion 2 also offers a hybrid mode <strong>GameMode.AutoHostOrClient</strong>. It first tries to join an existing session as a Client. If none is found, a new session is started, and the player becomes the <strong>Host</strong>. It can simplify the code and testing as you can use one method to both create a session and join it.</p>



<h3 class="wp-block-heading">Dedicated Server</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>GameMode.Server </strong>is used for the server. <strong>GameMode.Client</strong> is used for clients. Note that there is no auto mode.</p>



<h3 class="wp-block-heading">Shared</h3>



<p class="wp-block-paragraph">There is only one mode for all of the clients &#8211; it is <strong>GameMode.Shared</strong>.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<h3 class="wp-block-heading">GameMode.Single</h3>



<p class="wp-block-paragraph">One of Fusion 2’s most practical features is its ability to run in <strong>Singleplayer mode</strong> with almost no changes to your existing multiplayer logic. If your multiplayer game is designed in a way that can function with just one player, Fusion makes it easy to support offline play.</p>



<p class="wp-block-paragraph">All you have to do is start a session using <strong>GameMode.Single</strong>. This tells Fusion that the session will run entirely offline &#8211; no clients can join, and Photon Cloud services are disabled. The key benefit is that all the multiplayer systems &#8211; object spawning, state replication, event handling, simulation ticks &#8211; still work.</p>



<p class="wp-block-paragraph">The player in this mode behaves exactly like a Host. They act as both the server and the client, but in a self-contained, local-only session.</p>
</blockquote>
</blockquote>



<h2 class="wp-block-heading">Session Callbacks</h2>



<p class="wp-block-paragraph">Below is a list of callbacks from the <strong>INetworkRunnerCallbacks </strong>interface relevant to session management. While not every project will need to implement all of them, it&#8217;s useful to be aware of these options to better understand what Photon Fusion 2 provides.</p>



<ul class="wp-block-list">
<li><strong>OnConnectedToServer(NetworkRunner runner).</strong> This callback is triggered when a connection to the server or host is successfully established. You can use it to initialise the game state or notify the player that the connection has succeeded.</li>



<li><strong>OnConnectFailed(NetworkRunner runner, NetAddress remoteAddress, NetConnectFailedReason reason)</strong>. Triggered when a connection attempt to a server or host fails. It provides details about the target address and the failure reason, allowing the application to display error messages or retry the connection.</li>



<li><strong>OnConnectRequest(NetworkRunner runner, NetworkRunnerCallbackArgs.ConnectRequest request, byte[] token)</strong>. Triggered when the server receives a connection request from a remote client. This allows you to validate the request and either accept or reject it, useful for implementing custom authentication or access control systems.</li>



<li><strong>OnDisconnectedFromServer(NetworkRunner runner)</strong>. Triggered when the connection to the server or host is lost. You can use this to handle disconnection logic, such as saving progress, informing the player, or initiating a reconnection attempt.</li>



<li><strong>OnSessionListUpdated(NetworkRunner runner, List&lt;SessionInfo&gt; sessionList)</strong>. Called when the Photon Cloud returns an updated list of visible and joinable sessions in the connected lobby. You can use this data to populate a session browser UI for players.</li>



<li><strong>OnShutdown(NetworkRunner runner, ShutdownReason shutdownReason)</strong>. Triggered when the NetworkRunner is shutting down. It provides the reason for the shutdown and can be used to finalise the session, save progress, or clean up resources.</li>



<li><strong>OnCustomAuthenticationResponse(NetworkRunner runner, Dictionary&lt;string, object&gt; data)</strong>. Called after receiving a response from the authentication server. You can use the returned data to configure the session, adjust permissions, or customise gameplay based on user identity.</li>



<li><strong>OnHostMigration(NetworkRunner runner, HostMigrationToken hostMigrationToken)</strong>. Triggered when host migration begins, typically in Host-Client mode. It provides a migration token containing all necessary data to resume the session on a new host after a disconnection or crash.</li>
</ul>



<p class="wp-block-paragraph">To register callbacks, use NetworkRunner.AddCallbacks or the NetworkEvents component.</p>



<h2 class="wp-block-heading">Create and Join Session</h2>



<h3 class="wp-block-heading">Create session</h3>



<p class="wp-block-paragraph">Let’s move on to your first real implementation &#8211; creating a session so others can join!</p>



<p class="wp-block-paragraph">Photon Fusion 2 uses the NetworkRunner.StartGame(…) method to either create a new session or join an existing one, based on the parameters you provide. To create a new session, set the GameMode to Host.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="630" height="378" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-21.png" alt="" class="wp-image-5980" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-21.png 630w, https://theknightsofu.com/wp-content/uploads/2026/04/image-21-300x180.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-21-400x240.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-21-600x360.png 600w" sizes="auto, (max-width: 630px) 100vw, 630px" /></figure>



<p class="wp-block-paragraph">This is a simple way to start a new session without providing any extra details. If you don’t set the PlayerCount parameter, Fusion 2 will use the default value from the config file located at:<br>Assets/Photon/Fusion/Resources/NetworkProjectConfig.fusion.</p>



<h3 class="wp-block-heading">StartGameArgs</h3>



<p class="wp-block-paragraph">The StartGameArgs structure is used to provide data when creating or joining a session. Here are a few important parameters you should be aware of.</p>



<ul class="wp-block-list">
<li><strong>SessionName</strong> &#8211; A custom name for your session. It must be unique within the region and serve as the session’s identifier. If left empty, Fusion will generate a GUID.</li>



<li><strong>SessionProperties</strong> &#8211; A list of key-value pairs used during matchmaking as filters.</li>



<li><strong>CustomLobbyName</strong> &#8211; By default, Fusion creates a separate lobby per GameMode. You can override this behaviour and define your own lobby names to organise different types of sessions.</li>



<li><strong>IsOpen</strong> &#8211; Determines whether new players can join the session. It can be changed after the session has started.</li>



<li><strong>IsVisible</strong> &#8211; Controls whether the session is visible in matchmaking. If set to false, the Matchmaker won’t assign players to it, and it won’t appear in public session lists. This can also be changed later.</li>
</ul>



<h3 class="wp-block-heading">Join the Session directly</h3>



<p class="wp-block-paragraph">The most direct way to join sessions in Photon Fusion 2 is by using a specific SessionName. In Fusion, the session name serves as a unique ID within the region and cannot be duplicated. When creating a session, you can define its name via the SessionName property in StartGameArgs. If left empty, a GUID will be automatically generated as the SessionName.</p>



<p class="wp-block-paragraph">This approach is particularly useful for <strong>direct player invites</strong>. For example, you can create a private session and hide it from public matchmaking by setting StartGameArgs.IsVisible to false. Players can still join it by providing the correct SessionName.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="639" height="358" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-20.png" alt="" class="wp-image-5979" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-20.png 639w, https://theknightsofu.com/wp-content/uploads/2026/04/image-20-300x168.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-20-400x224.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-20-600x336.png 600w" sizes="auto, (max-width: 639px) 100vw, 639px" /></figure>



<p class="wp-block-paragraph">Keep in mind that when a SessionName is specified, it overrides all matchmaking filters. For instance, if you define a map type in SessionProperties, the system ignores it when you join a session by SessionName.</p>



<p class="wp-block-paragraph">In the example above, you call StartGame with GameMode set to Client, which means you join a session named &#8220;MyFirstSession&#8221; as a Client. If the session doesn’t exist or a connection error occurs, the StartGameResult will indicate the failure and include relevant error details.</p>



<h3 class="wp-block-heading">Matchmaking</h3>



<p class="wp-block-paragraph">The most commonly recommended approach to joining sessions in Photon Fusion 2 is <strong>Matchmaking</strong>, which looks for an open session that hasn’t reached its player limit. If you use GameMode.AutoHostOrClient in StartGameArgs and no matching session exists, Photon Fusion 2 automatically creates a new one and makes the first player the Host.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="855" height="497" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-18.png" alt="" class="wp-image-5977" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-18.png 855w, https://theknightsofu.com/wp-content/uploads/2026/04/image-18-300x174.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-18-768x446.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-18-400x233.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-18-600x349.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-18-800x465.png 800w" sizes="auto, (max-width: 855px) 100vw, 855px" /></figure>



<p class="wp-block-paragraph">Matchmaking in Fusion 2 can operate in one of three modes, each with different matching behaviour. You can configure this using StartGameArgs:</p>



<ul class="wp-block-list">
<li><strong>FillRoom</strong> &#8211; The default mode. It fills up the oldest available sessions first to average out waiting times. This mode ignores SessionProperties filters.</li>



<li><strong>SerialMatching</strong> &#8211; Distributes players more evenly between sessions and respects the SessionProperties filters.</li>



<li><strong>RandomMatching</strong> &#8211; Selects sessions at random but also respects the SessionProperties filters.</li>
</ul>



<p class="wp-block-paragraph">Now, let’s explain what these <strong>SessionProperties</strong> are. Fusion 2 offers a basic matchmaking system that lets you define a set of key-value pairs. A session will only be matched if all the provided properties <strong>exactly</strong> match. You can use this to filter by game mode, map name, or similar parameters. However, this approach doesn’t support more advanced scenarios like ranking-based matchmaking or skill brackets. There’s no built-in support for range comparisons in parameters.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="841" height="107" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-19.png" alt="" class="wp-image-5978" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-19.png 841w, https://theknightsofu.com/wp-content/uploads/2026/04/image-19-300x38.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-19-768x98.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-19-400x51.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-19-600x76.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-19-800x102.png 800w" sizes="auto, (max-width: 841px) 100vw, 841px" /></figure>



<h3 class="wp-block-heading">Matchmaking abuse</h3>



<p class="wp-block-paragraph"><strong>Photon Fusion 2 Matchmaking is client-driven</strong>. All matchmaking decisions are based solely on the data sent by the client looking to join. So if you plan to implement features like skill-based matchmaking, you’ll need to handle this on the client side &#8211; which opens the system up to potential <strong>tampering or abuse</strong>. Players can modify their request and claim that they belong to a different skill group.</p>



<p class="wp-block-paragraph">If your Matchmaking needs to be secure and cannot rely blindly on the data from the Client, you should turn to external solutions like <strong>Unity Game Services</strong>. You can integrate their Matchmaking with their Cloud Code to run sensitive logic securely on the backend. The Client never accesses this logic or data. Only the final result comes back to it.</p>



<h3 class="wp-block-heading">Lobby and Sessions List</h3>



<p class="wp-block-paragraph">The less-recommended way to join a session is manual session browsing. Player selects a session from a full list of all available sessions in a region.</p>



<p class="wp-block-paragraph">Before diving into how this works, let’s first define what a <strong>Region</strong> is. Photon Cloud sets up Regions as physical data center locations around the world to group player traffic locally and minimise latency. <strong>As of writing</strong>, Photon supports 16 regions, such as &#8220;eu&#8221; (Europe, hosted in Amsterdam) or &#8220;us&#8221; (US West, hosted in Washington).</p>



<p class="wp-block-paragraph">Fusion 2 supports both automatic region selection &#8211; based on player location &#8211; and manual selection when needed.</p>



<p class="wp-block-paragraph">A <strong>Lobby</strong> is a virtual container within a Region that groups active game sessions. When creating a session, you can assign it to a named Lobby. Within a Lobby, players cannot see each other, and there’s no communication or visibility of who else is present.</p>



<p class="wp-block-paragraph">If you want players to manually browse and select from a list of sessions, they must first join a specific Lobby. You can join the default lobby for Client-Server mode using the following code:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="776" height="193" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-22.png" alt="" class="wp-image-5981" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-22.png 776w, https://theknightsofu.com/wp-content/uploads/2026/04/image-22-300x75.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-22-768x191.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-22-400x99.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-22-600x149.png 600w" sizes="auto, (max-width: 776px) 100vw, 776px" /></figure>



<p class="wp-block-paragraph">After successfully joining a Lobby, any session updates within that Lobby will trigger the OnSessionListUpdated method from INetworkRunnerCallbacks. The sessionList parameter will provide information about all visible sessions. You can use this data to display a custom session browser in your UI and allow players to join a session of their choice.</p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="507" src="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-1024x507.png" alt="OnSessionListUpdate callback." class="wp-image-5305" style="width:652px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-1024x507.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-300x148.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-768x380.png 768w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-400x198.png 400w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-600x297.png 600w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-800x396.png 800w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1-1000x495.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/obraz-1.png 1079w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h4 class="wp-block-heading">Issues when using the Sessions List</h4>



<p class="wp-block-paragraph">This method generates <strong>significant network traffic</strong>. Every change in a session’s parameters triggers updates to all connected clients. Photon officially discourages this pattern and considers it legacy. Still, if your game requires advanced session filtering on the client side, this remains the only viable method.</p>



<p class="wp-block-paragraph"><strong>Security warning:</strong> All matchmaking and filtering logic in this model happens on the client side. Without a proper backend to validate data, you are exposed to potential manipulation, e.g. Client joining a session that should not be available to them.</p>



<h2 class="wp-block-heading">Private Session or Password-Protected Session</h2>



<p class="wp-block-paragraph">One important and sometimes overlooked topic in Photon Fusion 2 is how to create private sessions or sessions protected by a password.</p>



<h3 class="wp-block-heading">Session IsVisible</h3>



<p class="wp-block-paragraph">A private session is one where the <strong>IsVisible flag is set to false</strong>. This means the session will not be visible to the Matchmaker or listed in the OnSessionListUpdated callback. The only way to join it is by specifying its exact name in the SessionName field of StartGameArgs. If the name matches, the session is open (IsOpen), and there are free slots, the connection will be accepted. Otherwise, the join request will be refused.</p>



<h3 class="wp-block-heading">ConnectionToken</h3>



<p class="wp-block-paragraph">You may want to protect your session with a password. Fusion 2 does not natively support password-protected sessions, so this requires a manual approach. The only tools available for this are the SessionProperties dictionary and the <strong>ConnectionToken</strong> field. The Connection Token is a byte array in StartGameArgs. It can be set by the client and then sent to the Host/Server when a connection is attempted.</p>



<p class="wp-block-paragraph">The Client can send the password to the Host using the ConnectionToken. Never store sensitive data like access passwords in SessionProperties, since all Clients can read and manipulate that data. The Host/ Server must always verify the password to ensure secure validation. If the Client handles the verification, the system loses all reliability. A malicious Client could always claim that the password is correct, completely bypassing your intended access control.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="509" height="142" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-23.png" alt="" class="wp-image-5982" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-23.png 509w, https://theknightsofu.com/wp-content/uploads/2026/04/image-23-300x84.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-23-400x112.png 400w" sizes="auto, (max-width: 509px) 100vw, 509px" /></figure>



<p class="wp-block-paragraph">The Host triggers the OnConnectRequest callback when a player attempts to join a session. The Host can verify the password using the ConnectionToken and decide whether to accept or refuse the connection.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1020" height="535" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-25.png" alt="" class="wp-image-5984" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-25.png 1020w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-300x157.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-768x403.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-400x210.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-600x315.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-800x420.png 800w, https://theknightsofu.com/wp-content/uploads/2026/04/image-25-1000x525.png 1000w" sizes="auto, (max-width: 1020px) 100vw, 1020px" /></figure>



<h3 class="wp-block-heading">How can I tell if a session is password-protected?</h3>



<p class="wp-block-paragraph">This approach works reliably, but it only helps if the Client already knows that the session requires a password. Since there is no built-in password system, the application cannot automatically prompt for that information. The only way to communicate this is by adding a custom field like HasPassword to the SessionProperties. You can use this to display a lock icon in the session list or trigger a password prompt when the player tries to join.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="808" height="216" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-26.png" alt="" class="wp-image-5985" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-26.png 808w, https://theknightsofu.com/wp-content/uploads/2026/04/image-26-300x80.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-26-768x205.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-26-400x107.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-26-600x160.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-26-800x214.png 800w" sizes="auto, (max-width: 808px) 100vw, 808px" /></figure>



<p class="wp-block-paragraph">The main downside is that the Client must have <strong>access to this session metadata in advanc</strong>e. Even if the app doesn’t show the session list in the UI, the Client must fetch session data to check whether it should ask for a password. To enable this, the session must remain visible by setting IsVisible to true. Otherwise, the Client can only join by name and can’t detect if the session requires a password.</p>



<p class="wp-block-paragraph">In such cases, your only option is to show a pop-up asking the player to enter a session name and, optionally, a password. You can include a note telling players they need a password only if the session requires one. However, this information must come from an external source. When the session is not visible, the Client app cannot detect it automatically.</p>



<h2 class="wp-block-heading">Summary and What’s Next?</h2>



<p class="wp-block-paragraph">In the previous article of this series, we introduced Photon Fusion 2 and the network topologies it supports. Also, we discussed how to choose the right one for your game. Now you know how to set up a project ready for multiplayer development. If you missed it, you can read it here: <a href="#" class="">Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4</a>.</p>



<p class="wp-block-paragraph">In this post, you learned how to manage multiplayer sessions using the Host-Client model. We covered the basics of creating and joining sessions, matchmaking modes, and configuring <strong>public, private, and password-protected sessions</strong>. You also became familiar with the most important callbacks from the Fusion API that relate to the connection and session lifecycle.</p>



<p class="wp-block-paragraph">Now that you know how to establish and control a multiplayer session, it’s time to move on to the next key topic: <strong>data synchronisation</strong>. In the next article, we’ll explore how Fusion 2 replicates game state between clients. You’ll learn about synchronisation components, remote procedure calls (RPCs), handling player input, and how to structure your networked properties for efficient replication.</p>



<p class="wp-block-paragraph">Continue reading: <a href="https://theknightsofu.com/photon-fusion-2-data-synchronization-part-3-4/">Photon Fusion 2 &#8211; Data Synchronisation &#8211; Part 3/4</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4</title>
		<link>https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/</link>
		
		<dc:creator><![CDATA[Sir Maciej and Sir Elias]]></dc:creator>
		<pubDate>Tue, 21 Apr 2026 11:49:58 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Basics]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Multiplayer]]></category>
		<category><![CDATA[Photon Fusion 2]]></category>
		<category><![CDATA[Tips]]></category>
		<category><![CDATA[TKoU]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity]]></category>
		<category><![CDATA[architecture]]></category>
		<category><![CDATA[basic configuration]]></category>
		<category><![CDATA[fusion 2]]></category>
		<category><![CDATA[multiplayer]]></category>
		<category><![CDATA[networking]]></category>
		<category><![CDATA[photon]]></category>
		<category><![CDATA[setup]]></category>
		<category><![CDATA[topologies]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5234</guid>

					<description><![CDATA[<p>Ready to build your first multiplayer game, but not sure how to begin? In this article, you&#8217;ll learn how to set up a Unity project using Photon Fusion 2, explore network topologies, and grasp the basics needed to confidently start your multiplayer journey. Whether you&#8217;re completely new to multiplayer or just looking for clear, step-by-step &#8230; <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Ready to build your first <strong>multiplayer game</strong>, but not sure how to begin? In this article, you&#8217;ll learn how to set up a Unity project using <strong>Photon Fusion 2</strong>, explore network topologies, and grasp the basics needed to confidently start your multiplayer journey. Whether you&#8217;re completely new to multiplayer or just looking for clear, step-by-step guidance. Let&#8217;s dive in and get your multiplayer game up and running!</p>



<h2 class="wp-block-heading">Introduction</h2>



<p class="wp-block-paragraph">In this series of posts, I&#8217;ll guide you step-by-step through setting up your project with Photon Fusion 2, creating and managing sessions, and synchronising objects and variables across your scenes. You&#8217;ll learn the <strong>basics of matchmaking and private sessions</strong>. You&#8217;ll see how Fusion manages scene loading and spawning objects, configure and call your first RPCs, and become familiar with key concepts such as State Authority and Input Authority.</p>



<p class="wp-block-paragraph">For this guide, we’ll focus exclusively on the Host-Client architecture, where one player (the Host) acts as the game server. We will also discuss other Fusion architectures.</p>



<p class="wp-block-paragraph">In this first post, we’ll talk about what Photon Fusion 2 is, when and why you&#8217;d want to use it, and highlight its pros and cons. I&#8217;ll walk you through setting up your first Fusion 2 project and integrating it with Unity. Afterwards, we&#8217;ll touch on the <strong>Network Topologies</strong> supported by Fusion 2 and briefly discuss their main characteristics.</p>



<h2 class="wp-block-heading">Photon Fusion 2</h2>



<p class="wp-block-paragraph">Photon Fusion 2 is a high-level, high-performance solution designed to add multiplayer functionality to Unity games. It’s built with simplicity in mind and doesn&#8217;t require extensive low-level networking knowledge to start. Fusion 2 focuses heavily on synchronising game state, employing a variety of techniques you’ll explore in depth throughout this series.</p>



<p class="wp-block-paragraph">Additionally, Fusion 2 supports advanced networking features such as <strong>Lag Compensation</strong>, <strong>Rollback</strong>, and <strong>Area of Interest</strong>. It seamlessly handles various network topologies.</p>



<p class="wp-block-paragraph">Fusion 2 integrates smoothly with third-party hosting solutions like Unity Game Services and Unity Multiplay. It also provides built-in basic matchmaking capabilities, but doesn&#8217;t include typical backend services like player accounts, leaderboards, databases, or cloud code execution.</p>



<p class="wp-block-paragraph">This makes Fusion 2 ideal for rapid prototyping, gameplay testing, and multiplayer development for both small and large projects. However, it&#8217;s important to remember that Fusion 2 integrates closely with your game&#8217;s logic, which can complicate transitioning to another networking solution later.</p>



<p class="wp-block-paragraph">Photon Fusion 2 offers a highly developer-friendly experience, complete with detailed documentation and numerous sample projects, helping you understand and incorporate best practices directly into your game.</p>



<h2 class="wp-block-heading">Photon Fusion 2 Pre-Requirements</h2>



<p class="wp-block-paragraph">At the time of writing this post, the latest Photon Fusion 2 SDK is <strong>2.0.12 Stable Build 1861</strong>, released <strong>March 19, 2026</strong>. The minimum Unity version required for Photon Fusion 2 is <strong>2021.3.<strong>45</strong>/2022.3.x/6.0.x </strong>or <strong>newer</strong>.</p>



<p class="wp-block-paragraph">In the <strong>Project Setup</strong> chapter, you&#8217;ll learn how to create a new Photon Fusion 2 project and set up all the packages you&#8217;ll need to start working. Everything covered in this post is based on free solutions &#8211; you’ll need to create a Photon account, but it’s completely free.</p>



<p class="wp-block-paragraph">Check out the latest Photon Fusion 2 release and release notes <a href="https://doc.photonengine.com/fusion/current/getting-started/sdk-download">here</a>.</p>



<h2 class="wp-block-heading">Photon Fusion 2 Project Setup</h2>



<h3 class="wp-block-heading">Photon Dashboard</h3>



<p class="wp-block-paragraph">To start your adventure with Fusion 2, you&#8217;ll first need a <strong>Photon Engine account</strong> to set up a dedicated project for your game. If you don&#8217;t have an account yet, click <a href="https://id.photonengine.com/account/signup">this link</a> and create one. If you already have one, head over to the <a href="https://dashboard.photonengine.com/">Photon Dashboard</a> and set up your first Fusion 2 application to get started!</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="239" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1024x239.png" alt="" class="wp-image-5950" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1024x239.png 1024w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-300x70.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-768x179.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1536x359.png 1536w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-400x93.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-600x140.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-800x187.png 800w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1000x234.png 1000w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1280x299.png 1280w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1-1520x355.png 1520w, https://theknightsofu.com/wp-content/uploads/2026/04/image-1.png 1708w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Next, click <strong>CREATE A NEW APP</strong> to create your new application. Fill out the brief form by selecting the recommended Fusion SDK version 2, give your application a name, optionally provide a description and a website link, then click <strong>CREATE</strong>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="550" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-2-1024x550.png" alt="" class="wp-image-5951" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-2-1024x550.png 1024w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-300x161.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-768x412.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-400x215.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-600x322.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-800x429.png 800w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-1000x537.png 1000w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2-1280x687.png 1280w, https://theknightsofu.com/wp-content/uploads/2026/04/image-2.png 1364w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">Your application will appear in the <strong>Applications / Public Cloud</strong> section. Right now, the most important detail for you is your application&#8217;s <strong>App ID</strong>, which you can copy by clicking on the <strong>App ID</strong> field on your app’s tile. Your app is currently on the free plan, with a limit of<strong> 20 CCU (Concurrently Connected Users)</strong>. You can read more about CCU and Photon subscriptions <a href="https://www.photonengine.com/fusion/pricing">here</a>, but we won&#8217;t dive deeper into those topics in this post.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="807" height="392" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-3.png" alt="" class="wp-image-5952" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-3.png 807w, https://theknightsofu.com/wp-content/uploads/2026/04/image-3-300x146.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-3-768x373.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-3-400x194.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-3-600x291.png 600w, https://theknightsofu.com/wp-content/uploads/2026/04/image-3-800x389.png 800w" sizes="auto, (max-width: 807px) 100vw, 807px" /></figure>



<h3 class="wp-block-heading">Unity Project</h3>



<p class="wp-block-paragraph">We&#8217;re now ready to create a new Unity project and integrate the Fusion 2 SDK. Download the latest Fusion 2<code><em><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-gray-600-color">.unitypackage</mark></em></code> from <a href="https://doc.photonengine.com/fusion/current/getting-started/sdk-download">this link</a>, and import it into Unity via the <strong>Assets → Import Package → Custom Package</strong> menu. After a successful import, you should see the <strong>Photon Fusion 2 Hub</strong>, your main management panel. If it doesn’t pop up automatically, you can always find it in the top menu at <strong>Tools → Fusion → Fusion Hub</strong>.</p>



<p class="wp-block-paragraph">The Photon Fusion 2 Hub lets you manage the plugin’s configuration. Under the <strong>Fusion 2 Setup</strong> tab, you’ll find scriptable objects for configuring detailed connection and data-transfer settings. Photon&#8217;s default settings are usually good enough for most scenarios, but if you&#8217;re curious about advanced options, you can explore them in the documentation <a href="https://doc.photonengine.com/fusion/current/manual/network-project-config">here</a>.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="786" height="533" src="https://theknightsofu.com/wp-content/uploads/2026/04/image-4.png" alt="" class="wp-image-5953" srcset="https://theknightsofu.com/wp-content/uploads/2026/04/image-4.png 786w, https://theknightsofu.com/wp-content/uploads/2026/04/image-4-300x203.png 300w, https://theknightsofu.com/wp-content/uploads/2026/04/image-4-768x521.png 768w, https://theknightsofu.com/wp-content/uploads/2026/04/image-4-400x271.png 400w, https://theknightsofu.com/wp-content/uploads/2026/04/image-4-600x407.png 600w" sizes="auto, (max-width: 786px) 100vw, 786px" /></figure>



<p class="wp-block-paragraph">At this point, paste the <strong>App ID</strong> generated earlier from the Dashboard into the appropriate field. If the App ID you&#8217;ve pasted is valid, the icon will switch from red to green. Now you&#8217;re fully set up and ready to dive into Fusion 2!</p>



<p class="wp-block-paragraph">Your project setup is now complete. Before we jump into the technical details and start implementing, let’s go over the network topologies available in Fusion 2. It’s important to understand these concepts because choosing the right approach is critical for your project. Making the wrong choice here might become very expensive to correct later on.</p>



<h2 class="wp-block-heading">Supported Network Topologies</h2>



<p class="wp-block-paragraph">You&#8217;re ready to create your first multiplayer game &#8211; exciting, right? But remember, you have some important decisions to make first, including choosing the right Network Topology. Let&#8217;s go through the three network topologies in Photon Fusion 2, highlight their pros and cons, and help you pick the right one for your project.<br><br>You may ask &#8211; what is a network topology? It is simply an organised structure of elements that can communicate with each other over the network. It can describe their physical arrangement and, what is more important to us here, it can also describe the hierarchy of those elements, their responsibilities, and the way they communicate with each other. You may have also seen the term <strong>Network Architecture</strong>. While not exactly the same, it is quite similar. Here, I use the term Network Topology to avoid confusion with the Photon Fusion 2 documentation.</p>



<p class="wp-block-paragraph">In this series of posts, we won&#8217;t dive deeply into basic networking concepts like communication protocols, connection setup, NAT, IP, or data serialisation. As mentioned earlier, Photon Fusion 2 is a high-level solution designed to give you a reliable and efficient framework. You can focus on developing game features instead of dealing with low-level networking details.</p>



<p class="wp-block-paragraph">When thinking about multiplayer games, the first word that probably comes to mind is &#8220;Server&#8221;. That&#8217;s good &#8211; Servers are critical to multiplayer systems, though not always strictly necessary. For simplicity’s sake, when we say &#8220;Server&#8221; here, we mean a <strong>Game Server</strong>.</p>



<h3 class="wp-block-heading">Dedicated Server</h3>



<p class="wp-block-paragraph">The first topology we’ll cover is the <strong>Dedicated Server</strong>. This is usually what players imagine when thinking about multiplayer and &#8220;connecting to a server&#8221;. A Dedicated Server is a physical machine or a cloud-hosted virtual machine running your Game Session. Players &#8211; known as <strong>Clients</strong> &#8211; connect to this server to join the game. While this topology is powerful and comprehensive, its major downside is the relatively high maintenance cost.</p>



<p class="wp-block-paragraph">The basic concept behind this topology is that the entire game runs securely on the Dedicated Server, while Clients send inputs to control their actions in the game. The Server holds <strong>State Authority</strong>, meaning it exclusively manages game-state changes, data synchronisation, and object updates. Clients have Input Authority &#8211; they can request the Server to execute actions like moving characters, picking up items, driving vehicles, or firing weapons, but cannot modify the game state. They can only change the local state, but none of the other players will be affected by it.<br><br>Dedicated servers are usually managed and paid for by the developer/publisher. They can also be hosted by the players themselves if the game supports it. This transfers the cost of maintenance to users and gives them more freedom, like the possibility to mod the game. The disadvantage is that you need to be an advanced player to be able to set up the server correctly.</p>



<h4 class="wp-block-heading">Pros:</h4>



<ul class="wp-block-list">
<li><strong>Full Server Authority:</strong> The server fully controls the game state, significantly reducing cheating and malicious activity.</li>



<li><strong>High Stability:</strong> Server hosting from reputable providers is reliable and comes with solid technical support.</li>



<li><strong>Easy Scalability:</strong> Easily scales up to handle more players and sessions if your game becomes popular.</li>



<li><strong>Independent of Client Hardware Performance:</strong> Gameplay performance and quality don&#8217;t depend on player hardware.</li>



<li><strong>Simple Game Management:</strong> Easy deployment of server updates without always needing client-side updates, making balancing and testing easier.</li>



<li><strong>Regional Hosting:</strong> Hosting servers across multiple regions helps reduce latency and evenly distribute traffic.</li>



<li><span style="box-sizing: border-box; margin: 0px; padding: 0px;"><strong>Wide range of games supported:</strong>&nbsp;Server can run even when there is no player currently connected, which can support gameplay that may not be possible otherwise.</span></li>
</ul>



<h4 class="wp-block-heading">Cons:</h4>



<ul class="wp-block-list">
<li><strong>High Costs:</strong> Dedicated server hosting is usually expensive and might not be available for smaller projects.</li>



<li><strong>Technical Complexity:</strong> Requires more expertise for proper configuration and management.</li>
</ul>



<h3 class="wp-block-heading">Host-Client</h3>



<p class="wp-block-paragraph">If a Dedicated Server is too costly or impractical for your project, you should consider the <strong>Host-Client topology</strong>. In this setup, one player (typically the first one) serves as the Host, acting simultaneously as both the Server and a Client. Usually, this player initiates the session, creates a game, and waits for or invites other players to join.</p>



<p class="wp-block-paragraph">The Host holds full <strong>State Authority</strong>, giving them considerable control and potentially enabling them to cheat. Other Clients have only <strong>Input Authority</strong>, similar to Clients in a Dedicated Server scenario. This topology is best suited for smaller-scale projects with short sessions involving a few players. Keep in mind that if the Host disconnects or crashes, the session will be terminated immediately unless <strong>Host Migration</strong> is implemented.</p>



<p class="wp-block-paragraph">A significant advantage of the Host-Client model is its simplicity. It enables rapid prototyping and iteration. It&#8217;s easy to set up, doesn&#8217;t require advanced hosting services or additional server infrastructure. <br><br>What may come as a surprise is that an additional server, called a relay server, may still be required. While the exact reasons for that are out of the scope of this blog, it turns out that it is not that simple to establish a direct connection between two players. If you are interested, you can read more by searching for the <strong>NAT traversal problem</strong>. All you need to know is that you don&#8217;t have to worry about such a server; Photon Fusion 2 does it for you.</p>



<h4 class="wp-block-heading">Pros:</h4>



<ul class="wp-block-list">
<li><strong>Low Operational Costs:</strong> No dedicated server needed, drastically reducing costs.</li>



<li><strong>Ease of Implementation:</strong> Quick to set up, ideal for rapid development.</li>



<li><strong>Suitable for Non-competitive Games:</strong> Excellent for sessions with friends, co-op or casual multiplayer experiences.</li>
</ul>



<h4 class="wp-block-heading">Cons:</h4>



<ul class="wp-block-list">
<li><strong>Potential Cheating:</strong> Host has significant control, allowing him to easily cheat.</li>



<li><strong>Poor for Mobile Games:</strong> Host device limitations and mobile network throughput can negatively affect performance for all clients.</li>



<li><strong>Unequal Latency:</strong> Host experiences zero lag compared to Clients. It gives him an advantage that could be considered unfair.</li>



<li><span style="box-sizing: border-box; margin: 0px; padding: 0px;"><strong>Limited Scalability:</strong>&nbsp;Performance is tied directly to the host&#8217;s hardware and can&#8217;t handle as many players </span>or as complex simulation as a dedicated server.</li>
</ul>



<p class="wp-block-paragraph">When designing your game, you&#8217;ll likely have to choose whether it supports single-player, multiplayer, or both. Initially, this decision may seem straightforward since your game&#8217;s concept usually dictates whether it supports solo play or multiple players competing or cooperating. However, upon closer inspection, many games benefit significantly from offering both modes.</p>



<p class="wp-block-paragraph">For instance, you might want a strong narrative focus best suited to single-player, or you might include a tutorial that guides players through game mechanics step-by-step. Another consideration is player base size &#8211; if your game is new and still growing, multiplayer matchmaking may suffer, discouraging players with long wait times. Offering a single-player alternative can help address this issue.</p>



<p class="wp-block-paragraph">The Host-Client topology readily <strong>supports single-player modes</strong>. Fusion 2 is built in such a way that it&#8217;s simple to start a single-player session without an internet connection. From a developer&#8217;s point of view, this single-player session functions almost identically to a multiplayer session hosted by a player, just without additional clients joining.</p>



<h3 class="wp-block-heading">Shared</h3>



<p class="wp-block-paragraph">The Shared topology is a <strong>decentralised networking model</strong> where no single player acts as a Host. Instead, each player has State Authority over the objects they create. By default, a player owns and controls any objects they spawn.</p>



<p class="wp-block-paragraph">One of the core features of this model is the ability to dynamically transfer State Authority, though only the current authority holder can initiate the transfer. The system also introduces a special role called the <strong>SharedModeMasterClient</strong> &#8211; typically the first player to join a session. This player receives State Authority over all scene objects present at the start (unless reassigned), manages scene loading across all clients, and can despawn objects not owned by anyone. It might seem similar to a host, but here this player has fewer privileges and responsibilities.</p>



<p class="wp-block-paragraph">In Shared mode, inputs are applied instantly and locally, and only the resulting changes are synchronised. This results in zero input lag and a responsive player experience, but at the cost of reduced predictability when both players interact. Additionally, this topology is <strong>highly resilient</strong>. By default, if one player disconnects, it doesn&#8217;t affect the rest of the session (though some games may not be able to continue due to their design). There’s no central failure point as with Host or Dedicated Server models.</p>



<p class="wp-block-paragraph">Another strength of the Shared mode is load balancing. By reassigning State Authority, developers can ensure that weaker devices simulate fewer objects, while more capable machines handle more.</p>



<p class="wp-block-paragraph">However, the <strong>biggest trade-off is security</strong>. This topology is extremely vulnerable to cheating. Any player with authority over an object can modify it without any restriction. As such, it is only recommended for non-competitive games, preferably those played with friends or in trusted environments.</p>



<h4 class="wp-block-heading">Pros:</h4>



<ul class="wp-block-list">
<li><strong>Low cost</strong>: No dedicated server required.</li>



<li><strong>Zero input lag</strong>: Input is applied instantly on the local machine.</li>



<li><strong>Quick setup</strong>: Easy to prototype multiplayer functionality.</li>



<li><strong>Flexible authority management</strong>: State Authority can be transferred dynamically.</li>



<li><strong>Fault-tolerant</strong>: No single point of failure.</li>



<li><strong>For casual games:</strong> Ideal for casual, co-op games and internal development/testing.</li>
</ul>



<h4 class="wp-block-heading">Cons:</h4>



<ul class="wp-block-list">
<li><strong>Higher network traffic:</strong> Especially in sessions with many players.</li>



<li><strong>Completely insecure:</strong> Easy for any player to cheat.</li>



<li><strong>Not for competitive games</strong>: Due to the cheating, it is unsuitable for competitive environments.</li>
</ul>



<h3 class="wp-block-heading">Photon Fusion 2 Topologies &#8211; how to choose the correct one?</h3>



<p class="wp-block-paragraph">By now, you understand the essentials of different Photon Fusion 2 topologies. You&#8217;ve also learned how crucial choosing the right topology is, and why carefully weighing your decision early can save you significant effort and costs down the road. The later you make changes, the higher the costs. To help guide your decision-making, consider these important questions:</p>



<ul class="wp-block-list">
<li><strong>What gameplay modes will your game support?</strong></li>



<li><strong>Is your game competitive?</strong></li>



<li><strong>Is preventing cheating a top priority for your game?</strong></li>



<li><strong>Does your game require a continuously simulated world that operates independently of player presence?</strong></li>



<li><strong>Is your game performance-intensive on player devices or on the network throughput?</strong></li>



<li><strong>Can you handle the complexities and costs associated with running a Dedicated Server?</strong></li>



<li><strong>Do you want players to be able to host their own dedicated servers?</strong></li>
</ul>



<p class="wp-block-paragraph">Keep these key considerations in mind as you plan your game&#8217;s architecture. Consider structuring your core gameplay logic to easily support both multiplayer and single-player modes. Photon Fusion 2 offers built-in support for designing logic that&#8217;s reusable across multiple game modes.</p>



<h2 class="wp-block-heading">Summary and What’s Next?</h2>



<p class="wp-block-paragraph">In this first article of the series, you&#8217;ve learned what Photon Fusion 2 is all about, created your Photon account, and set up your project in the Photon Dashboard and Unity. You&#8217;re now equipped with essential knowledge about available network topologies and ready to start building!</p>



<p class="wp-block-paragraph">In the next article, we&#8217;ll explore how to create sessions and connect players using the <strong>Host-Client topology</strong>.</p>



<p class="wp-block-paragraph">Continue reading: <a href="https://theknightsofu.com/photon-fusion-2-sessions-management-part-2-4/">Photon Fusion 2 &#8211; Sessions Management &#8211; Part 2/4</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/photon-fusion-2-introduction-part-1-4/">Photon Fusion 2 &#8211; Introduction &#8211; Part 1/4</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Render Graph &#8211; Creative Shadowcasting</title>
		<link>https://theknightsofu.com/render-graph-creative-shadowcasting/</link>
					<comments>https://theknightsofu.com/render-graph-creative-shadowcasting/#respond</comments>
		
		<dc:creator><![CDATA[Sir Johny]]></dc:creator>
		<pubDate>Fri, 11 Jul 2025 09:03:40 +0000</pubDate>
				<category><![CDATA[Advanced]]></category>
		<category><![CDATA[All]]></category>
		<category><![CDATA[Rendering]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[Deferred]]></category>
		<category><![CDATA[GBuffer]]></category>
		<category><![CDATA[Graphics]]></category>
		<category><![CDATA[Rain Effect]]></category>
		<category><![CDATA[Render Graph]]></category>
		<category><![CDATA[Shadowcasting]]></category>
		<category><![CDATA[Shadows]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[unity 6]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5539</guid>

					<description><![CDATA[<p>In this article, I modify my rain atmosphere feature to keep surfaces dry when they are under some form of cover. To achieve this, I will utilize a shadowcasting technique. The roofs of the buildings will cast a shadow, which I will use to modulate the intensity of the effect. Contents: How the effect works &#8230; <a href="https://theknightsofu.com/render-graph-creative-shadowcasting/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/render-graph-creative-shadowcasting/">Render Graph &#8211; Creative Shadowcasting</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In this article, I modify my <a href="https://theknightsofu.com/render-graph-polishing-rain-atmosphere/">rain atmosphere feature</a> to keep surfaces dry when they are under some form of cover. To achieve this, I will utilize a shadowcasting technique. The roofs of the buildings will cast a shadow, which I will use to modulate the intensity of the effect.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="848" height="665" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase.jpg" alt="" class="wp-image-5542" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase.jpg 848w, https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase-300x235.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase-768x602.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase-400x314.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase-600x471.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-effect-showcase-800x627.jpg 800w" sizes="auto, (max-width: 848px) 100vw, 848px" /><figcaption class="wp-element-caption">This is the final result</figcaption></figure>



<h2 class="wp-block-heading">Contents:</h2>



<ol class="wp-block-list">
<li>Explaining how the effect works by this point.</li>



<li>Explaining shadowcasting technique.</li>



<li>Implementing shadowcasting to modulate the effect.</li>
</ol>



<h2 class="wp-block-heading">How the effect works</h2>



<p class="wp-block-paragraph">The effect was implemented in the deferred rendering path by modifying the GBuffer contents before the lighting was calculated. The effect reduces the albedo and GI and then increases the smoothness to mimic the wet look.</p>



<p class="wp-block-paragraph">This is how the lighting initially worked: Unity was rendering a GBuffer, which was then used by the lighting algorithm to produce a color buffer.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="677" height="257" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-original-deferred-graph.jpg" alt="" class="wp-image-5543" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-original-deferred-graph.jpg 677w, https://theknightsofu.com/wp-content/uploads/2025/06/004-original-deferred-graph-300x114.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-original-deferred-graph-400x152.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-original-deferred-graph-600x228.jpg 600w" sizes="auto, (max-width: 677px) 100vw, 677px" /><figcaption class="wp-element-caption">This is how the Unity&#8217;s render graph nodes look like. Gbuffer is fed to the deferred lighting pass.</figcaption></figure>



<p class="wp-block-paragraph">The rain atmosphere effect works by modifying a GBuffer just before the Deferred Lighting pass. It copies GBuffer content using the<code>InitializeRainResources</code> node, and then creates a shared resource <code>RainResourceData</code> that holds copy of the GBuffer.</p>



<p class="wp-block-paragraph"><code>ApplyRainAtmosphere</code> modifies the original GBuffer to increase smoothness and reduce global illumination and albedo, making surfaces appear more wet. The effect is applied using a full-screen shader, similar to a post-processing effect.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="264" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1024x264.jpg" alt="" class="wp-image-5544" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1024x264.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-300x77.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-768x198.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1536x396.jpg 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-400x103.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-600x155.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-800x206.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1000x258.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1280x330.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph-1520x392.jpg 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-effect-graph.jpg 1811w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Current implementation of the effect.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1022" height="615" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect.jpg" alt="" class="wp-image-5545" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect.jpg 1022w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-300x181.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-768x462.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-400x241.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-600x361.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-800x481.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-current-implementation-of-effect-1000x602.jpg 1000w" sizes="auto, (max-width: 1022px) 100vw, 1022px" /><figcaption class="wp-element-caption">Current implementation of the effect.</figcaption></figure>



<h2 class="wp-block-heading">Shadowcasting technique</h2>



<p class="wp-block-paragraph">I want to modify the effect to make the surfaces look dry when they are under any cover. I want to use the shadowcasting technique for that.</p>



<p class="wp-block-paragraph">Classic shadowcasting works by rendering the depth of the scene from the light point-of-view. This depth texture rendered by the light is called <strong>Shadowmap</strong>. Then, the lighting algorithm compares the rendered surface distance to the light with the distance saved in the depth rendered by the light to calculate the shadow.</p>



<p class="wp-block-paragraph">In my case, I like to interpret the falling rain as a directional light that emits &#8220;wetness&#8221; instead of light. Let&#8217;s see how we can implement that using this image.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="524" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-1024x524.jpg" alt="" class="wp-image-5546" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-1024x524.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-300x154.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-768x393.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-400x205.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-600x307.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-800x410.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1-1000x512.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation1.jpg 1037w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">1. Rendering depth from the light point-of-view</h3>



<p class="wp-block-paragraph">I will place the camera at the light source. This camera will render a depth-only scene. The created texture is called a shadowmap.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="517" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-1024x517.jpg" alt="" class="wp-image-5547" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-1024x517.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-300x151.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-768x388.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-400x202.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-600x303.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-800x404.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2-1000x505.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation2.jpg 1038w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Vertical lines represent the depth values stored within the shadowmap.</figcaption></figure>



<h3 class="wp-block-heading">2. The lighting algorithm uses shadowmap to estimate shadow</h3>



<p class="wp-block-paragraph">The lighting algorithm uses the shadowmap to calculate the shadows when rendering with the main camera.</p>



<p class="wp-block-paragraph">When calculating the lighting for any surface, the algorithm compares the distance to the light source with the distance saved in the shadowmap.</p>



<p class="wp-block-paragraph">If the distance to the light source is smaller or close to the distance saved in the shadowmap &#8211; there is no shadow.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="560" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-1024x560.jpg" alt="" class="wp-image-5548" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-1024x560.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-300x164.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-768x420.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-400x219.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-600x328.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-800x438.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3-1000x547.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation3.jpg 1040w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">The distance to a light source matches the distance saved in the shadow map &#8211; there is a light.</figcaption></figure>



<p class="wp-block-paragraph">Furthermore, when the distance from the surface to the light is larger than the distance saved in the shadowmap &#8211; there is a shadow.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="548" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-1024x548.jpg" alt="" class="wp-image-5549" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-1024x548.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-300x161.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-768x411.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-400x214.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-600x321.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-800x428.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4-1000x535.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-explanation-4.jpg 1054w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">The distance to the light source is larger than the distance stored in the shadowmap &#8211; there is a shadow.</figcaption></figure>



<p class="wp-block-paragraph">To make this technique possible, we need a way to convert world space position into shadowmap UV. This can be done using the view and projection matrices of the camera that was used to render the shadowmap.</p>



<p class="wp-block-paragraph">However, if we do this in the postprocess, we need to reconstruct the world space position from the main camera&#8217;s depth buffer because the world space position is not stored within the GBuffer.</p>



<h2 class="wp-block-heading">Implementing shadowcasting to modulate the rain effect</h2>



<p class="wp-block-paragraph">To implement the shadowcasting, I need to get this data into the shader that applies the effect:</p>



<ol class="wp-block-list">
<li>Main camera depth buffer &#8211; to reconstruct world space position from depth.</li>



<li>View and projection matrices used to render the camera &#8211; used to convert world space position into shadowmap UV</li>



<li>Shadowmap texture.</li>
</ol>



<p class="wp-block-paragraph">I will divide the implementation into a few steps:</p>



<ol class="wp-block-list">
<li>Configuring a camera that will render a shadowmap.</li>



<li>Modifying the render feature &#8211; forwarding the rendered shadowmap into the shader.</li>



<li>Implement shadowcasting in the shader.</li>
</ol>



<h3 class="wp-block-heading">1. Configuring shadowmap camera</h3>



<p class="wp-block-paragraph">I created a new camera and placed it on the scene. I set the camera to render orthographic and made it render top-down, capturing most of the scene.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="524" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-1024x524.jpg" alt="" class="wp-image-5550" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-1024x524.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-300x154.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-768x393.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-400x205.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-600x307.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-800x410.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-1000x512.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera.jpg 1160w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">The camera overlaps most of the scene content.</figcaption></figure>



<p class="wp-block-paragraph">Then I created a <code>RainCamera</code> component. This component is responsible for creating a shadowmap texture and assigning it to the camera.</p>



<p class="wp-block-paragraph">The component creates a shadowmap texture using a depth-only texture format. Then, it renders the camera into the shadow map and saves the parameters for further use in the render feature.</p>



<p class="wp-block-paragraph">The <code>RainCameraParameters</code> will be used in the render feature to render the effect.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.Rendering;

&#x5B;RequireComponent(typeof(Camera))]
&#x5B;ExecuteAlways]
public class RainCamera : MonoBehaviour
{
    // Keeping track of all created instances.
    public static List&lt;RainCamera&gt; AllInstances { get; private set; } = new();

    // Main instance - last registered one.
    public static RainCamera MainInstance =&gt; AllInstances.LastOrDefault();

    // All parameters that define the shadowmap. Those will be used in the render feature.
    public RainCameraParameters Parameters { get; private set; }

    private void OnEnable()
    {
        // Registering the new instance.
        AllInstances.Add(this);

        // Create a shadowmap texture
        RenderTexture ShadowmapTexture = new RenderTexture(1024, 1024, GraphicsFormat.None, GraphicsFormat.D32_SFloat, 0);
        ShadowmapTexture.name = $&quot;{gameObject.name}_ShadowmapTexture&quot;; // Assigning the name makes debugging easier.

        // Render the shadowmap
        var camera = GetComponent&lt;Camera&gt;();
        camera.targetTexture = ShadowmapTexture;
        camera.Render();

        // I&#039;m assuming that the rendered environment is static, so the camera can be disabled after the first render.
        camera.enabled = false; 

        // Save parameters of the shadowmap. Those will be used in the render feature.
        Parameters = new RainCameraParameters()
        {
            // Matrices are required to read and convert the world space position to the UV of the shadowmap.
            worldToCameraMatrix = camera.worldToCameraMatrix,
            projectionMatrix = camera.nonJitteredProjectionMatrix,

            // Shadowmap will be sampled in the shader.
            shadowmapTexture = ShadowmapTexture,
        };
    }

    private void OnDisable()
    {
        // Unregistering the instance.
        AllInstances.Remove(this);

        // Release resources allocated in the OnEnable method.
        Parameters.shadowmapTexture.Release();
    }
}

// All parameters that define the rain camera. 
public struct RainCameraParameters
{
    public Matrix4x4 worldToCameraMatrix;
    public Matrix4x4 projectionMatrix;
    public RenderTexture shadowmapTexture;
}
</pre></div>


<p class="wp-block-paragraph">I assigned the component to the created camera. I used a Frame Debugger to see if the shadowmap texture renders properly. After enabling the camera in the inspector, the shadowmap looks correct in the FrameDebugger.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="650" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-1024x650.jpg" alt="" class="wp-image-5552" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-1024x650.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-300x190.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-768x487.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-400x254.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-600x381.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-800x508.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger-1000x635.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-rain-camera-frame-debugger.jpg 1232w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Camera properly renders the scene content, checked in the Frame Debugger window.</figcaption></figure>



<h3 class="wp-block-heading">2. Modifying the render feature</h3>



<p class="wp-block-paragraph">Now, I need to find a way to bind this shadowmap and camera depth buffer to the shader that renders the effect.</p>



<p class="wp-block-paragraph">Initialy I used <code>InitializeRainResourcesPass</code> to prepare all the resources used by the effect, so it looks like a perfect place to set shadowmap parameters.</p>



<p class="wp-block-paragraph"><code>RainResourceData</code> is a class that shares all the resources of the effect with other passes. I decided to add a shadowmap parameters here.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class RainResourceData : ContextItem
{
    public TextureHandle normalSmoothnessTexture { get; internal set; }

    // Added shadowmap camera parameters here.
    public RainCameraParameters? rainCameraParameters { get; internal set; } = null;

    public override void Reset()
    {
        normalSmoothnessTexture = TextureHandle.nullHandle;

        // Resetting it here.
        rainCameraParameters = null;
    }
}
</pre></div>


<p class="wp-block-paragraph">If the <code>RainCamera</code> component is present on a scene, I set the parameters for other passes.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public class InitializeRainResourcesPass : ScriptableRenderPass
{
...
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        RainResourceData rainResources = frameData.GetOrCreate&lt;RainResourceData&gt;();

        var urpResources = frameData.Get&lt;UniversalResourceData&gt;();
        var gBuffer = urpResources.gBuffer;

        if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#x5B;0].Equals(TextureHandle.nullHandle))
            return;

        rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer&#x5B;2].GetDescriptor(renderGraph));

        // If a rain camera is present, share its parameters with other passes.
        RainCamera mainRainCamera = RainCamera.MainInstance;
        if (mainRainCamera != null)
            rainResources.rainCameraParameters = mainRainCamera.Parameters;

        using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
        {
...
</pre></div>


<p class="wp-block-paragraph">Then I moved to the <code>ApplyRainAtmospherePass</code>, where all rendering is implemented. I need to do two things here:</p>



<ul class="wp-block-list">
<li>Forward the shadowmap parameters to the shader.</li>



<li>Forward the camera depth buffer to the shader.</li>
</ul>



<p class="wp-block-paragraph">I modified the <code>PassData</code> that stores resources used in the render function.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
internal class PassData
{
	internal Material material;
	internal TextureHandle gBuffer2;

	// Added camera depth handle to use it in the render function.
	internal TextureHandle cameraDepthTexture;

	// Added shadowmap parameters to use it in the render function.
	internal RainCameraParameters? rainCameraParameters;
}
</pre></div>


<p class="wp-block-paragraph">Then, I modified the render pass that applies the rain effect. I accessed the shared resources and forwarded them into the render function. I marked new lines using comments.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        UniversalResourceData urpResources = frameData.Get&amp;lt;UniversalResourceData&gt;();
        TextureHandle&#x5B;] gBuffer = urpResources.gBuffer;

        // Accessing the active depth buffer from the camera
        TextureHandle depthBuffer = urpResources.activeDepthTexture;

        if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#x5B;0].Equals(TextureHandle.nullHandle))
            return;

        RainResourceData rainResources = frameData.Get&amp;lt;RainResourceData&gt;();

        using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
        {
            builder.UseTexture(rainResources.normalSmoothnessTexture);

            // Declare the usage of the depth buffer to avoid render graph resource stripping.
            // Shadowmap is always present in the memory, so it&#039;s usage doesn&#039;t need to be declared.
            builder.UseTexture(depthBuffer);

            passData.gBuffer2 = rainResources.normalSmoothnessTexture;
            passData.material = material;

            // Forwarding camera&#039;s depth texture to the render function;
            passData.cameraDepthTexture = depthBuffer;

            // Forwarding rain camera parameters to the render function
            passData.rainCameraParameters = rainResources.rainCameraParameters;

            for (int i = 0; i &amp;lt; gBuffer.Length; i++)
                builder.SetRenderAttachment(gBuffer&#x5B;i], i, AccessFlags.Write);

            builder.SetRenderFunc((PassData passData, RasterGraphContext context) =&gt; RenderFunction(passData, context));
        }
    }
</pre></div>


<p class="wp-block-paragraph">Then, I modified the render function to set all the resources in the shader. I used MaterialPropertyBlock from the render graph pool.</p>



<p class="wp-block-paragraph"><code>_CameraDepthTexture</code> &#8211; Game camera&#8217;s depth texture<br><code>_RainCamera_Shadowmap</code> &#8211; Rendered shadowmap<br><code>_RainCamera_MatrixV</code>, <code>_RainCamera_MatrixP</code> &#8211; view and projection matrices of the camera that rendered a shadowmap (required to convert world space position to shadowmap UV).</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: csharp; title: ; notranslate">
public static class Uniforms
{
    internal static readonly int _GBuffer2 = Shader.PropertyToID(nameof(_GBuffer2));

    // I added those uniform constants.
    internal static readonly int _CameraDepthTexture = Shader.PropertyToID(nameof(_CameraDepthTexture));
    internal static readonly int _RainCamera_Shadowmap = Shader.PropertyToID(nameof(_RainCamera_Shadowmap));
    internal static readonly int _RainCamera_MatrixV = Shader.PropertyToID(nameof(_RainCamera_MatrixV));
    internal static readonly int _RainCamera_MatrixP = Shader.PropertyToID(nameof(_RainCamera_MatrixP));
}
</pre></div>

<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
	propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer2);

	// Setting camera depth buffer.
	propertyBlock.SetTexture(Uniforms._CameraDepthTexture, passData.cameraDepthTexture);

	// If a rain camera exists, set up shader uniforms.
	if (passData.rainCameraParameters.HasValue)
	{
		propertyBlock.SetTexture(Uniforms._RainCamera_Shadowmap, passData.rainCameraParameters.Value.shadowmapTexture);
		propertyBlock.SetMatrix(Uniforms._RainCamera_MatrixV, passData.rainCameraParameters.Value.worldToCameraMatrix);
		Matrix4x4 projectionMatrix = GL.GetGPUProjectionMatrix(passData.rainCameraParameters.Value.projectionMatrix, true);
		propertyBlock.SetMatrix(Uniforms._RainCamera_MatrixP, projectionMatrix);
	}
	else
	{
		// Otherwise, if the rain camera is not present, set all uniforms to default
		propertyBlock.SetTexture(Uniforms._RainCamera_Shadowmap, context.defaultResources.defaultShadowTexture);
		propertyBlock.SetMatrix(Uniforms._RainCamera_MatrixV, Matrix4x4.identity);
		propertyBlock.SetMatrix(Uniforms._RainCamera_MatrixP, Matrix4x4.identity);
	}

	context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}
</pre></div>


<h3 class="wp-block-heading">3. Implementing shadowcasting in the shader</h3>



<p class="wp-block-paragraph">After all the required matrices and textures are forwarded to the, I need to implement the shadowcasting inside the shader. There are a few steps to be done:</p>



<ol class="wp-block-list">
<li>Declare all resources and uniforms in the shader.</li>



<li>Reconstruct world space position from the depth buffer.</li>



<li>Calculate shadowmap UV from the world space position.</li>



<li>Calculate the rain-camera depth value using the world space position.</li>



<li>Compare the calculated depth with the value in the shadowmap.</li>
</ol>



<h4 class="wp-block-heading">3.2 Reconstruct world space position from the depth buffer</h4>



<p class="wp-block-paragraph">Now, I need to get the world space position that I could use in the shadowcasting algorithm. Because GBuffer doesn&#8217;t contain world space position, I need to derive it from the value in the depth buffer.</p>



<p class="wp-block-paragraph">The goal is to sample a raw depth buffer value. Then, I will reconstruct the clip space position of the game camera using the screen UV and this raw depth value. Clip space position can be converted into a world space position using the inversed view-projection matrices.</p>



<p class="wp-block-paragraph">This is how I read the depth value from the depth buffer.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
// Get raw depth buffer value
float rawScreenDepth = _CameraDepthTexture.SampleLevel(pointClampSampler, IN_uv, 0.0f).r;
</pre></div>


<p class="wp-block-paragraph">Then I can reconstruct the clip space position using the UV and the raw depth value.</p>



<p class="wp-block-paragraph">The screen UV range is from [0,1], and clip space XY is in [-1,1]. I just need to remap it to [-1, 1] and set the Z value to the sampled depth.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
// Reconstructing Clip Space position from UV and raw depth.
float4 positionCS = float4((IN_uv.xy * 2.0 - 1.0), rawScreenDepth, 1.0);
#if UNITY_UV_STARTS_AT_TOP
	positionCS.y *= -1.0; // Some rendering APIs invert the Y axis in Unity.
#endif
</pre></div>


<p class="wp-block-paragraph">Next, I need to convert the clip space position into a world space position. Usually, vertex shaders use a View-Projection matrix to convert a world space position into clip space, so I can use an inversed View-Projection matrix to flip the transformation. Unity stores the inversed view-projection matrix in the <code>UNITY_MATRIX_I_VP</code>.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
// Use an inversed view-projection matrix to transform CS into WS.
float4 positionWS = mul(UNITY_MATRIX_I_VP, positionCS); 
positionWS /= positionWS.w; // The final value is not normalized. I normalize it by dividing it by the W component.
</pre></div>


<p class="wp-block-paragraph">When making such changes, I like to print the results on the screen using just an emission buffer. It is helpful for me to see if the depth data is sampled properly or if the reconstructed position WS is correct.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
OUT_GBuffer0.rgba = 0.0;
OUT_GBuffer2.rgba = 0.0;
OUT_GBuffer3.rgb = frac(positionWS.xyz); // Display position WS

// Or 

OUT_GBuffer3.rgba = rawScreenDepth * 200.0; // Display raw depth, rescaled to actually see some of the values on the screen
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="282" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1024x282.jpg" alt="" class="wp-image-5553" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1024x282.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-300x83.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-768x212.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1536x423.jpg 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-2048x564.jpg 2048w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-400x110.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-600x165.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-800x220.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1000x276.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1280x353.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1520x419.jpg 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/004-color-depth-ws-1900x524.jpg 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">From the left: original image, displayed raw depth, displayed position WS. I check if these images are distorted in accordance with the 3D shapes. This image shows correctly sampled depth and position in world space.</figcaption></figure>



<h4 class="wp-block-heading">3.3 Calculate shadowmap UV from the world space position</h4>



<p class="wp-block-paragraph">Now, I can use the world space position to calculate the UV of the shadowmap. I will use the view and projection matrix of the rain-camera to calculate the clip space position.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
float4 rainPositionVS = mul(_RainCamera_MatrixV, positionWS); // Convert world space to shadowmap camera view
float4 rainPositionCS = mul(_RainCamera_MatrixP, rainPositionVS); // Convert shadowmap camera view space to shadowmap clip space
rainPositionCS /= rainPositionCS.w; // Normalize the clip space to &#x5B;-1, 1] values (NDC)

float2 rainUV = rainPositionCS.xy * 0.5 + 0.5; // Remap clip-space to shadowmap UV.
#if UNITY_UV_STARTS_AT_TOP
	rainUV.y = 1.0 - rainUV.y; // And flip UV.y if I&#039;m in Australia
#endif
</pre></div>


<p class="wp-block-paragraph">Again, to check if everything went correctly, I printed UV to the emission buffer:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="690" height="493" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-uv.jpg" alt="" class="wp-image-5554" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-uv.jpg 690w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-uv-300x214.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-uv-400x286.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-uv-600x429.jpg 600w" sizes="auto, (max-width: 690px) 100vw, 690px" /><figcaption class="wp-element-caption">UV of a shadowmap displayed on the screen.</figcaption></figure>



<p class="wp-block-paragraph">It appears that the shadowmap UV displays properly.</p>



<h4 class="wp-block-heading">3.4 Calculate rain-camera depth and sample shadowmap</h4>



<p class="wp-block-paragraph">It is the right time to sample the shadowmap and calculate the rain-camera depth value for the surface.</p>



<p class="wp-block-paragraph">I can sample the shadowmap using the calculated UV:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
float rawRainDepth = _RainCamera_DepthTexture.SampleLevel(linearClampSampler, sampleUV, 0.0f).r;
</pre></div>


<p class="wp-block-paragraph">And when printed to the screen, it looks correct:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="690" height="494" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-debug.jpg" alt="" class="wp-image-5555" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-debug.jpg 690w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-debug-300x215.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-debug-400x286.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowmap-debug-600x430.jpg 600w" sizes="auto, (max-width: 690px) 100vw, 690px" /><figcaption class="wp-element-caption">Shadowmap values sampled for a previously calculated world space position. You can notice that buildings stand out because the depth value under the roofs is the same as for the roofs. This is a correct result.</figcaption></figure>



<p class="wp-block-paragraph">I need to get the rain-camera depth value for the surface using the world space position, but…</p>



<p class="wp-block-paragraph">…it is actually the Z value of the rain camera clip space. I can just use that because it is already calculated.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
float surfaceRainDepth = rainPositionCS.z;
</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="688" height="494" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-calculated-shadow-depth-debug.jpg" alt="" class="wp-image-5556" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-calculated-shadow-depth-debug.jpg 688w, https://theknightsofu.com/wp-content/uploads/2025/06/004-calculated-shadow-depth-debug-300x215.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-calculated-shadow-depth-debug-400x287.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-calculated-shadow-depth-debug-600x431.jpg 600w" sizes="auto, (max-width: 688px) 100vw, 688px" /><figcaption class="wp-element-caption">Notice that the calculated depth values look more correct. And that&#8217;s the key. We will compare the value in the shadow map with the calculated value to determine if there is a shadow or not.</figcaption></figure>



<h4 class="wp-block-heading">3.5. Calculating the shadow</h4>



<p class="wp-block-paragraph">It&#8217;s time to compare the calculated depth value with the value read from the shadowmap. If the calculated value is smaller than the value in the shadowmap, there is a shadow.</p>



<p class="wp-block-paragraph">However, the values in the shadow map are quantized and not perfect. I need to apply some offset to reduce artifacts. I tested a few different offsets, and 0.002 works fine. Later, I could polish the effect and calculate this offset on the fly instead of hardcoding it, but right now, it&#8217;s fine.</p>



<p class="wp-block-paragraph">The offset should depend on the rain-camera render distance and depth encoding.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
float shadowMultiplier = 1.0;
if (surfaceRainDepth &lt; rawRainDepth - 0.002)
	shadowMultiplier = 0.0;
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="219" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1024x219.jpg" alt="" class="wp-image-5557" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1024x219.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-300x64.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-768x164.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1536x328.jpg 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-2048x437.jpg 2048w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-400x85.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-600x128.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-800x171.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1000x213.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1280x273.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1520x324.jpg 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadows-offsets-1900x405.jpg 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">This is how shadowMultiplier looks like when displayed on the screen.</figcaption></figure>



<p class="wp-block-paragraph">I used the calculated <code>shadowMultiplier</code> to modify the effect strength.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
// Effect strength is now modulated by the calculated shadow multiplier
float effectStrength = _EffectStrength * shadowMultiplier;

float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);
rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), effectStrength);

OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, effectStrength), 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, effectStrength), 1.0);
</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="685" height="442" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-sharp-effect-edge.jpg" alt="" class="wp-image-5558" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-sharp-effect-edge.jpg 685w, https://theknightsofu.com/wp-content/uploads/2025/06/004-sharp-effect-edge-300x194.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-sharp-effect-edge-400x258.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-sharp-effect-edge-600x387.jpg 600w" sizes="auto, (max-width: 685px) 100vw, 685px" /><figcaption class="wp-element-caption">This is how the effect looks like when we just use the simplest shadowcasting technique.</figcaption></figure>



<p class="wp-block-paragraph">The problem is that the calculated shadowmask is super sharp. The transition between dry and wet surfaces appears too obvious. It will be nice to blur it a little.</p>



<p class="wp-block-paragraph">I will implement blurring by sampling many shadowmap samples from the nearby UVs and calculating the average shadow multiplier from those.</p>



<p class="wp-block-paragraph">I will use a kernel to sample a shadowmap a few times from different positions, and then I will calculate the average shadow mask value from all those samples. The sampling kernel is an array of offsets that should be applied to UV before sampling.</p>



<p class="wp-block-paragraph">I asked ChatGPT to generate a sampling kernel with 16 points with Poisson-Disk distribution. I decided to use this kernel and placed it above the fragment shader:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
static const int poissonDiskCount = 16;

static const float2 poissonDisk&#x5B;poissonDiskCount] = 
{
	float2(-0.094184, -0.929388),
	float2( 0.143831, -0.141007),
	float2( 0.344959,  0.293877),
	float2( 0.199841,  0.786413),
	float2(-0.241888,  0.997065),
	float2(-0.382775,  0.276768),
	float2(-0.264969, -0.418930),
	float2(-0.942016, -0.399062),
	float2(-0.915885,  0.457716),
	float2(-0.814099,  0.914375),
	float2(-0.815442, -0.879124),
	float2( 0.537429, -0.473734),
	float2( 0.791975,  0.190901),
	float2( 0.974843,  0.756483),
	float2( 0.945586, -0.768907),
	float2( 0.443233, -0.975115)
};
</pre></div>


<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="389" height="357" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-plot-of-poisson-disk.jpg" alt="" class="wp-image-5559" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-plot-of-poisson-disk.jpg 389w, https://theknightsofu.com/wp-content/uploads/2025/06/004-plot-of-poisson-disk-300x275.jpg 300w" sizes="auto, (max-width: 389px) 100vw, 389px" /><figcaption class="wp-element-caption">Plotted all of the offsets within a kernel.</figcaption></figure>



<p class="wp-block-paragraph">I iterated through all the offsets in the kernel and calculated the shadow multiplier for each offset. Then I calculated the average and used it to modulate the effect strength. After implementing the kernel, one last thing was to play with the offsets.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
// I will accumulate the shadowmask value here
float shadowMultiplierSum = 0.0;

// Iterate through each offset in the kernel
for (int i = 0; i &lt; poissonDiskCount; i++)
{
	float2 offset = poissonDisk&#x5B;i].xy * 0.01; // Scale the offset
	float rawRainDepth = _RainCamera_Shadowmap.SampleLevel(linearClampSampler, rainUV + offset, 0.0f).r; // Sample the shadowmap

	// Calculate shadow multiplier for each sample
	float shadowMultiplier = 1.0;
	if (surfaceRainDepth &lt; rawRainDepth - 0.03)
		shadowMultiplier = 0.0;

	// Accumulate shadow multiplier
	shadowMultiplierSum += shadowMultiplier;
}

// Calculate the average
float shadowmapMultiplierAvg = shadowMultiplierSum / (float)poissonDiskCount;

// And modulate the effect based on the average
float effectStrength = _EffectStrength * shadowmapMultiplierAvg;
</pre></div>


<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="221" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1024x221.jpg" alt="" class="wp-image-5560" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1024x221.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-300x65.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-768x166.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1536x331.jpg 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-2048x441.jpg 2048w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-400x86.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-600x129.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-800x172.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1000x215.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1280x276.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1520x328.jpg 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/004-tweaking-shadow-parameters-1900x409.jpg 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">You can&#8217;t really notice the banding in the final result.</figcaption></figure>



<p class="wp-block-paragraph">I see that there is a lot of banding that could be fixed. But the truth is that the player will never see the mask. Try to spot this banding when it&#8217;s in action as a mask for the effect. I will add the parameters to the material to control all the offsets.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
Shader &quot;Hidden/ApplyRainAtmosphere&quot;
{
    Properties
    {
		...

        _ShadowBlurRadius(&quot;Shadow blur radius&quot;, Range(0.0, 0.1)) = 0.007
        _ShadowBlurShape(&quot;Shadow blur shape&quot;, Range(0.0, 2.0)) = 0.65
        _MinEffectStrength(&quot;Min effect strength in shadows&quot;, Range(0.0, 1.0)) = 0.1
        _DepthOffset(&quot;Depth offset&quot;, Range(0.0, 0.1)) = 0.03
        _DepthFadeWidth(&quot;Depth fade width&quot;, Range(0.0, 0.1)) = 0.05
    }
...
</pre></div>


<p class="wp-block-paragraph">This is the final fragment shader code will all the tweaks:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: cpp; title: ; notranslate">
void frag 
(
    in float2 IN_uv : TEXCOORD0,
    out float4 OUT_GBuffer0 : SV_Target0,
    out float4 OUT_GBuffer2 : SV_Target2,
    out float4 OUT_GBuffer3 : SV_Target3
) 
{
    // Calculating world space position from depth.
    float rawScreenDepth = _CameraDepthTexture.SampleLevel(pointClampSampler, IN_uv, 0.0f).r;
    float4 positionCS = float4((IN_uv.xy * 2.0 - 1.0), rawScreenDepth, 1.0); 
    #if UNITY_UV_STARTS_AT_TOP
        positionCS.y *= -1.0;
    #endif
    float4 positionWS = mul(UNITY_MATRIX_I_VP, positionCS); 
    positionWS /= positionWS.w;

    // Calculating rain-camera clip-space position. It contains depth value.
    float4 rainPositionVS = mul(_RainCamera_MatrixV, positionWS);
    float4 rainPositionCS = mul(_RainCamera_MatrixP, rainPositionVS);
    rainPositionCS /= rainPositionCS.w;

    // Remapping rain-camera clip-space position to shadowmap UV
    float2 rainUV = rainPositionCS.xy * 0.5 + 0.5; 
    #if UNITY_UV_STARTS_AT_TOP
        rainUV.y = 1.0 - rainUV.y;
    #endif

    // Calculating the shadows.
    float surfaceRainDepth = rainPositionCS.z;
    float shadowMultiplierSum = 0.0;
    for (int i = 0; i &lt; poissonDiskCount; i++)
    {
        float2 offset = poissonDisk&#x5B;i].yx * _ShadowBlurRadius;
        float rawRainDepth = _RainCamera_Shadowmap.SampleLevel(linearClampSampler, rainUV + offset, 0.0f).r;

        // Tweaked shadow multiplier calculation to hide sharp banding
        float shadowMultiplier = smoothstep(-_DepthOffset, -_DepthOffset + _DepthFadeWidth, surfaceRainDepth - rawRainDepth);

        shadowMultiplierSum += shadowMultiplier;
    }

    float shadowMultiplierAvg = shadowMultiplierSum / (float)poissonDiskCount;

    // Calculating the final effect strength using the calculated shadows.
    // Added min strength and gamma-modifier to the calculated shadow value
    float minEffectStrength = _EffectStrength * _MinEffectStrength;
    float effectStrength = minEffectStrength + _EffectStrength * pow(shadowMultiplierAvg, _ShadowBlurShape) * (1.0 - minEffectStrength);

    float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);
    rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), effectStrength);

    OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, effectStrength), 1.0);
    OUT_GBuffer2 = rawGBuffer2;
    OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, effectStrength), 1.0);
}
</pre></div>


<p class="wp-block-paragraph">This is the final effect:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="433" src="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-1024x433.gif" alt="" class="wp-image-5561" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-1024x433.gif 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-300x127.gif 300w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-768x325.gif 768w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-400x169.gif 400w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-600x254.gif 600w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-800x339.gif 800w, https://theknightsofu.com/wp-content/uploads/2025/06/004-shadowcasting-parameters-1000x423.gif 1000w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">About the author</h2>



<p class="wp-block-paragraph">Jan Mróz / Sir Johny – Graphics Programmer at The Knights of U. He specializes in rendering pipeline extensions and performance optimization across CPU, GPU, and memory. Sir Johny also leads tech-art initiatives that improve visual quality while cutting iteration time.</p>



<p class="wp-block-paragraph">Learn more from Jan at <a href="https://proceduralpixels.com">proceduralpixels.com</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/render-graph-creative-shadowcasting/">Render Graph &#8211; Creative Shadowcasting</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/render-graph-creative-shadowcasting/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Render Graph &#8211; Polishing Rain Atmosphere</title>
		<link>https://theknightsofu.com/render-graph-polishing-rain-atmosphere/</link>
					<comments>https://theknightsofu.com/render-graph-polishing-rain-atmosphere/#respond</comments>
		
		<dc:creator><![CDATA[Sir Johny]]></dc:creator>
		<pubDate>Tue, 17 Jun 2025 13:00:00 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Bloom Flickering]]></category>
		<category><![CDATA[game development]]></category>
		<category><![CDATA[GBuffer]]></category>
		<category><![CDATA[GPU Performance]]></category>
		<category><![CDATA[Graphics Programming]]></category>
		<category><![CDATA[Material Parameters]]></category>
		<category><![CDATA[Rain Effect]]></category>
		<category><![CDATA[Render Graph API]]></category>
		<category><![CDATA[Rendering Pipeline Optimization]]></category>
		<category><![CDATA[Shader Optimization]]></category>
		<category><![CDATA[Shader Programming]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[unity 6]]></category>
		<category><![CDATA[URP]]></category>
		<category><![CDATA[Visual Effects]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5175</guid>

					<description><![CDATA[<p>In this article, I will improve the rain atmosphere effect I built in the previous article. This effect works by postprocessing a GBuffer in deferred rendering path, just before the light caluclation, where I increased a smoothness and decreased the GI and albedo. There were two main issues I had to tackle: Bloom flickering High &#8230; <a href="https://theknightsofu.com/render-graph-polishing-rain-atmosphere/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/render-graph-polishing-rain-atmosphere/">Render Graph &#8211; Polishing Rain Atmosphere</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In this article, I will improve the rain atmosphere effect I built in the <a href="https://theknightsofu.com/render-graph-creating-rain-atmosphere/">previous article</a>. This effect works by postprocessing a GBuffer in deferred rendering path, just before the light caluclation, where I increased a smoothness and decreased the GI and albedo.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="276" src="https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1024x276.png" alt="" class="wp-image-5379" srcset="https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1024x276.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-300x81.png 300w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-768x207.png 768w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1536x413.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-2048x551.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-400x108.png 400w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-600x161.png 600w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-800x215.png 800w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1000x269.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1280x344.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1520x409.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/06/002-RainAtmosphereEffect-1900x511.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">There were two main issues I had to tackle:</p>



<ol class="wp-block-list">
<li>High smoothness resulted in flickering when using the bloom post process.</li>



<li>All parameters were hard-coded, so I needed to make them accessible through the material&#8217;s properties.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="589" height="294" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-BloomFlicker.gif" alt="" class="wp-image-5177"/><figcaption class="wp-element-caption">Bloom flickers a lot during small camera movements.</figcaption></figure>



<h2 class="wp-block-heading">Bloom flickering</h2>



<p class="wp-block-paragraph">High smoothness in the GBuffer can create very bright points on the screen in certain light conditions. It makes the bloom postprocess shimmer and flicker. Bloom flicker can be implemented by modifying the bloom effect itself or modifying its inputs. I decided to limit the smoothness value in the GBuffer to fix the issue. I used hardware blending for GBuffer modifications, which has limited capabilities and doesn&#8217;t allow me to limit the maximum value of the target texture. This can be fixed by copying the normal-smoothness buffer and sampling the copy to implement a custom blending in the fragment shader. I will keep the smoothness in the reasonable range in the shader that applies the effect.</p>



<p class="wp-block-paragraph">The current render graph looks like this (<em>Apply Rain Atmosphere</em> was implemented in the previous article).</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="813" height="225" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph.jpg" alt="" class="wp-image-5178" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph.jpg 813w, https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph-300x83.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph-768x213.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph-400x111.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph-600x166.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/003-PreviousGraph-800x221.jpg 800w" sizes="auto, (max-width: 813px) 100vw, 813px" /></figure>



<p class="wp-block-paragraph">I can add a custom node between <em>DrawGBuffer</em> and <em>Apply Rain Atmosphere</em> that will create a copy of the GBuffer[2] texture and share it with other passes. The pass responsible for that will be called <em>Initialize Rain Resources</em>. I will call the data shared between passes <em>RainResourceData</em>. Then, the <em>Apply Rain Atmosphere</em> pass will access the copy of GBuffer[2] and use it in the shader with custom blending.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="255" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-1024x255.jpg" alt="" class="wp-image-5179" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-1024x255.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-300x75.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-768x191.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-400x100.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-600x149.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-800x199.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-1000x249.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph-1280x319.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/05/003-TargetGraph.jpg 1385w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>My plan:</strong></p>



<ol class="wp-block-list">
<li>Implement <em>Initialize Rain Resources</em> pass.
<ol class="wp-block-list">
<li>Create a <em>RainResourceData</em> class to store resources shared between passes.</li>



<li>Copy the GBuffer[2] contents into a custom texture using a custom shader.</li>
</ol>
</li>



<li>Access the copied GBuffer[2] in <em>Apply Rain Atmosphere</em> pass.</li>



<li>Sample the copied GBuffer[2] in the fragment shader to implement custom blending and avoid bloom shimmer.</li>
</ol>



<h3 class="wp-block-heading">Implement resource initialization pass</h3>



<p class="wp-block-paragraph">My goal is to create a new texture in the Render Graph and copy the content of GBuffer[2] into this texture.</p>



<p class="wp-block-paragraph">I created an <code>InitializeRainResourcesPass</code> and modified the <code>RainAtmosphereFeature</code> to execute it before the main pass. I&#8217;ve added a material I want to use with a custom shader to copy the GBuffer[2] contents.</p>



<pre class="wp-block-code"><code>public class RainAtmosphereFeature : ScriptableRendererFeature
{
    public Material copyGBufferMaterial; // Added a material that will be used to copy GBuffer&#91;2] contents.
    public Material applyRainMaterial;

    private bool HasValidResources =&gt; applyRainMaterial != null &amp;&amp; copyGBufferMaterial != null; // Update validation

    private InitializeRainResourcesPass initializeResourcesPass; // Added this pass to the render feature
    private ApplyRainAtmospherePass applyRainAtmospherePass;

    public override void Create()
    {
        applyRainAtmospherePass = new ApplyRainAtmospherePass();
        initializeResourcesPass = new InitializeRainResourcesPass();
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
        if (isGameOrSceneCamera &amp;&amp; HasValidResources)
        {
            initializeResourcesPass.material = copyGBufferMaterial; // Updated the material
            applyRainAtmospherePass.material = applyRainMaterial;

            renderer.EnqueuePass(initializeResourcesPass); // Enqueued as first pass
            renderer.EnqueuePass(applyRainAtmospherePass);
        }
    }
}

// This pass will initialize the resources and copy the GBuffer.
public class InitializeRainResourcesPass : ScriptableRenderPass
{
    public Material material { get; internal set; }

    public InitializeRainResourcesPass()
    {
        this.renderPassEvent = RenderPassEvent.AfterRenderingGbuffer;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        // TODO: Initialize shared resources
        // TODO: Copy GBuffer&#91;2] contents
    }
}</code></pre>



<h4 class="wp-block-heading">Shared resources</h4>



<p class="wp-block-paragraph">I started by creating a class that store the shared resources. Shared resources inherit <code>ContextItem</code>. They must release manually allocated resources in the <code>Reset()</code> function. In our case, we only need to store a <code>TextureHandle</code> &#8211; the copy of GBuffer[2].</p>



<pre class="wp-block-code"><code>// Resources shared between passes need to inherit from ContextItem
public class RainResourceData : ContextItem
{
	// Handle for Gbuffer&#91;2] copy.
    public TextureHandle normalSmoothnessTexture { get; internal set; }

	// And reset all resources using this method.
    public override void Reset()
    {
	    // Use TextureHandle.nullHandle to reset texture handles.
        normalSmoothnessTexture = TextureHandle.nullHandle;
    }
}</code></pre>



<p class="wp-block-paragraph">I created the shared resources in the <em>Initialize Rain Resources Pass</em> using the <code>frameData.GetOrCreate&lt;&gt;()</code> API. After using this method, I can access those resources in other passes.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	// Initialize the shared resources
	RainResourceData rainResources = frameData.GetOrCreate&lt;RainResourceData&gt;();
	...</code></pre>



<h4 class="wp-block-heading">Accessing the GBuffer</h4>



<p class="wp-block-paragraph">I got the reference to the GBuffer and validated it to avoid executing the pass in a forward rendering path.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	RainResourceData rainResources = frameData.GetOrCreate&lt;RainResourceData&gt;();

	// Access URP GBuffer
	var urpResources = frameData.Get&lt;UniversalResourceData&gt;();
	var gBuffer = urpResources.gBuffer;

	// Check if the GBuffer exists.
	if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#91;0].Equals(TextureHandle.nullHandle))
		return;</code></pre>



<h4 class="wp-block-heading">Allocate GBuffer copy texture</h4>



<p class="wp-block-paragraph">I allocated the target texture using the <code>renderGraph.CreateTexture(textureDescriptior)</code> method. This method does not allocate the texture immediately, but it allows the Render Graph to manage the lifetime of the texture. Render Graph will ensure that the texture exists when our render function executes.</p>



<pre class="wp-block-code"><code>...
if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#91;0].Equals(TextureHandle.nullHandle))
		return;

// Allocate the texture handle in the render graph.
rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer&#91;2].GetDescriptor(renderGraph));</code></pre>



<h4 class="wp-block-heading">Creating render pass</h4>



<p class="wp-block-paragraph">Then, I created a pass in the render graph. I used a raster render pass where I specified:</p>



<ul class="wp-block-list">
<li>Render attachment &#8211; our freshly allocated texture handle</li>



<li>Dependencies &#8211; gBuffer[2] texture</li>



<li>Render function</li>
</ul>



<pre class="wp-block-code"><code>	...
	rainResources.normalSmoothnessTexture = renderGraph.CreateTexture(gBuffer&#91;2].GetDescriptor(renderGraph));
	
	// Create a render pass that will create a copy of GBuffer&#91;2]
	using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
	{
		// Specify dependencies
		builder.SetRenderAttachment(rainResources.normalSmoothnessTexture, 0);
		builder.UseTexture(gBuffer&#91;2]);

		// Save data for render function
		passData.gBuffer_2 = gBuffer&#91;2];
		passData.material = material;

		// Set the render function
		builder.SetRenderFunc((PassData passData, RasterGraphContext context) =&gt; RenderFunction(passData, context));
	}
}

private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	// TODO: Implement the rendering here
}

internal class PassData
{
	internal Material material; // Material used to copy the GBuffer
	internal TextureHandle gBuffer_2; // Original GBuffer&#91;2] handle
}</code></pre>



<h4 class="wp-block-heading">Render function</h4>



<p class="wp-block-paragraph">Then, I implemented a render function. <em>Raster Graph Context</em> contains object pooling for <em>PropertyBlocks</em> &#8211; I used that to set the shader properties. <em>_GBuffer2</em> property was set to the original texture. The shader used in the material will implement that property.</p>



<pre class="wp-block-code"><code>private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	// Get property block and set shader parameters.
	MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
	propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer_2);

	// Execute procedural fullscreen draw.
	context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}

// Utility class that I like to use to store shader properties.
public static class Uniforms
{
    internal static readonly int _GBuffer2 = Shader.PropertyToID(nameof(_GBuffer2));
}</code></pre>



<h3 class="wp-block-heading">Writing shader</h3>



<p class="wp-block-paragraph">The RenderGraph node is ready, and it&#8217;s time to implement the shader that copies the contents of the GBuffer. The shader should read the contents of the GBuffer texture and then output those in the fragment shader. I used the fullscreen draw template to achieve that.</p>



<pre class="wp-block-code"><code>Shader "Hidden/CopyGBuffer2"
{
    SubShader
    {
        Pass
        {
            // Disable triangle culling and depth testing
            Cull Off
            ZTest Off
            ZWrite Off
            
            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            // Fulscreen quad in clip space
            static const float4 verticesCS&#91;] = 
            {
                float4(-1.0, -1.0, 0.0, 1.0),
                float4(-1.0, 1.0, 0.0, 1.0),
                float4(1.0, 1.0, 0.0, 1.0),
                float4(-1.0, -1.0, 0.0, 1.0),
                float4(1.0, 1.0, 0.0, 1.0),
                float4(1.0, -1.0, 0.0, 1.0)
            };
			
            void vert (uint vertexID : SV_VertexID, out float4 OUT_PositionCS : SV_Position)
            {
	            // Output a fullscreen quad.
                float4 positionCS = verticesCS&#91;vertexID];
            }

            void frag (out float4 OUT_Color : SV_Target0) 
            {
				// TODO.
            }

            ENDHLSL
        }
    }
}</code></pre>



<h4 class="wp-block-heading">Vertex Shader</h4>



<p class="wp-block-paragraph">I modified the vertex shader to output a UV calculated from the clip space. Clip space range is from -1.0 to 1.0, and UV is from 0.0 to 1.0, so I needed to remap those values.<br>On some platforms, UV starts at the top of the screen. In this case, Y-axis needs to be flipped.</p>



<pre class="wp-block-code"><code>// Add UV interpolator output to the vertex shader.
void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
	float4 positionCS = verticesCS&#91;vertexID];

	// Calculate screen UV from the clip space
	float2 uv = positionCS.xy * 0.5 + 0.5;
	#if UNITY_UV_STARTS_AT_TOP
		uv.y = 1.0 - uv.y;
	#endif

	// Set position on the screen and UV
	OUT_PositionCS = verticesCS&#91;vertexID];
	OUT_uv = uv;
}</code></pre>



<h4 class="wp-block-heading">Fragment Shader</h4>



<p class="wp-block-paragraph">I declared <code>Texture2D _GBuffer2</code> &#8211; the texture I assigned in the render function. I used the UV to sample the GBuffer and output its contents into the render attachment. An object-oriented HLSL syntax was used to sample the texture.</p>



<pre class="wp-block-code"><code>SamplerState pointClampSampler; // Point clamp sampler we will use to sample the GBuffer
Texture2D _GBuffer2; // GBuffer texture

void frag (in float2 IN_uv : TEXCOORD0, out float4 OUT_Color : SV_Target0) 
{
	// Use screen UV to sample and output the GBuffer contents
	OUT_Color = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0f);
}</code></pre>



<p class="wp-block-paragraph">Now I could create a material with this shader and assign it to the renderer feature.</p>



<h2 class="wp-block-heading">Access the copied GBuffer in ApplyRainAtmospherePass</h2>



<p class="wp-block-paragraph">After assigning the material to the render feature, there is no <code>InitializeRainResourcesPass</code> in the Frame Debugger and Render Graph Viewer. It doesn&#8217;t execute at all.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="319" height="64" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassMissing.jpg" alt="" class="wp-image-5180" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassMissing.jpg 319w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassMissing-300x60.jpg 300w" sizes="auto, (max-width: 319px) 100vw, 319px" /><figcaption class="wp-element-caption">My pass didn&#8217;t appear!</figcaption></figure>



<p class="wp-block-paragraph">Render Graph detects that I don&#8217;t use the created texture in other passes, so it culls the render node created in the <code>InitializeRainResourcesPass</code>. To fix that, I must declare the texture usage in the <code>ApplyRainAtmospherePass</code>. I modified the <code>ApplyRainAtmospherePass</code> by accessing shared resources with the <code>frameData.Get&lt;&gt;()</code> API and declared the texture usage.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	...
	// Get the rain resources
	RainResourceData rainResources = frameData.Get&lt;RainResourceData&gt;();

	using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
	{
		// Declare the texture usage
		builder.UseTexture(rainResources.normalSmoothnessTexture);
		...
	}
}</code></pre>



<p class="wp-block-paragraph">And after this change, Frame Debugger and Render Graph Viewer correctly display the <code>InitializeRainResourcesPass</code>.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="972" height="245" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing.jpg" alt="" class="wp-image-5181" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing.jpg 972w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing-300x76.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing-768x194.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing-400x101.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing-600x151.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/003-FrameDebugger-PassNotMissing-800x202.jpg 800w" sizes="auto, (max-width: 972px) 100vw, 972px" /><figcaption class="wp-element-caption">My pass worked!</figcaption></figure>



<p class="wp-block-paragraph">Right now, when I know that Render Graph properly executes the initialization pass, I can use the copy of the GBuffer in the <code>ApplyRainAtmospherePass</code>. I forwarded the texture handle to the <em>passData</em>, used in our render function.</p>



<pre class="wp-block-code"><code>...
builder.UseTexture(rainResources.normalSmoothnessTexture);

passData.gBuffer2 = rainResources.normalSmoothnessTexture; // Pass the texture to the render function
passData.material = material;
...</code></pre>



<pre class="wp-block-code"><code>internal class PassData
{
	internal Material material;
	internal TextureHandle gBuffer2; // Added the texture handle to the data used in the render function
}</code></pre>



<p class="wp-block-paragraph">I used a property block from the render graph pool to set the texture in the shader.</p>



<pre class="wp-block-code"><code>private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	// Get property block from the pool and assign the texture.
	MaterialPropertyBlock propertyBlock = context.renderGraphPool.GetTempMaterialPropertyBlock();
	propertyBlock.SetTexture(Uniforms._GBuffer2, passData.gBuffer2);

	context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1, propertyBlock);
}</code></pre>



<p class="wp-block-paragraph">All the data is set right now, so it&#8217;s time to adjust the shader.</p>



<h3 class="wp-block-heading">Adjusting the rain atmosphere shader</h3>



<p class="wp-block-paragraph">My goal was to reduce the shimmering in the bloom postprocess. I could do that by implementing a custom blending for the smoothness stored in the alpha channel of the second GBuffer target. First of all, I needed to declare the texture in the shader.</p>



<pre class="wp-block-code"><code>SamplerState pointClampSampler; // Point clamp sampler we will use to sample the GBuffer
Texture2D _GBuffer2; // GBuffer texture</code></pre>



<p class="wp-block-paragraph">Then, I needed a UV to sample the texture. I copied the vertex shader from the <em>CopyGBuffer.shader</em>.</p>



<pre class="wp-block-code"><code>void vert (uint vertexID : SV_VertexID, out float2 OUT_uv : TEXCOORD0, out float4 OUT_PositionCS : SV_Position)
{
	float4 positionCS = verticesCS&#91;vertexID];

	float2 uv = positionCS.xy * 0.5 + 0.5;
	#if UNITY_UV_STARTS_AT_TOP
		uv.y = 1.0 - uv.y;
	#endif

	OUT_PositionCS = verticesCS&#91;vertexID];
	OUT_uv = uv;
}</code></pre>



<p class="wp-block-paragraph">It is a good practice to turn off the hardware blending when switching to the software blending &#8211; it can save some GPU resources.</p>



<pre class="wp-block-code"><code>Blend 2 Off // Disable hardware blending for the 2nd target.</code></pre>



<p class="wp-block-paragraph">The last step was to sample the GBuffer contents in the fragment shader and write a custom blending. I&#8217;ve found that additive blending for smoothness and clamping the result to a max of 0.86 works best in my case, so I hardcoded those values. This is a final fragment shader code:</p>



<pre class="wp-block-code"><code>void frag 
(
	in float2 IN_uv : TEXCOORD0, // Getting UV from the vertex shader
	out float4 OUT_GBuffer0 : SV_Target0,
	out float4 OUT_GBuffer2 : SV_Target2,
	out float4 OUT_GBuffer3 : SV_Target3
) 
{
	// Access GBuffer&#91;2] value
	float4 rawGBuffer2 = _GBuffer2.SampleLevel(pointClampSampler, IN_uv, 0.0);

	// Increase smoothness and clamp the value
	rawGBuffer2.a = min(0.86, rawGBuffer2.a + 0.7);

	OUT_GBuffer0 = float4(0.5, 0.5, 0.5, 1.0);
	OUT_GBuffer2 = rawGBuffer2; // Write it back into GBuffer
	OUT_GBuffer3 = float4(0.5, 0.5, 0.5, 1.0);
}</code></pre>



<p class="wp-block-paragraph">I could test the results. As you can see, I successfully reduced most of the flickering.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="589" height="294" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-BloomFlicker-1.gif" alt="" class="wp-image-5182"/></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="471" height="289" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-FixedBloomIssue.gif" alt="" class="wp-image-5183"/><figcaption class="wp-element-caption">Comparison of both effects. Bloom flickering is successfully reduced.</figcaption></figure>



<p class="wp-block-paragraph">You probably noticed that I needed to write much more code just to copy a texture. In the previous API I would use a <code>CommandBuffer.Blit</code> function to achieve that. Here I needed to explicitly create a separate pass. There are some utility methods to <em>blit</em> textures in Render Graph, but they still require creating a separate pass, custom shader, and managing the dependencies between passes &#8211; no way around that.</p>



<h2 class="wp-block-heading">Shader Parameters</h2>



<p class="wp-block-paragraph">Because I hardcoded all values in the shader, I can&#8217;t adjust them outside the code. I will add parameters to the material to make the visuals easier to adjust:</p>



<ul class="wp-block-list">
<li>Albedo multiplier</li>



<li>Ambient light multiplier</li>



<li>Smoothness</li>



<li>Max Smoothness</li>



<li>Effect strength &#8211; controls the strength of the effect. 0 &#8211; no effect is applied, 1 &#8211; full effect is applied.</li>
</ul>



<p class="wp-block-paragraph">Let&#8217;s start by adding shader properties. I added this at the beginning of the shader.</p>



<pre class="wp-block-code"><code>Shader "Hidden/ApplyRainAtmosphere"
{
    Properties
    {
        _AlbedoMultiplier("Albedo multiplier", Range(0.0, 1.0)) = 0.5
        _AmbientLightMultiplier("Ambient multiplier", Range(0.0, 1.0)) = 0.5
        _SmoothnessAdd("Added smoothness", Range(0.0, 1.0)) = 0.7
        _SmoothnessMax("Maximum smoothness", Range(0.0, 1.0)) = 0.86
        _EffectStrength("Overall effect strength", Range(0.0, 1.0)) = 1.0
    }
    ...</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="383" height="252" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-MaterialParameters.jpg" alt="" class="wp-image-5184" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-MaterialParameters.jpg 383w, https://theknightsofu.com/wp-content/uploads/2025/05/003-MaterialParameters-300x197.jpg 300w" sizes="auto, (max-width: 383px) 100vw, 383px" /><figcaption class="wp-element-caption">This section added sliders to the material that renders the effect.</figcaption></figure>



<p class="wp-block-paragraph">Then, I included all those properties in the shader code, just above the fragment shader.</p>



<pre class="wp-block-code"><code>...
float _AlbedoMultiplier;
float _AmbientLightMultiplier;
float _SmoothnessAdd;
float _SmoothnessMax;
float _EffectStrength;

void frag 
(
	in float2 IN_uv : TEXCOORD0,
	...</code></pre>



<p class="wp-block-paragraph">Next, I replaced all parameters except <code>_EffectStrength</code>.</p>



<pre class="wp-block-code"><code>...
rawGBuffer2.a = min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd);

OUT_GBuffer0 = float4((float3)_AlbedoMultiplier, 1.0);
OUT_GBuffer2 = rawGBuffer2; // Write it back into GBuffer

OUT_GBuffer3 = float4((float3)_AmbientLightMultiplier, 1.0);
...</code></pre>



<p class="wp-block-paragraph">To implement the <code>_EffectStrength</code> parameter, blending in the shader needs to do nothing when the value is 0. I implemented that using a Lerp function that interpolate all the values to the neutral ones. If the effect strength is set to 0, we must output 1 for the targets with multiply-blend and leave the smoothness as is.</p>



<pre class="wp-block-code"><code>...
// Lerp from original smoothness value to the custom one
rawGBuffer2.a = lerp(rawGBuffer2.a, min(_SmoothnessMax, rawGBuffer2.a + _SmoothnessAdd), _EffectStrength);

// Lerp from 1 to custom values in multiply-blended targets
OUT_GBuffer0 = float4((float3)lerp(1.0, _AlbedoMultiplier, _EffectStrength), 1.0);
OUT_GBuffer2 = rawGBuffer2;
OUT_GBuffer3 = float4((float3)lerp(1.0, _AmbientLightMultiplier, _EffectStrength), 1.0);
...</code></pre>



<h3 class="wp-block-heading">Result</h3>



<p class="wp-block-paragraph">All parameters work nicely.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="337" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-1024x337.gif" alt="" class="wp-image-5185" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-1024x337.gif 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-300x99.gif 300w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-768x253.gif 768w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-400x132.gif 400w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-600x197.gif 600w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-800x263.gif 800w, https://theknightsofu.com/wp-content/uploads/2025/05/003-EfectStrengthParameter-1000x329.gif 1000w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Effect strength parameter can smoothly interpolate the atmosphere of the scene.</figcaption></figure>



<p class="wp-block-paragraph">My rain atmosphere effect is ready. It takes only 0.44ms to render in 1440p on RTX 3060, increasing frame time from 7.77ms to 8.21ms (5.7% increase) &#8211; Measured with NVidia Nsight Graphics. The shader is mostly screen pipe bound &#8211; which means that most of the time is spent blending the GBuffer colors using the hardware units &#8211; I can&#8217;t get much faster than that!</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="436" height="323" src="https://theknightsofu.com/wp-content/uploads/2025/05/003-GPUPerformanceMetrics.jpg" alt="" class="wp-image-5186" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/003-GPUPerformanceMetrics.jpg 436w, https://theknightsofu.com/wp-content/uploads/2025/05/003-GPUPerformanceMetrics-300x222.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/003-GPUPerformanceMetrics-400x296.jpg 400w" sizes="auto, (max-width: 436px) 100vw, 436px" /><figcaption class="wp-element-caption">Overview of GPU hardware units throughput while rendering our feature.</figcaption></figure>



<h2 class="wp-block-heading">About the author</h2>



<p class="wp-block-paragraph">Jan Mróz / Sir Johny – Graphics Programmer at The Knights of U. He specializes in rendering pipeline extensions and performance optimization across CPU, GPU, and memory. Sir Johny also leads tech-art initiatives that improve visual quality while cutting iteration time.</p>



<p class="wp-block-paragraph">Learn more from Jan at <a href="https://proceduralpixels.com">proceduralpixels.com</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/render-graph-polishing-rain-atmosphere/">Render Graph &#8211; Polishing Rain Atmosphere</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/render-graph-polishing-rain-atmosphere/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Leverage your profiling abilities with Profile Analyzer in Unity 6</title>
		<link>https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/</link>
					<comments>https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/#respond</comments>
		
		<dc:creator><![CDATA[Sir Rob]]></dc:creator>
		<pubDate>Tue, 10 Jun 2025 13:00:00 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[CPU Optimization]]></category>
		<category><![CDATA[game development]]></category>
		<category><![CDATA[Game Performance]]></category>
		<category><![CDATA[GPU Bottleneck]]></category>
		<category><![CDATA[Performance Analysis]]></category>
		<category><![CDATA[Performance Optimization]]></category>
		<category><![CDATA[Profile Analyzer]]></category>
		<category><![CDATA[Profiler Data]]></category>
		<category><![CDATA[Profiling Techniques]]></category>
		<category><![CDATA[Rendering Performance]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[Unity Optimization]]></category>
		<category><![CDATA[Unity Profiler]]></category>
		<category><![CDATA[Unity Tools]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5064</guid>

					<description><![CDATA[<p>Have you ever stared at Unity Profiler trying to figure out what is really a bottom cause for performance hiccups of what you should put your efforts into in terms of optimizing your game? Or maybe you always wanted to compare two Profiler data? Profile Analyzer tool is the answer to this. The Profile Analyzer &#8230; <a href="https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/">Leverage your profiling abilities with Profile Analyzer in Unity 6</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Have you ever stared at Unity Profiler trying to figure out what is really a bottom cause for performance hiccups of what you should put your efforts into in terms of optimizing your game? Or maybe you always wanted to compare two Profiler data? Profile Analyzer tool is the answer to this.</p>



<p class="wp-block-paragraph">The Profile Analyzer aggregates and visualizes data from Unity&#8217;s built-in Profiler, offering insights into CPU usage, rendering times, and other performance metrics. By analyzing this data, you can pinpoint areas where the game may be underperforming and prioritize optimizations accordingly.</p>



<p class="wp-block-paragraph">Let’s dive into the details!</p>



<h2 class="wp-block-heading"><strong>How to get started</strong></h2>



<p class="wp-block-paragraph">You can install Profile Analyzer via <strong>Window > Package Manager</strong>.</p>



<p class="wp-block-paragraph">After opening the Profile Analyzer window you can see a couple of buttons. You can switch between Single and Compare mode, Export profiler data as CSV files, Pull Data from Profiler, Load or Save .pdata file (this is the data representing Profile Analyzer session).</p>



<p class="wp-block-paragraph">In order to get started, first you have to capture some data with the Profiler. Once it is done, the Pull Data button in the Profile Analyzer will enable. Click it to pull the captured data from Profiler. You can also load previously saved Profiler data into the Profiler and then pull this data to the Profile Analyzer.</p>



<p class="wp-block-paragraph">Once you’ve loaded your data, you’ll see it in the frame time graph. This is an interactive region &#8211; you can select a range of frames you want to analyze further.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="759" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1024x759.png" alt="" class="wp-image-5067" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1024x759.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-300x222.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-768x569.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1536x1139.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-2048x1518.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-400x297.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-600x445.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-800x593.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1000x741.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1280x949.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1520x1127.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectingFrameRegion-1900x1409.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Profile Analyzer window with selected frame region.</figcaption></figure>



<p class="wp-block-paragraph">The analysis starts immediately, and the data populates the frame time graph. On the top you can see white and cyan bars. The white bars represent the frame durations, the cyan bars however represent the currently selected marker. This allows you to compare the selected marker’s contribution to total frame time.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="716" src="https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1024x716.png" alt="" class="wp-image-5068" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1024x716.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-300x210.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-768x537.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1536x1074.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-2048x1432.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-400x280.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-600x420.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-800x559.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1000x699.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1280x895.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1520x1063.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfileAnalyzer-1-1900x1329.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Profile Analyzer window.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Filters</strong></h2>



<p class="wp-block-paragraph">You can search for markers using Filters. The Marker Details automatically update to reflect the subset that you’ve selected, and in the Count column you’ll see how many occurrences it has. The frame time graph highlights the marker. When you select a marker, the Marker Summary details panel also updates to show analysis statistics for that particular marker.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="547" src="https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1024x547.png" alt="" class="wp-image-5069" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1024x547.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-300x160.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-768x410.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1536x821.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-400x214.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-600x321.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-800x428.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1000x534.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1280x684.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch-1520x812.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/PAFilterSearch.png 1641w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Searching by filters in Profile Analyzer.</figcaption></figure>



<p class="wp-block-paragraph">There are following Filter available:</p>



<h4 class="wp-block-heading">1) <strong>By name</strong></h4>



<p class="wp-block-paragraph">You can search markers by substring ot a collection of substrings. If the name contains spaces, make sure to put quotes around the substring. The same goes for Exclude Names Filter. You can also populate marker filters by right-clicking on a marker of interest in the table and adding it to the filter selection.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="795" height="339" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter.png" alt="" class="wp-image-5070" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter.png 795w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter-300x128.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter-768x327.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter-400x171.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_AddTOFilter-600x256.png 600w" sizes="auto, (max-width: 795px) 100vw, 795px" /><figcaption class="wp-element-caption">Including marker to Filter.</figcaption></figure>



<h4 class="wp-block-heading">2) <strong>Remove Filter</strong></h4>



<p class="wp-block-paragraph">You can also choose to hide one marker and all child samples under these markers from the data, both in the tables and the graph. You can remove markers by selecting and choosing the Remove Marker option from the right click context menu.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="790" height="336" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker.png" alt="" class="wp-image-5071" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker.png 790w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker-300x128.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker-768x327.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker-400x170.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_RemoveMarker-600x255.png 600w" sizes="auto, (max-width: 790px) 100vw, 790px" /><figcaption class="wp-element-caption">Removing marker from Filter.</figcaption></figure>



<p class="wp-block-paragraph">You can only remove one marker at a time; you can add a marker back by choosing <strong>None </strong>in the dropdown.</p>



<h4 class="wp-block-heading">3) <strong>Threads</strong></h4>



<p class="wp-block-paragraph">You can choose to see data for specific worker threads. To see all of them aggregated, check “All” at the top of the thread Select pop-up window.</p>



<h4 class="wp-block-heading">4) <strong>Depth Slice</strong></h4>



<p class="wp-block-paragraph">You can select marker callback depth to analyze.</p>



<h4 class="wp-block-heading">5) <strong>Parent</strong></h4>



<p class="wp-block-paragraph">To filter by parent, right-click on the marker in the marker table and select Set as Parent Marker Filter.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="788" height="332" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent.png" alt="" class="wp-image-5072" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent.png 788w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent-300x126.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent-768x324.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent-400x169.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SetAsParent-600x253.png 600w" sizes="auto, (max-width: 788px) 100vw, 788px" /><figcaption class="wp-element-caption">Setting marker as Parent.</figcaption></figure>



<h4 class="wp-block-heading">6) <strong>Analysis Type</strong></h4>



<p class="wp-block-paragraph">By default, the Profile Analyzer shows you Total time for the markers, but you can change to Self time. Self time is useful if you want to drill down to details to see where your time is really spent.</p>



<h4 class="wp-block-heading">7) <strong>Units</strong></h4>



<p class="wp-block-paragraph">The default units are milliseconds, but for smaller timings, you can switch to microseconds.</p>



<h4 class="wp-block-heading">8) <strong>Marker Columns</strong></h4>



<p class="wp-block-paragraph">This filter enables you to customize columns of the Marker Details section for your current needs.</p>



<h2 class="wp-block-heading"><strong>Top 10 markers on median frame</strong></h2>



<p class="wp-block-paragraph">This section shows the top 10 markers for the filters that you’ve selected. It is a very powerful tool if used properly. </p>



<p class="wp-block-paragraph">For example, most Unity’s subsystems have high-level markers at depth 3. Knowing that you can set Filters to:</p>



<ul class="wp-block-list">
<li>Thread: Main Thread</li>



<li>Analysis Type: Total</li>



<li>Depth Slice: 3</li>
</ul>



<p class="wp-block-paragraph">This will show you the most consuming markers of Unity’s subsystems. This is a good way of making the very first diagnoze of your game’s performance.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="909" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1024x909.png" alt="" class="wp-image-5073" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1024x909.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-300x266.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-768x682.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1536x1364.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-400x355.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-600x533.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-800x710.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1000x888.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1280x1136.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1-1520x1349.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_Top10Markers-1.png 1639w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Top 10 Markers view in Profile Analyzer.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Marker Details</strong></h2>



<p class="wp-block-paragraph">At the bottom of the Profile Analyzer window you will find a table of the marker data from the Profiler, based on the filters you applied. The Unity Profiler data for all the markers are aggregated to provide median and other frame times, which are also displayed in this table. You can sort this data freely by any column.</p>



<h2 class="wp-block-heading"><strong>Frame Summary and Marker Summary</strong></h2>



<p class="wp-block-paragraph">The right side of the Profile Analyzer window starts with Frame Summary which displays highlights on the frame time, including the minimum, maximum, and median, with buttons that let you jump to specific frames in the Profiler. This area also shows you the frame time distribution histogram and a box and whisker plot. </p>



<p class="wp-block-paragraph">Below that there is the Marker Summary which shows the current selected markers contribution to the frame time. You can see up to 10 most costly frames of that marker and preview nice graph representing times on selected frames. At the bottom, there is information about the minimum and maximum times for individual marker instances within the aggregated data set.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="314" height="1024" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary-314x1024.png" alt="" class="wp-image-5074" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary-314x1024.png 314w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary-92x300.png 92w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary-300x978.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary-400x1304.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_FrameAndMarkerSummary.png 444w" sizes="auto, (max-width: 314px) 100vw, 314px" /><figcaption class="wp-element-caption">Right panel of Profile Analyzer.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Make it work</strong></h2>



<p class="wp-block-paragraph">It is important to remember that the Profile Analyzer should be used alongside Unity&#8217;s Profiler. In other words, Profile Analyzer can help you filter through the most important markers and find frames that are the most important which you can then keep analyzing in the Profiler.</p>



<p class="wp-block-paragraph">It is important to make sure that the data you’re looking at is representative. You can easily find the representative frame using Profile Analyzer Frame Summary. Just click on the Median frame from the selection of frames range &#8211; it will select this frame in the Profiler. You can also right-click on the frame time graph and select Select Median Frame.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="449" height="961" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectMedianFrame.png" alt="" class="wp-image-5075" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectMedianFrame.png 449w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectMedianFrame-140x300.png 140w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectMedianFrame-300x642.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_SelectMedianFrame-400x856.png 400w" sizes="auto, (max-width: 449px) 100vw, 449px" /><figcaption class="wp-element-caption">Selecting median frame from context menu.</figcaption></figure>



<p class="wp-block-paragraph">But let&#8217;s not limit ourselves to single point entry. Right-click on the frame time graph and select Order By Frame Duration. Now you can select a set of frames around the median frame for a representative sample that smooths out some of the noise in your data.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="126" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-1024x126.png" alt="" class="wp-image-5076" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-1024x126.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-300x37.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-768x94.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-400x49.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-600x74.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-800x98.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-1000x123.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration-1280x157.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_OrderByFrameDuration.png 1374w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Frames ordered by the duration.</figcaption></figure>



<h2 class="wp-block-heading"><strong>How to check if you are GPU-bound?</strong></h2>



<p class="wp-block-paragraph">You can do it pretty easily with the Profile Analyzer. First, find the Gfx.WaitForPresentOnGfxThread marker by typing its name to Name Filter. Analyze its data &#8211; if the median value of that marker is non-zero, then the CPU is waiting on the GPU to finish its activity before it can continue, aka &#8211; you are most likely GPU bound.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="292" src="https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1024x292.png" alt="" class="wp-image-5077" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1024x292.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-300x86.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-768x219.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1536x439.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-400x114.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-600x171.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-800x229.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1000x286.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1280x366.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound-1520x434.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/PA_GPUBound.png 1649w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Searching for Gfx.WaitForPresentOnGfxThread marker.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Comparing data sets</strong></h2>



<p class="wp-block-paragraph">Lastly, you can compare two sets of Profiler data. To do it select Compare mode at the top of the Profile Analyzer window. Then pull data from two different Profiler data sets. You can also pull data from one Profiler data set to compare for example two different frame ranges.</p>



<p class="wp-block-paragraph">One data set is displayed in blue, and the other in orange. Additionally, every section of the Profile Analyzer window now visualizes loaded data sets, so you can quickly compare them. </p>



<p class="wp-block-paragraph">You can use this mode to e.g. check the effect of the global project setting change such as Graphics Jobs or Physics Solver Type. To do that, first make a baseline Profiler capture then, run the game the second time after applying the changes and make another capture. Pull both captures to the Profile Analyzer and compare results in the Frame Summary section.</p>



<h2 class="wp-block-heading"><strong>Exporting data</strong></h2>



<p class="wp-block-paragraph">It is good to remember that you can export the Profile Analyzer data to CSV format for further analysis. It is recommended to store that data alongside the Profiler capture data which it was generated based on.</p>



<h2 class="wp-block-heading"><strong>Time to analyze!</strong></h2>



<p class="wp-block-paragraph">Profile Analyzer brings new quality to the process of analyzing Profilers data. Finding bottlenecks and key performance indicators is now much easier than ever. Do not hesitate, check it yourself!</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/">Leverage your profiling abilities with Profile Analyzer in Unity 6</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/leverage-your-profiling-abilities-with-profile-analyzer-in-unity-6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Render Graph &#8211; Creating Rain Atmosphere</title>
		<link>https://theknightsofu.com/render-graph-creating-rain-atmosphere/</link>
					<comments>https://theknightsofu.com/render-graph-creating-rain-atmosphere/#respond</comments>
		
		<dc:creator><![CDATA[Sir Johny]]></dc:creator>
		<pubDate>Tue, 03 Jun 2025 12:24:20 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Rendering]]></category>
		<category><![CDATA[Tutorial]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[Deferred Rendering]]></category>
		<category><![CDATA[game development]]></category>
		<category><![CDATA[GBuffer]]></category>
		<category><![CDATA[Graphics Optimization]]></category>
		<category><![CDATA[Graphics Programming]]></category>
		<category><![CDATA[Rain Effect]]></category>
		<category><![CDATA[Render Graph API]]></category>
		<category><![CDATA[Rendering Pipeline]]></category>
		<category><![CDATA[Shader Programming]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[unity 6]]></category>
		<category><![CDATA[Unity Tutorial]]></category>
		<category><![CDATA[URP]]></category>
		<category><![CDATA[Visual Effects]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5161</guid>

					<description><![CDATA[<p>In this blog I will explain how I created a rain atmosphere effect by altering the URP Gbuffer. This is a second article in the Render Graph series. You can read the previous article here: Render Graph API &#8211; Introduction GBuffer Before diving into the feature, I want to touch on what GBuffer is. GBuffer &#8230; <a href="https://theknightsofu.com/render-graph-creating-rain-atmosphere/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/render-graph-creating-rain-atmosphere/">Render Graph &#8211; Creating Rain Atmosphere</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In this blog I will explain how I created a rain atmosphere effect by altering the URP Gbuffer. This is a second article in the Render Graph series. You can read the previous article here:</p>



<p class="wp-block-paragraph"><a href="https://theknightsofu.com/render-graph-introduction/">Render Graph API &#8211; Introduction</a></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="276" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1024x276.jpg" alt="" class="wp-image-5163" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1024x276.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-300x81.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-768x207.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1536x413.jpg 1536w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-2048x551.jpg 2048w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-400x108.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-600x161.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-800x215.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1000x269.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1280x344.jpg 1280w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1520x409.jpg 1520w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RainAtmosphereEffect-1900x511.jpg 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">GBuffer</h2>



<p class="wp-block-paragraph">Before diving into the feature, I want to touch on what GBuffer is.</p>



<p class="wp-block-paragraph">GBuffer is an array of textures containing various surface properties, like albedo, specular, smoothness, ambient occlusion, and emission. When you select the deferred rendering path in URP, Unity renders all scene objects into GBuffer. It means that instead of calculating the lighting at each draw, they output surface properties into GBuffer. Then, URP applies the lighting in the form of a postprocess.</p>



<p class="wp-block-paragraph">URP GBuffer consists of 4 obligatory textures + some additional ones. According to <a href="https://docs.unity3d.com/6000.0/Documentation/Manual/urp/rendering/g-buffer-layout.html">URP documentation</a> obligatory textures are:<br><strong>GBuffer[0]</strong> &#8211; albedo in the RGB channels and material flags in the alpha channel.<br><strong>GBuffer[1]</strong> &#8211; specular color in the RGB channels and ambient occlusion in the alpha channel.<br><strong>GBuffer[2]</strong> &#8211; normal in the RGB channels and smoothness in the alpha channel<br><strong>GBuffer[3]</strong> &#8211; the combined emission, GI, and lighting in the RGB channels.</p>



<p class="wp-block-paragraph">The Render Deferred Lighting pass combines all the GBuffer textures and calculates dynamic lights.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="763" height="362" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-GBuffer.png" alt="" class="wp-image-5165" style="width:840px;height:auto" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-GBuffer.png 763w, https://theknightsofu.com/wp-content/uploads/2025/05/002-GBuffer-300x142.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/002-GBuffer-400x190.png 400w, https://theknightsofu.com/wp-content/uploads/2025/05/002-GBuffer-600x285.png 600w" sizes="auto, (max-width: 763px) 100vw, 763px" /><figcaption class="wp-element-caption">The image illustrates how Deferred Lighting Pass composes various GBuffer textures.</figcaption></figure>



<h2 class="wp-block-heading">How rain atmosphere can be achieved</h2>



<p class="wp-block-paragraph">To achieve the rain atmosphere effect, I need to manipulate the GBuffer content <em>before</em> the Render Deferred Lighting pass. I want to make the surface look wet after the lighting. But how do wet objects differ from dry objects? Here&#8217;s a photo illustrating the difference between dry and wet sand.</p>



<figure class="wp-block-image size-full is-resized"><img loading="lazy" decoding="async" width="243" height="214" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-DryWetComparison.jpg" alt="" class="wp-image-5166" style="width:249px;height:auto"/><figcaption class="wp-element-caption">Photo illustrating the visual difference between wet and dry sand.</figcaption></figure>



<p class="wp-block-paragraph">Wet surfaces appear darker and reflected light is sharp and bright. I can achieve that by reducing the intensity of albedo color and ambient light stored in the Gbuffer[0].RGB and GBuffer[3].RGB. Sharper reflections can be implemented by increasing the smoothness in the alpha channel in GBuffer[2].</p>



<p class="wp-block-paragraph">To test the concept I adjusted material properties. I increased the smoothness and darkened the color. The visual impact matched what I hoped for.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="368" height="435" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-DryWetTest.jpg" alt="" class="wp-image-5167" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-DryWetTest.jpg 368w, https://theknightsofu.com/wp-content/uploads/2025/05/002-DryWetTest-254x300.jpg 254w, https://theknightsofu.com/wp-content/uploads/2025/05/002-DryWetTest-300x355.jpg 300w" sizes="auto, (max-width: 368px) 100vw, 368px" /><figcaption class="wp-element-caption">Materials with darker albedo color and higher smoothness feel more wet.</figcaption></figure>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">Implementation</h2>



<p class="wp-block-paragraph">I broke the implementation into 3 steps:</p>



<ol class="wp-block-list">
<li>Set up the render feature</li>



<li>Create the Render Graph pass</li>



<li>Write the shader</li>
</ol>



<h2 class="wp-block-heading">1. Setting up the render feature</h2>



<p class="wp-block-paragraph">First, I ensured that the project uses a deferred rendering path in the URP renderer settings. I disabled <em>Accurate G-Buffer Normals</em> as this option encodes the normal buffer differently. I want to use standard normal encoding to minimize complexity at this stage.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="521" height="280" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-URPRendererSettings.jpg" alt="" class="wp-image-5168" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-URPRendererSettings.jpg 521w, https://theknightsofu.com/wp-content/uploads/2025/05/002-URPRendererSettings-300x161.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/002-URPRendererSettings-400x215.jpg 400w" sizes="auto, (max-width: 521px) 100vw, 521px" /><figcaption class="wp-element-caption">My URP renderer settings.</figcaption></figure>



<p class="wp-block-paragraph">Then I started by implementing the Scriptable Renderer Feature. <code>RainAtmosphereFeature</code> injects the <code>ApplyRainAtmospherePass</code> into the render pipeline. <code>ApplyRainAtmospherePass</code> is set to be injected after rendering GBuffer by using a <code>RenderPassEvent.AfterRenderingGbuffer</code>. I will implement all rendering logic in the <code>RecordRenderGraph</code> method.</p>



<pre class="wp-block-code"><code>using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

// This class is responsible for injecting the render feature into the URP renderer. 
public class RainAtmosphereFeature : ScriptableRendererFeature
{
	// Material used for rendering
    public Material applyRainMaterial;

    private bool HasValidResources =&gt; applyRainMaterial != null;

	// This pass will handle the gbuffer modification
    private ApplyRainAtmospherePass applyRainAtmospherePass;

    public override void Create()
    {
        applyRainAtmospherePass = new ApplyRainAtmospherePass();
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
        if (isGameOrSceneCamera &amp;&amp; HasValidResources)
        {
	        // Update resources in the pass
            applyRainAtmospherePass.material = applyRainMaterial;
			
			// And add the pass to the renderer
            renderer.EnqueuePass(applyRainAtmospherePass);
        }
    }
}

// This class handles the rendering logic. Here I will build new nodes and insert them into RenderGraph.
public class ApplyRainAtmospherePass : ScriptableRenderPass
{
    public Material material { get; internal set; }

    public ApplyRainAtmospherePass()
    {
        this.renderPassEvent = RenderPassEvent.AfterRenderingGbuffer;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        // TODO: I will implement the rendering logic here
    }</code></pre>



<h3 class="wp-block-heading">2. Creating the Render Graph pass</h3>



<p class="wp-block-paragraph">Here I will define a Render Graph pass that directly modifies the GBuffer contents.</p>



<p class="wp-block-paragraph">URP shares the GBuffer between all passes. It means I can access it using <code>frameData.Get&lt;&gt;()</code> API. The GBuffer reference is stored in the <code>UniversalResourceData.gBuffer</code> and is only initialized when using a deferred path. Otherwise, it may not exist, or it can be set to a null handle. I need to check that before adding a custom node to the render graph.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    // Get GBuffer handles
    UniversalResourceData urpResources = frameData.Get&lt;UniversalResourceData&gt;();
    TextureHandle&#91;] gBuffer = urpResources.gBuffer;

    // Check if GBuffer exists and if it is properly initialized (it will not be initialized in the forward path)
    if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#91;0].Equals(TextureHandle.nullHandle))
        return;
}</code></pre>



<p class="wp-block-paragraph">Once I confirmed GBuffer was valid, I proceeded with the node creation. I will modify GBuffer by executing a fullscreen procedural draw call to alter its contents. I created a raster graph node and a PassData class to store resources required in the render function. Then, I set GBuffer textures as render attachments of the node. GBuffer is an array of textures, so I used the <code>for</code> loop to bind all of them.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	UniversalResourceData urpResources = frameData.Get&lt;UniversalResourceData&gt;();
	TextureHandle&#91;] gBuffer = urpResources.gBuffer;

	if (gBuffer == null || gBuffer.Length == 0 || gBuffer&#91;0].Equals(TextureHandle.nullHandle))
		return;

	// Start creating a render graph node
	using (var builder = renderGraph.AddRasterRenderPass(GetType().Name, out PassData passData))
	{
		// Set all resources needed in a render function
		passData.material = material;

		// Set GBuffer as render attachment
		for (int i = 0; i &lt; gBuffer.Length; i++)
			builder.SetRenderAttachment(gBuffer&#91;i], i, AccessFlags.Write);
	}

}

// Resources used in the render function
internal class PassData
{
	// Material with the shader that will render the effect
	internal Material material;
}</code></pre>



<p class="wp-block-paragraph">For the render function, I kept it minimal: just a <code>DrawProcedural</code> call that uses my material. Since I’m rendering two triangles (a fullscreen quad), I drew 6 vertices. Each of the three consecutive vertices creates a triangle &#8211; two triangles in this case.</p>



<pre class="wp-block-code"><code>...
		for (int i = 0; i &lt; gBuffer.Length; i++)
			builder.SetRenderAttachment(gBuffer&#91;i], i, AccessFlags.Write);

		// Set render function
		builder.SetRenderFunc((PassData passData, RasterGraphContext context) =&gt; RenderFunction(passData, context));
	}
}

private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	// Draw a procedural mesh. The mesh will be defined in a shader, so we specify that we want to render 
	// 6 vertices as triangles using our material.
	context.cmd.DrawProcedural(Matrix4x4.identity, passData.material, 0, MeshTopology.Triangles, 6, 1);
}
...</code></pre>



<h3 class="wp-block-heading">3. Writing the shader</h3>



<p class="wp-block-paragraph">Now, I need to create a shader that will render into GBuffer. I must write a fragment shader that will output colors to three render targets. I prepared a basic shader that contains an empty vertex and fragment shader. We will work on these in the following steps.</p>



<pre class="wp-block-code"><code>Shader "Hidden/ApplyRainAtmosphere"
{
    SubShader
    {
        Pass
        {
            // Disable triangle culling
            Cull Off

            // Disable depth buffer usage
            ZTest Off
            ZWrite Off

            HLSLPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            void vert ()
            {
                // TODO: Draw a fullscreen quad here
            }

            void frag () 
            {
                // TODO: Modify GBuffer here
            }

            ENDHLSL
        }
    }
}</code></pre>



<h4 class="wp-block-heading">Vertex shader</h4>



<p class="wp-block-paragraph">The vertex shader&#8217;s main role is to define each vertex&#8217;s position on the screen. The position of the screen is defined in the Clip Space. In short, <em>x</em>,<em>y</em> defines the screen&#8217;s pixel, where (-1, -1) is one corner of the screen and (1, 1) is a second corner. <em>z</em> defines the depth. The position, as in <em>float4,</em> uses this format: <em>(x*w, y*w, z*w, w)</em>.</p>



<p class="wp-block-paragraph">Notice that when the <em>w</em> component is set to 1, the whole vector simplifies to (x, y, z, 1). We will use that trick in the next step.</p>



<p class="wp-block-paragraph">In the render function, I declared drawing with six vertices, using two triangles. Each of the three consecutive vertices will form a triangle. The illustration below shows how the fullscreen mesh looks like.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="146" height="139" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-ClipSpaceTriangles.jpg" alt="" class="wp-image-5169"/></figure>



<p class="wp-block-paragraph">I stored those vertices in the constant array in the shader. I can access the ID of each vertex using the <em>SV_VertexID</em> semantic. This vertex shader outputs a fullscreen triangle. Here is what the code looks like:</p>



<pre class="wp-block-code"><code>// Fullscreen rectangle in clip space
static const float4 verticesCS&#91;] = 
{
	float4(-1.0, -1.0, 0.0, 1.0),
	float4(-1.0, 1.0, 0.0, 1.0),
	float4(1.0, 1.0, 0.0, 1.0),
	float4(-1.0, -1.0, 0.0, 1.0),
	float4(1.0, 1.0, 0.0, 1.0),
	float4(1.0, -1.0, 0.0, 1.0)
};

void vert (uint IN_VertexID : SV_VertexID, out float4 OUT_PositionCS : SV_Position)
{
	// Output the rectangle to clip space
	OUT_PositionCS = verticesCS&#91;IN_VertexID];
}</code></pre>



<h4 class="wp-block-heading">Blending</h4>



<p class="wp-block-paragraph">Shader will target GBuffer0, GBuffer2, and GBuffer3, corresponding to albedo, normal-smoothness, and ambient light. In Unity shaders, I can specify the blending operation for each target texture using a <code>Blend</code> keyword at the beginning of the pass.</p>



<p class="wp-block-paragraph">Each respective value produced by the fragment shader will affect the GBuffer with its blend operation. This is how I configured the blending for those targets.</p>



<p class="wp-block-paragraph">GBuffer0 = ShaderOutput0 * GBuffer0<br>GBuffer2 = ShaderOutput2 + GBuffer2<br>GBuffer3 = ShaderOutput3 * GBuffer3</p>



<pre class="wp-block-code"><code>...
ZTest Off
ZWrite Off

// Multiply blending for albedo
Blend 0 DstColor Zero
// Additive blending for normal-smoothness
Blend 2 One One
// Multiply blending for ambient light
Blend 3 DstColor Zero

HLSLPROGRAM
...</code></pre>



<h4 class="wp-block-heading">Fragment Shader</h4>



<p class="wp-block-paragraph">Now its a time to make the fragment shader output values into GBuffer. This is done using the <code>SV_Target&lt;ID&gt;</code> semantics. For example, I reduced the albedo and ambient light color by half and add 0.7 to the smoothness.</p>



<pre class="wp-block-code"><code>void frag 
(
	out float4 OUT_GBuffer0 : SV_Target0,
	out float4 OUT_GBuffer2 : SV_Target2,
	out float4 OUT_GBuffer3 : SV_Target3
) 
{
	// Multiply blending - reduce colors by half, but leave the alpha channel as is.
	OUT_GBuffer0 = float4(0.5, 0.5, 0.5, 1.0);

	// Additive blending - leave normals in RGB as is, but add 0.7 to smoothness.
	OUT_GBuffer2 = float4(0.0, 0.0, 0.0, 0.7);

	// Multiply blending - reduce colors by half, but leave the alpha channel as is.
	OUT_GBuffer3 = float4(0.5, 0.5, 0.5, 1.0);
}</code></pre>



<h3 class="wp-block-heading">Result</h3>



<p class="wp-block-paragraph">Shader is ready. It&#8217;s time to create a material with it and assign it to the render feature. You can see that my feature is working correctly. I was so used to the old look of the Viking Village, it is nice to have something fresh!</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="366" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-1024x366.gif" alt="" class="wp-image-5170" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-1024x366.gif 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-300x107.gif 300w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-768x274.gif 768w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-400x143.gif 400w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-600x214.gif 600w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-800x286.gif 800w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-1000x357.gif 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/002-FeatureIsWorkingRain-1280x457.gif 1280w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Altering a GBuffer content is a great way to simulate weather changes in the game.</figcaption></figure>



<p class="wp-block-paragraph">It is also displayed by the Render Graph Viewer (Window/Analysis/Render Graph Viewer) between Draw GBuffer pass and Render Deferred Lighting.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="584" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-1024x584.jpg" alt="" class="wp-image-5171" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-1024x584.jpg 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-300x171.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-768x438.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-400x228.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-600x342.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-800x456.jpg 800w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer-1000x570.jpg 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/002-RenderGraphViewer.jpg 1035w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Render Graph Viewer shows my render pass.</figcaption></figure>



<p class="wp-block-paragraph">In the next article I will fix the bloom flickering caused by the high smoothness. I will also implement the parameters to control the visuals.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="589" height="294" src="https://theknightsofu.com/wp-content/uploads/2025/05/002-BloomFlicker.gif" alt="" class="wp-image-5172"/><figcaption class="wp-element-caption">My render feature causes flickering in a bloom postprocess.</figcaption></figure>



<h2 class="wp-block-heading">About the author</h2>



<p class="wp-block-paragraph">Jan Mróz / Sir Johny – Graphics Programmer at The Knights of U. He specializes in rendering pipeline extensions and performance optimization across CPU, GPU, and memory. Sir Johny also leads tech-art initiatives that improve visual quality while cutting iteration time.</p>



<p class="wp-block-paragraph">Learn more from Jan at <a href="https://proceduralpixels.com">proceduralpixels.com</a>.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/render-graph-creating-rain-atmosphere/">Render Graph &#8211; Creating Rain Atmosphere</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/render-graph-creating-rain-atmosphere/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Memory optimization guide in Unity 6</title>
		<link>https://theknightsofu.com/memory-optimization-guide-in-unity-6/</link>
					<comments>https://theknightsofu.com/memory-optimization-guide-in-unity-6/#respond</comments>
		
		<dc:creator><![CDATA[Sir Rob]]></dc:creator>
		<pubDate>Tue, 27 May 2025 13:00:00 +0000</pubDate>
				<category><![CDATA[Advanced]]></category>
		<category><![CDATA[All]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[Addressables]]></category>
		<category><![CDATA[Asset Optimization]]></category>
		<category><![CDATA[game development]]></category>
		<category><![CDATA[GPU Memory]]></category>
		<category><![CDATA[Managed Memory]]></category>
		<category><![CDATA[Memory Fragmentation]]></category>
		<category><![CDATA[Memory Leak]]></category>
		<category><![CDATA[Memory Management]]></category>
		<category><![CDATA[Memory Profiling]]></category>
		<category><![CDATA[Native Memory]]></category>
		<category><![CDATA[Optimization]]></category>
		<category><![CDATA[Shader Optimization]]></category>
		<category><![CDATA[Texture Compression]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[Unity Memory Profiler]]></category>
		<category><![CDATA[Unity Performance]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5079</guid>

					<description><![CDATA[<p>Have you ever experienced your application crashing with a system out of memory exception? Memory management is an important part of game development and can save you many headaches. In this post we will take a look at how memory is allocated, how you can profile the memory state of your application and finally &#8211; &#8230; <a href="https://theknightsofu.com/memory-optimization-guide-in-unity-6/">Czytaj dalej</a></p>
<p>The post <a href="https://theknightsofu.com/memory-optimization-guide-in-unity-6/">Memory optimization guide in Unity 6</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Have you ever experienced your application crashing with a system out of memory exception? Memory management is an important part of game development and can save you many headaches. In this post we will take a look at how memory is allocated, how you can profile the memory state of your application and finally &#8211; how can you improve it.</p>



<h2 class="wp-block-heading"><strong>How memory works</strong></h2>



<p class="wp-block-paragraph">The operating system is the manager of the memory. It allocates the amount of memory that processes can use. The applications will issue requests to it such as “I need more memory to run, please give it to me” or “I no longer need that bunch of memory, please take it and release it”. </p>



<p class="wp-block-paragraph">Applications usually allocate their memory with some margin and then create a wrapper for such allocated chunks, making this memory available within their application using so-called allocators. When they need more memory than previously allocated margin, they issue a request to the OS to give them more memory. They sometimes can face rejection because the OS cannot give them more resources. This is how the ‘out of memory’ exception is thrown by an application which cannot longer function.</p>



<h2 class="wp-block-heading"><strong>Memory fragmentation</strong></h2>



<p class="wp-block-paragraph">Memory is sequential. It means that if you want to create e.g. array of data, the application allocates memory for that as a non-interrupted memory block. Memory blocks are allocated one after another. Frequent allocations and deallocations can lower the actual available memory for the application.</p>



<p class="wp-block-paragraph">Let’s say that we have this memory block:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryBlock.png" alt="" class="wp-image-5080" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryBlock.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryBlock-300x13.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryBlock-400x17.png 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption class="wp-element-caption">Representation of single memory block.</figcaption></figure>



<p class="wp-block-paragraph">And these memory allocations:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="59" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/Tiny.png" alt="" class="wp-image-5081"/><figcaption class="wp-element-caption">Representation of tiny memory allocation.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="118" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/Small.png" alt="" class="wp-image-5082"/><figcaption class="wp-element-caption">Representation of small memory allocation.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="176" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/Large.png" alt="" class="wp-image-5083"/><figcaption class="wp-element-caption">Representation of large memory allocation.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="235" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/SuperLarge.png" alt="" class="wp-image-5084"/><figcaption class="wp-element-caption">Representation of super large memory allocation.</figcaption></figure>



<p class="wp-block-paragraph">We made some allocations during our application lifetime, so our memory block now looks like this:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/AllocatedMemoryBlock.png" alt="" class="wp-image-5085" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/AllocatedMemoryBlock.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/AllocatedMemoryBlock-300x13.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/AllocatedMemoryBlock-400x17.png 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption class="wp-element-caption">Memory block with couple of allocations.</figcaption></figure>



<p class="wp-block-paragraph">Now we are releasing SuperLarge allocation from memory, so it looks like this:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/WithoutSuperLarge.png" alt="" class="wp-image-5086" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/WithoutSuperLarge.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/WithoutSuperLarge-300x13.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/WithoutSuperLarge-400x17.png 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption class="wp-element-caption">Memory block with SuperLarge allocation released.</figcaption></figure>



<p class="wp-block-paragraph">Next we allocate a bunch of smaller allocations:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="25" src="https://theknightsofu.com/wp-content/uploads/2025/04/AnotherAllocations.png" alt="" class="wp-image-5087" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/AnotherAllocations.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/AnotherAllocations-300x13.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/AnotherAllocations-400x17.png 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption class="wp-element-caption">Memory block with new allocations.</figcaption></figure>



<p class="wp-block-paragraph">Now we want to allocate another SuperLarge allocation. There is a space available between the last Tiny and Large allocation, but there is too small room for uninterrupted SuperLarge allocation. Our application needs to issue a request to the OS to grant it more memory space. After that our memory block with allocated another SuperLarge allocation looks like this:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="600" height="51" src="https://theknightsofu.com/wp-content/uploads/2025/04/Fragmentation.png" alt="" class="wp-image-5088" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/Fragmentation.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/Fragmentation-300x26.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/Fragmentation-400x34.png 400w" sizes="auto, (max-width: 600px) 100vw, 600px" /><figcaption class="wp-element-caption">Representation of memory fragmetation.</figcaption></figure>



<p class="wp-block-paragraph">That gap in the memory block is called memory fragmentation.</p>



<p class="wp-block-paragraph">Unity&#8217;s Garbage Collector has the ability to move blocks of memory in order to prevent memory from fragmentation, but it’s related only to Managed memory &#8211; we will cover it in the next chapter. It has no such power when it comes to Native memory &#8211; frequently allocation of that memory can cause memory fragmentation.</p>



<p class="wp-block-paragraph">Below you can see examples of fragmented and non-fragmented memory. See how nice each memory allocation is lined up in the memory block when there is no (or little to no) fragmentation.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1004" height="55" src="https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory.png" alt="" class="wp-image-5089" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory.png 1004w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-300x16.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-768x42.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-400x22.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-600x33.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-800x44.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/FragmentedMemory-1000x55.png 1000w" sizes="auto, (max-width: 1004px) 100vw, 1004px" /><figcaption class="wp-element-caption">Fragmented memory.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1004" height="205" src="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation.png" alt="" class="wp-image-5090" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation.png 1004w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-300x61.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-768x157.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-400x82.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-600x123.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-800x163.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryWithoutFragmentation-1000x204.png 1000w" sizes="auto, (max-width: 1004px) 100vw, 1004px" /><figcaption class="wp-element-caption">Memory without fragmentation.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Types of memory</strong></h2>



<p class="wp-block-paragraph">There are couple types of memory allocating by Unity:</p>



<ul class="wp-block-list">
<li><strong>Native Memory</strong></li>



<li><strong>Executables and Mapped</strong></li>



<li><strong>Managed Memory</strong></li>



<li><strong>GPU Driver Memory</strong></li>



<li><strong>Untracked Memory</strong></li>
</ul>



<p class="wp-block-paragraph"><strong>Native Memory</strong> is a part of memory allocated by Unity itself and build-in memory allocator. It is used by objects such as:</p>



<ul class="wp-block-list">
<li>Scene Objects (Game Objects and their Components),</li>



<li>Assets and Managers</li>



<li>Native Allocations including Native Arrays and other Native Containers</li>



<li>CPU side of Graphics Asset memory</li>



<li>And other</li>
</ul>



<p class="wp-block-paragraph">This is a memory which is not handled by the Garbage Collector.</p>



<p class="wp-block-paragraph"><strong>Executables and Mapped</strong> is memory taken up by the build code of the application, including all shared libraries and assemblies, managed and native.</p>



<p class="wp-block-paragraph"><strong>Managed Memory</strong> is a runtime C# memory. Here you will find all created lists, arrays, hashsets, class objects etc.</p>



<p class="wp-block-paragraph"><strong>GPU Driver Memory</strong> &#8211; memory allocated by the GPU driver for the purpose of rendering. Here lies most of the textures, meshes etc. </p>



<p class="wp-block-paragraph"><strong>Untracked Memory</strong> &#8211; memory allocated by third party plugins which Unity cannot track through its DLLs. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="223" src="https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-1024x223.png" alt="" class="wp-image-5091" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-1024x223.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-300x65.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-768x167.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-400x87.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-600x131.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-800x174.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-1000x218.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory-1280x279.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/TypesOfMemory.png 1322w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Types of memory shown in Memory Profiler.</figcaption></figure>



<h2 class="wp-block-heading"><strong>What is allocated in my project?</strong></h2>



<p class="wp-block-paragraph">Now that we know how allocation in general works and what type of memory there is in Unity, let&#8217;s see how Unity is handling allocating particular objects.</p>



<p class="wp-block-paragraph">The base principle is that Unity allocates what is inside a loaded scene and all references to any object which are held by loaded objects. What does it mean?</p>



<p class="wp-block-paragraph">Let’s say that we are loading a scene with 5 objects. These objects are obviously loaded into memory. Now one of these objects has a MonoBehaviour script which contains a reference to a ScriptableObject. This ScriptableObject on the other hand has a reference to a mesh which is not used in the loaded scene. Result? The mesh references by the ScriptableObject will be loaded to the memory and kept in there until one part of the reference chain will be cut. This will probably happen after the scene was unloaded. It would be much worse if ScriptableObject would be referenced to a Singleton with DontDestroyOnLoad flag enabled. In that case the mesh would not be unloaded from the memory event after unloading the scene. </p>



<pre class="wp-block-code"><code>using UnityEngine;


public class SomeMonobehaviourWithReference : MonoBehaviour
{
   public Object reference;
}</code></pre>



<pre class="wp-block-code"><code>using UnityEngine;


&#91;CreateAssetMenu(fileName = "SomeScriptableObjectWithReference", menuName = "Scriptable Objects/SomeScriptableObjectWithReference")]
public class SomeScriptableObjectWithReference : ScriptableObject
{
   public Object reference;
}</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="265" src="https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-1024x265.png" alt="" class="wp-image-5092" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-1024x265.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-300x78.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-768x199.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-400x103.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-600x155.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-800x207.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-1000x259.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference-1280x331.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/MonoBehaviorWithReference.png 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Test Scene setup with Monobehaviour containing reference to ScriptableObject.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="671" height="207" src="https://theknightsofu.com/wp-content/uploads/2025/04/SOWithReference.png" alt="" class="wp-image-5093" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/SOWithReference.png 671w, https://theknightsofu.com/wp-content/uploads/2025/04/SOWithReference-300x93.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/SOWithReference-400x123.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/SOWithReference-600x185.png 600w" sizes="auto, (max-width: 671px) 100vw, 671px" /><figcaption class="wp-element-caption">Scriptable Object with a reference.</figcaption></figure>



<p class="wp-block-paragraph">This example on a large scale can cause serious memory issues, in particular if there are references to objects which hold all game logic. How can we solve this? There are plenty of solutions, one of them is to use the Addressables system and load objects only when you need it. </p>



<p class="wp-block-paragraph">Instead of having hard reference to a mesh, we could use <code>AssetReference&lt;TObject></code>:</p>



<pre class="wp-block-code"><code>&#91;SerializeField] private AssetReference meshReference;</code></pre>



<p class="wp-block-paragraph">Now we can use <code>LoadAssetAsync&lt;TObject>()</code> to load our Asset Reference:</p>



<pre class="wp-block-code"><code>AsyncOperationHandle handle = meshReference.LoadAssetAsync&lt;Mesh>();</code></pre>



<p class="wp-block-paragraph">And <code>ReleaseAsset()</code> to release it from memory:</p>



<pre class="wp-block-code"><code>meshReference.ReleaseAsset();</code></pre>



<p class="wp-block-paragraph">Besides references to assets, Unity always loads the contents of the Resource folder to the memory. Therefore using it is nowadays considered a bad practice.</p>



<h2 class="wp-block-heading"><strong>My game has huge memory allocations. How to get rid of them?</strong></h2>



<p class="wp-block-paragraph">First you need to diagnose a source of the problem. You can do it by using Memory Profiler. </p>



<p class="wp-block-paragraph">You can install Memory Profiler via Package Manager. </p>



<p class="wp-block-paragraph">Before we jump in, you have to take memory snapshots of your project. There are couple of basic rules in terms of memory profiling: </p>



<ol class="wp-block-list">
<li><strong>Always profile on the built project, on your target platform</strong>. Every platform has its own unique memory management, therefore profiling your project in the Editor will not give you relevant data. In fact, you will be profiling the Editor rather than your game. So make the build for your target platform, run it and then profile it.</li>



<li><strong>Make a couple of snapshots from a single gameplay.</strong> Every time you run a build, your applications memory can act differently. If you want to know what is a memory flow throughout your entire application you should make a couple of snapshots of a single gameplay in key points, e.g.:
<ul class="wp-block-list">
<li>from the main menu after opening a game &#8211; this will give you knowledge about base memory loaded at the beginning,</li>



<li>from the gameplay scene, right after loading it from the main menu &#8211; this will show you what you are loading at the beginning of the gameplay loop</li>



<li>at the end of the gameplay session &#8211; this will show you which parts are constant in the memory and which are unloading</li>



<li>in the main menu, after the gameplay session &#8211; this will tell you if gameplay assets are properly unloaded after quitting to the main menu.</li>
</ul>
</li>
</ol>



<p class="wp-block-paragraph">Additionally you can add a couple of snapshots throughout the gameplay to inspect your memory behaviour in particular places of your game that you are interested in examining.</p>



<ol start="3" class="wp-block-list">
<li><strong>If possible, make memory snapshots on the device with extended RAM.</strong> If you struggle with quickly reaching maximum memory capacity of a device, you would like to profile it on a device with extended RAM in order to be able to make proper snapshots and diagnose the problem.</li>



<li><strong>If possible, use different tools to profile memory alongside Memory Profiler.</strong> Memory Profiler can only estimate the memory usage and sometimes does not show everything. That is why you should compare its output with other tools.</li>
</ol>



<p class="wp-block-paragraph">The newest version of Memory Profiler contains 3 tabs:</p>



<ol class="wp-block-list">
<li>Summary &#8211; overall visualization of used memory</li>



<li>Unity Objects &#8211; list of all Unity objects with their allocated size, percentage impact to all memory, native size, managed size and graphic size.</li>



<li>All Of Memory &#8211; list of all objects allocated in memory with division to memory type.</li>
</ol>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="955" src="https://theknightsofu.com/wp-content/uploads/2025/04/Summary-1024x955.png" alt="" class="wp-image-5094" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/Summary-1024x955.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-300x280.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-768x716.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-400x373.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-600x560.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-800x746.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-1000x933.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary-1280x1194.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/Summary.png 1367w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Memory Profiler Summary tab.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="920" height="1024" src="https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-920x1024.png" alt="" class="wp-image-5095" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-920x1024.png 920w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-269x300.png 269w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-768x855.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-300x334.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-400x445.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-600x668.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-800x891.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-1000x1113.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects-1280x1425.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/UnityObjects.png 1358w" sizes="auto, (max-width: 920px) 100vw, 920px" /><figcaption class="wp-element-caption">Memory Profiler Unity Objects tab.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="344" src="https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-1024x344.png" alt="" class="wp-image-5096" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-1024x344.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-300x101.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-768x258.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-400x135.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-600x202.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-800x269.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-1000x336.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory-1280x430.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/AllOfMemory.png 1359w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Memory Profiler All Of Memory tab.</figcaption></figure>



<p class="wp-block-paragraph">You can view snapshots in single mode as well as in compare mode. This is useful if you want to show differences between particular parts of your game. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="959" src="https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1024x959.png" alt="" class="wp-image-5097" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1024x959.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-300x281.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-768x719.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1536x1438.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-400x375.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-600x562.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-800x749.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1000x936.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1280x1199.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode-1520x1423.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/ProfilerCompareMode.png 1618w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Unity Objects tab in Compare Mode.</figcaption></figure>



<p class="wp-block-paragraph">After inspecting memory snapshots you can evaluate what is the root problem in your case. Is it too much of a base memory and assets are just taking up too much space in memory? Or maybe there are some memory leaks throughout gameplay which are causing crashes? Let&#8217;s see how we can resolve each case.</p>



<h2 class="wp-block-heading"><strong>What’s taking up so much?</strong></h2>



<p class="wp-block-paragraph">During memory optimization, the first thing you should do is to check which assets are taking up the most memory and how you can improve it. You just want to reach for the low hanging fruits at first as it is the easiest way to improve memory management in your project by lowering the base memory amount. So what could it be?</p>



<h3 class="wp-block-heading"><strong>Texture2D</strong></h3>



<p class="wp-block-paragraph">The size of texture in RAM depends solely on the:</p>



<ol class="wp-block-list">
<li>Texture’s resolution</li>



<li>Texture’s format</li>



<li>Number of mip-maps</li>
</ol>



<p class="wp-block-paragraph"><strong>Texture’s size in RAM does not depend on Crunch compression! </strong>Crunch compression has only an impact on the texture’s size in build. After loading such a texture to RAM it is ‘unpacked’. So the size of a RGB Compressed DXT1 texture and RGB Crunched DXT1 texture is the same in RAM, even if Editor is claiming differently. It is good to keep that in mind.</p>



<p class="wp-block-paragraph">It is a good practice to keep the texture&#8217;s resolution no bigger than the resolution of your target device. The 4K texture will not show all of its details on a 720p screen. You adjust the max size of a texture in the texture importer in the Editor. </p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="785" height="234" src="https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter.png" alt="" class="wp-image-5098" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter.png 785w, https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter-300x89.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter-768x229.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter-400x119.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/TextureImporter-600x179.png 600w" sizes="auto, (max-width: 785px) 100vw, 785px" /><figcaption class="wp-element-caption">Applying size and format for particular platform in Texture Importer.</figcaption></figure>



<p class="wp-block-paragraph">You can also set a format which texture will be compressed to. The most important thing to remember about that is the resolution of compressed textures has to be a multiple of the size of a compression block. It means that e.g. DXT1 compression which has a 4&#215;4 compression block requires the resolution of the compressed texture to be a multiple of 4.</p>



<p class="wp-block-paragraph"><strong>An exception to that rule is ASTC compression which can compress everything, except a texture with a mipmaps which is not a power-of-two.</strong></p>



<p class="wp-block-paragraph">If a texture has mip-maps, in order to be compressible, it has to have a power-of-two resolution. So a 800&#215;1200 texture with mip-maps cannot be compressed, however it is possible with a 1024&#215;1024 texture.</p>



<p class="wp-block-paragraph">You can see it in the following example. A 256&#215;255 texture is marked as NPOT and cannot be compressed.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="703" height="915" src="https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample.png" alt="" class="wp-image-5099" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample.png 703w, https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample-230x300.png 230w, https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample-300x390.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample-400x521.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/NPOTExample-600x781.png 600w" sizes="auto, (max-width: 703px) 100vw, 703px" /><figcaption class="wp-element-caption">Example of NPOT texture.</figcaption></figure>



<p class="wp-block-paragraph">After adding just one row of pixels it becomes POT and compressible, which reduces its size almost six times.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="704" height="944" src="https://theknightsofu.com/wp-content/uploads/2025/04/POTExample.png" alt="" class="wp-image-5100" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/POTExample.png 704w, https://theknightsofu.com/wp-content/uploads/2025/04/POTExample-224x300.png 224w, https://theknightsofu.com/wp-content/uploads/2025/04/POTExample-300x402.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/POTExample-400x536.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/POTExample-600x805.png 600w" sizes="auto, (max-width: 704px) 100vw, 704px" /><figcaption class="wp-element-caption">There&#8217;s no difference in visuals but now we can compress it!</figcaption></figure>



<p class="wp-block-paragraph">You can also tell the texture importer to automatically scale NPOT texture. The options are: 1) ToNearest 2) ToLarger 3) ToSmaller. It is a convenient way to make sure that imported texture can be compressed, although sometimes you will want to do it manually for better control.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="703" height="217" src="https://theknightsofu.com/wp-content/uploads/2025/04/AutoScaleNPOT.png" alt="" class="wp-image-5101" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/AutoScaleNPOT.png 703w, https://theknightsofu.com/wp-content/uploads/2025/04/AutoScaleNPOT-300x93.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/AutoScaleNPOT-400x123.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/AutoScaleNPOT-600x185.png 600w" sizes="auto, (max-width: 703px) 100vw, 703px" /><figcaption class="wp-element-caption">NPOT auto scale menu in Texture Importer.</figcaption></figure>



<p class="wp-block-paragraph">Below you have a cheat sheet showing which textures can be compressed in relation to mip-maps.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td></td><td class="has-text-align-left" data-align="left"><strong>Not supported resolution (e.g. 193&#215;921)</strong></td><td><strong>Multiple by 4 resolution (e.g. 356&#215;272)</strong></td><td><strong>Power-of-two resolution (e.g. 256&#215;256)</strong></td></tr><tr><td><strong>Mip mapy enabled</strong></td><td class="has-text-align-left" data-align="left"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-danger-500-color">no compression</mark></td><td><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-danger-500-color">no compression</mark></td><td>compression available</td></tr><tr><td><strong>Mip mapy disabled</strong></td><td class="has-text-align-left" data-align="left"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-danger-500-color">no compression*</mark></td><td>compression available</td><td>compression available</td></tr></tbody></table><figcaption class="wp-element-caption"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-danger-500-color">*some formats, e.g. ASTC allows compression in this case</mark></figcaption></figure>



<h2 class="wp-block-heading"><strong>Mesh</strong></h2>



<p class="wp-block-paragraph">Meshes can consume a large amount of project memory, in particular if they are complex and huge in number.</p>



<p class="wp-block-paragraph">There are a couple things you can do to reduce the size of meshes:</p>



<ol class="wp-block-list">
<li>If your mesh is not using normal maps or any other effects based on tangent space, you can go ahead and turn off importing tangents.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="693" height="233" src="https://theknightsofu.com/wp-content/uploads/2025/04/MeshTangents.png" alt="" class="wp-image-5102" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MeshTangents.png 693w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshTangents-300x101.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshTangents-400x134.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshTangents-600x202.png 600w" sizes="auto, (max-width: 693px) 100vw, 693px" /><figcaption class="wp-element-caption">Setting Tangents to None in Mesh Importer.</figcaption></figure>



<ol start="2" class="wp-block-list">
<li>If your mesh has animations that are not used, you can turn off Import Blend Shapes.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="698" height="148" src="https://theknightsofu.com/wp-content/uploads/2025/04/MeshBlendShapes.png" alt="" class="wp-image-5104" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MeshBlendShapes.png 698w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshBlendShapes-300x64.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshBlendShapes-400x85.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshBlendShapes-600x127.png 600w" sizes="auto, (max-width: 698px) 100vw, 698px" /><figcaption class="wp-element-caption">Setting Import Blend Shapes to false in Mesh Importer.</figcaption></figure>



<ol start="3" class="wp-block-list">
<li>Remember to keep the Read/Write enabled flag to false if you do not need it (e.g. if you do not want to modify it in runtime). If you enable this option, mesh will be duplicated and kept in RAM as well as in GPU memory.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="701" height="125" src="https://theknightsofu.com/wp-content/uploads/2025/04/MeshReadWrite-1.png" alt="" class="wp-image-5105" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MeshReadWrite-1.png 701w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshReadWrite-1-300x53.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshReadWrite-1-400x71.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MeshReadWrite-1-600x107.png 600w" sizes="auto, (max-width: 701px) 100vw, 701px" /><figcaption class="wp-element-caption">Setting Read/Write to false in Mesh Importer.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Animation</strong></h2>



<p class="wp-block-paragraph">You can decrease the animation size of imported models by enabling Animation Compression. This way Unity will omit some of the unnecessary keyframes.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="230" src="https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1024x230.png" alt="" class="wp-image-5106" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1024x230.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-300x67.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-768x173.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1536x346.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-400x90.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-600x135.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-800x180.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1000x225.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1280x288.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1520x342.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression-1900x427.png 1900w, https://theknightsofu.com/wp-content/uploads/2025/04/AnimationCompression.png 1929w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Animation Compression settings in Mesh Importer.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Shader</strong></h2>



<p class="wp-block-paragraph">Every shader can have multiple variants, meaning it can be compiled multiple times and used for different features. In practice there can be a situation when some of the shader variants wont be necessary &#8211; it depends on the number of lights, their settings or quality settings. That is why it is important to track shader variants and control them. Unity often cannot tell which variants to strip, aka exclude from the build, so you have to do this on your own.</p>



<p class="wp-block-paragraph">You can do it by adding IPreprocessShaders script to your project, like so:</p>



<pre class="wp-block-code"><code>using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEditor.Build;
using UnityEditor.Rendering;


public class ShaderStripper : IPreprocessShaders
{
   private List&lt;ShaderKeyword> keywords = new List&lt;ShaderKeyword>()
   {
       new ShaderKeyword("POINT_COOKIE"),
       new ShaderKeyword("DIRECTIONAL_COOKIE"),
   };


   public int callbackOrder { get { return 0; } }


   public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList&lt;ShaderCompilerData> data)
   {
       StripVariants(shader, snippet, data, keywords);
   }


   private void StripVariants(Shader shader, ShaderSnippetData snippet, IList&lt;ShaderCompilerData> data, List&lt;ShaderKeyword> keywordsToStrip)
   {
       for (int i = 0; i &lt; data.Count; ++i)
       {
           if (keywordsToStrip.Any(keyword => data&#91;i].shaderKeywordSet.IsEnabled(keyword)))
           {
               data.RemoveAt(i);
               --i;
           }
       }
   }
}</code></pre>



<p class="wp-block-paragraph">It is enough to have it laying somewhere in your project &#8211; it will be executed during compilation.</p>



<p class="wp-block-paragraph">You can track the number of shaders and shader variants used in your project via Project Settings -> Graphics window. Go into Play Mode and play your game. Notice how those numbers are increasing each time another shader variant needs to be loaded. If you are satisfied with the current state, exit Play Mode and click on the ‘Save to asset…’ button to save the list of shader variants to the .shadervariants file. Next you can view this file in Inspector and check which shader variants were being loaded. </p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="221" src="https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-1024x221.png" alt="" class="wp-image-5107" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-1024x221.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-300x65.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-768x166.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-400x86.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-600x129.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-800x173.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants-1000x216.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariants.png 1265w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Shader Variants tracker in Project Settings -> Graphics window.</figcaption></figure>



<p class="wp-block-paragraph">Based on that you should establish which shader variant can be stripped. Just add appropriate keywords to your script and you should notice much lighter shaders in the build.</p>



<p class="wp-block-paragraph">There is also another ‘magic’ setting in Unity which can improve memory usage of shader variants called Shader Variant Loading Settings. You can find it in Project Settings -> Player Settings -> Other Settings.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="144" src="https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-1024x144.png" alt="" class="wp-image-5108" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-1024x144.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-300x42.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-768x108.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-400x56.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-600x84.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-800x113.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading-1000x141.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/ShaderVariantLoading.png 1280w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Shader Variant Loading Settings.</figcaption></figure>



<p class="wp-block-paragraph">Unity stores several ‘chunks’ of compressed shader variant data. Each chunk contains multiple shader variants. When Unity loads a scene at runtime, it loads all the scene’s chunks into CPU memory and decompresses them. By default, Unity decompresses all the shader variants into another area of CPU memory. Then Unity passes the shader variant and its data to the graphics API and the graphics driver. The graphics driver creates a GPU-specific version of the shader variant and uploads it to the GPU. Unity caches each GPU-specific shader variant, to avoid another stall when Unity needs the shader variant again. Finally, Unity removes the shader variant completely from CPU and GPU memory when there are no longer any objects that reference the shader variant.</p>



<p class="wp-block-paragraph">With Shader Variant Loading Settings you can control the size of chunks and how many decompressed chunks Unity keeps in memory. </p>



<ul class="wp-block-list">
<li><strong>Default chunk size (MB)</strong> controls the maximum size of compressed chunks.</li>



<li><strong>Default chunk count</strong> controls how many decompressed chunks Unity keeps in memory. The default is 0, which means there’s no limit.</li>
</ul>



<p class="wp-block-paragraph">You can override those values for every platform. If you decide to override it, you should experiment with different values to see how it impacts your project&#8217;s runtime. This setting is useful in particular on platforms with limited memory.</p>



<h2 class="wp-block-heading"><strong>What about memory leaks?</strong></h2>



<p class="wp-block-paragraph">If memory usage of your application is consistently and substantially rising during the gameplay loop, you are probably dealing with memory leak.</p>



<p class="wp-block-paragraph">Memory leak is a state where there are objects in the memory which will no longer be used. This situation can arise because of couple of reasons:</p>



<ol class="wp-block-list">
<li>There is a reference to an object which will no longer be used and GC cannot release it from memory because of said reference.</li>



<li>There are structures used which require manual allocation and deallocation and the latter is not executing.</li>
</ol>



<p class="wp-block-paragraph">To detect a memory leak you should profile the memory of your application during the main game loop. Remember about profiling principles which we covered earlier. Analyze and compare memory snapshots after a couple of main game loop iterations. If memory is rising roughly 10-20MB then it is likely no problem &#8211; could be just fragmentation. If it is much more, e.g. 100MB then it is probably a memory leak.</p>



<p class="wp-block-paragraph">Here we can see a huge memory leak. These two snapshots were taken in the main menu of the game &#8211; first one at the beginning of the gameplay, second after a couple of main game loop iterations.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="696" src="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1024x696.png" alt="" class="wp-image-5109" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1024x696.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-300x204.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-768x522.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1536x1044.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-400x272.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-600x408.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-800x544.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1000x680.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1280x870.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1520x1033.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks-1900x1291.png 1900w, https://theknightsofu.com/wp-content/uploads/2025/04/MemoryLeaks.png 1917w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Memory Profiler Summary tab in Compare Mode.</figcaption></figure>



<p class="wp-block-paragraph">You can also enable memory leaks detection in the Editor but it refers only to native allocation, such as NativeArray, UnsafeList, UnsafeParallelHashSet, AllocatorManager.Allocate() etc.</p>



<p class="wp-block-paragraph">You can turn it on in the Preferences -> Jobs tab. Native memory leaks will be printed in the console as errors.</p>



<h2 class="wp-block-heading"><strong>How to fix a memory leak?</strong></h2>



<p class="wp-block-paragraph">To find a particular memory leak, you should open a memory snapshot in single mode and move to All Of Memory tab. You can actually type ‘leaked’ in the search bar and the list below will show all managed shell leaks (they are marked as ‘Leaked Managed Shell’).</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="841" src="https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-1024x841.png" alt="" class="wp-image-5110" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-1024x841.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-300x246.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-768x631.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-400x329.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-600x493.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-800x657.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-1000x821.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks-1280x1051.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/SearchingLeaks.png 1517w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Managed leaks can be found in All Of Memory tab.</figcaption></figure>



<p class="wp-block-paragraph">Let’s take a look at a simple example of a memory leak that was prepared for the need of this post. Looking at the memory snapshot we can see that there are 140 objects called ‘Bullet’ leaked.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="925" height="1024" src="https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-925x1024.png" alt="" class="wp-image-5111" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-925x1024.png 925w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-271x300.png 271w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-768x850.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-1387x1536.png 1387w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-300x332.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-400x443.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-600x664.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-800x886.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-1000x1107.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked-1280x1417.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletsLeaked.png 1503w" sizes="auto, (max-width: 925px) 100vw, 925px" /><figcaption class="wp-element-caption">Diving deeper into memory leaks.</figcaption></figure>



<p class="wp-block-paragraph">Let’s select one of them and focus on the right side of the memory profiler window.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="633" height="1024" src="https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-633x1024.png" alt="" class="wp-image-5112" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-633x1024.png 633w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-186x300.png 186w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-768x1242.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-300x485.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-400x647.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-600x970.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails-800x1294.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/BulletLeakDetails.png 909w" sizes="auto, (max-width: 633px) 100vw, 633px" /><figcaption class="wp-element-caption">You can track hierarchy of references on the right side panel.</figcaption></figure>



<p class="wp-block-paragraph">We can see References and Selection Details sections. As you can see under the Selection Details, Unity is even instructing us why this object is a leak. It appears that it was supposed to be destroyed but Garbage Collector was not able to release it because the object is still being referenced somewhere. Let’s focus on the Referenced By tab above.</p>



<p class="wp-block-paragraph">How to read that tree?</p>



<ol class="wp-block-list">
<li>Reference to List&lt;Bullet> is stored in object of type Weapon</li>



<li>Reference to object of type Weapon is stored in object of type Entity</li>



<li>Reference to Entity is stored in a List&lt;Entity></li>



<li>List&lt;Entity> is stored in a type of EntitiesManager as a ‘Static field type reference on Type EntitiesManager’</li>
</ol>



<p class="wp-block-paragraph">We have reached the root of our problem &#8211; it is the static reference to List&lt;Entity> in EntitiesManager. What is happening here is Entities objects were destroyed after unloading a scene where they were instantiated but they are still referenced and therefore were not released from memory. What is more, Entity kept a reference to another object &#8211; Weapon and Weapon kept a reference to Bullet objects, so they were also not released from memory.</p>



<pre class="wp-block-code"><code>public class EntitiesManager : MonoBehaviour
{
   public static List&lt;Entity> Entities { get; private set; } = new List&lt;Entity>();

   ...
}</code></pre>



<p class="wp-block-paragraph">After properly releasing static references, everything should be fine.</p>



<h2 class="wp-block-heading"><strong>Summary</strong></h2>



<p class="wp-block-paragraph">As you can see, there are different possible sources of memory issues, as well as many tools and practices to fight them. As with every kind of optimization, the most important is to correctly recognize the root of an issue. I hope the knowledge presented in this post helps you in properly managing memory in your project.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/memory-optimization-guide-in-unity-6/">Memory optimization guide in Unity 6</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/memory-optimization-guide-in-unity-6/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Render Graph &#8211; Introduction</title>
		<link>https://theknightsofu.com/render-graph-introduction/</link>
					<comments>https://theknightsofu.com/render-graph-introduction/#respond</comments>
		
		<dc:creator><![CDATA[Sir Johny]]></dc:creator>
		<pubDate>Tue, 20 May 2025 13:00:00 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Rendering]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[Custom Rendering Nodes]]></category>
		<category><![CDATA[GPU optimization]]></category>
		<category><![CDATA[Graphics Programming]]></category>
		<category><![CDATA[Render Graph]]></category>
		<category><![CDATA[Render Pipeline]]></category>
		<category><![CDATA[Scriptable Render Pipeline]]></category>
		<category><![CDATA[Shader Graph]]></category>
		<category><![CDATA[unity 6]]></category>
		<category><![CDATA[Unity Performance]]></category>
		<category><![CDATA[Unity Rendering]]></category>
		<category><![CDATA[URP]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5145</guid>

					<description><![CDATA[<p>Render Graph is a new rendering API available from Unity 6.0. In Unity 6.1, it is the only way of extending URP with custom rendering features. In this article, I will cover the basics of Render Graph API.</p>
<p>The post <a href="https://theknightsofu.com/render-graph-introduction/">Render Graph &#8211; Introduction</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Render Graph is a new rendering API available from Unity 6.0. In Unity 6.1, it is the only way of extending URP with custom rendering features. In this article, I will cover the basics of Render Graph API.</p>



<p class="wp-block-paragraph">Despite the name, Render Graph isn&#8217;t a node-based visual editor &#8211; it&#8217;s a programming interface. Its name originated from how it stores the data about the render pipeline. This new programming API works by implementing custom rendering &#8220;nodes&#8221; and adding them to the Render Graph.</p>



<h2 class="wp-block-heading">Why Unity needed Render Graph in the first place.</h2>



<p class="has-body-large-d-font-size wp-block-paragraph">Before Render Graph came in, I used <code>ScriptableRenderPasses</code> in URP, where I implemented most of the rendering logic imperatively using various methods. While this approach gave us programmers a lot of freedom, it completely broke down at scale. Larger projects became fragile and hard to debug. There was no organized way to store references to the GPU resources, which resulted in a tremendous mess when we needed to access many resources used by URP or different rendering features, the maintenance cost was high. While URP source code and different rendering features started to grow, no uniform way of managing resources became the most significant source of code spaghettification. Do you need an access to the ColorBuffer? Fine &#8211; override <code>ScriptableRendererFeature.SetupRenderPasses(...)</code> and get it there. Do you need to access a GBuffer? I hope you like <code>System.Reflection</code> and internal hacks.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="482" height="274" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-GBufferMeme.jpg" alt="" class="wp-image-5147" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/001-GBufferMeme.jpg 482w, https://theknightsofu.com/wp-content/uploads/2025/05/001-GBufferMeme-300x171.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/001-GBufferMeme-400x227.jpg 400w" sizes="auto, (max-width: 482px) 100vw, 482px" /><figcaption class="wp-element-caption">Fortunately that&#8217;s not true when using Render Graph.</figcaption></figure>



<p class="wp-block-paragraph">Since Vulkan and DirectX12, a programmer is responsible for allocating and managing GPU memory using the graphics API. There is a lot of planning, pipeline state caching, and resource reusing. Render Graph API makes this much easier to handle as it introduces declarative scheduling, automatic resource lifetime management and GPU-aware optimizations, like pass culling and tile-based rendering.</p>



<h3 class="wp-block-heading">Render Graph Caveats</h3>



<p class="wp-block-paragraph"><strong>When the Render Graph becomes strong in larger projects, it has some caveats:</strong></p>



<ul class="wp-block-list">
<li><strong>Steep learning curve</strong> &#8211; the previous model was more imperative and less restricted, while the new one is more declarative and requires the developer to better understand rendering concepts like pass culling and resource management.</li>



<li><strong>Debugging complexity</strong> &#8211; The graph is compiled and optimized under the hood. It is harder to debug rendering issues, often it is just a guessing nightmare with manual checking of all the dependencies. Render Graph lacks the basic debugging tools. We have Render Graph Viewer, but it helps only when everything already works fine.</li>



<li><strong>For simpler effects, it is boilerplate-heavy</strong> and may be unintuitive. Blitting and drawing a single mesh now requires four levels of ceremony.</li>



<li><strong>Performance implications</strong> &#8211; Pass merging and tile-based rendering are great, but declarative scheduling makes fine-tuned control harder.</li>
</ul>



<h2 class="wp-block-heading">How to use Render Graph?</h2>



<p class="wp-block-paragraph">Programming with RenderGraph API requires us to create custom rendering nodes, called <em>Passes</em> and add them into a Render Graph. To familiarize yourself with the concept, I analyzed how I create a custom node using visual tools like Shader Graph. In case of creating a Custom Function node in the Shader graph, I need to:</p>



<ol class="wp-block-list">
<li>Create the node</li>



<li>Define inputs.</li>



<li>Define outputs.</li>



<li>Define a function that creates outputs from the inputs.</li>



<li>Connect the node somewhere in the graph &#8211; find the resources I want to use.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="544" height="594" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-CustomFunctionNode.png" alt="" class="wp-image-5148" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/001-CustomFunctionNode.png 544w, https://theknightsofu.com/wp-content/uploads/2025/05/001-CustomFunctionNode-275x300.png 275w, https://theknightsofu.com/wp-content/uploads/2025/05/001-CustomFunctionNode-300x328.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/001-CustomFunctionNode-400x437.png 400w" sizes="auto, (max-width: 544px) 100vw, 544px" /><figcaption class="wp-element-caption">Creating a custom function node.</figcaption></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="838" height="323" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-ConnectingNode.gif" alt="" class="wp-image-5150"/><figcaption class="wp-element-caption">Connecting the created node into the shader graph.</figcaption></figure>



<p class="wp-block-paragraph">When programming with Render Graph API, <strong>all those steps are done using the C# code</strong> in the <code>ScriptableRenderPass.</code> In the next section, I will describe how to do that.</p>



<h2 class="wp-block-heading">How I implemented my first render graph node!</h2>



<p class="wp-block-paragraph">I will implement a basic render feature to draw a mesh into a screen using a material. The setup for the <code>ScriptableRendererFeature</code> and pass is identical to the old URP system — nothing new.</p>



<pre class="wp-block-code"><code>using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering;

// This class is responsible for injecting the render feature into the URP renderer. 
public class RenderIntoColorBufferFeature : ScriptableRendererFeature
{
    // Mesh and material that will be used for rendering.
    public Mesh mesh;
    public Material material;

    // This pass will handle the rendering logic
    private RenderIntoColorBufferPass pass;
    
    public override void Create()
    {
	    // Create passes in this method
        pass = new RenderIntoColorBufferPass();
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        bool isGameOrSceneCamera = renderingData.cameraData.cameraType == CameraType.Game || renderingData.cameraData.cameraType == CameraType.SceneView;
        bool hasValidResources = mesh != null &amp;&amp; material != null;
        if (isGameOrSceneCamera &amp;&amp; hasValidResources)
        {
            // Update resources in our pass
            pass.mesh = mesh;
            pass.material = material;

            // And add pass to the renderer
            renderer.EnqueuePass(pass);
        }
    }
}

// This class handles the rendering logic. Here we should build new nodes and insert them into RenderGraph.
public class RenderIntoColorBufferPass : ScriptableRenderPass
{
    public Mesh mesh;
    public Material material;

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
	    // In this method we will create a custom render graph node that renders a single mesh.
	    ...
    }
}</code></pre>



<p class="wp-block-paragraph">I added the feature to the URP renderer in the inspector. I used a random mesh from the project and a new material with unlit shader.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="590" height="512" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-AddingRenderFeatire.gif" alt="" class="wp-image-5151"/><figcaption class="wp-element-caption">Adding the render feature into URP renderer.</figcaption></figure>



<p class="wp-block-paragraph">The interesting part starts in <code>RecordRenderGraph()</code>, where I will use the new API to create and connect a Render Graph node.</p>



<h4 class="wp-block-heading">Step 1 &#8211; Create a node</h4>



<p class="wp-block-paragraph">I used <code>renderGraph.AddRasterRenderPass</code> method to create a node &#8211; this gives me a builder object and a data block. RenderGraph supports several pass types &#8211; I am using raster here to output geometry to a target texture.</p>



<p class="wp-block-paragraph">I also needed to create a custom class, <code>PassData</code>, that holds data used by the <em>CommandBuffer</em> during the rendering.</p>



<p class="wp-block-paragraph">I used a <em>Dispose()</em> method to finish node creation.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	// Create a new pass (node) in the graph. Note that this method creates a builder and a PassData objects for us.
	IRasterRenderGraphBuilder rasterPassBuilder = renderGraph.AddRasterRenderPass(nameof(RenderIntoColorBufferPass), out PassData passData);

	// Finish pass creation
	rasterPassBuilder.Dispose();
}

// Here, we need to store all the necessary resources during the rendering (resources used by the CommandBuffer).
public class PassData
{
}</code></pre>



<h4 class="wp-block-heading">Steps 2 and 3 &#8211; Define inputs and outputs</h4>



<p class="wp-block-paragraph">I defined inputs by passing the resources to our <em>PassData</em> object. Here, I needed to store all references that are used by a <em>CommandBuffer</em> API during the rendering.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	IRasterRenderGraphBuilder rasterPassBuilder = renderGraph.AddRasterRenderPass(nameof(RenderIntoColorBufferPass), out PassData passData);

	// Assign the resources
	passData.mesh = mesh;
	passData.material = material;

	rasterPassBuilder.Dispose();
}

public class PassData
{
	//we will use a mesh and a material during the rendering.
	public Mesh mesh;
	public Material material;
}</code></pre>



<p class="wp-block-paragraph">Now, I needed to define the render target, but how can I get the reference to the color target texture &#8211; content of the screen? I can access shared GPU resources using the <code>frameData.Get&lt;T&gt;()</code> API. I requested URP resources (<code>UniversalResourceData</code>) to get the <code>activeColorTexture</code>. This is the target texture I want to render into, so I saved the reference.</p>



<p class="wp-block-paragraph">I must also tell the render graph node that I want to render the triangles into the color texture. This is done using the <code>rasterPassBuilder.SetRenderAttachment</code>. I also noticed that nodes can render only into one set of render targets. You can&#8217;t change the targets (attachments) during the rendering!</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	// Gather all needed references using the frameData.Get API.
	UniversalResourceData urpResources = frameData.Get&lt;UniversalResourceData&gt;();
	TextureHandle colorTarget = urpResources.activeColorTexture;

	IRasterRenderGraphBuilder rasterPassBuilder = renderGraph.AddRasterRenderPass(nameof(RenderIntoColorBufferPass), out PassData passData);

	// Set the render target
	rasterPassBuilder.SetRenderAttachment(colorTarget, 0, AccessFlags.Write);

	passData.mesh = mesh;
	passData.material = material;

	rasterPassBuilder.Dispose();
}</code></pre>



<h4 class="wp-block-heading">Step 4 &#8211; Define the render function</h4>



<p class="wp-block-paragraph">Then, I implemented the actual rendering &#8211; defined a render function using the <code>rasterPassBuilder.SetRenderFunc()</code> method. The way I must wrote this line of code is quite weird, as Unity required me to write an explicit lambda expression here. No shortcuts allowed.</p>



<p class="wp-block-paragraph">Making <code>RenderFunction</code> static forced me to keep everything inside <code>PassData</code> &#8211; and I think that&#8217;s a good thing.<br>In this method, I don&#8217;t set any render targets and I don&#8217;t create any <em>CommandBuffers</em>, I jump right into the drawing using the <em>RasterCommandBuffer</em> in <code>context.cmd</code>.</p>



<pre class="wp-block-code"><code>public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
	UniversalResourceData urpResources = frameData.Get&lt;UniversalResourceData&gt;();
	TextureHandle colorTarget = urpResources.activeColorTexture;

	IRasterRenderGraphBuilder rasterPassBuilder = renderGraph.AddRasterRenderPass(nameof(RenderIntoColorBufferPass), out PassData passData);

	rasterPassBuilder.SetRenderAttachment(colorTarget, 0, AccessFlags.Write);

	passData.mesh = mesh;
	passData.material = material;

	// Define a rendering function for the node.
	rasterPassBuilder.SetRenderFunc((PassData passData, RasterGraphContext context) =&gt; RenderFunction(passData, context));

	rasterPassBuilder.Dispose();
}

// This is a render function for our node.
private static void RenderFunction(PassData passData, RasterGraphContext context)
{
	// Here we implement our rendering logic. 

	// Notice that we use a RasterCommandBuffer, not a CommandBuffer. It contains only a subset of CommandBuffer methods.
	RasterCommandBuffer cmd = context.cmd;

	// We use this RasterCommandBuffer to render a mesh. In this method, we can use only resources from the passData object.
	cmd.DrawMesh(passData.mesh, Matrix4x4.identity, passData.material);
}</code></pre>



<h4 class="wp-block-heading">Step 5 &#8211; Connect the node to the graph</h4>



<p class="wp-block-paragraph">Render Graph will automatically connect the node to the graph based on the usage of the resources. But this is not too precise. In my case, I only used a color target from URP resources. I set <code>renderPassEvent</code> in the constructor &#8211; this ensures the pass connects at the right stage. Set <code>renderPassEvent</code> to <code>AfterRenderingPostProcessing</code> to ensure that the mesh will be rendered on the screen after postprocesses.</p>



<pre class="wp-block-code"><code>public class RenderIntoColorBufferPass : ScriptableRenderPass
{
    public Mesh mesh;
    public Material material;

    public RenderIntoColorBufferPass()
    {
        //It is essential for the graph builder to build our pass after postprocesses are added to the graph.
        this.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        UniversalResourceData urpResources = frameData.Get&lt;UniversalResourceData&gt;();
        ...</code></pre>



<p class="wp-block-paragraph">You can see that the feature is working!</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1014" height="524" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-FeatureIsWorking.gif" alt="" class="wp-image-5152"/><figcaption class="wp-element-caption">Render feature renders a single mesh using an unlit shader and it works!</figcaption></figure>



<p class="wp-block-paragraph">And in the Render Graph Viewer (Window/Analysis/Render Graph Viewer), you can see my &#8220;Render Into Color Buffer Pass&#8221;. It also uses _CameraTargetAttachmentB texture as a write-only target (red color).</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="974" height="610" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer.jpg" alt="" class="wp-image-5153" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer.jpg 974w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer-300x188.jpg 300w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer-768x481.jpg 768w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer-400x251.jpg 400w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer-600x376.jpg 600w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RenderGraphViewer-800x501.jpg 800w" sizes="auto, (max-width: 974px) 100vw, 974px" /><figcaption class="wp-element-caption">Render Graph Viewer displays my render pass and it was batched with the Blit Post Processing pass.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Summary</strong></h2>



<ul class="wp-block-list">
<li>Render Graph API is a new way of render pipeline scripting in Unity.</li>



<li>Render Graph works by appending custom rendering nodes to the graph using a C# API. It is not a visual editing tool.</li>



<li>All rendering code is written in <code>ScriptableRenderPass.RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)</code></li>



<li>To create a node in render graph, use <code>var builder = renderGraph.Add...Pass()</code>. The method returns the <em>node builder</em> and a data block of custom type.</li>



<li>Add inputs and outputs to your node by using <code>builder.SetAttachment...</code> and <code>builder.Use...</code> methods. Add all resources required in the render function to the data block.</li>



<li>To access or create resources shared between nodes by using <code>frameData.Get&lt;&gt;()</code> or <code>frameData.GetOrCreate&lt;&gt;()</code> methods.</li>



<li>Create a render function and assign it to the node by using <code>builder.SetRenderFunc(...)</code></li>



<li>Finish node creation by calling <code>builder.Dispose()</code>.</li>
</ul>



<p class="wp-block-paragraph">In the upcoming article, I will explain how to use a render graph to render custom content into URP GBuffer. I will alter the GBuffer contents to simulate some weather conditions &#8211; rain atmosphere effect:</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="276" src="https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1024x276.png" alt="" class="wp-image-5154" srcset="https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1024x276.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-300x81.png 300w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-768x207.png 768w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1536x413.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-2048x551.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-400x108.png 400w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-600x161.png 600w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-800x215.png 800w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1000x269.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1280x344.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1520x409.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/05/001-RainAtmosphereEffect-1900x511.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">The effect I will implement in the next article.</figcaption></figure>



<h2 class="wp-block-heading">About the author</h2>



<p class="wp-block-paragraph">Jan Mróz / Sir Johny – Graphics Programmer at The Knights of U. He specializes in rendering pipeline extensions and performance optimization across CPU, GPU, and memory. Sir Johny also leads tech-art initiatives that improve visual quality while cutting iteration time.</p>



<p class="wp-block-paragraph">Learn from real production challenges and solutions at <a class="" href="https://proceduralpixels.com">proceduralpixels.com</a>, where Jan shares tools and insights developed to support studio pipelines and push visual quality efficiently.</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/render-graph-introduction/">Render Graph &#8211; Introduction</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/render-graph-introduction/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Boost performance of your game in Unity 6 with GPU Resident Drawer</title>
		<link>https://theknightsofu.com/boost-performance-of-your-game-in-unity-6-with-gpu-resident-drawer/</link>
					<comments>https://theknightsofu.com/boost-performance-of-your-game-in-unity-6-with-gpu-resident-drawer/#respond</comments>
		
		<dc:creator><![CDATA[Sir Rob]]></dc:creator>
		<pubDate>Thu, 15 May 2025 13:00:00 +0000</pubDate>
				<category><![CDATA[All]]></category>
		<category><![CDATA[Guide]]></category>
		<category><![CDATA[Tips]]></category>
		<category><![CDATA[Unity 6]]></category>
		<category><![CDATA[BatchRendererGroup]]></category>
		<category><![CDATA[CPU Overhead Reduction]]></category>
		<category><![CDATA[Draw Calls]]></category>
		<category><![CDATA[game development]]></category>
		<category><![CDATA[GPU Instancing]]></category>
		<category><![CDATA[GPU Occlusion Culling]]></category>
		<category><![CDATA[GPU Resident Drawer]]></category>
		<category><![CDATA[HDRP]]></category>
		<category><![CDATA[Performance Optimization]]></category>
		<category><![CDATA[Rendering Optimization]]></category>
		<category><![CDATA[unity]]></category>
		<category><![CDATA[unity 6]]></category>
		<category><![CDATA[Unity Rendering]]></category>
		<category><![CDATA[Universal Render Pipeline]]></category>
		<category><![CDATA[URP]]></category>
		<guid isPermaLink="false">https://theknightsofu.com/?p=5020</guid>

					<description><![CDATA[<p>Today we will take a look at GPU Resident Drawer which can reduce rendering workload on CPU. </p>
<p>The post <a href="https://theknightsofu.com/boost-performance-of-your-game-in-unity-6-with-gpu-resident-drawer/">Boost performance of your game in Unity 6 with GPU Resident Drawer</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Unity 6 introduced a bunch of new features when it comes to performance optimization. Today we will take a look at GPU Resident Drawer which can reduce rendering workload on CPU. The setup in this post is done using Universal Render Pipeline but it is applicable also to High Definition Render Pipeline.</p>



<h2 class="wp-block-heading"><strong>What is GPU Resident Drawer</strong></h2>



<p class="wp-block-paragraph">GPU Resident Drawer is Unity’s internal rendering system which automatically uses the BatchRendererGroup API to draw GameObjects<strong> </strong>with GPU instancing. The result is reduced draw calls and freed CPU processing time.</p>



<p class="wp-block-paragraph">It is best suited for large complex scenes with many objects like a large number of environment objects.</p>



<h2 class="wp-block-heading"><strong>Ok, and BatchRendererGroup is?</strong></h2>



<p class="wp-block-paragraph">BatchRendererGroup is a technique which makes it possible to efficiently render a large number of objects via instanced draw calls. It is done using draw commands, containing all information required to create an instanced draw call, e.g. batchID, meshID or materialID. With GPU Resident Drawer you do not have to worry about technicalities of BRG API, because Unity will do all the rendering magic for you.</p>



<h2 class="wp-block-heading"><strong>How to make it work</strong></h2>



<p class="wp-block-paragraph">In order to make GPU Resident Drawer work, you have to meet following requirements:</p>



<ol class="wp-block-list">
<li>Set proper settings in Unity</li>



<li>Make a GameObject compatible with GPU Resident Drawer</li>
</ol>



<p class="wp-block-paragraph">GPU Resident Drawer requires following settings:</p>



<ul class="wp-block-list">
<li>The Forward+ rendering path</li>



<li>Graphics APIs and platforms that support compute shaders , except OpenGL ES</li>



<li>GameObjects that have a Mesh Renderer component</li>
</ul>



<p class="wp-block-paragraph">To enable the GPU Resident Drawer follow these steps:</p>



<ol class="wp-block-list">
<li>Go to Project Settings &gt; Graphics, then in the Shader Stripping section set BatchRendererGroup Variants to Keep All.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1302" height="188" src="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all.png" alt="" class="wp-image-5021" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all.png 1302w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-300x43.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-1024x148.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-768x111.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-400x58.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-600x87.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-800x116.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-1000x144.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Keep-all-1280x185.png 1280w" sizes="auto, (max-width: 1302px) 100vw, 1302px" /></figure>



<ol start="2" class="wp-block-list">
<li>Double-click the renderer in the Renderer List to open the Universal Renderer, then set Rendering Path to Forward+.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="811" height="392" src="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward.png" alt="" class="wp-image-5022" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward.png 811w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward-300x145.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward-768x371.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward-400x193.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward-600x290.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-Forward-800x387.png 800w" sizes="auto, (max-width: 811px) 100vw, 811px" /></figure>



<ol start="3" class="wp-block-list">
<li>Go to the active URP Asset and enable SRP Batcher.</li>



<li>Set GPU Resident Drawer to Instanced Drawing.</li>
</ol>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="799" height="571" src="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4.png" alt="" class="wp-image-5023" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4.png 799w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4-300x214.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4-768x549.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4-400x286.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/BRG-3-4-600x429.png 600w" sizes="auto, (max-width: 799px) 100vw, 799px" /></figure>



<p class="wp-block-paragraph">To make GameObject compatible with the GPU Resident Drawer, check if:</p>



<ul class="wp-block-list">
<li>It has a Mesh Renderer component.</li>



<li>In the Mesh Renderer component, Light Probes is not set to Use Proxy Volume.</li>



<li>It uses only static global illumination, not real time global illumination.&nbsp;</li>



<li>It uses a shader that supports DOTS instancing.&nbsp;</li>



<li>It doesn’t move position after one camera finishes rendering and before another camera starts rendering.&nbsp;</li>



<li>It doesn’t use the MaterialPropertyBlock API.&nbsp;</li>



<li>It doesn’t have a script that uses a per-instance callback, for example OnRenderObject.</li>
</ul>



<p class="wp-block-paragraph">You can also exclude a GameObject from the GPU Resident Drawer by adding a Disallow GPU Driven Rendering component to it.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="804" height="108" src="https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG.png" alt="" class="wp-image-5024" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG.png 804w, https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG-300x40.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG-768x103.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG-400x54.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG-600x81.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/Disallow-BRG-800x107.png 800w" sizes="auto, (max-width: 804px) 100vw, 804px" /></figure>



<p class="wp-block-paragraph">Also &#8211; according to Unity &#8211; these settings may speed up GPU Resident Drawer, but are not required for it to work:</p>



<ul class="wp-block-list">
<li>Go to Project Settings &gt; Player, then in the Other Settings section disable Static Batching.</li>



<li>Go to Window &gt; Panels &gt; Lighting, then in the Lightmapping Settings section enable Fixed Lightmap Size and disable Use Mipmap Limits.</li>
</ul>



<p class="wp-block-paragraph">Feel free to experiment with it.</p>



<p class="wp-block-paragraph">And that’s it &#8211; you are good to go!</p>



<h2 class="wp-block-heading"><strong>Final results</strong></h2>



<p class="wp-block-paragraph">Let’s take a look at how many draw calls we were able to reduce.&nbsp;</p>



<p class="wp-block-paragraph">We have prepared a test scene with over 35 000 objects representing various types of foliage &#8211; trees, grass and bushes. Here’s how it looks.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-1024x576.png" alt="" class="wp-image-5025" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-1024x576.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-300x169.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-768x432.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-400x225.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-600x338.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-800x450.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene-1000x563.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Test-Scene.png 1280w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Test Scene View.</figcaption></figure>



<p class="wp-block-paragraph">We have performed tests on a PC platform with GPU Resident Drawer enabled and disabled. All tests have been performed in 1920&#215;1080 resolution.</p>



<p class="wp-block-paragraph">Below are results with GPU Resident Drawer disabled.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="268" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1024x268.png" alt="" class="wp-image-5026" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1024x268.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-300x79.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-768x201.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1536x402.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-2048x536.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-400x105.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-600x157.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-800x209.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1000x262.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1280x335.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1520x398.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Times-1900x497.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">CPU and GPU times with GPU Resident Drawer disabled.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="467" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-1024x467.png" alt="" class="wp-image-5027" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-1024x467.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-300x137.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-768x350.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-400x182.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-600x274.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-800x365.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering-1000x456.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Rendering.png 1110w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Rendering statistics with GPU Resident Drawer disabled.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="433" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1024x433.png" alt="" class="wp-image-5028" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1024x433.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-300x127.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-768x325.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1536x649.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-2048x866.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-400x169.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-600x254.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-800x338.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1000x423.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1280x541.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1520x642.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Disabled-Memory-1900x803.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Memory statistics with GPU Resident Drawer disabled.</figcaption></figure>



<p class="wp-block-paragraph">As you can see, we are not even close to our target frame rate which is 60 FPS (16,667 ms). We are performing 43.5k draw calls with 43 483 batches and rendering 5 million triangles. We are also allocating 0.82GB of memory.</p>



<p class="wp-block-paragraph">Below are results with GPU Resident Drawer enabled.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="284" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1024x284.png" alt="" class="wp-image-5029" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1024x284.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-300x83.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-768x213.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1536x425.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-2048x567.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-400x111.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-600x166.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-800x221.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1000x277.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1280x354.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1520x421.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Times-1900x526.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">CPU and GPU times with GPU Resident Drawer enabled.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="491" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-1024x491.png" alt="" class="wp-image-5030" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-1024x491.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-300x144.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-768x369.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-400x192.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-600x288.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-800x384.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering-1000x480.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Rendering.png 1073w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Rendering statistics with GPU Resident Drawer enabled.</figcaption></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="432" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1024x432.png" alt="" class="wp-image-5031" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1024x432.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-300x127.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-768x324.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1536x648.png 1536w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-2048x864.png 2048w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-400x169.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-600x253.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-800x337.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1000x422.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1280x540.png 1280w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1520x641.png 1520w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Enabled-Memory-1900x801.png 1900w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">Memory statistics with GPU Resident Drawer enabled.</figcaption></figure>



<p class="wp-block-paragraph">CPU and GPU times drastically dropped. The GPU is taking over 16 ms to process a frame but most of the time it’s idle and waiting for the target frame rate. Draw calls and batches were reduced to 128. The CPU is no longer bottled with too many draw calls and is able to quickly pass them to the GPU.</p>



<p class="wp-block-paragraph">That said, we can observe that the performance improvement is huge.</p>



<h2 class="wp-block-heading"><strong>Are there any drawbacks?</strong></h2>



<p class="wp-block-paragraph">There is some cost of using GPU Resident Drawer.&nbsp;</p>



<p class="wp-block-paragraph">First of all there is an increase in memory usage &#8211; in our case it was about 100 MB which you can see on profiler extracts above.</p>



<p class="wp-block-paragraph">Second, and what Unity is stating, is that the build times are longer with GPU Resident Drawer. So keep that in mind if you’re planning to use this feature.</p>



<p class="wp-block-paragraph">We have not noticed any difference in time of entering the Play Mode in the Editor.</p>



<h2 class="wp-block-heading"><strong>Bonus: GPU Occlusion Culling</strong></h2>



<p class="wp-block-paragraph">You probably noticed how much the amount of rendering geometry has drastically dropped. This is a credit of GPU Occlusion Culling &#8211; another game-changing feature introduced in Unity 6.</p>



<p class="wp-block-paragraph">The option to enable it shows in URP Asset right after enabling GPU Resident Drawer.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="777" height="570" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling.png" alt="" class="wp-image-5032" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling.png 777w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling-300x220.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling-768x563.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling-400x293.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Occlusion-culling-600x440.png 600w" sizes="auto, (max-width: 777px) 100vw, 777px" /></figure>



<p class="wp-block-paragraph">What it does is it uses a depth buffer from the previous frame to check if a particular instance of geometry is occluded by other objects. This results in fast and accurate occlusion culling made entirely on GPU.</p>



<p class="wp-block-paragraph">You can actually enable debug mode of this feature to see how it works. To do that go to Window -&gt; Analysis -&gt; Rendering Debugger. Then select a GPU Resident Drawer tab and check Occlusion Test Overlay checkbox.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="760" height="419" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger.png" alt="" class="wp-image-5033" style="object-fit:cover" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger.png 760w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-300x165.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-400x221.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-600x331.png 600w" sizes="auto, (max-width: 760px) 100vw, 760px" /></figure>



<p class="wp-block-paragraph">Now the Scene and Game View will show you which geometry is being culled by GPU Occlusion Culling. The more warmer areas the more geometry is being culled.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" src="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-1024x576.png" alt="" class="wp-image-5034" srcset="https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-1024x576.png 1024w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-300x169.png 300w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-768x432.png 768w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-400x225.png 400w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-600x338.png 600w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-800x450.png 800w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View-1000x563.png 1000w, https://theknightsofu.com/wp-content/uploads/2025/04/GPU-Debugger-Scene-View.png 1280w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /><figcaption class="wp-element-caption">GPU Occlusion Culling debug view.</figcaption></figure>



<h2 class="wp-block-heading"><strong>Summary</strong></h2>



<p class="wp-block-paragraph">GPU Resident Drawer alongside GPU Occlusion Culling can significantly improve performance of your project in just a few clicks. Do not hesitate to test it yourself!</p>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/boost-performance-of-your-game-in-unity-6-with-gpu-resident-drawer/">Boost performance of your game in Unity 6 with GPU Resident Drawer</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/boost-performance-of-your-game-in-unity-6-with-gpu-resident-drawer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>A Tool for Calculating Cyclomatic Complexity in Blueprints</title>
		<link>https://theknightsofu.com/a-tool-for-calculating-cyclomatic-complexity-in-blueprints/</link>
					<comments>https://theknightsofu.com/a-tool-for-calculating-cyclomatic-complexity-in-blueprints/#respond</comments>
		
		<dc:creator><![CDATA[Gaida San]]></dc:creator>
		<pubDate>Fri, 27 Sep 2024 12:50:44 +0000</pubDate>
				<category><![CDATA[Tools]]></category>
		<category><![CDATA[Unreal Engine]]></category>
		<category><![CDATA[CICD]]></category>
		<category><![CDATA[Cyclomatic Complexity]]></category>
		<category><![CDATA[Data Validation]]></category>
		<category><![CDATA[Print String Validation]]></category>
		<category><![CDATA[UE5]]></category>
		<guid isPermaLink="false">https://uesamurais.com/?p=798</guid>

					<description><![CDATA[<p>In this post, I’m sharing a custom tool I made for calculating Cyclomatic Complexity in Blueprints.</p>
<p>The post <a href="https://theknightsofu.com/a-tool-for-calculating-cyclomatic-complexity-in-blueprints/">A Tool for Calculating Cyclomatic Complexity in Blueprints</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">In this post, I’m sharing a <a href="https://github.com/TheSamurais/SamuraiProjectValidation.git" target="_blank" rel="noreferrer noopener">custom tool</a> I made for calculating Cyclomatic Complexity in Blueprints.</p>



<h2 class="wp-block-heading">What is Cyclomatic Complexity?</h2>



<p class="wp-block-paragraph">Cyclomatic Complexity (CC) is a software metric used to measure the complexity of a program&#8217;s control flow. It was introduced back in 1976 and is used to quantify the number of linearly independent paths through a program&#8217;s source code. This helps assess the complexity and maintainability of code, often being an indicator of how difficult it might be to test or understand. It&#8217;s commonly used in static code analysis and serves as a guideline to manage the complexity and improve the overall quality of software.</p>



<h2 class="wp-block-heading">How to calculate CC for BP?</h2>



<p class="wp-block-paragraph">The original formula can be found <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity" target="_blank" rel="noreferrer noopener">on the Internet</a> as well as tools that allow you to <a href="https://www.cppdepend.com/" target="_blank" rel="noreferrer noopener">calculate CC for C++</a>. </p>



<p class="wp-block-paragraph">What about UE Blueprints? Valentin Galea <a href="https://youtu.be/1SH9DBlfE4I?t=676" target="_blank" rel="noreferrer noopener">gave a speech</a> at Unreal Fest about his custom tool, which was created exactly for this purpose. I recreated his algorithm and wrapped it in a nice plugin that you can use to check CC in your BPs in any UE project.</p>



<p class="wp-block-paragraph">Here is how the algorithm works:</p>



<ol class="wp-block-list">
<li>In a given BP, collect all Entry Nodes (the first Node in the function, or Event Node).</li>



<li>Skip Entry Nodes that are not connected to anything.</li>



<li>For each Entry Node iterate through connected Nodes and count all the branches in the execution flow. Start the counter with 1 since there is always one (default) flow.</li>



<li>Skip disconnected or duplicated flows.</li>



<li>Sum all branches from all selected functions &#8211; that’s the CC for this BP.</li>
</ol>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc1.png" alt="" class="wp-image-799"/><figcaption class="wp-element-caption">Pic 1 &#8211; CC = 3 since the disconnected “False” pin is not counted.</figcaption></figure>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc2.png" alt="" class="wp-image-800"/><figcaption class="wp-element-caption">Pic 2 &#8211; CC = 3. The second flow from the same branch is not counted. The second flow from different branches is counted.</figcaption></figure>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc3.png" alt="" class="wp-image-801"/><figcaption class="wp-element-caption">Pic 3 &#8211; CC = 4. Even if there is an infinite Branch-Delay loop, the flow is counted once.</figcaption></figure>



<h2 class="wp-block-heading">How to use the plugin?</h2>



<ol class="wp-block-list">
<li>Download the plugin from <a href="https://github.com/TheSamurais/SamuraiProjectValidation.git" target="_blank" rel="noreferrer noopener">repo</a>.</li>



<li>Add it to your project’s Plugin folder.</li>



<li>Compile the code.</li>



<li>Open Editor, right-click on Blueprint or the entire folder, and select “Validate Asset”. The result will be printed in Message Log -> Asset Check.</li>
</ol>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc4.png" alt="" class="wp-image-802"/><figcaption class="wp-element-caption">Pic 4 &#8211; Validate selected BP.</figcaption></figure>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc5.png" alt="" class="wp-image-803"/><figcaption class="wp-element-caption">Pic 5 &#8211; Review Validator results in Message Log.</figcaption></figure>



<p class="wp-block-paragraph">If you like to see CC for individual functions, check “Enable Verbose Logging” in Project Settings -&gt; Plugins -&gt; Samurai Project Validation.</p>



<p class="wp-block-paragraph">This validator will be executed every time you save the BP (by default). You can disable this in Editor Preferences -&gt; Advanced -&gt; Data Validation -&gt; Validate on Save.</p>



<h2 class="wp-block-heading">Do you have a bonus for me?</h2>



<p class="wp-block-paragraph">Yes, I do!</p>



<p class="wp-block-paragraph">In the plugin, you can find a Print String Validator which checks whether there is any PrintString() or PrintText() function in your BP that has the “Print to Screen” enabled. If there is, you will get an error in the Message Log. No more forgotten prints that cover your gameplay!</p>



<figure class="wp-block-image size-full"><img decoding="async" src="https://uesamurais.com/wp-content/uploads/2024/09/cyc6.png" alt="" class="wp-image-804"/><figcaption class="wp-element-caption">Pic 6 &#8211; Print String Validator results are printed to Message Log as well.</figcaption></figure>



<p class="wp-block-paragraph"></p>
<p>The post <a href="https://theknightsofu.com/a-tool-for-calculating-cyclomatic-complexity-in-blueprints/">A Tool for Calculating Cyclomatic Complexity in Blueprints</a> appeared first on <a href="https://theknightsofu.com">The Knights of U</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://theknightsofu.com/a-tool-for-calculating-cyclomatic-complexity-in-blueprints/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
