Titanium Core

Titanium Core

Project Details


Production Time - 7 weeks, full time

Team Size - 12

Role - Lead programmer

Responsibilities - AI, Level Editor, Gameplay

Engine - Unity

Pathfinding


Our player mechs and the AI needed to be able to traverse a grid, to achieve this I implemented pathfinding using the A* algorithm. 


This worked fine for the player characters since we only had to find a path for every grid cell that the player hovers over with the mouse. For the AI however we needed to find a way to quickly present a whole bunch of paths so that the AI could quickly make a decision of where to go.


The way we solved this was essentialy to save all available paths for the AI in a file so that information could quickly be referenced without having to find a path to every cell on the grid during runtime.

Game Description


Titanium core is a tactial squad based combat game. Set in an alternate future where roman empire inspired robots clash with the players celtic inspired mechs.


The game plays like a turn based tactical action game in the vein of X-Com or Mutant. We hoped to achieve gameplay where the player wanted to use the different player controlled mechs unique abilities and movement patterns to get a tactical edge on the AI controlled opponents.



    public List<GridCell> AStar(GridCell startGridCell, GridCell targetGridCell, bool ignoresCovers = false)
    {
        //Evaluated nodes
        HashSet<Vector2Int> closedSet = new HashSet<Vector2Int>();
        //Unevaluated discovered nodes, default only start is known
        HashSet<Vector2Int> openSet = new HashSet<Vector2Int>{startGridCell.Position};
        //Where each node came from
        Dictionary<Vector2Int, Vector2Int> cameFrom = new Dictionary<Vector2Int, Vector2Int>();
        //G score map, cost of getting to node from start, default value infinity
        Dictionary<Vector2Int, float> gScore = _grid.Cast<GridCell>().ToDictionary(cell => cell.Position, cell => float.MaxValue);
        //F score map, gCost + heuristic cost, default value infinity
        Dictionary<Vector2Int, float> fScore = new Dictionary<Vector2Int, float>(gScore);
        //Start has g score 0
        gScore[startGridCell.Position] = 0.0f;
        //Start has f score as pure heuristic
        fScore[startGridCell.Position] = Vector2Int.Distance(startGridCell.Position, targetGridCell.Position);

        while (openSet.Count > 0)
        {
            //Current node is the node with the lowest f score present in the open set
            float lowest = openSet.Min(c => fScore[c]);
            Vector2Int current = openSet.First(node => Mathf.Approximately(fScore[node], lowest));
            //If we are at the target position, reconstruct the path
            if (current == targetGridCell.Position)
                return GetPath(current);
            
            //Current node is set as evaluated
            closedSet.Add(current);
            openSet.Remove(current);
            
            //Iterate over all neighbouring cells
            List<Vector2Int> neighbours = GenerateNeighbours(current);
            foreach (Vector2Int neighbour in neighbours)
            {
                //If node is already evaluated, skip it
                if (closedSet.Contains(neighbour))
                    continue;

                //Cost between nodes is hard set as 1.0f for now
                float tentativeGScore = gScore[current] + 1.0f;
                
                if (!openSet.Contains(neighbour))    //Newly discovered node
                    openSet.Add(neighbour);
                else if (tentativeGScore >= gScore[neighbour]) //Lower cost path exists
                    continue;

                cameFrom[neighbour] = current;
                gScore[neighbour] = tentativeGScore;
                fScore[neighbour] = tentativeGScore + Vector2Int.Distance(neighbour, targetGridCell.Position);
            }
        }
        return null;

        List<Vector2Int> GenerateNeighbours(Vector2Int node)
        {
            Vector2Int right = node + Vector2Int.right;
            Vector2Int left = node + Vector2Int.left;
            Vector2Int up = node + Vector2Int.up;
            Vector2Int down = node + Vector2Int.down;
            Vector2Int[] neighbourPositions = { right, left, up, down };
            return neighbourPositions.Where(position => position.x >= 0 && position.y >= 0 && position.x < GridSize && position.y < GridSize && 
                                            (GetCell(position).Type == 0 && !GetCell(position).Occupied || ignoresCovers) ).ToList();
        }
        List<GridCell> GetPath(Vector2Int node)
        {
            List<GridCell> path = new List<GridCell>();
            Vector2Int current = targetGridCell.Position;
            while(cameFrom.ContainsKey(current))
            {
                path.Add(GetCell(current));
                current = cameFrom[current];
            }
            path.Reverse();
            path.Add(GetCell(node));
            return path;
        }
    }

    


    public static List<(GridCell, int)> GetCellsInRange(Vector2Int origin, int range, bool ignoreAvailability = false)
    {
        if(!OnGrid(origin)) return new List<(GridCell,int)>();
        HashSet<Vector2Int> closedSet = new HashSet<Vector2Int>();
        HashSet<Vector2Int> openSet = new HashSet<Vector2Int>{origin};
        Dictionary<Vector2Int, int> accumulatedCost = Grid.Cast<GridCell>().ToDictionary(cell => cell.Position, cell => int.MaxValue);
        accumulatedCost[origin] = 0;        
        while (openSet.Count > 0)
        {
            int lowestCost = openSet.Min(pos => accumulatedCost[pos]);
            Vector2Int current = openSet.First(pos => accumulatedCost[pos] == lowestCost);
            
            closedSet.Add(current);
            openSet.Remove(current);
            
            foreach (Vector2Int direction in Directions)
            {
                Vector2Int neighbour = current + direction;
                if (!OnGrid(neighbour) || Instance.GetCell(neighbour).Type == GridCell.CellType.NonWalkable || !Instance.GetCell(neighbour).Available && !ignoreAvailability) continue;

                int cost = accumulatedCost[current] + 1;
                if (cost > range) continue;

                if (closedSet.Contains(neighbour))
                    continue;
                
                if (!openSet.Contains(neighbour))
                {
                    openSet.Add(neighbour);
                    accumulatedCost[neighbour] = cost;
                }
                else if (accumulatedCost[neighbour] > cost)
                    accumulatedCost[neighbour] = cost;
            }
        }
        List<(GridCell, int)> newList = new List<(GridCell, int)>();
        newList.AddRange(closedSet.Select(pos => (Instance.GetCell(pos), accumulatedCost[pos])));
        return newList;
    }
    //From GridManager, used for finding if the cell the AI moves to is closer to the player
    public static int GetDistance(Vector2Int start, Vector2Int end)
    {
        if (_distances.ContainsKey((start, end))) return _distances[(start, end)];
        Debug.Log($"Failed to find cached path, start: {start}, end: {end}");
        Listt<Vector2Int> path = AStar(start, end);
        return path?.Count ?? 1000;
    }
    

