麦田物语第十八天

系列文章目录

麦田物语第十八天



一、(Editor)制作 [SceneName] Attribute 特性

在本节课我们编写Unity的特性Attribute来更好的完善我们项目,具体是什么呢,就是当我们在Inspector面板看到的属性面板类似于滑动条一样的类型(本来Sell Percentage只是float的类型的值,因为我们在其前面加上了[Range(0,1)]的Attribute),所有我们自己编写一个新的特性,用作我们跳转场景脚本的场景选择可以使用下拉框的形式进行选择而不是手动输入,这样可以避免因为输入错误导致无法跳转场景等问题,从而使得我们在开发过程中获得更多的便捷。

注意:我们要添加的是Editor的功能,但是我们编写代码的文件夹位置不能放在Editor中,否则就无法使用。

首先我们先创建脚本,该脚本的命名由固定的方法(以Attribute结尾,一定不要拼写错误),然后创建Scripts->Utilities->Attribute->SceneNameAttribute脚本,打开该脚本,在这个脚本中我们不需要写其他的代码,只是将其改成继承自PropertyAttribute即可。

SceneNameAttribute脚本代码如下:

using UnityEngine;

public class SceneNameAttribute : PropertyAttribute
{
    
}

解释:Property代表我们Inspector面板的每一个变量,一个String类型,int类型,Vector3类型都是一个Property。

除了上述的代码之外,我们还需要更改Property的绘制,创建Scripts->Utilities->Attribute->SceneNameDrawer脚本,麦扣老师使用的是自动填写的模板,但是我不知道怎么使用,这里我就自己手敲出来了,模板截图如下:
SceneNameDrawer脚本的模板截图
同时将报红的地方更改为我们编写的SceneNameAttribute即可,下面就需要编写这个代码啦!

解释:Rect包括其高度,宽度,xyz的值
SerializedProperty代表的是我们对应标记了SceneName的那个Property,此处我们会在TransitionManager和Teleport中使用。
GUIContent在Unity官方文档的解释就是“构建一个仅包含文本的 GUIContent 对象,同时可以在其里面添加代码使其具备其他的功能”,和Eclipse里面的内容比较相似,如果可以了解的话自己查一下Unity官方文档也可以。

接着我们就可以在TransitionManager脚本的StartSceneName变量上添加我们最新定义的Attribute,还有Teleport脚本的SceneToGo变量也要添加,虽然添加之后我们会发现啥都没有,这是因为我们还没有编写具体代码但是覆盖了原有的写法,接着我们来编写具体的代码吧!
首先我们需要定义一个场景编号sceneIndex,并初始值为-1,还要定义一个GUIContent的数组sceneNames,将每一个场景(名称)都被保存为GUIContent,最后由于我们要将取得的场景的名称从文件目录的形式中分离出来,我们就必须定义分隔符scenePathSplit。
然后是OnGUI代码的编写,首先,如果我们发现Build Setting中的场景数量为0,那么代表此时不能进行这个操作,直接return。
我们怎么拿到场景的名称呢,这是就需要找到Scene变量,它的描述是*.unity 文件的运行时数据结构,通过这个我们就可以得到其位置,然后通过String的方法去获得场景的名称。这个我们在GetSceneNameArray方法中编写。

这个也可以查看官方文档:
https://docs.unity.cn/cn/2021.3/ScriptReference/SceneManagement.Scene.html

在GetSceneNameArray中我们需要先得到在Build Setting中场景的目录scenes(类型为EditorBuildSettingsScene[]),然后在将sceneNames数组初始化,即初始化数组的大小为场景数量,接着我们需要遍历scenes变量,将其通过split方法分割成只有场景名称的字符串(删除后面的.unity以及前面的/),分割成字符串后我们要判断其长度是否大于0,如果大于0,那么将其赋值给sceneNames,反之将其text设置为Deleted Scene,代表虽然找到了,但是被删除了。

Split方法具有多个重载,我们使用我们定义的分隔符,并且舍去空余的部分,我们可以查找Unity官方文档去看看其他的使用方法。

接着我们还要判断如果没有发现场景,代表我们Build Setting中没有添加场景,我们编写这段代码提醒自己:

sceneNames = new[] { new GUIContent(“Check Your Build Settings”) };

其实还有问题,就是当我们重新添加脚本时会发现,刚开始没有赋初值的话应该为空,导致无法选择,那么我们就需要对这个空值进行调整:
我们先进行判断property的StringValue是否为空或者Null(因为我们的场景为String类型并且Property无法自动识别Value值的类型,所以选择StringValue就可以),如果为空的话,那么将sceneIndex赋值为0,如果不为空的话,代表我们给其手动敲了,因此我们要判断我们敲的那个名称是否正确,正确的话,我们在给sceneIndex赋值为对应的那个场景序号,同时将bool值nameFound设置为true代表找到了并退出循环,那我们输入错误了捏,我们也将sceneIndex赋值为0就好了。
最后我们将property的内容更改为sceneIndex所表示的数组位置的内容。
这样GetSceneNameArray方法就编写完成了,我们返回OnGUI的编写:
我们初始化时如果sceneIndex为-1,那么调用GetSceneNameArray方法,然后我们要将其画成弹出式窗口,EditorGUI.Popup方法也有很多重载,我们也可以去官方查看一下(我之前也没用过),这样编写完成之后我们就会有下拉菜单,但是不能进行选择。
我们要接着做的工作就是点按切换里面的内容:我们将点击完成之后的值返回sceneIndex,并存储点击之前的sceneIndex,如果发生改变的话,就再次使用property.stringValue = sceneNames[sceneIndex].text;该赋值语句。

