第 8 课

目录

为重叠的 State 区域创建系统

通过在进入 Trigger 时设置 State(状态),我们可以让音乐随着 Player 所在位置动态地变换。这种方式对没有重叠、相互分离的区域来说挺好,但是一旦存在重叠就会出现一个问题。我们来看看下面的 Village Trigger 和 Woodlands Trigger 的概念图。

图中的 Woodlands Trigger 与 Village Trigger 有一小部分重叠。在 Player 从 Village(村庄)前往 Woodlands(林地)并进入 Woodlands Trigger 时,State 将会切换到 Woodlands。不过,根据前面章节中运用的逻辑,随着 Player 深入 Woodlands 并远离 Village,OnTriggerExitState() 函数将把全局 Music_Regions State 设为 Nowhere。如此一来,就会出现 Player 身在 Woodlands 区域而听到 Ambient Music 的矛盾情形。

为了解决这一问题,我们可以构建一个相应的系统,同时创建 State 列表,来记录 Player 进入的每个 Trigger,以此统一管理音乐主题的优先级。

有了这样的系统,便可在 Player 进入 Trigger 时累计 State 列表所含条目数,从而准确获知 Player 目前在多少个 Trigger 之内。一旦 Player 离开 Trigger,该系统就会马上检查其是否进入另一 Trigger。若否,则将 State 重新设为 Nowhere。接下来,我们将使用前面章节中创建的 SetMusicState 脚本,并加以扩展来兼顾存在重叠的多个 Trigger。为了实现这一点,我们将创建一个 State 列表,并让其随着 Player 进入和离开区域 Trigger 来动态地更新。

  1. 在 Unity 菜单栏中,依次转到 Audiokinetic > Certification > 301 > Lesson 8,然后选择 Creating a System for Intersecting State Areas

    在本项练习中,我们将使用 Village 和 Woodlands 音乐区域。两者都需要关联前面练习中创建的 SetMusicState 脚本。

  2. 在 Hierarchy 中,同时选中 Village Music TriggerWoodlands Music Trigger 游戏对象。

    [技巧]

    您可以按住 Shift 或 Ctrl 来同时选中这两个游戏对象。

    在同时选中多个游戏对象的情况下,假如在 Inspector 中单击 Add Component,将会把组件一并添加到全部所选游戏对象。

  3. 在 Inspector 中,单击 Add Component,然后搜索并选中 SetMusicState

    在打开脚本之前,我们先来指派 Wwise 专有 State。注意,要确保两个 Music Trigger 仍处于选中状态。

  4. 在同时选中 VillageWoodlands Music Trigger 游戏对象的情况下,单击 On Trigger Exit State 属性,然后依次转到 MusicStates > Music_Regions,并双击 Nowhere

    另外,我们还要单独编辑 Trigger,以便为其设置不同的 State。

  5. 在 Hierarchy 中,仅选中 Village Music Trigger 一项。

  6. 在 Inspector 中,单击 OnTriggerEnterState,然后依次转到 MusicStates > Music_Regions,并双击 Village

  7. 在 Hierarchy 中,仅选中 Woodlands Music Trigger 一项。

  8. 在 Inspector 中,单击 OnTriggerEnterState,然后依次转到 MusicStates > Music_Regions,并双击 Woodlands

    接下来,我们打开脚本并执行必要的修改。

  9. 在 Inspector 中,双击 SetMusicState 脚本。

    接下来,我们要创建列表并以此记录游戏当中 Player - Trigger 交互导致的 State 变化。为了创建列表,我们需要键入以下内容。

                                List< >  
     

    在尖括号之内,需要声明列表中所要存储的属性类型。在此,我们将使用 Wwise 类类型 AK.Wwise.State(键入方式跟指派属性时一样)。

  10. 打开 SetMusicState 脚本,然后在 SetMusicState 类的最上面添加一个空白行,并键入 List<AK.Wwise.State>

         
        using System.Collections;
        using System.Collections.Generic;
        using UnityEngine;
    
        public class SetMusicState : MonoBehaviour {
            List<AK.Wwise.State>
            public AK.Wwise.State OnTriggerEnterState;
            public AK.Wwise.State OnTriggerExitState;
            private void OnTriggerEnter(Collider other){
                if(other.CompareTag("Player")){
                    OnTriggerEnterState.SetValue();
                }
             }
            private void OnTriggerExit(Collider other){
                if(other.CompareTag("Player")){
                    OnTriggerExitState.SetValue();
                }
             }

    现在我们声明了 Wwise 专有 State 列表,不过接下来还要为其命名。

  11. List<AK.Wwise.State> 之后,将其命名为 ListOfStates

    public class SetMusicState : MonoBehaviour {
        List<AK.Wwise.State> ListOfStates

    现在 List 创建好了。不过,为了能从其他脚本看到它并确保可向其添加 State,我们还要稍微修改一下。首先,为其添加 public 修饰符。这样的话在该类之外也能看到 State。

  12. 在列表之前插入 public

    public class SetMusicState : MonoBehaviour {
        public List<AK.Wwise.State> ListOfStates

    接下来,就跟平时列购物清单要写明品目一样,我们需要将列表实例化,然后慢慢往里面添加内容。为此,可使用 new 修饰符来将列表实例化。

  13. public List<AK.Wwise.State> ListOfStates 之后,添加 = new List<AK.Wwise.State>();

     public class SetMusicState : MonoBehaviour {
        public List<AK.Wwise.State> ListOfStates = new List<AK.Wwise.State>();

    在前面的练习中,我们创建了两个函数:OnTriggerEnter() 和 OnTriggerExit()。不过,Village Trigger 和 Woodlands Trigger 使用了相同的脚本,而且将函数的适用范围设成了 private,所以各个 Trigger 会分别调用自身对应的 OnTriggerEnter() 和 OnTriggerExit() 函数实例。这样的话就可以将每个绑定了 SetMusicState 脚本的 Music Trigger 分别添加到新建的 State 列表。不过,在针对各个游戏对象将脚本实例化时,如何确保所有脚本都引用相同的列表呢?答案是使用 static 修饰符。在将列表设为 static 后,任何时候都只能有一个列表。如此一来,所有脚本都将引用同一列表。

  14. public 修饰符之后插入 static

     public class SetMusicState : MonoBehaviour {
        public static List<AK.Wwise.State> ListOfStates = new List<AK.Wwise.State>();

    接下来,我们可以通过修改 OnTriggerEnter() 和 OnTriggerExit() 函数,来使其允许添加和移除此列表中的 State。在继续执行下一步之前,我们来回到前面展示的 Trigger 示意图。假定 Player 从 Village 出发,然后前往 Woodlands。在 Player 进入 Woodlands Trigger 时,会在列表最上面插入 Woodlands State。

    倘若 Player 继续深入 Woodlands,会直接从列表中移除 Village State,而最上面的 State (Woodlands) 将保持不变。不过,若是 Player 选择返回 Village,在将要离开 Woodlands 但还没离开 Village Trigger 时,则将移除 Woodlands State 并设置一个新的顶部 State (Village)。

    为了创建这样的一个系统,我们可以针对列表应用 Insert() 函数,以便将某个 State 插入到列表中的特定位置。然后,在 Insert 函数的圆括号之内,给出相应的插入位置,并在后面跟随 State 属性。因为只需在列表的最上面插入 State,所以我们可以直接在列表中将位标声明为 0(所有列表以 0 为起点,然后是 1、2、3…)。

  15. OnTriggerEnter() 函数之内,将 OnTriggerEnterState.SetValue(); 替换为 ListOfStates.Insert(0, OnTriggerEnterState);

        public static List<AK.Wwise.State> ListOfStates = new List<AK.Wwise.State>();
        public AK.Wwise.State OnTriggerEnterState;
        public AK.Wwise.State OnTriggerExitState;
        private void OnTriggerEnter(Collider other){
            if(other.CompareTag("Player")){
                ListOfStates.Insert(0, OnTriggerEnterState);
            }
         }

    Insert() 函数永远不会改写目标位置,而只会将列表中的其余元素下移。

    为了访问列表中的第一个元素,我们可以使用方括号 [ ] 来封装所要访问的元素的位置。因为列表最上面的元素对应于最近进入的区域,所以不妨直接将值设为 0 。为了获取最上面的 State,我们先要声明列表的名称,然后添加一个左方括号,接着键入数字 0,最后添加一个右方括号。在获取 State 之后,便可调用 SetValue() 函数(如“第 5 课”所述)。

  16. 在 ListOfStates.Insert(0, OnTriggerEnterState); 下面添加一个空白行,然后键入 ListOfStates[0].SetValue();

    public static List<AK.Wwise.State> ListOfStates = new List<AK.Wwise.State>();
    public AK.Wwise.State OnTriggerEnterState;
    public AK.Wwise.State OnTriggerExitState;
    private void OnTriggerEnter(Collider other){
        if(other.CompareTag("Player")){
            ListOfStates.Insert(0, OnTriggerEnterState);
            ListOfStates[0].SetValue();
        }
    }

    现在会将 State 添加到列表并为其设定位置值,但并不会在离开 Trigger 时从列表中移除 State。简单来说,在 Player 离开 Village Trigger 时,应当从列表中移除 Village Trigger 的 State,同时设置列表最上面的 State。

    为了从列表中移除某个元素,我们可以使用列表的 Remove() 函数。它将会查找并移除指定的 State,而不论其处在哪个位置。

  17. 在 OnTriggerExit() 函数之内,将 OnTriggerExitState.SetValue(); 替换为 ListOfStates.Remove(OnTriggerEnterState);,然后保存脚本

        private void OnTriggerExit(Collider other){
            if(other.CompareTag("Player")){
                ListOfStates.Remove(OnTriggerEnterState);
            }
         }

    接下来,我们直接试玩游戏,并测试一下整合效果。

  18. 在 Unity 中,单击 Play 按钮,然后跑进 Village Trigger,接着走到小桥上但不要离开 Village Trigger,最后再返回 Village。

    我们注意到,在离开 Woodlands Trigger 和 Village Trigger 的重叠区域时,音乐没有切换回 Village 主题。这是因为在移除元素时不会重复设置 State。也就是说,在移除列表中的某个元素之后,需要重新设置最上面的 State。现在您可以退出游戏了。

  19. ListOfStates.Remove(OnTriggerEnterState); 下面添加一个空白行,然后键入 ListOfStates[0].SetValue();

    private void OnTriggerExit(Collider other){
        if(other.CompareTag("Player")){
            ListOfStates.Remove(OnTriggerEnterState);
            ListOfStates[0].SetValue();
        }
    }

    别着急,马上就完成了!只剩一个问题了。在没在任何 Music State Trigger 之内时,应当设置 Nowhere State。根据目前的整合情况,列表将会变为空白,进而收到 Null Reference Exception 消息。为了解决这一问题,我们需要添加一个 If 语句,来判定列表是否包含元素。若否,则设置 Nowhere State。为此,可使用 .count 属性来检查列表中实际元素的个数;若个数大于 0,则表示列表不是空的。

  20. ListOfStates.Remove(OnTriggerEnterState); 下面添加一个空白行,然后键入 if(ListOfStates.Count > 0){

        if(other.CompareTag("Player")){
            ListOfStates.Remove(OnTriggerEnterState);
            if(ListOfStates.Count > 0){
                ListOfStates[0].SetValue();
        }

    我们注意到,此行中只输入了左大括号 {,而没有输入右大括号 }。因为我们需要只在条件为 true 时运行 ListOfStates[0].SetValue();,所以应当在 ListOfStates[0].SetValue(); 之后键入 },以此定义在 If 语句为 true 时所要运行的代码段。

  21. ListOfStates[0].SetValue(); 下面添加一个空白行,然后键入右大括号 }

        if(other.CompareTag("Player")){
            ListOfStates.Remove(OnTriggerEnterState);
            if(ListOfStates.Count > 0){
                ListOfStates[0].SetValue();
            }
        }

    现在只会在列表不为空时设置列表中的数值。但是,如何针对所有不含 Music Trigger 的区域设置 Nowhere State 呢?假如列表计数为 0,应当设置 Nowhere State 才对。接下来,我们使用 if 语句以及 else{} 条件来判定列表是否为空。

    利用 Else 语句,可将任何条件为 false 的调用指向 Else 语句的大括号内包含的代码。

  22. 在 If 语句的右大括号之后,键入 else{

            if(other.CompareTag("Player")){
                ListOfStates.Remove(OnTriggerEnterState);
                if(ListOfStates.Count > 0){
                    ListOfStates[0].SetValue();
                }else{
            }

    [备注]

    若代码编辑器自动完成 Else 语句并插入右大括号,则可直接跳过下一步。

  23. 按下 Enter 两次,并键入右大括号 }

            if(ListOfStates.Count > 0){
                ListOfStates[0].SetValue();
            }
            else{
    
            }

    现在,只要调用 else 代码段,就会将 State 设为 Nowhere。这样便可确保在 Player 没在任何 Music Region Trigger 之内时,将全局 State 设为 Nowhere 并让音乐播放 Ambient 主题。

  24. 在 Else 语句的大括号之内,键入 OnTriggerExitState.SetValue();

        public class SetMusicState : MonoBehaviour {
            public static List<AK.Wwise.State> ListOfStates = new List<AK.Wwise.State>();
            public AK.Wwise.State OnTriggerEnterState;
            public AK.Wwise.State OnTriggerExitState;
            private void OnTriggerEnter(Collider other){
                if(other.CompareTag("Player")){
                    ListOfStates.Insert(0, OnTriggerEnterState);
                    ListOfStates[0].SetValue();
                }
            }
            private void OnTriggerExit(Collider other){
                if(other.CompareTag("Player")){
                    ListOfStates.Remove(OnTriggerEnterState);
                    if(ListOfStates.Count > 0){
                        ListOfStates[0].SetValue();
                    }else{
                        OnTriggerExitState.SetValue();
                    }
                }
            }

    下面来试试吧!

  25. 在 Unity 中,单击 Play 按钮,然后跑进 Village Trigger,接着走到小桥上但不要离开 Village Trigger,最后再返回 Village。

    在进入 Village 时,最初会播放 Village 主题;一旦进入小桥上的 Trigger 重叠区,便会过渡到 Woodlands 主题;倘若此时返回 Village,则会再次播放 Village 主题。

  26. 继续深入 Woodlands Trigger,直到离开 Village Trigger。

    随着 Player 深入 Woodlands 并远离 Village,Village 主题将会过渡到 Woodlands 主题,并保持播放状态,直到再次进入 Village Trigger。按照我们添加到脚本中的条件,只有在 Player 没在任何 Music Trigger 之内时,才会设置 Nowhere State。现在您可以退出游戏了。

这样我们便创建了一个可兼顾多个 State Trigger 的系统。假如这是您第一次编写代码,那么您应当为目前取得的成果感到自豪。虽然您可能并不认为自己是一名程序员,但现在还是学会了如何使用各种基本的编程方法,而这些正是许多程序员每天用到的东西。通过灵活地运用 List 函数(Remove 或 Insert),您几乎可以控制任何类型的属性或变量。在开发音频集成以外的东西时,程序员一样会使用其中的很多方法。总之,通过对 If 语句和 List 函数等各种方法的简单学习,您最终大致了解了如何进行编程。