Data table optimization

Hi. The question is possibly simple, but I have not found a clear answer. I’m creating a game with items and equipment. I write most of it in cpp because I know the language, but I don’t know the engine itself that well yet. According to the tutorials, the list of items can be done in a data table. It is super convenient, adding new items is easy, but the question is how the case is with performance.

In the player eq array, for example, store the id of the item and later read the rest of the properties from the datatable. In my experience, anything that requires a “search” in a larger collection runs slower than a hard-coded one. Maybe in the eq array I should keep all the item data (instead of the id itself) that the player has, so that I don’t have to search for it in the table? Maybe the UE has such well-optimized data tables that even searching large sets doesn’t affect performance?

I have also heard of “FDataTableRowHandle”, maybe it helps to get good performance, because it keeps the references to a particular row?

Unfortunately, tutorials often focus on ease of creation, and performance is overlooked. I look forward to advice from the more experienced in game development. :slight_smile:

at the higher end of a project there are continual trade offs between Computation time and Memory overhead, are we going to take up cycles on the CPU to calculate a float when we need it so that we “know” it is accurate (but now we need an equation to get it), or do we store that value in a float table where we just quarry based on the inputs and get the value out (there are still some equations going on, but for stored values I at least feel we have a higher change to have “Floating Point Approximation” happen, but for all these trade offs the question is not “which is better outright” but rather “how many times will it matter”

for example if I only need to calculate that Float value when the player levels up and they will only do that every few minutes to an hour or so later on in the game then the computational hit of doing logarithms and fractional roots will be expensive but it will be running while I pull up a level up screen and the action is kind of paused. while if this was say a damage calculation that needs to happen potentially a few to hundreds of times a frame then looking up the value would probably be faster, but incur a higher memory overhead.

I am going to presume that we are talking about these Items/Equipments being blueprints of probably some inheritance chain from AActor (just so we can spawn them, and destroy them dynamically; while having a physical presence) there is overhead based on the Virtual Function Table to even get AActor and then computation for keeping track of them, and then there is the inheritance steps to be to BP_Sword35 on top of the USkeletalMesh (so that it can be used in animations), the materials/textures, animations, and potentially particle/sound effects; each of these has memory weight including the computational/memory weight by having the UWorld keep track of these AActors and having like a few dozen just sitting around wouldn’t be “too much” weight but as soon as you have “too many” each you might being now asking the end user to need 5GB of memory "just for the inventory system to work" when the player becomes a hoarder and wants to hold 50 of each item they can get their hands on.

so “is a DataTable fast” yes/no/sometimes/maybe/:person_shrugging: it will depend on how many things are in your DataTable, how complex the search needs to be, how big the DataTableRow is, how many sub DataTables are nested inside, or how many DataTables are referenced by you CombinedDataTable.

instead of having it search for “Sword of the Falcon” have it search for some “internalID number” expressed as the “RowName” (you can and probably should still have the “InternalID” also be a specific column value for sanity and fail safe) where “Sword of the Falcon” is actually InternalID=82 by doing the RowNames as a duplicate of InternalID the comparison is now a 2 char compares (or 3 depending on how line termination is handled) instead of a 19(20) char compares.

granted yes an signed int (so we can use FString::FromInt() and Atoi()) can express up to 2 billion something (10 positions) this will be bounded by how many items you will actually have in your game. will you have a few hundred, or will you stop just before you hit 10K ?

1 Like

this is also with the understanding that the DataTable does not hold like TSubclassOf<ASpawnableItem> itself but rather a TSoftObjectPtr<ASpawnableItem>/TSoftClassPtr<ASpawnableItem>
where TSubclassOf<T> still has that full T in memory for the duration that the holder is in memory, so even using the memory saving of a DataTable wouldn’t do much if each of those DataTableRows still had a hard reference to the blueprint.
but now you need to look into loading what those SoftClass/SoftObject pointers, point to probably through the AssetManager to fully realize the Memory overhead savings.

I would also suggest that the Inventory holds some struct that holds these internalIDs and maybe the other “needed” values then the Equipment slots hold the full blueprints (this is a safeguard against those hoarders I was talking about earlier)

1 Like

If I’m not mistaken UDataTable creates TMap<FName, uint8*> RowMap; and uses it to get the row. TMap is a hashing container. It only iterates when you call GetAllRows().

2 Likes

TY for your reply. Maybe I’m too concerned about performance, but I’m the kind of programmer who likes to do something “better” than it sometimes needs to be done, especially when it comes to optimization :grin: . At this point I do not know how complex the structure will be, I am currently in the planning stage. At first there will be a few hundred items, maybe a few thousand after a longer development period.

as I understand it, the utility slots hold bp and frequently used data (dmg etc.), while the entire eq is an array (struct type) that stores, e.g quantity, wear and “id” of the item according to that in the table. An object that lies on the ground is bp (aactor’s child). It has a mesh(+animations) and only the numerical data directly related to that instance. When I pick up an item, its ID and related data go into the eq. When a player in the eq hovers over an item, using ID search for the item data (tooltip etc.) and display it. Right?

As I mentioned earlier about skipping performance in tutorials. I saw that someone made struct that holds bp class, 2d texture, name, amount and several other values. The player’s eq was an array of the type of this structure. :neutral_face: Cool, convenient because you just do a copy paste of the whole struct when lifting item from the ground and everything fits. But performance? I guess how bad it would look with more items. Another thing I saw there. Every time the eq is turned on, a new widget is created and a grid is generated. On turning it off it is removed. Wouldn’t it be a better option to create a widget once and simply hide it? Then in the background you can update fields when you pick up an item, rather than creating a new widget every time you turn on the eq and generating the whole grid again. (I don’t know how keeping the eq open in the background affects memory).

I can do some wild memory optimizations like combining several bools or int8 (1 byte) into a single int8 (also 1 byte) by setting the appropriate bits, but this came in handy for me when saving thousands of rows in a database. Probably not very useful in games

One more small question. It is known that operations on float are more demanding and float takes up more space. In many cases, fractions can be avoided by properly managing numbers to be ints. Is it in UE a good solution throughout the application to use e.g. uint8, uint16 (when the range allows it) than to mindlessly push floats?

for the creating the widget vs the widget just sitting around until needed, still comes down to how many there are going to be? the smaller the number create them and get rid of them, if there are going to be “many” then repurpose them.

for my standpoint if you can scroll the list/grid then you should probably be repurposing if you can.

A DataTableRow is by definition a USTRUCT and you can choose whether to have the UDataTableRow be the Struct that just so happens to be your item definition, or you can have it just so happen to have the same data Fields as another USTRUCT and then just create helper functions to convert one to the other. because USTRUCT doesn’t really work that well with multi-inheritance, and interfaces are mostly designed for classes in general (in C++ the only real difference between struct and class is default access privilege…) I personally like the UDataTableRow being a separate struct that shares data values. then in my inventory I have a SimpleItem (UniqueID, Tex2d, Qty, modification) then the ItemDefinition inherits SimpleItem and also has the same data stuff as the ItemTableRow this way the ItemDefinition can include the SoftClassPtr/SoftObjectPtr to the blueprint

for doing bitwise stuff you are again moving computation time for memory space. each bool does result in 1 byte, but to do the bitwise operation would need to create the resulting bool (1 Byte) and then also have the test criteria as well (1 byte) technically through Unreal you can also define a “Bitmask” using int32.

on size of float: this depends on which floats you are talking about when you just write float you get the 32-bit float, but the float that is in the transforms is a double (64-bit floating point). though keep in mind with any floating point number that “Floating Point Approximation” is a thing (why I despise that the Engine says things like Health is a float :angry:) 1.0 is not always ==1 sometimes it is 1.0000002, and sometimes it is 0.9999999 but "technically “it is 1”

1 Like

So, all I need is to make a struct SimpleItem which contains only unique information (including UniqueID). Later I create another struct ItemDefinition which contains SimpleItem and some additional values like name, description, bp softpointer etc. Based on this struct I create a data table, where RowName == SimpleItem.UniqueID. Everywhere I need to transfer data e.g. player->chest, I use this SimpleItem and when I need other information like a description or bp, I retrieve the entire row from the table using the UniqueID I have in SimpleItem.

The following is not meant as a tutorial, nor a best practice, but rather an example to illustrate previous statements.
for what I will often do for an item/Inventory system

USTRUCT(BlueprintType)
struct FSimpleItem
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString PublicName;
	// name for internal lookup, and sorting
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 InternalID = 0;
	// how many of the item is held (primarily for consumables, but stacking can be a thing)
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 Quantity = 1;
    //...
};

