XYZ

Devlog 7 - Air Units

mutas

Air units are a staple of any RTS, and specially how air units in starcraft work is something I wanted to recreate in XYZ. And of course there were quite a few things I needed to add to make this work. So let's break down how air units are different from ground units.

Requirements to fly

Now I'm currently using Unreal's AIController with some custom functions to move ground units in the games navmesh. Since our units are derived from the Character class they also have a MovementComponent which allows flight controls, but I have a feeling this was not intended to be used for my specific use case.

Air Movement

Since we're always going to have our air units a set Z height, it's simplifies to having 2d movement on a plane. We don't even worry about collision which is why instead of using MovementComponent of our characters it would be a lot simpler to just use a very barebones movement system.

void FlyToLocation(FVector TargetLocation){
TargetLocation.Z = FlyingZOffset;
FVector Direction = (TargetActorLocation - CurrentLocation).GetSafeNormal();
FVector NewLocation = CurrentLocation + DirectionToAttackLocation * FlyingSpeed * DeltaSeconds;
NewRotation = (TargetActorLocation - NewLocation).Rotation();
SetActorLocation(NewLocation);
SetActorRotation(NewRotation);
}

Whenever we need to move our air unit to a location on the world we can just override it's TargetLocation.Z to our FlyingZOffset and this might as well be 2D movement.

FlyingUnitDemo1

For our different kinds of actions that affect movement MOVE, STOP, ATTACK_MOVE, ATTACKING, etc. I have a custom XYZAIController which handles these different actions, and processes them for our agents in our navmesh. Since our flying units do not use navmesh I derived a XYZFlyingAIController class that has the same methods, but with their simplified versions.

For example here is the XYZMoveToLocation that gets triggered on a units MOVE action.

void AXYZFlyingAIController::XYZMoveToLocation(FVector TargetLocation, float AcceptanceRadius)
{
	AXYZActor* OwningActor = GetXYZActor();
	if(!OwningActor) return;
	GetXYZActor()->SetState(EXYZUnitState::MOVING);
	OwningActor->TargetLocation = TargetLocation;
	bIsMoving = true;
}

Collision

This is actually since all our units are based on Character class we can just set the Collision Profile to Overlap with everything as they will reside above our regular units. There was an issue with our single selection as it since it wasn't blocking any of the collision channels the ray shot from the mouse to the world wasn't colliding. This was fixed by using LineTraceMultiByChannel which returns overlapped hit results.

Handling Air vs Ground attacks

I went with what I thought was the simplest solution 3 booleans.

bool bIsFlying
bool bCanAttackAir
bool bCanAttackGround

In our blueprint editor we can mark whichever ones we want for a unit, and then when we're looking for Units to attack we make make a simple check to see whether we can attack a that unit.

return TargetActor->bIsFlying ? bCanAttackAir : bCanAttackGround;

All Seeing Vision

If you took a look at my devlog on Fog of War you know that the map is subdivided into grids, and mark grid locations of the map as visible if a unit's vision range reaches that grid location. This then takes into account the height at which the actor grid location the actor is at vs the grid location heights in it's vision range.

Since our air units have a flag that let's us know we're flying, we bypass that height check when generating vision if bIsFlying = true HighGroundVision

Stack and Spread!

Now here is the part where I say it's a work in progress! Air units have no collision, so if I assign a move location to a group of air units they will all go into the same spot. I could use the formation logic that my ground units use (if the unit density is small enough they go to offset location of the target destination based on their start location relative to its group). But I do want to have the mechanic where you control a group of units and you are able to stack them together. So I'll have to do some modifications to my formation logic and apply to air units to make it work nicely.

Anyways so for now moving is simple go to location, but once a flying unit is idle it should spread out if it's clumped up. While not perfect this is my solution so far.

We keep track of what units reside in each cell of our map grid. So when a flying unit is idle it should try to slowly drift away from other air units until it's alone in one of the grids. Each grid is 100 x 100 units which is a pretty good spread distance.

case EXYZUnitState::IDLE:
// Code that checks if we found a nearby unit to attack
// Code that gets all other flying units in current grid location
FVector WeightedAvgDirection = FVector::ZeroVector;
float TotalWeight = 0.0f;

for (const FVector& Location : OtherFlyingUnitLocations)
{
float Weight = 1.0f / FMath::Max(1.0f, FVector::Dist(Location, GetActorLocation()));
WeightedAvgDirection += Weight * (Location - GetActorLocation());
TotalWeight += Weight;
}

if (TotalWeight > 0.0f)
{
WeightedAvgDirection /= TotalWeight;
WeightedAvgDirection.Normalize();
}

SetActorLocation(GetActorLocation() - WeightedAvgDirection * SpreadSpeed * DeltaSeconds);

I want to push our current flying unit away from the closest average direction in its group.

float Weight = 1.0f / FMath::Max(1.0f, FVector::Dist(Location, GetActorLocation())); //Higher weight the closer the distance is to our unit
WeightedAvgDirection += Weight * (Location - GetActorLocation()); // bigger direction vector based on the weight
WeightedAvgDirection /= TotalWeight;
WeightedAvgDirection.Normalize(); //Our direction we will move this flying unit towards

So yeah I'm no mathmatecian obviously, but it's sort of working...? I'll need to build upon this, but for now it's a good start.

Spreadout

Update

I realized searching for other flying should search in a perimeter AND the cell that the flying unit since the unit could be overlapping tiles.

TArray<FVector> OtherFlyingUnitLocations;
				TArray<FIntVector2> UnitAreaCoords = MapManager->GetPerimeterCoords(GridCoord, FIntVector2(1,1));
				UnitAreaCoords.Add(GridCoord);

				for(FIntVector2 AreaCoord : UnitAreaCoords)
				{
					TSet<AXYZActor*> FlyingUnitsInCoord = MapManager->Grid[AreaCoord].ActorsInCell;
					for (AXYZActor* FlyingUnit : FlyingUnitsInCoord)
					{
						if (!FlyingUnit) continue;
						if (FlyingUnit->bIsFlying &&
							FlyingUnit->State == EXYZUnitState::IDLE &&
							FlyingUnit->TeamId == TeamId &&
							FlyingUnit != this &&
							FVector::Distance(FlyingUnit->GetActorLocation(), GetActorLocation()) < GetCapsuleComponent()->GetScaledCapsuleRadius()*2.0f)
						{
							OtherFlyingUnitLocations.Add(FlyingUnit->GetActorLocation());
						}
					}
				}

betterspread I also made sure to only check for other IDLE units as this was causing idle air units that are being followed to get perma pushed in a direction.

Well this is as far as I got so far! Thank you for reading and if you have any questions you can reach me at jandro@xyzrts.com

View original