统一拖动和放置全息图


This article is featured in the first ever DZone Guide to Game Development. Get your free copy for more insightful articles, industry statistics, and more! 

我一直在努力创造全息透镜全息图应用程序中的效果:当你从菜单中拉出一个全息图时,它会“停留在你的视线中”并跟随它。你可以打开水龙头,然后它会在你离开的地方悬在空中,但你也可以把它放在地板上、桌子上或墙边。你不能把它推过一个表面。也就是说,大多数时候。So, like this.

在视频中,你可以看到它跟随凝视光标在空中浮动,直到它碰到左边的一面墙,然后停止,然后向下,直到它碰到床,然后停止,然后再向上,直到我最后把它放在地板上。

新的一年,新的工具包

正如技术前沿经常发生的那样,事情往往变化很快。全息透镜国家也是如此。我已经投入到Unity 5.5和new HoloToolkit有一些大的变化。自从上一次迭代以来,事情变得简单多了。另外,我想指出,在本教程中,我使用了the latest patch release

设置初始项目

这幅画最能说明这一点。如果你已经建立了项目,我们只需要这个。管理者和全息收藏都是简单的空游戏对象,用来把东西组合在一起。它们在这里没有任何其他特定的功能。将四个蓝色预设拖放到指定的位置,然后为立方体设置一些属性(见下文)。

立方体是将要被移动的东西。现在是“一些”代码的时候了。

主要演员

有两个剧本扮演主角,还有几个配角。

  • 移动视线

  • IntialPlaceByTap

第一个使物体移动,第二个实际上结束它。顺便说一下,实际的移动是由我们的老朋友iTween完成的,他的有用性和应用已经在AMS全息图像系列的第5部分中描述过了。因此,您需要将它包含在项目中,以防止各种讨厌的错误。不管怎样,让我们来看看这个节目的明星,移动视线。

凝视着移动

它是这样开始的:

using UnityEngine;
using HoloToolkit.Unity.InputModule;
using HoloToolkit.Unity.SpatialMapping;
namespace LocalJoost.HoloToolkitExtensions
{
    public class MoveByGaze : MonoBehaviour
    {
        public float MaxDistance = 2f;
        public bool IsActive = true;
        public float DistanceTrigger = 0.2f;
        public BaseRayStabilizer Stabilizer = null;
        public BaseSpatialMappingCollisionDetector
        CollisonDetector;
        private float _startTime;
        private float _delay = 0.5f;
        private bool _isJustEnabled;
        private Vector3 _lastMoveToLocation;
        private bool _isBusy;
        private SpatialMappingManager MappingManager
        {
            get { return SpatialMappingManager.Instance; }
        }
        void OnEnable()
        {
            _isJustEnabled = true;
        }
        void Start()
        {
            _startTime = Time.time + _delay;
            _isJustEnabled = true;
            if (CollisonDetector == null)
            {
                CollisonDetector =
                gameObject.AddComponent<Default
                MappingCollisionDetector>();
            }
        }
    }
}


上面是设置:

  • MaxDistance是行为试图将对象放置在表面上时与头部的最大距离。再远一点,它就会浮在空中。

  • I主动确定行为是否是主动的(废话)。

  • 距离是指在物体开始移动之前,你的视线与它的距离。它有点跟不上你的视线。这可以防止物体以非常紧张的方式移动。

  • 稳定器是由输入管理员制造、使用和维护的稳定器。你必须把输入管理器从你的场景拖到这个区域来使用稳定器。这不是强制性的,但强烈推荐。

  • 碰撞检测器是我们将在后面看到的一个类——它基本上确保你拖动的对象不会被推过任何表面。你需要给你正在拖动的游戏对象添加一个碰撞检测器——或者可能是你正在拖动的游戏对象的一部分的另一个游戏对象。然后,碰撞检测器需要用移动凝视拖到这个区域。这不是强制性的。如果您没有添加一个,您附加了“移动视线”的对象将只是跟随您的视线并穿过任何对象。这是缺省映射碰撞检测器的工作,本质上是一个空模式实现。

无论如何,所有的工作都是在更新方法中完成的:

void Update()
{
    if (!IsActive || _isBusy || _startTime > Time.time)
    return;
    _isBusy = true;
    var newPos = GetPostionInLookingDirection();
    if ((newPos - _lastMoveToLocation).magnitude >
    DistanceTrigger || _isJustEnabled)
    {
        _isJustEnabled = false;
        var maxDelta = CollisonDetector.GetMaxDelta(newPos
        - transform.position);
        if (maxDelta != Vector3.zero)
        {
            newPos = transform.position + maxDelta;
            iTween.MoveTo(gameObject,
            iTween.Hash("position", newPos, "time",
            2.0f * maxDelta.magnitude,
            "easetype", iTween.EaseType.
            easeInOutSine, "islocal", false,
            "oncomplete", "MovingDone",
            "oncompletetarget", gameObject));
            _lastMoveToLocation = newPos;
        }
        else
        {
            _isBusy = false;
        }
    }
    else
    {
        _isBusy = false;
    }
}
private void MovingDone()
{
    _isBusy = false;
}


只有当行为活跃时,我们才会做任何事情,而不是忙碌,而且是在前半秒内。第一件事是——告诉世界我们确实很忙。像所有更新一样,这种方法被称为“每秒60次”,我们希望在这里保持一点控制。比赛条件令人讨厌。

然后我们得到用户正在看的方向上的一个位置,如果这个位置超过了触发距离——或者这是我们第一次到达这里——我们首先通过使用碰撞检测器找到沿着这个凝视我们可以放置实际物体的前方多远。如果这是可能的——也就是说,如果碰撞检测器没有发现任何障碍物——我们实际上可以用它来移动物体。重要的是要注意,无论何时移动是不可能的,_isBusy都会立即设置为false。

另外,请注意,距离越小,移动越快。这是为了确保在正确的位置设置对象的最后调整不会花费很长时间。否则,_isBusy仅在成功移动后重置。

这种行为的最后一部分:

private Vector3 GetPostionInLookingDirection()
{
    RaycastHit hitInfo;
    var headReady = Stabilizer != null
        ? Stabilizer.StableRay
        : new Ray(Camera.main.transform.position, Camera.
        main.transform.forward);
    if (MappingManager != null &&
        Physics.Raycast(headReady, out hitInfo,
        MaxDistance, MappingManager.LayerMask))
        {
            return hitInfo.point;
        }
        return CalculatePositionDeadAhead(MaxDistance);
    }
    private Vector3 CalculatePositionDeadAhead(float distance)
    {
        return Stabilizer != null
            ? Stabilizer.StableRay.origin +
            Stabilizer.StableRay.direction.normalized *
            distance
        : Camera.main.transform.position +
            Camera.main.transform.forward.normalized *
             distance;
}


GetPostionInLookingDirection首先尝试确定您要查看的方向。它试图用稳定器的稳定器来达到这个目的。稳定器是输入管理器的一个组件,它稳定你的视图——光标也使用它。这可以防止光标在你不能保持头部完全静止时摆动太多(大多数人都不会——包括我)。

稳定器平均移动60个样本,这使得看起来不那么紧张。如果你没有定义稳定器,它只需要你实际的观察方向——相机的位置和你的观察方向。

然后,它会试着观察最终的光线是否击中墙壁或地板——但不会超过最大距离。如果它看到命中,它返回这一点。如果没有,它会在空气中给出一个点,最大距离,沿着一条不可见的光线从你的眼睛出来。这就是CalculatePositionDeadAhead所做的——但也尝试首先使用稳定器来找到方向。

检测冲突

好的,那么这个著名的碰撞检测器是什么,它利用空间感知来防止东西被推过墙壁和地板,使全息透镜成为一个如此独特的设备?这其实很简单,尽管我花了一段时间才真正做到如此简单。

using UnityEngine;
namespace LocalJoost.HoloToolkitExtensions {
    public class SpatialMappingCollisionDetector:
        BaseSpatialMappingCollisionDetector {
            public float MinDistance = 0.0 f;
            private Rigidbody _rigidbody;
            void Start() {
                _rigidbody = GetComponent <Rigidbody> () ??
                    gameObject.AddComponent <Rigidbody> ();
                _rigidbody.isKinematic = true;
                _rigidbody.useGravity = false;
            }
            public override bool CheckIfCanMoveBy(Vector3 delta) {
                RaycastHit hitInfo;
                // Sweeptest wisdom from
                //http://answers.unity3d.com/questions/499013/
                cubecasting.html
                return !_rigidbody.SweepTest(delta, out hitInfo, delta.magnitude);
            }
            public override Vector3 GetMaxDelta(Vector3 delta) {
                RaycastHit hitInfo;
                if (!_rigidbody.SweepTest(delta, out hitInfo,
                        delta.magnitude)) {
                    return KeepDistance(delta, hitInfo.point);;
                }
                delta *= (hitInfo.distance / delta.magnitude);
                for (var i = 0; i <= 9; i += 3) {
                    var dTest = delta / (i + 1);
                    if (!_rigidbody.SweepTest(dTest, out hitInfo, dTest.magnitude)) {
                        return KeepDistance(dTest, hitInfo.point);
                    }
                }
                return Vector3.zero;
            }
            private Vector3 KeepDistance(Vector3 delta,
                Vector3 hitPoint) {
                var distanceVector = hitPoint - transform.
                position;
                return delta - (distanceVector.normalized *
                    MinDistance);
            }
        }
}


