A code-first data management workflow for C# and Unity, powered by BakingSheet and Source Generators.
- Clear separation between runtime data models and authoring data sources.
- Authoring code, data sources and configuration will NOT be included in the build.
- Data sources can be Google Sheets or CSV files (powered by BakingSheet).
- Support for complex data types (powered by BakingSheet).
- There are only a few Limitations.
- Automatic mapping between data sources and data models (powered by Source Generators).
- Code-first approach with minimal configuration on Unity Inspector.
- Flexible and automatic data type conversion mechanism.
- Unity 2022.3 or later
-
Open menu
Window
->Package Manager
. -
Click the
+
button at the top-left corner, then chooseAdd package from git URL...
. -
Enter the package URL
https://github.com/Zitga-Tech/ZBase.Foundation.Data/tree/main/Packages/ZBase.Foundation.Data
.
- Install OpenUPM CLI.
- Run the following command in your Unity project root directory:
openupm add com.zbase.foundation.data
At high level, the usage workflow usually consists of the following steps:
- Data Authoring: Create data sources in Google Sheets or CSV files.
- Data Modeling: Design
IData
models in C# code along with the table assets to store them. - Data Importing: Leverage BakingSheet to import authored data from step 1 into each corresponding table asset. This step requires a piece of bridging code and a config asset.
For this tutorial:
- Data sources are defined in Google Sheets
- Data models and table assets located at Assets/Samples
- Data importing configs located at Assets/Samples.Authoring
Data sources can be either Google Sheets or CSV files.
Figure 1: map_regions
table
You must choose one of these strategies and apply it consistently for all sheets, columns, and CSV files.
Pascal | Camel | Snake | Kebab |
---|---|---|---|
SheetName |
sheetName |
sheet_name |
sheet-name |
ColumnName |
columnName |
column_name |
column-name |
FileName.csv |
fileName.csv |
file_name.csv |
file-name.csv |
- Define a data model, can be
struct
orclass
, and must implementIData
interface. - Any field that should be mapped to a column in the data source must be decorated with
[SerializeField]
.- A public property will be generated for such valid fields.
- In case you prefer writing properties, each should be decorated with
[DataProperty]
.- The underlying field and methods will be generated for such valid properties.
- The data model must be
partial
so that source generators can generate the underlying implementation. - Fields or properties are matched to columns in the data source by name, after applying the naming strategy.
- The ID of a data model can be a complex structure, consists of multiple fields.
- These field named
_id
orid
or the property namedId
will be recognized as the ID of that model.
- These field named
Listing 1: Model for the ID of a map region entry
public partial struct MapRegionIdData : IData
{
[SerializeField]
private int _mapId;
[SerializeField]
private int _region;
// IData source generator will generate
// a property for each field.
// ===
// public int MapId { get => _mapId; init => _mapId = value; }
// public int Region { get => _region; init => _region = value; }
}
Listing 2: Model for the map region entry
public partial class MapRegionData : IData
{
[DataProperty]
public MapRegionIdData Id => Get_Id();
[DataProperty]
public int UnlockCost => Get_UnlockCost();
// IData source generator will generate
// a field and a `Get_XXX()` method for each property.
// ===
// [SerializeField]
// private MapRegionIdData _id;
// private readonly MapRegionIdData Get_Id() => _id;
// [SerializeField]
// private int _unlockCost;
// private readonly int Get_UnlockCost() => _unlockCost;
}
- Each data table asset type should inherit from either
DataTableAsset<TEntryId, TEntry>
orDataTableAsset<TEntryId, TEntry, TConvertedId>
.TEntryId
is the type of theId
property ofTEntry
.TEntry
is the data model, corresponding to a row in the data source.TConvertedId
is the type of theId
property ofTEntry
after being converted fromTEntryId
.
- It is required to implement
IDataTableAsset
interface so source generator can generate additional but necessary code. - Ultimately this is a
ScriptableObject
to store the imported data.
Listing 3: Data table asset for map region
public sealed partial class MapRegionDataTableAsset
: DataTableAsset<MapRegionIdData, MapRegionData, MapRegionId>
, IDataTableAsset
{
protected override MapRegionId Convert(MapRegionIdData value)
=> value;
// IDataTableAsset source generator will generate
// a constant field `NAME` and a `GetId()` method.
// ===
// public const string NAME = nameof(MapRegionDataTableAsset);
// protected override MapRegionIdData GetId(in MapRegionData data)
// {
// return data.Id;
// }
}
- Define a partial class of any name.
- Decorate the class with
[Database]
attribute. - Can specify a global naming strategy for all tables.
- Decorate the class with
- Inside the class, define a property for each table asset. Each will be mapped to a sheet in the data source.
- Decorate the property with
[Table]
attribute.
- Decorate the property with
- In case a column in the data source should be a vertical list, decorate the property with
[VerticalList]
attribute.- First parameter of the attribute is the type of the data model, which can be a part of the table, not necessary the main data model.
- Second parameter is the name of the property that should be treated as a vertical list.
Listing 4: A bridge to map each table to its source sheet
[Database(NamingStrategy.SnakeCase)]
public partial class DatabaseDefinition
{
[Table] public MapRegionDataTableAsset MapRegions { get; }
[VerticalList(typeof(HeroData), nameof(HeroData.Multipliers))]
[Table] public HeroDataTableAsset Heroes { get; }
}
After defining the bridge, BakingSheet related code will be generated so that data importing can function.
There are 2 ways to implement this functionality:
- Create an asset for
DatabaseCsvSheetConfig
and use its Inspector functionality (Appendix A). - Write a function to import the data source (Appendix B).
-
Define a class that inherits from
DatabaseCsvSheetConfig
.[CreateAssetMenu( fileName = nameof(SampleDatabaseCsvSheetConfig) , menuName = "Sample Database Csv Sheet Config" , order = 0 )] public partial class SampleDatabaseCsvSheetConfig : DatabaseCsvSheetConfig<DatabaseDefinition.SheetContainer> // ^^^^^^^^^^^^^^^^^^ // Replace this with your class defined at step 3.1. { protected override DatabaseDefinition.SheetContainer CreateSheetContainer() { return new DatabaseDefinition.SheetContainer(UnityLogger.Default); // ^^^^^^^^^^^^^^^^^^ // Replace this with your class defined at step 3.1. } protected override string GetDatabaseAssetName() { // Any name of your choice. return "SampleDatabaseAsset"; } }
-
In the Project window, create an asset out of it.
-
Fill in the fields in the Inspector. Then click the
Export All Assets
button.
-
Define a class that inherits from
DatabaseGoogleSheetConfig
.[CreateAssetMenu( fileName = nameof(SampleDatabaseGoogleSheetConfig) , menuName = "Sample Database Google Sheet Config" , order = 0 )] public partial class SampleDatabaseGoogleSheetConfig : DatabaseGoogleSheetConfig<DatabaseDefinition.SheetContainer> // ^^^^^^^^^^^^^^^^^^ // Replace this with your class defined at step 3.1. { protected override DatabaseDefinition.SheetContainer CreateSheetContainer() { return new DatabaseDefinition.SheetContainer(UnityLogger.Default); // ^^^^^^^^^^^^^^^^^^ // Replace this with your class defined at step 3.1. } protected override string GetDatabaseAssetName() { // Any name of your choice. return "SampleDatabaseAsset"; } }
-
In the Project window, create an asset out of it.
-
Follow this tutorial to properly configure the Spreadsheet so it can be imported. It also explains how to acquire the ID of a spreadsheet, and the Google Credential JSON.
-
Fill in the fields in the Inspector. Then click the
Export All Assets
button.
using System;
using System.Threading.Tasks;
using Cathei.BakingSheet.Unity;
using UnityEditor;
using ZBase.Foundation.Data.Authoring;
public static partial class DatabaseImporter
{
public static async Task<bool> FromCsvFilesAsync(
string csvFolderPath
, string assetOutputFolderPath
, bool includeSubFolders = true
, bool includeCommentedFiles = true
)
{
var converter = new DatabaseCsvSheetConverter(
csvFolderPath
, TimeZoneInfo.Utc
, includeSubFolders: includeSubFolders
, includeCommentedFiles: includeCommentedFiles
);
var sheetContainer = new DatabaseDefinition.SheetContainer(UnityLogger.Default);
// ^^^^^^^^^^^^^^^^^^
// Replace this with your class defined at step 3.1.
var result = await sheetContainer.Bake(converter).ConfigureAwait(true);
if (result == false)
{
return false;
}
var exporter = new DatabaseAssetExporter<DatabaseAsset>(
assetOutputFolderPath
, nameof(DatabaseAsset)
);
result = await sheetContainer.Store(exporter).ConfigureAwait(true);
if (result == false)
{
return false;
}
AssetDatabase.Refresh();
return true;
}
}
Note: Follow this tutorial to properly configure the Spreadsheet so it can be imported. It also explains how to acquire the ID of a spreadsheet, and the Google Credential JSON.
using System;
using System.IO;
using System.Threading.Tasks;
using Cathei.BakingSheet.Unity;
using UnityEditor;
using ZBase.Foundation.Data.Authoring;
public static partial class DatabaseImporter
{
public static async Task<bool> FromGoogleSheetsAsync(
string spreadsheetId
, string googleCredentialJson
, string assetOutputFolderPath
)
{
var converter = new DatabaseGoogleSheetConverter(
spreadsheetId
, googleCredentialJson
, TimeZoneInfo.Utc
);
var sheetContainer = new DatabaseDefinition.SheetContainer(UnityLogger.Default);
// ^^^^^^^^^^^^^^^^^^
// Replace this with your class defined at step 3.1.
var result = await sheetContainer.Bake(converter).ConfigureAwait(true);
if (result == false)
{
return false;
}
var exporter = new DatabaseAssetExporter<DatabaseAsset>(
assetOutputFolderPath
, nameof(DatabaseAsset)
);
result = await sheetContainer.Store(exporter).ConfigureAwait(true);
if (result == false)
{
return false;
}
AssetDatabase.Refresh();
return true;
}
}
[WIP]
- Nested vertical list is not supported.
- Cross-Sheet Reference is not supported.