How to Make Interactable Tectonic Plates in AR – Part 2

If you haven’t read Part 1 of the tutorial yet, it’s highly recommended you do so.

In the first part of our tutorial, we began setting up our tectonic plates with animations and so forth.  We’re ready to continue with our educational AR app and set it up so that the correct animation can run at the right time.

ON SALE – FINAL DAYS
BUILD GAMES
SUPERCHARGE YOUR CODING SKILLS

✓ 250+ coding courses

✓ Interactive lessons

✓ Guided learning paths

✓ Help from expert mentors

GET IT ALL FOR JUST
$1

Continuing with Scripting the Touch Manager

Now it’s time to create the “CalculateDragDirectionOnPlate” function. It takes in a touch end position in pixel coordinates. This is a pretty big function, so let’s take it in steps.

// called after the dragged touch is released
void CalculateDragDirectionOnPlate (Vector3 touchEnd)
{

}

First, we make a variable which will hold the world position of the touch. Then we create a new ray which shoots from the screen to the touch position.
// create world pos for touch end
Vector3 touchEndWorldPos = Vector3.zero;

// create ray from camera to touch
Ray ray = Camera.main.ScreenPointToRay(touchEnd);

This is unlike the normal raycast though. Instead of shooting a ray and checking if it hit a collider, we’re doing a plane raycast. This creates an abstract, flat plane which we then get the position that the ray intersected it. That’s our world position.
// we are raycasting to a plane so it's Y axis remains 0
Plane rayPlane = new Plane(Vector3.up, Vector3.zero);

float enter = 0.0f;

// send the raycast
if(rayPlane.Raycast(ray, out enter))
{
    touchEndWorldPos = ray.GetPoint(enter);
}

We now need to check the distance of the drag. If it’s too short, then we’ll un-assign the plate (we’ll make this later) and return.
// was this not a drag but a tap? if so, unassign the plate
if(Vector3.Distance(touchStartWorldPos, touchEndWorldPos) < 3.0f && touchingPlate)
{
    // un-assign the plate movement
    return;
}

Then, we need to calculate a normalized direction between the two positions. Next, we round the X and Z values to the nearest integer. This means that each axis will be either: -1, 0 or 1. With this, we can then calculate the direction of the drag in 4 ways.
// get direction between 2 points and round it
Vector3 dir = Vector3.Normalize(touchEndWorldPos - touchStartWorldPos);
dir = new Vector3(Mathf.Round(dir.x), 0, Mathf.Round(dir.z));

Now we check for a right, left, forwards or back drag by looking at the axis direction.
Vector3 plateDir = Vector3.zero;

// dragged RIGHT
if (dir.x == 1.0f)
    plateDir = Vector3.right;
// dragged LEFT
else if (dir.x == -1.0f)
    plateDir = Vector3.left;
// dragged UP
else if (dir.z == 1.0f)
    plateDir = Vector3.forward;
// dragged DOWN
else if (dir.z == -1.0f)
    plateDir = Vector3.back;

Then we’ll assign that direction to the plate.
// assign the plate movement

Let’s make that script now.

Scripting the Plate Manager

Create a new C# script called “PlateManager” and attach it to the “Manager” object.

We need to add a few libraries, for using the timelines.

using UnityEngine.Timeline;
using UnityEngine.Playables;

Now let’s start on our variables.

First, we got our left and right plates which hold all the data for their corresponding plates. “currentlyAnimating” is a bool which is true when we’re animating the plates.

// plates
public Plate leftPlate;             // left tectonic plate
public Plate rightPlate;            // right tectonic plate

// states
public bool currentlyAnimating;     // are the plates currently animating?

Then we need to list all the playable assets (timelines) for each of our animations.
// timeline playable assets
public PlayableAsset transformForwardsBackAnim;
public PlayableAsset transformBackForwardsAnim;
public PlayableAsset divergentAnim;
public PlayableAsset convergentOverUnderAnim;
public PlayableAsset convergentUnderOverAnim;

We need to also access the playable director, so let’s link it.
// components
public PlayableDirector director;   // component used to play the PlayableAssets above

Finally, we create an instance (singleton) of the script so that we can access it easier later on.
// instance
public static PlateManager instance;

void Awake ()
{
    // set instance to this script
    instance = this;
}

Our first function is our largest and most important. “AssignPlateMovement” is a function which is called from the TouchManager. It sends over the plate to assign and a global movement direction.
// called after the user drags a direction for the plate to move in
public void AssignPlateMovement (Plate plate, Vector3 moveDirection)
{

}

