Featured image of post 麦田物语开发日记(六)

麦田物语开发日记(六)

完成游戏的第二场景以及场景切换功能

第二场景绘制

设置场景Manager

每次创建新的场景后,都需要先在File->Builder Settings中将所有场景添加进入列表

  • 编写场景转化代码TransitionManager.cs

**思路:**该部分代码主要实现基础的对场景的异步加载,使用协程的方式完成主要的加载函数,然后使用其他函数对该部分内容进行调用

而具体调用的函数主要使用事件中心来触发,以减少单例模式的使用

  • 实现传送点

在场景中设置一个专门的传送点点GO,该GO将传递两个内容,分别是要激活的场景名称和人物生成的坐标,然后调用传送函数即可

实现场景加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// <summary>
/// 实现场景的转换
/// </summary>
/// <param name="sceneName">加载的场景名称</param>
/// <param name="transPoint">传送点</param>
public IEnumerator TransitionScene(string sceneName, Vector3 transPoint)
{
    /* 先卸载当前场景,并使用协程加载当前场景 */
    yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());
    yield return LoadSceneSetActive(sceneName);

    // todo 将人物加载到传送点
}

/// <summary>
/// 使用协程激活场景
/// </summary>
/// <param name="sceneName">需要被激活的场景名称</param>
/// <returns></returns>
public IEnumerator LoadSceneSetActive(string sceneName)
{
    /* 使用协程完成场景加载,并激活场景 */
    yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);    // 由于场景有多个UI和具体场景,因此需要使用叠加的方式
    Scene currentScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
    SceneManager.SetActiveScene(currentScene);

}

然后通过事件中心对 协程进行调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void OnEnable()
{
    EventHandler.TransitionEvenet += OnTransitionEvent;
}

private void OnDisable()
{
    EventHandler.TransitionEvenet -= OnTransitionEvent;
}

private void OnTransitionEvent(string sceneName, Vector3 transPoint)
{
    StartCoroutine(TransitionScene(sceneName, transPoint));
}

而每个传送点的代码则是直接对事件中心进行调用

注意每次游戏初期都会加载某一个场景,因此游戏初期需要将所有场景unload,否则导致同时出现两个被加载的场景,在触发场景切换的时候会多次触发。

完善场景切换的物体切换

由于之前代码中很多内容是在场景建立初期的start中写的,而场景的加载由于会删除场景的物品,因此会导致错误。

为此需要通过设置两个事件函数BeforeSceneUnloadEventAfterSceneloadEvenet两个事件,然后将场景切换会影响的代码需要在注册事件的函数中编写,每次切换场景的时候调用整个事件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* 设置场景切换前后的激活事件 */
public static event Action BeforeSceneUnloadEvent;
public static event Action AfterSceneLoadEvent;

public static void CallBeforeSceneUnloadEvent()
{
    BeforeSceneUnloadEvent?.Invoke();
}

public static void CallAfterSceneLoadEvent()
{
    AfterSceneLoadEvent?.Invoke();
}

增加场景修改位置:

  • 摄像机边界
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// switchBounds.cs
private void OnEnable()
{
    EventHandler.BeforeSceneUnloadEvent += SwitchConfinerShape;
}

private void OnDisable()
{
    EventHandler.BeforeSceneUnloadEvent -= SwitchConfinerShape;
}
  • 修改物品UI并取消高亮显示
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// inventoryUI.cs
private void OnEnable()
{
    EventHandler.UpdateInventoryUI += OnUpdateInventoryUI;
    EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
}

private void OnDisable()
{
    EventHandler.UpdateInventoryUI -= OnUpdateInventoryUI;
    EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
}

private void OnBeforeSceneUnloadEvent()
{
    /*切换场景前取消高亮显示*/
    UpdateSlotHighlight(-1);
}
  • 为每个场景增加itemParent
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// itemManager.cs
private void OnEnable()
{
    EventHandler.InstantiateItemInScene += OnInstantiateItemInScene;
    EventHandler.AfterSceneLoadEvent += OnAfterSceneLoadEvent;
}

private void OnDisable()
{
    EventHandler.InstantiateItemInScene -= OnInstantiateItemInScene;
    EventHandler.AfterSceneLoadEvent -= OnAfterSceneLoadEvent;
}

