麦田物语AStar算法(二)--- 测试 + 具体实现

系列文章目录

文章目录


前言

在上一个博客中我们讲述了AStar算法的理论知识,并且编写了AStar算法怎么计算路径,这次我们利用堆栈来进行路径的存储,并且对我们之前编写的代码进行测试,最后我们将把AStar算法应用到NPC身上。


一、AStar算法的测试

我们为了记录NPC移动的时间戳,因此需要定义新的数据结构MovementStep来存储这些数据。

MovementStep脚本的代码如下:

csharp 复制代码
namespace MFarm.AStar
{
    public class MovementStep : MonoBehaviour
    {
        public string sceneName;
        public int hour;
        public int minute;
        public int second;
        public Vector2Int gridCoordinate;
    }

}

因为NPC可以进行跨场景移动,因此我们需要定义场景名称,同时我们要记录角色在什么时间该到什么地方,我们需要定义时分秒以及网格坐标这些变量。

我们之前已经找到了最短路径了,现在我们就来构建NPC的最短路径,其实就是将路径存入到Stack中,当NPC移动的时候使用即可。

我们在AStar脚本中编写UpdatePathOnMovementStepStack方法。

AStar脚本的UpdatePathOnMovementStepStack方法代码如下:

csharp 复制代码
private void UpdatePathOnMovementStepStack(string sceneName, Stack<MovementStep> npcMovementStep)
{
    Node nextNode = targetNode;
    
    while (nextNode != null)
    {
        MovementStep newStep = new MovementStep();
        newStep.sceneName = sceneName;
        newStep.gridCoordinate = new Vector2Int(nextNode.gridPosition.x + originX, nextNode.gridPosition.y + originY);
        //压入堆栈
        npcMovementStep.Push(newStep);
        nextNode = nextNode.parentNode;
    }
}

这段代码就是我们从终点一次遍历找到其父节点,知道找到起点(节点不为空)即可。我们首先创建一个movementStep的变量newStep,然后对其sceneName和网格坐标进行赋值,并将这个变量压入栈中即可。(这块并没有对时间戳进行赋值,之后开始移动之后才会对时间戳进行赋值)

接着在BuildPath中调用这个方法,我们的AStar脚本就基本上写好了

csharp 复制代码
namespace MFarm.AStar
{
    public class AStar : Singleton<AStar>
    {
        private GridNodes gridNodes;
        private Node startNode;
        private Node targetNode;
        private int gridWidth;
        private int gridHeight;
        private int originX;
        private int originY;

        private List<Node> openNodeList; //当前选中Node周围的8个点
        private HashSet<Node> closedNodeList; //所有被选中的点(用Hash表查找的速度更快)

        private bool pathFound;


        /// <summary>
        /// 构建路径更新Stack的每一步
        /// </summary>
        /// <param name="sceneName"></param>
        /// <param name="startPos"></param>
        /// <param name="endPos"></param>
        /// <param name="npcMovementStack"></param>
        public void BuildPath(string sceneName, Vector2Int startPos, Vector2Int endPos, Stack<MovementStep> npcMovementStack)
        {
            pathFound = false;
            //Debug.Log(endPos);

            if (GenerateGridNodes(sceneName, startPos, endPos))
            {
                //查找最短路径
                if (FindShortestPath())
                {
                    //构建NPC移动路径
                    UpdatePathOnMovementStepStack(sceneName, npcMovementStack);
                }

            }
        }


