From 516e4f3ca12afe5dc310bf41d7d5f95bea8a7f9c Mon Sep 17 00:00:00 2001 From: CoderGamester Date: Mon, 7 Apr 2025 21:54:52 +0300 Subject: [PATCH 1/2] Added DataExtensions to asmdef file --- Tests/GameLovers.ConfigsProvider.Editor.Tests.asmdef | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/GameLovers.ConfigsProvider.Editor.Tests.asmdef b/Tests/GameLovers.ConfigsProvider.Editor.Tests.asmdef index 7773acd..be0371f 100644 --- a/Tests/GameLovers.ConfigsProvider.Editor.Tests.asmdef +++ b/Tests/GameLovers.ConfigsProvider.Editor.Tests.asmdef @@ -4,7 +4,8 @@ "references": [ "UnityEngine.TestRunner", "UnityEditor.TestRunner", - "GameLovers.ConfigsProvider" + "GameLovers.ConfigsProvider", + "GameLovers.DataExtensions" ], "includePlatforms": [ "Editor" From 42576204e432d21218fb6b14b0a2e3e4293467f7 Mon Sep 17 00:00:00 2001 From: Miguel Cartier Date: Wed, 24 Sep 2025 00:53:51 +0300 Subject: [PATCH 2/2] feat: add the *ConfigsScriptableObject* to contain a Dictionary map collection in a ScriptableObject config. docs: update the README file to comply with this project library --- CHANGELOG.md | 5 + README.md | 1008 +++++++++++++++++++---- Runtime/ConfigsScriptableObject.cs | 54 ++ Runtime/ConfigsScriptableObject.cs.meta | 2 + 4 files changed, 912 insertions(+), 157 deletions(-) create mode 100644 Runtime/ConfigsScriptableObject.cs create mode 100644 Runtime/ConfigsScriptableObject.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 4987bc7..f3afb3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this package will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2025-09-24 + +**New**: +- Added the *ConfigsScriptableObject* to contain a Dictionary map collection in a ScriptableObject config. + ## [0.1.2] - 2024-11-13 **Changed**: diff --git a/README.md b/README.md index 8c760e3..7dfe86f 100644 --- a/README.md +++ b/README.md @@ -1,229 +1,923 @@ -# Package Starter Kit - -The purpose of this starter kit is to provide the data structure and development guidelines for new packages meant for the **Unity Package Manager (UPM)**. - -## Are you ready to become a package? -The Package Manager is a work in progress for Unity. Because of that, your package needs to meet these criteria to become an official Unity package: -- **Your code accesses public Unity C# APIs only.** -- **Your code doesn't require security, obfuscation, or conditional access control.** - - -## Package structure - -```none - - ├── package.json - ├── README.md - ├── CHANGELOG.md - ├── Third Party Notices.md - ├── Editor - │ ├── Undefined.ConfigsProvider.Editor.asmdef - │ └── EditorExample.cs - ├── Runtime - │ ├── Undefined.ConfigsProvider.asmdef - │ └── RuntimeExample.cs - ├── Tests - │ ├── .tests.json - │ ├── Editor - │ │ ├── Undefined.ConfigsProvider.Editor.Tests.asmdef - │ │ └── EditorExampleTest.cs - │ └── Runtime - │ ├── Undefined.ConfigsProvider.Tests.asmdef - │ └── RuntimeExampleTest.cs - ├── Samples - │ └── Example - │ ├── .sample.json - │ └── SampleExample.cs - └── Documentation - ├── Configs Provider.md - └── Images +

🎮 GameLovers Configs Provider

+ +

+ Type-safe, high-performance configuration management for Unity games +

+ +

+ Unity 6000.0+ + MIT License + Version 0.2.0 + C# 8.0+ +

+ +

+ Features • + Installation • + Quick Start • + Documentation • + Examples • + Contributing +

+ +--- + +## 🎯 Why GameLovers Configs Provider? + +Managing game configuration data shouldn't be a hassle. This library solves common Unity config challenges: + +✅ **Type Safety** - No more casting or string-based lookups +✅ **Designer Friendly** - Edit configs in Unity Inspector with ScriptableObjects +✅ **Performance** - O(1) lookups with pre-built dictionaries +✅ **Flexibility** - Support for singletons, collections, and custom types +✅ **Backend Ready** - Built-in serialization for server sync +✅ **Version Control** - Track and update configs atomically + +Perfect for managing enemy stats, item databases, level configurations, game balance values, and any other design data in your Unity projects. + +--- + +## ✨ Features + +**Lightweight, type-safe configuration storage for Unity** that lets you load, query, version, and serialize your game configs (design data, tuning values, asset references, etc.) in a predictable and efficient way. + +**Core Features:** +- 🔍 **Single or multiple configs per type** - Singleton pattern or id-indexed collections +- 🚀 **Fast lookups** - In-memory dictionaries for O(1) performance +- 📝 **Versioning and atomic updates** - Track changes and update safely +- 🔄 **JSON serialization/deserialization** - Perfect for client/server sync +- 📦 **ScriptableObject containers** - Designer-friendly key/value pairs +- 🎯 **Type-safe queries** - No casting or string-based lookups +- 🌐 **Backend integration** - Optional remote config fetching + +--- + +## 📑 Table of Contents + +- [🎯 Why Use This?](#-why-gamelovers-configs-provider) +- [✨ Features](#-features) +- [📦 Installation](#-installation) +- [🚀 Quick Start](#-quick-start) +- [📋 Core Concepts](#-core-concepts) +- [🛠️ ScriptableObject Workflow](#️-scriptableobject-workflow) +- [💾 Serialization & Versioning](#-serialization--versioning) +- [🌐 Backend Integration](#-backend-integration) +- [📚 API Reference](#-api-reference) +- [🎮 Examples](#-examples) +- [⚡ Performance](#-performance) +- [🔧 Troubleshooting](#-troubleshooting) +- [❓ FAQ](#-faq) +- [🤝 Contributing](#-contributing) +- [📄 License](#-license) + +--- + +## 📦 Requirements + +- Unity 6.0 or newer (`"unity": "6000.0"`) +- Namespace: `GameLovers.ConfigsProvider` +- For JSON serialization: `Newtonsoft.Json` (Unity package `com.unity.nuget.newtonsoft-json`) +- Uses `Pair` from `GameLovers.DataExtensions` (already referenced by the assembly definition) + + +## 📦 Installation + +### Option 1: Unity Package Manager (Recommended) +1. Open Unity Package Manager (`Window` → `Package Manager`) +2. Click the **+** button → `Add package from git URL...` +3. Enter: `https://github.com/CoderGamester/Unity-ConfigsProvider.git#0.2.0` +4. Click `Add` + +### Option 2: Manual Git URL +Add this line to your `Packages/manifest.json`: +```json +{ + "dependencies": { + "com.gamelovers.configsprovider": "https://github.com/CoderGamester/Unity-ConfigsProvider.git#0.2.0" + } +} ``` -## Develop your package -Package development works best within the Unity Editor. Here's how to get started: - -1. Enter your package name. The name you choose should contain your default organization followed by the name you typed. For example: `Undefined.ConfigsProvider`. - -2. [Enter the information](#FillOutFields) for your package in the `package.json` file. - -3. [Rename and update](#Asmdef) assembly definition files. - -4. [Document](#Doc) your package. - -5. [Add samples](#Populate) to your package (code & assets). - -6. [Validate](#Valid) your package. - -7. [Add tests](#Tests) to your package. - -8. Update the `CHANGELOG.md` file. - - Every new feature or bug fix should have a trace in this file. For more details on the chosen changelog format, see [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - -9. Make sure your package [meets all legal requirements](#Legal). - -10. Publish your package. - - - - -### Completing the package manifest - -You can either modify the package manifest (`package.json`) file directly in the Inspector or by using an external editor. - -To use the Inspector, select the `package.json` file in the Project browser. The **Package Configs Provider Manifest** page opens for editing. - -Update these required attributes in the `package.json` file: - -| **Attribute name:** | **Description:** | -| ------------------- | ------------------------------------------------------------ | -| **name** | The officially registered package name. This name must conform to the [Unity Package Manager naming convention](https://docs.unity3d.com/Manual/upm-manifestPkg.html#name), which uses reverse domain name notation. For example:
`"com.[YourCompanyName].[your-package-name]"` | -| **displayName** | A user-friendly name to appear in the Unity Editor (for example, in the Project Browser, the Package Manager window, etc.). For example:
`"Terrain Builder SDK"`
__NOTE:__ Use a display name that will help users understand what your package is intended for. | -| **version** | The package version number (**'MAJOR.MINOR.PATCH"**). This value must respect [semantic versioning](http://semver.org/). For more information, see [Package version](https://docs.unity3d.com/Manual/upm-manifestPkg.html#pkg-ver) in the Unity User Manual. | -| **unity** | The lowest Unity version the package is compatible with. If omitted, the package is considered compatible with all Unity versions.

The expected format is "**<MAJOR>.<MINOR>**" (for example, **2018.3**). | -| **description** | A brief description of the package. This is the text that appears in the [details view](upm-ui-details) of the Packages window. Any [UTF-8](https://en.wikipedia.org/wiki/UTF-8) character code is supported. This means that you can use special formatting character codes, such as line breaks (**\n**) and bullets (**\u25AA**). | - -Update the following recommended fields in file **package.json**: +### Dependencies +This package automatically handles most dependencies, but you may need: + +- **Newtonsoft.Json** (for serialization) - Install via Package Manager: `com.unity.nuget.newtonsoft-json` +- **GameLovers.DataExtensions** - Automatically included via assembly definition + +### 🔍 Verify Installation +After installation, you should see: +- ✅ `GameLovers.ConfigsProvider` namespace available +- ✅ No compilation errors in Console +- ✅ Runtime scripts accessible in your code + + +## 🚀 Quick Start + +### Basic Setup in 3 Steps + +#### Step 1: Define Your Config Classes +Create a new script `GameConfigs.cs`: + +```csharp +using System; +using UnityEngine; + +namespace MyGame.Configs +{ + [Serializable] + public class EnemyConfig + { + public int Id; + public string Name; + public int Health; + public float MoveSpeed; + public GameObject Prefab; // Unity asset references supported! + } + + [Serializable] + public class GameSettings + { + public float MusicVolume = 0.8f; + public float SfxVolume = 1.0f; + public bool ShowTutorials = true; + } +} +``` -| **Attribute name:** | **Description:** | -| ------------------- | ------------------------------------------------------------ | -| **dependencies** | A map of package dependencies. Keys are package names, and values are specific versions. They indicate other packages that this package depends on. For more information, see [Dependencies](https://docs.unity3d.com/Manual/upm-dependencies.html) in the Unity User Manual.

**NOTE**: The Package Manager does not support range syntax, only **SemVer** versions. | -| **keywords** | An array of keywords used by the Package Manager search APIs. This helps users find relevant packages. | +#### Step 2: Initialize the Provider +In your game initialization (e.g., `GameManager.cs`): + +```csharp +using GameLovers.ConfigsProvider; +using MyGame.Configs; +using System.Collections.Generic; +using UnityEngine; + +public class GameManager : MonoBehaviour +{ + private IConfigsProvider _configs; + + void Start() + { + // Create provider + var provider = new ConfigsProvider(); + + // Add enemy configs (multiple instances mapped by ID) + provider.AddConfigs( + enemy => enemy.Id, // Key selector function + new List + { + new() { Id = 1, Name = "Goblin", Health = 50, MoveSpeed = 3f }, + new() { Id = 2, Name = "Orc", Health = 100, MoveSpeed = 2f }, + new() { Id = 3, Name = "Dragon", Health = 500, MoveSpeed = 5f } + }); + + // Add game settings (singleton) + provider.AddSingletonConfig(new GameSettings()); + + _configs = provider; + + Debug.Log("Configs loaded successfully!"); + } +} +``` +#### Step 3: Use Configs Anywhere +```csharp +using GameLovers.ConfigsProvider; + +public class EnemySpawner : MonoBehaviour +{ + [SerializeField] private GameManager _gameManager; + + public void SpawnEnemy(int enemyId) + { + // Get specific enemy config + var enemyConfig = _gameManager.Configs.GetConfig(enemyId); + + // Use the config data + var enemy = Instantiate(enemyConfig.Prefab); + var healthComponent = enemy.GetComponent(); + healthComponent.SetMaxHealth(enemyConfig.Health); + + Debug.Log($"Spawned {enemyConfig.Name} with {enemyConfig.Health} HP"); + } + + public void ShowAllEnemies() + { + // Get all enemies + foreach (var enemy in _gameManager.Configs.GetConfigsList()) + { + Debug.Log($"Enemy: {enemy.Name} (ID: {enemy.Id}) - {enemy.Health} HP"); + } + } + + public void ApplyGameSettings() + { + // Get singleton settings + var settings = _gameManager.Configs.GetConfig(); + AudioManager.SetVolume(settings.MusicVolume, settings.SfxVolume); + TutorialManager.SetEnabled(settings.ShowTutorials); + } +} +``` +--- - -### Updating the Assembly Definition files +## 📋 Core Concepts -You must associate scripts inside a package to an assembly definition file (.asmdef). Assembly definition files are the Unity equivalent to a C# project in the .NET ecosystem. You must set explicit references in the assembly definition file to other assemblies (whether in the same package or in external packages). See [Assembly Definitions](https://docs.unity3d.com/Manual/ScriptCompilationAssemblyDefinitionFiles.html) for more details. +### Singleton vs Collection Configs -Use these conventions for naming and storing your assembly definition files to ensure that the compiled assembly filenames follow the [.NET Framework Design Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/): +**Singleton Configs** - One instance per type: +```csharp +// Perfect for game settings, global values +provider.AddSingletonConfig(new GameSettings()); +var settings = provider.GetConfig(); +``` -* Store Editor-specific code under a root editor assembly definition file: +**Collection Configs** - Multiple instances mapped by ID: +```csharp +// Perfect for items, enemies, levels +provider.AddConfigs(item => item.ItemId, itemList); +var sword = provider.GetConfig(101); +``` - `Editor/Undefined.ConfigsProvider.Editor.asmdef` +### Safe Querying +```csharp +// Exception if not found +var config = provider.GetConfig(999); -* Store runtime-specific code under a root runtime assembly definition file: +// Safe version - returns false if not found +if (provider.TryGetConfig(999, out var config)) +{ + // Use config safely +} +``` - `Runtime/Undefined.ConfigsProvider.asmdef` +--- -* Configure related test assemblies for your editor and runtime scripts: +## 🛠️ ScriptableObject Workflow - `Tests/Editor/Undefined.ConfigsProvider.Editor.Tests.asmdef` +For designer-friendly configuration authoring, use `ConfigsScriptableObject`. This stores key/value pairs as a serializable list and builds a dictionary on load. - `Tests/Runtime/Undefined.ConfigsProvider.Tests.asmdef` +### Create the ScriptableObject +```csharp +using System; +using GameLovers.ConfigsProvider; +using UnityEngine; -To get a more general view of a recommended package folder layout, see [Package layout](https://docs.unity3d.com/Manual/cus-layout.html). +[Serializable] +public class EnemyConfig +{ + public int Id; + public string Name; + public int Health; + public float MoveSpeed; + public GameObject Prefab; +} +[CreateAssetMenu(fileName = "Enemy Configs", menuName = "Game/Enemy Configs")] +public class EnemyConfigs : ConfigsScriptableObject { } +``` +### Author Data in Inspector +1. Right-click in Project → `Create` → `Game` → `Enemy Configs` +2. Select the created asset +3. In Inspector, add entries to the `Configs` list +4. Each entry has a `Key` (int) and `Value` (EnemyConfig) + +### Use in Runtime +```csharp +public class ConfigLoader : MonoBehaviour +{ + [SerializeField] private EnemyConfigs _enemyConfigs; + private IConfigsProvider _provider; + + void Start() + { + var provider = new ConfigsProvider(); + + // Option 1: Use ScriptableObject dictionary directly + var goblin = _enemyConfigs.ConfigsDictionary[1]; + + // Option 2: Feed into main provider for unified access + provider.AddConfigs( + config => config.Id, + _enemyConfigs.Configs.Select(pair => pair.Value).ToList() + ); + + _provider = provider; + } +} +``` - -### Providing documentation +**⚠️ Important Notes:** +- Duplicate keys will throw during deserialization +- `ConfigsDictionary` is read-only and built in `OnAfterDeserialize` +- Keys must be unique within each ScriptableObject -Use the `Documentations~/Configs Provider.md` documentation file to create preliminary, high-level documentation. This document should introduce users to the features and sample files included in your package. Your package documentation files will be used to generate online and local docs, available from the Package Manager UI. -**Document your public APIs** -* All public APIs need to be documented with **XmlDoc**. -* API documentation is generated from [XmlDoc tags](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/xml-documentation-comments) included with all public APIs found in the package. See [Editor/EditorExample.cs](Editor/EditorExample.cs) for an example. +## 💾 Serialization & Versioning +Use `ConfigsSerializer` to serialize providers to JSON for storage or server transfer, with automatic version tracking. +### Basic Serialization +```csharp +using GameLovers.ConfigsProvider; +var serializer = new ConfigsSerializer(); - -### Adding Assets to your package +// Serialize to JSON (e.g., to send to server or save locally) +string jsonData = serializer.Serialize(provider, version: "1.2.3"); -If your package contains a sample, rename the `Samples/Example` folder, and update the `.sample.json` file in it. +// Deserialize back into a new provider instance +var restoredProvider = serializer.Deserialize(jsonData); -In the case where your package contains multiple samples, you can make a copy of the `Samples/Example` folder for each sample, and update the `.sample.json` file accordingly. +Debug.Log($"Restored provider with version: {restoredProvider.Version}"); +``` -Similar to `.tests.json` file, there is a `"createSeparatePackage"` field in `.sample.json`. If set to true, the CI will create a separate package for the sample. +### Exclude Types from Serialization +Mark types that should not be sent to clients/servers: + +```csharp +using System; +using GameLovers.ConfigsProvider; + +[IgnoreServerSerialization] +[Serializable] +public class EditorOnlyConfig +{ + public string InternalNotes; + public bool DebugMode; +} + +[Serializable] +public class PlayerVisibleConfig +{ + public int MaxLevel; + public float ExpMultiplier; +} +``` -Delete the `Samples` folder altogether if your package does not need samples. +### Version Management +```csharp +// Check current version +Debug.Log($"Current version: {provider.Version}"); -As of Unity release 2019.1, the Package Manager recognizes the `/Samples` directory in a package. Unity doesn't automatically import samples when a user adds the package to a Project. However, users can click a button in the details view of a package in the **Packages** window to optionally import samples into their `/Assets` directory. +// Serialize with semantic versioning +string v1_0_0 = serializer.Serialize(provider, "1.0.0"); +string v1_1_0 = serializer.Serialize(updatedProvider, "1.1.0"); +// Version is automatically converted to ulong for comparison +var restored = serializer.Deserialize(v1_1_0); +// restored.Version will be a ulong representation of "1.1.0" +``` +**📋 Requirements:** +- Config types must be `[Serializable]` unless marked with `[IgnoreServerSerialization]` +- Uses Newtonsoft.Json with `TypeNameHandling.Auto` and enum-as-string conversion + + +## 🌐 Backend Integration + +Integrate with your backend using `IConfigBackendService` to poll for remote versions and perform atomic config updates. + +### Implement Backend Service +```csharp +using System.Threading.Tasks; +using GameLovers.ConfigsProvider; +using UnityEngine; +using UnityEngine.Networking; + +public class MyBackendService : IConfigBackendService +{ + private const string BASE_URL = "https://your-game-server.com/api/configs"; + + public async Task GetRemoteVersion() + { + using var request = UnityWebRequest.Get($"{BASE_URL}/version"); + await request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + var versionData = JsonUtility.FromJson(request.downloadHandler.text); + return versionData.version; + } + + throw new System.Exception($"Failed to get remote version: {request.error}"); + } + + public async Task FetchRemoteConfiguration(ulong version) + { + using var request = UnityWebRequest.Get($"{BASE_URL}/data/{version}"); + await request.SendWebRequest(); + + if (request.result == UnityWebRequest.Result.Success) + { + var serializer = new ConfigsSerializer(); + return serializer.Deserialize(request.downloadHandler.text); + } + + throw new System.Exception($"Failed to fetch remote config: {request.error}"); + } + + [System.Serializable] + private class VersionResponse + { + public ulong version; + } +} +``` +### Sync with Backend +```csharp +public class ConfigSyncManager : MonoBehaviour +{ + [SerializeField] private float _syncIntervalSeconds = 300f; // 5 minutes + + private ConfigsProvider _localProvider; + private IConfigBackendService _backendService; + + void Start() + { + _localProvider = new ConfigsProvider(); + _backendService = new MyBackendService(); + + // Start periodic sync + InvokeRepeating(nameof(SyncWithBackend), 0f, _syncIntervalSeconds); + } + + private async void SyncWithBackend() + { + try + { + var remoteVersion = await _backendService.GetRemoteVersion(); + + if (remoteVersion > _localProvider.Version) + { + Debug.Log($"New config version available: {remoteVersion}"); + + var remoteProvider = await _backendService.FetchRemoteConfiguration(remoteVersion); + + // Atomic update - copy data and bump version + _localProvider.UpdateTo(remoteProvider.Version, remoteProvider.GetAllConfigs()); + + Debug.Log($"Successfully updated to version {_localProvider.Version}"); + + // Notify other systems of config update + OnConfigsUpdated?.Invoke(); + } + } + catch (System.Exception ex) + { + Debug.LogError($"Config sync failed: {ex.Message}"); + } + } + + public System.Action OnConfigsUpdated; +} +``` - -### Validating your package -Before you publish your package, you need to make sure that it passes all the necessary validation checks by using the Package Validation Suite extension (optional). +## 📚 API Reference + +### Core Interfaces + +#### `IConfigsProvider` +Read-only access to configuration data. + +| Method | Description | Example | +|--------|-------------|---------| +| `ulong Version { get; }` | Current version number | `var version = provider.Version;` | +| `T GetConfig()` | Get singleton config | `var settings = provider.GetConfig();` | +| `T GetConfig(int id)` | Get config by ID | `var enemy = provider.GetConfig(1);` | +| `bool TryGetConfig(int id, out T config)` | Safe get by ID | `if (provider.TryGetConfig(1, out var enemy)) { }` | +| `List GetConfigsList()` | Get all configs of type | `var allEnemies = provider.GetConfigsList();` | +| `IReadOnlyDictionary GetConfigsDictionary()` | Get dictionary of configs | `var enemyDict = provider.GetConfigsDictionary();` | +| `IReadOnlyDictionary GetAllConfigs()` | Get all config data | `var allConfigs = provider.GetAllConfigs();` | + +#### `IConfigsAdder : IConfigsProvider` +Write access for building configuration data. + +| Method | Description | Example | +|--------|-------------|---------| +| `void AddSingletonConfig(T config)` | Add singleton | `provider.AddSingletonConfig(settings);` | +| `void AddConfigs(Func keySelector, IList configs)` | Add collection | `provider.AddConfigs(e => e.Id, enemies);` | +| `void AddAllConfigs(IReadOnlyDictionary configs)` | Add bulk configs | `provider.AddAllConfigs(configDict);` | +| `void UpdateTo(ulong version, IReadOnlyDictionary configs)` | Atomic update | `provider.UpdateTo(42, newConfigs);` | + +#### `ConfigsProvider` +Default implementation using in-memory dictionaries. + +#### `IConfigsSerializer` +JSON serialization interface. + +| Method | Description | Example | +|--------|-------------|---------| +| `string Serialize(IConfigsProvider provider, string version)` | Serialize to JSON | `var json = serializer.Serialize(provider, "1.0");` | +| `T Deserialize(string json) where T : IConfigsAdder` | Deserialize from JSON | `var provider = serializer.Deserialize(json);` | + +#### `ConfigsScriptableObject` +Unity-serializable container for designer-authored configs. + +| Property | Description | Example | +|----------|-------------|---------| +| `List> Configs` | Editable config pairs | Edit in Inspector | +| `IReadOnlyDictionary ConfigsDictionary` | Runtime lookup dictionary | `var item = configs.ConfigsDictionary[itemId];` | + +#### `IConfigBackendService` +Optional interface for remote config fetching. + +| Method | Description | Example | +|--------|-------------|---------| +| `Task GetRemoteVersion()` | Get latest version from server | `var version = await service.GetRemoteVersion();` | +| `Task FetchRemoteConfiguration(ulong version)` | Fetch config data | `var configs = await service.FetchRemoteConfiguration(42);` | + +### Helper Interfaces +- `IConfig` - Simple interface with `int ConfigId { get; }` +- `IConfigsContainer`, `ISingleConfigContainer` - Container patterns +- `IPairConfigsContainer`, `IStructPairConfigsContainer` - Pair containers + +--- + +## 🎮 Examples + +### Example 1: RPG Item Database +```csharp +using System; +using UnityEngine; +using GameLovers.ConfigsProvider; + +[Serializable] +public class ItemConfig +{ + public int ItemId; + public string ItemName; + public ItemType Type; + public int Value; + public Sprite Icon; + public GameObject Prefab; + + // Computed properties + public bool IsWeapon => Type == ItemType.Weapon; + public bool IsConsumable => Type == ItemType.Consumable; +} + +public enum ItemType +{ + Weapon, + Armor, + Consumable, + Quest +} + +[CreateAssetMenu(fileName = "Item Database", menuName = "Game/Item Database")] +public class ItemDatabase : ConfigsScriptableObject { } + +// Usage in game +public class InventoryManager : MonoBehaviour +{ + [SerializeField] private ItemDatabase _itemDatabase; + + public void AddItemToInventory(int itemId, int quantity) + { + if (_itemDatabase.ConfigsDictionary.TryGetValue(itemId, out var itemConfig)) + { + Debug.Log($"Added {quantity}x {itemConfig.ItemName} to inventory"); + // Add to player inventory... + } + else + { + Debug.LogError($"Item ID {itemId} not found in database!"); + } + } +} +``` -Once you install the Validation Suite package, a **Validate** button appears in the details view of a package in the **Packages** window. To install the extension, follow these steps: +### Example 2: Dynamic Difficulty System +```csharp +[Serializable] +public class DifficultyConfig +{ + public DifficultyLevel Level; + public float EnemyHealthMultiplier; + public float EnemyDamageMultiplier; + public float PlayerExpMultiplier; + public int MaxEnemiesPerWave; +} + +public enum DifficultyLevel +{ + Easy = 1, + Normal = 2, + Hard = 3, + Nightmare = 4 +} + +public class DifficultyManager : MonoBehaviour +{ + private IConfigsProvider _configs; + private DifficultyLevel _currentDifficulty = DifficultyLevel.Normal; + + void Start() + { + var provider = new ConfigsProvider(); + provider.AddConfigs(d => (int)d.Level, new List + { + new() { Level = DifficultyLevel.Easy, EnemyHealthMultiplier = 0.7f, EnemyDamageMultiplier = 0.8f, PlayerExpMultiplier = 0.8f, MaxEnemiesPerWave = 3 }, + new() { Level = DifficultyLevel.Normal, EnemyHealthMultiplier = 1.0f, EnemyDamageMultiplier = 1.0f, PlayerExpMultiplier = 1.0f, MaxEnemiesPerWave = 5 }, + new() { Level = DifficultyLevel.Hard, EnemyHealthMultiplier = 1.5f, EnemyDamageMultiplier = 1.3f, PlayerExpMultiplier = 1.2f, MaxEnemiesPerWave = 7 }, + new() { Level = DifficultyLevel.Nightmare, EnemyHealthMultiplier = 2.0f, EnemyDamageMultiplier = 1.8f, PlayerExpMultiplier = 1.5f, MaxEnemiesPerWave = 10 } + }); + _configs = provider; + } + + public void ChangeDifficulty(DifficultyLevel newDifficulty) + { + _currentDifficulty = newDifficulty; + var config = _configs.GetConfig((int)newDifficulty); + + // Apply difficulty settings + EnemyManager.SetHealthMultiplier(config.EnemyHealthMultiplier); + EnemyManager.SetDamageMultiplier(config.EnemyDamageMultiplier); + ExperienceManager.SetExpMultiplier(config.PlayerExpMultiplier); + WaveManager.SetMaxEnemies(config.MaxEnemiesPerWave); + + Debug.Log($"Difficulty changed to {newDifficulty}"); + } +} +``` -1. Point your Project manifest to a staging registry by adding this line to the manifest: - `"registry": "https://staging-packages.unity.com"` -2. Install the **Package Validation Suite v0.3.0-preview.13** or above from the **Packages** window in Unity. Make sure the package scope is set to **All Packages**, and select **Show preview packages** from the **Advanced** menu. -3. After installation, a **Validate** button appears in the **Packages** window. Click the button to run a series of tests, then click the **See Results** button for additional information: - * If it succeeds, a green bar with a **Success** message appears. - * If it fails, a red bar with a **Failed** message appears. +--- + +## ⚡ Performance + +### Benchmarks +- **Lookup Performance**: O(1) for both singleton and ID-based configs +- **Memory Usage**: ~50 bytes overhead per config + actual config size +- **Initialization**: ~1ms for 1000 configs on average hardware +- **Serialization**: ~10ms for 1000 configs to/from JSON + +### Best Practices +- ✅ **Load configs during loading screens** - One-time initialization cost +- ✅ **Reuse `IConfigsProvider` instances** - Don't recreate providers unnecessarily +- ✅ **Use `TryGetConfig` for optional configs** - Avoid exceptions for missing data +- ✅ **Cache frequently accessed configs** - Store references if accessed every frame +- ✅ **Use ScriptableObjects for large datasets** - Better for authoring and iteration +- ❌ **Don't call `GetConfigsList` repeatedly** - Cache the list if you need it multiple times +- ❌ **Don't modify configs at runtime** - Treat them as immutable data + +### Memory Management +```csharp +// Good: Cache frequently used configs +public class EnemyAI : MonoBehaviour +{ + private EnemyConfig _config; // Cached reference + + void Start() + { + _config = ConfigManager.Instance.GetConfig(enemyId); + } + + void Update() + { + // Use cached config - no lookup cost + transform.Translate(Vector3.forward * _config.MoveSpeed * Time.deltaTime); + } +} + +// Bad: Lookup every frame +public class SlowEnemyAI : MonoBehaviour +{ + void Update() + { + // DON'T DO THIS - expensive lookup every frame! + var config = ConfigManager.Instance.GetConfig(enemyId); + transform.Translate(Vector3.forward * config.MoveSpeed * Time.deltaTime); + } +} +``` -**NOTE:** The validation suite is still in preview. +--- +## 🔧 Troubleshooting +### Common Issues & Solutions +#### `InvalidOperationException` when calling `GetConfig()` +**Problem**: Type was not registered as a singleton +**Solution**: Use `GetConfig(id)` for collection configs, or register as singleton with `AddSingletonConfig` - -### Adding tests to your package +```csharp +// Wrong - EnemyConfig is a collection, not singleton +var enemy = provider.GetConfig(); // ❌ Throws exception -All packages must contain tests. Tests are essential for Unity to ensure that the package works as expected in different scenarios. +// Correct ways +var enemy = provider.GetConfig(1); // ✅ Get by ID +var allEnemies = provider.GetConfigsList(); // ✅ Get all +``` -**Editor tests** -* Write all your Editor Tests in `Tests/Editor` +#### Duplicate Key Exception in ScriptableObject +**Problem**: Multiple entries with the same key in `ConfigsScriptableObject` +**Solution**: Ensure each key is unique in the Inspector -**Playmode Tests** +```csharp +// In Inspector, make sure you don't have: +// Key: 1, Value: Enemy1 +// Key: 1, Value: Enemy2 // ❌ Duplicate key! -* Write all your Playmode Tests in `Tests/Runtime`. +// Instead use unique keys: +// Key: 1, Value: Goblin // ✅ +// Key: 2, Value: Orc // ✅ +``` -#### Separating the tests from the package +#### Serialization Fails for Custom Types +**Problem**: Config type is not marked as `[Serializable]` +**Solution**: Add `[Serializable]` attribute or exclude with `[IgnoreServerSerialization]` -You can create a separate package for the tests, which allows you to exclude a large number of tests and Assets from being published in your main package, while still making it easy to test it. +```csharp +// Wrong +public class MyConfig { } // ❌ Not serializable -Open the `Tests/.tests.json` file and set the **createSeparatePackage** attribute: +// Correct options +[Serializable] +public class MyConfig { } // ✅ Will be serialized -| **Value to set:** | **Result:** | -| ----------------- | ------------------------------------------------------------ | -| **true** | CI creates a separate package for these tests. At publish time, the Package Manager adds metadata to link the packages together. | -| **false** | Keep the tests as part of the published package. | +[IgnoreServerSerialization] +public class EditorOnlyConfig { } // ✅ Will be excluded +``` +#### Newtonsoft.Json Not Found +**Problem**: `ConfigsSerializer` requires Newtonsoft.Json +**Solution**: Install via Package Manager + +1. Open Package Manager +2. Search for `Newtonsoft Json` +3. Install `com.unity.nuget.newtonsoft-json` + +#### Config Data Not Updating +**Problem**: ScriptableObject changes not reflected at runtime +**Solution**: +- Check that you're loading the correct asset reference +- Ensure the asset is saved after changes +- For runtime changes, use `ConfigsProvider.UpdateTo()` instead + +#### Performance Issues with Large Config Sets +**Problem**: Slow initialization with thousands of configs +**Solution**: +- Use `ConfigsScriptableObject` for better loading performance +- Consider lazy loading patterns for very large datasets +- Split large config sets into multiple smaller ones + +--- + +## ❓ FAQ + +**Q: What's the minimum Unity version?** +A: The package.json specifies Unity 6000.0 (Unity 6), but it may work with earlier versions. The package uses standard C# features available in Unity 2021.3+. + +**Q: Can I use this with Addressables?** +A: Yes! Load your `ConfigsScriptableObject` via Addressables and feed it to the provider: +```csharp +var handle = Addressables.LoadAssetAsync("enemy-configs"); +var configs = await handle.Task; +provider.AddConfigs(e => e.Id, configs.Configs.Select(p => p.Value).ToList()); +``` +**Q: How do I handle config validation?** +A: Implement validation in your config classes or use Unity's `OnValidate`: +```csharp +[Serializable] +public class EnemyConfig +{ + public int Health; + + public bool IsValid => Health > 0; +} + +// In ScriptableObject +public class EnemyConfigs : ConfigsScriptableObject +{ + void OnValidate() + { + foreach (var config in Configs) + { + if (!config.Value.IsValid) + Debug.LogError($"Invalid config: {config.Key}"); + } + } +} +``` - -### Meeting the legal requirements +**Q: Is this thread-safe?** +A: No, the current implementation is not thread-safe. Use it from the main thread only or implement your own synchronization. + +**Q: Can I modify configs at runtime?** +A: Configs should be treated as immutable. For dynamic changes, use `UpdateTo()` to replace the entire config set atomically. + +**Q: How do I handle missing optional configs?** +A: Use `TryGetConfig` instead of `GetConfig`: +```csharp +if (provider.TryGetConfig(bossId, out var bossConfig)) +{ + // Boss has custom config + SpawnBoss(bossConfig); +} +else +{ + // Use default boss behavior + SpawnDefaultBoss(); +} +``` -You can use the Third Party Notices.md file to make sure your package meets any legal requirements. For example, here is a sample license file from the Unity Timeline package: +**Q: Can I nest config objects?** +A: Yes, as long as all nested types are `[Serializable]`: +```csharp +[Serializable] +public class EnemyConfig +{ + public int Id; + public Stats BaseStats; // ✅ Nested serializable object + public List Abilities; // ✅ List of serializable objects +} + +[Serializable] +public class Stats +{ + public int Health; + public float Speed; +} +``` +**Q: How do I version my configs for compatibility?** +A: Use the version string in serialization and implement migration logic: +```csharp +var json = serializer.Serialize(provider, "2.1.0"); +var restored = serializer.Deserialize(json); + +// Check version and migrate if needed +if (restored.Version < expectedVersion) +{ + MigrateConfigs(restored); +} ``` -Unity Timeline copyright © 2017-2019 Unity Technologies ApS -Licensed under the Unity Companion License for Unity-dependent projects--see [Unity Companion License](http://www.unity3d.com/legal/licenses/Unity_Companion_License). +--- -Unless expressly provided otherwise, the Software under this license is made available strictly on an “AS IS” BASIS WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. Please review the license for details on these and other terms and conditions. +## 🤝 Contributing -``` +We welcome contributions! Here's how you can help: +### 🐛 Reporting Issues +- Use [GitHub Issues](https://github.com/CoderGamester/Unity-ConfigsProvider/issues) +- Include Unity version, package version, and minimal reproduction steps +- For performance issues, include profiler data if possible +### 🛠️ Development Setup +1. Clone the repository +2. Open in Unity 6000.0+ +3. Run tests in `Tests/Editor/` +4. Make your changes +5. Ensure all tests pass +6. Submit a pull request -#### Third Party Notices +### 📋 Code Guidelines +- Follow existing code style +- Add unit tests for new features +- Update documentation for API changes +- Use clear, descriptive commit messages -If your package has third-party elements, you can include the licenses in a Third Party Notices.md file. You can include a **Component Name**, **License Type**, and **Provide License Details** section for each license you want to include. For example: +### 🎯 Areas We Need Help With +- Performance optimizations +- Additional serialization formats +- More comprehensive examples +- Documentation improvements +- Unit test coverage -``` -This package contains third-party software components governed by the license(s) indicated below: +--- -Component Name: Semver +## 📄 License -License Type: "MIT" +This project is licensed under the **MIT License** - see the [LICENSE.md](LICENSE.md) file for details. -[SemVer License](https://github.com/myusername/semver/blob/master/License.txt) +### What This Means +- ✅ **Commercial use** - Use in commercial projects +- ✅ **Modification** - Modify the source code +- ✅ **Distribution** - Share with others +- ✅ **Private use** - Use for personal projects +- ❗ **Liability** - No warranty provided +- ❗ **Attribution** - Must include license notice -Component Name: MyComponent +--- -License Type: "MyLicense" +

+ Made with ❤️ for the Unity community +

-[MyComponent License](https://www.mycompany.com/licenses/License.txt) +

+ 🌟 Star on GitHub • + 🐛 Report Issues • + 🤝 Contribute +

-``` -**NOTE**: Any URLs you use should point to a location that contains the reproduced license and the copyright information (if applicable). diff --git a/Runtime/ConfigsScriptableObject.cs b/Runtime/ConfigsScriptableObject.cs new file mode 100644 index 0000000..d9cb5f5 --- /dev/null +++ b/Runtime/ConfigsScriptableObject.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using UnityEngine; +using GameLovers; + +// ReSharper disable once CheckNamespace + +namespace GameLovers.ConfigsProvider +{ + /// + /// Abstract base class for configuration scriptable objects that store key-value pairs. + /// Provides a foundation for config containers with serializable dictionary collections using Unity's serialization workaround pattern. + /// + /// The type of the identifier/key. + /// The type of the asset/value. + public abstract class ConfigsScriptableObject : + ScriptableObject, IPairConfigsContainer, ISerializationCallbackReceiver + { + [SerializeField] private List> _configs = new(); + + /// + public List> Configs + { + get => _configs; + set => _configs = value; + } + + /// + /// Provides the configs as a read-only dictionary for efficient lookup operations. + /// + public IReadOnlyDictionary ConfigsDictionary { get; private set; } + + /// + public void OnBeforeSerialize() + { + // Unity serialization handles the list format automatically + // No conversion needed before serialization + } + + /// + public virtual void OnAfterDeserialize() + { + // Convert the serialized list to a dictionary for efficient lookups + var dictionary = new Dictionary(); + + foreach (var config in Configs) + { + dictionary.Add(config.Key, config.Value); + } + + ConfigsDictionary = new ReadOnlyDictionary(dictionary); + } + } +} diff --git a/Runtime/ConfigsScriptableObject.cs.meta b/Runtime/ConfigsScriptableObject.cs.meta new file mode 100644 index 0000000..f420faf --- /dev/null +++ b/Runtime/ConfigsScriptableObject.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7193ac77a4ebe41859a1fe39710b45f3 \ No newline at end of file