SceneNameDrawer脚本代码如下:

using UnityEngine;
using UnityEditor;

[CustomPropertyDrawer(typeof(SceneNameAttribute))]
public class SceneNameDrawer : PropertyDrawer
{
    int sceneIndex = -1;
    GUIContent[] sceneNames;

    readonly string[] scenePathSplit = { "/", ".unity" };

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (EditorBuildSettings.scenes.Length == 0) return;

        //尝试绘制
        if (sceneIndex == -1)
            GetSceneNameArray(property);

        //将其绘制成弹出式窗口,并保存选择的数组序号
        int oldIndex = sceneIndex;
        sceneIndex = EditorGUI.Popup(position, label, sceneIndex, sceneNames);

        if (sceneIndex != oldIndex)
            property.stringValue = sceneNames[sceneIndex].text;
    }

    private void GetSceneNameArray(SerializedProperty property)
    {
        var scenes = EditorBuildSettings.scenes;
        //初始化数组
        sceneNames = new GUIContent[scenes.Length];

        for (int i = 0; i < sceneNames.Length; i++)
        {
            string path = scenes[i].path;
            //具有多个重载,我们使用我们定义的分隔符,并且舍去空余的部分。
            string[] splitPath = path.Split(scenePathSplit,System.StringSplitOptions.RemoveEmptyEntries);

            string sceneName = "";

            if (splitPath.Length > 0)
            {
                sceneName = splitPath[splitPath.Length - 1];
            }
            else
            {
                sceneName = "(Deleted Scene)";
            }
            //找到了场景的名字之后,通过new GUIContent的方法生成新的GUIContent并添加到数组中(GUIContent的构造函数)
            sceneNames[i] = new GUIContent(sceneName);
        }

        //如果经过上述操作后还是没有选项的话,那么就需要检查BuildSettings的设置。
        if (sceneNames.Length == 0)
        {
            sceneNames = new[] { new GUIContent("Check Your Build Settings") };
        }

        //如果我们将脚本挂载新物体上时,那么这时这个选项就是空的,所以我们需要在选项为空时为其赋初值
        if (!string.IsNullOrEmpty(property.stringValue))
        {
            //如果不为空的话,代表我们之前自己手打上去的,但是我们要对这个东西进行判断是否敲对了,敲对了就直接赋值了
            bool nameFound = false;

            for (int i = 0; i < sceneNames.Length; i++)
            {
                if (sceneNames[i].text == property.stringValue)
                {
                    sceneIndex = i;
                    nameFound = true;
                    break;
                }
            }

            if (nameFound == false)
                sceneIndex = 0;
        }
        else
        {
            sceneIndex = 0;
        }

        //此时我们已经选择好了选择框里面的内容,就可以将其赋值了
        property.stringValue = sceneNames[sceneIndex].text;
    }
}

本节课的内容就先这样啦!!!

二、场景切换淡入淡出和动态 UI 显示

本节课要制作在我们切换场景时的加载动画,通过增加过渡动画从而使得跳转场景更加自然。
首先我们需要新建一个Canvas,并设置其Sort Order为2,使其显示在所有的UI之前,同时更改Canvas Scaler组件的设置,如下图所示。
Canvas Scaler组件的设置
接着创建一个Panel,然后注意Panel的背景是圆角边,但是我们需要实现全遮挡的效果,因此我们可以更改Panel的Image组件,将其Source Image设置为Square,但是我们如果搜索的话没办法直接找到,我们可以点击那个眼睛的按钮,就可以找到了,然后选用第一个即可;同时调整该Panel的颜色的不透明度,使其变成黑色完全遮住UI。
找到Square的方式
然后给该Panel改名为Fade Panel,并为其添加组件Canvas Group,可以通过调整该组件的Alpha属性调整该panel的透明度,同时记得取消勾选Canvas Group组件的Blocks Raycasts。(因为我们这个组件不能遮挡鼠标的点击)

提示:添加了Canvas Group组件的子物体也会随着其Alpha值变为0而消失,变为1而显现。

接下来的场景我们就可以自定义了,麦扣老师的最后效果是这样的。(给跑动的小人图标添加Animator组件并创建相应的Animator Controller)
Fade Panel的最终效果
然后我们就可以使用代码去调用这个UI界面的显示和隐藏了:
首先我们在TransitionManager脚本中先声明fadeCanvasGroup变量,在Start方法中通过其身上的组件获取它(因为目前只有一个物体挂载了Canvas Group组件),我们要做的就是写一个携程,让其Alpha从0-1,1-0之间进行切换;接着还要创建一个bool类型的变量isFade,用来表示canvas是否被隐藏。
接着我们来编写协程Fade啦,通过传递targetAlpha参数来实现切换的目标值是多少;在协程开始时我们现将isFade设置为true,同时将该canvas设置为不可点击状态。
然后我们需要隆重讲解一下这两个公式,因为我每次写这种协程都不会写。

