About Me

About Me

Hello, I'm Charles Hollingworth and i'm a Games Developer in North Yorkshire and this is my portfolio, please browse through my Games and Technical Explorations. If you have any questions please feel free to reach out to me on LinkedIn

I have recently finished my final year in a Games Technology course and I am looking to start my career in games. Training in Performing Arts, Kitchens, Wearhouses, and Casinos has given me a wide range of skills, hobbies, and experience that i'd like to bring to my work in games.


Skills

C# Unity Git Linux Systems Vulkan DirectX Network Tooling Target Manager C++ Systems Administration Source PhyreEngine Steamworks Godot Flax Engine Unreal LARP Leather Work HEMA

Active/Recent Projects

Click on a project to see a code snippet or trailer!
Lampray

Lampray

Lampray is an open-source C++ Mod Manager for Baldur's gate 3 & Cyberpunk 2077 for the Linux Platform.


Burglar Battle

Burglar Battle

This is my final university project where the entire year of around 40 student collaborated to make a single game. I specifically crated the patrol & management systems for the AI and deployment to the Playstation platfrom.

Pullshot

PullShot Tower Defense

My current project is a Multiplayer Online Tower Defense based upon a map that can shift to create new paths changing the layout little by little every 5 rounds.



Norway

Dissertation: Network Communication Standards

My Dissertation project has been to create a network protocol built on top of UDP, this however was simplified down to the creation of a standard for data transference built on top of TCP due to time constraints. The TCP server in the example project is wrapped into a Shared Library made for the standard.

Jam Games

Click to play (most require a controller plugged in)!
Porks Must DIE

Porks Must Die 2600 (2023)

This Game Jam was to recreate a modern game in the spirit of the Atari 2600, this was a recreation of Orcs Must Die.

This House has Secrets (2023)

This House has Secrets (2023)

This House has Secrets was a hardware restricted jam to create an original game on an Intel Celeron NuC with integrated graphics.

Root Wars: The Thirst for Life (2023)

Root Wars: The Thirst for Life (2023)

This Game Jam was for GGJ's Roots theme. We also worked with a student from another course, in this case a Music Technology student who created all audio for the game

Warden's Teddy (2021)

Warden's Teddy (2021)

This Game Jam's theme was Lost and Found as part of GGJ. Searching through an abandoned prison complex to get the teddy lost somewhere inside, whilst the game is fully completable it is designed as a rage game.

Technical Exploration Tasks

Level Completability (2023)

Level Completability (2023)

This project was to try and create a tool to check if levels are completable by using Unity's built-in navmesh, with focus being on making the tool easy to use. By adding the checker into the scene and having a navmesh it makes sure that all areas marked with a checkpoint are reachable and that the end is reachable. It does this via visual indicators such as turning the most efficient movement path from green to red if a point is unreachable and making a very obvious beacon or red and white on that checkpoint.

Mass Agent Simulation (2023)

Mass Agent Simulation (2023)

This project was to learn about optimisation and task control in a more structured manner than in the Populus remake, each agent having 6 individual needs with modifiers to them such as social obligation and morality. Its a framework that allowed on fairly low end hardware to simulate up to 600 agents going about their days in the simulated town.

Populus Remake in DirectX (2022)

Populus Remake in DirectX (2022)

This was a group effort to recreate the game Populus in DirectX in a week, I was tasked with creating the backend to allow for control rebinding as our target platform was an arcade cabinet, although difficult to see in the demonstration video each pop, or human, will create housing on level ground and will create new pops.


Voice & Theater Projects

Mojo

Mojo (Grey Cardinal Studios 2019)

Mojo was a project run by a now dissolved company owned by me and Lewis Macey. During this project i took on many roles, as a Production Manager, Social Media Manager, Fundraising Manager, and many other roles. During this project we raised enough money to create a full production of the play Mojo by Jez Butterworth and perform it at the edinburgh fringe, even bringing in actor an actor from Los Angeles (Ceili Lang) and multiple from the East15 Acting school we created a notable show at the fringe with Bouquets & Brickbats calling the show a Gem.

Want

Want (2018)

In 2018 as part of the NT Connections Project, i was in a collective production of Want, as part of the collective we each brought ideas to the table for how to create our production and ended with a piece of Physical Theater that was performed in the National Theater London.


Powered by w3.css
×

Header

London

London is the most populous city in the United Kingdom, with a metropolitan area of over 9 million inhabitants.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Paris

Paris is the capital of France.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Tokyo

Tokyo is the capital of Japan.


×

Burglar Battle

Documentation has to the top of the page, Doxygen tags have been removed.

Patrol Points for the AI Systems.