        /// <summary>
        /// 构建网格节点信息,初始化两个列表
        /// </summary>
        /// <param name="sceneName">场景名字</param>
        /// <param name="startPos">起点</param>
        /// <param name="endPos">终点</param>
        /// <returns></returns>
        private bool GenerateGridNodes(string sceneName, Vector2Int startPos, Vector2Int endPos)
        {
            if (GridMapManager.Instance.GetGridDimensions(sceneName, out Vector2Int gridDimensions, out Vector2Int gridOrigin))
            {
                //根据瓦片地图返回构建网格移动节点范围数组
                gridNodes = new GridNodes(gridDimensions.x, gridDimensions.y);
                gridWidth = gridDimensions.x;
                gridHeight = gridDimensions.y;
                originX = gridOrigin.x;
                originY = gridOrigin.y;

                openNodeList = new List<Node>();

                closedNodeList = new HashSet<Node>();
            }
            else
                return false;

            //gridNodes的范围是从0,0开始所以需要减去原点坐标得到实际位置
            startNode = gridNodes.GetGridNode(startPos.x - originX, startPos.y - originY);
            targetNode = gridNodes.GetGridNode(endPos.x - originX, endPos.y - originY);

            for (int x = 0; x < gridWidth; x++)
            {
                for (int y = 0; y < gridHeight; y++)
                {
                    var key = (x + originX) + "x" + (y + originY) + "y" + sceneName;

                    TileDetails tile = GridMapManager.Instance.GetTileDetails(key);

                    if (tile != null)
                    {
                        Node node = gridNodes.GetGridNode(x, y);

                        if (tile.isNPCObstacle) node.isObstacle = true;
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// 找到最短路径所有node添加到closedNodeList
        /// </summary>
        /// <returns></returns>
        private bool FindShortestPath()
        {
            //添加起点
            openNodeList.Add(startNode);

            while(openNodeList.Count > 0)
            {
                //节点排序,Node内涵比较函数
                openNodeList.Sort();

                Node closeNode = openNodeList[0];

                openNodeList.RemoveAt(0);

                closedNodeList.Add(closeNode);
                 
                if (closeNode == targetNode)
                {
                    pathFound = true;
                    break;
                }

                //计算周围8个Node补充到OpenList
                EvaluateNeighbourNodes(closeNode);
            }
            return pathFound;
        }

        /// <summary>
        /// 评估周围八个点并得到消耗值
        /// </summary>
        /// <param name="currentNode"></param>
        private void EvaluateNeighbourNodes(Node currentNode)
        {
            Vector2Int currentNodePos = currentNode.gridPosition;
            Node validNeighbourNode;

            for (int x = -1; x <= 1; x++)
            {
                for (int y = -1; y <= 1; y++)
                {
                    if (x == 0 && y == 0) continue;

                    validNeighbourNode = GetVaildNeighbourNode(currentNodePos.x + x, currentNodePos.y + y);

                    if (validNeighbourNode != null)
                    {
                        if(!openNodeList.Contains(validNeighbourNode))
                        {
                            validNeighbourNode.gCost = currentNode.gCost + GetDistance(currentNode, validNeighbourNode);
                            validNeighbourNode.hCost = GetDistance(validNeighbourNode, targetNode);
                            //连接父节点
                            validNeighbourNode.parentNode = currentNode;
                            openNodeList.Add(validNeighbourNode);
                        }
                    }
                }
            }
        }

        private Node GetVaildNeighbourNode(int x, int y)
        {
            if (x >= gridWidth || y >= gridHeight || x < 0 || y < 0) return null;

            Node neighbourNode = gridNodes.GetGridNode(x, y);

            if (neighbourNode.isObstacle || closedNodeList.Contains(neighbourNode))
                return null;
            else
                return neighbourNode;

        }

        /// <summary>
        /// 返回任意两点的距离值
        /// </summary>
        /// <param name="nodeA"></param>
        /// <param name="nodeB"></param>
        /// <returns>14的倍数 + 10的倍数</returns>
        private int GetDistance(Node nodeA, Node nodeB)
        {
            int xDistance = Mathf.Abs(nodeA.gridPosition.x - nodeB.gridPosition.x);
            int yDistance = Mathf.Abs(nodeA.gridPosition.y - nodeB.gridPosition.y);

            if (xDistance > yDistance)
            {
                return 14 * yDistance + 10 * (xDistance - yDistance);
            }
            return 14 * xDistance + 10 * (yDistance - xDistance);
        }

        private void UpdatePathOnMovementStepStack(string sceneName, Stack<MovementStep> npcMovementStep)
        {
            Node nextNode = targetNode;
            
            while (nextNode != null)
            {
                MovementStep newStep = new MovementStep();
                newStep.sceneName = sceneName;
                newStep.gridCoordinate = new Vector2Int(nextNode.gridPosition.x + originX, nextNode.gridPosition.y + originY);
                //压入堆栈
                npcMovementStep.Push(newStep);
                nextNode = nextNode.parentNode;
            }
        }
    }
}

我们现在就可以进行一些测试了,我们创建一个空物体作为控制器NPCManager,并将该控制器挂在AStar脚本,然后编写AStarTest脚本(也挂在AStar脚本上),用于编写测试代码。

AStarTest脚本代码如下:

csharp 复制代码
namespace MFarm.AStar
{
    public class AStarTest : MonoBehaviour
    {
        private AStar aStar;
        [Header("用于测试")]
        public Vector2Int startPos;
        public Vector2Int finishPos;
        public Tilemap displayMap;
        public TileBase displayTile;
        public bool displayStartAndFinish;
        public bool displayPath;

        private Stack<MovementStep> npcMovmentStepStack;

        private void Awake()
        {
            aStar = GetComponent<AStar>();
            npcMovmentStepStack = new Stack<MovementStep>();
        }

        private void Update()
        {
            ShowPathOnGridMap();
        }

        private void ShowPathOnGridMap()
        {
            if (displayMap != null && displayTile != null)
            {
                if (displayStartAndFinish)
                {
                    displayMap.SetTile((Vector3Int)startPos, displayTile);
                    displayMap.SetTile((Vector3Int)finishPos, displayTile);
                }
                else
                {
                    displayMap.SetTile((Vector3Int)startPos, null);
                    displayMap.SetTile((Vector3Int)finishPos, null);
                }

                if (displayPath)
                {
                    var sceneName = SceneManager.GetActiveScene().name;

                    aStar.BuildPath(sceneName, startPos, finishPos, npcMovmentStepStack);

                    foreach (var step in npcMovmentStepStack)
                    {
                        displayMap.SetTile((Vector3Int)step.gridCoordinate, displayTile);
                    }
                }
                else
                {
                    if (npcMovmentStepStack.Count > 0)
                    {
                        foreach (var step in npcMovmentStepStack)
                        {
                            displayMap.SetTile((Vector3Int)step.gridCoordinate, null);
                        }
                        npcMovmentStepStack.Clear();
                    }
                }
            }
        }
    }
}

我们现在来解释这一段代码,首先我们需要定义一些变量,例如aStar脚本的声明aStar,起始点startPos和终点finishPos;接着我们需要在PersistentScene场景中创建一个新的TileMap,用于绘制最短路径(displayMap);接着我们还需要声明可以展示的瓦片displayTile(需要自己制作,教程中使用的是红色的瓦片,显眼颜色的均可),然后定义两个bool值,当我们勾选之后,才会显示起始点和终点displayStartAndFinish,或者显示最短路径displayPath,最后需要一个用于存储移动路径的栈npcMovmentStepStack。

我们在Awake方法中拿到aStar变量,并且声明一个新的栈。

接着我们要编写ShowPathOnGridMap方法,这个方法首先需要判断displayMap和displayTile是否为空,如果不为空才能执行接下来的代码,如果displayStartAndFinish为true,那么将起始点和终点的网格改为我们想要修改成的网格,利用SetTile方法;如果displayPath,我们获得当前场景的场景名(测试只是在同场景中进行),然后利用AStar脚本的BuildPath方法将路径存储到栈中,我们把堆栈中的每一个网格都利用SetTile将其变成想要的网格,如果我们不进行显示,我们只需要将displayMap清空即可,最后清空栈。

我们打开Unity,进行测试,发现已经实现了找到最短路径的功能,但是问题就是当前的场景并没有设置障碍,因此我们需要对两个场景进行一些障碍的设置(Grid Properities - > NPC Obstacle),绘制我们想要的范围即可。

绘制完成之后我们就可以观察在有障碍的环境中的最短路径是如何绘制的了。

二、创建NPC信息并实现根据场景切换显示

和创建我们的主角相似,我们拿到NPC的Sprite后,为其添加Box Collider 2D(勾选IsTrigger),接着添加刚体组件,以实现角色的移动(关掉重力,锁定角色的Z轴,同时更改Collision Detection更改为Continuous,Sleeping Mode改为Never Sleep,Interpolate改为Extrapolate,这样做的目的应该是是的角色和NPC在移动过程中不会穿过彼此,同时禁用碰撞体的睡眠,虽然很影响系统资源,并根据下一帧移动的估计位置来实现平滑移动);

关于RigidBody2D的详细属性也可以去查看Unity手册:https://docs.unity.cn/cn/2020.3/Manual/class-Rigidbody2D.html#:\~:text=2D 刚体 (Rig

接着我们给NPC添加Animator,实现其动画的切换,动画的制作过程此处就不说明了,因为本篇博客的重点还是AStar算法的实现。

我们还要为NPC添加影子的子物体,切记设置其层以及层级顺序。

我们继续创建NPC的移动脚本NPCMovement,并将该脚本挂载到NPC上,接下来我们可以编写NPCMovement的脚本啦。

为了方便代码的讲解,我们先把代码给展示出来,然后对着代码去将每一步要干什么。

csharp 复制代码
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Animator))]
public class NPCMovement : MonoBehaviour
{
    public ScheduleDataList_SO scheduleData;
    private SortedSet<ScheduleDetails> scheduleSet;
    private ScheduleDetails currentSchedule;