private void OnAfterSceneLoadEvent()
{
    _itemParent = GameObject.FindGameObjectWithTag("ItemParent");
}
  • 最后将人物举起物品功能进行修正,每次切换场景则放下物品,并让高亮取消

  • 设置人物在场景移动的功能,使用事件中心去通知对应的转移函数。

学习编写特性,并将其设置为下拉菜单

Unity的特性Attribute可以将Unity界面显示的变量(Property)修改为我们期望的样子,比如将手写的string输入框设置为下拉菜单的模式。

而具体的做法是将需要修改的变量property,在前方增加描述[Attribute],然后编写一个用于描述该Attribute的代码SceneNameAttribute,其继承自PropertyAttribute;然后编写一个用于绘制对应GUI的代码SceneNameDrawer,该代码继承自PropertyDrawer,用于描述该特性怎么描述GUI。

注意这两部分代码不能放在原本绘制UI toolkit的位置,否则不能使用,因此放在Utility的位置

1
2
3
4
5
6
7
// SceneNameAttribute的写法,只需要明确有这个特性
using UnityEngine;

public class SceneNameAttribute : PropertyAttribute
{
    
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// SceneNameDrawer.cs ,需要将GUI的内容都确定下来,并对其进行赋值
using System;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(SceneNameAttribute))]
public class SceneNameDrawer: PropertyDrawer
{
    private int sceneIndex = -1;    // 当前选取的场景编号
    private string[] _sceneNameSplits = { "/", ".unity" };  // 由于BuildingSetting中场景有路径,因此需要被删除一部分
    private GUIContent[] _sceneNames; // 显示在GUI上的文本内容
    
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        /*如果场景数为0,则不再绘制*/
        if (EditorBuildSettings.scenes.Length  == 0) return;
        
        /*从buildSetting中找出所有的场景名称*/
        if (sceneIndex == -1)
            GetSceneNameArray(property);

        int oldIndex = sceneIndex;

        /*绘制GUI,并设置GUI点击的内容为我们的目标值*/
        // Popup参数分别是绘制的位置,显示的载体,要显示的内容的需要,下拉菜单的具体内容
        sceneIndex = EditorGUI.Popup(position, label, sceneIndex, _sceneNames);
        
        if (oldIndex != sceneIndex)
            property.stringValue = _sceneNames[sceneIndex].text;
    }

    private void GetSceneNameArray(SerializedProperty property)
    {
        // 获得当前BuildingSetting中所有的场景数据
        var scenes = EditorBuildSettings.scenes;
        /*对获取场景数据的函数进行初始化*/
        _sceneNames = new GUIContent[scenes.Length];
        
        /*通过循环将场景名称放入GUI的数组内容中*/
        for (int i = 0; i < scenes.Length; i++)
        {
            string path = scenes[i].path;
            string[] scenePath = path.Split(_sceneNameSplits, StringSplitOptions.RemoveEmptyEntries);
            
            string sceneName = String.Empty;

            if (scenePath.Length > 0)
            {
                sceneName = scenePath[scenePath.Length - 1];
            }
            else
            {
                sceneName = "(Delete Scene)";
            }

            _sceneNames[i] = new GUIContent(sceneName);
        }

        if (_sceneNames.Length == 0)
            _sceneNames = new[] { new GUIContent("Check your Build Settings") };
        
        /*对property中的内容进行赋值,首先需要确保如果内容不为空,那么其必须是我们设置的场景名称*/
        if (!string.IsNullOrEmpty(property.stringValue))
        {
            bool nameFound = false;
            for (int i = 0; i < _sceneNames.Length; i++)
            {
                if (property.stringValue == _sceneNames[i].text)
                {
                    sceneIndex = i;
                    nameFound = true;
                    break;
                }
            }

            if (!nameFound)
            {
                sceneIndex = 0;
            }
            
        }
        else
        {
            sceneIndex = 0;
        }
        
        /*用刚刚获得sceneIndex对应的场景名,对property进行赋值*/
        property.stringValue = _sceneNames[sceneIndex].text;
    }
    
}

过渡动画

思路:通过设置一个新的Canvas,增加一个覆盖层,通过对该覆层的透明度进行修改来实现过渡动画,该覆盖层可以通过设置CanvasGroup组件来保证其子物体也能一同进行淡入淡出的过渡。

而具体的执行函数将在TransitionManager.cs中通过协程进行完成。

注意:为了保存新的覆层能够覆盖整个屏幕,需要将该Canvas的排序进行修改

