XYZ

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

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. XYZ

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!

Selection

Have any questions? Email me at jandro@xyzrts.com

View original