Art Thief vs. Guards (ATvG) is an interactive simulation game where you can watch competing AI agents attempt to fulfill their goals. The scenario is a thief in a gallery trying to steal a piece of art, while the guards try to catch them. The thief uses a utility system, and the guards use a behaviour tree system. This project was originally made in my fourth year of university, but since then I've re-done almost all of it from the ground up with what I've learnt since then. I wanted this project to be visually presentable, using my most up-to-date knowledge to write the cleanest and most well-structured code I could, and creating a relatively complex AI scenario. View the simulation online here.
ATvG takes place in an art gallery level, with many paintings and a few sculptures that can be targeted to be stolen. There's a maximum of four guards roaming the level, and one thief - these AI agents have sensory modules that can see and hear things relevant to them. The thief AI will attempt to get to the piece of art it's targeting; steal it (waiting a few seconds); and then make it to the exit, while avoiding guards by hiding and evading them if spotted. Guards will wander the level, occasionally performing other activities like looking at art or visiting the break room, giving chase if they spot the thief.
When coming back to this project to improve it, my first course of action was to make a plan of what I wanted to change from the original. Firstly, I wanted the behaviour trees to be more complex, and for the utility AI to have more options for achieving its goals. The original project was very basic, and the thief was often caught because it was poor at finding good hiding places and evading the guards. Secondly, I wanted the level design to be better to accomodate the scenario, as having a better level would enable better hiding spots, line of sight blockers, more interesting behaviour etc. Thirdly, I wanted the project to more clearly show to the user what was happening and visualise the AI's internal workings.
Behaviour trees in this project are data-driven, and so are split between the actual logic, and the editor data that defines the tree's structure. The XNode graph editor is used as a base for creating a visual editor of nodes that can be linked together, with each data node corresponding to its logic node. At runtime, the data tree is parsed by a factory class to create the behaviour tree, instancing the nodes and passing their data.
Pure behaviour trees always run from the root node, but for my implementation I wanted to optimise the tree evaluation to make larger/more complex trees less costly to run. This project's behaviour tree uses a running stack to keep track of currently running nodes. For example, a Wait node would be stored on the top of the stack and run directly, instead of the tree evaluating from the root every update. To complement this design choice, I also created a Monitor node, which works like an interrupt for the tree. While in the running stack, the Monitor node will check a personal node, and if it's false then the Monitor node will kick the tree out of its current branch and wind back up to the Monitor node. This means trees are still able to instantly switch branches based on logic further up without forfeiting the stack optimisation. This is used to great effect in the guard agents, where when a guard changes mode e.g. from Passive to Alert, this switch can happen instantly. Several of the behaviour nodes are also able to get and set data on blackboards to pass data around without requiring new code, along with basic parsing to set primitives and address other variables, including on other agents.
A utility AI system uses a set of actions that are scored with motives to autonomously choose the action to perform. For this project, this AI system is also data-driven, using editor resources for each specific action the thief can take, as well as the motives that are used by the actions. The motives are given keys for which values to get from the thief's blackboard, and then a curve that the value is mapped to that gets the motive's score. All these motive scores are added up to get the score for the action relevant to it. The utility AI has a list of its available action resources and uses them at runtime to create the action logic instances that perform the actual action. Using editor resources makes it easy to edit which motives an action should be using, and allows a designer to edit the score curves in realtime during gameplay to tweak the effect of different motives. For example, when improving the thief's ability to hide and evade the guards, it was very useful to be able to change how sensitive it was to its danger motive and observe how it behaved in different scenarios by adjusting the curves. At first it was too skittish and hid when a guard wasn't even a threat to it, so I lowered its hiding score on the smaller end of the curve to make it stick around longer before hiding. A later issue was that when trying to hide it would sometimes get stuck between hiding and evading if the danger value increased while getting to a hiding spot, so I added another motive that reduced the score for the evade action for a few seconds when it started hiding.
When designing a better level for the scenario, I created several scripts for populating the level with information and making it easier to design the greybox. Each room in the gallery has a Room script attached to it with references for the box collider describing its bounds, the doorways connected to the room, the room's patrol path, and its hiding spots. This component allows for agents to refer accurately to the different rooms in their AI systems e.g. the thief prefers hiding spots that are in the room it's already in, and the guards will patrol specific rooms under certain cirumstances using the room's patrol path. Rooms also have an ID string which I used in the editor to draw in the scene, making it easier to identify which room was which at a glance when wiring up connecting rooms etc. Another level element is doorways which are covered with a DoorwayArea script, using a box collider to describe the doorway's bounds, and two positions for either end of the doorway. This script is especially relevant to the thief and was added to enable the thief to be smarter about evading guards. When evading, the thief will evaluate the doorways out of the room it's currently in for how risky they are (how close a guard is to the door etc.), and choose the least risky one that leads to a room it hasn't just been in. I also added a useful editor button to the Room script, which scanned for doorways connected to the room and listed them automatically, which was a very useful time saver when greyboxing. Hiding spots are handled by the HidingArea script, and placed in the level manually to mark spots the thief could hide in. These spots are placed manually because only some potential areas are actually a good idea for the thief to hide in based on other doorways and line of sight blockers. This script is also used by the thief to check whether the hiding spot is safe to use, checking for things like whether guards are looking at it. To aid the thief in avoiding the guards, the level has a select few guaranteed safe hiding spots, giving the thief a potential place to always escape to and lose the guards. To achieve this I used the off mesh link functionality of Unity's navigation system to place points where the thief can navigate to a sectioned off part of the navigation mesh, and disallowing the guards from using those links by excluding it from their navigation agent mask. The default link traversal was also unsatisfactory to me, so I added some manual handling of the links in the thief's code so that it would traverse the links at the same speed as was set on its navigation agent.
The AI systems and blackboards are able to be viewed at runtime, visualising the current AI state logic. The utility system was simple to visualise, just being a list of the actions and their scores, but the behaviour trees ended up being a difficult task to present and went through several iterations. My first attempt used lots of horizontal and vertical layout groups to automatically generate a tree graph using the logic tree from an agent. Unfortunately, this iteration was both difficult to look at and understand what was happening, but also very hacky. The way Unity's layout system works means that these groups wouldn't expand themselves correctly on their own, and required manual prompting to lay themselves out properly. This could easily bug out inside the scroll view the graph was in and cause issues too, so I was unhappy with this result and gave another crack at the problem. I did a couple more iterations of the automatic graph creation idea, but couldn't find a satisfactory way to do it, and so eventually relented and used the editor data to create the visualisation graph. I'd been reluctant to do so, but the data nodes don't just have data pertaining to the behaviour nodes, but also the node positions in the editor graph. I used these data nodes to align the UI nodes to a grid and draw lines etc. between them. This iteration I also added more information to the UI nodes, with realtime updates on things like timers and counters. To facilitate this I added an interface class for coupling nodes together using GUIDs - the editor data node, UI node, and logic node that were all related would use the same GUID, allowing me to easily grab the references I needed. This version of the graph was better, but I still wasn't happy with it. It used the grid layout group, and to place the actual node UI blocks in the right place, it required many blank objects to fill in the gaps of the grid. This created performance issues, especially when trying to view a whole graph at once, and so for the final iteration I wanted to resolve this. I re-created the grid layout component's behaviour in a new component where it would replicate the layout to place the UI blocks I gave it in the correct position without requiring extra blank objects. This allows the tree visualisation to view a full graph without a significant performance hit.
This project was very multi-disciplinary for me, as it wasn't just a programming project, but one where I also tried my best to make it visually presentable, added audio, tried to design a reasonable level etc. I learnt a lot about my own strengths and limitations, my approach to workloads and where I get stuck, as well as where I excel. I'm happy with how the AI's themselves turned out, I think they both act fairly smart and all the different systems work together better than I had hoped. I learnt a lot about Unity's scene drawing and gizmos functionality, using it to draw useful things in the editor scene while I was working, as well as deepening my knowledge on Unity's collision system when working on the senses for agents.
Were I to do this project again, I'd spend less time on the visuals of the project. While I wanted it to be presentable, I spent too much time polishing the project with a lot of work for not a lot of gain. For the behaviour tree graphs I should've appreciated sooner how having one big graph per tree was a poor approach, and made it so that I could have multiple smaller graphs that linked together. This would've helped both with logic and with editor performance, as the editor was very laggy when working on a large graph. I also wish I'd spent less time on the behaviour tree visualisation, as while I'm happy with the result, I should've been less stubborn about the solution and finished it quicker. The result wasn't worth the amount of time spent figuring it out.
Overall, I'm still very happy with how this project turned out, especially in regards to code quality and optimisation where I gave great consideration to the architecture and readability. Project planning also helped this project run much smoother even with the problems I did face, so I'm glad I made the effort to plan the project first before re-tooling it.