
About Me
Hi I'm Benjamin Morris MSc, BSc (Hons), I am primarily a games programmer, with a varied skillset capable of C++, C#, DirectX11 and Unity and Unreal Engine Development. I also specialise in Artificial Intelligence development, being well versed in both machine learning, game AI, and applying machine learning techniques to games. I am a University Graduate with a Distinction in MSc Games Programming (by negotiated study) and a First Class Honours in BSc Computer Games Design and Programming
Portfolio
Below is some of the projects I have developed, the examples show projects within Unity, Unreal Engine, C++ and C++ using DirectX 11, some of the examples are quite long so where possible they have been edited down to a snippet of gameplay in different sections.

During development my role was to help keep communication flowing throughout the different departments, help and give inputs with decisions and I also assigned tasks to developers using Jira - mainly to the programmers, and made sure people were updating and documenting their process. I also spent a lot of time with the junior programmers (level 5 programmers) helping them fix issues and bugs and teaching them good practises.
My contributions to the project were the Health Systems, Checkpoint System and all of the AI in the game. I made a lot of different AI agents utilising the Behaviour Tree system, I expanded the functionality of the system to support randomisation of nodes using decorators and created randomised timers which could change behaviours after so long using simple parallel nodes, this allowed me to create - for example, attacking movements which could random between having the AI moving straight towards the player, then move around them or wait based on different weighted odds. All of the blueprints that require tick use timers instead to reduce overhead, and programming interfaces are used for any type of blueprint communication to reduce coupling.

My contribution to the team was centred around AI, projectile, utility and health systems. I developed an object pool system which was leveraged by both the weapon systems and the enemy spawners; this allowed for optimal spawning of various game objects as no memory was being allocated at runtime, which was important given the number of objects spawning in as the game progresses. The developed health system allowed for drag and drop of a component providing instant health related functionality. The projectile system was utilised for both the player and the enemies, it leveraged the object pool to efficiently spawn projectiles when required, each enemy had a weapon script which fired each projectile; with stats of each weapon being handled within a scriptable object.
The game had 3 enemy types which I designed and developed, they were implemented using a Finite State Machine approach; this was used as it is a simple architecture, allowing for efficient development while sustaining the simple behaviours each enemy required. The Finite State Machine system was created as a library allowing for further use in other projects if required.
Collaboration on the project was performed using GitHub, given the small scale of the overall game and team - no source control related issues ever emerged.

The Genetic Algorithm attempts to evolve solutions to land the spacecraft, where it is required to land below a target speed, orientation and at a set landing pad. Chromosomes contain real values representing button inputs and the duration of each press - which controls the lunar lander.
To evaluate the fitness of each chromosome, the fitness function considers the speed, rotation and distance to the landing zone. Each component of the fitness score is scaled is based on a multiplier, the distance is scaled the highest, then the speed and rotation are scaled less; this is so chromosomes which are closer to the landing zone are favoured. The speed and rotation multipliers are scaled by the distance, so the closer to the landing zone, the more influence they have on the final fitness value.
The Genetic Algorithm utilises Roulette Wheel and Single-point crossover for evolution, and a mutation operator which randomly selects a chromosome then a random gene where the values within are regenerated. Elitism selection can also be used when set on the initial level load.
The algorithm is also capable of tracking when and if the population is stuck in local maxima and can reset half the population to attempt escape after so many generations when set. It can also output the current progress to a file to load later and output the current stats of the population to a .csv file for loading in Excel.
Issues that needed to be overcome when creating this system was creating a Genetic Algorithm that was adaptable, as the framework and system is intended for future use as a building block for further development. To make it adaptable and modular, chromosomes act mostly as a generic container class which contain objects of type T where T is a gene, as genes consist of an abstract base class with a small interface for creating random starter values; derived classes can then implement the required encoding. The main Genetic Algorithm class is also built using object orientation with an abstract base declaring the interface that all future algorithm implementations will need, alongside any common functionality. Derived Genetic Algorithm classes will only need to implement problem specific code such as what gene type the chromosomes require, what evolution operators to use and the fitness function. As a benefit of this design, it means that evolution operators can be part of a single library of code and can work interchangeably with any future system as the chromosome class is all that is required for selection and evolution; if specific functionality is required in the future for evolving chromosomes, then genes can have relevant interfaces placed into their derived classes if necessary.
The current implementation can generate successfully landing chromosomes at a varying rate between runs, but typically between 20-60 generations. Other operators such as Tournament selection and Two-point crossover have also been tested and they can result in faster generation between 10-50.