    //临时存储变量
    [SerializeField] private string currentScene;
    private string targetScene;
    private Vector3Int currentGridPosition;
    private Vector3Int targetGridPosition;
    private Vector3Int nextGridPosition;
    private Vector3 nextWorldPosition;

    public string StartScene { set => currentScene = value; }

    [Header("移动属性")]
    private float normalSpeed = 2f;
    private float minSpeed = 1f;
    private float maxSpeed = 3f;
    private Vector2 dir;
    public bool isMoving;

    

    private Rigidbody2D rb;
    private SpriteRenderer spriteRenderer;
    private BoxCollider2D coll;
    private Animator anim;

    private Grid grid;

    private Stack<MovementStep> movementSteps;

    private bool isInitialised;
    private bool npcMove;
    private bool sceneLoaded;

    //动画计时器
    private float animationBreakTime;
    private bool canPlayStopAnimation;

    private AnimationClip stopAnimationClip;
    public AnimationClip blankAnimationClip;
    private AnimatorOverrideController animOverride;

    private TimeSpan GameTime => TimeManager.Instance.GameTime;



    private void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
        spriteRenderer = rb.GetComponent<SpriteRenderer>();
        coll = GetComponent<BoxCollider2D>();
        anim = rb.GetComponent<Animator>();
        movementSteps = new Stack<MovementStep>();