First, we check if we’re currently animating. If so, we can’t assign anything so return. Then we check if this is the left plate we’re setting. Since the left plate is a rotated version of the right one, we need to flip the X axis of the move direction.
// if we're currently animating, don't allow for assigning plate movement
if (currentlyAnimating)
    return;

// invert moveDirection X axis if the plate is the left one
if (plate == leftPlate)
    moveDirection.x = -moveDirection.x;

Now we check the direction and assign the corresponding movement to the plate.
// did the user swipe FORWARDS?
if(moveDirection == Vector3.forward)
{
    plate.assignedMovement = PlateMovement.TransformForward;
}
// did the user swipe BACKWARDS?
else if(moveDirection == Vector3.back)
{
    plate.assignedMovement = PlateMovement.TransformBack;
}
// did the user swipe AWAY from the center?
else if (moveDirection == Vector3.right)
{
    plate.assignedMovement = PlateMovement.Divergent;
}

If you’re converging the plates, the one on top depends on the last plate you drag. So we need to check if the other plate’s assigned movement is convergent under. If so, then the current one is over. Otherwise, we just set it to converge under.
// did the user swipe TOWARDS the center?
else if(moveDirection == Vector3.left)
{
    // get the other plate
    Plate otherPlate = plate == leftPlate ? rightPlate : leftPlate;

    // if the other plate converges under, converge over
    if (otherPlate.assignedMovement == PlateMovement.ConvergentUnder)
        plate.assignedMovement = PlateMovement.ConvergentOver;
    // otherwise, just converge under
    else
        plate.assignedMovement = PlateMovement.ConvergentUnder;
}

Now we need to activate the arrow and rotate it to face the direction we dragged.
// set arrow visual
plate.arrowVisual.SetActive(true);

// rotate arrow depending on assigned movement
switch(plate.assignedMovement)
{
    case PlateMovement.TransformForward: plate.arrowVisual.transform.localEulerAngles = new Vector3(0, plate == leftPlate? 90 : -90, 0); break;
    case PlateMovement.TransformBack: plate.arrowVisual.transform.localEulerAngles = new Vector3(0, plate == leftPlate ? -90 : 90, 0); break;
    case PlateMovement.Divergent: plate.arrowVisual.transform.localEulerAngles = new Vector3(0, 0, 0); break;
    case PlateMovement.ConvergentOver: plate.arrowVisual.transform.localEulerAngles = new Vector3(0, 180, 0); break;
    case PlateMovement.ConvergentUnder: plate.arrowVisual.transform.localEulerAngles = new Vector3(0, 180, 0); break;
}

If we have an assigned movement on both plates, we check to see if the plates are compatible. If so, we call the “PlayAnimation” function, otherwise we call the “OnAnimationEnd” function.
// do both plates have an assigned movement?
if (leftPlate.assignedMovement != PlateMovement.Unassigned && rightPlate.assignedMovement != PlateMovement.Unassigned)
{
    // are the 2 assigned movements compatable with eachother?
    if (PlatesAreCompatable())
        Invoke("PlayAnimation", 0.5f);
    else
        Invoke("OnAnimationEnd", 0.35f);
}

That’s 3 functions right there that we haven’t made yet. Let’s start with “PlatesAreCompatible”.

This returns a bool. True being that the plates are compatible and false being that they aren’t.

It’s basically checking for all the possible plate combinations.

// returns true is both plates assigned movement are compatible
bool PlatesAreCompatible ()
{
    // plates are both transforming forwards
    if (leftPlate.assignedMovement == PlateMovement.TransformForward && rightPlate.assignedMovement == PlateMovement.TransformForward)
        return false;
    // plates are both transforming backwards
    else if (leftPlate.assignedMovement == PlateMovement.TransformBack && rightPlate.assignedMovement == PlateMovement.TransformBack)
        return false;
    // left plate is diverging but right plate is not
    else if (leftPlate.assignedMovement == PlateMovement.Divergent && rightPlate.assignedMovement != PlateMovement.Divergent)
        return false;
    // left plate is not diverging but right plate is
    else if (leftPlate.assignedMovement != PlateMovement.Divergent && rightPlate.assignedMovement == PlateMovement.Divergent)
        return false;
    // left plate is converging over but right plate is not converging under
    else if (leftPlate.assignedMovement == PlateMovement.ConvergentOver && rightPlate.assignedMovement != PlateMovement.ConvergentUnder)
        return false;
    // left plate is converging under but right plate is not converging over
    else if (leftPlate.assignedMovement == PlateMovement.ConvergentUnder && rightPlate.assignedMovement != PlateMovement.ConvergentOver)
        return false;
    else
        return true;
}