The Fuzzy State Machine operates using standard Fuzzy Logic behaviour of multiple firing states, with the activation of a state being based on a Degree of Membership (DOM) value. To execute these states - which correspond to a behaviour, each one contains a specific equation calculating its DOM value. To allow for genetic manipulation, each DOM calculation contains a weight value, this can influence an agent's behaviour to be more partial to certain behaviours over others; with the Genetic Algorithm altering this value to achieve optimal decision-making performance.
The Genetic Algorithm system is based on my exhibited Lunar Lander Genetic Algorithm, it utilises and extends the library built – where required, to be more suited to this frameworks requirements. The chromosome structure consists of genes containing a float value - representing the multiplier used in the DOM calculations. Solutions evolved based on a fitness score considering total destroys, total damage dealt and survival time per life, weighted in favour of total destroys, allowing for agents which are more prolific in achieving high scores; however, it could it be altered, allowing for creation of many different AI personalities, such as more/less aggressive natured AIs, or a preference to move more independently instead of with their team.
The evolution stage was tested with over 30 different combinations of techniques as per our research, where it was intended to determine an optimal Genetic Algorithm setup for real value solutions. It was found a Simulated Binary Crossover, with Tournament Selection and a basic single gene randomisation for mutation was the most optimal setup; which incidentally coincided with our findings that – less destructive evolution operators provided more optimal evolution. The tested techniques were as follows: for crossover – Single-point, Two-point, Simulated Binary, for selection – Roulette Wheel, Tournament Selection, Stochastic State, Elitism, for mutation – Real-value, Real-value across the length of a chromosome, randomisation of a single gene, randomisation across the length of a chromosome; fitness scaling was also tested, as were other selection and crossover techniques in our initial testing, but the ones chosen were more effective in preliminary experiments.
The Fuzzy State Machine had 6 states, Attack, Distant Attack, Team Attack, Find Cover, Find Health and Flee. These provided ample behavioural complexity, often displaying types of emergent behaviour, while exhibiting enough variety to show a noticeable change in behaviour during evolution.
For navigation, a steering behaviour system was developed for movement, with it featuring Seek, Flee, Pursuit, Evade, Obstacle Avoidance, Separation, Alignment and Cohesion; the steering behaviours utilise a neighbourhood system, allowing for friendly and enemy tanks nearby and in certain directions to influence these movements. A* pathfinding is used for when an agent does not have line of sight to its movement position; a Dykstra search is also present and uses the pathfinding grid, however this is leveraged for the covering system, allowing a covered point to be found.
A large portion of framework development was devised to optimisation, this is because the Genetic Algorithm evolved solutions in real-time, so to avoid pauses and slowdown during these operations several supporting systems had to be developed; alongside this, the cover system required a large overhead, and the fact the Fuzzy State Machine executes each state simultaneously if required, both exasperated this issue. The first major optimisation was a pathfinding manager, which worked by having agents send off to the manager to queue for a path, when first queued, a fast - partial path is generated, which is used until the main path is created. Paths are constructed over a period of frames, with the manager allocating a certain amount of frame time to generating paths for a set number of different requests; when the frame time elapses, it is continued on the next frame. This system works for both the Dykstra cover search and A*, allowing them both to use the same queue, as they utilise the same interface and share the same base class hierarchy; this means a temporary covered tile can be quickly given to the Dykstra request, while a more suitable alternative is located. As mentioned, the cover system also required extensive optimisation, this is because each tile has a generated cover value which needs to be frequently updated. To mitigate this impact, a spatial partitioning system is used, meaning only AIs within a certain partition are considered for covering checks, minimising the amount of checks and calculations required per tile; furthermore, only so many tiles are checked a frame, with tiles only updating every fraction of a second if- and only if- all tiles have been updated on this loop. The agent's aiming and targeting system was also set to only evaluate so many tanks per frame to limit its impact, with all agents only updating every 0.15 seconds. Finally, when saving the Genetic Algorithms progress, the save file is generated asynchronously on a seperate thread.
There was another issue, but it was not performance based, the Fuzzy State Machines behavioural execution could lead to states fighting over steering behaviours, leading to incorrect and unintelligent movement. To get around this, a priority system was created, this works by having states with higher DOM values being able to alter and utilise the steering behaviours; for example, if Find Health is using Seek, but Team Attack has a higher DOM value and is also trying to use it, Team Attack takes priority altering Seek until a different state with a higher DOM uses it.
The Genetic Algorithm was capable of developing agent that can outperform hand tuned alternatives in less than 50 generations.

Chroma's main mechanic is what I've coined the Paint Mechanic, this allows the player to 'paint' monochrome parts of the level. Monochrome tiles will damage the player on contact, 'painting' these tiles with a variety of weapons the game offers will provide score and a way to progress.
From a technical standpoint this game heavily utilises coroutines, they are used to split heavy processing such as the paint mechanics tile colouring, AIs behaviours and bomb hazards between multiple frames to reduce CPU overhead. Other tricks such as using triggers to enable enemies when the player gets in range, then checking to see if the player is within the trigger and the enemy on screen before disabling them again to optimise performance is used, these triggers are also used for all the level hazards and moveable platforms; this set up allows the game to have everything loaded but only processed when needed reducing memory allocation at run time.

