In this tutorial we'll dive back into the Vuforia Augmented Reality (AR) library, exploring one of its most interesting resources—the Image Target. We'll expand on the Shoot the Cubes game that we created in earlier lessons, adding a new level where the player needs to defend their base from attacking cubes.
This tutorial can be completed alone, although if you want an introduction to AR with Vuforia and Unity3D, check out the earlier posts in the series.
- Mobile DevelopmentPokémon GO Style Augmented Reality With Vuforia
- Mobile DevelopmentCreate a Pokémon GO Style Augmented Reality Game With Vuforia
Image Targets
Any kind of image can be a Vuforia Image Target. However, the more detailed and intricate the image, the better it will be recognized by the algorithm.
A lot of factors will be part of the recognizing calculation, but basically the image must have a reasonable level of contrast, resolution, and distinguishing elements. A blue sky photograph wouldn't work very well, but a picture of some grass would work gracefully. Image Targets can be shipped with the application, uploaded to the application through the cloud, or directly created in the app by the user.
Adding a Target
Let’s begin by adding an ImageTarget
element to our Unity project.
First, download the course assets from the button in the sidebar. Then, in your Unity project, create a new scene called DefendTheBase: in the Project window, select the Scenes folder and click on Create > Scene. Now open that scene and remove all the default scene objects from the hierarchy.
Next we'll add a light and camera. Click on Add > Light > Directional Light to add a directional light. Select this new light and set Soft Shadow as the Shadow Type option.
After that, drag and drop an ARCamera object from Vuforia > Prefabs. Select the ARCamera object and in the inspector panel, set the App License Key created on the Vuforia developer page (see the first tutorial for instructions). Select DEVICE_TRACKING for the World Center Mod.
Finally, drag and drop an ImageTarget to the hierarchy from Vuforia > Prefabs.
Now we have to add a Vuforia Database. First, navigate to https://developer.vuforia.com/target-manager. Click on Add Database and choose a name.
There are three types of Database to choose from:
- Device: The Database is saved on the device and all targets are updated locally.
- Cloud: Database on the Vuforia servers.
- VuMark: Database exclusive to VuMark targets. It is also saved on the device.
In this case, choose the Device option and click on create.
Select the new database so we can start adding targets to it. Now it is time to add targets to the database. For now, we’ll just use the Single Image option.
Navigate to the previously downloaded files, pick ImageTarget1, and set its Width to 1 and click on Add. (Note: If you prefer to create your own Image Target, read the guide first.)
Now you can download the database, selecting Unity Editor as the chosen platform. Open the file and select all elements to be imported. We must also prepare our Unity scene to recognize the ImageTarget with this database we have created.
In the Unity editor, click on the ImageTarget object. First, find and expand Image Target Behavior in the object inspector. Select a Type of Predefined. Choose the image target we created earlier for Database. Finally, make sure that the Enable Extended Tracking and Enable Smart Terrain options are both disabled.
The ImageTarget prefab is made of a series of components, including some scripts like Image Target Behavior, Turn Off Behavior, and Default Tracker Event Handler. If you want to deeply understand how the system works, read those scripts and try to understand their relationship to other components.
For this tutorial, we won't dig too deep, though. We’ll only need to concentrate on the Default Tracker Event Handler, which receives calls when the image target tracking status changes. So let’s use this script as a base to create our own script behavior.
Create a copy of this script that we can extend. First select Default Tracker Event Handler, click on options and select Edit Script. Now, make a copy of the script. If you’re using MonoDevelop, click File > Save As and save as ImageTargetBehavior, saving it in the Scripts folder.
The TargetBehaviorScript Script
We won’t need the Vuforia
namespace in our script. Remove the line “namespace Vuforia
” and the brackets. That means we'll need to explicitly reference the Vuforia
namespace when we want to access its classes:
using UnityEngine; using System.Collections; public class BaseScript : MonoBehaviour, Vuforia.ITrackableEventHandler { // code here }
The most important method in this class will be the OnTrackableStateChanged
method that receives calls when the image target is found or lost by the camera device. According to the target status, it calls OnTrackingFound
or OnTrackingLost
, and we’ll need to edit those methods as well. But first, let’s think about how we want the image target to behave.
In this game, the user will defend a base that appears on an image target. Let’s consider the following game mechanics:
- Once the target is recognized by the system, the base appears and enemies start to spawn and fly toward the base in a kamikaze style.
- Every time an enemy hits the base, the base will take some damage and the enemy will be destroyed.
- To win the game the user must shoot and destroy all enemies before the base is destroyed.
- If the image target is lost (is no longer visible from the device camera), the game will start a countdown timer. If the timer gets to zero, the game is lost. While the target is lost, all enemies will stop advancing toward the base.
So we’ll need to adapt those game mechanics on top of what we built in the last tutorial. We'll create the enemy spawning logic in the next section with an empty object named _SpawnController, using the same logic adopted in the first part of the game.
For now, let's look at the tracking found logic.
private void OnTrackingFound () { EnableRendererAndCollider (); // Inform the system that the target was found StartCoroutine (InformSpawnCtr (true)); } private void OnTrackingLost () { DisableRendererAndCollider (); // Inform the system that the target was lost StartCoroutine (InformSpawnCtr (false)); } // inform SpanController that base was founded private IEnumerator InformSpawnCtr (bool isOn) { // move spawning position GameObject spawn = GameObject.FindGameObjectWithTag ("_SpawnController"); yield return new WaitForSeconds (0.2f); // inform SpanController if (isOn) { spawn.GetComponent<SpawnScript2> ().BaseOn (transform.position); } else { spawn.GetComponent<SpawnScript2> ().BaseOff (); } }
Back in the Unity editor, we can create the base object that will be spawned by the spawn controller.
First, on the ImageTarget object, disable the Default Trackable Event Handler script.
Next, click on Add Component and select the Target Behavior Script. From the Hierarchy panel, right click on ImageTarget and create a new cube named "Base". This cube should be inserted inside the ImageTarget object.
Make sure that the Base has Box Collider and Mesh Renderer enabled.
Optionally, you could also insert a Plane object inside the ImageTarget using the ImageTarget submitted earlier in Vuforia as a texture. This would create an interesting effect, projecting shadows from the target and creating a richer experience.
Adapting the SpawnScript
Now we will adapt the _SpawnController used in the last tutorial. Save the current scene and open ShootTheCubesMain from the last tutorial. In the Hierarchy panel, select the _SpawnController and drag it to the Prefabs folder to make it a Unity Prefab.
Save this new scene and reopen DefendTheBase. Drag _SpawnController from the prefabs folder to the Hierarchy panel. With the _SpawnController selected, click on Add Tag on the Inspector panel. Name the new tag _SpawnController and apply it to the object.
In the Project window, select the Cube element in the Prefab folder and set its Tag, back on its inspector, to 'Enemy'.
Finally, open the Scripts folder and open SpawnScript. We need to make this script adapt itself to the loaded scene.
using UnityEngine; using UnityEngine.SceneManagement; using System.Collections; using System.Collections.Generic; using Vuforia; public class SpawnScript : MonoBehaviour { #region VARIABLES private bool mSpawningStarted = false; // Cube element to spawn public GameObject mCubeObj; // Qtd of Cubes to be Spawned public int mTotalCubes = 10; private int mCurrentCubes = 0; // Time to spawn the Cubes public float mTimeToSpawn = 1f; private int mDistanceFromBase = 5; private List<GameObject> mCubes; private bool mIsBaseOn; private Scene mScene; #endregion // VARIABLES #region UNITY_METHODS // Use this for initialization void Start () { mScene = SceneManager.GetActiveScene(); mCubes = new List<GameObject> (); if ( mScene.name == "ShootTheCubesMain" ) { StartSpawn(); } } // Update is called once per frame void Update () { } #endregion // UNITY_METHODS
Next, we need to create two public methods to receive calls from TargetBehaviorScript
when the target is found or lost:
-
BaseOn (Vector3 basePosition)
will be called when the target is found by the camera and the Base object is shown. It will change the spawning position, start the process, and inform all cubes that were previously added to the stage that the base is visible. -
The
BaseOff()
method will be used when the target is lost. It will stop the staging process and inform all cube elements that the base was lost.
#region PUBLIC_METHODS // Base was found by the tracker public void BaseOn (Vector3 basePosition) { Debug.Log ("SpawnScript2: BaseOn"); mIsBaseOn = true; // change position SetPosition (basePosition); // start spawning process if necessary StartSpawn (); // inform all cubes on screen that base appeared InformBaseOnToCubes (); } // Base lost by the tracker public void BaseOff () { mIsBaseOn = false; mSpawningStarted = false; // inform all cubes on screen that base is lost InformBaseOffToCubes (); } #endregion // PUBLIC_METHODS
The SetPosition (System.Nullable<Vector3> pos)
uses the target’s current position to modify the object x, y, and z axes, and it can also receive a null
value when the scene loaded is ShootTheCubesMain.
#region PRIVATE_METHODS // We'll use a Coroutine to give a little // delay before setting the position private IEnumerator ChangePosition () { Debug.Log ("ChangePosition"); yield return new WaitForSeconds (0.2f); // Define the Spawn position only once // change the position only if Vuforia is active if (VuforiaBehaviour.Instance.enabled) SetPosition (null); } // Set position private void SetPosition (System.Nullable<Vector3> pos) { if (mScene.name == "ShootTheCubesMain") { // get the camera position Transform cam = Camera.main.transform; // set the position 10 units ahead of the camera position transform.position = cam.forward * 10; } else if (mScene.name == "DefendTheBase") { if (pos != null) { Vector3 basePosition = (Vector3)pos; transform.position = new Vector3 (basePosition.x, basePosition.y + mDistanceFromBase, basePosition.z); } } }
InformBaseOnToCubes()
and InformBaseOffToCubes()
are responsible for informing all staged cubes of the current base status.
// Inform all spawned cubes of the base position private void InformBaseOnToCubes () { // Debug.Log("InformBaseOnToCubes"); foreach (GameObject cube in mCubes) { cube.GetComponent<CubeBehaviorScript> ().SwitchBaseStatus (mIsBaseOn); } } // Inform to all cubes that the base is off private void InformBaseOffToCubes () { // Debug.Log("InformBaseOffToCubes"); foreach (GameObject cube in mCubes) { cube.GetComponent<CubeBehaviorScript> ().SwitchBaseStatus (mIsBaseOn); } }
The SpawnLoop()
and SpawnElement()
methods are using almost the same logic as the last tutorial.
// Start spawning process private void StartSpawn () { if (!mSpawningStarted) { // begin spawn mSpawningStarted = true; StartCoroutine (SpawnLoop ()); } } // Loop Spawning cube elements private IEnumerator SpawnLoop () { if (mScene.name == "ShootTheCubesMain") { // Defining the Spawning Position StartCoroutine (ChangePosition ()); } yield return new WaitForSeconds (0.2f); // Spawning the elements while (mCurrentCubes <= (mTotalCubes - 1)) { // Start the process with different conditions // depending on the current stage name if (mScene.name == "ShootTheCubesMain" || (mScene.name == "DefendTheBase" && mIsBaseOn)) { mCubes.Add (SpawnElement ()); mCubes [mCurrentCubes].GetComponent<CubeBehaviorScript> ().SwitchBaseStatus (mIsBaseOn); mCurrentCubes++; } yield return new WaitForSeconds (Random.Range (mTimeToSpawn, mTimeToSpawn * 3)); } } // Spawn a cube private GameObject SpawnElement () { // spawn the element on a random position, inside a imaginary sphere GameObject cube = Instantiate (mCubeObj, (Random.insideUnitSphere * 4) + transform.position, transform.rotation) as GameObject; // define a random scale for the cube float scale = Random.Range (0.5f, 2f); // change the cube scale cube.transform.localScale = new Vector3 (scale, scale, scale); return cube; } #endregion // PRIVATE_METHODS
Creating the Enemies
Now we’ll need to create some enemies. We'll use the Cube object that we created in the last tutorial, making some modifications to its script.
In the Prefabs folder, add a Cube object to the hierarchy. Then select the object and edit the CubeBehaviorScript.
We’ll preserve almost the same logic in this script, but with the following differences:
- The Cube will pursue the Base when the target is found by the camera.
- When the Cube hits the Base, it will destroy itself and give some damage to the Base.
- The script needs to know the name of the scene loaded and adapt itself accordingly.
using UnityEngine; using UnityEngine.SceneManagement; using System.Collections; public class CubeBehaviorScript : MonoBehaviour { #region VARIABLES public float mScaleMax = 1f; public float mScaleMin = 0.2f; public int mCubeHealth = 100; // Orbit max Speed public float mOrbitMaxSpeed = 30f; public float velocityToBase = 0.4f; public int damage = 10; // Orbit speed private float mOrbitSpeed; // Orbit direction private Vector3 mOrbitDirection; // Max Cube Scale private Vector3 mCubeMaxScale; // Growing Speed public float mGrowingSpeed = 10f; private bool mIsCubeScaled = false; private bool mIsAlive = true; private AudioSource mExplosionFx; private GameObject mBase; private bool mIsBaseVisible = false; private Vector3 mRotationDirection; private Scene mScene; #endregion
If the scene's name is DefendTheBase
, it must find the Base object and start to move towards it.
#region UNITY_METHODS void Start () { // Get Scene name mScene = SceneManager.GetActiveScene(); CubeSettings(); } void Update () { // makes the cube orbit and rotate RotateCube(); if ( mScene.name == "DefendTheBase" ) { // move cube towards the base, when it's visible MoveToBase (); } // scale cube if needed if ( !mIsCubeScaled ) ScaleObj(); } #endregion
The CubeSettings()
also need to adapt according to the scene loaded. The Cube only orbits on the y-axis for the DefendTheBase
scene.
#region PRIVATE_METHODS private void CubeSettings () { // defining the orbit direction float x = Random.Range ( -1f, 1f ); float y = Random.Range (-1f, 1f); float z = Random.Range ( -1f, 1f ); // TODO update tutorial with new code // define settings according to scene name if ( mScene.name == "ShootTheCubesMain" ) { mOrbitDirection = new Vector3( x, y, z ); } else if ( mScene.name == "DefendTheBase" ) { // orbit only on y axis mOrbitDirection = new Vector3 (0, y, 0); // scale size must be limited mScaleMin = 0.05f; mScaleMax = 0.2f; velocityToBase = 0.2f; } // rotating around its axis float rx = Random.Range (-1f, 1f); float ry = Random.Range (-1f, 1f); float rz = Random.Range (-1f, 1f); mRotationDirection = new Vector3 (rx, ry, rz); // defining speed mOrbitSpeed = Random.Range (5f, mOrbitMaxSpeed); // defining scale float scale = Random.Range (mScaleMin, mScaleMax); mCubeMaxScale = new Vector3 (scale, scale, scale); // set cube scale to 0, to grow it later transform.localScale = Vector3.zero; // getting Explosion Sound Effect mExplosionFx = GetComponent<AudioSource> (); }
We’ll add some new logic to the RotateCube()
method. The cube objects will rotate around the base while the target is visible. When the target is not visible, they will continue to rotate around the Camera, using the same logic as in the last tutorial.
// Rotate the cube around the base private void RotateCube () { // rotate around base or camera if (mIsBaseVisible && mBase != null && mIsAlive) { // rotate cube around base transform.RotateAround ( mBase.transform.position, mOrbitDirection, mOrbitSpeed * Time.deltaTime); } else { transform.RotateAround ( Camera.main.transform.position, mOrbitDirection, mOrbitSpeed * Time.deltaTime); } transform.Rotate (mRotationDirection * 100 * Time.deltaTime); } // Scale object from 0 to 1 private void ScaleObj(){ // growing obj if ( transform.localScale != mCubeMaxScale ) transform.localScale = Vector3.Lerp( transform.localScale, mCubeMaxScale, Time.deltaTime * mGrowingSpeed ); else mIsCubeScaled = true; }
To move the object toward the base, we’ll need to check first if the base is present, and then apply the position steps to the object.
// Move the cube toward the base private void MoveToBase () { // make the cube move towards the base only if base is present if (mIsBaseVisible && mIsAlive && gameObject != null && mBase != null) { float step = velocityToBase * Time.deltaTime; transform.position = Vector3.MoveTowards (transform.position, mBase.transform.position, step); } }
The DestroyCube()
method is the same as before, but now we'll add a new method—the TargetHit(GameObject)
method—that will be called when the base is hit. Note that the BaseHealthScript referenced in TargetHit()
hasn't been created yet.
// make a damage on target private void TargetHit (GameObject target) { Debug.Log ("TargetHit: " + target.name); if (target.name == "Base") { // make damage on base MyBase baseCtr = target.GetComponent<MyBase> (); baseCtr.TakeHit (damage); StartCoroutine (DestroyCube ()); } } // Destroy Cube private IEnumerator DestroyCube(){ mIsAlive = false; mExplosionFx.Play(); GetComponent<Renderer>().enabled = false; yield return new WaitForSeconds(mExplosionFx.clip.length); Destroy(gameObject); } #endregion
Finally, we’ll add the public methods to be called when the cube takes a hit, when it collides with the base, or when the base changes status.
#region PUBLIC_METHODS // Cube gor Hit // return 'false' when cube was destroyed public bool Hit( int hitDamage ){ mCubeHealth -= hitDamage; if ( mCubeHealth >= 0 && mIsAlive ) { StartCoroutine( DestroyCube()); return true; } return false; } public void OnCollisionEnter (Collision col) { TargetHit (col.gameObject); } // Receive current base status public void SwitchBaseStatus (bool isOn) { // stop the cube on the movement toward base mIsBaseVisible = isOn; if (isOn) { mBase = GameObject.Find ("Base"); } else { mBase = null; } } #endregion
Controlling the Base Health
The enemies are being staged and flying toward the base, but they don't cause any damage when they collide—neither to the base nor to the enemy. We need to create a script to respond to collisions and also to add a health bar to the screen, so the user knows how well they are doing.
Let’s begin adding the health bar. In the Hierarchy panel in the Unity editor, click on Create > UI > Slider. A new Canvas element will be added to the hierarchy. It contains UI elements, including the new Slider. Expand the Canvas and select the Slider.
Change the slider element name to UIHealth. In the Inspector panel, expand Rect Transform and set Width to 400 and Height to 40. Set Pos X to -220, Pos Y to 30, and Pos Z to 0.
Now expand the slider script in the hierarchy. Unselect the Interactable option. For Target Graphic, click on the small ‘dot’ on the right side and select the Background image.
- Set the Min Value to 0 and Max Value to 100.
- Select Whole Numbers.
- Set Value to 100.
Now, expand the Slider panel to expose its child elements: Background, Fill Area, and Handle Slide Area.
- Delete Handle Slide Area.
- Select Background and set its Color to a darker shade of green, like
#12F568FF
. - Expand Fill Area and select the Fill object and set its color to
#7FEA89FF
.
This is how the Game Window should look with the health bar.
The Base Health Script
The code is very simple; it just subtracts the damage made by the enemies from the total amount of the base’s health. Once the health gets to zero, the player loses the game. It will also add a rotation animation to the Base. Create a new C# script called MyBase.
using UnityEngine; using UnityEngine.UI; using System.Collections; public class MyBase : MonoBehaviour { #region VARIABLE public float rotationSpeed = 10f; public int health = 100; public AudioClip explosionSoundFx; public AudioClip hitSoundFx; // TODO choose a different sound for the Hit private bool mIsAlive = true; private AudioSource mAudioSource; public Slider mHealthSlider; #endregion // VARIABLES #region UNITY_METHODS // Use this for initialization void Start () { mAudioSource = GetComponent<AudioSource> (); mHealthSlider.maxValue = health; mHealthSlider.value = health; } // Update is called once per frame void Update () { RotateBase (); } #endregion // UNITY_REGION #region PRIVATE_METHODS private void RotateBase () { if ( mIsAlive && gameObject != null ) { // implement object rotation transform.Rotate ( Vector3.up, rotationSpeed * Time.deltaTime); } } // Destroy base private IEnumerator DestroyBase () { mIsAlive = false; mAudioSource.clip = explosionSoundFx; mAudioSource.Play (); GetComponent<Renderer> ().enabled = false; // inform all Enemies that Base is Lost GameObject[] enemies = GameObject.FindGameObjectsWithTag ("Enemy"); foreach (GameObject e in enemies) { e.gameObject.GetComponent<EnemyScript> ().SwitchBaseStatus (false); } yield return new WaitForSeconds (mAudioSource.clip.length); Destroy (gameObject); } #endregion // PRIVATE_METHODS #region PUBLIC_METHODS // receive damage public void TakeHit (int damage) { health -= damage; mHealthSlider.value = health; if (health <= 0) { StartCoroutine (DestroyBase ()); } else { mAudioSource.clip = hitSoundFx; mAudioSource.Play (); } } #endregion // PUBLIC_METHODS }
Now we need to add and configure the script.
Select the Base in the hierarchy, click on Add Component, and add an Audio Source. Now drag MyBase to the Base element and, in the Inspector panel, expand MyBase. Select a sound effect for the explosion and hit. I’ve used the explosion clip used in the last tutorial, but feel free to add your own. Finally, in the Health Slider, select the UISlider element.
Defending the Base
Our new game experience is almost done. We only need to shoot some lasers to start defending our base. Let's create a script for the laser!
First drag the _PlayerController from the Prefab folder to the hierarchy. Expand _PlayerController and select _LaserController. In the Inspector panel, find Laser Script and click on Edit.
The only thing that we need to change in this script is the position of the laser.
// Shot the Laser private void Fire () { // Get ARCamera Transform Transform cam = Camera.main.transform; // Define the time of the next fire mNextFire = Time.time + mFireRate; // Set the origin of the RayCast Vector3 rayOrigin = cam.position; // Show the Laser using a Coroutine StartCoroutine (LaserFx ()); // Holds the Hit information RaycastHit hit; // Set the origin position of the Laser Line // It will add 10 units down from the ARCamera // We adopted this logic for simplicity Vector3 laserStartPos = new Vector3 (cam.position.x, cam.position.y -2f, cam.position.z); mLaserLine.SetPosition (0, laserStartPos); // Checks if the RayCast hit something if (Physics.Raycast (rayOrigin, cam.forward, out hit, mFireRange)) { // Set the end of the Laser Line to the object hit mLaserLine.SetPosition (1, hit.point); // check target type if (hit.collider.tag == "Enemy") { CubeBehaviorScript cubeCtr = hit.collider.GetComponent<CubeBehaviorScript> (); if (cubeCtr != null) { if (hit.rigidbody != null) { hit.rigidbody.AddForce (-hit.normal * mHitForce); cubeCtr.Hit (mLaserDamage); } } } } else { // Set the enfo of the laser line to be forward the camera // using the Laser range mLaserLine.SetPosition (1, cam.forward * mFireRange); } }
Trying Out the Game
That was a lot of work, but now it's time to play the game! Print out the target image and try to run your game on your phone or tablet. Have some fun with it and see if you can come up with some ways to improve the game!
At this point, you have a good understanding of how the Vuforia system works and how to use it with Unity. I expect that you've enjoyed this journey as much as I have. See you soon!
To learn more about Augmented Reality with Vuforia and Unity, check out our video course here on Envato Tuts+!
Comments