具体做法,首先设置一个Canvas,设置号Canvas相关的属性后,屏幕尺寸和排序(必须),然后在Canvas下创建一个Panel,即该Panel就是一个覆层。然后对该Panel设置对应的CanvasGroup以保证画布中的内容能一同渐变,然后通过的代码对整个Canvas进行控制即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/// <summary>
/// 场景渐变
/// </summary>
/// <param name="targetAlpha">1为全覆盖,0为不覆盖</param>
/// <returns></returns>
public IEnumerator SceneFader(float targetAlpha)
{
    isFading = true;

    sceneFaderCanvasGroup.blocksRaycasts = true;    // 当前图片不能点击
    
    /*使用数学方法计算场景的渐变过程速度*/
    float speed = Mathf.Abs(targetAlpha - sceneFaderCanvasGroup.alpha) / Settings.SceneFaderDuration;

    while (!Mathf.Approximately(sceneFaderCanvasGroup.alpha, targetAlpha))
    {
        // 如果没达到目标,则不断将当前的透明度向目标移动
        sceneFaderCanvasGroup.alpha = Mathf.MoveTowards(sceneFaderCanvasGroup.alpha,
            targetAlpha, speed * Time.deltaTime);
        yield return null;
    }

    isFading = false;
    sceneFaderCanvasGroup.blocksRaycasts = false;
}

其中isFading用于标志只用当渐变结束,才能移动目标

保存和加载场景物品

思路:每次卸载场景的时候,用一个内容记录场景中所有物体的内容和坐标,然后加载场景的时候读取并生成即可。因此使用一个哈希表用于存储相关内容,key为场景名称,value为需要存储数据的列表。

注意在生成数据的时候,需要先将场景中原有的数据都删除,防止重复生成。

由于Vector3在一个类中不能被序列化,因此需要设计一个专门的类用来描述Vector3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 在dataCollection.cs中描述保存Item位置的变量
/// <summary>
/// 定义一个能被Unity识别的Vector3
/// </summary>
[System.Serializable]
public class SerializableVector3
{
    public float x, y, z;

    public SerializableVector3(Vector3 pos)
    {
        this.x = pos.x;
        this.y = pos.y;
        this.z = pos.z;
    }

    public Vector3 ToVector3()
    {
        return new Vector3(x, y, z);
    }
    
    /*获得瓦片地图的位置*/
    public Vector2 ToVector2()
    {
        return new Vector2((int)x, (int)y);
    }
}

/// <summary>
///  定义场景中item的描述类
/// </summary>
[System.Serializable]
public class SceneItem
{
    public int itemId;
    public SerializableVector3 itemPos;
}
  • 存储与读取
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/// <summary>
/// 将场景中所有item存储进入字典中
/// </summary>
private void GetAllSceneItem()
{
    /*获取当前场景的所有item*/
    List<SceneItem> currentItems = new List<SceneItem>();

    foreach (var item in FindObjectsOfType<Item>())
    {
        SceneItem sceneItem = new SceneItem
        {
            itemId = item.itemID,
            itemPos = new SerializableVector3(item.transform.position)
        };
        currentItems.Add(sceneItem);
    }
    
    /*将场景中所有的item存放在对应的dict中*/
    if (_sceneItemDict.ContainsKey(SceneManager.GetActiveScene().name))
    {
        _sceneItemDict[SceneManager.GetActiveScene().name] = currentItems;
    }
    else
    {
        _sceneItemDict.Add(SceneManager.GetActiveScene().name, currentItems);
    }
    
}

/// <summary>
/// 重建当前场景中的所有item
/// </summary>
public void ReBuildSceneItems()
{
    List<SceneItem> currentItems = new List<SceneItem>();
    /*获取字典中当前场景item的存储*/
    if (_sceneItemDict.TryGetValue(SceneManager.GetActiveScene().name, out currentItems))
    {
        if (currentItems != null)
        {
            // 先将当前场景清场,防止有代码遗漏的物品
            foreach (var item in FindObjectsOfType<Item>())
            {
                Destroy(item.gameObject);
            }
            
            // 重建所有内容
            foreach (var item in currentItems)
            {
                Item newItem = Instantiate(itemPrefab, item.itemPos.ToVector3(),
                    Quaternion.identity, _itemParent.transform);
                newItem.InitItem(item.itemId);
            }
        }    
    }
    
}
Built with Hugo
Theme Stack designed by Jimmy