AActor, UObject, or UStruct for the units in an rts game?

Im making a game where the units are instances of a instanced static mesh component.
Thats the only physical representation they have in the world.
They have no collision because the game is tile based and its 2d.
So I identify where the units are by the tile.

The only thing i need, is the data for the units.
Their location is also stored in the instance of the instanced static mesh component as is.

Im conflicted because i have been receiving different and opposing suggestions.
I was about to do a struct + the instance. Experienced unreal users said that is a good idea, saying there is no reason to use AActor in this context, others dont really like the idea of not doing this in anything but actors, even uobject they say its not a good idea. Though it left me with the impression that not all understood the context, or that i didn’t explain it well.

So I started with the hardest hypothesis. The struct + ism.
I created one actor that includes all the units as instances of one instanced static mesh component.
So all units physical representation are part of this ISM component.
Then i created the struct that is the data of the unit. With its health, the current tile, the checkpoints for the movement, etc…
I tried moving it and everything seems to be working for now.
Though i came accross some things that perhaps for not being used to them make me hesitant in pursuing with this…

Im afraid something down the line might make this system a bad idea.
And from my searches i gathered that UStructs COULD be worse in this context for these reasons, though there could be many more:

  • Replication (I dont have experience with unreal multiplayer system though i made multiplayer games in the past). Though this is not supposed to be multiplayer.

  • It is not garbage collected. Though it seems you can just store the unit data struct in an array, and just removing it from the array when the unit dies will just work. *

  • UStruct is bad in blueprints. Though i dont intend to use blueprints a lot.

  • *Combat : When 2 units are fighting, unit A vs Unit B. Unit B will get the struct pointer of unit A, and start combat. If unit A dies. I remove unit instance and remove its corresponding struct from the array of structs. Though the pointer seems to stay alive in the Enemies. Whereas an actor seems that it would fix that automatically, and using IsValid would also give a safety net. With structs i would have to clear also this pointer and any other enemies pointer engaging this unit. Sounds that this could be problematic but also a good exercise.

Anyways. I halted all work, and dont know what to do. I’m with decision paralysis. And dont want to program the whole thing as actors only to have to change it to Struct later.

What should i do in this context?

Edit:
After making some more research, I came accross this reddit post comment:

https://www.reddit.com/r/unrealengine/comments/ihw8lc/comment/g38ddgd/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button

That mentions Data Asset struct.
With this one i could have functions in my class, and could use UFUNCTION for returning pointers of struct. Is suitable for blueprints, etc…

Hi
You may want to use an ECS like Mass for a RTS game.

1 Like

Thanks I have a custom ECS system in line. So i dont think ill need Mass. Though my decision dilemma still persists, now with a 4th option, UDataAsset struct.

If you are using an ECS, why would you need actors or uobjects for units ? What is your custom ECS ?

1 Like

Yes i dont think i need actors for this just structs.
Though i might be missing something as its typical and then later shoot footy :foot::gun:
The ecs is:
Have an array for the entities thats the indexes of my instances, then a component for the movement thats an array with FVectors.
Then i have a subsystem that iterates through all the entities and manages their movement, thats the system.

from what you’re explaining a struct seems like the best idea although all are viable so don’t paralyze yourself.

regarding a data asset this is good for initializing your units but doesn’t replace the instantiated struct.

the only reason i can think to go heavier is if you want to bind to events which may be more efficient than iterating though the whole array

2 Likes

Are you sure you know what an ECS is ?
An array of pointers would make no sense for an ECS. If you are making something yourself, maybe consider using an existing solution : Mass, FLECS, etc …

1 Like

IMO you can make it work with just structs. (@Auran131 beat me to it)

TSharedPtr<> are in a sense.. when the reference count reaches zero (meaning no TSharedPtr instances are pointing to the object anymore), the struct is automatically deleted.
https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/SmartPointerLibrary/

TWeakPtr<> could help you with that assuming there will only be one TSharedPtr owning the data.


1 Like

Thanks i think ill go with the UStruct. Though it feels kind of risky.

regarding a data asset this is good for initializing your units but doesn’t replace the instantiated struct.

Though doesnt that mean that if i creat a struct from a UDataAsset, I can benefit from it? Meaning i can make functions in it, use inheritance properly (UStruct doesnt support inheritance), garbage collected and functional in blueprints?

the only reason i can think to go heavier is if you want to bind to events which may be more efficient than iterating though the whole array

I could do this in UDataAsset struct right?
Or i could do this in the actor that will manage the ism movement etc…

So what you are saying here is that i can use one TSharedPtr array that contains all the unit structs. And then a TWeakPtr for all the other situations where i need to get the Unit data additionally, like for example when i have other units fighting this unit i make them grab the pointer using TWeakPtr that then self deletes if the TSharedPtr is destroyed. Is that correct?

there is only one instance of the dataasset so while you can use functions you cant do anything async, ie event callbacks.

1 Like

