{"id":15160580,"url":"https://github.com/949886/lunarframework","last_synced_at":"2025-10-24T18:31:42.482Z","repository":{"id":249627161,"uuid":"832034036","full_name":"949886/LunarFramework","owner":"949886","description":"Unity Framework for Game Development","archived":false,"fork":false,"pushed_at":"2025-01-23T02:18:00.000Z","size":2439,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-31T03:37:31.183Z","etag":null,"topics":["unity","unity3d"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/949886.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-22T08:14:28.000Z","updated_at":"2025-01-23T02:18:05.000Z","dependencies_parsed_at":"2024-09-11T07:38:01.931Z","dependency_job_id":"e9a11fd0-5c4c-40a6-abc4-0ecedc00a0d3","html_url":"https://github.com/949886/LunarFramework","commit_stats":{"total_commits":73,"total_committers":2,"mean_commits":36.5,"dds":0.09589041095890416,"last_synced_commit":"a4a7a97609a541fb83722108b663c823ee7d0efa"},"previous_names":["949886/lunarframework"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/949886%2FLunarFramework","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/949886%2FLunarFramework/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/949886%2FLunarFramework/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/949886%2FLunarFramework/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/949886","download_url":"https://codeload.github.com/949886/LunarFramework/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238024405,"owners_count":19403836,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["unity","unity3d"],"created_at":"2024-09-26T23:02:25.550Z","updated_at":"2025-10-24T18:31:42.464Z","avatar_url":"https://github.com/949886.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Lunar Framework\r\n\r\n![](https://img.shields.io/badge/Unity-2021.3-green.svg?style=flat-square)\r\n\u003cimg src=\"https://komarev.com/ghpvc/?username=lunareclipse-lunarframework\u0026color=green\u0026style=flat-square\u0026label=Views\" width=\"1px\"\u003e\r\n\r\nLunar Framework is a set of tools and patterns for Unity3D development. It's designed to make the development process easier and more efficient.\r\n\r\n---\r\n\r\n## Table of Contents\r\n\r\n- [Installation](#installation)\r\n- [UI](#UI)\r\n  - [UI Navigation](#navigation)\r\n- [Asset Management](#asset-management)\r\n- [Event](#event)\r\n- [Tools](#tools)\r\n  - [Object Pool](#object-pool)\r\n- [Scripting](#scripting)\r\n  - [Attributes](#attributes)\r\n\r\n\r\n## Installation\r\n\r\n### Unity Package Manager\r\n\r\n1. Open the Unity Package Manager by selecting `Window` \u003e `Package Manager`.\r\n2. Click the `+` button in the top-left corner and select `Add package from git URL...`.\r\n3. Enter the git URL `https://github.com/949886/LunarFramework.git` and click `Add`.\r\n\r\n\u003c!--\r\n\r\n## Gameplay\r\n\r\n### Third Person \r\n\r\n--\u003e\r\n\r\n\r\n## UI\r\n\r\n### Navigation\r\n\r\n#### Navigator\r\n\r\nTo manage the navigation of the UI with the `Navigator` by following these steps:\r\n\r\n1. Create a new game object and attach the `Navigator` component to it.\r\n2. Set the root `Widget` you want to show first when the game starts (Root widget is a game object that has a component of the subclass of the `Widget` or `StatefulWidget`).\r\n3. [Optional] Set the root canvas if you want to use the `Navigator` in a different canvas. If you don't set the root canvas, the `Navigator` will find it automatically.\r\n\r\n\r\n#### Push a widget\r\n\r\n```cs\r\npublic void OnSettingsButtonClicked()  \r\n{  \r\n    Navigator.Push\u003cRouletteSettingsView\u003e();  \r\n}\r\n```\r\n\r\n`Navigator.Push` method will push a `Widget` to the top of the navigation stack and the widget will be instantiated and added to the root canvas.\r\n\r\nThis generic method takes a type of subclass of the `Widget` class as a parameter.\r\n\r\n\u003e [!NOTE]  \r\n\u003e **A widget type should only have one prefab that corresponds to it.**  \r\n\u003e Any prefab with a `Widget` component will automatically be registered in the Widgets prefab database\r\n\u003e (if addressables package is installed, the prefab will be registered as an addressable asset, otherwise it will be registered as a scriptable object resource).\r\n\r\n\r\n#### Pop a widget\r\n\r\nPop current widget by calling the `Navigator.Pop` method. The top widget will be removed from the navigation stack and destroyed.\r\n\r\n```cs\r\nvoid Update()\r\n{\r\n    if (Input.GetKeyDown(KeyCode.Escape)) \r\n        Navigator.Pop();\r\n}\r\n```\r\n\r\nAlternatively, you can pop a widget by calling the `Navigator.PopUntil\u003cTarget\u003e` or `Navigator.PopToRoot` method to pop widgets until a specific widget or the root widget.\r\n\r\n\r\n#### Pass data\r\n\r\n\r\n**View -\u003e Subview**\r\n\r\nYou can use a lambda callback to pass data to the widget. The callback will be executed after the widget is instantiated.\r\n\r\n```cs\r\npublic void OnBlueButtonClick()\r\n{\r\n    Navigator.Push\u003cRouletteGameView\u003e(async (view) =\u003e {\r\n        // Basically, you can pass data to the widget by setting corresponding properties directly.\r\n        // You can also use UniTask to pass data at the next frame if you want to do something after the widget is enabled.\r\n        /* await UniTask.NextFrame(); */\r\n        view.RouletteData = Data;\r\n    });\r\n}\r\n```\r\n\r\n\u003e [!NOTE]  \r\n\u003e The callback will be executed before the widget is enabled, so you should initialize the widget in Awake method.\r\n\r\n\r\n**Subview -\u003e View**\r\n\r\nBasically, you can pass data back to the previous widget by using the `Navigator.Pop` method.\r\n\r\n```cs\r\npublic void OnExitButtonClick()\r\n{\r\n    Navigator.Pop(data);\r\n}\r\n```\r\n\r\nThen you can get the data from the previous widget by casting the return value of the `Navigator.Push` method.\r\n\r\n```cs\r\npublic async void OnClickSettingsButton()  \r\n{  \r\n    var data = await Navigator.Push\u003cRouletteSettingsView\u003e() as string;\r\n    \r\n    // Following code will be executed after the SettingsView pop.\r\n    HandleSettingsData(data);\r\n    EventSystem.current.SetSelectedGameObject(settingsButton.gameObject);  \r\n}\r\n```\r\n\r\n\r\n**View \u003c-\u003e Subview**\r\n\r\n\r\n```cs\r\npublic async void OnEditButtonClick()\r\n{\r\n    // Create a new roulette and let the user edit it.\r\n    var roulette = new RouletteData();\r\n    roulette.title = \"New Roulette\";\r\n    roulette.sectors = new List\u003cRouletteSector\u003e();\r\n    for (int i = 0; i \u003c 8; i++)\r\n        roulette.sectors.Add(new RouletteSector()\r\n        {\r\n            content = $\"Roulette {i}\",\r\n            weight = 1,\r\n            color = Color.HSVToRGB(1.0f / 8 * i, 0.5f, 1f),\r\n        });\r\n    \r\n    // Open edit view with a new roulette.\r\n    var result = await Navigator.Push\u003cRouletteEditView\u003e((view) =\u003e {\r\n        view.Data = roulette;\r\n    }) as RouletteData;\r\n    \r\n    // Add edited new roulette to front.\r\n    if (result != null)\r\n        roulettes.Insert(0, result);\r\n}\r\n```\r\n\r\n#### Pop to specific widget\r\n\r\nYou can pop to a specific widget by calling the `Navigator.PopUntil` method. The top widget will be removed from the navigation stack until the target widget is found.\r\n\r\n```cs\r\npublic void OnBackToHomeButtonClicked()  \r\n{  \r\n    Navigator.PopUntil\u003cRouletteHomeView\u003e();  \r\n}\r\n```\r\n\r\n#### Pop to root widget\r\n\r\nYou can pop to the root widget by calling the `Navigator.PopToRoot` method. All widgets except the root widget will be removed from the navigation stack.\r\n\r\n```cs\r\npublic void OnBackToHomeButtonClicked()  \r\n{  \r\n    Navigator.PopToRoot();  \r\n}\r\n```\r\n\r\n#### Replace top widget\r\n\r\nYou can replace the top widget by calling the `Navigator.PushReplacement` method. The top widget will be removed from the navigation stack and destroyed, and the new widget will be pushed to the top of the navigation stack.\r\n\r\n```cs\r\npublic void OnShowAgainButtonClicked()  \r\n{  \r\n    Navigator.PushReplacement\u003cRouletteGameView\u003e();\r\n}\r\n```\r\n\r\n#### Compatibility\r\n\r\nYou can have a new navigator anywhere with a game object as root to adapt old code.\r\n\r\n```cs\r\nvoid Start()\r\n{\r\n    Navigator.Create(gameObject);\r\n}\r\n```\r\n\r\n\r\n\u003c!--\r\n\r\n### Show Modal Dialog\r\n\r\nYou can show a modal dialog by calling the `Navigator.ShowModal` method.\r\n\r\n--\u003e\r\n\r\n\r\n## Asset Management\r\n\r\n### R.cs\r\n\r\nLunar framework provides a simple way to manage assets in Unity:  \r\n\r\n1. It will automatically generate a script `R.cs` that contains the asset references when the editor starts or recompiles.  \r\n2. You can also regenerate this file manually by selecting the menu item `Tools \u003e LunarFramework \u003e Generate R.cs`.\r\n\r\nIf you have some audio files like this file tree:\r\n```\r\nAssets\r\n  └ Audios\r\n      ├ bgm_lobby.mp3\r\n      └ sf_click.mp3\r\n```\r\n\r\nThe generated `R.cs` will look like this:\r\n\r\n```csharp\r\nnamespace R\r\n{\r\n    public static class Audios\r\n    {\r\n        public static Asset\u003cAudioClip\u003e BgmLobby = new(\"Audios/bgm_lobby\");\r\n        public static Asset\u003cAudioClip\u003e SfxClick = new(\"Audios/sf_click\");\r\n    }\r\n}\r\n```\r\n\r\nThen you can load and get the asset easily, for example:\r\n\r\n```csharp\r\nprivate void PlaySfxSync() =\u003e SFXManager.Play(R.Audios.SfxClick);\r\n\r\nprivate void PlaySfxAsync1() =\u003e R.Audios.SfxClick.Load().Then(SFXManager.Play);\r\nprivate async void PlaySfxAsync2()\r\n{\r\n    var clip = await R.Audios.SfxClick.Load();\r\n    SFXManager.Play(clip);\r\n}\r\n```\r\n\r\nHere is an example of preloading assets with a loading indicator:\r\n\r\n```csharp\r\npublic class PreloadingExample : MonoBehaviour\r\n{\r\n    public GameObject prefab;\r\n    \r\n    private async void Start()\r\n    {\r\n        // Show loading indicator before necessary assets are loaded\r\n        await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);\r\n        Navigator.ShowModal\u003cCircularLoadingIndicator\u003e();\r\n        \r\n        // Load bgm\r\n        var clip = await R.Audios.BgmLobby.Load();\r\n        BgmManager.Play(clip);\r\n        \r\n        // Load audios with both \"Audio\" and \"Games.Common\" labels if you enabled addressables package\r\n        await Assets.Load(\"Games.Common\", \"Audio\");\r\n\r\n        Navigator.PopToRoot();\r\n    }\r\n}\r\n```\r\n\r\n### Asset\r\n\r\nThe `Asset\u003cT\u003e` class is a generic class that represents an asset in Unity. It can be used to load assets from the Resources folder or from the Addressables system.\r\n\r\n```csharp\r\nAsset\u003cSceneInstance\u003e asset = new(\"Assets/Game/World/Teyvat/Mondstadt.unity\");\r\nasset.onProgress += (progress) =\u003e {\r\n    Debug.Log($\"[Assets] Loading {asset.Address}: {progress * 100}%\");\r\n};\r\nasset.onDownload += status =\u003e {\r\n    Debug.Log($\"[Assets] Downloading {asset.Address}: {status.Percent * 100}%\");\r\n};\r\nasset.onError += (error) =\u003e {\r\n    Debug.LogError($\"[Assets] Error loading {asset.Address}: {error}\");\r\n};\r\nSceneInstance scene = await asset.Load(LoadSceneMode.Additive);\r\n```\r\n\r\n\u003e[!NOTE] \r\n\u003e If you want to load a scene, you should use the `SceneInstance` type as the generic type parameter.  \r\n\u003e If you use `Load()` method to load a scene asset, it will load the scene asynchronously with `LoadSceneMode.Single` mode by default.\r\n\r\n\r\n\r\n## Event\r\n\r\nDefine some events in a class as static fields.\r\n\r\n```cs\r\npublic static class MyEvents\r\n{\r\n    public static Event TestEvent = new();\r\n    public static Event\u003c(string a, string b)\u003e TestArgumentEvent = new();\r\n    public static Event\u003c(string a, string b)\u003e TestFilteredEvent = new() {\r\n        filter = (sender, receiver, args) =\u003e false\r\n    };\r\n}\r\n```\r\n\r\nSubscribe and unsubscribe to the event in a class.\r\n\r\n```csharp\r\npublic class EventSubscriber : MonoBehaviour\r\n{\r\n    private void OnEnable()\r\n    {\r\n        MyEvents.TestEvent += OnEvent;\r\n        MyEvents.TestArgumentEvent += OnEvent;\r\n        MyEvents.TestFilteredEvent += OnEvent;\r\n    }\r\n    \r\n    private void OnDisable()\r\n    {\r\n        MyEvents.TestEvent -= OnEvent;\r\n        MyEvents.TestArgumentEvent -= OnEvent;\r\n        MyEvents.TestFilteredEvent -= OnEvent;\r\n    }\r\n\r\n    private void OnEvent(object sender) { }\r\n    private void OnEvent(object sender, (string a, string b) args) { }\r\n}\r\n```\r\n\r\nTrigger the event in another class.\r\n\r\n```csharp\r\npublic class EventTrigger : MonoBehaviour\r\n{\r\n    private void Update()\r\n    {\r\n        if (Input.GetKeyDown(KeyCode.Space))\r\n        {\r\n            MyEvents.TestEvent.Invoke(this);\r\n            MyEvents.TestArgumentEvent.Invoke(this, (\"Hello\", \"World\"));\r\n            MyEvents.TestFilteredEvent.Invoke(this, (\"Hello\", \"Filtered World\"));\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n\r\n\r\n## Tools\r\n\r\n### Object Pool\r\n\r\n#### Get an object from the pool\r\n\r\nYou can get an object from the pool by calling the `ObjectPool.Get` method easily.  \r\nThe method will return an object from the pool if there is an available **inactive** object in the pool.\r\nOtherwise, it will instantiate a new object from the prefab.\r\n\r\n```csharp\r\nprivate async void Shoot(GameObject bulletPrefab)\r\n{\r\n  GameObject bulletObject = ObjectPool.Get(bulletPrefab);\r\n  bulletObject.SetActive(true); // \u003c- **Important** to activate the object\r\n  \r\n  // align to gun barrel/muzzle position\r\n  bulletObject.transform.SetPositionAndRotation(muzzlePosition.position, muzzlePosition.rotation);\r\n  \r\n  // move projectile forward\r\n  var rigidbody = bulletObject.GetComponent\u003cRigidbody\u003e();\r\n  rigidbody.velocity = Vector3.zero;\r\n  rigidbody.angularVelocity = Vector3.zero;\r\n  rigidbody.AddForce(bulletObject.transform.forward * muzzleVelocity, ForceMode.Acceleration);\r\n  \r\n  // turn off after a few seconds\r\n  await UniTask.Delay(TimeSpan.FromSeconds(3));\r\n  bulletObject.SetActive(false); // \u003c- **Important** set inactive to return to the pool\r\n}\r\n```\r\n\r\n## Scripting\r\n\r\n### Attributes\r\n\r\n**`OnChanged`**\r\n\r\nThis attribute is used to monitor the changes of a field in Unity Editor. The method will be invoked when the field is changed.\r\nIt will add a callback to the field. The callback will be invoked when the field value is changed.\r\n\r\n```csharp\r\n// Change the color of the mesh renderer when the intensity is changed.\r\n\r\n[SerializeField, OnChanged(nameof(OnColorChange))]\r\npublic Color color;\r\n\r\npublic void OnColorChange() \r\n{\r\n    if (meshRenderer == null)\r\n        ChangeColor(GetComponent\u003cMeshRenderer\u003e(), color, intensity);    \r\n    else ChangeColor(meshRenderer, color, intensity);\r\n}\r\n\r\nprivate void ChangeColor(MeshRenderer meshRenderer, Color color, float intensity)\r\n{\r\n    foreach (var material in meshRenderer.materials)\r\n        if (material.IsKeywordEnabled(\"_EMISSION\"))\r\n            material.SetColor(PropertyName, color * intensity);\r\n}\r\n```\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F949886%2Flunarframework","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F949886%2Flunarframework","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F949886%2Flunarframework/lists"}