Howl of Iron is a 3rd Person Action Adventure game mixing a thrilling HUNT & COMBAT gameplay.
The player takes control of Vincent Volk, a renowned engineer and former Red Gear worker who has lived a hard childhood and after suffering the loss of his loved ones, his wife and daughter, because of carbogenosis, he decides to completely dehumanize and transfers his consciousness to a mechanical werewolf. With his mechanical body he threads to destroy everything that once harmed him and consummating his revenge.
Howl of Iron was developed as the final thesis of my games programming master degree. It has been developed by 17 students of 4 different master degrees: game programming, game art, game design and production. We have developed the game in 9 months using Unreal Engine by 16 Gears.
You can find more information about all projects in its itch.io website and the source code in this link.
- Developer : 16Gears
- Engine : Unreal Engine 4.27
- Languages : C++, Blueprints
- Role : Gameplay & AI Programmer
My Contribuctions
We created our own perception System. During two weeks my team have try to create a well-functional system. After a few tries we created a modular system that allows us to create different types of perception that increase the float value of the detection. We created 2 types of components: DetectorActorComponent and DetectableActorComponent, with this we can create different types of detectable actors like the player or even the dead bodies of the other enemies that warn others of the position of the player.
UCLASS(BlueprintType)
class HOWLOFIRON_API UHIDetectableActorsArray : public UObject
{
GENERATED_BODY()
private:
static UPROPERTY() TArray detectableActorsArray;
public:
static void HIAddDetectableActor(UHIDetectableActorComponent* detectableActor);
static void HIRemoveDetectableActor(UHIDetectableActorComponent* detectableActor);
static TArray HIGetDetectableActors();
};
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class HOWLOFIRON_API UHIDetectableActorComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UHIDetectableActorComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
virtual void BeginDestroy() override;
};
TArray UHIDetectableActorsArray::detectableActorsArray;
// Sets default values for this component's properties
UHIDetectableActorComponent::UHIDetectableActorComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
//PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bCanEverTick = false;
}
// Called when the game starts
void UHIDetectableActorComponent::BeginPlay()
{
Super::BeginPlay();
UHIDetectableActorsArray::HIAddDetectableActor(this);
}
void UHIDetectableActorComponent::BeginDestroy()
{
Super::BeginDestroy();
UHIDetectableActorsArray::HIRemoveDetectableActor(this);
}
void UHIDetectableActorsArray::HIAddDetectableActor(UHIDetectableActorComponent* detectableActor)
{
detectableActorsArray.Add(detectableActor);
}
void UHIDetectableActorsArray::HIRemoveDetectableActor(UHIDetectableActorComponent* detectableActor)
{
detectableActorsArray.Remove(detectableActor);
}
TArray UHIDetectableActorsArray::HIGetDetectableActors()
{
TArray returnedDetectableActors;
for (UHIDetectableActorComponent* detectableActor : detectableActorsArray)
{
IAbilitySystemInterface* detectableAbilityCharacter = Cast(detectableActor->GetOwner());
if (detectableAbilityCharacter)
{
FGameplayTagContainer abilityCharacterTags;
detectableAbilityCharacter->GetAbilitySystemComponent()->GetOwnedGameplayTags(abilityCharacterTags);
if (abilityCharacterTags.HasAny(FGameplayTagContainer(GET_GAMEPLAY_TAG(HIGHSTEAM_TAG))) == false)
{
returnedDetectableActors.Add(detectableActor->GetOwner());
}
}
else {
returnedDetectableActors.Add(detectableActor->GetOwner());
}
}
return returnedDetectableActors;
}
UCLASS()
class HOWLOFIRON_API UHIDetectionAttributes : public UAttributeSet
{
GENERATED_BODY()
public:
UHIDetectionAttributes() {};
public:
UPROPERTY(Category = "Ability System | Attributes", VisibleAnywhere, BlueprintReadOnly)
FGameplayAttributeData detectionValue;
public:
ATTRIBUTE_ACCESSORS(UHIDetectionAttributes, detectionValue);
};
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class HOWLOFIRON_API UHIDetectorActorComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UHIDetectorActorComponent();
private:
UPROPERTY(VisibleAnywhere)
float detectionValue;
UPROPERTY(VisibleAnywhere, meta = (displayName = "Detection lost per second"))
float detectionLostValue;
UPROPERTY(VisibleAnywhere)
AActor* detectedActor;
UPROPERTY(Transient, BlueprintType)
UAbilitySystemComponent* attachedAbilitySystem;
UPROPERTY(Transient)
UHIDetectionConfiguration* detectionConfiguration;
UPROPERTY(Transient)
const UHIDetectionAttributes* detectionAttributes;
UPROPERTY(EditAnywhere)
UHIDetectionDataAsset* initialDetectionConfiguration;
UPROPERTY(Transient)
UGameplayEffect* gameplayeffect = nullptr;
UPROPERTY()
bool canDecrease;
UPROPERTY()
float decreaseWaitTimer;
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
void HIUpdatedDetector(float DeltaTime);
void HIAttachAbilitySystem(UAbilitySystemComponent* newAttachedAbilitySystem);
void HIInitializeDetectionValue(UDataTable* abilityDataTable);
void HISetDetectionConfiguration(UHIDetectionConfiguration* newDetectionConfiguration);
void HISetDetectionConfiguration(UHIDetectionDataAsset* newDetectionAsset);
void HISetDetectionLostValue(float newDetectionLostValue);
const UHIDetectionAttributes* HIGetDetectionAttributes() const { return detectionAttributes; };
void HISetInitialDetection() { HISetDetectionConfiguration(initialDetectionConfiguration); };
void HISetDetectionValue(float _newValue);
AActor* HIGetDetectedActor();
};
/ Called when the game starts
void UHIDetectorActorComponent::BeginPlay()
{
Super::BeginPlay();
detectionValue = 0.f;
detectionConfiguration = UHIDetectionConfiguration::HICreateDetectionConfiguration(initialDetectionConfiguration);
gameplayeffect = NewObject();
canDecrease = true;
decreaseWaitTimer = 0.f;
}
void UHIDetectorActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
FGameplayTagContainer tagcontainer;
AActor* prueba = this->GetOwner();
tagcontainer.AddTag(GET_GAMEPLAY_TAG(DEAD_TAG));
tagcontainer.AddTag(GET_GAMEPLAY_TAG(ENEMY_DISABLE_TAG));
tagcontainer.AddTag(GET_GAMEPLAY_TAG(STOP_BT_TAG));
tagcontainer.AddTag(GET_GAMEPLAY_TAG(DISABLE_DISTANCE_TAG));
if (!attachedAbilitySystem->HasAnyMatchingGameplayTags(tagcontainer))
{
HIUpdatedDetector(DeltaTime);
}
}
void UHIDetectorActorComponent::HIUpdatedDetector(float DeltaTime)
{
if (ensure(detectionConfiguration))
{
if (attachedAbilitySystem)
{
if (detectionAttributes->GetdetectionValue() != detectionValue)
{
canDecrease = false;
decreaseWaitTimer = 0.f;
}
detectionValue = detectionAttributes->GetdetectionValue();
}
AActor* owner = GetOwner();
UDetectionResult* resultDetection = detectionConfiguration->HIGetDetection(owner);
//float calculedDetection = detectionConfiguration->HIGetDetection(GetOwner());
float previousDetectionValue = detectionValue;
if (resultDetection)
{
if (resultDetection->detectionValue != 0.f)
{
detectionValue = FMath::Min(100.f, detectionValue + resultDetection->detectionValue * DeltaTime);
detectedActor = resultDetection->detectionActor;
canDecrease = false;
decreaseWaitTimer = 0.f;
}
else
{
if (canDecrease)
{
detectionValue = FMath::Max(0.f, detectionValue - (detectionLostValue * DeltaTime));
detectedActor = nullptr;
}
else {
decreaseWaitTimer += DeltaTime;
if (decreaseWaitTimer >= 2.f)
{
canDecrease = true;
decreaseWaitTimer = 0.f;
}
}
}
}
else
{
if (canDecrease)
{
detectionValue = FMath::Max(0.f, detectionValue - (detectionLostValue * DeltaTime));
detectedActor = nullptr;
}
else {
decreaseWaitTimer += DeltaTime;
if (decreaseWaitTimer >= 2.f)
{
canDecrease = true;
decreaseWaitTimer = 0.f;
}
}
}
if (attachedAbilitySystem)
{
gameplayeffect->Modifiers.Empty();
FGameplayModifierInfo modif = FGameplayModifierInfo();
modif.ModifierOp = EGameplayModOp::Additive;
modif.Attribute = detectionAttributes->GetdetectionValueAttribute();
float detectionVariation = (detectionValue - previousDetectionValue);
modif.ModifierMagnitude = FGameplayEffectModifierMagnitude(FScalableFloat(detectionVariation));
gameplayeffect->Modifiers.Add(modif);
attachedAbilitySystem->ApplyGameplayEffectToSelf(gameplayeffect, 1, FGameplayEffectContextHandle());
}
}
}
void UHIDetectorActorComponent::HIAttachAbilitySystem(UAbilitySystemComponent* newAttachedAbilitySystem)
{
attachedAbilitySystem = newAttachedAbilitySystem;
}
void UHIDetectorActorComponent::HIInitializeDetectionValue(UDataTable* abilityDataTable)
{
detectionAttributes = Cast(attachedAbilitySystem->InitStats(UHIDetectionAttributes::StaticClass(), abilityDataTable));
}
void UHIDetectorActorComponent::HISetDetectionConfiguration(UHIDetectionConfiguration* newDetectionConfiguration)
{
detectionConfiguration = newDetectionConfiguration;
}
void UHIDetectorActorComponent::HISetDetectionConfiguration(UHIDetectionDataAsset* newDetectionAsset)
{
detectionConfiguration = UHIDetectionConfiguration::HICreateDetectionConfiguration(newDetectionAsset);
}
void UHIDetectorActorComponent::HISetDetectionLostValue(float newDetectionLostValue)
{
detectionLostValue = newDetectionLostValue;
}
void UHIDetectorActorComponent::HISetDetectionValue(float _newValue)
{
detectionValue = _newValue;
}
AActor* UHIDetectorActorComponent::HIGetDetectedActor()
{
return detectedActor;
}
First, we created the detection module, in wich, we start with the calculation the distance between the player and the enemy, if the distance between them is near enough, then it will raycast to check if the enemy can see the player.
UCLASS()
class HOWLOFIRON_API UHIConeDetection : public UHIDetection
{
GENERATED_BODY()
public:
float maxConeDistance;
float angleAperture;
float coneHeight;
float heightOffset;
float steamDistance;
protected:
virtual UDetectionResult* HICalculateDetection(AActor* detectorActor) override;
virtual const float HICalculateHeuristic(float detectionValue) const override;
virtual void HIDrawDebug(AActor* detectorActor);
};
UDetectionResult* UHIConeDetection::HICalculateDetection(AActor* detectorActor)
{
detectionref->detectionActor = nullptr;
detectionref->detectionValue = 0.f;
AHICharacter* owner = Cast(detectorActor);
FVector detectorLocation = detectorActor->GetActorLocation();
FVector distToEmitterVector;
float distToEmitter;
FVector distToEmitterNormalized;
FVector coneDirection = UKismetMathLibrary::GetForwardVector(owner->GetMesh()->GetSocketRotation("HeadSocket"));
coneDirection.Z = 0.f;
coneDirection.Normalize();
FVector headLocation = owner->GetMesh()->GetSocketLocation("HeadSocket");
float detection = MAX_FLT;
for (AActor* detectableActor : DETECTABLE_ACTORS)
{
FVector detectableActorLocation = detectableActor->GetActorLocation();
IAbilitySystemInterface* interfaceDetectable = Cast(detectableActor);
ensure(interfaceDetectable);
UAbilitySystemComponent* abilitydetectable = interfaceDetectable->GetAbilitySystemComponent();
//Check angle
distToEmitterVector = detectableActorLocation - detectorLocation;
distToEmitterNormalized = distToEmitterVector.GetSafeNormal(0.f);
float dotProduct = FVector::DotProduct(coneDirection, distToEmitterNormalized);
float angleToConeDirection = FMath::RadiansToDegrees(FMath::Acos(dotProduct));
if (angleToConeDirection > angleAperture)
{
break;
}
//Height check
FVector topDetectionHeigth = detectorActor->GetActorUpVector();
topDetectionHeigth.Z *= (coneHeight + heightOffset);
topDetectionHeigth += detectorActor->GetActorLocation();
FVector downDetectionHeight = detectorActor->GetActorUpVector();
downDetectionHeight.Z *= heightOffset;
downDetectionHeight += detectorActor->GetActorLocation();
if (detectableActorLocation.Z > topDetectionHeigth.Z)
{
break;
}
if (detectableActorLocation.Z < downDetectionHeight.Z)
{
break;
}
//DistanceCheck
distToEmitter = distToEmitterVector.Size();
if (distToEmitter < maxConeDistance)
{
//passed
}
else
{
break;
}
//Raycast check
FVector vectordireccion = (detectableActorLocation - detectorLocation).GetSafeNormal(0.f);
FVector raypos = detectorLocation + (vectordireccion * maxConeDistance);
UWorld* world = detectorActor->GetWorld();
FHitResult hitResult;
FCollisionQueryParams collisionParams;
collisionParams.AddIgnoredActor(detectorActor);
if (world->LineTraceSingleByChannel(hitResult, headLocation, raypos, ECC_Visibility, collisionParams) /*== false*/)
{
FHitResult SteamResult;
if (world->LineTraceSingleByChannel(SteamResult, headLocation, raypos, GAME_TRACE_STEAM ,collisionParams))
{
if (hitResult.GetActor() == detectableActor && SteamResult.GetActor() == detectableActor)
{
detection = FMath::Min(distToEmitter, detection);
detectionref->detectionActor = detectableActor;
}
}
}
if (abilitydetectable->HasMatchingGameplayTag(GET_GAMEPLAY_TAG(HIGHSTEAM_TAG)))
{
detection = MAX_FLT;
}
else if (abilitydetectable->HasMatchingGameplayTag(GET_GAMEPLAY_TAG(LOWSTEAM_TAG)))
{
if (distToEmitter > steamDistance)
{
detection = MAX_FLT;
}
else
{
detection = (detection / 2.0f);
}
}
}
detectionref->detectionValue = detection;
return detectionref;
}
const float UHIConeDetection::HICalculateHeuristic(float detectionValue) const
{
if (detectionValue == MAX_FLT)
{
return 0.f;
}
return FMath::Min((maxConeDistance - detectionValue) / maxConeDistance, 1.f);
}
void UHIConeDetection::HIDrawDebug(AActor* detectorActor)
{
AHICharacter* owner = Cast(detectorActor);
FVector detectorLocation = detectorActor->GetActorLocation();
FVector coneDirection = UKismetMathLibrary::GetForwardVector(owner->GetMesh()->GetSocketRotation("HeadSocket"));
coneDirection.Z = 0.f;
coneDirection.Normalize();
FVector headLocation = owner->GetMesh()->GetSocketLocation("HeadSocket");
DrawDebugLine(detectorActor->GetWorld(), headLocation, headLocation + coneDirection * maxConeDistance, FColor::Blue);
FRotator rightRotator = FRotator(0.f, angleAperture, 0.f);
FVector rightAngleVector = rightRotator.RotateVector(coneDirection);
FRotator leftRotator = FRotator(0.f, -angleAperture, 0.f);
FVector leftAngleVector = leftRotator.RotateVector(coneDirection);
FVector topDetectionHeigth = detectorActor->GetActorUpVector();
topDetectionHeigth.Z *= (coneHeight + heightOffset);
topDetectionHeigth += detectorLocation;
FVector downDetectionHeight = detectorActor->GetActorUpVector();
downDetectionHeight.Z *= heightOffset;
downDetectionHeight += detectorLocation;
//Base of the "cacke"
FVector baseRight = topDetectionHeigth + rightAngleVector * maxConeDistance;
FVector baseLeft = topDetectionHeigth + leftAngleVector * maxConeDistance;
DrawDebugLine(detectorActor->GetWorld(), topDetectionHeigth, baseRight, FColor::Yellow, false, -1.f, 0, 10.f);
DrawDebugLine(detectorActor->GetWorld(), topDetectionHeigth, baseLeft, FColor::Yellow, false, -1.f, 0, 10.f);
//DrawDebugLine(detectorActor->GetWorld(), baseRight, baseLeft, FColor::Yellow, false, -1.f, 0, 10.f);
//Top of the "cacke"
FVector topRight = downDetectionHeight + rightAngleVector * maxConeDistance;
FVector topLeft = downDetectionHeight + leftAngleVector * maxConeDistance;
DrawDebugLine(detectorActor->GetWorld(), downDetectionHeight, topRight, FColor::Yellow, false, -1.f, 0, 10.f);
DrawDebugLine(detectorActor->GetWorld(), downDetectionHeight,topLeft, FColor::Yellow, false, -1.f, 0, 10.f);
//DrawDebugLine(detectorActor->GetWorld(), topRight,topLeft, FColor::Yellow, false, -1.f, 0, 10.f);
// //veritcal Lines
// DrawDebugLine(detectorActor->GetWorld(), topRight, baseRight, FColor::Yellow, false, -1.f, 0, 10.f);
// DrawDebugLine(detectorActor->GetWorld(), topLeft, baseLeft, FColor::Yellow, false, -1.f, 0, 10.f);
// DrawDebugLine(detectorActor->GetWorld(), topDetectionHeigth, downDetectionHeight, FColor::Yellow, false, -1.f, 0, 10.f);
}
Another module is NearPerception. The module increases the detection value if the player is in range, we use it to detect the player when is in the enemy’s back. Another type is SoundDetection, the module is activated when the player does some action or ability, calls a detector actor if the enemy is in range, and sends to the enemy the increased value from the sound. Also each type of detection has a weight to allow the Design departament to change the values when they want.
In this game we have three types of enemies. I developed the «Hunter». It have a 3 states that manage the behavior of this enemy. His first state is Patrol, in it, he follows the route around a square, when he see or heard something he start the alert State where he increase his movement speed and also have more vision range. When he completely sees our character enter combat mode, in it, he changes his movement set and uses different weapons depending on the distance or the immunity state. When our main character is near he uses a FlameThrower, in a middle distance he uses a ShotGun and when Vincent is too far our Hunter will use an electric projectile, if it hits it will reduce the movement speed of our main character.
Also we have to create our own decorators, services and tasks, this is one example of one decorator we have to create.
UCLASS()
class HOWLOFIRON_API UHIDecoratorCheckGameplayTag : public UBTDecorator
{
GENERATED_BODY()
public:
UHIDecoratorCheckGameplayTag();
~UHIDecoratorCheckGameplayTag() {};
private:
UPROPERTY(EditAnywhere, Category = GameplayTagCheck)
EGameplayContainerMatchType matchType;
UPROPERTY(EditAnywhere, Category = GameplayTagCheck)
FGameplayTagContainer gameplayTags;
protected:
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
virtual void OnNodeActivation(FBehaviorTreeSearchData& SearchData) override;
virtual void OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds);
private:
bool CheckHasAllGameplayTags(UAbilitySystemComponent* ownerAbilitySystem) const;
bool CheckHasAnyGameplayTags(UAbilitySystemComponent* ownerAbilitySystem) const;
};
UHIDecoratorCheckGameplayTag::UHIDecoratorCheckGameplayTag()
{
bNotifyActivation = true;
bNotifyBecomeRelevant = true;
bNotifyTick = true;
}
bool UHIDecoratorCheckGameplayTag::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
const UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == NULL)
{
return false;
}
AAIController* ownerController = Cast(OwnerComp.GetOwner());
IAbilitySystemInterface* ownerAbilitySystemInterface = Cast(ownerController->GetPawn());
if (ownerAbilitySystemInterface == NULL)
{
return false;
}
UAbilitySystemComponent* abilitySystem = ownerAbilitySystemInterface->GetAbilitySystemComponent();
switch (matchType)
{
case EGameplayContainerMatchType::All:
return this->CheckHasAllGameplayTags(abilitySystem);
break;
case EGameplayContainerMatchType::Any:
return CheckHasAnyGameplayTags(abilitySystem);
break;
default:
{
UE_LOG(LogBehaviorTree, Warning, TEXT("Invalid value for TagsToMatch (EGameplayContainerMatchType) %d. Should only be Any or All."), static_cast(matchType));
return false;
}
}
}
void UHIDecoratorCheckGameplayTag::OnNodeActivation(FBehaviorTreeSearchData& SearchData)
{
Super::OnNodeActivation(SearchData);
}
void UHIDecoratorCheckGameplayTag::OnBecomeRelevant(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::OnBecomeRelevant(OwnerComp, NodeMemory);
OwnerComp.RequestExecution(this);
}
void UHIDecoratorCheckGameplayTag::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
OwnerComp.RequestExecution(this);
}
bool UHIDecoratorCheckGameplayTag::CheckHasAllGameplayTags(UAbilitySystemComponent* ownerAbilitySystem) const
{
FGameplayTagContainer OwnedTags;
ownerAbilitySystem->GetOwnedGameplayTags(OwnedTags);
return OwnedTags.HasAll(gameplayTags);
}
bool UHIDecoratorCheckGameplayTag::CheckHasAnyGameplayTags(UAbilitySystemComponent* ownerAbilitySystem) const
{
FGameplayTagContainer OwnedTags;
ownerAbilitySystem->GetOwnedGameplayTags(OwnedTags);
return OwnedTags.HasAny(gameplayTags);
}
Another perception is NearPerception, it increases the detection value if the player is in range, we use it to detect the player when is in the enemy’s back. Another type is SoundDetection, when the player does some action or ability, it calls a detector actor if it is in range, and sends to it the increased value from the sound. Also each type of detection has a weight to allow the Design departament to che the values when they want.
During this project we have a problem with the optimization of the game. Our environment was procedural and small pieces, in our first map we have 40000 actors in a scene. I develop with other programmers a tool that allows us to group and merge actors in the scene. We inherit from BoxTrigger and make some filters to select actors. The first method is to select all that is inside the trigger, the second method filters actors by staticMesh, it only selects the actors that have the mesh filter, also we upgrade this filter to become an array filter and use it with two or more static mesh. The third method is similar to the second one, but it is filtered by Material of the StaticMesh, also it was upgraded a few days after with the array filter. The last mode allows us to create Instantiated Actors, it creates an Actor with the Instanced Actor component and copy the transform of the selections to the component. After two weeks with this tool, we reduced the actors in our map from 40000 to only 7000 actors in editor, with this we upgraded our performance by around 10 ms and allowed us to reach 60 fps in game.
UENUM()
enum ToolType
{
InstanceMode,
MeshFilterMode,
SelectMode,
MaterialFilterMode,
MultipleMeshFilterMode
};
UCLASS()
class HOWLOFIRONEDITOR_API AHIMergeActorsTool : public ATriggerBox
{
GENERATED_BODY()
public:
AHIMergeActorsTool(const FObjectInitializer& ObjectInitializer);
~AHIMergeActorsTool();
public:
/*
InstanceMode -> Create an instanced actors filtering by mesh.
MeshFilter -> Select all actors inside of the trigger filtering by mesh.
SelectMode -> Select all actors inside of the trigger.
MaterialFilterMode -> Select all actors inside of the trigger filtering by material.
MultipleMeshFilterMode -> Select all actors inside of the trigger filtering by multiple meshes.
*/
UPROPERTY(EditAnywhere, Category = "Actor Merge Tool")
TEnumAsByte typeOfTool;
UPROPERTY(EditAnywhere, Category = "Actor Merge Tool", meta = (EditCondition = "typeOfTool==ToolType::MeshFilterMode || typeOfTool==ToolType::InstanceMode", EditConditionHides))
UStaticMesh* meshToCompare;
UPROPERTY(EditAnywhere, Category = "Actor Merge Tool", meta = (EditCondition = "typeOfTool==ToolType::MultipleMeshFilterMode", EditConditionHides))
TArray meshesToCompare;
UPROPERTY(EditAnywhere, Category = "Actor Merge Tool", meta = (EditCondition = "typeOfTool==ToolType::MaterialFilterMode", EditConditionHides))
UMaterialInterface* materialToFilter;
// Set true if you want to delete the actors from the map when you instantiate the actors
UPROPERTY(EditAnywhere, Category = "Actor Merge Tool", meta = (EditCondition = "typeOfTool==ToolType::InstanceMode", EditConditionHides))
bool destroyOriginalMesh;
protected:
private:
UPROPERTY(Transient)
TArray objects;
UPROPERTY(Transient)
TArray primitiveobjects;
public:
virtual void BeginPlay() override;
UFUNCTION(CallInEditor, Category = "Actor Merge Tool")
void HIApplyMode();
protected:
private:
UFUNCTION()
void HIInstanceActors();
UFUNCTION()
void HIMeshFilter();
UFUNCTION()
void HISelectActors();
UFUNCTION()
void HIMaterialsFilter();
UFUNCTION()
void HIMeshArrayFilter();
UFUNCTION()
void HIBoxTraceMultiForObject(TArray& _auxobjectsInside);
UFUNCTION()
void HIClearArrays();
};
AHIMergeActorsTool::AHIMergeActorsTool(const FObjectInitializer& ObjectInitializer) :
Super(ObjectInitializer.DoNotCreateDefaultSubobject(FName("Sprite")))
{
PrimaryActorTick.bCanEverTick = false;
destroyOriginalMesh = false;
}
AHIMergeActorsTool::~AHIMergeActorsTool()
{
meshesToCompare.Empty();
HIClearArrays();
}
void AHIMergeActorsTool::BeginPlay()
{
Super::BeginPlay();
Destroy();
}
void AHIMergeActorsTool::HIInstanceActors()
{
TArray auxobjectsInside;
HIBoxTraceMultiForObject(auxobjectsInside);
for (FHitResult iter : auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(iter.GetActor());
if (actormesh)
{
if (actormesh->GetStaticMeshComponent()->GetStaticMesh() == meshToCompare)
{
objects.Add(actormesh);
primitiveobjects.Add(actormesh->GetStaticMeshComponent());
GEditor->SelectActor(actormesh, true, true, true);
}
}
}
if (objects.Num() != 0)
{
FActorSpawnParameters params;
params.OverrideLevel = GetLevel();
AHIInstancedActor* HIInstancedActor = GetWorld()->SpawnActor(FVector::ZeroVector, FRotator::ZeroRotator, params);
UInstancedStaticMeshComponent* comp = HIInstancedActor->HIGetInstancedMeshComponent();
comp->SetStaticMesh(meshToCompare);
for (UPrimitiveComponent* iter : primitiveobjects)
{
comp->AddInstance(iter->GetComponentTransform());
}
if (destroyOriginalMesh)
{
for (AStaticMeshActor* iter : objects)
{
iter->Destroy();
}
}
GEditor->SelectActor(HIInstancedActor, true, true, true);
}
}
void AHIMergeActorsTool::HIMeshFilter()
{
TArray auxobjectsInside;
HIBoxTraceMultiForObject(auxobjectsInside);
for (FHitResult iter : auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(iter.GetActor());
if (actormesh)
{
if (actormesh->GetStaticMeshComponent()->GetStaticMesh() == meshToCompare)
{
objects.Add(actormesh);
primitiveobjects.Add(actormesh->GetStaticMeshComponent());
GEditor->SelectActor(actormesh, true, true, true);
}
}
}
}
void AHIMergeActorsTool::HISelectActors()
{
TArray auxobjectsInside;
HIBoxTraceMultiForObject(auxobjectsInside);
for (FHitResult iter : auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(iter.GetActor());
if (actormesh)
{
objects.Add(actormesh);
primitiveobjects.Add(actormesh->GetStaticMeshComponent());
GEditor->SelectActor(actormesh, true, true, true);
}
}
}
void AHIMergeActorsTool::HIMaterialsFilter()
{
bool validate = false;
TArray auxobjectsInside;
HIBoxTraceMultiForObject(auxobjectsInside);
for (FHitResult iter : auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(iter.GetActor());
if (actormesh)
{
for (int32 i = 0; i < actormesh->GetStaticMeshComponent()->GetMaterials().Num(); ++i)
{
if (actormesh->GetStaticMeshComponent()->GetMaterial(i) == materialToFilter)
{
validate = true;
break;
}
}
}
if (validate)
{
objects.Add(actormesh);
primitiveobjects.Add(actormesh->GetStaticMeshComponent());
GEditor->SelectActor(actormesh, true, true, true);
validate = false;
}
}
}
void AHIMergeActorsTool::HIMeshArrayFilter()
{
TArray auxobjectsInside;
HIBoxTraceMultiForObject(auxobjectsInside);
for (FHitResult iter : auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(iter.GetActor());
if (actormesh)
{
for (UStaticMesh* actors : meshesToCompare)
{
if (actormesh->GetStaticMeshComponent()->GetStaticMesh() == actors)
{
objects.Add(actormesh);
primitiveobjects.Add(actormesh->GetStaticMeshComponent());
GEditor->SelectActor(actormesh, true, true, true);
}
}
}
}
}
void AHIMergeActorsTool::HIApplyMode()
{
USelection* selectedActors = GEditor->GetSelectedActors();
selectedActors->Deselect(this);
switch (typeOfTool)
{
case ToolType::InstanceMode:
{
HIInstanceActors();
}
break;
case ToolType::MeshFilterMode:
{
HIMeshFilter();
}
break;
case ToolType::SelectMode:
{
HISelectActors();
}
break;
case ToolType::MaterialFilterMode:
{
HIMaterialsFilter();
}
break;
case ToolType::MultipleMeshFilterMode:
{
HIMeshArrayFilter();
}
break;
default:
break;
}
HIClearArrays();
}
void AHIMergeActorsTool::HIBoxTraceMultiForObject(TArray& _auxobjectsInside)
{
FVector originPos;
FVector extendBox;
GetActorBounds(false, originPos, extendBox);
TArray actorsignore;
TArray> objectType;
objectType.Add(EObjectTypeQuery::ObjectTypeQuery1);
UKismetSystemLibrary::BoxTraceMultiForObjects(GetWorld(), originPos, originPos, extendBox, GetActorRotation(), objectType, false, actorsignore, EDrawDebugTrace::None, _auxobjectsInside, true, FLinearColor::Transparent, FLinearColor::Transparent, 0.f);
TArray newObjects;
for (FHitResult it : _auxobjectsInside)
{
AStaticMeshActor* actormesh = Cast(it.GetActor());
if (actormesh && !actormesh->IsHiddenEd())
{
newObjects.Add(it);
}
}
_auxobjectsInside = newObjects;
}
void AHIMergeActorsTool::HIClearArrays()
{
objects.Empty();
primitiveobjects.Empty();
}
In this game we play as Vincent Volk, a mechanical Werewolf. We use the Gameplay Ability System to develop our combat. We created different abilities for each hit of our character, this can allow us to make different powers in each hit in our combo system, also we create different state abilities that allow us the behaviors mentioned in the Section “Enemies Behavior” like Combat state or Immunity State. More stuff was a long list of tasks than we developed to allow the Design departament create difficult abilities or actions in an easy way in Blueprints. After this project I can say I have a very good experience using the Gameplay Ability System and I’m looking forward to starting a new project using it.
This is some example of one of our abilities:
Also we have to create so many gameplay tasks to do specific things that our designers ask us to create like this:
#include "CoreMinimal.h"
#include "UObject/ObjectMacros.h"
#include "Abilities/Tasks/AbilityTask.h"
#include
#include
#include
#include "HITask_Dash.generated.h"
/**
*
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FActivateDash);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FObstacledDash);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FEndDash);
class ACharacter;
UCLASS()
class HOWLOFIRON_API UHITask_Dash : public UAbilityTask
{
GENERATED_BODY()
public:
UHITask_Dash();
~UHITask_Dash() { UE_LOG(LogTemp, Log, TEXT("Dash task deleted.")); }
public:
UPROPERTY(BlueprintAssignable)
FActivateDash OnActive;
UPROPERTY(BlueprintAssignable)
FObstacledDash OnObstacled;
UPROPERTY(BlueprintAssignable)
FEndDash OnEnd;
protected:
private:
float force;
float dashCorrection;
float time;
bool obstacled;
FVector forwardVector;
FVector targetDash;
FVector dashDirection;
FVector initVector;
ACharacter* owner;
AActor* minActor = nullptr;
FAlphaBlend climbingBlend;
public:
UFUNCTION(BlueprintCallable, Category = "Ability|Tasks", meta = (HidePin = "OwningAbility", DefaultToSelf = "OwningAbility", BlueprintInternalUseOnly = "TRUE"))
///
/// Crea la Task y le iguala los valores de argumento a los de la clase
///
///
///
///
/// wide box for redirect direction to an enemy
///
static UHITask_Dash* HIDash(UGameplayAbility* OwningAbility, float _DashDistance, float _time, float _dashCorrection);
protected:
private:
///
/// First take a FVector in the player to take the LeftStick to know the direction of the dash and rotate the player to the direction
///if 0 will use forward vector of the player
/// create a box ahead the player for check if is an enemy inside, if there are more than 1 enemy the dash correction
/// will choose the enemy near to the forward vector of the player
/// then will check if the target enemy is on screen, if false will use the forward vector for the dash
///
virtual void Activate() override;
///
/// will add the direction vector to the target enemy, if no enemy target, will use the forward vector * force, and will check if the floor below
/// is flat, to change the direction dash for a direction parallel to the floor.
///
///
virtual void TickTask(float DeltaTime) override;
};
#include "HITask_Dash.h"
#include "Characters/VincentVolk/HIWerewolf.h"
#include "DrawDebugHelpers.h"
#include "Game/HIGameData.h"
#include "GameFramework/Character.h"
#include "HIMacros.h"
#include "Kismet/KismetMathLibrary.h"
UHITask_Dash::UHITask_Dash()
{
bTickingTask = true;
}
UHITask_Dash* UHITask_Dash::HIDash(UGameplayAbility* OwningAbility, float _DashDistance, float _time, float _dashCorrection)
{
UHITask_Dash* dash = NewAbilityTask(OwningAbility);
dash->force = _DashDistance;
if (_time <= 0.f)
{
dash->time = 0.00001f;
}
else
{
dash->time = _time;
}
dash->dashCorrection = _dashCorrection;
return dash;
}
void UHITask_Dash::Activate()
{
AActor* actor = GetAvatarActor();
owner = Cast(actor);
AHIWerewolf* player = UHIGameData::HIGetPlayerWerewolf();
initVector = owner->GetActorLocation();
bool isRayHit = false;
FHitResult outHit;
FCollisionQueryParams collisionParams;
collisionParams.AddIgnoredActor(owner);
if (player->resultVectorInput.Size() != 0)
{
FVector targetPoint = (player->resultVectorInput * force) + owner->GetActorLocation();
owner->SetActorRotation(UKismetMathLibrary::FindLookAtRotation(player->GetActorLocation(), targetPoint));
}
player->resultVectorInput = FVector::ZeroVector;
forwardVector = owner->GetActorForwardVector();
targetDash = (forwardVector * force) + owner->GetActorLocation();
APlayerController* Controll = owner->GetController();
FVector playerLocation = player->GetActorLocation();
FVector CubePosition = playerLocation + (forwardVector * (2.f * (force / 3.f)));
FVector cubeShape = FVector(force / 3.f, dashCorrection, 300.f);
TArray hitResults;
float mindot = 0.f;
if (GetWorld()->SweepMultiByChannel(hitResults, CubePosition, CubePosition + 0.00001f, forwardVector.ToOrientationQuat() /*player->GetActorQuat()*/, GAME_TRACE_ENEMY, FCollisionShape::MakeBox(cubeShape)))
{
if (UHIGameData::HIGetDebugMode())
{
DrawDebugBox(GetWorld(), CubePosition, cubeShape, forwardVector.ToOrientationQuat()/*player->GetActorQuat()*/, FColor::Red, false, 5.f);
DrawDebugLine(GetWorld(), playerLocation, playerLocation + (forwardVector * force), FColor::Red, false, 5.f);
}
for (FHitResult hit : hitResults)
{
float tempdot = 0;
FVector tempdotvector = hit.Actor->GetActorLocation() - player->GetActorLocation();
tempdotvector = tempdotvector.GetSafeNormal();
tempdot = FVector::DotProduct(player->GetActorForwardVector(), tempdotvector);
if (tempdot >= mindot)
{
minActor = hit.GetActor();
mindot = tempdot;
}
}
}
else
{
if (UHIGameData::HIGetDebugMode())
{
DrawDebugBox(GetWorld(), CubePosition, cubeShape, forwardVector.ToOrientationQuat() /*player->GetActorQuat()*/, FColor::Green, false, 5.f);
DrawDebugLine(GetWorld(), playerLocation, playerLocation + (forwardVector * force), FColor::Green, false, 5.f);
}
}
if (minActor)
{
if (UHIGameData::HIIsPositionOnScreen(minActor->GetActorLocation()))
{
targetDash = minActor->GetActorLocation();
}
}
dashDirection = targetDash - owner->GetActorLocation();
dashDirection.Normalize(0.00001);
isRayHit = GetWorld()->LineTraceSingleByChannel(outHit, FVector(playerLocation.X, playerLocation.Y, playerLocation.Z - 20.f), FVector(playerLocation.X, playerLocation.Y, playerLocation.Z - 20.f) + dashDirection * force, ECC_Visibility, collisionParams);
if (isRayHit)
{
float force2 = UKismetMathLibrary::Vector_Distance(playerLocation, outHit.Location);
float time2 = time * force2 / force;
if (UHIGameData::HIGetDebugMode())
{
DrawDebugLine(GetWorld(), FVector(playerLocation.X, playerLocation.Y, playerLocation.Z - 20.f), outHit.Location, FColor::Purple, false, 5.f, 0.f, 15.f);
}
force = force2;
time = time2;
obstacled = true;
}
climbingBlend.SetBlendOption(EAlphaBlendOption::Linear);
climbingBlend.SetBlendTime(time);
climbingBlend.Reset();
OnActive.Broadcast();
}
void UHITask_Dash::TickTask(float DeltaTime)
{
climbingBlend.Update(DeltaTime);
if (minActor)
{
owner->AddActorWorldOffset((dashDirection * (force / time)) * DeltaTime, true);
}
else
{
FHitResult ground;
FVector tracedown = FVector(0.f, 0.f, -500.f);
FCollisionObjectQueryParams QueryParams;
QueryParams.AddObjectTypesToQuery(ECC_WorldStatic);
GetWorld()->LineTraceSingleByObjectType(ground, owner->GetActorLocation(), owner->GetActorLocation() + tracedown, QueryParams);
FVector slopeNormal = ground.Normal;
FVector leftVector = owner->GetActorRightVector();
leftVector = leftVector * -1.f;
dashDirection = FVector::CrossProduct(slopeNormal, leftVector);
owner->AddActorWorldOffset((dashDirection * (force / time)) * DeltaTime, true);
}
if (climbingBlend.IsComplete())
{
if (!obstacled)
{
OnEnd.Broadcast();
}
else
{
OnObstacled.Broadcast();
}
EndTask();
return;
}
return;
}
During the development we need to create paths for the enemies’ patrols so we created waypoint tasks. When the enemies arrive at a waypoint it triggers all the HIWaypointTask that are instanced in the waypoint, with this we can allow the enemies to play animations or wait time on the waypoint. Also with this, our design team can create his own conditions like go across a trigger to start the patrol.
UCLASS(BlueprintType, Blueprintable)
class HOWLOFIRON_API UHIWaitTask : public UHIWaypointTask
{
GENERATED_BODY()
public:
UHIWaitTask();
public:
UPROPERTY(EditAnywhere, Category = "WaitPoint")
float waitTime;
UPROPERTY(EditAnywhere, Category = "WaitPoint")
float rotationSpeed;
protected:
private:
UPROPERTY()
float elapsedWait;
public:
protected:
virtual void HIStartTask(AHIEnemy* _enemy, AHIPatrolPoint* _waypoint) override;
virtual void HIUpdateTask(float DeltaTime) override;
};
UHIWaitTask::UHIWaitTask()
{
// taskStarted = false;
}
void UHIWaitTask::HIStartTask(AHIEnemy* _enemy, AHIPatrolPoint* _waypoint)
{
Super::HIStartTask(_enemy,_waypoint);
elapsedWait = 0.f;
}
void UHIWaitTask::HIUpdateTask(float DeltaTime)
{
if (HIIsExecuting())
{
elapsedWait += DeltaTime;
FRotator interpRot = FMath::RInterpConstantTo(enemy->GetActorRotation(), waypoint->GetActorRotation(), DeltaTime, rotationSpeed);
FRotator actorRot = enemy->GetActorRotation();
actorRot.Yaw = interpRot.Yaw;
enemy->SetActorRotation(actorRot);
if (elapsedWait >= waitTime)
{
HIEndTask();
}
}
}
UCLASS(BlueprintType, Blueprintable)
class HOWLOFIRON_API AHIPatrolPoint : public ATargetPoint
{
GENERATED_BODY()
public:
AHIPatrolPoint();
~AHIPatrolPoint() { UE_LOG(LogTemp, Log, TEXT("Targetpoint script deleted.")); }
public:
UPROPERTY()
FOnTaskEnd delegatewaypoint;
private:
UPROPERTY(EditAnywhere, Category = "Patrol")
TArray neighbours;
UPROPERTY(EditAnywhere, Instanced, Category = "Patrol", meta = (DisplayName = "WaypointTasksInstances"))
TArray waypointTasks;
UPROPERTY()
bool taskStarted = false;
UPROPERTY(Transient)
class AHIEnemy* enemy;
public:
const TArray HIGetNeighbours() const;
UFUNCTION()
void HIAbortAction();
UFUNCTION()
///
/// Starts waypoint's tasks functionality
///
/// Enemy associed to the waypoint tasks
/// true if tasks are executed, false instead
bool HIArriveAction(AHIEnemy* _enemy);
UFUNCTION()
///
/// Function executed by the initial patrol point before return the substitute patrol point instead of the nearest point
///
/// Returns substitute patrol point
virtual AHIPatrolPoint* UpdateRequieredPatrolPoint();
virtual void Tick(float DeltaTime) override;
class AHIEnemy* const HIGetEnemyReference() { return enemy; }
protected:
virtual void HISetEnemy(class AHIEnemy* newEnemy);
private:
#if WITH_EDITOR
UFUNCTION()
virtual bool ShouldTickIfViewportsOnly() const override;
UFUNCTION()
void HIEditorTick();
#endif
};
AHIPatrolPoint::AHIPatrolPoint()
{
PrimaryActorTick.bCanEverTick = true;
}
const TArray AHIPatrolPoint::HIGetNeighbours() const
{
return neighbours;
}
void AHIPatrolPoint::HIAbortAction()
{
if (taskStarted)
{
for (UHIWaypointTask* iter : waypointTasks)
{
iter->HIAbortTask();
}
}
taskStarted = false;
}
bool AHIPatrolPoint::HIArriveAction(AHIEnemy* _enemy)
{
HISetEnemy(_enemy);
if (waypointTasks.Num() == 0)
{
return false;
}
for (UHIWaypointTask* iter : waypointTasks)
{
iter->HIStartTask(_enemy, this);
taskStarted = true;
}
return true;
}
void AHIPatrolPoint::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (taskStarted)
{
FVector cosa = enemy->GetActorLocation();
bool taskFinished = true;
for (UHIWaypointTask* iter : waypointTasks)
{
if (iter->HIIsExecuting())
{
iter->HIUpdateTask(DeltaTime);
taskFinished = false;
}
}
if (taskFinished)
{
taskStarted = false;
delegatewaypoint.Broadcast(true);
}
}
#if WITH_EDITOR
if (GetWorld() != nullptr && GetWorld()->WorldType == EWorldType::Editor)
{
HIEditorTick();
}
#endif
}
AHIPatrolPoint* AHIPatrolPoint::UpdateRequieredPatrolPoint()
{
return nullptr;
}
void AHIPatrolPoint::HISetEnemy(AHIEnemy* newEnemy)
{
enemy = newEnemy;
}
#if WITH_EDITOR
bool AHIPatrolPoint::ShouldTickIfViewportsOnly() const
{
if (GetWorld() != nullptr && GetWorld()->WorldType == EWorldType::Editor)
{
return true;
}
return false;
}
void AHIPatrolPoint::HIEditorTick()
{
for (AHIPatrolPoint* neighbour : neighbours)
{
if (neighbour)
{
DrawDebugDirectionalArrow(GetWorld(), GetActorLocation(), neighbour->GetActorLocation(), 300.f, FColor::Magenta, false, -1.f, 0.f, 20.f);
}
}
}
#endif