        animOverride = new AnimatorOverrideController(anim.runtimeAnimatorController);
        anim.runtimeAnimatorController = animOverride;

        scheduleSet = new SortedSet<ScheduleDetails>();

        foreach (var schedule in scheduleData.scheduleList)
        {
            scheduleSet.Add(schedule);
        }
    }

    private void OnEnable()
    {
        EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadedEvent;
        EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent;
        EventHandler.GameMinuteEvent += CallGameMinuteEvent;
    }

    private void OnDisable()
    {
        EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadedEvent;
        EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent;
        EventHandler.GameMinuteEvent -= CallGameMinuteEvent;
    }

    

    private void Update()
    {
        if (sceneLoaded)
            SwitchAnimation();

        //计时器
        animationBreakTime -= Time.deltaTime;
        canPlayStopAnimation = animationBreakTime <= 0;
    }

    private void FixedUpdate()
    {
        if (sceneLoaded)
            Movement();
    }

    private void CallGameMinuteEvent(int minute, int hour, int day, Season season)
    {
        int time = (hour * 100) + minute;

        ScheduleDetails matchSchedule = null;

        //在该数组中已经按照时间大小排好顺序了,那么每次foreach循环的Time肯定是递增的,所以只有有一个Schedule的事件大于当前的事件,那么退出循环即可。
        foreach (var schedule in scheduleSet)
        {
            if (schedule.Time == time)
            {
                if (schedule.day != day && schedule.day != 0) continue;
                if (schedule.season != season) continue;
                matchSchedule = schedule;
            }
            else if (schedule.Time > time)
                break;
        }

        if (matchSchedule != null)
            BuildPath(matchSchedule);
    }

