Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

AI in ECS. What approaches do we have?

Discussion in 'Entity Component System' started by Shinyclef, May 4, 2019.

  1. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    No. It is quite different actually. The idea is you have two different algorithms for evaluating the same axis. There's the "true" algorithm, which is expensive, and then there's the "approximate" algorithm, which takes the last evaluations inputs and utility value as well as the current evaluation's input and estimates a new utility value. The idea is that you use some heuristic to accumulate the deltas of the inputs with each evaluation and once that crosses a threshold, the approximation algorithm "expires" and you run the "true" algorithm again.

    I'm all too familiar with their flaws, and have posted quite a bit about it on these forums. If the flaws are still present in the next drop, I'll be flooding Unity with bug reports, because change filtering is pretty critical to performance.
    If you store an extra int within your component (or as a separate component), you can make it work per entity.
     
    lclemens likes this.
  2. mmankt

    mmankt

    Joined:
    Apr 29, 2015
    Posts:
    49
    In Spuds Unearthed we used a bunch of generic systems (like UnitStateSystem<T1,T2> T1- unit type, T2 state type) and components (ex UnitStateComponent<T> T state type) to make our units behave like in a regular state machine, each of those systems had utility functions to enter and exit a given state so the user just had to validate a state and then move to a next one, the system would add/remove all components it needed to run. it also gave a nice way to stack multiple states to units such as move to position and shoot/follow lane and search for enemies. it requierd to code all the functionality so we didn't have any visual behaviour trees. But with enough time it could be a nice addition to the system with a bunch of utility states/methods and a visual editor to compose units.
     
  3. Arnold_2013

    Arnold_2013

    Joined:
    Nov 24, 2013
    Posts:
    262
    GDC 2015: Dave Mark - "Building a Better Centaur: AI at Massive Scale"
    https://archive.org/details/GDC2015Mark


    In this talk he goes over optimalisations (26:30), where he does mention
    - sort DSE (decision score evaluator) by weight
    - sort Considerations by likelyhood
    - sort Boolean considerations first (since a 0 would stop the action from having a change)
    - only do expensive calculations at the very end
    Stop calculating when a action cannot win anymore, because a consideration can never increase the score only decrease it.

    I have not implemented this myself, since good enough AI was good enough for now. But IAUS feels like it should be able to make units look like they make understandable decisions.
     
  4. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Ah, I misunderstood - I thought when you said "change filters or expiring evaluation shortcut caches" you were talking about an alternative to the two-pass idea, but you were referring to enhancements to the two-pass proposal.


    Yes, that will allow you to narrow it down to the specific component, however, don't you still end up iterating over all the entities within that chunk and checking which one matches? Of course, if there are only a couple of components per chunk then it's not a big deal, but if it's a few hundred, that's a waste and partially defeats the purpose of having a change filter in the first place.
     
  5. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    You're right! I watched that episode a long time ago, but I forgot about that part. That solution is very similar to what I was suggesting with #2 in my option list - sorting based on complexity and shorting-out when possible. One difference is that Dave also considers likelyhood. I'm not totally sure how to translate his code structure into ECS with Jobs yet since that whole architecture with decision contexts and everything is extremely OOP centric (using delegates or overrides). I'll see if I can come up with something over the next few days.
     
    Last edited: Dec 13, 2022
  6. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    I don't understand this slide...

    upload_2022-1-18_14-9-40.png

    So bonus is the weight (which was defined as a positive integer in another part of the talk) added to a few other things like momentum and "other-bonuses" whatever they are. It looks to me like if bonus is a positive number, then the loop will break every single time and never consider anything no matter what. If ((finalScore > 0) || (whatever)) { break; }... where finalScore is initialized to bonus which is likely a positive number?

    Even if bonus is a negative number, finalScore will be changed to a positive number after the first iteration and then all following considerations will be ignored. Is the || supposed to be && ?
     
  7. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    It is not really an alternative nor an enhancement. It is a completely independent idea which could be combined with the two-pass but doesn't have to. Personally, this is the optimization I would employ first, and then maybe go two-pass if I really needed the extra performance and that got it.

    Well if your axis input calculation is very expensive, then checking the version for every entity will be a small price to pay for the amount of expensive operations you skip.

    Anyways, I should probably elaborate more by explaining my own design - MachAxle, even if it is on hold atm (designing an authoring experience and editor tooling isn't a problem I find very exciting right now).

    So in an IAUS, you have axes. But really an axis is composed of an input (or three if you have dynamic bookends), a curve, and an output. My approach is that from the input value onward, everything should be evaluated in a full SIMD brute-force manner up to when all the actions have computed utility values. Consequently, I have a very specific data structure for representing axes and actions, and much of that structure is baked into a blob asset. For inputs, there is a persistent dynamic buffer, and each input value has a specific offset in that buffer which can either be queried from the blob asset or cached in a component during conversion. Therefore the logic for calculating these input values and the logic for evaluating utility are completely decoupled. Choosing not to recalculate an input is fine because the input value from the last evaluation will be retained in the buffer (the SIMD utility evaluation logic only reads the buffer).

    Because of that, the systems which calculate the input values can use whatever change filtering, heuristics, or round-robin strategies they see fit. I would optimize those as necessary just like any other DOTS system.
     
    ercptz and Ylly-avvyland like this.
  8. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    I concur. I guess my main point was that change filters have so many "gotchas" that I often end up just using another mechanism instead.

    Thanks so much for that! That is reassuring because the architecture I'm building is very similar to yours... a buffer for inputs and all the axis and actions baked into a blob asset, with the logic for calculating input values and evaluation decoupled. I haven't quite laid out how I'll set up my systems yet, but the data structure and curve evaluation math is in place. I probably won't have the time to build an editor GUI, so for now I'm just using a web based curve plotter that lets me save, load, and tweak curves: https://www.desmos.com/calculator/pxenwlz4gm .
     
  9. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    Oh wow! That is really similar. I'll drop this tidbit then. Sort your actions by most to least axes used. Then sort your input first by number of curves and then by minimum action index using a curve with that input. And lastly rearrange your curves to line up with the inputs. That will get you really good memory access patterns, fully linear memory access for evaluating curves and highly cohesive memory access for evaluating actions.
     
    lclemens and Krajca like this.
  10. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Did you do anything with a context structure? Also, are you handling multiple targets?
     
    Last edited: Jan 21, 2022
  11. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    Not really. I just have a function that takes a NativeArray<float> and a BlobAssetReference as input as well as a bump allocator and spits out a NativeArray<float> as output. The input array comes from the input dynamic buffer. And the output array is the utility values for each action which I typically store in another dynamic buffer. So the utility calculation is not just decoupled from the input but also the final decision on the resulting action. This opens the potential for some really unique stuff like taking one of the utility output values and use that as an input for another IAUS pass, essentially making the AI hierarchical.

    So typically one entity at a time, but entities can be computed in parallel, typically by chunk iteration. Consuming logic has full control over inputs and how to act upon the output utility values. No predefined systems (other than conversion). Only the heavy utility calculations are abstracted, which I find to be the sweet spot for my designs.
     
    lclemens likes this.
  12. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    The scoring function seems like the most straightforward to me. The main problem I'm having is trying to figure out how and where to fill the input array in the first place.

    Some of those inputs might be really simple like for example "my heath". Most likely there is a HealthData component on each enemy for example. Do you modify the player's attack system so that it decrements the value in HealthData AND changes the value in the input array? That seems a intrusive because I would have to go modify every single one of my systems that affects an input and make them duplicate the data to the input array. Then those systems would no longer be generic and reusable with other projects. This seems like a bad option.

    Another option would be to make one or more systems that periodically copy data from various sources (current entity, target entity, global value, elapsed time, influence map, etc) into the input array. One problem is that if one of the inputs requires a complex calculation, it would get calculated periodically no matter what, even if the action or actions that use it are impossible at the given time.

    Another way to avoid running complex input calculations by checking other scores first is to move the execution of all the input filling code into the same thread so it is done sequentially right before scoring. But since input data can come from all over the place, that means having multiple queries and GetComponentData() calls all lumped into one place. This seems not very ECS-like, but maybe it could work.

    Maybe your hierarchical idea could help. I'll have to chew on that for a bit.

    Switching topics, as for the multiple targets, this slide shows how they should be handled: upload_2022-1-22_12-46-34.png

    So to make a fully generic system the score calculation function must somehow account for multiple targets, which are going to vary as agents move around the map. I haven't quite figured out how to work that in yet.
     
  13. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    Keep in mind that I have only done the design work for the runtime and haven't actually designed a good authoring experience nor implemented this in an actual project. With that said, I probably would do this:
    And regarding your concern:
    First off, for each type of calculation, there's a choice as to how to populate the input buffers. For data in calculations that are used by other systems (in which case the result of such a calculation is likely stored on another component), I can simply have a system that copies it on a periodic frequency, or perhaps by simply using change filters. But for calculations exclusive for AI, those can write directly to inputs on their own schedule. How and when these inputs need to update is game-specific. I don't put any unnecessary limitations on them.

    Like I said, I have input calculations run on their own schedule. Nothing breaks if a calculation just doesn't run, since its previous output is latched in the input buffer. If somehow there's a lot of expensive calculations that are AI-exclusive, I might make 0-masks using product utility assumptions, where I basically evaluate the cheap inputs first, and then I can check if any of those inputs for each action is 0, and if so, skip calculating the expensive input.
    This is the real use-case for hierarchy idea. Each target has a utility, and then I can feed the best target's utility into the second level that determines if that target is worth attacking. While I could just duplicate the axes for each target, doing it this way reduces the number of axes to evaluate since the axes for choosing the target can just assume that the action of attacking a target is going to be taken.
     
    lclemens likes this.
  14. awefasdgh

    awefasdgh

    Joined:
    Mar 4, 2018
    Posts:
    3
    If you go to 27:37 in the video, you'll see the same code block again, but the '<' has become a '>.' Definitely an error.

    How do you implement momentum?
     
    lclemens likes this.
  15. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Thanks, I didn't notice that!

    I have not implemented it. I most likely won't implement it unless I start running into problems. I'm not really sure why Dave and Mike separated it that way - it seems to me like momentum could be implemented as just another consideration - like TimeSinceMyActionChanged or something.

    I didn't implement bonus either because I'm not even sure what it is. I implemented weights though (Dave talked about those in earlier videos) and maybe those are the same as the bonus?
     
  16. awefasdgh

    awefasdgh

    Joined:
    Mar 4, 2018
    Posts:
    3
    Bonus is weight + momentum + other mystery bonuses that I don't think are ever addressed or explained.

    Have you implemented the compensation factor?
     
    lclemens likes this.
  17. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    For the record, you are an awesome source of info! Wish I could thank you with a beer or something.

    I like that idea of separating the CopySystem and other systems that fill inputs from the expensive functions that depend on AI results to determine whether they are calculated. I think those AI-dependent calculations will need to be done in the same thread and system as the AI scoring in order to ensure that they run or are skipped at the proper time during AI score calculation. That is unfortunate because those AI-dependent calculations are game-specific and it would be nice to have a separate dedicated scoring system that is generic and can be re-used among different games.

    On a different topic - I have a crappy inspector GUI right now for building the blob asset.

    upload_2022-1-23_22-38-7.png

    It's really basic though... no fancy curve graphs or anything. I'm still using that desmos graphing calculator webpage for graphs and I don't plan on building a fancy GUI anytime soon. Perhaps when you push Mach-Axle to the Latios-framework repo I can help with the GUI or something.
     
  18. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Ah, gotcha.

    Yes, I did implement the compensation factor.
     
  19. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    If you can't filter directly on the inputs, you can make pseudo-actions which only consider the cheap inputs and use the resulting utility as both an input to the second pass and as something you can check for 0f on. For the second pass, just use a y = x curve.

    It is unlikely I push anything directly to the repo without a UI. Maybe I will post a prototype in a separate repo, but that won't happen until I finish my current priorities, which is a new video and my new animation tech. But if by the time I finish all that you are still interested, I'd love to team up and push some boundaries with this!
     
    lclemens likes this.
  20. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    A quick update on my progress....

    So far, I'm mostly happy with how it turned out.

    ---------------------------------------------- GENERIC AI STUFF -----------------------------------------------------

    AiTableBlobAsset: The AI Table Blob Asset is at the heart of it all. It is created from a scriptable object that lets the designer define and assign all the inputs, considerations, decisions, and decision sets. A single MonoBehaviour in the scene is responsible for converting that information into a blob asset at the start of the game so that it can be accessed with GetSingleton() anywhere it is needed.

    upload_2022-3-15_10-47-57.png

    There is no fancy curve editing tool. I use desmos.com for tweaking the curves for at the moment.

    AiAgentAuthoring: The next piece is an authoring script that gets attached to a prefab or archetype to give an entity the ability to be an AI agent. It adds 3 components:
    1. An input buffer of floats - one slot for each input (DynamicBuffer). Its values are normalized.
    2. The current decision set for the agent (an enum).
    3. The current recommended action at any given time. It contains oldAction, newAction, and isFresh.
    In the future I might make the decision set an array so that an agent could have multiple decision sets and even add or remove them - like the example Mark gave where mage character walks into a tavern and he's given an additional set of actions to choose from (or he might even temporarily lose an action set so he doesn't start the tavern on fire).

    upload_2022-3-15_12-45-0.png

    AiDecisionScoreSystem: Another really important piece is the system that does all the scoring. It's fairly simple really - for every single AI agent in the scene it loops through every decision in the decision set, scores it, and sets the action in the recommended action component. It only runs 4 times per second. It accounts for weights and also compensates scores for the number of inputs. The decision system is smart enough to cache consideration curve outputs that have already been calculated and re-use them as inputs to other decisions (avoiding duplicate calls to math.sin(), math.pow(), divisions, etc). It also is smart enough to stop calculating a score for an action when it can't possibly win. It is fully bursted and runs in parallel for all agents.

    AiGizmo: I think utility AI would be pretty much impossible without a way to display the current numbers contributing to a decision score at any given time. The gizmo displays each agent's current decision score. At the moment it's pretty ugly and could use some work, but it gets the job done.

    upload_2022-3-15_11-54-49.png

    Of course there are a few static utility classes like CurveUtils.cs, UtilityAiUtils.cs, etc.

    And that is it for the whole generic AI part. It's a lot less code than you might think!

    ------------------------------------------- GAME SPECIFIC STUFF ---------------------------------------------

    There are a few other systems that are specific to my game. These systems will vary for different games, but they will also have some similarities to the ones I am using.

    For example, I have a AgentInputSystem that is responsible for filling all of the inputs for an agent. It runs about 5 times per second - just enough to keep the data fresh for AiDecisionScoreSystem. It runs a bunch of jobs such as this one used to fill the health input:
    Code (CSharp):
    1. // MyHealth
    2.         Entities.WithNone<DeathData>().ForEach((ref DynamicBuffer<AiInputElement> inputs, in HealthData health, in MaxHealthData maxHealth) => {
    3.             inputs[(byte)AiInputType.MyHealth] = new AiInputElement { isValid = true, value = health.health / maxHealth.health };
    4.         }).ScheduleParallel();
    One thing to note is that all these jobs write to the same input buffer so they end up being scheduled and run sequentially. Without pointers, I don't know how to fix that.

    There is also an AgentAnimationSystem system that changes the current animation based on the recommended action. An AgentMovementSystem changes the destination path for an agent so the pathfinding systems can take it where it needs to go (or stop the agent if it shouldn't move).

    Of course there are even more systems that indirectly contribute to filling up the inputs. For example, a system must be running that continually updates the distance between an agent and its target so that the AgentInputSystem can copy the data from the distance-to-target component to the appropriate slot in the input buffer. I also have an AgentTargeterSystem that is responsible for choosing a target (see item 3 below).

    ----------------------------------------- CONCLUSION AND THOUGHTS ------------------------------------

    The generic AI part is simple and elegant. Overall I'm happy with how it is working and I now have some agents running around that will travel towards a target when they are healthy, attack a hostile target, run to a health station when they are hurt, and idle when they don't have any targets or are between attacks. It's very simple, but using IAUS I can now make more complicated behaviors in the future.

    I still have some concerns that I am pondering though.

    1) One minor issue is the extra memory used by the input buffer where each slot is accessed via an auto-generated enum. In some ways this is great because storing and reading inputs is super fast random-access. However, each agent stores a buffer with slots for ALL potential inputs even if only some of them are used, which results in some wasted memory for every agent. This could be fixed by using a map, however, I haven't really figured out an easy way to use maps within components. And of course the map lookup will be slightly slower than the current access-by-index. I'm torn between keeping it the way it is vs switching to a map. Another idea would be to generate unique enums and input buffers for each decision set, but then it would get really messy writing systems to fill those inputs while having to equate between the different enums. Overall, it's a pretty small amount of wasted memory, so I think for now I'll keep it as-is and if I run into memory problems on mobile devices I can try the map idea.

    2) Another issue I have not solved is the ability to skip expensive calculations based on when a consideration cannot possibly win. This issue was discussed in this thread earlier and I didn't want to get that fancy in the first prototype. My current idea as suggested by @DreamingImLatios is a two-pass approach. In frame 0 AiDecisionScoreSystem would run a first pass and update certain inputs with a needsCalculation flag.. Then in frame 1 various systems would look for that flag and run the calculation. Then in frame 2, a full score calculation would be done and isFresh would be set to true. It's messy because all of those independent systems will have to be changed to only run when they see the needsCalculation flag for their particular input. For now I am going to just use what I have because none of my calculations are super expensive (yet).

    3) Unlike Dave Mark's system, I haven't handled multiple targets. At the moment, I just have AgentTargeterSystem that periodically loops through each agent and uses unity.physics with world.OverlapAabb() to find enemies within range of the agent and choose the nearest agent. So basically the AI systems only ever know about a single target and my "target choosing AI" is hardcoded into a system. I think maybe I could set up some type of hierarchical approach, but that concept is not quite concrete in my mind yet.

    4) My last, and largest concern is that I didn't implement any sort of auto-input-parameterization like in the video https://archive.org/details/GDC2015Mark at 14:30. A lot of times the "range" (or min/max) for how an input is scaled is actually dependent on a variable instead of a fixed number. And that variable can come from anywhere - the agent, the target, a global, or some other calculation. For example, I have a WhenAHealthStationIsInHealRange parameter. Right now I have hardcoded the number "10" into the AI table. That 10 happens to be the same value that the health station has its range set at. So if I change the range of the health-station from 10 to 5, and I forget to change the number in the AI table, then the agent will walk up to within 10 units of the health station and just sit there not getting healed because the range of the health station is only 5. It turns out that there are quite a few inputs like this that depend on ranges based on variables from all over the place. I'm not sure what the best way to enumerate all the possible min/max parameters is. All I know is that I don't like having to change a number in two places.

    So.... that's my update. I've only had it working for a couple of days, but when I've flushed out all of the bugs and finish tweaking it, I might throw the generic utility AI part up on a public repo. Any suggestions and comments are welcome.
     
    Last edited: Mar 15, 2022
    Morvar, ZammyIsOnFire1, bb8_1 and 6 others like this.
  21. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    Wow! Very impressive!
    I am very curious how comfortable people find that authoring interface. If it is "good enough", I might prioritize my implementation. If DOTS 0.50 doesn't come out before I wrap up convex colliders and smart blobbers, I'll have a tough decision between this and collision layer queries. :p

    You'd have to combine jobs, which is a pain if you rely on Entities.ForEach.

    This is one of the areas where our designs diverge. In my design, rather than use enums for all inputs, I compute the offsets and store them in components. It decouples the names of the inputs from the IAUS which makes it more general purpose. It is usually lighter on memory but you can easily come up with edge cases where it is worse.

    For each set, the mapping is just an array of indices for each enum value. So you index the array using the enum value and you get an offset into the buffer to write to. No hashmap nonsense required.

    I would probably do a "top X sorted by" approach for a little diversity. I could then choose some custom replacement algorithm where maybe the worst score gets swapped out. Another option I have considered is defining an IAUS only for actions involving a single target. Then I would repeatedly evaluate IAUS in a loop, swapping out the target and the inputs (which would likely be a row in a 2D buffer).

    Make it part of the input definition. That means that some (if not all) of your inputs would be effectively float3 instead of float. This is another reason why I avoided an enum to define input indices.

    All in all, I look forward to seeing how far you take it. Congrats!
     
    FilmBird, lclemens and Krajca like this.
  22. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Thanks again for your input @DreamingImLatios !

    So you mean put everything into one single monster big job? I think that would be messy... there would be a ton of parameters as input (which I'm guessing is why you said that Entities.ForEach would be difficult - it only allows a handful of parameters). I have seen mention of WithNativeDisableContainerSafetyRestriction()... maybe it's possible I could do something with that and by converting the dynamic buffer to a native container and copying it back with ecb.SetBuffer()? For now I'm willing to trade simplicity for a little speed, but I may revisit it in the future.

    That approach seems reasonable. I forgot to mention that I had actually started writing that same concept, but at the time I was just trying to get the guts of the thing built so I abandoned the idea for a later time. I still have the "map" generation code function. For every Decision Set, the set of inputs would be constant so the map could be stored in blob-data instead of as a component on the agent.

    I'll have to keep thinking about this one. One of the issues I keep bumping into is that I don't want to store a huge list of targets on any of the agents - I would prefer to have a system look at everything and choose the "best" one based on a certain criteria and only store a single result. For turrets, that criteria is dynamic - the player can set one turret to prioritize the strongest target in its range, and another turret might prioritize the furthest away, etc. Meanwhile there are a lot of other filters like some turrets can't target flying agents. Your second option might work with that... I suppose I'd have to have a flag in the blob-data and a checkbox on the GUI to denote that a particular input requires special handling for targets? In my mind, the implementation details are still murky.

    I think maybe I didn't describe the dilemma well. The problem I am running into is not that there isn't a place to store the min/max, it's how can the designer can "map" which min/max to use for inputs that rely on dynamic variables.

    Perhaps this is a better example: One of the input examples Dave uses often is DistanceToTarget. Let's say that the agent has an attack range and the attack is completely useless if it doesn't meet that minimum range, which varies throughout the game based on the player's experience level and if they have any active buffs or poison-effects or whatever. Let's also say that the agent also has a range of visibility (which is also variable throughout the game). Let's pretend there are two considerations like WhenTargetIsInAttackRange and WhenTargetIsVisible. I think in Dave's examples he would set up both considerations to use the same input - DistanceToTarget and then on his GUI under the consideration there is a dropdown combo box containing the variables "MaxVisibility" and "MaxAttackRange" to let the designer choose how to scale it. In the video he said "As soon as you select an input it knows what parameters might be important for that input." He also gives an example where there is a enum dropdown box in a "Params" section underneath the consideration for "Buff Status" that includes "Invisibility".

    I'm not crazy about making a bunch of context sensitive enums or trying to use introspection or something to try and make a smart GUI like that.

    So now for some brainstorming....

    One approach is that instead of having one input (DistanceToTarget) like Dave does, I could break it into 2 inputs - DistanceToTargetAttack and DistanceToTargetVisibility, and then it would be trivial to choose one or the other on the GUI and fill them up in the AgentInputSystem. I could either use a single float and scale/normalize the inputs as they are placed into the input buffer, or use two or three floats (value/min/max) and let the AiDecisionScoreSystem handle the normalization. As long as I'm only calculating the distance-to-target value once, I don't see any huge downsides, although I suppose it might increase the number of inputs by quite a bit depending on the scenario. It's also slightly redundant - storing two values when one could be used instead. I could implement this idea right now with no extra code changes.

    A second possibility would be to put min/max values on the GUI in with each consideration, but also have an option to choose a dynamic value from the input list. Then I could make 3 inputs DistanceToTarget, MaxAttackRange, and MaxVisibilityRange. For the max value in the WhenTargetIsVisible consideration, I would select "MaxVisibilityRange". The two max ranges might not be used in any scoring calculations, but who knows, maybe they would be useful sometime. The min/max values of MaxAttackRange and MaxVisibilityRange would just be fixed numbers and likely ignored. The AgentInputSystem could easily fill all 3 inputs within a single job. Dave also mentioned using the outputs of other considerations or actions as well, and with this approach, designers could get extra fancy and set the max-value of a consideration as the output of some other consideration or action. One disadvantage is that the input array would require 3 slots (1 float each), but the extra flexibility seems worth it. Another disadvantage is that the order in which the inputs are filled might matter, but realistically the worst case scenario in my example is that the two considerations might use the cached max-attack-range value that is 1/4 second old. No biggie. The last issue I can think of is that in the AgentInputSystem there will be an extra if-statement to evaluate. Someting like --> maxValue = consideration.useFixedValue ? consideration.maxValue : inputs[consideration.maxInputId]; . Seems pretty harmless.

    I'm leaning towards the second possibility.
     
    FilmBird and DreamingImLatios like this.
  23. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    Approach 1 was what I had in mind. But after hearing you describe approach 2, I like that much better! The way I am reading it, every dynamic value is an independent single-float value. Then each consideration has three inputs which each can either be constants or come from the input buffer. I don't know why the order would matter, as you would have the same issue just forwarding condition values to a new input regardless of what role it played (min, max, or the "x"). As for the dynamic reading of the value, it probably won't be a big deal for you. It will matter a lot more if you try to simd-ify everything. But it doesn't sound like that is something you have focused much on.
     
    FilmBird and lclemens like this.
  24. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    I concur on the order not mattering much. I actually went ahead and implemented it - it was really only a handful of lines of code change, and I am just about to test it. I just dropped back in the forums here to see if you had any more of your wise advice to double check and make sure I'm on the right track.

    I got a little fancy with the scriptable object UI so that designers can enable or disable the ranges.

    Screenshot without ranges (inputs are assumed to be already normalized, such as passing in health as a percent). This is the default mode for considerations:
    upload_2022-3-16_20-33-25.png


    Screenshot with ranges (Min is static and Max is set to the dynamic "MyMaxAttackRange" value).
    upload_2022-3-16_20-35-55.png

    The min/max floats and a mask for determining how to interpret them are stored in the blob data. The input buffer still just holds a single float per element (well technically a float and an isValid bool so the decision system can ignore inputs that haven't been filled yet).

    One thing that changed is previously I was running all the normalizations inside of AgentInputSystem, but now that the min/max is available in the blob-asset, I moved the normalization into the AiDecisionScoreSystem. This is useful because it's one less step for adding new inputs. There are some inputs like MyHealth that get normalized outside of AiDecisionScoreSystem, but the nice thing is that it's flexible enough to accommodate either approach.

    I might implement the input "map" next week, but I want to play around with the current version for a few days and maybe profile it some. Technically, right now since I only have one decision-set, all the inputs are being used so there is no wasted space. Once I start adding more decision sets, the map will become more useful. I will still keep the auto-generated enums around though because they'll be handy for specifying an input to the map, plus they're fantastic for using in the debug-gizmo that displays all the current decision/consideration/input scores and values.

    Overall, this new min/max specifying scheme is quite promising.
     
  25. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    @DreamingImLatios - So I decided to revisit my Utility AI. I've been using it with great success over the past 9 months. Just recently I made a few minor adjustments, but now I'm considering making a larger one, and I wanted to double-check something with you and also write out my thought processes on the change which sometimes helps me gain some insights into the best direction to take.

    Also, I'm curious if you have made any new milestones with your IAUS since our last discussion!

    So currently, I have a DynamicBuffer for inputs that resides on every AI agent. The scoring system loops through all agents and calculates the scores and only stores the winning decision (or action) score in a component on the agent as a float and decision identifier/index. So all the scores for the non-winning decision are thrown away.

    After reading through this thread again, I believe that instead of throwing away non-winning scores, you are storing all your decision scores in a DynamicBuffer on the agents (at least that's my understanding, but I could have misinterpreted). I am considering the pros and cons of doing this. Obviously one disadvantage is that it uses more memory because instead of a storing a simple winning decision index per agent, it's storing an entire DynamicBuffer of scores per agent. One advantage I can think of is that it would be useful for displaying debug information. Another advantage might be that maybe if multiple systems were calculating different scores, maybe they wouldn't need to be synchronized because one system could use cached scores from another system? Currently I only have one system doing the scores, so that isn't much of an advantage, but perhaps there are situations where it would be useful to have multiple scoring systems? Is there any particular advantage that you know of?
     
    bb8_1 likes this.
  26. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    I was just about to start really investing in it, and then Entities 1.0 experimental dropped. Now I'm writing a Transform System. :p So hopefully sometime in 20223 I will get around to it.

    My design is that the API will be a method that takes the input as a NativeArray, the BlobAssetReference, a working memory cache, and output NativeArray to populate all the decision scores into. What backs that output array is up to the caller. It could be a DynamicBuffer, or it could be Temp memory, or something else entirely. I don't define a system, just the mechanisms for creating the Blob Asset and the Burst-compatible evaluation method.
     
    bb8_1 likes this.
  27. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Yeah, I know what you mean on the 1.0 drop - I was torn between wrapping up these IAUS tweaks and converting the project to 1.0.

    My API has a very similar function with the same parameters with the only difference being that my function puts the result score into a "winner action" index+score instead of an array of all scores. If an array of scores is used - In the end... the person using the API has to do something with the array. Allocating and reallocating temporary memory would probably be a bad idea. Reusing temporary memory might work, but that would be tricky if the call is made from parallel threads since threads would be writing simultaneously. Storing it as a DynamicBuffer per agent is a thread-safe performant possibility that uses extra memory.

    Regardless of what the API user does with the array, I'm mainly just wondering if there are any overall reasons why it might be useful to present the result as an array vs a single winning decision. In theory there will be some overhead (memory and/or performance) with a result array. After looking over some of Dave Mark's slides again, I guess I can't really come up with a compelling reason to present the results as an array other than for debugging. I suppose in rare scenarios someone might want to show the player some of the contributing decisions and why they weren't chosen, but I think the player would be more interested in seeing some of the contributing input parameters like "hunger level" as opposed to failed instantaneous decisions like "utility of eat-food score".

    Or maybe there is a scenario where one decision might rely on the score of another decision calculated by a different scoring system? It looked to me like Mark talked about inputs relying on other inputs (or "utility measurements"), but he also showed a scenario of scores that rely on other scores. If that were true, there are two ways I can think of to allow for that:
    1. Devise a way of expressing that relationship in the GUI (and hence the blob asset).
    2. The API user would need to define a new input and then copy the value from the result score array into the input.

    The first case could get people into trouble if they had a circular dependency or they might get stale data if the order of the calculations was reversed. The second case could also suffer stale data depending on when the input-collection and scoring systems execute.

    One last thing... good luck with the transform system!
     
    Last edited: Dec 13, 2022
    tmonestudio likes this.
  28. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    This isn't actually that hard and is what I would do in situations where I don't want to store the results in a Dynamic Buffer.

    I've thought of a few. Debugging is one of them. This is another.
    I can totally see myself using the resulting product weight of one decision as an input to another evaluation with something like an inverse curve applied. I'd purposefully design a phase 1 and phase 2 evaluations to avoid the circular dependencies.

    But the third and possibly most important reason to keep the array is that there could be multiple actions that could be taken at once. For example, I could have an AI choose to move left and shoot at the same time. Sure, I could make a separate blob of inputs, curves, and decisions for each type of independent action, but my whole goal of MachAxle is to batch-process the data. Sharing inputs and curves reduces the overall workload, and doing more things at once allows for better vectorization opportunities.

    Appreciated! I hope people like it.
     
    lclemens likes this.
  29. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Thanks for the input!

    I'm beginning to think that to truly capture all scenarios where inputs depend on other inputs and decisions depend on other decisions, one would need a graph structure with a GUI for it. I once saw a Utility AI package with a graph GUI like that, but I can't find it again. However, I think that capability is more advanced than I want to go right now. A GUI that can handle for example, letting the designer program in the "cover" equation in the screenshot below or define relationships that depend on other relationships (and checking for cycles) is not something that I anticipate needing. I'll support being able to implement those things, but it won't be entirely possible without programmer intervention. For example... the programmer will need to code up a "cover" input equation instead of the designer being able to do it in the editor. And it will be up to the coder and designer to ensure there are no infinite cycles.

    upload_2022-12-14_15-51-41.png


    On a different topic, I have been looking into what it would take to make a graphing UI...

    The first idea I had was using the Desmos API https://www.desmos.com/api/v1.8/docs/index.html . One hurdle is that Unity took out the webkit core in 2020, so there's no easy way to embed a webpage into the editor. Doing it outside in a browser webpage might not be so bad - I could provide an inspector/editor button that would generate an html page with the graph and parameter values set via the API. Unfortunately, I couldn't figure out an easy way to extract the parameters after the user changes them and then suck them back into unity so I can use the values to make the blobasset.

    A second idea would be to build a custom editor. I think NoOpArmy Utility AI did that, but I'm not sure how it was done. BTW - that package has been released for game-objects and supposedly a DOTS version will be out soon http://y2u.be/1UndgSWy1dA .

    My last idea would be to use the animation curve editor built into unity with interpolation instead of the mkbc equations. In a reddit post, someone asked Dave what the advantage of using mkbc equations (sigmoid, quadratic, etc) is vs point interpolation, and his reply was basically that interpolation would require a lot of searching for the proper segment so the equations are faster. However, I'm not so sure I agree with that. The points will be ordered by x value, so doing a binary search in a 16 point line would be 4 comparisons in the worst case, and 2 in the average case. After that, the interpolation equation is trivial. So we're really looking at "1 to 4 comparisons plus some additions/multiplication" vs "the evaluation of equations (some of them which use sin(), exp(), division, and other relatively slow operations) plus a switch statement". I don't know which is faster, but I suspect both methods are extremely fast. In some ways, I think using the point curve editor would intuitive for designers - there's no need to select a mathematical function and tweak it via sliders trying to get the right shape - just start dragging points around to make whatever shape you want. Storing the curves would take slightly more memory, but they can be stored in the blob-asset and they would only be stored in a single instance so the amount of memory needed for all the points for all the curves in an AI system would be tiny.
     
  30. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    I believe UI Toolkit has a new line and mesh drawing API. I planned to use that.

    At low resolution, this works quite well. But with more points, you start to use up more memory bandwidth to read your curves and the math operations are going to be faster, especially with Burst which has extremely optimized special functions via sleef.

    I thought I remember a solution somewhere where the mkbc equations could be boiled down to just two equations. That means that with AVX I could evaluate both equations for 8 elements at a time and then select between the results with a mask.

    Anyways, if you do go higher resolution with interpolation, you may want to look into ACL. Instead of binary searches, it uses direct lookups with variable bit rates to keep the data size down while also performing fast sampling.
     
    lclemens likes this.
  31. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    I don't see any reason why more than 16 points would be needed. Maybe I could see 32 points (worst case 5 comparisons instead of 4) in extreme situations, but in 99% of cases, the player is never going to know the difference between 16 points and 32 points.

    That's a good point about there being slightly more memory during evaluation though. I read somewhere once that in general on a CPU division is 2x to 4x slower than multiplication. The Logit function that Dave likes to use so much contains two divisions as well as a logn(). In either scenario, the functions would be evaluated via burst and in parallel, so it seems to me that the extra comparisons of the interpolation would approximately cancel out the division and log() operations in the mathematical approach, but without benchmarking, it's really tough to know for sure. I suspect both methods would handle millions of agents with room to spare.

    As for the number of mkbc equations, it depends on how fancy you get. At the moment, I'm using about 6 different ones because I wanted a few extra curve types in my arsenal. The Curvature project uses Linear, Polynomial, Logistic, Logit, Normal, and Sine. I'm pretty sure that linear and polynomial can be combined, but I don't know about the others.

    I think UI Toolkit calls it the Vector API https://forum.unity.com/threads/introducing-the-vector-api-in-unity-2022-1.1210311/ . I'll look into it - thanks!
     
  32. MaNaRz

    MaNaRz

    Joined:
    Aug 24, 2017
    Posts:
    117
    I am doing this in my Utility AI. Considerations feeding into other Considerations to eventually get a Decision. Everything is still losely based on the infinite Axis model but i had to extend it quite a bit.
    upload_2022-12-15_6-35-45.png

    I am using AnimationCurves to display (but i am not sampling them at runime) and as the graph UI I use the experimental Graphview API. Graphview gets the job done but is far from nice to work with.

    The inspector of a node looks like this:
    upload_2022-12-15_6-39-46.png

    My goal is to support 100% editor workflow without any code needed (I achieved this already but i am not yet happy with the clunky workflow currently). For me that means i have to codegen some stuff for the user.


    BTW ignore the values set in the graph. This does not make sense. Its my TestGraph for UI coding.
     
    Morvar, Antypodish, toomasio and 2 others like this.
  33. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Oh wow - very cool!!

    That's interesting because Dave talks about stacking inputs and decisions, but I don't think I saw anything about stacking considerations.

    Will your GUI let designers code in equations like in Dave's Cover example? "Cover = (0.2 + Reload + (Heal * 1.5)) * (Threat * 1.3))"

    Also what widget did you use for drawing the graphs?
     
    Last edited: Dec 16, 2022
  34. MaNaRz

    MaNaRz

    Joined:
    Aug 24, 2017
    Posts:
    117
    Do you mind sharing the Source? I didnt remember hearing anything about stacking. I would like to check out how Dave does it.

    I actually do have some more stacking I did not show:
    Each Agent can have multiple such Graphs ("DecisionLayers"). So for example an Agent could have a CombatLayer and a ResourceGatheringLayer. At the end the highest Decision of all Layers still wins. One of the next features I will implement uses those Layers to implement the possibility to have multiple Graphs producing multiple Decisions.

    Inputs are not really stacked but staggered. I only get inputs when the first consideration needs those inputs but i reuse them for all consecutive considerations.

    I also allow reading Decisions and Considerations from other AIs. That makes it possible to have a Global Combat Director evaluating coverpoints and then all agents can use these scores to do their individual logic. It is kind of like stacking those graphs one after the other. This feature is needed for performance since its impossible for 1k agents to evaluate thousands of coverpoints each frame. This way a Global AI does most of the work once and the agents only consider already scored Coverpoints based on their own location.

    Yes, the Graph basically builds that equation and makes every part of it reuseable for any later consideration.

    https://docs.unity3d.com/2022.2/Documentation/ScriptReference/Experimental.GraphView.GraphView.html
    But as i wrote above its not really nice to work with.
    I do think I wrote as much UI Code as UtilityAI Code.
    (Though I have some unnecassary UI features like force-directed automatic graph layout)
     
    Last edited: Dec 16, 2022
  35. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    The screenshots of slides I shared above are slides where Dave talked about "stacking". In the left one, he is using an equation composed of 3 previous decision scores to make a new one ("Cover"). In the middle one, he's talking about collecting and stacking inputs. In the one on the right that has the text "Stacking it all Up", I believe those are inputs as well (like MyHealth), but some of them might be other decision scores - I can't tell for sure. I don't recall the exact video timestamp where he talks about "stacking", but I got the screenshots from these two videos:

    https://www.gdcvault.com/play/1012410/Improving-AI-Decision-Modeling-Through
    https://www.gdcvault.com/play/1018040/Architecture-Tricks-Managing-Behaviors-in

    Nice! Multiple graphs would be cool. I used to call the top level "action sets" but I am now calling them "profiles". So an agent can have a ResourceGathering profile, a Combat profile, etc -- it's very similar to yours.

    That seems like a reasonable idea. I don't have cover-points in my game, but I imagine that I'd do something like that if I did. Are you compiling the consideration/decision/profile data into blob-assets like DreamingImLatios and I are doing?

    In mine I can sort-of handle that equation on the GUI because like Dave specified, I added a "weight" to each decision. But there are two reasons why the weight doesn't suffice. The first is because the weight is an addition, but that Cover equation has multiplication as well. The second problem is that the weight is associated with a single decision so it's fixed. It will break if I want one profile to use Threat with a multiplier of 0.5 and another profile to calculate Cover where the multiplier is 1.3 - I would have to create two different Threat decisions that are exactly the same but with two different weights. That seems hacky. So I'm thinking that at this point, I will have the developer code up a cover equation and just make a new input called "Cover" that relies on the output scores of the other 3 numbers (Reload, Heal, and Threat). I'm not sure how Dave categorizes those numbers, but in my system anyway, I treat Reload and Heal as decisions/actions, and Threat is something I'd likely calculate in code separately so it would probably be an input instead of a decision.

    I didn't even know that GraphView existed, so thanks for the info - wish I had known about that earlier when I was making my baked vertex animation tool. I forgot that there are two types of graphs we are talking about so I wasn't specific enough... The graph I was actually asking about is the one you used to graph the curve equations.
     
  36. MaNaRz

    MaNaRz

    Joined:
    Aug 24, 2017
    Posts:
    117
    Well i wouldnt really call it compiling in my case. It's a pretty straight forward baking of authoring data to a blob.
    Each node in the graph creates its own System getting its own blob.

    I am not sure why the weight is added instead of multiplied in Daves design. I decided to multiply and only allow numbers between 0 and 1 as a factor so each score still always stays between 0 and 1. But it turned out to be rather useless because there are just better ways to handle those priority cases in the graph. I am thinking about removing it altogether.

    My Nodes actually support add, subtract, multiply and divide operations between axis (but essentially through another multiply every output value will still be between 0 and 1). But using those other operations makes everything much more complex i feel.They are not really useful to generate a Score but for example to combine Targets that would otherwise be zerod out with multiplication or to calculate differences in army strength when engaging enemies.

    All nodes you see in the graph are global to all other graphs. That might be unintuitive but it allows me to share logic between agents and profiles. So i could give all enemys the exact same "Threat level" consideration but then use it differently to make further more agent/profile specific considerations. That works because that "Threatlevel" scores are again treated as an InputAxis and thus can be remapped to other Scores for each following consideration.

    Ah my bad. Im using AnimationCurves but the other way around. I calculate 100 keyframes based on the m,k,b,c values and feed those to the curve. I would love to actually code my own curves at some point to be able to edit them in a nicer way than setting those 4 values but for now displaying-only works well enough with unitys animation curves.
     
  37. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,087
    I've been reading about the Infinite Axis Utility System (IAUS) over the past few days and gained some understanding of how it works. designing an IAUS implementation seems feasible using ECS, although performance optimizations such as collecting axes only when needed per agent and skipping computations for low priority actions may be required. by making design compromises and using random data access, these desired goals can be achieved. As mentioned by @Joachim_Ante , "Some problems cannot be expressed with linear memory processing".

    Although the technical side of implementing IAUS seems doable, I'm not sure if it's right for my project. I consider both IAUS and machine learning as potential options. About 70% of my game's abilities can be described using a dynamic data model that can make them suitable for IAUS, but there are also many specific abilities that may not be easily handled by IAUS. A potential solution could be to create custom or shared dynamic Actions and Axes for each ability, but this could be time consuming.
    On the other hand, machine learning can provide a more advanced solution for these specific abilities.

    However, machine learning also has its compromises, including testing and training time, which can impact development iteration.
    In some cases, like when precise agent control is required for things like (turrets or simple following pet), machine learning may not be a good choice. I would greatly appreciate your advice on the most appropriate approach for my project.

    Thanks!
     
    lclemens likes this.
  38. DreamersINC

    DreamersINC

    Joined:
    Mar 4, 2015
    Posts:
    130
    In my opinion, machine learning should never be used as an AI type. It can be used to improve an AI system that is already running in the game. But it is not a standalone AI. You can use machine learning to tweet the parameters and adjust how an AI will respond to a set of events. The issue with relying on machine learning as an entire AI is that at you you have no control the AI and it's no longer predictable. Players look for the AI systems respond in a certain way. If you think about first person shooters, you'll see that the there are no first person shooters where the AI uses flanking. With machine learning you end up with a problem of how do you make sure that the AI is fun and beatable by the player?
     
    ercptz, Antypodish, calabi and 3 others like this.
  39. Opeth001

    Opeth001

    Joined:
    Jan 28, 2017
    Posts:
    1,087
    I have been researching the use of machine learning in game programming, including by watching GDCs and reading forums and threads on the topic. From my observations, it appears that machine learning is still a relatively new field and there is not yet a strong theoretical understanding or extensive practical experience with it. Additionally, the implementation of machine learning can vary significantly from one project to another.

    One potential issue with machine learning is that it can result in AI algorithms that behave in ways that developers do not expect or desire. This can require significant debugging efforts to address and prevent these unintended behaviors. There are also challenges related to training time and the need for diverse environments to create dynamic agents, as well as the risk of agents forgetting previously learned information during new training.

    It is worth noting that human-made algorithms will never come close to the capabilities of machine learning. However, given these considerations, it may be more suitable for small teams to use Utility AI rather than machine learning at this stage, as there may be hidden costs associated with implementing machine learning. Ultimately, the most appropriate approach will depend on the specific requirements of the project
     
    apkdev and lclemens like this.
  40. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Thanks to glamberous in the TMG forums, I now can use animation curves for showing and editing the curve directly in the editor!

    upload_2023-5-26_12-55-29.gif
     
    Last edited: May 26, 2023
    apkdev and Opeth001 like this.
  41. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Hey everyone - good news!

    I finally put my repo up on gitlab. It has a few things things left on the todo list, but overall it's usable at the moment and is working fine in my game. The repo has documentation and a working demo.

    One gotcha is that currently it is using Odin Inspector (a paid asset) in two places. When I find some spare time I'll remove the Odin stuff and replace it with something else. If anyone here wants to contribute just let me know and I'll grant you access to the repo or approve a pull request.

    https://gitlab.com/lclemens/lightweightdotsutilityai

    Cheers!
     
    Last edited: Jun 26, 2023
  42. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    I'm just getting near to pushing some code that will allow skipping of expensive input calculations based on low decision scores. However.... I'm debating on two different approaches. I implemented both of them, but only one is necessary so I must choose. Maybe some of you have some suggestions or insights?

    Stating the problem: You first score as many decisions as possible using all the "cheap" inputs like MyHealth, and then you need to run an expensive input function. If that suddenly results in a new high score, then other incomplete decisions can "short-out" because their score is so low that they can't possibly win. Then repeat. Any new score could possibly become the new high-score and knock other decisions out of the running. So if you truly want to minimize how many expensive inputs run, then you realize that it is imperative to alternate - score, then run an input function, score, then run an input function, score, then run an input... repeat until everything is scored. This alternation guarantees that any decisions can be knocked out of the running by a new high score.

    The two phase approach we discussed earlier in this forum thread doesn't alternate... so in theory you might end up running all expensive inputs functions even though the very first one knocked out the rest of the decisions, making it so that the remaining expensive inputs don't need to run. So while the two phase approach is better than a single phase, it does not truly minimize the number of expensive inputs that are run like Dave Mark's OOP execution of virtual functions would.

    One important feature that I am trying to support - we must allow the developer's input calculation systems/jobs to run ANYTIME during the simulation system group. It's important because we have no idea what kind of data the developer is collecting for his input. It might be some physics stuff so he must run his system in the FixedSystemUpdate group. It might be yanking stuff from the GUI and need to run in the main thread. It might be dependent on some third-party system and be required to run after or before a particular system. If I dictate exactly when in the frame each input-collection system runs, then it could really mess up system order dependencies.

    Me and a couple of other guys came up with several ideas, and eventually narrowed it down to two. Both approaches respect the principal that the developer's expensive input can run anytime during the simulation group.

    -------------------------------------- Approach 1 (One-Input-Per-Frame) --------------------------------------
    1. First score all the actions that only use "cheap" inputs at the beginning of frame 0. Mark one of the expensive inputs as "NeedsCalculation".
    2. Anytime during the SimulationSystemGroup of frame 1, a developer's expensive-input job will see that its input is flagged as NeedsCalculation and it will run.
    3. Before the next frame, the actions that used the recently completed input are scored. Some actions might get canceled if their scores are too low, which might cancel some inputs. A new input is marked for NeedsCalculation (if there is one).
    4. The process is repeated until all decision scores are complete (either via full calculation or skipped due to low score).
    This results in 1 expensive input being calculated per frame until the scoring is complete.

    Advantages:
    1. As a side-benefit, it automatically spreads out the expensive functions over different frames so they don't all get run at the same time.
    2. It's easy to implement.
    3. The only thing the developer needs to do is check the NeedsCalculation for their expensive input job.
    4. It uses the least amount of memory and slightly less CPU.
    5. Scoring always occurs in parallel with Burst and without random access.
    Disadvantages:
    1. One limitation is minor - if the expensive job is a multi-frame job, then it won't work. I think it's pretty rare for those types of jobs to arise.
    2. In the worst case scenario - a mobile platform, where the frame rate is limited to 30fps... If the Scoring is run at an interval of 0.25s, then there will be a maximum of 6 frames dedicated to expensive input calculations. So you're only allowed 6 expensive inputs in that scenario. If the developer lengthens the scoring interval to say 1s, then the number of allowed expensive frames increases to 30, but the agent response time might start to become noticeable to the player at that point. At the moment I only have 1 expensive input. Glamberous in the TMG discord initially had 1, but then he changed that job to an influence map so it must be calculated periodically anyway and now he's down to 0 expensive inputs. I highly doubt my final game will have more than 2 or 3 expensive inputs that are dependent on an AI action. If a lot of influence maps are used, that number gets especially small. So maybe that 6 frame limitation on mobile platforms is not so bad?

    ----------------------------------------- Approach 2 (Score-After-Run) -----------------------------------------
    1. First score all the actions that only use "cheap" inputs at the beginning of frame 1. Mark any actions that couldn't be calculated because they have missing inputs as incomplete. Mark ALL of the expensive inputs as "NeedsCalculation".
    2. One of the developer's expensive-input job will see that its input is flagged as NeedsCalculation and it will run. I won't know which one it is. When it finishes, it will call ScoreInput(inputId).
    3. The ScoreInput() function will fill in the input and then score incomplete actions. Some actions might get canceled if their scores are too low, which might cancel other inputs.
    4. The process is repeated until all decision scores are complete (either via full calculation or skipped due to low score).
    This results in an alternation of scoring and expensive input running. I also setup a counter for frames so that the developer can optionally run a particular input system in a different frame if they want (to distribute the expensive inputs over multiple frames).

    upload_2023-8-5_12-50-36.png

    Advantages:
    1. There is no limitation on expensive input count.
    2. Jobs that go over one frame don't cause a problem.
    3. Developers can optionally spread their input calculations out so they don't all get calculated in one frame.
    Disadvantages:
    1. It's slightly more expensive to run (maybe 5%?). Because I don't know which input will run, I can't keep my place in the scoring process so I have to visit every incomplete action score instead of just the current one.
    2. I have to do some extra book-keeping like counting input references, which results in more memory usage per agent (Around 15%?)
    3. It requires the developer to call the scoring function at the end of the expensive input function, which is not super difficult but it's one more thing to remember and it also assumes that the expensive input function is not part of a third party closed source library or something.
    4. The largest issue is that's its code is quite a bit more complicated than the previous approach's code. There's more bookkeeping with reference counting and temporary scores. It's more difficult to maintain.
    5. The developer could run a partial scoring in a single or the main thread without burst and/or with random access.
     
    Last edited: Aug 5, 2023
    Opeth001 and apkdev like this.
  43. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,001
    The main disadvantage I see with the alternation approach is that it makes everything random access which makes the scoring itself much more expensive. Then again, if you don't know how to make the scoring function tight and efficient, then you don't really lose anything.

    The one thing I'd be wary of is that if you oppose architecture on the user, it makes the system less usable. There's a beauty to allowing the user to compute inputs whenever and however they want independent of when scoring runs. And with a multi-phase approach, you can still provide feedback for additional culling.

    But for a specific game, honestly, it really depends on which performance/maintainability graph frontier point you like better.
     
    lclemens and apkdev like this.
  44. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Both of the approaches I listed "alternate". For the One-Input-Per-Frame approach, it is impossible to run the scoring with random access and it will always be Bursted and run in job with parallel threads. That brings up a good point though because in the Score-After-Run approach a developer could run the scoring at the end of an input system in a job in a single-thread or even in the main thread without burst. That part of the scoring part wouldn't get the benefits of burst or parallel threading. It's more of a "partial scoring" of only incomplete decisions at that point, so it's going to be a lot cheaper to run than a full scoring of everything. All the "cheap" decisions will still be scored with burst and in parallel. A developer would really have to go out of their way to do an expensive input partial-scoring without random access, but in theory it would be possible. The fact that the developer could run a partial-scoring without burst on the main thread and even with random access... that seems like an advantage for the One-Input-Per-Frame approach. I will add it to the list.

    All of the "cheap" inputs can be calculated anytime anywhere by the developer so there's no imposition of architecture with those. For example, an agent's health values change as units attack each other and it works that way regardless of what state the AI is in - making it a cheap input that is just a copy of the agent's current Health value. Even the "expensive" inputs can be run whenever and however they need to by the developer. I'm just providing an optional helper via the NeedsCalculation flag that lets a developer make them a little more efficient by skipping running if a particular agent's AI has no possible need to run it. Like if the AI is in retreat mode running away, it doesn't need to be constantly selecting a target to attack. But it's entirely optional - if the user has a better way of deciding when to calculate a particular input, then they can do that, and the input turns into just another "cheap" input. In fact that is what glamberous did when he switched his expensive input to use an influence map (which runs much faster, but it can't be setup to be skipped due to an AI not needing it).
     
  45. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
    Thanks for sharing this @lclemens and thumbs up for all your time and effort you put into this. However, I could not find a licence information (am I just too blind/stupid?). This makes using your package "difficult". Maybe you want to consider a free licence (like MIT)?
     
  46. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Ah yeah I suppose I should add that. I'll just use the same one that I used at the bottom of the readme in my AnimationCooker repo.
     
    exiguous and BelkinAlex like this.
  47. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    715
    Just to clarify for expensive input jobs....

    The first approach (1-Input-Per-Frame) looks like this:

    Code (CSharp):
    1.  
    2. // for every agent (preferably in parallel with burst)
    3. public void Execute(AiAgentAspect ai, ...other params here... )
    4. {
    5.         // Early-out if this agent's AI doesn't need this expensive input calculation.
    6.         if (!ai.NeedsCalculation(AiInputType.MyExpensiveOperation)) { return; }
    7.  
    8.        // Do expensive stuff for the current agent here...
    9.  
    10.        // Later at the end of the frame a separate scoring system will score this in parallel with burst.
    11. }
    12.  
    The second approach (Score-After-Run) looks like this:

    Code (CSharp):
    1.  
    2. // for every agent (preferably in parallel with burst)
    3. public void Execute(AiAgentAspect ai, ...other params here...)
    4. {
    5.         // Early-out if this agent's AI doesn't need this expensive input calculation.
    6.         if (!ai.NeedsCalculation(AiInputType.MyExpensiveOperation)) { return; }
    7.  
    8.        // Do expensive stuff for the current agent here...
    9.  
    10.        // This score function must be called at the end!
    11.        // Hopefully this job is parallel and burst... but it's up to the developer because they launched the job.
    12.        // This function is runs synchronously and updates the agent's buffer data immediately.
    13.        ai.ScoreInput(AiInputType.MyExpensiveOperation);
    14. }
    15.  
     
    Last edited: Aug 15, 2023
    exiguous likes this.