Creating Basic Patrol Paths

  1. Create three empty game objects and attach this script to them.
  2. Connect them by setting the next patrol point in each respective inspector.
  3. Change the colour of the patrol by changing the colour of the first patrol point.
  4. Place a guard and assign their colour to the same as the patrol path, this is case-sensitive.
  5. Assign the guard's first patrol point and hit play the guard will now follow the created path.

Using Dynamic Link

  1. Create three empty game objects and attach this script to them.
  2. Connect them by setting the next patrol point in each respective inspector, do not set this for the last patrol point, instead enable dynamic link
  3. Dynamic link will attempt to connect to the closest PatrolPoint of the same colour, if this fails then it will target the nearest patrol point. It will attempt to do this upon game start.
using System;
      using UnityEngine;
      using UnityEngine.AI;
      
      [ExecuteInEditMode]
      
      public class PatrolPoint : MonoBehaviour
      {
          // An Enum used to define all possible types of PatrolPoint.
          public enum PatrolPointType
          {
              NO_TYPE = -1,
              POINT = 0,
              AREA = 1,
              COMPLEX = 2,
              SPLIT = 3
          }
      
          // Stop the patrol point from finding the next patrol point.
          [SerializeField] protected bool noNextPatrolPoint = false;
          
          // The Type of the patrol point, used largely in sub-classes.
          public PatrolPointType Type { get; protected set; }
      
          [SerializeField] private PatrolPoint _nextPatrolPoint = null;
          
          // Public getter for the next patrol point. This should not be set in script.
          public PatrolPoint nextPatrolPoint { get => _nextPatrolPoint; }
          [Space] 
          [Header("Dynamic Link finds the next patrol point. \nPlease make sure that the next nearest point is the intended target.")]
          [SerializeField] private bool _dynamicLink = false;
          
          // Each patrol path has a colour determined by its origin point, this colour is case-sensitive. 
          // It is not advisable to set this colour during runtime as it will only have an effect on the origin patrol point. 
          [Header("Will only take effect on the origin Patrol Point.")]
          [SerializeField] public Color patrolRouteColour = Color.black;
      
          // Start is kept private as we want our inherited classes to use their own.
          private void Start()
          {
              Type = PatrolPointType.POINT;
              Init();
              
          }
      
          /// Init contains continuity checking and the DynamicLink functionality.
          /// Init is used as the start function due to this class being inheritable.
          public void Init()
          {
              if (Application.isPlaying && _nextPatrolPoint == null && !_dynamicLink)
              {
                  // Patrols are expected to be a closed loop.
                  // This will attempt to automatically resolve with dynamic link, please check this patrol point's configuration.
                  _dynamicLink = true;
              }
      
              if (Application.isPlaying)
              {
                  GuardManager.Register(this);
              }
      
              if (noNextPatrolPoint)
              {
                  return;
              }
              
              if (_dynamicLink && Application.isPlaying)
              {
                  PatrolPoint[] potentials = FindObjectsOfType();
                  Array.Sort(potentials,
                      delegate(PatrolPoint x, PatrolPoint y)
                      {
                          return Vector3.Distance(x.gameObject.transform.position, gameObject.transform.position)
                              .CompareTo(Vector3.Distance(y.gameObject.transform.position, gameObject.transform.position));
                      });
                  foreach (var vPatrolPoint in potentials)
                  {
                      if ((vPatrolPoint.patrolRouteColour == patrolRouteColour) && (vPatrolPoint != this))
                      {
                          _nextPatrolPoint = vPatrolPoint;
                          break;
                      }
                  }
                  
              }
              
              
          }
          
      
      #if UNITY_EDITOR
          private NavMeshPath _examplePath = null;
          void OnDrawGizmos()
          {
              Gizmos.color = Color.black;
              if (_nextPatrolPoint)
              {
                  Gizmos.color = patrolRouteColour;
              }
      
              Gizmos.DrawCube(transform.position, new Vector3(1, 1, 1));
          }
      
          public void Update()
          {
              try
              {
                  if (_nextPatrolPoint is null) return;
      
                  _examplePath = new NavMeshPath();
                  NavMesh.CalculatePath(transform.position, _nextPatrolPoint.gameObject.transform.position,
                      NavMesh.AllAreas, _examplePath);
      
                  for(int i = 0; i < _examplePath.corners.Length - 1; i++)
                  {
                      Debug.DrawLine(_examplePath.corners[i], _examplePath.corners[i + 1], patrolRouteColour);
                  }
      
                  if (_nextPatrolPoint.patrolRouteColour != patrolRouteColour)
                  {
                      _nextPatrolPoint.patrolRouteColour = patrolRouteColour;
                  }
              }
              catch
              {
                  
              }
          }
      #endif
          
          
      }
      
