Saturday, November 15, 2014

AI and Parsing Data Files

This post is long overdue. For most of the past month I've been working on getting the AI from random attack to something coordinated and useful (in addition to other things).

The AI system is composed of a series of packages attached to a unit. Each package is composed of a number of conditions and actions. A condition is a simple query about the state of the game. Things like "are there enemies in the area?" or "am I wounded?" and many more are all available as conditions. In order for a package to be able to run, all conditions must evaluate to true. An action is a single thing that a unit might do. Something like finding a target, moving somewhere, or attacking something are all covered. Actions are run in order.

Each unit has a number of these packages as well as a package that does nothing (so that there is always a package that can be run). When the AI system goes to run for a unit, the packages are evaluated in order (so they need to be specified in a certain order). When a package is found that is capable of running (all conditions evaluate to true), then it is run. The last package (the one that does nothing) has no conditions and so it can always run and will be chosen if nothing more specific can run. When running a package, each action is evaluated in turn, in the order they are specified. Usually, a simple package will involve finding a target to attack based on various criteria, moving towards that target and then if able to attack (if the unit is next to the target and had the AP), the unit will attack.

A sample AI system for a unit would look something like this:

AI
    Range 3
    Name BasicEnemyR4
    Package
        Conditions
            PlayersInArea 1
        Actions
            GetNearest
            GetLowestHealth
            GetRandom
            GetDestination
            MoveTo
            Attack
    Package
        Conditions
            HasPreviousTarget
        Actions
            GetPreviousTarget
            MoveTo

This describes an AI system where the unit has a range (which controls how far a unit can look to find an enemy) of 3 with two packages. The first package is conditioned that there is at least one player with the area defined by the range. If there is, then that package will be chosen. It first gets the nearest enemy. If there are multiple enemies at the same distance, then it will narrow that list based on the remaining health of the enemies. If there is still a tie a target is chosen at random. Once there is a target, the unit will find the closest open point next to the target, move there (it might not make it if there it doesn't have enough AP), and then, if possible, it will attack the target. If there are no targets available, then the second package is checked. This is conditioned that the unit previously had a target. This means that the unit must have had a target picked out during the previous turn, which might happen if it moved toward a target and then the target moved away, out of range. If it had a previous target, then the first action will take the last known position of that old target, set it as the destination and then the unit will move there. This simulates a sort of search. Finally, if none of these packages can be run, then the empty package (which is added automatically and not specified in an AI file) is run.

Now, this brings me to the next big change. All AI systems, as well as information for unit stats and attack information, are all stored in text files now. This makes it a lot easier to change values. The AI file shown above is an example of what an AI file might look like. The files for stats and attacks look similar, but specify their own information, obviously. The parser is a small library written in F#, mainly for its pattern matching abilities. The biggest challenge was getting that to interface correctly with Unity. While, it would load the library correctly, it turns out that Unity's .NET runtime is not capable of inferring types from external F# libraries, specifically the var keyword would not work correctly. I make rather extensive use of var simply because it's easier to type and the type is almost always obvious from looking at the name and/or assigned value, especially for the parsing library because everything was wrapped in what is effectively a static class. So, in order to specify the class that represents an AI package, I'd have to write DarkHorseParsing.AIPackage (and that is after importing the namespace). Now writing that isn't really an issue, but when the code wouldn't compile, it took me a long time to arrive at the conclusion that var was the cause of my issues. In end, the code was fixed up, types were specified and now everything works just fine and our data files can be easily manipulated.

No comments:

Post a Comment