This game has the player control a single character starting off with 3 AI characters accompanying them, as the game progresses more AIs will join the players team to create a large attacking force; the player is able to switch between every AI and possess them whenever they want.
The aim of the game is to progress up the map and take over villages and castles, villages get taken by defeating the enemies that are present within them, castles require a flag to be captured by having friendly AI within a capture radius while being the majority team within it, while capturing waves of enemies will keep appearing based on time and how many enemies are currently in the castle, friendly reinforcements will also appear.
The game is won once all of the castles are taken over.
This game utilised the Behaviour Tree system within Unreal for all of the AIs.

SDL was selected for graphics rendering due to its simplicity, ease of use and its effective input handling system and cross platform capability if required; furthermore, while not leveraged here - it also allows for OpenGL or DirectX integration allowing for advanced rendering features.
The games architecture utilises reusable components, with a rendering system capable of shapes, shape outlines, text and textures, alongside a level management, audio and input system.
Event driven programming was heavily utilised, with inputs and gameplay logic leveraging an observer pattern, which optimises many of the gameplay systems functionality, as it is only executed when required - limiting if statement checks.
To optimise memory usage, the audio, text and texture rendering systems were developed to only load one instance of a type, for example, if two text renderers required the same font, only one font is ever loaded, with the two sharing the font; this reduces the need for duplicate texture, sound and font loading - saving a significant portion of memory.
It also supports multiple different levels/screens, with the game starting on a title level, before switching to the main game on an enter press.
All the core engine systems i.e, graphics, input, sound and level management, were developed to allow for use in future projects as they do not contain game specific code; moreover, given their design, they can also be effectively expanded upon depending on project requirements.

To evolve Decision Trees, Genetic Algorithm Chromosomes are encoded with an entire tree, with individual nodes making up each gene. Decision nodes are assembled by retrieving a random feature from a dataset, with leaf/classification nodes, being given a random classification. Before evolution, the Decision Trees are randomly assembled with an initial max depth of 2, to encourage smaller trees; once the limit has been reached, the build process forces these nodes to be of a classification type. Unique to this Genetic Algorithm compared to my other examples, this features variable Chromosome sizes that can increase/decrease depending on the evolution outcome.
When evolving, the Genetic Algorithm uses Roulette Wheel Selection, Elitism and two developed tree-based techniques – Single Branch Crossover and Single Node Mutation. Single Branch Crossover works by selecting a node within two separate trees and swapping the two. Whereas Single Node Mutation, selects a random node in a tree, and randomly changes it; this could be just altering its attribute or classification, or completely changing the node type altering the tree size.
To evaluate a chromosomes fitness, a classification score is created by running the training dataset through each individual, its final classification percentage alongside a Decision Trees overall size is considered in the final calculation; this is to prefer smaller, more efficient trees.
The algorithm was capable of achieving 95-100% accuracy in the datasets we tested, with stats for each generation being output to a .csv file for loading in Excel. It should be noted that any .csv file can be used, so long as the top row is set to attribute titles, and every row below is corresponding data; with the final column being the final classification of the data row.
At the end of a simulation, the highest fitness Decision Tree is output to a file for saving, this can then be viewed in a separate tool developed for displaying Decision Trees created by the Genetic Algorithm. This application was developed in C++, using the ImGui and ImNodes Library.
The Genetic Algorithm was created as an adaptable system, capable of translating to many other challenges and issues, so it could be potentially reused for alternative work if required. The system leverages a base Genetic Algorithm class which houses a relevant interface and functionality all potential implementations will require; and a chromosome template class, housing a vector of type T, allowing for scaling chromosome sizes, while containing any required data, rather than limiting it to a gene class hierarchy.
This Decision Tree implementation utilises binary trees, it was created with the intention of being game ready, allowing for future testing within games. The system has a built in Blackboard architecture, which is used by Decision Nodes within the tree, to compare their values to those within the Blackboard. Within this project, all decisions are Boolean based, however, the system has the capability to handle numerical values; the system can and will compare numerical values and can handle attributes with more than true/false descriptions, but the decision nodes will only compare for equivalency. The Blackboard system is capable of all inbuilt types - including void* for objects, and std::weak_ptr<void> for managed objects. The decision to only use Boolean comparisons for this project testing, was made based off research of other systems, and due to limited time constraints.
The developed system is capable of self-optimisation and pruning, as if it creates a smaller tree with a comparable accuracy, it will class the smaller tree as the top performer; as trees with fewer nodes can be traversed more effectively than larger ones.
There were a couple of issues that needed to be overcome during development, one issue being - viewing the final constructed tree. To get around this issue, a GUI application was developed to read and display the final output tree; this program can display many different trees at a time, allowing for comparisons between potential solutions. Another was execution speed, to create optimal solutions large datasheets may require population sizes of thousands to create effective results, which can be slow to simulate. To get around this, the std::async library is used to simulate all the decision trees asynchronously, which dramatically speeds up execution time. We tested on several different devices, a modern i7 11700H in our testing could simulate a population of 500-2000 on variously sized datasets in less than a second, whereas a 7-year-old i5 7500, could effectively handle a 250-500 sized population in similar scenarios – the provided video shows the simulation on an i5 7500. Another issue is too large trees could cause issues with the evolution stage by overflowing ID values required for evolution; to solve this, depth checks were implemented to ensure trees could not surpass a max depth of 20 – which sufficed for our testing.

