Devlog 3 - Selecting Units
Selecting units is something that every RTS has and if it's good you won't really even notice it, but how do we replicate that in our game?
Let's breakdown what selecting a unit is doing.
Inputs
- Primary Input - our left click for single target selection
- Primary Modifier - think of shift click a unit and we add that to our current selection or remove it's in the selection already
- Secondary Modifier - this will be ctrl as default and if held with a primary input we will select all units of a certain type
- Primary Input Held - If we are holding our primary input we will create the box
- Primary Input Release - We select all units in our drawn box with some exceptions
Ok so now we got the basic inputs our client can do we can easily bind them in unreal to know when we are pressing/triggering/releasing them
EnhancedInputComponent->BindAction(PrimaryInputAction, ETriggerEvent::Started, this, &AXYZPlayerController::OnInputStarted, EXYZInputType::PRIMARY_INPUT);
EnhancedInputComponent->BindAction(PrimaryInputAction, ETriggerEvent::Triggered, this, &AXYZPlayerController::OnInputTriggered, EXYZInputType::PRIMARY_INPUT);
EnhancedInputComponent->BindAction(PrimaryInputAction, ETriggerEvent::Completed, this, &AXYZPlayerController::OnInputReleased, EXYZInputType::PRIMARY_INPUT);
// do so for the rest of the inputs
Now that we know when our player is inputting certain actions how do we actually Select a unit?
Single Target Selection
If our player single target selects we can easily convert our mouse location to our world location and see if there is a Unit at that location
void AXYZPlayerController::OnInputStarted(EXYZInputType InputType)
{
AXYZActor* HitActor = FindActorFromMousePos(GetMousePos());
switch (InputType){
case PRIMARY_INPUT:
Select(HitActor);
// ** rest of code
Easy enough but theres a structure i haven't mentioned yet SelectionStructure
this is just a Unreal Object that keeps track of what units you have selected in a map.
class UXYZSelectionStructure
{
TSortedMap<int32, TMap<int32, AXYZActor*>> SelectedActors;
int32 ActiveActor;
int32 ActiveIndex;
int32 Num;
void Add(AXYZActor* Actor);
void Add(TArray<AXYZActor*> Actors);
void Remove(AXYZActor* Actor);
void Remove(int32 ActorUId);
void Remove(TArray<AXYZActor*> Actors);
bool Contains(AXYZActor* Actor);
bool Contains(int32 ActorUId);
void CycleSelection();
void Empty();
bool IsEmpty();
};
Shift Click Selection
Instead of adding to our selection structure we will have two possible cases add actor to selection or remove if its already in the structure
Box selection
Ok now this is the big one the bread and butter of rts games the green square
there's a few things we need to keep track of
FVector2D BoxStart
FVector2D BoxEnd
We will set the box start on a click and update BoxEnd on the holding of our primary input.
We also need to broadcast these location over to a blueprint so it can draw onto the screen a box.
With Unreal creating a derived class from HUD let's us use the very convinient GetActorsInSelectionRectangle
with our BoxStart/End variables
We also want to always store all the actors on the screen for our Control click so we just use the TopLeft and BottomRight of our screen location
void AXYZHUD::DrawHUD()
{
Super::DrawHUD();
if (bSelectActors) {
GetActorsInSelectionRectangle(TopLeft, BottomRight, AllActorsOnScreen, false);
GetActorsInSelectionRectangle(BoxStart, BoxEnd, SelectedActors, false);
}
else {
ClearActors();
}
}
Now that we are storing our selection we can easily pass our selected units when we send an input to the server!
CreateAndQueueInput(SelectionStructure->ToActorIdArray(), XYZActorHitId, WorldHit.Location, EXYZInputType::ATTACK_MOVE, bPrimaryModifier);
One last thing to note is that a lot of RTS like starcraft allow you to tab through units in your selection. Which is why we use a sorted map with the key of ActorId (this indicates a units id not a unique id) and when we press tab we add an index that loops back in our selection structure. We then store our current active index, so when we need to cast an ability we only call the selected active actors.
And another thing to note... Control groups this are quite simple as we have a set amount of possible control groups which we create empty SortedMaps and if we select a control group we change our SelectedMap to be one of the control groups.
void AXYZPlayerController::OnControlGroupInputStarted(int32 ControlGroupIndex) {
bool bDoubleInput = LastControlGroupInputTime[ControlGroupIndex] <= DoubleInputThreshold;
LastControlGroupInputTime[ControlGroupIndex] = 0.0f;
if (bPrimaryModifier) {
SelectionStructure->AddToControlGroup(ControlGroupIndex);
}
else if (bSecondaryModifier) {
SelectionStructure->SetControlGroup(ControlGroupIndex);
}
else {
SelectionStructure->SelectControlGroup(ControlGroupIndex);
OnSelectionIdsEvent.Broadcast(SelectionStructure->ToActorIdArray());
if(bDoubleInput && !SelectionStructure->IsEmpty())
{
FVector TargetActorLocation = SelectionStructure->ToArray()[0]->GetActorLocation();
CameraController->SetActorLocation(FVector(TargetActorLocation.X, TargetActorLocation.Y, CameraController->GetActorLocation().Z) + FVector(-250.0f, 50.0f, 0.0f));
}
}
TArray<int32> ControlGroups;
for (TSortedMap<int32, TMap<int32, AXYZActor*>> ControlGroup : SelectionStructure->ControlGroups) {
ControlGroups.Add(ControlGroup.Num());
}
OnControlGroupEvent.Broadcast(ControlGroups);
}
And our final result is!
Have any questions? Email me at jandro@xyzrts.com