Skip to content

Coroutines: Parallel Execution and Experiment Protocols (adapt to no VR)

We have mastered automated, simple events with the Cube Factory. Now let's create more complex flows and dive deeper into coroutines!

Reuse previous Unity project

Let's keep using the project we created in 2•Interaction to keep the first-person navigation.

Archiving and copying Unity projects

Another way to continue work without sacrificing previously finished projects is to making a copy of a whole Unity project in your file explorer, e.g., by going to the Unity hub, right-clicking your project to "Show in Finder/Explorer" and copying its whole folder. You can rename the copied folder to anything you want, then go back to the Hub and clicking Open instead of new New Project, then point it to your newly copied folder.

Setting the scene

We create again a few new objects.

Inside the Environment Object of the Playground scene, create two new Plane 3D objects and configure them like this:

  • "StandingMark"
    • Position: (-2.333, .01, -2.333)
    • Scale: (0.15, 1, 0.15)
  • "OriginMark"
    • Position (0, .01, 0)
    • Scale (0.15, 1, 0.15)

Assign StandingMark the old BlueBox material, and the RedBox material to OriginMark. You can also simply recreate them by following the steps again.

Outside the Environment object, at empty spaces in the hierarchy of our scene, create:

  • A Sphere 3D object and name it "Zeppelin":
    • Position (-1, 1.5, 4)
    • Rotation (0, 90, 0)
    • Scale (0.5, 0.2, 0.2)
  • A new Cube:
    • Position (0, 1.5, -1)
    • Scale (0.2, 0.1, 0.2)

Attach to the new Sphere (Zeppelin) and the new cube a new Script called RaycastHitChecker as a component. It will be responsible for checking if the object is currently being hit by a raycast, e.g. from the camera, an change its object accordingly.

Write the following code into the script:

RaycastHitChecker.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RaycastHitChecker : MonoBehaviour
{
    // Boolean variables to hold its current and previous hit state
    // the currentHitState can be accessed publicly, but only changed privately
    public bool currentHitState { get; private set; }
    private bool previousHitState = false;

    // a public array of colors to hold the colors for the hit and not hit states
    public Color[] colors;
    // a private reference to the material of the object
    private Material _material;

    void Start ()
    {
        // Set the current hit state to false at the beginning
        currentHitState = false;
        // Get the material of the object
        _material = GetComponent<MeshRenderer> ().material;
        // Set the color of the material to the first color in the array, if both exist
        if (_material != null && colors.Length > 0)
            _material.color = colors[0];
    }

    // a public method to update the hit state
    public void UpdateHitState (bool hitState)
    {
        // Set the current hit state to the new hit state
        currentHitState = hitState;
        // Check if the current hit state is different from the previous hit state
        if (currentHitState != previousHitState)
        {
            // If the current hit state is true, switch to the hit color
            if (currentHitState)
                SwitchColor (1);
            // If the current hit state is false, switch to the not hit color
            else
                SwitchColor (0);
        }
        // Set the previous hit state to the current hit state
        previousHitState = currentHitState;
    }

    private void SwitchColor (int index)
    {
        // Set the color of the material to the color at the index in the array
        // check if the index is within the bounds of the array
        if (index < colors.Length)
            _material.color = colors[index];
    }
}

Once that script is saved and attached to both objects, you can access it in their respective Inspectors and assign two new colors to each colors arrays. Choose two fancy colors, and preferably different for each object!

Colors

Setting objects in motion

Now we should have the two last objects simply hovering in the air. Let's write new scripts to get them moving.

Create a script called Moving and attach it to the Zeppelin object:

Moving.cs
using UnityEngine;

public class Moving : MonoBehaviour
{
    [Tooltip("Units per second")] public float speed;
    [Tooltip("Start position in 3D space")] public Vector3 startPoint;
    [Tooltip("End position in 3D space")] public Vector3 endPoint;

    public float interpolator = 1f;
    public bool isDone => interpolator > .999f;

    void Update()
    {
        print(isDone);
        if (isDone) return;

        interpolator += speed * Time.deltaTime;
        transform.position = Vector3.Lerp(startPoint, endPoint, interpolator);
    }
}

