Unity游戏环境交互系统

概述

交互功能使用同一个按钮或按钮列表,在不同情况下显示不同的内容,按下执行不同的操作。

按选项个数分类

环境交互系统可分为两种,单选项交互,一般使用射线检测;多选项交互,一般使用范围检测。第一人称游戏单选多选都可以用,因为第一人称人物背对一个可交互对象时显示交互选项让玩家疑惑,所以第一人称使用射线检测或使用人物面前的触发器进行范围检测。第三人称因为人物一般在或者接近画面中心,基本上用以人物为中心的范围检测。

本质上,单选项交互让玩家通过转动视角选择交互选项,在1.可交互对象很密集、2.交互对象不明显的情景,玩家很难准确瞄准交互对象,但开发简单。

多选项交互玩家选择想要的交互选项更容易,开发复杂,需要维护交互对象列表,还有一个选中项标记,在交互对象列表变化时可能还要更新选择项,以免选中项出范围。

按输入方式分类

电脑通过键盘按键(一般是F)执行交互,滑轮选选项;手机通过按屏幕执行交互,上下滑动选选项,这导致二者的实现方式又不一样。电脑通过InputManager或PlayerInput,手机通过UI按钮回调。就是说电脑上的多个选项可以只是Image,手机上的选项有Button。

环境交互系统由这几个部分构成:

  1. 环境检测;
  2. 交互类型判断;
  3. UI部分:控件显示和回调添加、选中项的显示和更新;
  4. 执行;

对于玩家,他能直接感觉到的只有UI显示和按下执行,环境检测、根据类型判断UI显示的内容、根据类型判断执行的类型完全由程序完成,所以玩家感觉不到交互系统的复杂性。

数据结构

一个游戏的交互类型选项是确定的,有限的,适合用枚举表示。把游戏支持的所有交互类型定义为枚举的选项:

public enum ActionOption{
    None,TakeGun,SwapGun,TakeItem,Talk,CheckPack
}

如果是单选项交互,就声明一个交互枚举变量,如果是多选项交互,就声明一个交互枚举列表。

数据结构的维护

对于单选项交互,程序每一帧都执行环境检测,更新交互信息变量,开销还可以接受。

多选项交互如果每帧都删掉交互列表再重新获得开销太大,可以在OnTriggerEnter()添加,OnTriggerExit()移除,但因为执行交互后需要对交互列表更新,此时的更新与触发器进入退出无关,所以清空再刷新交互列表的函数肯定要写。而根据测试,执行拿起物品同时放下身上物品会触发OnTriggerEnter(),如果在OnTriggerEnter()时添加交互项,执行交互后又更新交互项,那么执行拿起并放下物品时拿起刚被放下的物品的交互项就会多一个。总之通过增加、减少元素的方法维护一个列表,如果不能完全预测可能出现增加减少的情况,极易出现多加多减的问题。可靠的做法是把列表清空再重新获得,开销大但是至少保持数据正确性。

综合起来:清空交互列表再重新检测比较稳健,不会造成列表多余或缺少交互项(这个结论适用于任何列表的维护),但每帧更新交互列表开销又大,所以可以在OnTriggerEnter()、OnTriggerExit()和执行交互后三个时机通过Physics.OverlapSphere()更新交互项列表。这样既避免了每帧检测的开销,又保证了数据准确。但是要注意触发器的位置和半径要和Physics.OverlapSphere()的都一致。

然后又发现在执行一个物体A的OnTriggerExit()里面执行Physics.OverlapSphere()范围和触发器范围一样,还能检测到A,导致离开物体的交互区域时捡起它的交互选项没有消失。不过很快又发现原来是Physics.OverlapSphere()的中心点和触发器的中心点不一致。

至此交互项列表的维护问题才基本解决。就是解决两个问题:1.防止交互项列表比实际的多余或缺失;2.防止每帧清空刷新造成的巨大开销。

环境检测

环境交互函数通过射线检测或范围检测得到碰撞体,从中筛选出可交互对象,使用分类函数得到这个交互的类型、对象名称,以此得到显示在界面上的文本、按下需要执行的操作。

筛选可交换对象可以反复用TryGetComponent()把可能有交互的脚本都判断一遍,但因为可交互对象可能是一个物品、一个NPC、一扇门,交互对象类型虽然毫不相干,但是都有“可交互”的特征。这很符合接口的设计初衷,所以可以定义接口:

public interface Interactive
{
    public abstract void InterAct();
}