    private void OnAfterSceneLoadedEvent()
    {
        grid = FindObjectOfType<Grid>();
        CheckVisiable();

        if (!isInitialised)
        {
            InitNPC();
            isInitialised = true;
        }

        sceneLoaded = true;
    }

    private void OnBeforeSceneUnloadEvent()
    {
        sceneLoaded = false;
    }

    private void CheckVisiable()
    {
        if (currentScene == SceneManager.GetActiveScene().name)
            SetActiveInScene();
        else
            SetInactiveInScene();
    }


    private void InitNPC()
    {
        targetScene = currentScene;

        //保持在当前坐标的网格中心点
        currentGridPosition = grid.WorldToCell(transform.position);
        transform.position = new Vector3(currentGridPosition.x + Settings.gridCellSize / 2f, currentGridPosition.y + Settings.gridCellSize / 2f, 0);

        targetGridPosition = currentGridPosition;
    }

    /// <summary>
    /// 在FixUpdate中调用该方法,然后在npc可以移动时一次一个网格的进行移动 ---  主要移动方法
    /// </summary>
    private void Movement()
    {
        if (!npcMove)
        {
            if (movementSteps.Count > 0)
            {
                MovementStep step = movementSteps.Pop();
                currentScene = step.sceneName;
                //检查是否属于当前场景
                CheckVisiable();
                nextGridPosition = (Vector3Int)step.gridCoordinate;
                //拿到UpdateTimeOnPath方法获得的时间戳
                TimeSpan stepTime = new TimeSpan(step.hour, step.minute, step.second);

                MoveToGridPosition(nextGridPosition, stepTime);
            }
            else if (!isMoving && canPlayStopAnimation)
            {
                StartCoroutine(SetStopAnimation());
            }
        }
    }

    private void MoveToGridPosition(Vector3Int gridPos, TimeSpan stepTime)
    {
        StartCoroutine(MoveRoutine(gridPos, stepTime));
    }