It takes speed and two Vector3 variables for its moving speed and for start and end points of its journey.

isDone?

The expression isDone => interpolator > .999f is a handy shortcut: the => operator assigns to isDone the result of evaluating interpolator > .999f, similar to how an if statement would do. You can read more about this at the C# documentation.

What this effectively does is to constantly check if the interpolator is greater than .999f, and setting isDone to true if so, and false if not.

interpolator being already 1 at the beginning will set isDone to true, thus aborting the execution of Update(). It will need to be set to a smaller value to get it going.

This function also outputs the state of isDone to the Console at each update using the print() command: you can see it when the program is running, as it will be filling up the console with printouts quickly and keep scrolling down.

Printing to console

It can be a good practice to output the state of variables to the console to have a clear understanding of what is going on. Doing it as above (printing at each Update()) is one way, but usually it is used only at specific events, like when a variable is changed. Using print() — or the Unity equivalent Debug.Log() — well is a powerful helper to understanding and debugging your code, so feel free to try it on other variables and other positions in your code by yourself!

Set speed in the inspector to 0.25, and give the original coordinates (look at the Transform!) as the start point and a different position as the end point for its trajectory.

Now create a script called Rotating for the new cube object:

Rotating.cs
using UnityEngine;

public class Rotating : MonoBehaviour
{
    [Tooltip("Units per second")] public float speed;
    [Tooltip("Axis to rotate around")] public Vector3 axis;

    public bool isRotating = true;

    void Update()
    {
        if (!isRotating) return;

        transform.Rotate(axis, speed * Time.deltaTime);
    }
}

This script takes a speed and an axis Vector3 variable to define its rotation speed and axis. It also has a public isRotating boolean variable to control if it should rotate or not, which is accessible from the outside.

Set the speed to 180 and the rotation axis to (1, 0, 0) in the inspector.

If you run the game now, only the the cube object should be rotating, and then only if its isRotating parameter is set to true in its Rotating component in the inspector. It should be on by default, as we've assigned it like this in the script above.

Try it out by pressing Play, and play around with the parameters in the inspector to see their effects:

Rotating

Scripting a protocol

With the two moving objects and two areas to step on have set the stage to introduce more complex flows, such as might be needed for actual experiments (or even games).

Variables and assignments

Create a new script for the MainCamera object and name it Protocol:

Protocol.cs
using System.Collections;
using UnityEngine;

public class Protocol : MonoBehaviour
{

    public Moving movingComponent;
    public Rotating rotatingComponent;

    public RaycastHitChecker zeppelinHitChecker;
    public RaycastHitChecker cubeHitChecker;

    public Transform cameraTransform;
    public Transform standingMark, originMark;

    public float positionTolerance = 0.5f;

    private bool isRunning = true;
}

At this point, nothing in this first part of the declaration should be unfamiliar: Protocol.cs holds a number of public variables to store a SteamVR action, four components from other objects, two transforms, a floating point number, and a boolean.

Save the script as it is so far, and make sure it's attached to our MainCamera. Go back to the Unity editor to assign the unfilled variables in the inspector.

Challenge: assign it yourself!

Can you figure out yourself how to fill the fields in the inspector for the MainCamera's Protocol component correctly?

The names and expected types should make this easy.

Continue only after you're confident you set it up correctly. Don't hesistate to ask if you're having trouble here.

Remember: you can directly drag an object from the Hierarchy into one of these fields, and the correct component will be automatically chosen.

Interaction functions

Let's give us the ability to interact with Protocol.cs within the VR world. Add these functions:

Protocol.cs
private bool IsStandingOnTarget (Vector2 targetPos)
{
    Vector3 pos3D = cameraTransform.position;
    Vector2 standingPos = new Vector2 (pos3D.x, pos3D.z);

    return Vector2.Distance (standingPos, targetPos) < positionTolerance;
}

The Update() loop should be clear: if the mouse button is pressed AND isRunning is already true, THEN turn the variable isRunning to false.