Now let’s create a function called “UnassignPlate”. This basically resets a plate’s assigned movement and disables the arrow visual.
// unassigns a plate's assigned movement and disables arrow
public void UnassignPlate (Plate plate)
{
    plate.assignedMovement = PlateMovement.Unassigned;
    StartCoroutine(DeactivateArrowVisual(plate));
}

// deactivates desired plate's arrow visual
IEnumerator DeactivateArrowVisual (Plate plate)
{
    plate.arrowAnimator.SetTrigger("Exit");
    yield return new WaitForSeconds(0.3f);
    plate.arrowVisual.SetActive(false);
}

“OnAnimationEnd” is a function that will be called after the animation has finished. This basically resets the plates. When the animation begins, this function gets invoked to be called after [timeline duration] seconds.
// called after the plate animation has ended
void OnAnimationEnd ()
{
    currentlyAnimating = false;
    UnassignPlate(leftPlate);
    UnassignPlate(rightPlate);
}

Now let’s create the “PlayAnimation” function.
// plays the assigned plate animation
void PlayAnimation ()
{

}

First, we deactivate the arrow visuals.
// disable arrows
StartCoroutine(DeactivateArrowVisual(leftPlate));
StartCoroutine(DeactivateArrowVisual(rightPlate));

Then we set the corresponding timeline to the director.
// assign the corresponding timeline to the director
switch (leftPlate.assignedMovement)
{
    case PlateMovement.TransformForward:
    {
        director.playableAsset = transformForwardsBackAnim;
        break;
    }
    case PlateMovement.TransformBack:
    {
        director.playableAsset = transformBackForwardsAnim;
        break;
    }
    case PlateMovement.Divergent:
    {
        director.playableAsset = divergentAnim;
        break;
    }
    case PlateMovement.ConvergentOver:
    {
        director.playableAsset = convergentOverUnderAnim;
        break;
    }
    case PlateMovement.ConvergentUnder:
    {
        director.playableAsset = convergentUnderOverAnim;
        break;
    }
}

Lastly, we play the animation and invoke “OnAnimationEnd” to be called in [timeline duration] seconds.
// play animation
currentlyAnimating = true;
director.Play();
Invoke("OnAnimationEnd", (float)director.playableAsset.duration);

The last function we call is “DoubleTapResetPlates”. This just resets everything when you double tap.
// called when the player double taps the screen, resets plates
public void DoubleTapResetPlates ()
{
    director.Stop();
    director.playableAsset = null;
    // --disable UI text
    currentlyAnimating = false;
    UnassignPlate(leftPlate);
    UnassignPlate(rightPlate);
}

Lava Visual

Something that adds to the visual element is a lava element.

Create a new Plane object (right click Hierarchy > 3D Object > Plane) and call it “Lava”. Set the material to “Lava”. Then, scale and position it so it leaks around the edges and just rises above the base. Finally, set it as a child of the “TectonicPlates” object.

Lava object in Unity with Transform component highlighted

We’re also going to add a slight animation which will “pulse” the intensity of the lava.

First, un-parent the lava object so it’s got no parent. We need to do this due to the original parent already having an animator component.

So, with the lava selected, go to the Animation window and create a new animation.

Here, I ping-ponged the scale and color.

Lava object animation demonstration in Unity

Once you’ve finished animating, you can make the “Lava” object a child of the “TectonicPlates” object.

Creating the UI Element

Since this app is aimed at education, it would be good to show the user what type of plate movement they just triggered.

Create a new Canvas (right click Hierarchy > UI > Canvas). Set the “Render Mode” to World Space. Set the scale to 0.015, and position it behind the plates.

Unity Canvas UI object added to scene

Add a new Text element to the canvas (right click Canvas > UI > Text). Resize it and edit the Text component to your liking. I added an Outline component to contrast any possible background.

Unity canvas with tectonic plate text object added

Now create an animation called “InfoText_Entrance” that makes the text pop up and after a few seconds it disappears.

Unity animation for UI text element

With the animation completed, let’s disable the text object and begin scripting.

Create a new C# script called “UI” and attach it to the “Manager” object.

Since we’re going to be using UI, we’ll need to reference Unity’s UI library.

using UnityEngine.UI;

Our 2 variables are the “infoText”, which is the actual text element we’re going to edit, and “infoTextAnim” is the Animator component attached to the text.
public Text infoText;
public Animator infoTextAnim;

We’re also going to include an instance (singleton) so that we can access this script easier later on.
// instance
public static UI instance;

