├── .gitignore ├── Content ├── Considerations │ └── NotWhenTooCloseConsideration.uasset ├── Curves │ ├── LinearUp.uasset │ └── QuadraticUp.uasset ├── Decisions │ ├── CombatDSE.uasset │ ├── CombatDecision.uasset │ ├── DecisionMaker.uasset │ └── TargetSelectDSE.uasset └── Skills │ ├── Skill.uasset │ ├── SkillDSE.uasset │ └── SkillSet.uasset ├── LICENSE ├── README.md ├── Resources └── Icon128.png ├── Source └── UtilityAI │ ├── Private │ ├── Considerations │ │ ├── AllyConsideration.cpp │ │ ├── BlackboardConsideration.cpp │ │ ├── Consideration.cpp │ │ ├── DistanceConsideration.cpp │ │ └── EnemyConsideration.cpp │ ├── Controllers │ │ ├── UtilityAIInterface.cpp │ │ └── UtilityIntelligence.cpp │ ├── DSE │ │ ├── DecisionScoreEvaluator.cpp │ │ ├── NonSkillDecisionScoreEvaluator.cpp │ │ └── SkillDecisionScoreEvaluator.cpp │ ├── Decisions │ │ ├── CombatDecision.cpp │ │ ├── Decision.cpp │ │ ├── DecisionBase.cpp │ │ ├── DecisionMaker.cpp │ │ └── Skills │ │ │ ├── Skill.cpp │ │ │ └── SkillSet.cpp │ ├── UtilityAI.cpp │ └── UtilityAIHelpers.cpp │ ├── Public │ ├── Considerations │ │ ├── AllyConsideration.h │ │ ├── BlackboardConsideration.h │ │ ├── Consideration.h │ │ ├── DistanceConsideration.h │ │ └── EnemyConsideration.h │ ├── Controllers │ │ ├── UtilityAIInterface.h │ │ └── UtilityIntelligence.h │ ├── DSE │ │ ├── DecisionScoreEvaluator.h │ │ ├── NonSkillDecisionScoreEvaluator.h │ │ └── SkillDecisionScoreEvaluator.h │ ├── Decisions │ │ ├── CombatDecision.h │ │ ├── Decision.h │ │ ├── DecisionBase.h │ │ ├── DecisionMaker.h │ │ └── Skills │ │ │ ├── Skill.h │ │ │ └── SkillSet.h │ ├── UtilityAI.h │ └── UtilityAIHelpers.h │ └── UtilityAI.Build.cs └── UtilityAI.uplugin /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Visual Studio 2015 database file 5 | *.VC.db 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.ipa 36 | 37 | # Shortcuts 38 | *.lnk 39 | 40 | # These project files can be generated by the engine 41 | *.xcodeproj 42 | *.xcworkspace 43 | *.sln 44 | *.suo 45 | *.opensdf 46 | *.sdf 47 | *.VC.db 48 | *.VC.opendb 49 | 50 | # Precompiled Assets 51 | SourceArt/**/*.png 52 | SourceArt/**/*.tga 53 | 54 | # Binary Files 55 | Binaries/* 56 | 57 | # Builds 58 | Build/* 59 | Builds/* 60 | 61 | # Whitelist PakBlacklist-.txt files 62 | !Build/*/ 63 | Build/*/** 64 | !Build/*/PakBlacklist*.txt 65 | 66 | # Don't ignore icon files in Build 67 | !Build/**/*.ico 68 | 69 | # Built data for maps 70 | *_BuiltData.uasset 71 | 72 | # Configuration files generated by the Editor 73 | Saved/* 74 | 75 | # Compiled source files for the engine to use 76 | Intermediate/* 77 | 78 | # Cache files for the editor to use 79 | DerivedDataCache/* 80 | -------------------------------------------------------------------------------- /Content/Considerations/NotWhenTooCloseConsideration.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Considerations/NotWhenTooCloseConsideration.uasset -------------------------------------------------------------------------------- /Content/Curves/LinearUp.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Curves/LinearUp.uasset -------------------------------------------------------------------------------- /Content/Curves/QuadraticUp.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Curves/QuadraticUp.uasset -------------------------------------------------------------------------------- /Content/Decisions/CombatDSE.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Decisions/CombatDSE.uasset -------------------------------------------------------------------------------- /Content/Decisions/CombatDecision.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Decisions/CombatDecision.uasset -------------------------------------------------------------------------------- /Content/Decisions/DecisionMaker.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Decisions/DecisionMaker.uasset -------------------------------------------------------------------------------- /Content/Decisions/TargetSelectDSE.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Decisions/TargetSelectDSE.uasset -------------------------------------------------------------------------------- /Content/Skills/Skill.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Skills/Skill.uasset -------------------------------------------------------------------------------- /Content/Skills/SkillDSE.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Skills/SkillDSE.uasset -------------------------------------------------------------------------------- /Content/Skills/SkillSet.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Content/Skills/SkillSet.uasset -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jay Stevens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unreal Engine Utility AI 2 | 3 | This is a plugin which aims to implement a utility AI system in Unreal Engine 4, replacing the built-in behavior tree system. It uses some of the classes from the default Behavior Tree system, but also implements a number of new ones. 4 | 5 | ## What is Utility AI? 6 | 7 | See the following GDC talks: 8 | 9 | 1. https://www.gdcvault.com/play/1012410/Improving-AI-Decision-Modeling-Through 10 | 11 | 2. https://www.gdcvault.com/play/1021848/Building-a-Better-Centaur-AI 12 | 13 | ## How does the Unreal Engine Utility AI work? 14 | 15 | There are a few components which make the AI tick: 16 | 17 | * `UtilityIntelligence`: A component you add to your AI to enable the Utility AI system. Contains `DecisionMakers` and `SkillSets`. Inform the intelligence whenever your AI detects an enemy or friendly, and give your AI any interesting locations it might want to check out (patrol points or the last place it saw an enemy). 18 | 19 | * `DecisionMaker` and `SkillSet`: These two classes are somewhat similar, but have different purposes. A `DecisionMaker` is intended for long(ish)-term planning -- should I run away? Should I try to get closer to an enemy? How is my health doing? A `SkillSet` handles any *short-term* actions -- should I shoot my gun? Do I throw a grenade? Should I do a melee attack? The `DecisionMaker` is a list of `Decisions`, whereas a `SkillSet` is a map matching `Skills` to `SkillDecisionScoreEvaluators`. 20 | 21 | * A `DecisionScoreEvaluator` (or **DSE**) evaluates scores to determine whether we should make a `Decision`. You can have `SkillDecisionScoreEvaluators`, which give scores to individual `Skills` as well as `NonSkillDecisionScoreEvaluators`, which give scores to `Decisions`. The DSE gets a "starting" weight (generally 1) and a bonus factor (generally 0). The weight and bonus factor get added together and then sent through a bunch of `Considerations`. Each `Consideration` is queried and returns a score between 0 and 1. Our current score is multiplied by the score provided by each `Consideration`, and the final result gets returned. The `DecisionMaker` or `SkillSet` then selects whatever DSE scores the highest. 22 | 23 | * A `Consideration`, as mentioned, simply looks at what's going on in a game and provides a value between 0 and 1, with 0 meaning "absolutely do not do this under any circumstances". One `Consideration` can look at which enemies are nearby and give a high score to a melee attack, for example, but then the next `Consideration` can look and see that your character is on extremely low health and shouldn't even *think* about getting close and attacking. Generally, the score provided by a `Consideration` should be run through a FloatCurve to provide multiple behaviors without modifying any code. 24 | 25 | * A `Decision` gets fired a few times a second (*not* every frame). As mentioned, a `Decision` is involved in long-term planning. An example `Decision` would select a target from an array stored on the AI and then try to move close to that target. You can make multiple `Decisions` inside of a `Decision` if needed; the example `CombatDecision` class takes an array of potential enemies and runs an extra DSE on top of the "normal" DSE it's associated with to make a choice about which enemy to attack. In that example, the "normal" DSE should just look at the character's health and the number of enemies, whereas the "combat" DSE should look at enemy health and distance. Note that `Decisions` can't be created at runtime; `CombatDecision` condenses each potential target down to its bare context and then decides based on the raw contexts. 26 | 27 | * A `Skill` is very similar to a `Decision`. The main difference is that a `Decision` should *always* use the same DSE, whereas `Skills` may need different DSEs for different characters. For example, a generic "sword attack" skill might mean different things to a Ninja (which should prioritize attacking from behind) vs. a Pirate (which doesn't really care where he is, only that the sword can hit). The `Skill` can be reused, but the `SkillSet` maps it to a different `SkillDecisionScoreEvaluator` based on desired character behavior. Like a `Decision`, a `Skill` also only gets run a few times a second -- generally speaking, it gets run more than `Decisions` do, but still not every frame. You can specify the tick rate for `Decisions` and `Skills` in your `UtilityIntelligence`. 28 | 29 | ## What to Override 30 | 31 | Generally, you should create custom: 32 | 33 | * `Considerations`, to give a score based on your game mechanics (health levels, amount of enemies, etc.). 34 | 35 | * `Decisions`, to make your AI take custom actions. 36 | 37 | * `Skills`, which implement any behaviors you want your AI to have (shooting, for example). 38 | 39 | You can also create custom `DecisionScoreEvaluators` if you wanted to play with the concept of "momentum" -- making a decision more or less likely to be chosen if it was chosen recently. However, `Considerations` and `DecisionScoreEvaluators` shouldn't implement any gameplay behavior -- save those for your `Decisions` and `Skills`. 40 | 41 | ## Dynamic Behaviors 42 | 43 | Each `DecisionMaker` contains a set of related `Decisions` that a character can make. The nice thing about these is that you can dynamically add and remove `DecisionMakers` whenever you want. For example, if a character walks into a tavern, they should be able to go up to the tavern and ask for a drink, right? You can set it up so as soon as the AI enters a trigger associated with the tavern, a special "tavern" `DecisionMaker` gets added to the list of decisions they can make. This `DecisionMaker` has `Decisions` for going up to the bar and ordering a drink, making drunken emotes, getting in random brawls, etc. It gets treated just like any of the character's "natural" behaviors, allowing the character to do things they should do in a tavern -- but all the underlying logic remains the same, so if the character gets punched they can still use the same combat behavior they would anywhere else in the world. When the AI leaves the tavern, the special tavern `DecisionMaker` gets removed, and the AI goes back to making `Decisions` like it would normally. 44 | 45 | `SkillSets` are handled the same way; if a character picks up a new weapon, that weapon can add an additional `SkillSet` to the character's repertoire, allowing them to execute any special combat-related behaviors associated with that weapon if needed. If the character gets disarmed or the weapon is otherwise removed, then they can lose the `SkillSet` associated with the weapon and it would no longer be a valid option if the character got into trouble. 46 | 47 | 48 | ## Getting Started 49 | 50 | 1. You should start by adding a `UtilityIntelligence` component to your AIController. You can also implement the `GameplayTags` interface on your AIController if you wanted to have `Considerations` "cooldown" based on a GameplayTag, but this is optional. You can add the `UtilityIntelligence` to the actual Pawn instead, if you wish, but bear in mind that to start the actual AI system you're going to need to pass a reference to a valid AI Controller. 51 | 52 | 2. The next step is to create the actual assets. Everything else in the system is a `DataAsset`, which is a file stored on the disk that contains data. Because these classes are physical files containing data, you should avoid modifying them at runtime -- in C++, all the functions you override are marked as `const`, but the `const` modifier had to be dropped in some places to provide access via Blueprints. 53 | 54 | 3. Next, you should override the `Consideration`, `Decision`, and `Skill` classes and implement your custom behaviors. You can use either C++ or Blueprint for this. I've noticed that Unreal's Blueprint support for DataAssets is a bit buggy; it works, but use it at your own risk. A few child classes have already been created for you. 55 | 56 | 4. In the Content Browser, hit the "Add New" button and go down to "Miscellaneous -> Data Asset." There'll be a list of a whole bunch of files -- among them `DecisionMaker`, `SkillSet`, `SkillDecisionScoreEvaluator`, `NonSkillDecisionScoreEvaluator`, as well as the custom child classes you made for `Consideration`, `Decision`, and `Skill`. Go ahead and create your own DataAssets for each one of these; there's a few already provided for you in the Plugin as well, if needed. 57 | 58 | 5. Once you create each DataAsset, open it up and modify anything you want to modify. Most things should have descriptive names and tooltips; if you find something that's lacking, feel free to open up an issue here on this GitHub and request clarification, or submit a pull request yourself! 59 | 60 | 6. Add all your default `DecisionMakers` and `Skills` to the `UtilityIntelligence` attached to your AI Controller. Inside the Blueprint (or C++) code for the AI Controller, call `UtilityIntelligence::StartAI` and pass in your AI Controller and Blackboard Component. 61 | 62 | 7. You're done! You should see your AI executing decisions when you run the game. Note that there's not much in the way of debugging tools; better visualizations for debugging are still a work in progress. 63 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jay2645/UnrealUtilityAI/c8a9f6b357fdc994f2680126a59edb0782f37479/Resources/Icon128.png -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Considerations/AllyConsideration.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "AllyConsideration.h" 5 | #include "UtilityIntelligence.h" 6 | 7 | UAllyConsideration::UAllyConsideration() 8 | { 9 | Description = "A consideration based on how many allies we have nearby."; 10 | MaxAllies = 4; 11 | } 12 | 13 | float UAllyConsideration::Score_Implementation(const FDecisionContext & Context) const 14 | { 15 | if (ResponseCurve == NULL) 16 | { 17 | return 0.0f; 18 | } 19 | else 20 | { 21 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 22 | if (intelligence == NULL) 23 | { 24 | return 0.0f; 25 | } 26 | float allyCount = (float)intelligence->GetAllies().Num(); 27 | return ResponseCurve->GetFloatValue(allyCount / (float)MaxAllies); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Considerations/BlackboardConsideration.cpp: -------------------------------------------------------------------------------- 1 | #include "BlackboardConsideration.h" 2 | #include "BehaviorTree/BlackboardComponent.h" 3 | 4 | UBlackboardConsideration::UBlackboardConsideration() 5 | { 6 | Description = "A consideration based on a Blackboard value."; 7 | } 8 | 9 | float UBlackboardConsideration::Score_Implementation(const FDecisionContext& Context) const 10 | { 11 | if (Context.AIBlackboard != NULL) 12 | { 13 | return Context.AIBlackboard->GetValueAsFloat(BlackboardKey.SelectedKeyName); 14 | } 15 | else 16 | { 17 | return 0.0f; 18 | } 19 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Considerations/Consideration.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "Consideration.h" 4 | #include "GameplayTagAssetInterface.h" 5 | #include "UtilityAIInterface.h" 6 | #include "UtilityIntelligence.h" 7 | 8 | UConsideration::UConsideration() 9 | { 10 | Description = "Default consideration, will return a score of 0"; 11 | } 12 | 13 | bool UConsideration::CanScore(const FDecisionContext& Context) const 14 | { 15 | AActor* target = Context.OurTarget; 16 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 17 | if (intelligence == NULL) 18 | { 19 | return false; 20 | } 21 | AAIController* us = intelligence->GetController(); 22 | if (!HasAllRequiredGameplayTags(target, us)) 23 | { 24 | return false; 25 | } 26 | 27 | if (!IsInRange(target, us)) 28 | { 29 | return false; 30 | } 31 | 32 | 33 | return CooldownIsValid(Context); 34 | } 35 | 36 | bool UConsideration::IsInRange(AActor* Target, AAIController* Us) const 37 | { 38 | if (Radius <= 0.0f) 39 | { 40 | return true; 41 | } 42 | 43 | if (Target == NULL || Us == NULL) 44 | { 45 | return false; 46 | } 47 | 48 | APawn* ourPawn = Us->GetPawn(); 49 | if (ourPawn == NULL) 50 | { 51 | return false; 52 | } 53 | 54 | FVector ourLocation = ourPawn->GetActorLocation(); 55 | FVector targetLocation = Target->GetActorLocation(); 56 | return FVector::DistSquared(targetLocation, ourLocation) <= Radius * Radius; 57 | } 58 | 59 | bool UConsideration::HasAllRequiredGameplayTags(AActor* Target, AAIController* Us) const 60 | { 61 | if (Target == NULL || Us == NULL) 62 | { 63 | return false; 64 | } 65 | 66 | // Target gameplay tags 67 | if (TargetRequiredTags.Num() > 0) 68 | { 69 | IGameplayTagAssetInterface* targetInterface = Cast(Target); 70 | if (targetInterface != NULL) 71 | { 72 | if (!targetInterface->HasAllMatchingGameplayTags(TargetRequiredTags)) 73 | { 74 | return false; 75 | } 76 | } 77 | } 78 | 79 | // Our gameplay tags 80 | if (OurRequiredTags.Num() > 0) 81 | { 82 | IGameplayTagAssetInterface* ourInterface = Cast(Us->GetPawn()); 83 | if (ourInterface != NULL) 84 | { 85 | if (!ourInterface->HasAllMatchingGameplayTags(OurRequiredTags)) 86 | { 87 | return false; 88 | } 89 | } 90 | } 91 | 92 | return true; 93 | } 94 | 95 | bool UConsideration::CooldownIsValid(const FDecisionContext& Context) const 96 | { 97 | // See if we can get away with skipping this entirely 98 | if (CooldownTime <= 0.0f && CooldownDecisions.Num() == 0) 99 | { 100 | return true; 101 | } 102 | 103 | // Check to see if we inherit the right interface 104 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 105 | if (intelligence == NULL) 106 | { 107 | return false; 108 | } 109 | IUtilityAIInterface* ourAI = Cast(intelligence->GetController()); 110 | if (ourAI == NULL) 111 | { 112 | #if WITH_EDITOR 113 | UE_LOG(LogTemp, Warning, TEXT("You can only \"properly\" use Utility AI if your Controller inherits UtilityAIInterface")); 114 | UE_LOG(LogTemp, Warning, TEXT("Make sure to add it as a valid interface to get everything working correctly!")); 115 | #endif 116 | return false; 117 | } 118 | 119 | TArray decisions = ourAI->GetRecentDecisions(); 120 | #if WITH_EDITOR 121 | if (decisions.Num() > 2) 122 | { 123 | if (decisions[1].DecisionTime < decisions[0].DecisionTime) 124 | { 125 | UE_LOG(LogTemp, Error, TEXT("Decisions are being added in the wrong order!")); 126 | UE_LOG(LogTemp, Error, TEXT("Make sure that decisions made later get placed at the end of the array.")); 127 | } 128 | } 129 | #endif 130 | 131 | // Check cooldowns 132 | if (CooldownTime > 0.0f) 133 | { 134 | if (!DecisionsAreValid(Context, decisions, CooldownTime, bInvertCooldown)) 135 | { 136 | return false; 137 | } 138 | } 139 | 140 | // Check other decisions 141 | // This happens after cooldowns since it's slower (n * m as opposed to n) 142 | for (int i = 0; i < CooldownDecisions.Num(); i++) 143 | { 144 | if (!DecisionsAreValid(Context, decisions, CooldownDecisions[i].Cooldown, CooldownDecisions[i].bInvert)) 145 | { 146 | return false; 147 | } 148 | } 149 | 150 | return true; 151 | } 152 | 153 | bool UConsideration::DecisionsAreValid(const FDecisionContext& Context, const TArray& Decisions, float Cooldown, bool bInvert) const 154 | { 155 | // Decisions are made in UTC 156 | FDateTime now = FDateTime::UtcNow(); 157 | for (int i = Decisions.Num() - 1; i <= 0; i++) 158 | { 159 | FTimespan elapsed = now - Decisions[i].DecisionTime; 160 | // All decisions are made with the earliest first 161 | // This means as soon as we exceed the cooldown period for one decision, 162 | // we've exceeded the cooldown period for all decisions 163 | if (elapsed.GetSeconds() > Cooldown) 164 | { 165 | // We've processed everything we care about without hitting 166 | // any snags, move on to the next step 167 | if (bInvert) 168 | { 169 | // We can only process this consideration in that window 170 | return false; 171 | } 172 | break; 173 | } 174 | 175 | // We're within the cooldown period for this decision 176 | if (Decisions[i].Decision == Context.Decision) 177 | { 178 | // We've made this decision recently 179 | if (bInvert) 180 | { 181 | // We can only do this decision recently in the first place! 182 | break; 183 | } 184 | else 185 | { 186 | // Too soon, Executus! 187 | return false; 188 | } 189 | } 190 | } 191 | return true; 192 | } 193 | 194 | float UConsideration::Score_Implementation(const FDecisionContext& Context) const 195 | { 196 | return 0.0f; 197 | } 198 | 199 | float UConsideration::CalculateScore(const FDecisionContext& Context) const 200 | { 201 | if (CanScore(Context)) 202 | { 203 | return Score(Context); 204 | } 205 | else 206 | { 207 | return 0.0f; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Considerations/DistanceConsideration.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "DistanceConsideration.h" 5 | 6 | UDistanceConsideration::UDistanceConsideration() 7 | { 8 | Description = "A Consideration which uses the distance between us and our target to provide a score based on a curve."; 9 | } 10 | 11 | 12 | float UDistanceConsideration::Score_Implementation(const FDecisionContext& Context) const 13 | { 14 | if (Radius <= 0.0f) 15 | { 16 | return 0.0f; 17 | } 18 | if (ResponseCurve == NULL) 19 | { 20 | return 0.0f; 21 | } 22 | AActor* target = Context.OurTarget; 23 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 24 | if (intelligence == NULL) 25 | { 26 | return 0.0f; 27 | } 28 | AAIController* us = intelligence->GetController(); 29 | if (target == NULL || us == NULL) 30 | { 31 | return 0.0f; 32 | } 33 | APawn* ourPawn = us->GetPawn(); 34 | if (ourPawn == NULL) 35 | { 36 | return 0.0f; 37 | } 38 | 39 | // All checks are valid, time for the actual function 40 | float dist = FVector::DistSquared(ourPawn->GetActorLocation(), target->GetActorLocation()); 41 | float sqrRadius = Radius * Radius; 42 | // Return the actual value from the curve 43 | return ResponseCurve->GetFloatValue(FMath::Clamp(dist / sqrRadius, 0.0f, 1.0f)); 44 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Considerations/EnemyConsideration.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "EnemyConsideration.h" 5 | #include "UtilityIntelligence.h" 6 | 7 | UEnemyConsideration::UEnemyConsideration() 8 | { 9 | Description = "A consideration which takes into account how many enemies we know about."; 10 | MaxEnemies = 4; 11 | } 12 | 13 | float UEnemyConsideration::Score_Implementation(const FDecisionContext & Context) const 14 | { 15 | if (ResponseCurve == NULL) 16 | { 17 | return 0.0f; 18 | } 19 | else 20 | { 21 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 22 | if (intelligence == NULL) 23 | { 24 | return 0.0f; 25 | } 26 | float enemyCount = (float)intelligence->GetPossibleTargets().Num(); 27 | return ResponseCurve->GetFloatValue(enemyCount / (float)MaxEnemies); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Controllers/UtilityAIInterface.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "UtilityAIInterface.h" 4 | 5 | TArray IUtilityAIInterface::GetRecentDecisions_Implementation() const 6 | { 7 | return TArray(); 8 | } 9 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Controllers/UtilityIntelligence.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "UtilityIntelligence.h" 5 | #include "AISystem.h" 6 | 7 | // Sets default values for this component's properties 8 | UUtilityIntelligence::UUtilityIntelligence() 9 | { 10 | PrimaryComponentTick.bCanEverTick = false; 11 | SkillTick = 0.05f; 12 | DecisionTick = 0.125f; 13 | 14 | ContextTarget = NULL; 15 | 16 | AcceptableRadius = GET_AI_CONFIG_VAR(AcceptanceRadius); 17 | bReachTestIncludesGoalRadius = bReachTestIncludesAgentRadius = GET_AI_CONFIG_VAR(bFinishMoveOnGoalOverlap); 18 | bAllowStrafe = GET_AI_CONFIG_VAR(bAllowStrafing); 19 | bAllowPartialPath = GET_AI_CONFIG_VAR(bAcceptPartialPaths); 20 | bTrackMovingGoal = true; 21 | bProjectGoalLocation = true; 22 | bUsePathfinding = true; 23 | } 24 | 25 | 26 | void UUtilityIntelligence::TickSkills() 27 | { 28 | // This ticks anything which should be happening WHILE another action happens 29 | // For example, this will handle shooting while we run away 30 | FDecisionContext startContext = GetDecisionContext(); 31 | float bestScore = -1.0f; 32 | FDecisionContext bestContext = startContext; 33 | for (const USkillSet* skillSet : SkillSets) 34 | { 35 | skillSet->FindBestSkill(startContext, bestContext, bestScore); 36 | } 37 | 38 | if (bestScore <= 0.0f) 39 | { 40 | // Score too low to make a decision 41 | return; 42 | } 43 | 44 | // Make the actual decision 45 | MakeDecision(bestContext); 46 | } 47 | 48 | void UUtilityIntelligence::TickDecisions() 49 | { 50 | // This handles strategic planning and long-term decisions 51 | // For example, this plans out a good spot to run away to and starts running there 52 | FDecisionContext startContext = GetDecisionContext(); 53 | float bestScore = -1.0f; 54 | FDecisionContext bestContext = startContext; 55 | for (const UDecisionMaker* decsionMaker : Decisions) 56 | { 57 | // Iterate over every DecisionMaker 58 | // These can be added or removed at will, so if the AI enters a tavern 59 | // a "tavern" DecisionMaker can be added to the list, allowing for special 60 | // tavern behaviors 61 | decsionMaker->FindBestDecision(startContext, bestContext, bestScore); 62 | } 63 | 64 | if (bestScore <= 0.0f) 65 | { 66 | // Score too low to make a decision 67 | return; 68 | } 69 | 70 | // Make the actual decision 71 | MakeDecision(bestContext); 72 | } 73 | 74 | FDecisionContext UUtilityIntelligence::GetDecisionContext() 75 | { 76 | FDecisionContext context; 77 | context.OurIntelligence = this; 78 | context.Decision = NULL; 79 | context.OurTarget = ContextTarget; 80 | context.AIBlackboard = AIBlackboard; 81 | return context; 82 | } 83 | 84 | void UUtilityIntelligence::EndPlay(EEndPlayReason::Type EndPlayReason) 85 | { 86 | Super::EndPlay(EndPlayReason); 87 | StopAI(); 88 | } 89 | 90 | TArray UUtilityIntelligence::GetRecentDecisions_Implementation() const 91 | { 92 | return RecentDecisions; 93 | } 94 | 95 | void UUtilityIntelligence::MakeDecision(const FDecisionContext& Context) 96 | { 97 | if (Context.Decision == NULL) 98 | { 99 | return; 100 | } 101 | 102 | UE_LOG(LogTemp, Log, TEXT("Running decision %s."), *Context.Decision->GetName()); 103 | RecentDecisions.Add(Context.Decision->RunDecision(Context)); 104 | } 105 | 106 | void UUtilityIntelligence::StartAI(AAIController* Controller, UBlackboardComponent* Blackboard) 107 | { 108 | OurController = Controller; 109 | AIBlackboard = Blackboard; 110 | 111 | FTimerManager& worldManager = GetWorld()->GetTimerManager(); 112 | worldManager.SetTimer(DecisionTickTimer, this, &UUtilityIntelligence::TickDecisions, DecisionTick, true, 0.0f); 113 | worldManager.SetTimer(SkillTickTimer, this, &UUtilityIntelligence::TickSkills, SkillTick, true, 0.0f); 114 | 115 | } 116 | 117 | void UUtilityIntelligence::StopAI() 118 | { 119 | GetWorld()->GetTimerManager().ClearAllTimersForObject(this); 120 | } 121 | 122 | void UUtilityIntelligence::UpdateContextTarget(AActor* NewTarget) 123 | { 124 | ContextTarget = NewTarget; 125 | } 126 | 127 | void UUtilityIntelligence::AddNewPossibleTarget(APawn* NewTarget) 128 | { 129 | ValidTargets.Add(NewTarget); 130 | } 131 | 132 | void UUtilityIntelligence::RemovePossibleTarget(APawn* Target) 133 | { 134 | ValidTargets.Remove(Target); 135 | } 136 | 137 | TSet UUtilityIntelligence::GetPossibleTargets() const 138 | { 139 | return ValidTargets; 140 | } 141 | 142 | void UUtilityIntelligence::AddNewAlly(APawn* NewAlly) 143 | { 144 | ValidAllies.Add(NewAlly); 145 | } 146 | 147 | void UUtilityIntelligence::RemoveAlly(APawn* Ally) 148 | { 149 | ValidAllies.Remove(Ally); 150 | } 151 | 152 | TSet UUtilityIntelligence::GetAllies() const 153 | { 154 | return ValidAllies; 155 | } 156 | 157 | void UUtilityIntelligence::AddAreaOfInterest(const FVector& InterestingPoint) 158 | { 159 | AreasOfInterest.Add(InterestingPoint); 160 | } 161 | 162 | void UUtilityIntelligence::RemoveAreaOfInterest(const FVector& InterestingPoint) 163 | { 164 | AreasOfInterest.Remove(InterestingPoint); 165 | } 166 | 167 | AAIController* UUtilityIntelligence::GetController() const 168 | { 169 | return OurController; 170 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/DSE/DecisionScoreEvaluator.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "DecisionScoreEvaluator.h" 4 | 5 | UDecisionScoreEvaluator::UDecisionScoreEvaluator() 6 | { 7 | Description = "A generic decision score evaluator."; 8 | Weight = 1.0f; 9 | Priority = EEvaluatorPriority::Ambient; 10 | } 11 | 12 | float UDecisionScoreEvaluator::GetMomentum_Implementation(const FDecisionContext& Context) const 13 | { 14 | // By default, we don't give any bonus nor penalty for anything being "stale." 15 | return 0.0f; 16 | } 17 | 18 | float UDecisionScoreEvaluator::GetCompensationFactor(float Score, float Bonus) const 19 | { 20 | // This gets run to offset the fact that an evaluator with many considerations 21 | // gets effectively "penalized" over time due to the high number of considerations. 22 | // 23 | // Since the score is multiplicative and clamped between 0 and 1, a DSE with 25 24 | // considerations that all give a score of 0.9 all get multiplied together, 25 | // effectively bringing down its overall score to 0.072. Meanwhile, a DSE with 1 26 | // consideration that gives a score of 0.1 would be selected. 27 | // 28 | // The DSE with a lot of considerations got a higher score overall and is (likely) 29 | // more "fit," but because everything was multiplicative it lowered the score 30 | // significantly. This corrects for that. 31 | 32 | if (Considerations.Num() == 0 || Score == 0.0f) 33 | { 34 | return Score; 35 | } 36 | 37 | // Using the above example of 25 considerations, and a score of 0.072, 38 | // assuming that the bonus is 1.0: 39 | // Modification factor = 0.96 40 | float modificationFactor = Bonus - (Bonus / (float)Considerations.Num()); 41 | // Make-up value = 0.928 * 0.96 = 0.891 42 | float makeUpValue = (Bonus - Score) * modificationFactor; 43 | // Final score = 0.136 -- an improvement! 44 | // It's not perfect, but it's at least been slightly compensated for. 45 | return Score + (makeUpValue * Score); 46 | } 47 | 48 | float UDecisionScoreEvaluator::GetWeight() const 49 | { 50 | return (float)Priority * Weight; 51 | } 52 | 53 | float UDecisionScoreEvaluator::CalculateScore_Implementation(FDecisionContext& Context, float MinToBeat, float Bonus) const 54 | { 55 | float startingBonus = GetWeight() + GetMomentum(Context) + Bonus; 56 | float score = startingBonus; 57 | for (const UConsideration* consideration : Considerations) 58 | { 59 | score *= FMath::Clamp(consideration->CalculateScore(Context), 0.0f, 1.0f); 60 | if (score == 0.0f || score < MinToBeat) 61 | { 62 | break; 63 | } 64 | } 65 | return GetCompensationFactor(score, startingBonus); 66 | } 67 | 68 | void UDecisionScoreEvaluator::FindBestContext(TArray& Contexts, const UDecisionScoreEvaluator* Evaluator, FDecisionContext& OutContext, float& OutBestScore) 69 | { 70 | if (Evaluator == NULL) 71 | { 72 | return; 73 | } 74 | 75 | for (FDecisionContext& context : Contexts) 76 | { 77 | float score = Evaluator->CalculateScore(context, OutBestScore); 78 | if (score > OutBestScore) 79 | { 80 | OutBestScore = score; 81 | OutContext = context; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/DSE/NonSkillDecisionScoreEvaluator.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "NonSkillDecisionScoreEvaluator.h" 4 | 5 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/DSE/SkillDecisionScoreEvaluator.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "SkillDecisionScoreEvaluator.h" 4 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/CombatDecision.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "CombatDecision.h" 5 | #include "UtilityIntelligence.h" 6 | #include "UtilityAIHelpers.h" 7 | 8 | UCombatDecision::UCombatDecision() 9 | { 10 | MaxCombatRange = 5000.0f; 11 | } 12 | 13 | APawn* UCombatDecision::FindTarget(const FDecisionContext& Context) const 14 | { 15 | UUtilityIntelligence* intelligence = Context.OurIntelligence; 16 | TArray targets; 17 | if (intelligence == NULL) 18 | { 19 | return NULL; 20 | } 21 | 22 | targets = intelligence->GetPossibleTargets().Array(); 23 | if (targets.Num() == 0) 24 | { 25 | return NULL; 26 | } 27 | 28 | TArray contexts = TArray(); 29 | contexts.SetNum(targets.Num()); 30 | for (int i = 0; i < targets.Num(); i++) 31 | { 32 | contexts[i] = FDecisionContext(Context); 33 | contexts[i].OurTarget = targets[i]; 34 | } 35 | 36 | FDecisionContext bestContext = FDecisionContext(); 37 | float bestScore = 0.0f; 38 | UDecisionScoreEvaluator::FindBestContext(contexts, FindTargetDSE, bestContext, bestScore); 39 | AActor* target = bestContext.OurTarget; 40 | if (target == NULL) 41 | { 42 | UE_LOG(LogTemp, Warning, TEXT("Found no target from %d contexts and %d targets!"), contexts.Num(), targets.Num()); 43 | return NULL; 44 | } 45 | 46 | // Inform the intelligence about our target 47 | intelligence->UpdateContextTarget(target); 48 | return Cast(target); 49 | } 50 | 51 | TEnumAsByte UCombatDecision::MoveToTarget(const FDecisionContext& Context, APawn* Target) const 52 | { 53 | if (Target == NULL) 54 | { 55 | return EBTNodeResult::Failed; 56 | } 57 | return UUtilityAIHelpers::MoveToActor(Context.OurIntelligence, MaxCombatRange, Target); 58 | } 59 | 60 | 61 | FMadeDecision UCombatDecision::RunDecision_Implementation(const FDecisionContext& Context) const 62 | { 63 | FMadeDecision decision = Super::RunDecision_Implementation(Context); 64 | APawn* target = FindTarget(Context); 65 | decision.DecisionStatus = MoveToTarget(Context, target); 66 | return decision; 67 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/Decision.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "Decision.h" 5 | 6 | float UDecision::CalculateScore(FDecisionContext& Context, float MinToBeat, float Bonus) const 7 | { 8 | if (DecisionScoreEvaluator == NULL) 9 | { 10 | return 0.0f; 11 | } 12 | else 13 | { 14 | return DecisionScoreEvaluator->CalculateScore(Context, MinToBeat, Bonus); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/DecisionBase.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "DecisionBase.h" 4 | 5 | FMadeDecision UDecisionBase::RunDecision_Implementation(const FDecisionContext& Decision) const 6 | { 7 | FMadeDecision decision; 8 | decision.Decision = this; 9 | // Make the decision in UTC to avoid daylight savings time 10 | decision.DecisionTime = FDateTime::UtcNow(); 11 | decision.DecisionStatus = EBTNodeResult::Succeeded; 12 | return decision; 13 | } 14 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/DecisionMaker.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "DecisionMaker.h" 4 | 5 | UDecisionMaker::UDecisionMaker() 6 | { 7 | Bonus = 0.0f; 8 | } 9 | 10 | UDecision* UDecisionMaker::FindBestDecision(const FDecisionContext& StartContext, FDecisionContext& OutContext, float& OutBestScore) const 11 | { 12 | UDecision* best = NULL; 13 | for (UDecision* decision : Decisions) 14 | { 15 | if (decision != NULL) 16 | { 17 | FDecisionContext context = FDecisionContext(StartContext); 18 | context.Decision = decision; 19 | float score = decision->CalculateScore(context, OutBestScore, Bonus); 20 | if (score > OutBestScore) 21 | { 22 | best = decision; 23 | OutBestScore = score; 24 | OutContext = context; 25 | } 26 | } 27 | } 28 | return best; 29 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/Skills/Skill.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "Skill.h" 5 | 6 | -------------------------------------------------------------------------------- /Source/UtilityAI/Private/Decisions/Skills/SkillSet.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "SkillSet.h" 4 | 5 | USkill* USkillSet::FindBestSkill(const FDecisionContext& StartContext, FDecisionContext& OutContext, float& OutBestScore) const 6 | { 7 | USkill* best = NULL; 8 | for (auto& kvp : Skills) 9 | { 10 | USkill* skill = kvp.Key; 11 | USkillDecisionScoreEvaluator* dse = kvp.Value; 12 | if (dse != NULL) 13 | { 14 | FDecisionContext context = FDecisionContext(StartContext); 15 | context.Decision = skill; 16 | 17 | float score = dse->CalculateScore(context, OutBestScore); 18 | if (score > OutBestScore) 19 | { 20 | best = skill; 21 | OutBestScore = score; 22 | OutContext = context; 23 | } 24 | } 25 | } 26 | return best; 27 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Private/UtilityAI.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "UtilityAI.h" 4 | 5 | #define LOCTEXT_NAMESPACE "FUtilityAIModule" 6 | 7 | void FUtilityAIModule::StartupModule() 8 | { 9 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 10 | } 11 | 12 | void FUtilityAIModule::ShutdownModule() 13 | { 14 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 15 | // we call this function before unloading the module. 16 | } 17 | 18 | #undef LOCTEXT_NAMESPACE 19 | 20 | IMPLEMENT_MODULE(FUtilityAIModule, UtilityAI) -------------------------------------------------------------------------------- /Source/UtilityAI/Private/UtilityAIHelpers.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #include "UtilityAIHelpers.h" 5 | #include "AITypes.h" 6 | #include "NavFilters/NavigationQueryFilter.h" 7 | #include "Navigation/PathFollowingComponent.h" 8 | 9 | TEnumAsByte UUtilityAIHelpers::PerformMoveTask(UUtilityIntelligence* Intelligence, FBTMoveToTaskMemory& OutMemory, float MaxDistance, AActor* TargetActor, const FVector& TargetLocation) 10 | { 11 | AAIController* myController = Intelligence->GetController(); 12 | 13 | TEnumAsByte nodeResult = EBTNodeResult::Failed; 14 | if (myController) 15 | { 16 | FAIMoveRequest moveReq; 17 | moveReq.SetAcceptanceRadius(Intelligence->AcceptableRadius + MaxDistance); 18 | moveReq.SetNavigationFilter(*Intelligence->FilterClass ? Intelligence->FilterClass : myController->GetDefaultNavigationFilterClass()); 19 | moveReq.SetCanStrafe(Intelligence->bAllowStrafe); 20 | moveReq.SetAllowPartialPath(Intelligence->bAllowPartialPath); 21 | moveReq.SetReachTestIncludesAgentRadius(Intelligence->bReachTestIncludesAgentRadius); 22 | moveReq.SetReachTestIncludesGoalRadius(Intelligence->bReachTestIncludesGoalRadius); 23 | moveReq.SetProjectGoalLocation(Intelligence->bProjectGoalLocation); 24 | moveReq.SetUsePathfinding(Intelligence->bUsePathfinding); 25 | 26 | if (TargetActor != NULL) 27 | { 28 | if (Intelligence->bTrackMovingGoal) 29 | { 30 | moveReq.SetGoalActor(TargetActor); 31 | } 32 | else 33 | { 34 | const FVector goalLocation = TargetActor->GetActorLocation(); 35 | moveReq.SetGoalLocation(goalLocation); 36 | OutMemory.PreviousGoalLocation = goalLocation; 37 | } 38 | } 39 | else 40 | { 41 | moveReq.SetGoalLocation(TargetLocation); 42 | OutMemory.PreviousGoalLocation = TargetLocation; 43 | } 44 | 45 | if (moveReq.IsValid()) 46 | { 47 | FPathFollowingRequestResult requestResult = myController->MoveTo(moveReq); 48 | switch (requestResult.Code) 49 | { 50 | case EPathFollowingRequestResult::RequestSuccessful: 51 | OutMemory.MoveRequestID = requestResult.MoveId; 52 | nodeResult = EBTNodeResult::InProgress; 53 | break; 54 | case EPathFollowingRequestResult::AlreadyAtGoal: 55 | nodeResult = EBTNodeResult::Succeeded; 56 | break; 57 | case EPathFollowingRequestResult::Failed: 58 | nodeResult = EBTNodeResult::Failed; 59 | break; 60 | default: 61 | checkNoEntry(); 62 | break; 63 | } 64 | } 65 | } 66 | 67 | return nodeResult; 68 | } 69 | 70 | TEnumAsByte UUtilityAIHelpers::MoveTo(UUtilityIntelligence* Intelligence, FBTMoveToTaskMemory& OutMemory, float MaxDistance, AActor* MoveActor, FVector MoveLocation) 71 | { 72 | if (Intelligence == NULL) 73 | { 74 | return EBTNodeResult::Failed; 75 | } 76 | 77 | TEnumAsByte nodeResult = EBTNodeResult::InProgress; 78 | 79 | OutMemory.PreviousGoalLocation = FAISystem::InvalidLocation; 80 | OutMemory.MoveRequestID = FAIRequestID::InvalidRequest; 81 | 82 | AAIController* myController = Intelligence->GetController(); 83 | OutMemory.bWaitingForPath = myController->ShouldPostponePathUpdates(); 84 | if (!OutMemory.bWaitingForPath) 85 | { 86 | nodeResult = PerformMoveTask(Intelligence, OutMemory, MaxDistance, MoveActor, MoveLocation); 87 | } 88 | 89 | return nodeResult; 90 | } 91 | 92 | TEnumAsByte UUtilityAIHelpers::MoveToPoint(UUtilityIntelligence* Intelligence, float MaxDistance, const FVector& Point) 93 | { 94 | FBTMoveToTaskMemory memory = FBTMoveToTaskMemory(); 95 | return MoveTo(Intelligence, memory, MaxDistance, NULL, Point); 96 | } 97 | 98 | TEnumAsByte UUtilityAIHelpers::MoveToActor(UUtilityIntelligence* Intelligence, float MaxDistance, AActor* Actor) 99 | { 100 | FBTMoveToTaskMemory memory = FBTMoveToTaskMemory(); 101 | return MoveTo(Intelligence, memory, MaxDistance, Actor); 102 | } -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Considerations/AllyConsideration.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Considerations/Consideration.h" 7 | #include "AllyConsideration.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class UTILITYAI_API UAllyConsideration : public UConsideration 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | // How many allies it takes to send a value of 1 to the curve. 19 | UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1")) 20 | uint8 MaxAllies; 21 | 22 | public: 23 | UAllyConsideration(); 24 | 25 | protected: 26 | virtual float Score_Implementation(const FDecisionContext& Context) const override; 27 | }; 28 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Considerations/BlackboardConsideration.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Considerations/Consideration.h" 7 | 8 | #include "BehaviorTree/BehaviorTreeTypes.h" 9 | 10 | #include "BlackboardConsideration.generated.h" 11 | 12 | /** 13 | * Uses a key from a Blackboard to score a consideration. 14 | */ 15 | UCLASS() 16 | class UTILITYAI_API UBlackboardConsideration : public UConsideration 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Blackboard") 22 | FBlackboardKeySelector BlackboardKey; 23 | 24 | public: 25 | UBlackboardConsideration(); 26 | 27 | protected: 28 | virtual float Score_Implementation(const FDecisionContext& Context) const override; 29 | }; 30 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Considerations/Consideration.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "UnrealString.h" 5 | #include "AIController.h" 6 | #include "Curves/CurveFloat.h" 7 | #include "Engine/DataAsset.h" 8 | #include "GameplayTagContainer.h" 9 | 10 | #include "DecisionBase.h" 11 | 12 | #include "Consideration.generated.h" 13 | 14 | 15 | /* 16 | * A context that checks if we have make a decision recently. 17 | */ 18 | USTRUCT(BlueprintType) 19 | struct UTILITYAI_API FCooldownDecisionContext 20 | { 21 | GENERATED_BODY() 22 | 23 | public: 24 | // The Decision that the Consideration should check. 25 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 26 | const UDecisionBase* Decision; 27 | // How long we should wait until after the given action before 28 | // this context can be valid again. 29 | UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (MinValue = "0.1")) 30 | float Cooldown; 31 | // If this is set to true, then this context is only valid during 32 | // the given time period (instead of only valid once the time 33 | // period has elapsed). 34 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 35 | bool bInvert; 36 | }; 37 | 38 | /** 39 | * A Consideration is the base class governing the choices that our AI makes. 40 | * Examples of possible considerations could be "how much health do I have?" 41 | * It takes context about what is going on inside the game and uses that to 42 | * provide a score, which gets used as a multiplier when making choices. 43 | * This score is normalized between 0 and 1. 44 | */ 45 | UCLASS(Abstract, Blueprintable) 46 | class UTILITYAI_API UConsideration : public UPrimaryDataAsset 47 | { 48 | GENERATED_BODY() 49 | 50 | public: 51 | // A simple description of what this consideration does. 52 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 53 | FString Description; 54 | // A curve used in our scoring function to dictate behavior. 55 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 56 | UCurveFloat* ResponseCurve; 57 | 58 | // Only run this consideration if the target is within a certain radius of our controller. 59 | // A value of <= 0 disables the radius check. 60 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 61 | float Radius; 62 | 63 | // How long before this Consideration can run again after it succeeds. 64 | // A value of <= 0 disables cooldown check. 65 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 66 | float CooldownTime; 67 | // Whether we should invert the cooldown timer. 68 | // Note that if you enable this, you should make sure that any Decisions 69 | // associated with this Consideration can be called somewhere else. 70 | // Otherwise, they'll never get called! 71 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 72 | bool bInvertCooldown; 73 | 74 | // All tags that need to be on the pawn associated with the controller which is 75 | // calling the score function for it to consider returning a value. 76 | // Skipped if the pawn doesn't implement IGameplayTagAssetInterface. 77 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 78 | FGameplayTagContainer OurRequiredTags; 79 | 80 | // All tags that need to be on the controller calling the score function for it to 81 | // consider returning a value. 82 | // Skipped if the target doesn't implement IGameplayTagAssetInterface. 83 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 84 | FGameplayTagContainer TargetRequiredTags; 85 | 86 | // We check to see if the pawn has performed any of these decisions within the specified 87 | // time period. If we've performed one of these decisions recently, then we skip this 88 | // consideration. An empty array means we won't check anything. 89 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 90 | TArray CooldownDecisions; 91 | 92 | public: 93 | UConsideration(); 94 | 95 | private: 96 | bool CanScore(const FDecisionContext& Context) const; 97 | bool IsInRange(AActor* Target, AAIController* Us) const; 98 | bool HasAllRequiredGameplayTags(AActor* target, AAIController* us) const; 99 | bool CooldownIsValid(const FDecisionContext& Context) const; 100 | bool DecisionsAreValid(const FDecisionContext& Context, const TArray& Decisions, float Cooldown, bool bInvert) const; 101 | 102 | protected: 103 | virtual float Score_Implementation(const FDecisionContext& Context) const; 104 | 105 | // The heart of the Consideration. 106 | // Returns a multiplier which gives data about how likely it is that an AI should 107 | // choose this action. 108 | UFUNCTION(BlueprintNativeEvent, BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Consideration") 109 | float Score(const FDecisionContext& Context) const; 110 | 111 | public: 112 | // Calculates a score for this Consideration given a context. 113 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Consideration") 114 | float CalculateScore(const FDecisionContext& Context) const; 115 | }; 116 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Considerations/DistanceConsideration.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Consideration.h" 7 | #include "DistanceConsideration.generated.h" 8 | 9 | /** 10 | * This uses the provided curve to provide the score for the overall consideration. 11 | * The curve will be extrapolated to match the bounds of the radius, if one is provided. 12 | */ 13 | UCLASS() 14 | class UTILITYAI_API UDistanceConsideration : public UConsideration 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | UDistanceConsideration(); 20 | 21 | protected: 22 | virtual float Score_Implementation(const FDecisionContext& Context) const override; 23 | }; 24 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Considerations/EnemyConsideration.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Considerations/Consideration.h" 7 | #include "EnemyConsideration.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class UTILITYAI_API UEnemyConsideration : public UConsideration 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | // How many enemies it takes to send a value of 1 to the curve. 19 | UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1")) 20 | uint8 MaxEnemies; 21 | 22 | public: 23 | UEnemyConsideration(); 24 | 25 | protected: 26 | virtual float Score_Implementation(const FDecisionContext& Context) const override; 27 | }; 28 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Controllers/UtilityAIInterface.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/Interface.h" 7 | 8 | #include "DecisionBase.h" 9 | 10 | #include "UtilityAIInterface.generated.h" 11 | 12 | 13 | // This class does not need to be modified. 14 | UINTERFACE(MinimalAPI) 15 | class UUtilityAIInterface : public UInterface 16 | { 17 | GENERATED_BODY() 18 | }; 19 | 20 | /** 21 | * 22 | */ 23 | class UTILITYAI_API IUtilityAIInterface 24 | { 25 | GENERATED_BODY() 26 | 27 | protected: 28 | virtual TArray GetRecentDecisions_Implementation() const; 29 | 30 | public: 31 | // Gets all decisions that we have made recently. 32 | // IMPORTANT: Decisions which we have made most recently should be at the END of the array. 33 | UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "AI|Utility AI|Consideration") 34 | TArray GetRecentDecisions() const; 35 | }; 36 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Controllers/UtilityIntelligence.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Components/ActorComponent.h" 7 | #include "NavFilters/NavigationQueryFilter.h" 8 | 9 | #include "UtilityAIInterface.h" 10 | #include "SkillSet.h" 11 | #include "DecisionMaker.h" 12 | 13 | #include "UtilityIntelligence.generated.h" 14 | 15 | 16 | UCLASS(Blueprintable, meta=(BlueprintSpawnableComponent)) 17 | class UTILITYAI_API UUtilityIntelligence : public UActorComponent, public IUtilityAIInterface 18 | { 19 | GENERATED_BODY() 20 | 21 | private: 22 | UPROPERTY() 23 | FTimerHandle SkillTickTimer; 24 | UPROPERTY() 25 | FTimerHandle DecisionTickTimer; 26 | 27 | UPROPERTY(Transient) 28 | UBlackboardComponent* AIBlackboard; 29 | 30 | UPROPERTY(Transient) 31 | AActor* ContextTarget; 32 | 33 | protected: 34 | UPROPERTY(BlueprintReadOnly) 35 | TArray RecentDecisions; 36 | UPROPERTY(BlueprintReadOnly) 37 | AAIController* OurController; 38 | UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"), Category = "Decisions") 39 | float SkillTick; 40 | UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, meta = (AllowPrivateAccess = "true"), Category = "Decisions") 41 | float DecisionTick; 42 | 43 | UPROPERTY(EditInstanceOnly, meta = (AllowPrivateAccess = "true")) 44 | TSet ValidTargets; 45 | UPROPERTY(EditInstanceOnly, meta = (AllowPrivateAccess = "true")) 46 | TSet ValidAllies; 47 | UPROPERTY(EditInstanceOnly, meta = (AllowPrivateAccess = "true")) 48 | TSet AreasOfInterest; 49 | 50 | public: 51 | // Fixed distance added to threshold between AI and goal location in destination reach test 52 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding") 53 | float AcceptableRadius; 54 | // "None" will result in default filter being used 55 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding") 56 | TSubclassOf FilterClass; 57 | // Should we allow strafing towards our goal? 58 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding") 59 | uint32 bAllowStrafe : 1; 60 | // If set, use incomplete path when goal can't be reached 61 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding") 62 | uint32 bAllowPartialPath : 1; 63 | // If set, radius of AI's capsule will be added to threshold between AI and goal location in destination reach test 64 | UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Pathfinding") 65 | uint32 bReachTestIncludesAgentRadius : 1; 66 | // If set, radius of goal's capsule will be added to threshold between AI and goal location in destination reach test 67 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Pathfinding") 68 | uint32 bReachTestIncludesGoalRadius : 1; 69 | // If set, goal location will be projected on navigation data (navmesh) before using 70 | UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Pathfinding") 71 | uint32 bProjectGoalLocation : 1; 72 | // If set, move will use pathfinding 73 | UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Pathfinding") 74 | uint32 bUsePathfinding : 1; 75 | // If set, path to goal actor will update itself when actor moves 76 | UPROPERTY(EditAnywhere, BlueprintReadWrite, AdvancedDisplay, Category = "Pathfinding") 77 | uint32 bTrackMovingGoal : 1; 78 | 79 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decisions") 80 | TSet SkillSets; 81 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decisions") 82 | TSet Decisions; 83 | 84 | public: 85 | UUtilityIntelligence(); 86 | 87 | private: 88 | UFUNCTION() 89 | void TickSkills(); 90 | UFUNCTION() 91 | void TickDecisions(); 92 | 93 | FDecisionContext GetDecisionContext(); 94 | 95 | protected: 96 | virtual void EndPlay(EEndPlayReason::Type EndPlayReason) override; 97 | virtual TArray GetRecentDecisions_Implementation() const override; 98 | 99 | public: 100 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence") 101 | void MakeDecision(const FDecisionContext& Context); 102 | 103 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence") 104 | void StartAI(AAIController* Controller, UBlackboardComponent* Blackboard); 105 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence") 106 | void StopAI(); 107 | 108 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 109 | void UpdateContextTarget(AActor* NewTarget); 110 | 111 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 112 | void AddNewPossibleTarget(APawn* NewTarget); 113 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 114 | void RemovePossibleTarget(APawn* Target); 115 | 116 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 117 | TSet GetPossibleTargets() const; 118 | 119 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 120 | void AddNewAlly(APawn* NewAlly); 121 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 122 | void RemoveAlly(APawn* Ally); 123 | 124 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 125 | TSet GetAllies() const; 126 | 127 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 128 | void AddAreaOfInterest(const FVector& InterestingPoint); 129 | UFUNCTION(BlueprintCallable, Category = "AI|Utility AI|Intelligence|Context") 130 | void RemoveAreaOfInterest(const FVector& InterestingPoint); 131 | 132 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Intelligence") 133 | AAIController* GetController() const; 134 | }; 135 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/DSE/DecisionScoreEvaluator.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Engine/DataAsset.h" 7 | 8 | #include "Considerations/Consideration.h" 9 | 10 | #include "DecisionScoreEvaluator.generated.h" 11 | 12 | UENUM(BlueprintType) 13 | enum class EEvaluatorPriority : uint8 14 | { 15 | // Emotes, patrolling, etc. 16 | Ambient = 0, 17 | // Chasing after an enemy, taking cover 18 | Tactical = 10, 19 | // Firing a weapon, throwing a grenade 20 | Skill = 20, 21 | // Probably should take action soon, we're pretty concerned 22 | MinorEmergency = 30, 23 | // Going to die if we don't take action ASAP 24 | MajorEmergency = 40 25 | }; 26 | 27 | /** 28 | * This takes a list of considerations and outputs a total weight for all of them. 29 | * Essentially, it evaluates a score which then gets used to select a decision. 30 | * Each decision should have a DecisionScoreEvaluator mapped to it, and when that 31 | * DecisionScoreEvaluator "wins," the decision gets executed. 32 | */ 33 | UCLASS(Abstract, Blueprintable) 34 | class UTILITYAI_API UDecisionScoreEvaluator : public UPrimaryDataAsset 35 | { 36 | GENERATED_BODY() 37 | public: 38 | // A simple description of what this evaluator does. 39 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 40 | FString Description; 41 | 42 | // A list of all considerations that this evaluator takes into account. 43 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 44 | TArray Considerations; 45 | 46 | // A weight assigned to this DSE. 47 | // Higher weights will inflate the score. 48 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 49 | float Weight; 50 | 51 | // What priority this DSE has. 52 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 53 | EEvaluatorPriority Priority; 54 | public: 55 | UDecisionScoreEvaluator(); 56 | 57 | protected: 58 | virtual float GetMomentum_Implementation(const FDecisionContext& Context) const; 59 | 60 | // A compensation factor that should be applied to make a DSE more "elastic" when there are 61 | // many considerations to make. 62 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score|Weight") 63 | virtual float GetCompensationFactor(float Score, float Bonus = 1.0f) const; 64 | // A weight given to this DSE, based on the assigned weight in the properties window and 65 | // the assigned priority. 66 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score|Weight") 67 | virtual float GetWeight() const; 68 | // How likely are we to change our mind? 69 | // This can make the AI more or less likely to switch once committed. 70 | UFUNCTION(BlueprintNativeEvent, BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score|Weight") 71 | float GetMomentum(const FDecisionContext& Context) const; 72 | 73 | virtual float CalculateScore_Implementation(FDecisionContext& Context, float MinToBeat, float Bonus) const; 74 | 75 | public: 76 | // The scoring function for this DSE. 77 | // Note that while each Consideration's score might be between 0 and 1, this can be any number. 78 | UFUNCTION(BlueprintCallable, BlueprintPure, BlueprintNativeEvent, Category = "AI|Utility AI|Score") 79 | float CalculateScore(UPARAM(ref) FDecisionContext& Context, float MinToBeat, float Bonus = 0.0f) const; 80 | 81 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI") 82 | static void FindBestContext(TArray& Contexts, const UDecisionScoreEvaluator* Evaluator, FDecisionContext& OutContext, float& OutBestScore); 83 | }; 84 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/DSE/NonSkillDecisionScoreEvaluator.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | #include "DecisionScoreEvaluator.h" 8 | 9 | #include "NonSkillDecisionScoreEvaluator.generated.h" 10 | 11 | /** 12 | * This is a DSE designed to be used for decisions which are *not* skills. 13 | * This runs in parallel to the Skill DSE to determine AI actions like "move to target" 14 | * and whatnot -- things which happen in the "background," as it were. 15 | */ 16 | UCLASS() 17 | class UTILITYAI_API UNonSkillDecisionScoreEvaluator : public UDecisionScoreEvaluator 18 | { 19 | GENERATED_BODY() 20 | }; 21 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/DSE/SkillDecisionScoreEvaluator.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | #include "DecisionScoreEvaluator.h" 8 | 9 | #include "SkillDecisionScoreEvaluator.generated.h" 10 | 11 | /** 12 | * This type of DecisionScoreEvaluator is meant to decide things about skills. 13 | * Skills are special actions that a character can take -- "shoot gun," "swing sword," etc. 14 | * They are *not* actions like "move to target" -- since only 1 skill will be executed at a 15 | * time, you should use a Non-Skill DSE. 16 | * 17 | * Additionally, this type of DSE isn't inherently associated with 1 particular skill. 18 | * Instead, it can be a set of related skills -- for example, 4 different melee attack "variants." 19 | */ 20 | UCLASS() 21 | class UTILITYAI_API USkillDecisionScoreEvaluator : public UDecisionScoreEvaluator 22 | { 23 | GENERATED_BODY() 24 | public: 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/CombatDecision.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Decisions/Decision.h" 7 | #include "CombatDecision.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class UTILITYAI_API UCombatDecision : public UDecision 14 | { 15 | GENERATED_BODY() 16 | public: 17 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 18 | const UNonSkillDecisionScoreEvaluator* FindTargetDSE; 19 | 20 | // How close we need to be to our target. 21 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 22 | float MaxCombatRange; 23 | 24 | public: 25 | UCombatDecision(); 26 | 27 | protected: 28 | virtual APawn* FindTarget(const FDecisionContext& Context) const; 29 | virtual TEnumAsByte MoveToTarget(const FDecisionContext& Context, APawn* Target) const; 30 | virtual FMadeDecision RunDecision_Implementation(const FDecisionContext& Context) const override; 31 | }; 32 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/Decision.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | #include "DecisionBase.h" 8 | #include "NonSkillDecisionScoreEvaluator.h" 9 | 10 | #include "Decision.generated.h" 11 | 12 | /** 13 | * 14 | */ 15 | UCLASS(Abstract) 16 | class UTILITYAI_API UDecision : public UDecisionBase 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 22 | const UNonSkillDecisionScoreEvaluator* DecisionScoreEvaluator; 23 | 24 | public: 25 | // Calculates the score for this decision. 26 | // By default, just returns the output of the DecisionScoreEvaluator, if one is hooked up. 27 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score") 28 | float CalculateScore(FDecisionContext& Context, float MinToBeat, float Bonus = 0.0f) const; 29 | }; 30 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/DecisionBase.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "DateTime.h" 7 | #include "Engine/DataAsset.h" 8 | #include "BehaviorTree/BlackboardData.h" 9 | #include "BehaviorTree/BehaviorTreeTypes.h" 10 | 11 | #include "DecisionBase.generated.h" 12 | 13 | class UDecisionBase; 14 | class UUtilityIntelligence; 15 | 16 | /* 17 | * A context that gets passed in giving real-world data about a decision. 18 | * This passes in the AI making the decision, the decision we're trying 19 | * to make, as well as the actor we're acting towards. 20 | */ 21 | USTRUCT(BlueprintType) 22 | struct UTILITYAI_API FDecisionContext 23 | { 24 | GENERATED_BODY() 25 | 26 | public: 27 | // The intelligence which is generating our considerations. 28 | // i.e. "Who is asking?" 29 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 30 | UUtilityIntelligence* OurIntelligence; 31 | // The actor which we are currently "thinking about." 32 | // i.e. "What are you going to do the action to?" 33 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 34 | AActor* OurTarget; 35 | // A Blackboard containing any "extra" context. 36 | // i.e. "What else is going on?" 37 | UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Transient) 38 | UBlackboardComponent* AIBlackboard; 39 | // The decision that we'll make if we succeed. 40 | // i.e. "What are you trying to do?" 41 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 42 | const UDecisionBase* Decision; 43 | 44 | public: 45 | FDecisionContext() 46 | { 47 | OurIntelligence = NULL; 48 | OurTarget = NULL; 49 | AIBlackboard = NULL; 50 | Decision = NULL; 51 | } 52 | }; 53 | 54 | /** 55 | * A struct representing a decision that we've made, 56 | * as well as the time we made it. 57 | */ 58 | USTRUCT(BlueprintType) 59 | struct UTILITYAI_API FMadeDecision 60 | { 61 | GENERATED_BODY() 62 | 63 | public: 64 | // The Decision that we've made. 65 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 66 | const UDecisionBase* Decision; 67 | // What time we made this decision (in UTC time). 68 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 69 | FDateTime DecisionTime; 70 | // The status of the decision after it was made. 71 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 72 | TEnumAsByte DecisionStatus; 73 | }; 74 | 75 | /** 76 | * This implements the actual AI code. 77 | * Each time this decision gets selected, `RunDecision` gets called to execute 78 | * any AI code. Note that the AI may not choose this decision next frame, so 79 | * any actions should be atomic, or at least not matter frame-to-frame. 80 | */ 81 | UCLASS(Abstract, Blueprintable) 82 | class UTILITYAI_API UDecisionBase : public UPrimaryDataAsset 83 | { 84 | GENERATED_BODY() 85 | 86 | public: 87 | // A Blackboard containing any kind of static data relevant to 88 | // every single time this decision is run, no matter the actor. 89 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 90 | const UBlackboardData* StaticDecisionBlackboard; 91 | 92 | protected: 93 | virtual FMadeDecision RunDecision_Implementation(const FDecisionContext& Context) const; 94 | 95 | public: 96 | UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "AI|Utility AI|Decision") 97 | FMadeDecision RunDecision(const FDecisionContext& Context) const; 98 | }; 99 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/DecisionMaker.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Engine/DataAsset.h" 7 | 8 | #include "Decision.h" 9 | 10 | #include "DecisionMaker.generated.h" 11 | 12 | /** 13 | * This is a list of DecisionScoreEvaluators, mapped to Decisions. 14 | * You can add or remove DecisionMakers to AI to enable special behaviors. 15 | * 16 | * For example, if a character enters a tavern, you can have a special 17 | * "tavern" DecisionMaker object which gets associated with that character. 18 | * The "tavern" DecisionMaker will give increased weights that cause the 19 | * character to eat, drink, and be merry. Once the character leaves the 20 | * tavern, the DecisionMaker is removed and the character goes back to normal. 21 | * 22 | * A DecisionMaker is very similar to a SkillSet, except that a DecisionMaker 23 | * operates on *decisions* (i.e. actions that any AI can perform, in theory -- 24 | * for example, "Move to a target"), whereas a SkillSet operates on *skills* 25 | * (special actions AIs can perform in parallel to their decisions -- i.e. 26 | * "Shoot a gun"). 27 | */ 28 | UCLASS() 29 | class UTILITYAI_API UDecisionMaker : public UPrimaryDataAsset 30 | { 31 | GENERATED_BODY() 32 | 33 | public: 34 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 35 | TSet Decisions; 36 | // A flat bonus given to this DecisionMaker, allowing for increased 37 | // weights for special behaviors associated with it. 38 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 39 | float Bonus; 40 | 41 | public: 42 | UDecisionMaker(); 43 | 44 | public: 45 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score") 46 | UDecision* FindBestDecision(const FDecisionContext& StartContext, FDecisionContext& OutContext, float& OutBestScore) const; 47 | }; 48 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/Skills/Skill.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "DecisionBase.h" 7 | #include "Skill.generated.h" 8 | 9 | /** 10 | * A skill is an action that a character can perform alongside things like movement. 11 | * Essentially, if "move to" is the background action, "shoot" would be the skill -- 12 | * the foreground action. 13 | * 14 | * Skills are designed to have multiple "variants" that can be selected. For example, 15 | * "Melee attack 1," "Melee attack 2," etc. 16 | */ 17 | UCLASS(Abstract) 18 | class UTILITYAI_API USkill : public UDecisionBase 19 | { 20 | GENERATED_BODY() 21 | }; 22 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/Decisions/Skills/SkillSet.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Engine/DataAsset.h" 7 | 8 | #include "SkillDecisionScoreEvaluator.h" 9 | #include "Skill.h" 10 | 11 | #include "SkillSet.generated.h" 12 | 13 | /** 14 | * A selection of skills that an AI can have. 15 | * For example, a "weapon" SkillSet would have things like "shoot," 16 | * "reload," "aim," "charge," etc. 17 | * 18 | * SkillSets are similar to DecisionMakers, but a DecisionMaker gets 19 | * run for things "in the background," like moving to a point. 20 | * SkillSets are run for "active" behaviors that should be able to 21 | * happen even when the character is moving -- for example, shooting. 22 | */ 23 | UCLASS() 24 | class UTILITYAI_API USkillSet : public UPrimaryDataAsset 25 | { 26 | GENERATED_BODY() 27 | 28 | public: 29 | // All our skills, mapped to DSEs which score them. 30 | UPROPERTY(EditAnywhere, BlueprintReadWrite) 31 | TMap Skills; 32 | 33 | public: 34 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "AI|Utility AI|Score") 35 | USkill* FindBestSkill(const FDecisionContext& StartContext, FDecisionContext& OutContext, float& OutBestScore) const; 36 | }; 37 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/UtilityAI.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class FUtilityAIModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | }; 16 | -------------------------------------------------------------------------------- /Source/UtilityAI/Public/UtilityAIHelpers.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Kismet/BlueprintFunctionLibrary.h" 7 | #include "BehaviorTree/BehaviorTreeTypes.h" 8 | #include "BehaviorTree/Tasks/BTTask_MoveTo.h" 9 | 10 | #include "Controllers/UtilityIntelligence.h" 11 | 12 | #include "UtilityAIHelpers.generated.h" 13 | 14 | /** 15 | * A collection of helpers for the utlity AI. 16 | */ 17 | UCLASS() 18 | class UTILITYAI_API UUtilityAIHelpers : public UBlueprintFunctionLibrary 19 | { 20 | GENERATED_BODY() 21 | 22 | private: 23 | static TEnumAsByte PerformMoveTask(UUtilityIntelligence* Intelligence, FBTMoveToTaskMemory& OutMemory, float MaxDistance, AActor* TargetActor, const FVector& TargetLocation); 24 | static TEnumAsByte MoveTo(UUtilityIntelligence* Intelligence, FBTMoveToTaskMemory& OutMemory, float MaxDistance, AActor* MoveActor = NULL, FVector MoveLocation = FVector(FLT_MAX)); 25 | 26 | public: 27 | UFUNCTION(BlueprintCallable, Category = "AI") 28 | static TEnumAsByte MoveToPoint(UUtilityIntelligence* Intelligence, float MaxDistance, const FVector& Point); 29 | UFUNCTION(BlueprintCallable, Category = "AI") 30 | static TEnumAsByte MoveToActor(UUtilityIntelligence* Intelligence, float MaxDistance, AActor* Actor); 31 | }; 32 | -------------------------------------------------------------------------------- /Source/UtilityAI/UtilityAI.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class UtilityAI : ModuleRules 6 | { 7 | public UtilityAI(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicIncludePaths.AddRange( 12 | new string[] { 13 | // ... add public include paths required here ... 14 | } 15 | ); 16 | 17 | 18 | PrivateIncludePaths.AddRange( 19 | new string[] { 20 | // ... add other private include paths required here ... 21 | } 22 | ); 23 | 24 | 25 | PublicDependencyModuleNames.AddRange( 26 | new string[] 27 | { 28 | "Core", 29 | // ... add other public dependencies that you statically link with here ... 30 | } 31 | ); 32 | 33 | 34 | PrivateDependencyModuleNames.AddRange( 35 | new string[] 36 | { 37 | "CoreUObject", 38 | "Engine", 39 | "Slate", 40 | "SlateCore", 41 | "GameplayTags", 42 | "AIModule", 43 | "NavigationSystem", 44 | // ... add private dependencies that you statically link with here ... 45 | } 46 | ); 47 | 48 | 49 | DynamicallyLoadedModuleNames.AddRange( 50 | new string[] 51 | { 52 | // ... add any modules that your module loads dynamically here ... 53 | } 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /UtilityAI.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "0.1", 5 | "FriendlyName": "UtilityAI", 6 | "Description": "An implementation of a \"utility AI\" system, using weights to tell AI which tasks to do instead of a traditional behavior tree.", 7 | "Category": "Other", 8 | "CreatedBy": "Jay Stevens", 9 | "CreatedByURL": "https://github.com/Jay2645/UnrealUtilityAI", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": true, 15 | "Installed": false, 16 | "Modules": [ 17 | { 18 | "Name": "UtilityAI", 19 | "Type": "Runtime", 20 | "LoadingPhase": "Default" 21 | } 22 | ] 23 | } --------------------------------------------------------------------------------