Creating a Circuit in Unity

During a recent game jam, my team made a game called Elevator Circuit. The game is now available on for free on Itch: https://tykenn.itch.io/elevator-circuit. The objective is to complete a circuit by moving elevators holding circuit segments, aligning them into a complete path. It currently has five short puzzles with more to come. My biggest role in the project was to program the circuit system.

The idea was that any segment without a path to a generator would be red. A segment with a path from one side, but not a path from the other is yellow. When there is a path to a battery from both sides, current can actually pass through the segment, it turns green. I used Procedural Lightning to indicate that the connectors of two segments are close enough to be connected (It happens even when the two segments are not connected to a battery, but we needed some kind of feedback).

My first (failed) attempt

I made a script for each connector that checked for other connectors to enter and exit its trigger area. It then registered the segment of the other connector to current segment. The connector then told the segment to update its material by recursively checking its neighboring segments, and then its neighbor’s neighbors, until it either dead-ends, loops , or reaches a battery. All the segments along that path would update their materials accordingly. I soon ran into all sorts of race conditions and edge cases. The lightning would correctly connect segments, but the colors of the segments would be wrong half the time. So, I scrapped that and tried something new.

Working from the battery to the segment.

Since there would only ever be a handful of segments in any stage, it would not be too expensive to check for changes every frame. I kept what I had before with each connector registering other segments to its own segments, but instead of updating the materials only during a connection change, the battery would “pulse” every frame. Going recursively to each connected segment from the battery, it would mark the visited segments as “connected” either from the left or right, depending on where the pulse came from. The segments would then all update their materials accordingly, the frame would render, and then the connections would reset for the next pulse.

Here is the script that goes on the individual connectors:

using UnityEngine;

public class CircuitConnector : MonoBehaviour {

    public bool isLeft;
    CircuitSegment segment;

    private void Awake()
    {
        //Expect CircuitSegment script to be on parent.
        segment = transform.parent.GetComponent();
    }

    //Make a connection
    private void OnTriggerEnter(Collider other)
    {
        CircuitConnector otherCon = other.GetComponent();
        if (otherCon != null && otherCon != this)
        {
            //Register connection to segment
            if (isLeft)
            {
                segment.leftSegment = otherCon.segment;
            }
            else
            {
                segment.rightSegment = otherCon.segment;
            }
            //Code for anything else to do during a
            //connection, like instantiate lightning
        }
    }

    //Lose connection
    private void OnTriggerExit(Collider other)
    {
     
        CircuitConnector otherCon = other.GetComponent();
        if (otherCon != null && otherCon != this)
        {
            //Unregister segment
            if (isLeft && segment.leftSegment == otherCon.segment)
            {
                segment.leftSegment = null;
            }
            if (!isLeft && segment.rightSegment == otherCon.segment)
            {
                segment.rightSegment = null;
            }
        }
    }
}

And here is the script that goes on the circuit segment:

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class CircuitSegment : MonoBehaviour {

    [HideInInspector]
    public CircuitSegment leftSegment;
    [HideInInspector]
    public CircuitSegment rightSegment;
    [HideInInspector]
    private List meshesToPaint;

    //Usually only one of these in the scene at a time
    public bool hasBattery;

    //I used a red, yellow, and green material
    public Material deadMaterial;
    public Material oneWayMaterial;
    public Material poweredMaterial;

    //Optional event for receiving/losing power,
    //like opening and closing a door
    public UnityEvent powerEvent;
    public UnityEvent losePowerEvent;

    //Resets every frame, set by pulses
    private bool leftPowered = false;
    private bool rightPowered = false;

    //Does not reset, decided after pulsing finishes
    private bool powered = false;

    //only true during a pulse for checking
    //for loops
    private bool pulsing;

    private void Awake()
    {
        //Register all the meshes I want to paint.
        //I tagged all of them with "Wire"
        meshesToPaint = new List();
        foreach (Transform child in transform)
        {
            if (child.tag == "Wire")
            {
                meshesToPaint.Add(child.GetComponent());
            }
        }
        UpdateMat();
    }

    public void Update()
    {
        //Handle events for gaining and losing power.
        if (!powered && leftPowered && rightPowered)
        {
            powered = true;
            powerEvent.Invoke();
        }
        else if (powered && !(leftPowered && rightPowered))
        {
            powered = false;
            losePowerEvent.Invoke();
        }

        //Reset power
        leftPowered = false;
        rightPowered = false;
    }

    //Don't pulse until everything is reset from Update()
    private void LateUpdate()
    {
        //Only segments with batteries start a pulse
        if (hasBattery)
        {
            if (leftSegment != null)
                leftSegment.Pulse(this);
            if (rightSegment != null)
                rightSegment.Pulse(this);
        }
    }

    //Recursive function for deciding connections to batteries
    public void Pulse(CircuitSegment from)
    {
        //If a segment has already been visited in a pulse, it
        //must have looped.
        if (!pulsing)
        {
            pulsing = true;
            if (from == leftSegment && from == rightSegment)
            {
                //Handle edge case of a circuit with only two segments
                leftPowered = true;
                rightPowered = true;
            }
            else if (from == leftSegment)
            {
                leftPowered = true;
                if (rightSegment != null)
                    rightSegment.Pulse(this);
            }
            else if (from == rightSegment)
            {
                rightPowered = true;
                if (leftSegment != null)
                    leftSegment.Pulse(this);
            }
        }
        UpdateMat();
        pulsing = false;
    }

    // Update is called once per frame
    public void UpdateMat() {
        Material mat;
        if (hasBattery)
        {
            //Batteries work a little different. Never red.
            if (leftSegment != null && leftSegment.leftPowered && 
                rightSegment != null && rightSegment.rightPowered)
            {
                mat = poweredMaterial;
            }
            else
            {
                mat = oneWayMaterial;
            }
        }
        else
        {
            if (leftPowered && rightPowered)
            {
               mat = poweredMaterial;
            }
            else if (leftPowered || rightPowered)
            {
                mat = oneWayMaterial;
            }
            else
            {
                mat = deadMaterial;
            }
        }

        //Apply the material to every assigned mesh
        foreach (var rend in meshesToPaint)
        {
            rend.material = mat;
        }
    }
}