IsStandingOnTarget(Vector2 targetPos) takes a given target position (a 2D vector) and measures its distance to a 2D projection of the cameraTransform — if it's below the positionTolerance, it return true, otherwise false.

The Clicker turns into a Looker

If you still have the old Clicker.cs script attached to the MainCamera, we can now modify it to constantly cast a ray from the camera, not only when the mouse is clicked. We still keep the old functionality, but add much more.

Modify it to look like this:

Looker.cs
using UnityEngine;

public class Clicker : MonoBehaviour
{
    GameObject previouslyHitObject;

    // Update is called once per frame
    void Update ()
    {
        // constantly send out a raycast from the camera
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);

        if (Physics.Raycast (ray, out hit))
        {
            GameObject obj = hit.transform.gameObject;

            // check if the hit GameObject has a RaycastHitChecker component
            if (obj.GetComponent<RaycastHitChecker> () != null)
            {
                // Call the UpdateHitState method of the RaycastHitChecker component
                obj.GetComponent<RaycastHitChecker> ().UpdateHitState (true);
            }

            // Check if the mouse button is pressed down
            if (Input.GetMouseButtonDown (0))
            {
                Debug.Log ("Clicked on " + hit.transform.gameObject.name);
                // Check if the GameObject has a Clickable component
                if (obj.GetComponent<Clickable> () != null)
                {
                    // Call the OnClick method of the Clickable component
                    obj.GetComponent<Clickable> ().OnClick ();
                }
            }

            // if there was a previously hit object that is not the current hit object
            if (previouslyHitObject != null && previouslyHitObject != obj)
            {
                // check if the previously hit object has a RaycastHitChecker component
                if (previouslyHitObject.GetComponent<RaycastHitChecker> () != null)
                {
                    // Call the RaycastHitChecker component and set its state to false
                    previouslyHitObject.GetComponent<RaycastHitChecker> ().UpdateHitState (false);
                }
            }

            // remember the previously hit object
            previouslyHitObject = obj;
        }
        else
        {
            // We are not hitting anything. Check if there was a previously hit object
            if (previouslyHitObject != null)
            {
                // check if the previously hit object has a RaycastHitChecker component
                if (previouslyHitObject.GetComponent<RaycastHitChecker> () != null)
                {
                    // Call the RaycastHitChecker component and set its state to false
                    previouslyHitObject.GetComponent<RaycastHitChecker> ().UpdateHitState (false);
                }
            }
        }
    }
}

It now remembers the previously hit object, and if the current hit object is different from the previous one, it will set the previous one's hit state to false. This way, we can deactivate objects that are not being looked at any more, and do it in an economical way, as we don't need to constantly check if every object in the scene is still being hit.

Scripted flow

Let's now put these variables and functions to work.

We will use the Start() call for that, but first change it from a basic private void type function, we will turn it into an IEnumerator — this way, it acts as a coroutine and we can stop and continue its execution using WaitWhile(), WaitUntil(), and WaitForSecondsRealtime() commands.

Replace the current Start() function with this:

Protocol.cs
private IEnumerator Start ()
{
    // stop the cube from rotating it's not off yet
    rotatingComponent.isRotating = false;
    while (isRunning)
    {
        // Wait until user has moved onto square on the floor
        Vector3 standingMarkPos = standingMark.position;
        yield return new WaitWhile (() => !IsStandingOnTarget (standingMark));
        print ("Stepped on the first square");
        standingMark.gameObject.SetActive (false); // Hide

        // Wait until user has touched the zeppelin
        yield return new WaitUntil (() => zeppelinHitChecker.currentHitState);
        print ("Looked at the Zeppelin");

        movingComponent.interpolator = 0; // This triggers the start of Zeppelin's animation
        // Wait for moving animation to end
        yield return new WaitUntil (() => movingComponent.isDone);
        print ("Zeppelin's animation is done");
        // set the next target square to be visible
        originMark.gameObject.SetActive (true);

        // Move to center of the room
        yield return new WaitWhile (() => !IsStandingOnTarget (originMark));
        print ("Stepped on the origin square");
        originMark.gameObject.SetActive (false); // Hide

        // Wait until user has touched the cube
        yield return new WaitUntil (() => cubeHitChecker.currentHitState);
        rotatingComponent.isRotating = true; // Start rotating cube
        print ("Looked at the cube");

        // Wait one second while it rotates
        yield return new WaitForSecondsRealtime (1f);
        rotatingComponent.isRotating = false; // Stop rotating cube
        print ("One second has passed");

        // RESET everything
        standingMark.gameObject.SetActive (true); // Show the target mark again
        movingComponent.transform.position = movingComponent.startPoint;
        rotatingComponent.transform.rotation = Quaternion.identity;
        print ("Everything's reset!");
    }
}

