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

news2024/10/9 13:32:46

系列文章目录

文章目录

  • 系列文章目录
  • 前言
  • 一、AStar算法的测试
  • 二、创建NPC信息并实现根据场景切换显示


前言

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


一、AStar算法的测试

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

MovementStep脚本的代码如下:

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方法代码如下:

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脚本就基本上写好了

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脚本代码如下:

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%20%E5%88%9A%E4%BD%93%20(Rig

接着我们给NPC添加Animator,实现其动画的切换,动画的制作过程此处就不说明了,因为本篇博客的重点还是AStar算法的实现。
我们还要为NPC添加影子的子物体,切记设置其层以及层级顺序。
我们继续创建NPC的移动脚本NPCMovement,并将该脚本挂载到NPC上,接下来我们可以编写NPCMovement的脚本啦。
为了方便代码的讲解,我们先把代码给展示出来,然后对着代码去将每一步要干什么。

[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

[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的这两个操作。

#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方法。(代码如下)

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

那么这个方法又应该在什么位置调用呢,应该是在跳转场景之后,我们判断此时NPC是否可以在当前场景中进行显示,所以我们注册AfterSceneLoadedEvent事件,并在OnAfterSceneLoadedEvent方法中调用CheckVisiable。
现在就可以返回Unity测试,由于我们的初始场景为01.Field,我们将角色的currentScene手动更改为02.Home,运行,我们就会发现在当前场景中没有找到NPC。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2199136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

利士策分享,婚姻为何被称为大事?

利士策分享&#xff0c;婚姻为何被称为大事&#xff1f; 在历史的长河中&#xff0c;婚姻一直被视为人生中的头等大事&#xff0c;这一观念跨越时空&#xff0c;深深植根于各种文化和社会结构中。 古人为何将婚姻称为“大事”&#xff0c;这背后蕴含着丰富的社会、文化和心理寓…

JUC高并发编程6:Callable接口

1 创建线程的方式 在 Java 中&#xff0c;创建线程的方式主要有以下几种&#xff1a; 继承 Thread 类&#xff1a; 通过继承 Thread 类并重写 run() 方法来创建线程。示例代码&#xff1a;class MyThread extends Thread {Overridepublic void run() {// 线程执行的代码} }pub…

LeetCode题练习与总结:生命游戏--289

一、题目描述 根据 百度百科 &#xff0c; 生命游戏 &#xff0c;简称为 生命 &#xff0c;是英国数学家约翰何顿康威在 1970 年发明的细胞自动机。 给定一个包含 m n 个格子的面板&#xff0c;每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态&#xff1a; 1 即…

如何运行服务器上的web页面,打开Outlook 365的全球离线通讯簿功能?

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

Linux入门攻坚——35、Linux防火墙-iptables-1

Firewall&#xff1a;防火墙&#xff0c;就是一个隔离工具。工作于主机或网络的边缘&#xff0c;对于进出本主机或网络的报文根据事先定义好的检查规则做匹配检测&#xff0c;对于能够被规则所匹配到的报文做出相应处理的组件&#xff1a;这个组件可以是硬件&#xff0c;也可以…

WPS的JS宏实现删除某级标题下的所有内容

想要删除Word文档中&#xff0c;包含特定描述的标题下所有内容&#xff08;包含各级子标题以及正文描述&#xff09;。 例如下图中&#xff0c;想删除1.2.1.19.1业务场景下所有内容&#xff1a; 简单版&#xff1a; 删除光标停留位置的大纲级别下所有的内容。实现的JS代码如下…

机器学习笔记-2

文章目录 一、Linear model二、How to represent this function三、Function with unknown parameter四、ReLU总结、A fancy name 一、Linear model 线性模型过于简单&#xff0c;有很大限制&#xff0c;我们需要更多复杂模式 蓝色是线性模型&#xff0c;线性模型无法去表示…

ubuntu 开放 8080 端口快捷命令

文章目录 查看防火墙状态开放 80 端口开放 8080 端口开放 22端口开启防火墙重启防火墙**使用 xhell登录**&#xff1a; 查看防火墙状态 sudo ufw status [sudo] password for crf: Status: inactivesudo ufw enable Firewall is active and enabled on system startup sudo…

Flutter 3.24 发布:GPU模块及多视图嵌入功能

Flutter 3.24 发布&#xff1a;GPU模块及多视图嵌入功能 Flutter 3.24 带来了许多新功能和改进&#xff0c;让开发应用程序变得更加容易和有趣。这个版本重点展示了 Flutter GPU 的预览功能&#xff0c;让应用程序可以直接使用高级图形和 3D 场景功能。 此外&#xff0c;网页…

传智杯 第六届—B

题目&#xff1a; 擂台赛要开始了&#xff0c;现在有 n 名战士&#xff0c;其中第 i 名战士的战斗力为 ai​。现在准备从这些战士中挑两名战士进入擂台赛进行对战&#xff0c;由于观众们更喜欢看势均力敌的比赛&#xff0c;所以我们也要挑选两个战斗力尽可能相近的战士进行参赛…

Linux-分析 IO 瓶颈手册

分析IO瓶颈 此文主要内容&#xff1a;I/O性能重要指标、主要排查工具、主要排查手段、工具图示 磁盘 I/O 性能指标 四个核心的磁盘 I/O 指标 使用率&#xff1a;是指磁盘忙处理 I/O 请求的百分比。过高的使用率&#xff08;比如超过 60%&#xff09;通常意味着磁盘 I/O 存在…

Spring系列 Bean的生命周期

文章目录 初始化时机单例初始化流程getBeandoGetBeangetSingleton(String) 获取单例getSingleton(String, ObjectFactory) 创建单例beforeSingletonCreationcreateBeanafterSingletonCreation createBean 创建对象doCreateBeanaddSingletonFactory createBeanInstance 创建 Bea…

基于springboot vue 学生就业信息管理系统设计与实现

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作☆☆☆ 精彩专栏推荐订阅☆☆☆☆…

水库大坝安全监测预警系统守护大坝安全卫士

一、系统背景 近年来&#xff0c;受全球气候变化和人类活动影响&#xff0c;极端天气发生频度强度增加&#xff0c;加之我国城市化进程中&#xff0c;水库下游人口聚集、基础设施密集&#xff0c;对水库工程安全运行提出了新的更高要求。“十四五”以来我国建成并投入使用37593…

NeRS: Neural Reflectance Surfaces for Sparse-view 3D Reconstruction in the Wild

1. 2.优点1&#xff1a;我们的方法仅依赖于近似的相机位姿估计和粗略的类别级形状模板。 3.我们的关键见解是&#xff0c;我们可以强制执行基于表面的 3D 表示&#xff0c;而不是允许广泛用于体积表示的无约束密度。重要的是&#xff0c;这允许依赖于视图的外观变化 4.更重要…

迪士尼数据泄露事件:全面审视数据安全策略与未来防护方向

迪士尼数据泄露事件概述 一、 事件背景以及影响 在全球数字化转型加速的浪潮中&#xff0c;数据安全已成为企业运营不可忽视的基石。 华特迪士尼公司&#xff0c;作为全球知名的娱乐传媒巨头&#xff0c;其数据泄露事件无疑为业界敲响了警钟。此次事件不仅揭示了数据保护的严…

从0开始下载安装并使用unity

首先我们要在浏览器上找到unity的官网 这一个就是了&#xff0c;我们点进去后是这个界面&#xff1a; 然后我们点击上面这张图的左下角的“下载Unity Hub”&#xff0c;推荐后续安装都装在D盘&#xff1a; 这里他会让我们注册一个账号&#xff0c;如果之前有的话登录就行了&am…

2024年第二届龙信杯 WP

2024年龙信杯 author&#xff1a;mumuzi date&#xff1a;2024/9/30 取证的一手更新都在自己的博客上&#xff0c;分区为Forensic&#xff0c;https://mumuzi7179.github.io/或https://mumuzi.blog/ DK盾云服务器&#xff1a;DK盾 镜像下载地址&#xff1a; https://pan.ba…

i18n多语言项目批量翻译工具(支持84种语言)

这里写自定义目录标题 打开‘i18n翻译助手’小程序快捷访问 打开‘i18n翻译助手’小程序 1.将需要翻译的json文件复制到输入框&#xff08;建议一次不要翻译过多&#xff0c;测试1000条以内没什么问题&#xff09; 2.等待翻译 3.翻译完成&#xff0c;复制结果 快捷访问

极狐GitLab 发布安全补丁版本 17.4.1、17.3.4、17.2.8

GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 学习极狐GitLab 的相关资料&#xff1a; 极狐GitLab 官网极狐…