这种行为首先试图找到一个刚体,如果找不到,就添加它。我们需要这个来检查是否有任何“阻碍”但是——这很重要——我们将把is运动学设置为真,把引力设置为假,否则我们的物体将在Unity物理引擎的控制下掉落在地板上。在这种情况下,我们希望控制对象的移动。

因此,这个类有两个公共方法(它的抽象基类要求这样)。第一,CheckIfCanMoveBy(我们现在不使用它),只是说如果你能在不碰到任何东西的情况下,将你的物体向预定的方向移动预定的距离。另一个基本上做同样的事情,但是如果它在路上发现了一些东西,它也试图找到一个距离,在这个距离上你可以朝着想要的方向移动。为此,我们使用刚体扫描法。本质上,你给它一个向量,一个沿着向量的距离,它有一个输出变量,如果有任何命中,它会给你信息。

如果命中确实发生,它会在最初发现距离的1/4、1/7和1/10处再次尝试。如果一切失败,它将返回一个零向量。通过使用这种粗略的方法,一个物体在几步内快速移动,直到它不能再移动。然后它还会将对象向后移动一段距离,这段距离可以从编辑器中设置。这样可以使物体保持在离地面或墙壁稍高的位置。这就是保持距离的意义。顺便说一句,拥有一个基类BaseSpatialMappingLionCertifier的全部意义在于:a)启用由DefaultMappingCollisionDetector实现的空模式实现,以及b)根据不同的需要制作不同的冲突检测器。这是在统一开发的有时令人困惑的宇宙中的一点建筑考虑。

让它停止:InitialPlaceByTap

使移动通过凝视停止非常简单——将活动字段设置为假。现在我们只需要一些东西来实现它。使用全息工具包,这实际上非常非常简单:

using UnityEngine;
using HoloToolkit.Unity.InputModule;
namespace LocalJoost.HoloToolkitExtensions {
    public class InitialPlaceByTap: MonoBehaviour,
        IInputClickHandler {
            protected AudioSource Sound;
            protected MoveByGaze GazeMover;
            void Start() {
                Sound = GetComponent <AudioSource> ();
                GazeMover = GetComponent <MoveByGaze> ();
                InputManager.Instance.
                PushFallbackInputHandler(gameObject);
            }
            public void OnInputClicked(InputEventData eventData) {
                if (!GazeMover.IsActive) {
                    return;
                }
                if (Sound != null) {
                    Sound.Play();
                }
                GazeMover.IsActive = false;
            }
        }
}


通过实现InputClickHandler,输入管理器将在您空中点击该对象并通过凝视选择它时向它发送一个事件。但是通过将它作为回退处理程序,当它没有被选中时,它也会得到这个事件。事件处理非常简单——如果这个对象中的GazeMover是活动的,它就会被停用。此外,如果检测到音频源,则会播放其声音。我非常推荐这种音频反馈。

把它们连接在一起

在您的多维数据集上,拖动移动透视、空间映射碰撞检测器和初始化位置映射脚本。然后,再次将立方体拖到“移动焦点”的“碰撞检测器”栏上,并将“输入管理器”拖到“稳定器”栏上。统一将选择正确的组件。

因此,在这种情况下,我也可以使用GetComponent < SpatialmaPingCollinionDetector >,而不是您需要拖动的字段。但这种方式更灵活——在应用程序中,我不想使用整个对象的碰撞器,而只想使用子对象的碰撞器。请注意,我已经将空间映射碰撞检测器的思维距离设置为1厘米——它将与墙壁或地板保持额外的1厘米距离。

结束语

所以,这就是你如何或多或少地复制全息图应用程序的部分行为,通过用你的目光移动物体,然后用空气龙头把它们放在表面上。全息透镜的独特功能允许我们将对象放置在物理对象的旁边或上面,全息工具包使得使用这些功能变得非常容易。

完整的代码,根据我的MVP“商标”can be found here

This article is featured in the first ever DZone Guide to Game Development. Get your free copy for more insightful articles, industry statistics, and more!