    /// <summary>
    /// 使人物移动一个网格
    /// </summary>
    /// <param name="gridPos">想要移动到的网格位置</param>
    /// <param name="stepTime">移动到该网格的时间戳</param>
    /// <returns></returns>
    private IEnumerator MoveRoutine(Vector3Int gridPos, TimeSpan stepTime)
    {
        npcMove = true;
        nextWorldPosition = GetWorldPosition(gridPos);

        //还有时间用来移动
        //运行时根本没有过这个if判断,真奇怪呀
        //原因找到了。我在GameTime里面(就是TimeManager脚本里面)获取GameTime的时间时获取错了,
        //应该取得是时分秒,我取得是年分秒 位于TimeManager脚本的第14行
        if (stepTime > GameTime)
        {
            //用于移动的时间差,以秒为单位
            float timeToMove = (float)(stepTime.TotalSeconds - GameTime.TotalSeconds);
            //实际移动距离
            float distance = Vector3.Distance(transform.position, nextWorldPosition);
            //实际移动速度
            float speed = Mathf.Max(minSpeed, (distance / timeToMove / Settings.secondThreshold));


            if (speed <= maxSpeed)
            {
                while (Vector3.Distance(transform.position, nextWorldPosition) > Settings.pixelSize)
                {
                    //其实不是很懂为什么要先计算方向,在计算位移,不可以直接计算位移???
                    dir = (nextWorldPosition - transform.position).normalized;

                    Vector2 posOffset = new Vector2(dir.x * speed * Time.fixedDeltaTime, dir.y * speed * Time.fixedDeltaTime);

                    rb.MovePosition(rb.position + posOffset);


                    //???等待下一次FixedUpdate的执行???
                    yield return new WaitForFixedUpdate();
                }
            }
        }

        //如果时间到了就瞬移
        rb.position = nextWorldPosition;
        currentGridPosition = gridPos;
        nextGridPosition = currentGridPosition;

        npcMove = false;
        
    }


    /// <summary>
    /// 根据Schedule构建路径
    /// </summary>
    /// <param name="schedule"></param>
    public void BuildPath(ScheduleDetails schedule)
    {
        
        movementSteps.Clear();
        currentSchedule = schedule;
        targetGridPosition = (Vector3Int)schedule.targetGridPosition;
        stopAnimationClip = schedule.clipStop;

        if (schedule.targetScene == currentScene)
        {
            AStar.Instance.BuildPath(schedule.targetScene, (Vector2Int)currentGridPosition, schedule.targetGridPosition, movementSteps);
        }
        else if (schedule.targetScene != currentScene)
        {
            //跨场景移动
            SceneRoute sceneRoute = NPCManager.Instance.GetSceneRoute(currentScene, schedule.targetScene);
            //使用for循环将sceneRoute的scenePathList压入栈中,由于栈先进后出的特性,

            //??? 为什么List是栈的特性呢 ??? 不是很懂

            //所以我们在SceneRouteDataList_SO文件中先放的是目标场景的数据,再放的是起始场景的数据
            if (sceneRoute != null)
            {
                for (int i = 0; i < sceneRoute.scenePathList.Count; i++)
                {
                    Vector2Int fromPos, gotoPos;
                    ScenePath path = sceneRoute.scenePathList[i];

                    //这些代码很好理解
                    if (path.fromGridCell.x >= Settings.maxGridSize || path.fromGridCell.y >= Settings.maxGridSize)
                    {
                        fromPos = (Vector2Int)currentGridPosition;
                    }
                    else
                    {
                        fromPos = path.fromGridCell;
                    }

                    if (path.gotoGridCell.x >= Settings.maxGridSize || path.gotoGridCell.y >= Settings.maxGridSize)
                    {
                        gotoPos = (Vector2Int)currentGridPosition;
                    }
                    else
                    {
                        gotoPos = path.gotoGridCell;
                    }

                    AStar.Instance.BuildPath(path.sceneName, fromPos, gotoPos, movementSteps);
                }
            }
        }



        if (movementSteps.Count > 1)
        {
            //更新每一步对应的时间戳
            UpdateTimeOnPath();

        }
    }