所有的可交互对象继承可交互接口。射线或范围检测只要在检测到的碰撞体上尝试获得接口,就能得到所有可交互对象。然后发现继承Monobehavior的脚本再继承一个接口,在检查器的显示会报错。不过不看交互管理器的检查器就行。

if(Physics.Raycast(rayOrigin,rayVector,out raycastHit,10,interactionLayerMask)
        &&Vector3.Distance(player.transform.position,raycastHit.point)<interactionRange){
            if(raycastHit.collider.TryGetComponent(out interactive))
            {}
}

再把这个接口变量交给一个函数使用连续的if(interactive is xxx)判断它的具体类型 。

对话交互项的检测

如果玩家站在NPC背后允许发起对话,效果会很奇怪。所以NPC的继承interactive的脚本不应该放在人物身上,而在人物面前的节点,上面挂载一个触发器,最好是扇形的,但Unity没有扇形,只好用盒型或球形代替。

UI显示

界面上放一个显示交互选项列表的锚点,挂载Vertical Layout Group组件,制作一个交互选项预制体,由交互管理器脚本把预制体实例化到锚点下。

用滚轮改变选中的交互选项:使用了InputSystem系统。

void Update(){
        GetScrollInput();
    }
    public void GetScrollInput(){
        float delta=Mouse.current.scroll.ReadValue().y;
        if(delta<0){
            selectedInteraction++;
        }
        else if(delta>0){
            selectedInteraction--;
        }
        selectedInteraction=Mathf.Clamp(
            selectedInteraction,0,actionOptions.Count-1);
        UpdateSelectedInteraction();
    }

更新显示交互选项:使用白色突出选中项,未选中项使用灰色。

void UpdateSelectedInteraction(){
        for(int i=0;i<interactionInfos.Count;i++){
            Text text=interactionInfos[i].
            actionOptionGO.GetComponentInChildren<Text>();
            if(i==selectedInteraction){
                text.color=Color.white;
            }
            else{
                text.color=Color.gray;
            }
        }
    }

执行

使用switch()判断交互的类型执行相应的操作。

和对话系统的关系

对于对话时锁定视角,显示鼠标的对话系统,会需要对话时使交互功能失效。这包括1.隐藏交互菜单;2.关闭对F执行交互的响应;3.最好能关闭对滑轮改变选中的交互的响应。可以给交互管理器写一个开启关闭函数,关闭相应F需要声明一个bool,执行时判断一下再执行。

效果 

和CharacterController配合遇到的大坑

人物挂有CharacterController当人物在交互对象的交互范围内移动和改变仰角时,也会执行OnTriggerEnter()和OnTriggerExit()。但是打印Physics.OverlapSphere()探测到的碰撞体个数,为0.

猜想:CharacterController处理碰撞时是否改写了人物的位置?在循环的某些环节把人物放到了别的位置又放回来?导致人物在不停进出交互范围? 

然后我在OnTriggerEnter()和OnTriggerExit()打印交互管理器的位置,看到了匪夷所思的信息。

这是人物的位置,人物只做了一些小范围移动和改变仰角。

这是打印出来的位置。交互管理器的位置确实被改变了,只在处理碰撞和触发的环节改变了,编辑器显示的看起来没有变化.

我用一个物体标记OnTriggerEnter()和OnTriggerExit()里读到的这些位置,才能看到在物理检测环节交互管理器被放到了哪里。

又标记了OnTriggerEnter()和OnTriggerExit()里人物的位置,也在地形下面,人物小范围移动时上下抽风。在LateUpdate()标记人物的位置,又是正常的,也就是说在每一帧的物理检测环节CharacterController曾经把人物放到下面,帧结束前又把人物放到正常位置。

解决方法:交互管理器不再做玩家人物的子对象,而是独立物体,加一个脚本:

void LateUpdate(){
    transform.position=MyInput.Instance.player.transform.position;
}

既然每帧LateUpdate()里CharacterController会把人物摆好,那就在LateUpdate()里让交互管理器跟随玩家。避免了CharacterController在物理阶段执行一些检测算法时整个对象和子对象跟着它一起“抽风”。

交互管理器不在人物身上之后需要加一个刚体,以便和没有刚体的交互对象碰撞。

开关门

分为检测和执行2部分。

需要的触发器在门开和关着时应该不变,所以这个触发器不应该在门上,应该在“门框”上。门的旋转我选择用animator做。这个animator可以在门上或门框上,不同门的朝向不一样,要保证门相对于门框的旋转一致,才能共用动画。animator放门上的好处是门的名字不需要一致,如果放门框上,门的名字必须一致。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值