×

Dissertation Project

Documentation has been removed, Doxygen tags have been removed.

using UnityEngine;
using System;
using System.Runtime.InteropServices;
using System.Text;

public class UnityHelper : MonoBehaviour
{
    
    [DllImport("libcepe.so")]
    public static extern UIntPtr GetArraySizeSecondary();

    [DllImport("libcepe.so")]
    public static extern void launch_server();
    
    [DllImport("libcepe.so")]
    public static extern void launch_client();
    [DllImport("libcepe.so")]
    public static extern void launch_secondary_client();
    
    [DllImport("libcepe.so")]
    public static extern void send_data(byte[] data, UIntPtr size);
    
    [DllImport("libcepe.so")]
    public static extern void send_secondary_data(byte[] data, UIntPtr size);
    
    [DllImport("libcepe.so")]
    public static extern IntPtr grab_data();
    
    [DllImport("libcepe.so")]
    public static extern IntPtr grab_secondary_data();
    

public static byte[] ConvertToByteArray(int id, T value)
{
    // Get the type of the value
    Type type = typeof(T);

    // Serialize the ID as a 4-byte integer
    byte[] idBytes = BitConverter.GetBytes(id);

    // Serialize the Unix timestamp as a 4-byte integer
    byte[] timestampBytes = BitConverter.GetBytes((int)DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds);

    // Serialize the type name as a variable length ASCII string
    byte[] typeNameBytes = Encoding.ASCII.GetBytes(type.AssemblyQualifiedName);

    // Create the header by concatenating the ID, Unix timestamp, and type name bytes
    byte[] header = new byte[idBytes.Length + timestampBytes.Length + typeNameBytes.Length];
    Buffer.BlockCopy(idBytes, 0, header, 0, idBytes.Length);
    Buffer.BlockCopy(timestampBytes, 0, header, idBytes.Length, timestampBytes.Length);
    Buffer.BlockCopy(typeNameBytes, 0, header, idBytes.Length + timestampBytes.Length, typeNameBytes.Length);

    // Serialize the type-specific data
    byte[] data;
    if (type.IsValueType) {

        data = new byte[header.Length + Marshal.SizeOf(type)];
        Buffer.BlockCopy(header, 0, data, 0, header.Length);
        GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
        Marshal.StructureToPtr(value, handle.AddrOfPinnedObject() + header.Length, false);
        handle.Free();
    } else if (type == typeof(string)) {

        data = Encoding.Unicode.GetBytes((string)(object)value);
        byte[] lengthBytes = BitConverter.GetBytes(data.Length);
        byte[] newData = new byte[header.Length + sizeof(int) + data.Length];
        Buffer.BlockCopy(header, 0, newData, 0, header.Length);
        Buffer.BlockCopy(lengthBytes, 0, newData, header.Length, sizeof(int));
        Buffer.BlockCopy(data, 0, newData, header.Length + sizeof(int), data.Length);
        data = newData;
    } else {

        throw new NotSupportedException("Type not supported.");
    }
    
    return data;
}

public static T ConvertFromByteArray(int id, byte[] data, out int timestamp)
{
    timestamp = BitConverter.ToInt32(data, sizeof(int));
    
    Type type = GetTypeFromHeader(id, data);

    // Deserialize the type-specific data
    if (type == typeof(string)) {
        // For strings, get the length of the UTF-16LE encoded byte array and deserialize it to a string
        int length = BitConverter.ToInt32(data, sizeof(int) * 2 + type.AssemblyQualifiedName.Length);
        string value = Encoding.Unicode.GetString(data, sizeof(int) * 2 + type.AssemblyQualifiedName.Length + sizeof(int), length);
        return (T)(object)value;
    } else if (type.IsValueType) {
        // For value types, create a pinned handle to the byte array and deserialize it to the value type
        GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
        T value = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject() + 8 + type.AssemblyQualifiedName.Length, type);
        handle.Free();
        return value;
    } else {
        // Throw an exception if the type is not supported
        throw new NotSupportedException("Type not supported.");
    }
}

public static int GetIdFromByteArray(byte[] data) {
    return BitConverter.ToInt32(data, 0);
}

public static int GetTimestampFromByteArray(byte[] data) {
    return BitConverter.ToInt32(data, sizeof(int));
}