    private void UpdateTimeOnPath()
    {
        MovementStep previousStep = null;
        TimeSpan currentGameTime = GameTime;

        foreach (MovementStep step in movementSteps)
        {
            if (previousStep == null)
                previousStep = step;

            //我当时在想为什么我都明明可以传递过来,但是还要自己去下面重新计算时间戳呢???

            //上述观点是错误的,因为currentGameTime里面的时间只有第一次调用这个方法的时候传递过来的时间,而没有之后移动到其他格子的时间
            step.hour = currentGameTime.Hours;
            step.minute = currentGameTime.Minutes;
            step.second = currentGameTime.Seconds;

            //这样计算下来的值是准确的吗?
            TimeSpan gridMovementStepTime;
            if (MoveInDiagonal(step, previousStep))
                gridMovementStepTime = new TimeSpan(0, 0, (int)(Settings.gridCellDiagonalSize / normalSpeed / Settings.secondThreshold));
            else
                gridMovementStepTime = new TimeSpan(0, 0, (int)(Settings.gridCellSize / normalSpeed / Settings.secondThreshold));

            //累加获得下一步的时间戳,可是这些事件戳都保存到哪了呢?方便之后用?
            //上面的问题得到了解决,我在上面的赋值时已经存储到了MovementStep中了
            currentGameTime = currentGameTime.Add(gridMovementStepTime);
            //循环下一步
            previousStep = step;
        }

    }

    /// <summary>
    /// 判断是否是斜方向行走
    /// </summary>
    /// <param name="currentStep"></param>
    /// <param name="previousStep"></param>
    /// <returns></returns>
    private bool MoveInDiagonal(MovementStep currentStep, MovementStep previousStep)
    {
        return (currentStep.gridCoordinate.x != previousStep.gridCoordinate.x) && (currentStep.gridCoordinate.y != previousStep.gridCoordinate.y); 
    }

    /// <summary>
    /// 网格坐标返回世界坐标中心点
    /// </summary>
    /// <param name="gridPos"></param>
    /// <returns></returns>
    private Vector3 GetWorldPosition(Vector3Int gridPos)
    {
        Vector3 worldPos = grid.WorldToCell(gridPos);
        return new Vector3(worldPos.x + Settings.gridCellSize / 2, worldPos.y + Settings.gridCellSize / 2);
    }

    public void SwitchAnimation()
    {
        //targetGridPosition只有在InitNPC的时候更改过,所以确定是使用targetGridPosition吗?
        //解决了,我们在BuildPath方法中对targetGridPosition进行了赋值。
        isMoving = transform.position != GetWorldPosition(targetGridPosition);

        anim.SetBool("IsMoving", isMoving);
        if (isMoving)
        {
            anim.SetBool("Exit", true);
            anim.SetFloat("DirX", dir.x);
            anim.SetFloat("DirY", dir.y);
        }    
        else
        {
            anim.SetBool("Exit", false);
        }
    }


    private IEnumerator SetStopAnimation()
    {
        //强制面向镜头
        anim.SetFloat("DirX", 0);
        anim.SetFloat("DirY", -1);

        animationBreakTime = Settings.animationBreakTime;
        if (stopAnimationClip != null)
        {
            animOverride[blankAnimationClip] = stopAnimationClip;
            anim.SetBool("EventAnimation", true);
            yield return null;
            anim.SetBool("EventAnimation", false);
        }
        else
        {
            animOverride[stopAnimationClip] = blankAnimationClip;
            anim.SetBool("EventAnimation", false);
        }

    }

    #region 设置NPC显示情况
    private void SetActiveInScene()
    {
        spriteRenderer.enabled = true;
        coll.enabled = true;
        //TODO:影子关闭
        transform.GetChild(0).gameObject.SetActive(true);
    }

    private void SetInactiveInScene()
    {
        spriteRenderer.enabled = false;
        coll.enabled = false;
        //TODO:影子关闭
        transform.GetChild(0).gameObject.SetActive(false);
    }
    #endregion
}

首先,因为我们这个脚本是要挂载在所有NPC脚本上的,那么我们必须要该脚本所挂载的物体必须添加了刚体2D组件和Animator组件;接着我们要定义一些变量来存储NPC的信息,起始场景currentScene和目标场景targetScene,起始位置currentGridPosition和目标位置targetGridPosition以及设置一个变量StartScene对初始场景进行赋值;然后定义一些移动属性变量,例如NPC的移动速度normalSpeed,以及其移动速度的上下限minSpeed和maxSpeed(因为NPC在移动时会进行斜方向的移动,或者在某些NPC的移动过程中需要加速和减速,因此我们需要定义其最大速度和最小速度,使角色移动不至于太快或者太慢);最后为了实现动画状态机的动画切换,我们还要定义一个Vector2类型的变量dir和一个bool类型的变量isMoving。这样我们部分变量就算是创建完了。