下面将这两句代码摘出来

float speed = Mathf.Abs(fadeCanvasGroup.alpha - targetAlpha) / Settings.fadeDuration;

首先我们需要定义一个float的类型的速度,这个速度则是将目标的Alpha和当前的Canvas 的Alpha相减的绝对值/我们定义的转换时间。(在Settings中新添加的fadeDuration变量,并将原来的人物消失的时间变量改名为itemFadeDuration)

while(!Mathf.Approximately(fadeCanvasGroup.alpha,targetAlpha))
            {
                fadeCanvasGroup.alpha = Mathf.MoveTowards(fadeCanvasGroup.alpha,targetAlpha, speed * Time.deltaTime);
                yield return null;
            }

由于float类型用“=”进行比较时不是很准确,我们使用Mathf.Approximately这个方法,如果不相等的话,就一直执行里面的方法直至相等。
接着是里面调用的Mathf.MoveTowards方法,我们将fadeCanvasGroup的alpha值从当前值缓慢移动到目标值即可。
最后将fadeCanvasGroup改为不遮挡鼠标的状态,并将IsFade改为false就好了。
然后就返回Transition脚本,我们需要调用这个协程的方法,但是我们还要注意一点就是,我们为了避免人物的偏饭进出房间导致频繁切换场景,我们在OnTransitionEvent方法中开启协程时先判断是否为IsFade的变量。
最后我们还要在Transition协程中调用这个协程Fade,使其完成显现和消失的效果。

TransitionManager脚本的代码如下:(本节主要是Fade协程的编写)

namespace MFarm.Transition
{
    public class TransitionManager : MonoBehaviour
    {
        [SceneName]
        public string startSceneName = string.Empty;

        private CanvasGroup fadeCanvasGroup;
        private bool isFade;

        private void Start()
        {
            StartCoroutine(LoadSceneSetActive(startSceneName));
            fadeCanvasGroup = FindObjectOfType<CanvasGroup>();
        }

        private void OnEnable()
        {
            EventHandler.TransitionEvent += OnTransitionEvent;
        }

        

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


        private void OnTransitionEvent(string sceneToGo, Vector3 positionToGo)
        {
            if(!isFade)
                StartCoroutine(Transition(sceneToGo, positionToGo));
        }

        private IEnumerator Transition(string sceneName, Vector3 targetPosition)
        {
            EventHandler.CallBeforeSceneUnloadEvent();

            yield return Fade(1);

            yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene());

            yield return LoadSceneSetActive(sceneName);

            //切换场景之后需要将角色的坐标移动到合适的位置啦
            EventHandler.CallMoveToPosition(targetPosition);

            EventHandler.CallAfterSceneLoadedEvent();

            yield return Fade(0);
        }

        /// <summary>
        /// 加载场景并设置为激活状态
        /// </summary>
        /// <param name="sceneName"></param>
        /// <returns></returns>
        private IEnumerator LoadSceneSetActive(string sceneName)
        {
            //协程中异步加载,使用LoadSceneAsync;因为加载场景我们是通过场景叠加的方式,所以模式选取为Additive
            yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
            Scene newScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1);
            SceneManager.SetActiveScene(newScene);
        }


        /// <summary>
        /// 更改fadeCanvasGroup的Alpha值
        /// </summary>
        /// <param name="targetAlpha">1是黑,0是透明</param>
        /// <returns></returns>
        private IEnumerator Fade(float targetAlpha)
        {
            isFade = true;

            fadeCanvasGroup.blocksRaycasts = true;

            float speed = Mathf.Abs(fadeCanvasGroup.alpha - targetAlpha) / Settings.fadeDuration;

            while(!Mathf.Approximately(fadeCanvasGroup.alpha,targetAlpha))
            {
                fadeCanvasGroup.alpha = Mathf.MoveTowards(fadeCanvasGroup.alpha,targetAlpha, speed * Time.deltaTime);
                yield return null;
            }

            fadeCanvasGroup.blocksRaycasts = false;

            isFade = false;
        }
    }
}

我们运行代码发现可以实现场景切换了,但是场景切换时角色还是可以行走动画状态的,还记得上节课在Player脚本中设置的inputDisable变量,我们要判断这个变量为否为false,只有为true时才可以调用移动动画的代码(FixUpdate方法中),同时还要将Update方法中的inputDisable为false的else添加一句代码,使其isMoving变量设置为false,则角色无法移动。

Player脚本中添加的代码如下:

private void Update()
    {
        if (!inputDisable)
            PlayerInput();
        else
            isMoving = false;
        SwitchAnimation();
    }

    private void FixedUpdate()
    {
        if (!inputDisable)
            Movement();
    }
  • 42
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值