You should be able to read this script and understand what it's doing. The various yield return new lines halt the execution of the function (stopping the advancement to the next lines) until their Wait clauses are fulfilled, as explained in the comments for every line.

Save the script, go back to Unity, and make sure the cube is not set to rotate already.

Run the game, and try to advance through the different steps as you can see in the protocol! The console will print updates on the user's progress through the steps.

Trials and Quitting

The protocol repeats endlessly. Let's keep track of how many trials the user has completed, and have the application quit itself after a certain number of trials.

For that we need to change the Protocol.cs a bit. First, add using UnityEditor; to the top of the script, and then add two variables for managing the trials:

Protocol.cs
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class Protocol : MonoBehaviour
{
    private int trialCounter = 0;
    public int maxTrials = 3;

In the Inspector you can now set a maximum trial number, and the script will keep track of how many trials have been completed. For that to work, we will need to edit our experiment flow a bit.

The start function needs to be changed to this:

Protocol.cs
private IEnumerator Start ()
{
    // stop the cube from rotating it's not off yet
    rotatingComponent.isRotating = false;
    while (isRunning)
    {
        // increment the trial counter
        trialCounter++;
        Debug.Log ("Trial " + trialCounter + " is starting");
        // Wait until user has moved onto square on the floor
        Vector3 standingMarkPos = standingMark.position;
        yield return new WaitWhile (() => !IsStandingOnTarget (standingMark));
        print ("Stepped on the first square");
        standingMark.gameObject.SetActive (false); // Hide

        // Wait until user has touched the zeppelin
        yield return new WaitUntil (() => zeppelinHitChecker.currentHitState);
        print ("Looked at the Zeppelin");

        movingComponent.interpolator = 0; // This triggers the start of Zeppelin's animation
        // Wait for moving animation to end
        yield return new WaitUntil (() => movingComponent.isDone);
        print ("Zeppelin's animation is done");
        // set the next target square to be visible
        originMark.gameObject.SetActive (true);

        // Move to center of the room
        yield return new WaitWhile (() => !IsStandingOnTarget (originMark));
        print ("Stepped on the origin square");
        originMark.gameObject.SetActive (false); // Hide

        // Wait until user has touched the cube
        yield return new WaitUntil (() => cubeHitChecker.currentHitState);
        rotatingComponent.isRotating = true; // Start rotating cube
        print ("Looked at the cube");

        // Wait one second while it rotates
        yield return new WaitForSecondsRealtime (1f);
        rotatingComponent.isRotating = false; // Stop rotating cube
        print ("One second has passed");

        // one run is over
        print ("Trial " + trialCounter + " is over");
        // check if the trial counter is greater than or equal to the max trials
        if (trialCounter >= maxTrials)
        {
            print ("All trials are over");
            isRunning = false;
            // quit the application
#if UNITY_EDITOR
            // Application.Quit() does not work in the editor so
            // UnityEditor.EditorApplication.isPlaying need to be set to false to end the game
            EditorApplication.isPlaying = false;
#else
            Application.Quit ();
#endif
        }

        // RESET everything
        standingMark.gameObject.SetActive (true); // Show the target mark again
        movingComponent.transform.position = movingComponent.startPoint;
        rotatingComponent.transform.rotation = Quaternion.identity;
        print ("Everything's reset!");
    }
}

The changes are the trialCounter variable, which is incremented at the beginning of the function, and the check if the trial counter is greater than or equal to the max trials. If it is, the application will quit itself.

Make sure to change it accordingly, and test it out!