Level Editor


To be able to design our levels to conform to a grid, me and another programmer designed a level editor tool integrated with Houdini so our designers could quickly create and iterate on levels.


We achived this by creating a grid mesh that the user could paint on with different colors (representing buildings, streets, etc.) and then sending that information into houdini (the Houdini Unity plugin) to build the actual level. 




    private void GenerateMesh(Mesh mesh, int size, float scale)
    {
        Vector3[] vertices = new Vector3[size * size * 4];
        Color[] colors = new Color[vertices.Length];
        Vector2[] uv = new Vector2[vertices.Length];
        Vector3[] normals = new Vector3[vertices.Length];
        {
            for (int y = 0, i = 0; y < size; y++)
            {
                for (int x = 0; x < size; x++, i += 4)
                {
                    vertices[i] = new Vector3(x, 0, y) * scale;
                    vertices[i + 1] = vertices[i] + Vector3.right * scale;
                    vertices[i + 2] = vertices[i] + Vector3.forward * scale;
                    vertices[i + 3] = vertices[i + 2] + Vector3.right * scale;

                    normals[i] = normals[i + 1] = normals[i + 2] = normals[i + 3] = Vector3.up;
                    colors[i] = colors[i + 1] = colors[i + 2] = colors[i + 3] = Color.white;

                    uv[i] = new Vector2((float) x / size, (float) y / size);
                    uv[i + 1] = new Vector2((float) (x + 1) / size, (float) y / size);
                    uv[i + 2] = new Vector2((float) x / size, (float) (y + 1) / size);
                    uv[i + 3] = new Vector2((float) (x + 1) / size, (float) (y + 1) / size);

                    _grid[x, y] = new EditorGridCell
                    {
                        Vertices = new[] {i, i + 1, i + 2, i + 3},
                        ColorIndex = _grid[x,y] == null ? 0 : _grid[x,y] .ColorIndex
                    };
                }
            }
        }
        int[] triangles = new int[size * size * 6];
        {
            for (int y = 0, t = 0, i = 0; y < size; y++)
            {
                for (int x = 0; x < size; x++, t += 6, i += 4)
                {
                    //Triangle 1
                    triangles[t] = i;
                    triangles[t + 1] = i + 2;
                    triangles[t + 2] = i + 1;
                    //Triangle 2
                    triangles[t + 3] = i + 1;
                    triangles[t + 4] = i + 2;
                    triangles[t + 5] = i + 3;
                }
            }
        }


        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.normals = normals;
        mesh.colors = colors;
        mesh.triangles = triangles;
    }
    
    



    private void GenerateHoudiniFromSelection()
    {
        string[] extensions = {"HDAs", "otl,hda,otllc,hdalc,otlnc,hdanc"};
        string hdaPath = EditorUtility.OpenFilePanelWithFilters("Load Houdini Digital Asset",
            HEU_PluginSettings.LastLoadHDAPath, extensions);
        HEU_PluginSettings.LastLoadHDAPath = Path.GetDirectoryName(hdaPath);
        GameObject go = HEU_HAPIUtility.InstantiateHDA(hdaPath, Vector3.zero,
            HEU_SessionManager.GetOrCreateDefaultSession(), false);
        if (go != null)
        {
            HEU_EditorUtility.SelectObject(go);
            if (_buildingParent == null)
            {
                _buildingParent = new GameObject("BuildingParent").transform;
                Undo.RegisterCreatedObjectUndo(_buildingParent.gameObject, "Added building parent object");
            }

            go.transform.parent = _buildingParent;
        }
        else
        {
            Debug.Log("Failed to load hda asset");
            return;
        }

        Vector3[] vertices = new Vector3[_currentSelection.Count * 4];
        int[] triangles = new int[_currentSelection.Count * 6];

        int vi = 0, ti = 0;

        foreach (Vector2Int gridPos in _currentSelection)
        {
            int[] vertexIndices = _grid[gridPos.x, gridPos.y].Vertices;

            //Vertices
            vertices[vi + 0] = _gridMesh.vertices[vertexIndices[0]];
            vertices[vi + 1] = _gridMesh.vertices[vertexIndices[1]];
            vertices[vi + 2] = _gridMesh.vertices[vertexIndices[2]];
            vertices[vi + 3] = _gridMesh.vertices[vertexIndices[3]];
            //Triangle 1
            triangles[ti + 0] = vi;
            triangles[ti + 1] = vi + 2;
            triangles[ti + 2] = vi + 1;
            //Triangle 2
            triangles[ti + 3] = vi + 1;
            triangles[ti + 4] = vi + 2;
            triangles[ti + 5] = vi + 3;
            vi += 4;
            ti += 6;
        }

        GameObject subObject = new GameObject("Selection");
        Undo.RegisterCreatedObjectUndo(subObject, "New sub mesh");

        if (_selectionParent == null)
        {
            _selectionParent = new GameObject("SelectionParent").transform;
            Undo.RegisterCreatedObjectUndo(_selectionParent.gameObject, "Added selection parent object");
            _selectionParent.gameObject.SetActive(false);
        }

        subObject.transform.parent = _selectionParent;

        MeshRenderer renderer = subObject.AddComponent<MeshRenderer>();
        MeshFilter filter = subObject.AddComponent<MeshFilter>();
        Mesh mesh = new Mesh {vertices = vertices, triangles = triangles};

        filter.mesh = mesh;


        HEU_HoudiniAssetRoot asset = go.GetComponent<HEU_HoudiniAssetRoot>();
        List<HEU_InputNode> list = asset._houdiniAsset.GetInputNodes();
        list[0].AddInputEntryAtEnd(subObject);

        subObject.transform.position += Vector3.down * 0.01f;
        go.transform.position += Vector3.up * 0.01f;

        asset._houdiniAsset.RequestCook(true, true, false, true);
        //DestroyImmediate(subObject);
    }