public static Type GetTypeFromHeader(int id, byte[] data)
{
    // Create a byte array containing the ID, Unix timestamp, and type name bytes from the header
    byte[] header = new byte[8 + data.Length];
    Buffer.BlockCopy(BitConverter.GetBytes(id), 0, header, 0, sizeof(int));
    Buffer.BlockCopy(BitConverter.GetBytes(GetTimestampFromByteArray(data)), 0, header, sizeof(int), sizeof(int));
    Buffer.BlockCopy(Encoding.ASCII.GetBytes(typeof(T).AssemblyQualifiedName), 0, header, 8, typeof(T).AssemblyQualifiedName.Length);

    // Extract the type name as a variable length ASCII string from the header
    string typeName = Encoding.ASCII.GetString(header, 8, header.Length - 8);

    // Get the type from the type name
    Type type = Type.GetType(typeName);
    if (type == null) {
        // Throw an exception if the type is invalid
        throw new ArgumentException("Invalid type in header.");
    }
    return type;
}


public static String GetTypeFromHeader_String(int id, byte[] data)
{
    // Create a byte array containing the ID, Unix timestamp, and type name bytes from the header
    byte[] header = new byte[8 + data.Length];
    
    Buffer.BlockCopy(BitConverter.GetBytes(id), 0, header, 0, sizeof(int));
    Buffer.BlockCopy(BitConverter.GetBytes(GetTimestampFromByteArray(data)), 0, header, sizeof(int), sizeof(int));
    Buffer.BlockCopy(Encoding.ASCII.GetBytes(typeof(T).AssemblyQualifiedName), 0, header, 8, typeof(T).AssemblyQualifiedName.Length);

    // Extract the type name as a variable length ASCII string from the header
    string typeName = Encoding.ASCII.GetString(header, 8, header.Length - 8);
    return typeName;
}


}


    
×

Pullshot MapBuilder

Code in an early state.
 using System.Collections.Generic;
      using Unity.AI.Navigation;
      using UnityEngine;
      using Random = UnityEngine.Random;
      
      
      public class MapBuilder : MonoBehaviour
      {
          public NavMeshSurface x;
          [SerializeField] public Dictionary points = new Dictionary();
          public static MapBuilder MB;
          public bool ready = false;
      
          public bool main_menu_loop = false;
          public bool testDrop = false;
          public bool resetMap = false;
          void Awake()
          {
              if (MapBuilder.MB == null)
              {
                  MapBuilder.MB = this;
                  
              }
              else
              {
                  Destroy(this);
              }
              
          }
      
          public void startMainMenuLoop()
          {
              main_menu_loop = true;
          }
      
          public void beginGame()
          {
              resetMap = true;
          }
          
          public static void Identify(MapPoint val)
          {
              MB.points.Add(val.x +":"+ val.y,val);
              if (MB.points.Count == MB.points.Max)
              {
                  Debug.Log("Done");
                  MB.ready = true;
                  
              }
          }
      
          private int i_menu = 0; 
          private void LateUpdate()
          {
              if (MB.ready && main_menu_loop)
              {
                  i_menu++;
                  Pathfinder.PathfindingResult res;
                  Pathfinder.Pathfinding(MB.points[Mathf.CeilToInt(Random.Range(0, MB.points.YMax))+":"+Mathf.CeilToInt(Random.Range(0, MB.points.XMax))],MB.points["18:9"], out res,50);
                  if (res.sucsess) res.FreeDropPath();
                  Pathfinder.Pathfinding(MB.points[Mathf.CeilToInt(Random.Range(0, MB.points.YMax))+":"+Mathf.CeilToInt(Random.Range(0, MB.points.XMax))],MB.points["18:9"], out res,50);
                  if (res.sucsess) res.AlignPath();
              }else
              if (MB.ready && testDrop)
              {
                  Pathfinder.PathfindingResult res;
                  var telk = Random.Range(0, 20);
                  if (telk > 8)
                  {
                      Pathfinder.Pathfinding(
                          MB.points[MB.points.YMax + ":" + Mathf.RoundToInt(Random.Range(0, MB.points.XMax))],
                          MB.points["18:9"], out res, 50);
                  }
                  else
                  {
                      Pathfinder.Pathfinding(
                          MB.points[0 + ":" + Mathf.RoundToInt(Random.Range(0, MB.points.XMax))],
                          MB.points["18:9"], out res, 50);
                  }
      
                  if (res.sucsess) res.DropPath();
                  x.BuildNavMesh();
                  testDrop = false;
              }
              if (MB.ready && resetMap)
              {
                  resetAll();
              }
          }
      
          public void resetAll()
          {
              main_menu_loop = false;
              testDrop = false;
              resetMap = false;
              foreach (var point in MB.points)
              {
                  point.Value.Align();
              }
              
          }
          
          
      }