void Awake ()
{
    // set instance to this script
    instance = this;
}

Our one and only function is “SetText”. This sets the text to display a certain string and plays the animation.
// sets the info text
public void SetText (string textToDisplay)
{
    infoText.gameObject.SetActive(true);
    infoText.text = textToDisplay;
    infoTextAnim.Play("InfoText_Entrance");
}

Connecting the Scripts

Now that we have all of our scripts, it’s time to connect them together.

First, let’s start with the TouchManager script.

Create a new function called “DoubleTapCheck”. This checks to see if the user has double tapped, and calls the corresponding function in the PlateManager script if so.

// checks for a double tap to reset the plates
void DoubleTapCheck ()
{
    if(Time.time - lastTapTime <= doubleTapMaxTime)
    {
        PlateManager.instance.DoubleTapResetPlates();
    }

    lastTapTime = Time.time;
}

We also need to call this function up in Update, where we check…
// did the touch START this frame?
if(Input.touches[0].phase == TouchPhase.Began)
{
    DoubleTapCheck();
    SetStartTouch(Input.touches[0].position);
}

Then, in the “CalculateDragDirectionOnPlate” function, we need to do 2 things.

First, where we check if the player tapped and didn’t drag, call the “UnassignPlate” function in the PlateManager script.

// was this not a drag but a tap? if so, unassign the plate
if(Vector3.Distance(touchStartWorldPos, touchEndWorldPos) < 3.0f && touchingPlate)
{
    PlateManager.instance.UnassignPlate(touchingPlate);
    return;
}

And right at the end of the function, assign the plate movement.
// assign the plate movement
PlateManager.instance.AssignPlateMovement(touchingPlate, plateDir);

Now for the PlateManager script.

In the “PlayAnimation” function, we need to call the “SetText” function in UI to display the current animation playing.

// assign the corresponding timeline to the director
// also set the UI text to display the movement type
switch (leftPlate.assignedMovement)
{
    case PlateMovement.TransformForward:
    {
        director.playableAsset = transformForwardsBackAnim;
        UI.instance.SetText("Transform Boundry");
        break;
    }
    case PlateMovement.TransformBack:
    {
        director.playableAsset = transformBackForwardsAnim;
        UI.instance.SetText("Transform Boundry");
        break;
    }
    case PlateMovement.Divergent:
    {
        director.playableAsset = divergentAnim;
        UI.instance.SetText("Divergent Boundry");
        break;
    }
    case PlateMovement.ConvergentOver:
    {
        director.playableAsset = convergentOverUnderAnim;
        UI.instance.SetText("Convergent Boundry");
        break;
    }
    case PlateMovement.ConvergentUnder:
    {
        director.playableAsset = convergentUnderOverAnim;
        UI.instance.SetText("Convergent Boundry");
        break;
    }
}

Finally, in the “DoubleTapResetPlates” function, we need to deactivate the text object.
FindObjectOfType<UI>().infoText.gameObject.SetActive(false);

Connecting Inspector Properties

Make sure that each plate’s “Plate” script has the correct properties assigned to it.

Unity Plate script with object assignment

Do the same for the “Manager” object and its various scripts.

Plate Manager script in Unity with objects assigned

Testing in the Editor

You might want to test out the app in the editor before we build it to a device. There are three things we need to do:

  • Add mouse input
  • Disable EasyAR integration
  • Add a camera to the scene

To add mouse input, go to the “TouchManager” script. In the “Update” function, add:

if (Input.GetMouseButtonDown(0))
{
    DoubleTapCheck();
    SetStartTouch(Input.mousePosition);
}
else if(Input.GetMouseButtonUp(0))
{
    if(touchingPlate != null)
        CalculateDragDirectionOnPlate(Input.mousePosition);
}

Back in the editor we need to disable the EasyAR components, so that they don’t try and run. Deactivate the “EasyAR_Startup” GameObject, and disable the Image Target Behaviour script on the “ImageTarget_TectonicPlate” GameObject.

Unity with AR related functions deactivated

Now we just need to add a new camera to the scene (right click Hierarchy > Camera). Set its tag as “MainCamera” and position it where you like.

Unity Camera object as seen in Hierarchy and Inspector

You should now be able to press play and test the app out in the editor!

Unity tectonic plates app in action

Building

You can follow the last section of this tutorial on how to build to an Android device.

Conclusion

So we’ve made a working model of tectonic plates, with interactions to trigger certain plate interactions. If you want to use this in AR, you’ll need to print out the included image marker (located inside the StreamingAssets) folder.  You can locate the project files here.