Frogment

🎮 Link to game: https://coahl.itch.io/frogment

My Contributions:

  • Node Path System
  • Node Connection Tool
  • Player Pathfinding

This was my second game project at The Game Assembly, completed in 5 weeks in Unity. This was a puzzle game made for Android where you play as a frog character. You automatically move to a block by tapping on it, and there are puzzles to solve by pressing buttons or rotating blocks.

Node connections

On this project my main task was working with the player’s pathfinding and node path system.

Every walkable block has a WalkableNode script assigned to it. To keep things simple, this script only contains references to connected nodes and what node type it is (regular, stairs or ladders). I also added gizmo lines to show the node connections, which made it easy to spot missing or incorrect connections.

  1public class WalkableNode : MonoBehaviour
  2{
  3    [SerializeField] NodeType myType;
  4    public NodeType Type => myType;
  5
  6    public List<WalkableNode> myConnectedNodes = new();
  7
  8    Vector3 myLastPosition;
  9    Vector3 myLastForward;
 10
 11    [HideInInspector] public bool myIsBound;
 12    private void Awake()
 13    {
 14        myLastPosition = transform.position;
 15    }
 16
 17    public void ConnectNode(WalkableNode aNode)
 18    {
 19        if(myConnectedNodes.Contains(aNode))
 20        {
 21            Debug.Log("Already has node " + aNode.name + " in list", this);
 22            return;
 23        }
 24
 25        myConnectedNodes.Add(aNode);
 26    }
 27    public void DisconnectNode(WalkableNode aNode)
 28    {
 29        if (!myConnectedNodes.Contains(aNode))
 30        {
 31            Debug.Log("Does not have node " + aNode.name + " in its list", this);
 32            return;
 33        }
 34
 35        myConnectedNodes.Remove(aNode);
 36    }
 37
 38    public bool IsConnectedWith(WalkableNode aOther)
 39    {
 40        bool meToOther = myConnectedNodes.Contains(aOther);
 41        bool otherToMe = aOther.myConnectedNodes.Contains(this);
 42
 43        if(meToOther && !otherToMe)
 44        {
 45            Debug.LogWarning("This node has a 1 way connection with node " + aOther.name, this);
 46        }
 47        else if(!meToOther && otherToMe)
 48        {
 49            Debug.LogWarning("This node has a 1 way connection with node " + name, aOther);
 50        }
 51
 52        return meToOther && otherToMe;
 53    }
 54
 55    /// <summary>
 56    /// Returns the offset position the player should walk to when this is the current node
 57    /// </summary>
 58    public Vector3 GetWalkPosition()
 59    {
 60        switch(myType)
 61        {
 62            default:
 63            case NodeType.Regular:
 64                return transform.position + transform.up;
 65            case NodeType.Ladder:
 66                return transform.position + 0.5f * transform.up + 0.75f * transform.forward;
 67            case NodeType.Stair:
 68                return transform.position + 0.5f * transform.up;
 69                
 70        }
 71    }
 72
 73    private void OnDrawGizmos()
 74    {
 75        
 76        Gizmos.color = Color.black;
 77        Gizmos.DrawWireSphere(GetWalkPosition(), 0.15f);
 78        Gizmos.color = Color.green;
 79
 80        if(myConnectedNodes == null)
 81        {
 82            return;
 83        }
 84
 85        foreach (var node in myConnectedNodes)
 86        {
 87            switch(myType)
 88            {
 89                default:
 90                    Gizmos.DrawLine(GetWalkPosition() + Vector3.up * 0.15f, node.GetWalkPosition());
 91                    break;
 92                case NodeType.Ladder:
 93                    Gizmos.DrawLine(GetWalkPosition() + transform.forward * 0.15f, node.GetWalkPosition());
 94                    break;
 95                
 96            }
 97            
 98        }
 99    }
100}
101
102public enum NodeType
103{
104    Regular,
105    Ladder,
106    Stair,
107}

Connection Tool