Yes. TSharedPtr takes ownership of the struct. The struct can have more than 1 owner, which will keep the data alive in memory. UE internally keeps track of the owners. When all TSharedPtr associated are removed the data is removed from memory instantly (not on GC pass).

TWeakPtr does not take or share ownership, so it has no say in the life of the data it points to.

Here is a quick test:

	TArray<FString> Names = {"New York", "Tokyo", "London"};
	TArray<TSharedPtr<FMyStruct>> MySharedPtrArray;
	TArray<TWeakPtr<FMyStruct>> MyWeakPtrArray;
	TArray<FMyStruct*> MyRawPtrArray;

	for (const auto &itr : Names)
	{
		TSharedPtr<FMyStruct> tmp = MakeShareable(new FMyStruct); // Declare new TSharedPtr.
		tmp->City = itr;

		MySharedPtrArray.Add(MoveTemp(tmp)); // MoveTemp() transfers ownership without increasing or decreasing the reference count.
		MyWeakPtrArray.Add(MySharedPtrArray.Last()); // New TWeakPtr.
		MyRawPtrArray.Add(MySharedPtrArray.Last().Get()); // Raw pointer to the object owned by this shared pointer.
	}
	
	//MySharedPtrArray[FMath::RandRange(0, MySharedPtrArray.Num() - 1)].Reset(); // Reset random TSharedPtr.
	MySharedPtrArray.RemoveAtSwap(FMath::RandRange(0, MyWeakPtrArray.Num() - 1)); // remove random index.

	for (int32 i = 0; i < MyWeakPtrArray.Num(); ++i)
	{
		FString str = FString::Printf(
			TEXT("TWeakPtr: %s | RawPtr: %s"),
			// Check if current TWeakPtr points to valid TSharedPtr.
			(MyWeakPtrArray[i].IsValid()) ? *MyWeakPtrArray[i].Pin()->City : TEXT("Not valid."),
			*MyRawPtrArray[i]->City  
		);
		UE_LOG(LogTemp, Warning, TEXT("%s"), *str);
	}

Results:
image

image



If I’ve made any mistake please feel free to correct. :pray: I’m really interested in this topic.



.Pin() returns a TSharedPtr, might be best to simply TSharedPtr<FMyStruct> PinnedSharedPtr = MyWeakPtr.Pin(); inside the function when in use. Once its out of scope it will release the ownership.

There is some overhead, but imo the biggest challenge will always be the rendering part. For example: this is a test I did in my ongoing study of search algorythms. It’s a 125*125 grid with 10,000 dots (struct susing smart pointers) moving towards a specific random node.

Could make the lab bigger and add more structs but then I would need to get creative in managing the debug lines:



Hope this encourages you to keep at it.

This link is very useful if you want to dive deeper: Smart Pointer Library Test Code · GitHub

1 Like

Thats a great explanation and makes total sense.
Though after removing a pointer from the array TSharedPtr, how do you check if the TWeakPtr are no longer valid?
Because with AActors you just do an IsValid and you are done.
Here i have no clue. Is valid doesnt work with pointers.
So if Unit A attacks Unit B. Both will have a TWeakPtr of each other.
But if one dies, its enemies will try to attack it more, and their TWeakPtr s now point to → $%#

The other issue i found with structs is that inheritance wont work properly.
So if you have FUnitSwordsman and then a child of that FUnitArcher.
How do you check who is who and what to do, if cast is not possible?

Same as you would with AActor. :point_up:

This is where an enum and / or data assets can be implemented. These can also be used to manage custom state machines. This book specifically has helped me a lot with AI. First few chapters address this.


Imo this is the hard (old school) way of going at this. It will also teach you a **** ton more.

1 Like

Thanks a lot. I can now resume my work with these foundations.
I marked your other answer as the solution.
I hope others find this post also useful if they come accross similar dilemas.

1 Like

That’s a very bad design.
For other people who wanna make a RTS, just use existing DOD solutions. For the UE solution, check Mass, statetrees, zonegraph, VAT, etc …

Can you tell us why thats a bad design please?

1 Like

Because of cache locality, as i said previously, check how an ECS works so you can understand why it is important to organize your data for your hardware when you need performances.
Replacing an array of object pointers by an array of struct pointers has no sense.

1 Like

An array of Actors by an array of structs? Thats what we were last discussing.
If i make actors i will have to move the instance and move the actor. Pardon me, but are you sure you are not missing something here? We wrote and got a bit of back and forth above. i cant see why an actor would be a better idea in this context, and there are similar posts to this.
VAT is not useful for a 2d game. Im not using navigation mesh either. I think you didnt read properly.

an array of object pointers by an array of struct pointers

There is a difference between an array of structs and an array of struct pointers

TArray<TSharedPtr<FMyStruct>> MySharedPtrArray;

This is an array of struct pointers.

Did you read about DOD and ECS architectures ? Because you will have all your answers if you do.

1 Like