The Behaviour Tree system consists of Composite, Action and Decorator nodes that can be utilised for AI decision making. The Composite nodes comprise of Sequence and Selector nodes for controlling the flow of execution through the tree. Decorator nodes allow for if/conditional checks and to loop behaviours for a set number of attempts. The Action nodes are up to the use case and requirement of the implemented project. The system also features a Blackboard system that can hold all inbuilt types and be accessed using a string key like how the Unreal Engine allows for Blackboard data setting/getting.
The Behaviour Tree Editor allows for the construction of the trees using a node graph utilising the ImGui and ImNodes Libraries. The editor works by having nodes attached to each other to form branches, the higher the node is in relation to its parent and other branches, the earlier it executes. Nodes can have decorators applied to them by right clicking and selecting the required one, from here the options for these decorators appear on the right-hand window allowing for its settings to be altered. Action nodes can be added by going into options and adding the correct Action node name - for it to be used in the editor, the editor only requires non decorator nodes name to function as they can be loaded from only the name in both the main system and in the editor. Action node names can be output to a file for loading in next time or across different projects.
The editor allows for having multiple windows and trees loaded at once, they can be accessed via the tab at the top of the editor panel, where they can then be dragged away to allow for multiple windows. Save files can then be created or loaded using the Windows file dialogue box for easy saving/loading for the user, with the save file acting as a single solution for both loading in the editor and in the behaviour tree system.
Issues that arose during this project mainly relate to limitations with C++ or issues with ImGui and ImNodes. The first issue was getting over the lack of reflection within C++ so classes cannot be loaded with just a name from a file. To resolve this a factory pattern was created, with classes registering themselves to a dictionary alongside a string name key which will allow them to be accessed, the factory method would return a new instance by a virtual function within all nodes which returned a new instance of the same type; this allowed for node names within files to load the correct node. ImNodes and ImGui had issues with UI widgets being used on nodes, ImGui widgets did not often scale properly to ImNodes widgets, this led to no selection and hover effects as these would fly off the screen instead of cutting off at the edge of the node. The decorators also had to be apart of a separate node which followed the main node around, with the decorators being represented by a string of text on the node, as having them on the decorated node would cause stretching and visual problems.

There are two pathfinding algorithms showcased; A* and Dijkstra, these are both also capable of perpendicular only movement and not cutting corners when required.
Finally to show these off in a practical example a finite state machine AI had been set up utilising different steering behaviours and pathfinding. The state machine works by having the AI look for the player, alerting other AIs if any are within the radius then returning to where they first found the player. The AIs will enter a fight state when close enough to the player where they have a chance to either attack or dodge. When an AIs health is low it may run away to find health, in this instance if they locate an idle AI they will alert that AI of the players position which if they are not on low health will run to where the AI last saw the player. Once the evading AI has health it too will move back to the location it last saw the player.
These AIs will switch between steering behaviours and pathfinding if there is no line of sight to their target location.
In this project the players health is capped at 1 as the AIs behaviours alter slightly when the player is on low health. The AIs attacking and dodging odds will change to make them appear a bit more erratic and they have a chance to not run away and look for health while the players health is low when fighting.

This project features normal mapping, simple parallax mapping, parallax occlusion mapping and self shadowing, a render to texture to produce an object textured with the scene, the render to texture is also used for a screen quad for post process effects; a depth of field blur and a screen tint.

This project features a per pixel lighting local illumination model, a camera class which is capable of linear interpolation around set points, rotating around and zooming into targets, alpha clipping and blending is present as is billboarding, finally cell generation to create a grid of a set size has also been implemented.

The framework features a point mass physics system with a thrust force, friction with laminar and tubular flow resistance alongside gravity. Rotational physics have been implemented using a quaternion based system, this features angular drag and acceleration.
Collision is also present with a narrow phase implementation featuring a axis aligned bounding box and a bounding sphere, when two objects collide an impulse will occur with the forces taking into account the speed, weight and normal of the collision.