Node connections were then configured in the editor. Manually connecting every block would take ages, so I made a editor window with node connections functions. I started off with a button that would remove all nodes and a button that would automatically connect nodes that should connect (depending on their placement & node type). Later I added a button to auto disconnect nodes, a button to check if node has snapped position and a button that would remove invalid connections

  1public class NodeWindow : EditorWindow
  2{
  3    [MenuItem("Tools/NodeWindow")]
  4    public static void CreateWindow()
  5    {
  6        NodeWindow window = GetWindow<NodeWindow>();
  7        window.titleContent = new GUIContent("NodeWindow");
  8        window.minSize = new Vector2(200,500);
  9    }
 10
 11    private void OnGUI()
 12    {
 13        GUILayout.BeginVertical();
 14        GUILayout.FlexibleSpace();
 15        GUILayout.EndVertical();
 16
 17        GUILayout.BeginHorizontal();
 18        GUILayout.FlexibleSpace();
 19
 20        GUIContent buttonContent = new GUIContent("Clear all connections", "Removes all node connections!");
 21        if (GUILayout.Button(buttonContent, GUILayout.Width(130), GUILayout.Height(50)))
 22        {
 23            WalkableNode[] allNodes = FindObjectsOfType<WalkableNode>();
 24            foreach (WalkableNode node in allNodes)
 25            {
 26                EditorUtility.SetDirty(node);
 27                if (node.myConnectedNodes == null)
 28                {
 29                    node.myConnectedNodes = new();
 30                    continue;
 31                }
 32
 33                node.myConnectedNodes.Clear();
 34            }
 35        }
 36        GUILayout.FlexibleSpace();
 37        GUILayout.EndHorizontal();
 38
 39        GUILayout.BeginVertical();
 40        GUILayout.FlexibleSpace();
 41        GUILayout.EndVertical();
 42
 43        GUILayout.BeginHorizontal();
 44        GUILayout.FlexibleSpace();
 45
 46        buttonContent = new GUIContent("Auto connect", "Goes through all Walkable Nodes and connects it with neighbouring nodes it should be connected with");
 47        if (GUILayout.Button(buttonContent, GUILayout.Width(100), GUILayout.Height(50)))
 48        {
 49            WalkableNode[] allNodes = FindObjectsOfType<WalkableNode>();
 50            foreach (WalkableNode node in allNodes)
 51            {
 52                EditorUtility.SetDirty(node);
 53                NodeUtilities.AutoConnectNode(node);
 54            }
 55        }
 56
 57
 58        GUILayout.FlexibleSpace(); // Add flexible space to the right of the button
 59        GUILayout.EndHorizontal();
 60
 61        GUILayout.BeginVertical();
 62        GUILayout.FlexibleSpace();
 63        GUILayout.EndVertical();
 64
 65        GUILayout.BeginHorizontal();
 66        GUILayout.FlexibleSpace();
 67
 68        buttonContent = new GUIContent("Auto disconnect", "Goes through all Walkable Nodes and disconncects it with neighbouring nodes it should be connected with");
 69        if (GUILayout.Button(buttonContent, GUILayout.Width(120), GUILayout.Height(50)))
 70        {
 71            WalkableNode[] allNodes = FindObjectsOfType<WalkableNode>();
 72            foreach (WalkableNode node in allNodes)
 73            {
 74                EditorUtility.SetDirty(node);
 75                NodeUtilities.AutoDisconnectNode(node);
 76            }
 77        }
 78
 79        GUILayout.FlexibleSpace(); // Add flexible space to the right of the button
 80        GUILayout.EndHorizontal();
 81
 82        GUILayout.BeginVertical();
 83        GUILayout.FlexibleSpace();
 84        GUILayout.EndVertical();
 85
 86        GUILayout.BeginHorizontal();
 87        GUILayout.FlexibleSpace();
 88
 89        buttonContent = new GUIContent("Check every node", "Goes through all Walkable Nodes and detects any that are not snapped to a position");
 90        if (GUILayout.Button(buttonContent, GUILayout.Width(150), GUILayout.Height(50)))
 91        {
 92            WalkableNode[] allNodes = FindObjectsOfType<WalkableNode>();
 93            foreach (WalkableNode node in allNodes)
 94            {
 95                NodeUtilities.CheckNode(node);
 96            }
 97        }
 98
 99        GUILayout.FlexibleSpace(); // Add flexible space to the right of the button
100        GUILayout.EndHorizontal();
101
102        GUILayout.BeginVertical();
103        GUILayout.FlexibleSpace();
104        GUILayout.EndVertical();
105
106        GUILayout.BeginHorizontal();
107        GUILayout.FlexibleSpace();
108
109        buttonContent = new GUIContent("Fix every node", "Goes through all Walkable Nodes and removes connections to itself, null nodes and duplicate connections");
110        if (GUILayout.Button(buttonContent, GUILayout.Width(150), GUILayout.Height(50)))
111        {
112            WalkableNode[] allNodes = FindObjectsOfType<WalkableNode>();
113            foreach (WalkableNode node in allNodes)
114            {
115                EditorUtility.SetDirty(node);
116                NodeUtilities.FixNode(node);
117            }
118        }
119
120        GUILayout.FlexibleSpace(); // Add flexible space to the right of the button
121        GUILayout.EndHorizontal();
122
123        GUILayout.BeginVertical();
124        GUILayout.FlexibleSpace();
125        GUILayout.EndVertical();
126    }
127}

Trailer

I am part of The Game Assembly’s internship program. As per the agreement between the Games Industry and The Game Assembly, neither student nor company may be in contact with one another regarding internships before April 15. Any internship offers can be made on April 27th, at the earliest.