接着创建脚本NPCManager,并挂载在NPCManager物体上。通过这个脚本我们想要获取到当前脚本中所以的NPC并对其初始场景和初始坐标进行赋值,因此我们还需要创建一个新的类型,方便对上述变量进行赋值。

这个变量首先需要获取角色的Transform,所在场景和所在位置。

DataCollection脚本新建的变量类型NPCPosition

csharp 复制代码
[System.Serializable]
public class NPCPosition
{
    public Transform npc;
    public string startScene;
    public Vector3 position;
}

我们在NPCManager脚本中添加list来存储所有的NPC,返回Unity界面,将我们场景中的NPC拖拽过来即可。

接着我们返回NPCManager脚本中,我们现在要添加一些变量,例如:刚体(控制NPC移动),SpriteRenderer(因为NPC始终存在在场景中,所以NPC是始终都存在的,那么当角色从第一个场景跨越到第二个场景后,我们在视觉上直接关闭NPC的SpriteRenderer即可),碰撞体,存储移动网格的堆栈以及动画控制器,并在Awake中赋值。

由于我们要控制SpriteRenderer的关闭和打开,那么我们接下来编写两个方法,控制SpriteRenderer的这两个操作。

csharp 复制代码
#region 设置NPC显示情况
    private void SetActiveInScene()
    {
        spriteRenderer.enabled = true;
        coll.enabled = true;
        //TODO:影子关闭
        transform.GetChild(0).gameObject.SetActive(true);
    }

    private void SetInactiveInScene()
    {
        spriteRenderer.enabled = false;
        coll.enabled = false;
        //TODO:影子关闭
        transform.GetChild(0).gameObject.SetActive(false);
    }
    #endregion

那么这些方法应该怎么被调用嘞,那么我们首先编写一个方法,调用SetActiveInScene和SetInactiveInScene方法。(代码如下)

csharp 复制代码
private void CheckVisiable()
    {
        if (currentScene == SceneManager.GetActiveScene().name)
            SetActiveInScene();
        else
            SetInactiveInScene();
    }

那么这个方法又应该在什么位置调用呢,应该是在跳转场景之后,我们判断此时NPC是否可以在当前场景中进行显示,所以我们注册AfterSceneLoadedEvent事件,并在OnAfterSceneLoadedEvent方法中调用CheckVisiable。

现在就可以返回Unity测试,由于我们的初始场景为01.Field,我们将角色的currentScene手动更改为02.Home,运行,我们就会发现在当前场景中没有找到NPC。

相关推荐
新缸中之脑7 小时前
LLMUnity:在Unity 3D中使用大模型
3d·unity·游戏引擎
我与岁月的森林9 小时前
游戏中的对象池技术探索(一)
unity·c#·对象池
benben04420 小时前
Unity3d动画插件DoTween使用指南
游戏·unity
Thinbug1 天前
Unity 克隆Timeline并保留引用
unity
_时侍1 天前
【unity游戏开发】彻底理解AnimatorStateInfo,获取真实动画长度
服务器·unity·哈希算法
Unity官方开发者社区1 天前
Unity 如何在 iOS 新增键盘 KeyCode 响应事件
unity·ios·计算机外设
向宇it2 天前
【unity进阶知识10】从零手搓一个UI管理器/UI框架,自带一个提示界面,还有自带DOTween动画效果
ui·unity·游戏引擎
杳戢2 天前
技术美术百人计划 | 《5.4 水体渲染》笔记
人工智能·笔记·深度学习·算法·unity·c#·技术美术
charon87782 天前
Unity Shader Graph基础包200+节点及术语解释
unity·游戏引擎