在本教程中,我们将跳回Vuforia增强现实(AR)库,探索其最有趣的资源之一-Image Target 。 我们将在之前的课程中扩展“射击立方体”游戏,并增加一个新的等级,玩家需要捍卫自己的基地以免攻击立方体。
图像目标
任何类型的图像都可以是Vuforia 图像目标 。 但是,图像越详细和复杂,算法识别的效果就越好。
识别计算将包括很多因素,但是基本上图像必须具有合理水平的对比度,分辨率和区分元素。 蓝天的照片效果不佳,但是一张草的照片可以正常工作。 图像目标可以随应用程序一起提供,可以通过云上传到应用程序,也可以由用户直接在应用程序中创建。
添加目标
首先,向我们的Unity项目添加一个ImageTarget
元素。
首先,从侧栏中的按钮下载课程资源。 然后,在您的Unity项目中,创建一个名为DefendTheBase的新场景:在“ 项目”窗口中,选择“ 场景”文件夹,然后单击“ 创建” >“ 场景”。 现在打开该场景,并从层次结构中删除所有默认场景对象。
接下来,我们将添加一个灯光和一个摄像头。 单击添加 > 灯光 > 定向光以添加定向光。 选择此新光源并将“ 软阴影”设置为“ 阴影类型”选项。
之后,从Vuforia > Prefabs拖放一个ARCamera对象。 选择ARCamera对象,然后在检查器面板中设置在Vuforia开发人员页面上创建的App许可证密钥 ( 有关说明,请参阅第一个教程 )。 选择DEVICE_TRACKING作为World Center Mod 。
最后,拖放一个ImageTarget 到Vuforia > Prefabs的层次结构。
现在我们必须添加一个Vuforia数据库。 首先,浏览至https://developer.vuforia.com/target-manager 。 单击添加数据库,然后选择一个名称。
有三种类型的数据库可供选择:
- 设备 :数据库保存在设备上,并且所有目标都在本地更新。
- 云 :Vuforia服务器上的数据库。
- VuMark :VuMark目标专用的数据库。 它也保存在设备上。
在这种情况下,选择设备选项,然后单击创建 。
选择新数据库,以便我们可以开始向其中添加目标。 现在是时候向数据库添加目标了。 现在,我们仅使用“ 单个图像”选项。
导航到以前下载的文件,选择ImageTarget1 ,并将其Width设置为1并单击Add 。 (注意:如果您想创建自己的Image Target,请先阅读该指南 。)
现在,您可以下载数据库,选择Unity Editor作为所选平台。 打开文件,然后选择所有要导入的元素。 我们还必须准备Unity场景,以使用我们创建的该数据库识别ImageTarget 。
在Unity编辑器中,单击I mageTarget对象。 首先,在对象检查器中找到并展开“ 图像目标行为 ”。 选择一种预定义的类型 。 选择我们先前为Database创建的映像目标。 最后,确保同时禁用“ 启用扩展跟踪”和“ 启用智能地形”选项。
ImageTarget预制件由一系列组件组成,包括一些脚本,例如Image Target Behavior, T urn Off Behavior和Default Tracker Event Handler 。 如果您想深入了解系统的工作原理,请阅读这些脚本并尝试了解它们与其他组件的关系。
但是,对于本教程,我们不会做得太深。 我们只需要专注于Default Tracker Event Handler ,当图像目标跟踪状态发生变化时,它将接收呼叫。 因此,让我们将此脚本用作创建自己的脚本行为的基础。
创建此脚本的副本,我们可以对其进行扩展。 首先选择Default Tracker Event Handler ,单击选项并选择Edit Script 。 现在,复制脚本。 如果使用的是MonoDevelop,请单击“ 文件” >“ 另存为”,然后另存为ImageTargetBehavior ,然后将其保存在“ 脚本”文件夹中。
TargetBehaviorScript脚本
我们的脚本中不需要Vuforia
命名空间。 删除“ namespace Vuforia
”行和括号。 这意味着我们要访问Vuforia
名称空间时需要显式引用它:
using UnityEngine;
using System.Collections;
public class BaseScript : MonoBehaviour, Vuforia.ITrackableEventHandler
{
// code here
}
此类中最重要的方法是OnTrackableStateChanged
方法,该方法可在相机设备找到或丢失图像目标时接收呼叫。 根据目标状态,它调用OnTrackingFound
或OnTrackingLost
,我们也需要编辑这些方法。 但是首先,让我们考虑一下我们希望图像目标如何表现。
在此游戏中,用户将捍卫出现在图像目标上的基础。 让我们考虑以下游戏机制:
- 一旦目标被系统识别,基地就会出现,敌人开始以神风队风格生成并飞向基地。
- 每次敌人击中基地时,基地都会受到一定的伤害,敌人将被摧毁。
- 为了赢得游戏,用户必须在摧毁基地之前射击并消灭所有敌人。
- 如果图像目标丢失(从设备相机中不再可见),游戏将启动倒数计时器。 如果计时器为零,则游戏将失败。 当目标丢失时,所有敌人将停止向基地前进。
因此,我们需要在上一个教程中构建的内容之上调整这些游戏机制。 我们将在下一部分使用名为_SpawnController的空对象创建敌人的生成逻辑,使用游戏第一部分中采用的相同逻辑。
现在,让我们看一下跟踪发现的逻辑。
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 ();
}
}
回到Unity编辑器中,我们可以创建将由spawn控制器生成的基础对象。
首先,在ImageTarget对象上,禁用“ 默认可跟踪事件处理程序”脚本。
接下来,单击“ 添加组件 ”,然后选择“ 目标行为脚本” 。 在“ 层次结构”面板中,右键单击ImageTarget并创建一个名为“ Base ”的新多维数据集。 该多维数据集应插入ImageTarget对象内。
确保底座已启用Box Collider和Mesh Renderer 。
您也可以选择使用在Vuforia中较早提交的ImageTarget作为纹理在ImageTarget内插入一个Plane对象。 这将产生有趣的效果,从目标投射阴影并创造更丰富的体验。
修改SpawnScript
现在,我们将适应上一教程中使用的_SpawnController 。 保存当前场景, 然后从上一教程中打开ShootTheCubesMain 。 在“ 层次结构”面板中,选择_SpawnController并将其拖动到Prefabs文件夹以使其成为Unity Prefab 。
保存此新场景,然后重新打开DefendTheBase 。 将_SpawnController从prefabs文件夹拖动到“ 层次结构”面板。 选择_SpawnController后 ,在“ 检查器”面板上单击“ 添加标签 ”。 将新标签命名为_SpawnController并将其应用于对象。
在“项目”窗口中,选择“ 预制”文件夹中的“ 多维数据集”元素,然后将其“ 标记”设置回到“检查器”中,使其位于检查器中。
最后,打开Scripts文件夹并打开SpawnScript 。 我们需要使此脚本适应加载的场景。
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
接下来,我们需要创建两个公共方法来在找到或丢失目标时接收来自TargetBehaviorScript
调用:
当摄像机找到目标并显示Base对象时,将调用
BaseOn (Vector3 basePosition)
。 它将更改生成位置,开始该过程,并通知先前添加到该阶段的所有多维数据集可见该基础。当目标丢失时,将使用
BaseOff()
方法。 它将停止暂存过程,并通知所有多维数据集元素已丢失基础。
#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
SetPosition (System.Nullable<Vector3> pos)
使用目标的当前位置来修改对象的x,y和z轴,当加载的场景为ShootTheCubesMain时,它也可以接收null
值。
#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()
和InformBaseOffToCubes()
负责向所有暂存多维数据集通知当前基本状态。
// 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);
}
}
SpawnLoop()
和SpawnElement()
方法使用的逻辑与上一教程几乎相同。
// 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
创造敌人
现在我们需要创建一些敌人。 我们将使用在上一教程中创建的Cube对象,对其脚本进行一些修改。
在Prefabs文件夹中,将一个Cube对象添加到层次结构中。 然后选择对象并编辑CubeBehaviorScript 。
我们将在此脚本中保留几乎相同的逻辑,但有以下差异:
- 当摄像机找到目标时, 立方体将追击基地 。
- 当魔方击中基地时 ,它会自我破坏并对基地造成一定的伤害。
- 脚本需要知道加载的场景的名称并相应地进行调整。
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
如果场景的名称为DefendTheBase
,则它必须找到Base对象并开始向其移动。
#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
CubeSettings()
还需要根据加载的场景进行调整。 多维数据集仅在DefendTheBase
场景的y轴上旋转。
#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> ();
}
我们将向RotateCube()
方法添加一些新逻辑。 当目标可见时,多维数据集对象将围绕底座旋转。 当目标不可见时,它们将继续使用与上一教程相同的逻辑围绕Camera旋转。
// 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;
}
要将对象移向基座,我们首先需要检查基座是否存在,然后将定位步骤应用于该对象。
// 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);
}
}
DestroyCube()
方法与以前相同,但是现在我们将添加一个新方法,即TargetHit(GameObject)
方法,该方法在击中底座时将被调用。 请注意,BaseHealthScript中引用TargetHit()
尚未建立。
// 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
最后,我们将添加要在多维数据集命中时,与基础碰撞时或当基础更改状态时调用的公共方法。
#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
控制基础健康
敌人正在上演并飞向基地,但是它们碰撞时不会对基地和敌人造成任何伤害。 我们需要创建一个脚本来响应冲突,还需要在屏幕上添加一个健康栏,以便用户知道他们的状况如何。
让我们开始添加健康栏。 在Unity编辑器的“ 层次结构”面板中,单击创建 > UI > 滑块 。 新的Canvas元素将添加到层次结构中。 它包含UI元素,包括新的Slider 。 展开“ 画布”,然后选择“ 滑块” 。
将滑块元素名称更改为UIHealth 。 在“ 检查器”面板中,展开“ 矩形变换”,然后将“ 宽度”设置为400 ,将“ 高度”设置为40 。 将位置X设置为-220 , 位置Y设置为30 , 位置Z设置为0 。
现在,在层次结构中展开滑块脚本。 取消选择“ 交互”选项。 对于“ 目标图形” ,单击右侧的小“点”,然后选择背景图像。
- 将“ 最小值”设置为0 ,将“ 最大值”设置为100 。
- 选择整数 。
- 将值设置为100 。
现在,展开“ 滑块”面板以显示其子元素: Background , Fill Area和Handle Slide Area 。
- 删除处理幻灯片区域 。
- 选择背景并将其颜色设置为较深的绿色阴影,例如
#12F568FF
。 - 展开“ 填充区域”并选择“ 填充”对象,并将其颜色设置为
#7FEA89FF
。
这就是游戏窗口与运行状况栏的外观。
基本运行状况脚本
代码很简单; 它只是从基地生命值的总和中减去敌人造成的伤害。 一旦生命值降为零,玩家就会输掉比赛。 还将向基础添加旋转动画。 创建一个名为MyBase的新C#脚本。
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
}
现在,我们需要添加和配置脚本。
在层次结构中选择基础 ,单击添加组件 ,然后添加音频源 。 现在将MyBase拖动到Base元素,然后在“ 检查器”面板中,展开MyBase 。 为爆炸和打击选择声音效果。 我已经使用了上一教程中使用的爆炸剪辑,但可以随时添加自己的爆炸剪辑。 最后,在Health Slider中 ,选择UISlider元素。
保卫基地
我们新的游戏体验即将完成。 我们只需要发射一些激光就可以保卫我们的基地。 让我们为激光创建一个脚本!
首先将_PlayerController从Prefab文件夹拖到层次结构中。 展开_PlayerController并选择_LaserController 。 在“ 检查器”面板中,找到“ 激光脚本” ,然后单击“ 编辑” 。
我们需要在此脚本中更改的唯一内容是激光的位置。
// 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);
}
}
试用游戏
那是很多工作,但是现在该玩游戏了! 打印出目标图像,然后尝试在手机或平板电脑上运行游戏。 玩得开心,看看是否可以提出一些改进游戏的方法!
至此,您已经对Vuforia系统如何工作以及如何在Unity中使用它有了很好的了解。 我希望您和我一样喜欢这次旅行。 再见!