Skip to content

Instantly share code, notes, and snippets.

@moppius
Last active February 9, 2020 19:46
Show Gist options
  • Save moppius/ce9730dd14108d31051b0ccef2cc45b1 to your computer and use it in GitHub Desktop.
Save moppius/ce9730dd14108d31051b0ccef2cc45b1 to your computer and use it in GitHub Desktop.
Unreal Engine Angelscript - Asset Creation from Data Table Example

Unreal Engine Angelscript - Asset Creation from Data Table Example

This is a functional sketch of an Unreal Engine 4 Scripted Asset Action Utility written in Angelscript.

Prerequisites

Steps to implement

  1. Download the MyDataExample.csv file to your computer.

  2. Download or paste the code from AssetFromDataTableUtility.as into your Unreal project built on top of the Angelscript fork.

  3. Create a new Blueprint class in Unreal, based on the AssetFromDataTableUtility class

  4. Now we need to implement a few small things in Blueprint, since Angelscript doesn't currently support accessing the AssetTools or AssetRegistry objects, and Get Data Table Row is a templated function which is not exposed to Angelscript.

    So, implement the required functionality by overriding the following functions on the Blueprint class - do this by clicking the Override drop-down that appears when you hover next to the Functions header in your Blueprint, so your Functions list should look like this:

    Overridden functions in Blueprint

    • GetRowData:

      GetRowData Blueprint implementation

    • CreateAsset:

      CreateAsset Blueprint implementation

    • GetExistingAssets:

      GetExistingAssets Blueprint implementation

  5. Import the MyDataExample.csv file into the Content Browser as a Data Table, using MyDataTableRow as the DataTable Row Type.

  6. Right-click on the new Data Table asset, and choose Scripted Actions -> Create Assets.

You should now have 3 new assets in the same Content Browser folder, containing the values defined in the Data Table.

You could also use similar techniques in non-Editor classes to generate objects at runtime, if needed.

Relevant Links