Now, in Unity, make a GameObject and attach the CircuitSegment script. Then add some meshes that you want to change colors. Tag them as “Wire” and make the CircuitSegment object its parent. Then also make two connectors, attach the CircuitConnector script to each, and mark one as isLeft. Make them also children of the segment. Now, if you have some object you want a complete circuit to trigger, like opening and closing a door, make a new script with two public methods such as Open() and Close(). On the segment that powers the object, you can assign those methods in the inspector through the Power Event and Lose Power Event.

How to model procedurally in Unity3D

Unity3D is not a modelling tool. It is usually easier to design artwork in an animation program like Blender or Maya, and then import them into Unity. However, for some things, especially procedurally generated content, you’ll need to dive into the drawing tools Unity provides in its libraries.

Note: This tutorial assumes you have a basic knowledge of the Unity Editor. If you do not, you should first go through Unity’s beginner tutorial.

First, create a new scene in Unity. Go to GameObject > 3D Object > Cube to add a new object to the scene, and position it at the origin. Then, in the Project window, go to Create > C# Script. Name the SimpleMesh, and then double click it to launch Visual Studio. Now we are ready to start coding.

We’ll begin by creating a Mesh object. This is what hold the shape of our object. We make a mesh by creating a bunch of 3D vertices and then connecting these vertices to make triangles. Unity does not have a built-in way of making pyramids, so we’ll make one with our SimpleMesh script. I’ll show the code and then explain:

using UnityEngine;

public class SimpleMesh : MonoBehaviour {

    void Start () {

        Vector3[] verts = new Vector3[16];
        //base
        verts[0] = new Vector3(-1, 0, -1);
        verts[1] = new Vector3(-1, 0, 1);
        verts[2] = new Vector3(1, 0, 1);
        verts[3] = new Vector3(1, 0, -1);

        //front wall
        verts[4] = new Vector3(-1, 0, -1);
        verts[5] = new Vector3(0, 2, 0);
        verts[6] = new Vector3(1, 0, -1);

        //back wall
        verts[7] = new Vector3(1, 0, 1);
        verts[8] = new Vector3(0, 2, 0);
        verts[9] = new Vector3(-1, 0, 1);

        //left wall
        verts[10] = new Vector3(-1, 0, 1);
        verts[11] = new Vector3(0, 2, 0);
        verts[12] = new Vector3(-1, 0, -1);

        //right wall
        verts[13] = new Vector3(1, 0, -1);
        verts[14] = new Vector3(0, 2, 0);
        verts[15] = new Vector3(1, 0, 1);

        //order the indices of verts in patterns
        //to make triangles
        int[] tris = new int[18] { 3,2,0,2,1,0,4,
                     5,6,7,8,9,10,11,12,13,14,15};

        //add the vertices and triangles to a new mesh
        Mesh mesh = new Mesh();
        mesh.vertices = verts;
        mesh.triangles = tris;

        //Assign the mesh to the MeshFilter component
        GetComponent().mesh = mesh;
    }
}

Everything will happen inside the Start() method so it will happen once when we play the game. This is what the script does:

  1. Create an array of 3D vectors to store our vertices. Our pyramid will have five vertices: the top and each of the four corners. Many of these vertices are duplicated in our array. This is because we want hard edges. Shared vertices mean there will be a smooth edge, like the surface of a sphere. Duplicated vertices mean it will draw a hard edge like a corner. We’ll come back to that later.
  2. Create and array to store triangle information. This may seem like a strange list of numbers, but each corresponds to an index in the triangles array. Every three numbers represent one triangle connecting vertices counter-clockwise. So the first triangle connects vertices 3, 2, and zero, the second connects 2, 1, and zero, and so forth.
  3. Create a new Mesh object. This stores both our vertices and triangles.
  4. Assign the new mesh to the MeshFilter. This component should already be on our object, since we are creating it from a Cube. It will also use the material already assigned to the Cube. The material tells Unity how to draw the shape on the screen based on textures and lighting. Materials can also be generated, but that is outside the scope of this tutorial.

Assign the SimpleMesh script to the cube object. If you press play, you should see something like this:

pyramid

You may need to adjust the camera position or pause the game to get a better view. The shape is right, but it still looks kind of strange. There is nothing to distinguish the edge, and the lighting is weird. This is because we have not assigned normals. Normals are direction vectors that are supposed to point away from the object. There is one for each vertex. When light hits an object, it reflects more light if it hits directly than if it hits at an angle. The normals give Unity a reference to decide what is considered direct.

normal_explanation

Normals are why we duplicated vertices. Unity blends between the normals to get smooth curves, like in the top diagram. At a hard edge, though, where would the normal point? With two vertices, we can have two normals, one pointing away from each wall that meets at that corner. There is no blending because we have two separate surfaces.

Fortunately, for a shape as simple as this, the normals can easily be calculated. Add this inside the Start() method after everything else:

mesh.RecalculateNormals();

That’s it!. Press play and you should see a pyramid that looks something like this:

pyramid2