then later I define

/**
 * Definition for Items
 */
USTRUCT(BlueprintType)
struct FItemDef : public FSimpleItem
{
	GENERATED_BODY()
public:
	// Blueprint of the item (Mesh, Materials, EffectEmitters)
	// holding special logic for the item.
	// held as softClass so that it can be instanced multiple times then created and removed on the fly through repositories and asset manager
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSoftClassPtr<class ASpawnItemBase> ItemBlueprint;
//...
};

this approach to use Data Tables I need to define a

USTRUCT(BlueprintType)
struct FItemDataTableRow : public FTableRowBase
{
	GENERATED_USTRUCT_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString PublicName = TEXT("");
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	int32 InternalID = 0;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSoftClassPtr<class ASpawnItemBase> itemBlueprint;
//...
};

then you declare either in the second one, so the other is in context scope, or like in some static maybe BlueprintLibrary you declare/define some helper functions for conversion and resolution.

the inventory is just a glorified wrapper on TArray<FSimpleItem> and equip slots are ASpawnItemBase* where the ASpawnItemBase “has-a” FItemDef

notice in the FItemDataTableRow I don’t declare what the row name is that is defined in the DataTable itself (either in the editor, or in the CSV/JSon) then whenever I want a DataTable row based on the an InternalID I do an atoi() on internalID convert it to an FName to quarry the Table, and then when I get the Row I do a sanity check to see
if(Item.internalID == Row.InternalID) (if it fails I write error messages to the designers because they are the ones that messed up) then I go to async load the TSoftClassPtr just in case say the player wants to spontaneously equip something.

Ok, I’ll try to do it this way. Thanks for your help :sweat_smile:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.