Getting Started
Prerequisites
Crystal AI has no external dependencies. If you intend to use it with Unity make sure that the API Compatiblity Level is set to .NET 2.0 (this almost equivalent to .NET 3.5) and not .NET 2.0 Subset.
Installing
A NuGet package will soon be available, until then, simply drop the CrystalAI.dll into your project directory and link to it in your favourite IDE.
Installing in Unity
Compile the Debug or Release version of Crystal AI and drop in the dll in a folder named Plugins in your Unity directory.
Running the Tests
The Crystal AI test suite CrystalAI.Tests depends on NUnit 3.5.0. If you are on Visual Studio you can use the NuGet Package Manager to install download it. Otherwise, you can directly download NUnit 3.5.0 from their NuGet page. You can find documentation on NUnit here.
Command Line Mono
To run the unit tests using Mono cd into the directory you have downloaded Crystal AI into and execute the following commands
MonoDevelop Version Bundled with Unity
The MonoDevelop version bundled with Unity, although has its problems, is fairly good and obviously has the added benefit of seamless integration with Unity. The good part is that Unity has, in their managed folder, NUnit. However, I have not attempted to run the unit tests with the Unity version of NUnit. The reason main reason for this is that it is somewhat outdated. That shouldn’t create difficulties, simply download NUnit 3.5.0, link to it (in the CrystalAI.Tests project) and you all the tests should run.
Visual Studio 2015
To run the unit tests from within Visual Studio, apart from NUnit 3.5.0 you will need to install NUnit3TestAdapter v3.6.0.
Quick Start
The main set of interfaces in Crystal AI are associated as follows. A IDecisionMaker has a IUtilityAI to perform its function. In turn a IUtilityAI contains a set of IBehaviours, these IBehaviours contain IOptions and those IOptions have one or more IConsiderations. When the IConsiderations that belong to an IOption “win” over other IOptions IConsiderations, then the IAction associated with that IOption is executed.
Another important interface is IContext. Implementations of this interface are project dependent and are meant to contain all necessary information for the AI to make decisions. The AI then obtains this context from classes that implement the IContextProvider interface.
Context
Create a new console project in your favourite IDE, add the CrystalAI.dll and add a link to it.
First we’ll need a context for our AI. Add the following class to the project
For this example this will also hold all information for the AI controlled objects, our “Toons” in this case. Next, we need a IContextProvider implementing class
With these classes out of the way, it is time to implementing the AI itself.
Considerations and Evaluators
The values within FooContext, in and of themselves, are meaningless. Namely, what does it tell you that a Toon has Hunger of 32, and how does this compare to Thirst and Bladder? To enable comparison between these values we implement the IConsideration interface, or more conveniently we can derive from ConsiderationBase.
Considerations rely on IEvaluators to translate a given value from the context to the range [0,1]. IEvaluators are essentially functions that have an arbitrary compact interval for their domain and their range is any subinterval of the interval [0,1]. How this conversion is performed exactly is subjective and depends on the application. Crystal AI has three concrete IEvaluators, which are flexible enough and can also be combined to create piecewise functions, see CompositeEvaluator. Currently the available evaluators are the following
- LinearEvaluator - Linear evaluator interactive plot
- PowerEvaluator - Power evaluator interactive plot
- SigmoidEvaluator - Sigmoid evaluator interactive plot
Before reading any further it would be good if you experiment with a few different values in the above interactive plots to get a general sense on how the three main evaluators in Crystal AI work. Note that for these plots the domain is limited in the interval [0,1] only to simplify the plot, that of course is not the case for evaluators in Crystal. The points labelled as “a” and “b” in the plots are exactly the same points used to initialise the evaluators, as we did in the BladderConsideration above.
The most important override in a consideration is
The Consider method uses the appropriate variable (or variables) from the context and with the help of its IEvaluator calculates a Utility. Utility is a struct that has a Value and a Weight and these are constrained to be in the interval [0, 1]. The Value is often evaluated directly using an evaluator. Given that every consideration has a Weight of its own, in most situations it makes sense to pass it to the Utility directly as seen above.
We’ll need two more considerations for our AI, a HungerConsideration and a ThirstConsideration, their implementations can be found in the Crystal AI quick start project.
Actions
Now that we have a way to transform information from the context to utilities next are IActions. All actions should extend ActionBase. Actions implement the problem dependent logic for the game. Once an action is selected the first method that is executed is OnExecute(IContext context)
, if there is need to perform additional operations then these can be performed in OnUpdate(IContext context)
. For an action to stop execution, either EndInSuccess(IContext context)
or EndInFailure(IContext context)
must be called. Once either of these functions is called OnStop(IContext context)
is executed where any final clean up actions are executed.
Now lets look at the implementation of the ToiletAction
There are three more actions defined for this example IdleAction, EatAction and DrinkAction
, all of which are similar enough so they are not reproduced here. You can find these here.
AiConstructor
Now that all considerations and actions for our example are in place it is time for all the pieces to come together into an AI. A convenience class created for this purpose is AiConstructor. AiConstructor has five protected abstract functions that are executed in order initializing the AI building blocks. Once actions, considerations, options, behaviours and AIs are instantiated in a concrete AiConstructor there is no longer the need instantiate directly any of the AI components. All components (i.e. actions, considerations etc.) are named and that name should be unique. The AiConstructor has some convenience temporary variables (e.g. A for actions, C for considerations etc.) and a checking function IsOkay(bool expression)
whose use is not required but is highly advisable since it throw an exception if the initialization process fails at some point which is useful for debugging.
First we create an instance of all actions and considerations. These are then stored in a IActionCollection and IConsiderationCollection respectively. Actions, considerations, options, behaviours and AIs all have their individual collections. These are created by the AiCollectionConstructor.Create()
method in QsAiConstructor
(see below). These collections are “weaved” together to create the AiCollection. Namely, the OptionCollection requires for its construction an and a . The BehaviourCollection needs an OptionCollection and the AiCollection needs a BehaviourCollection. In the abstract class AiConstructor these collections are assigned to the following protected fields
Actions
- IActionCollectionConsiderations
- IConsiderationCollectionOptions
- IOptionCollectionBehaviours
- IBehaviourCollectionAIs
- IAiCollection
By passing these collections to the actions, considerations (etc.) constructors a prototype of the newly instantiated object is passed to the appropriate collection. For actions and considerations in our example this is accomplished as follows
Options and Measures
Next in line are IOptions. As mentioned options can have one or more considerations and at least one action. If you have a look at the implementation of Option you will notice that there is a property named Measure
. This returns an IMeasure which is similar to the normal mathematical definition of a measure (see Wikipedia). However, to allow for flexibility not all implementing classes of IMeasure are measures in the strict mathematical sense but have their use. So what is it that implementing classes of IMeasure do? As was mentioned, Options have one or more considerations, and each of these considerations has a Utility. Now, if there is more than of these considerations, how do we decide which Options is best? That’s the job of these IMeasures. That is, they accept a vector of utilities and return a single floating point value. That value is assigned to the Utility.Value of the given Option, thus enabling comparison between options. At this point a warning is in order, some implementations of utility AIs by game developers, use what is called in Crystal AI the MultiplicativePseudoMeasure to perform this operation. From a mathematical point of view that doesn’t make sense. That, in and of itself, doesn’t mean that this pseudo-measure shouldn’t be used or that it is somehow inherently a bad practice. However, due to the fact that this pseudo-measure multiplies the elements of the vector of utilities, it exhibits some peculiar behaviour for vectors of different length. For example, ignoring the weight in utilities for the time being, let’s say that you have a vector of utilities that is (0.9, 0.9)
and another that belongs to a different option that is (0.92, 0.92, 0.92)
. Using the MultiplicativePseudoMeasure on these two vectors will result in a final value for the first equal to 0.81
and 0.77
for the second. This means that the first option will be selected ignoring the fact that each of its considerations individually have lower utility compared with the second option. This is counter intuitive, and because of this, personally I would avoid this pseudo-measure. Nevertheless, it appears to be in common use so an implementation is available for completeness. If you have insights on the reasons behind the use of this particular pseudo-measure in the game development industry I would love to hear them!
The IMeasure implementations available in Crystal AI are the following
Chebyshev
- See ChebyshevWeightedMetrics
- See WeightedMetricsConstrainedChebyshev
- See ConstrainedChebyshevConstrainedWeightedMetrics
- See ConstrainedWeightedMetricsMultiplicativePseudoMeasure
- See MultiplicativePseudoMeasure
The default for Options is WeightedMetrics which can be changed as O.Measure = new SomeMeasure()
. A note on the constrained versions of the Chebyshev and the weighted metrics measures. The constraint in these is that for them to return a non-zero result all the utilities in passed to the measure should be above a certain threshold, otherwise they behave exactly like Chebyshev and WeightedMetrics.
Behaviours
A behaviour in Crystal is a collection of options. This can be useful when characters in a game cannot, or does not make sense, to perform a particular set of actions in every context. For example, let’s say our character enters a pub, the pub
may give the character a new behaviour with the following options Take part in quiz night, Play a table game, Have a pint(!) etc.
, once our character exits the pub it makes little sense to keep evaluating those options.
Right, now that we have our AI, let’s put it to work! Mostly everything in the Main()
method should be straightforward. The new thing here is the Scheduler. This is internally has two priority queues, one the think cycle and one for the update cycle. Decisions are made during the think cycle and in the update cycle any OnUpdate(...)
methods of actions are executed. The Schedulers job is to balance the workload on the given thread and limit it to a given number of milliseconds. So for example you could create 400000
characters (“toons”) and the Scheduler will do just fine, just don’t forget to bool verbose = false;
is you set N
to such a high value. A warning! This doesn’t mean that the AI is processing all 400000
AIs per update, but that it processes as many as it can (or just the ones that need updating) in the allotted time. That said, Crystal can actually handle a fairly large number of AIs. In the upcoming weeks I’ll share some benchmarking results.
All the files for this example can be downloaded from here.. As Crystal moves closer to version 1.0 the in code documentation will increase accordingly. After version 1.0 we’ll be adopting semantic versioning.
Resources
If you’ve never heard of Utility AI and you would like to know more about it these resources should help get you started
- Behavioural Mathematics for Game AI by David Mark
- Introducing GAIA: A Reusable, Extensible Architecture for AI Behavior, by Kevin Dill
- Improving AI Decision Modeling Through Utility Theory by Dave Mark and Kevin Dill (Talk, GDC2010)
- Embracing the Dark Art of Mathematical Modeling in Game AI by Dave Mark and Kevin Dill (Talk, GDC 2012)
This obiously is not an exhaustive list, and it wasn’t meant to be. However, if I’ve missed your favourite Utility AI resource let me know.
Feature Requests and Bugs
If you find any bugs in Crystal AI, or simply have an awesome suggestion, we’d love to hear about it. After all, squashing bugs is fun! The best way to report a bug or a feature request is via the GitHub Issue system here. For discussing anything Crystal you can visit our forums.