/**
* Row structure for our data table.
* The row name must be unique - this is the first column by default, unless otherwise specified on import.
*
* Note that if we're importing a DataTable from CSV, the column header (with spaces removed) must match the
* property name *exactly*, for example:
* - Column header: `Some Data`
* - Property name: `SomeData`
*/
struct FMyDataTableRow
{
/** Some data */
UPROPERTY()
int SomeData = 0;
/** Some other data */
UPROPERTY()
bool bMyBool = false;
};
/**
* A custom Data Asset that we will create & fill from our Data Table's row data.
*/
class UMyDataAsset : UDataAsset
{
/** We use this to track exactly which asset was created, since Key values must be unique */
UPROPERTY()
FName Key;
/** Some data */
UPROPERTY()
int SomeData = 0;
/** Some other data */
UPROPERTY()
bool bMyBool = false;
};
/**
* Editor utility that creates assets from a data table.
* Some functions in this Angelscript class are stubs that need to be implemented in a Blueprint subclass,
* because the functionality is not (yet) exposed to the Angelscript bindings.
*
* So, to fully implement this, we need to create a Blueprint class based on this class, and implement the
* following functions, using `AssetTools` and `AssetRegistry` as needed:
* - `CreateAsset`
* - `GetRowData`
* - `GetExistingAssets`
*/
class UAssetFromDataTableUtility : UAssetActionUtility
{
/**
* Only show the action utility when right-clicking on DataTable assets in the Content Browser.
*/
UFUNCTION(BlueprintOverride)
UClass GetSupportedClass() const
{
return UDataTable::StaticClass();
}
/**
* Create assets from the Data Table - this is exposed to the right-click menu in Unreal.
*/
UFUNCTION(CallInEditor)
void CreateAssets()
{
TArray<UObject> SelectedAssets = EditorUtility::GetSelectedAssets();
for (UObject Object : SelectedAssets)
{
CreateAssetsFromDataTable(Cast<UDataTable>(Object));
}
}
/**
* Generate new assets, or update existing assets, with data from the passed Data Table.
*/
private void CreateAssetsFromDataTable(UDataTable InDataTable)
{
const FString ReplaceName = InDataTable.GetName() + "." + InDataTable.GetName();
const FString BasePath = InDataTable.GetPathName().Replace(ReplaceName, "", ESearchCase::CaseSensitive);
auto TransientDataAssets = GenerateTransientAssets(InDataTable, BasePath);
// Examine existing data assets, we may need to delete or update them
TArray<UMyDataAsset> ExistingAssets;
GetExistingAssets(BasePath, ExistingAssets);
TMap<FName, UMyDataAsset> FinalAssets;
TArray<UMyDataAsset> AssetsToDelete;
for (UMyDataAsset ExistingAsset : ExistingAssets)
{
if (TransientDataAssets.Contains(ExistingAsset.Key))
{
FinalAssets.Add(ExistingAsset.Key, ExistingAsset);
}
else
{
AssetsToDelete.Add(ExistingAsset);
}
}
// Create new assets in the content browser if needed, update existing assets with data from the transients
for (TMapIterator<FName, UMyDataAsset> It : TransientDataAssets)
{
const FName Key = It.GetKey();
UMyDataAsset DataAsset;
// Asset didn't already exist, so make a new one
if (!FinalAssets.Find(Key, DataAsset))
{
const FString DataAssetName = "DA_" + Key.ToString().Replace(".", "_");
DataAsset = CreateAsset(DataAssetName, BasePath);
DataAsset.CopyScriptPropertiesFrom(It.GetValue());
FinalAssets.Add(Key, DataAsset);
}
// Asset existed, so update any changed properties and mark the asset as dirty if it was modified
else
{
CopyData(DataAsset, It.GetValue());
}
}
// Delete assets that are no longer referenced
for (UMyDataAsset DataAsset : AssetsToDelete)
{
// TODO: Ideally we'd delete assets automatically, but this doesn't seem possible via AS or BP yet
Warning("Key '" + DataAsset.Key + "' no longer exists, asset should be deleted: " + DataAsset.GetName());
}
if (FinalAssets.Num() == 0)
{
Error("Didn't create or find any Data Assets from the data in " + InDataTable.GetName() + "!");
}
}
/**
* Generate a map of transient Data Assets for each unique key in the Data Table.
*/
private TMap<FName, UMyDataAsset> GenerateTransientAssets(UDataTable InDataTable, const FString& BasePath)
{
TArray<FName> AssetNames;
DataTable::GetDataTableRowNames(InDataTable, AssetNames);
TMap<FName, UMyDataAsset> TransientAssets;
for (int i = 0; i < AssetNames.Num(); i++)
{
const FName AssetName = AssetNames[i];
FMyDataTableRow Row = GetRowData(InDataTable, AssetName);
// Find existing asset for this Key, or make a new one if not found
UMyDataAsset DataAsset;
if (TransientAssets.Find(AssetName, DataAsset) == false)
{
DataAsset = Cast<UMyDataAsset>(NewObject(this, UMyDataAsset::StaticClass(), bTransient = true));
TransientAssets.Add(AssetName, DataAsset);
}
// Update the data in the Data Asset with the data from the Data Table Row.
DataAsset.Key = AssetName;
DataAsset.SomeData = Row.SomeData;
DataAsset.bMyBool = Row.bMyBool;
DataAsset.Modify(true);
}
return TransientAssets;
}
/**
* Get the table row data for a given Key.
* This has to be implemented in Blueprint since BP's "Get Data Table Row" function is templated.
*
* Note that we **could** do this entirely in Angelscript using `DataTable::GetDataTableColumnAsString` for
* each column, and iterating through all the rows by index, but it'd be much more effort. This is easier!
*/
protected
UFUNCTION(BlueprintCallable, BlueprintEvent)
FMyDataTableRow GetRowData(UDataTable DataTable, const FName RowName)
{
FMyDataTableRow TableRow;
return TableRow;
}
/**
* Create a new DataAsset with the specified name at the specified path.
* This has to be implemented in Blueprint since AssetTools currently isn't bound in Angelscript.
*/
protected
UFUNCTION(BlueprintCallable, BlueprintEvent)
UMyDataAsset CreateAsset(const FString AssetName, const FString AssetPath)
{
return nullptr;
}
/**
* Find existing Data Assets at the specified path.
* This has to be implemented in Blueprint since AssetRegistry currently isn't bound in Angelscript.
*/
protected
UFUNCTION(BlueprintCallable, BlueprintEvent)
bool GetExistingAssets(const FString BasePath, TArray<UMyDataAsset>& OutDataAssets)
{
return false;
}
/**
* Update a Data Asset with data from another, and mark it as dirty if it was modified.
*/
private void CopyData(UMyDataAsset DataAsset, UMyDataAsset CopyFromDataAsset)
{
// If data is the same, no need to modify
if (DataAsset.SomeData == CopyFromDataAsset.SomeData
&& DataAsset.bMyBool == CopyFromDataAsset.bMyBool)
{
return;
}
DataAsset.SomeData = CopyFromDataAsset.SomeData;
DataAsset.bMyBool = CopyFromDataAsset.bMyBool;
DataAsset.Modify();
}
};
Key Some Data bMyBool
FirstAsset 32 false
SecondAsset 123 true
ThirdAsset 555 false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment