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

系列文章目录

文章目录

  • 系列文章目录
  • 前言
  • 一、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>();}elsereturn 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补充到OpenListEvaluateNeighbourNodes(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;elsereturn 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();elseSetInactiveInScene();}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));elsegridMovementStepTime = 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();elseSetInactiveInScene();}

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

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

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

相关文章

【数据结构与算法】简单聊聊图数据的存储

文章目录 1. 邻接矩阵&#xff08;Adjacency Matrix&#xff09;2. 邻接表&#xff08;Adjacency List&#xff09;3. 邻接多重表4. 十字链表5. 图数据库&#xff08;Graph Database&#xff09; 存储图数据的方法主要有几种&#xff0c;每种方法都有其特定的应用场景和优缺点。…

毕业设计项目-古典舞在线交流平台的设计与实现(源码/论文)

项目简介 基于springboot实现的&#xff0c;主要功能如下&#xff1a; 技术栈 后端框框&#xff1a;springboot/mybatis 前端框架&#xff1a;html/JavaScript/Css/vue/elementui 运行环境&#xff1a;JDK1.8/MySQL5.7/idea&#xff08;可选&#xff09;/Maven3&#xff08…

什么是物联网nb水表?

物联网NB水表是一种利用NB-IoT(窄带物联网)技术实现远程数据传输的智能水表。这种水表不仅能够精确计量用户的用水量&#xff0c;还能通过无线通信技术实现数据的远程传输和管理。下面我们来详细介绍物联网NB水表的主要特点和功能。 一、基本概念 -定义&#xff1a;物联网NB水…

如何优化spotbugsXml.xml文件来方便debug的落地方案来了

不优化的spotbugsXml.xml 使用maven 构建来运行spotbugs的小伙伴都知道&#xff0c;执行完下面的命令后 mvn clean install -U spotbugs:spotbugs 会在默认的在target目录下生成一个spotbugsXml.xml 文件&#xff0c;而打开这个文件&#xff0c;想要debug里面的具体问题&am…

嵌入式面试——FreeRTOS篇(六) 任务通知

本篇为&#xff1a;FreeRTOS 任务通知篇 任务通知简介 1、任务通知介绍 答&#xff1a; 任务通知&#xff1a;用来通知任务的&#xff0c;任务控制块中的结构体成员变量ulNotifiedValue就是这个通知值。 使用队列、信号量、事件标志组时都需要另外创建一个结构体&#xff0c…

Ubuntu终端配置

选择shell shell有很多&#xff0c;默认的是bash&#xff0c;一般就够用里&#xff0c;想要花里胡哨点就用zsh&#xff0c;还有最近比较火的fish 如果在刚开始安装完Ubuntu没有改shell&#xff0c;后面就不要改了。 安装的软件会设置环境变量&#xff0c;这些环境变量都是写入…

QDateTime 使用详解

QDateTime 是 Qt 框架中用于处理日期和时间的类。本篇文章详细介绍、通过示例 快速了解QDateTime的各种操作&#xff0c;包括: 当前时间、获取日期和时间、获取日期、获取时间、获取时间戳、格式化输出、年、月、日、QTime时间、获取微妙、操作日期和时间、添加时间、减去时间、…

无人机避障——4D毫米波雷达点云滤波去噪(四)

噪声的来源&#xff1a; 对于4D毫米波雷达的前后两帧点云数据进行去噪&#xff0c;可以采用多种方法。首先&#xff0c;需要了解点云数据的噪声来源&#xff0c;可能是由于硬件限制、环境干扰或目标本身的反射特性等因素造成的。噪声点通常包括漂移点、孤立点、冗余点和混杂点…

毕业设计项目——基于RISC-V的标签化跨层调度应用任务管理(论文/代码)

完整的论文代码见文章末尾 以下为核心内容 摘要 在现代操作系统中&#xff0c;高效的系统调度策略对于优化系统性能、提高资源利用率和保证系统稳定性至关重要。本文提出了一个基于Linux进程文件系统&#xff08;procfs&#xff09;的系统监控工具&#xff0c;旨在通过实时收…

Spring Cloud全解析:链路追踪之springCloudSleuth简介

文章目录 springCloudSleuth简介链路追踪&#xff1f;SpringCloudSleuth术语链路示意图zipkin依赖配置 springCloudSleuth简介 链路追踪&#xff1f; 什么是链路追踪&#xff1f;就是将一次分布式请求还原成调用链路&#xff0c;将一次分布式请求的调用情况集中展示&#xff…

算法:1、动态规划算法DP(Dynamic Programming)

算法介绍 动态规划&#xff08;Dynamic Programming&#xff0c;DP&#xff09;‌&#xff0c;通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。它的关键思想是对于最终结果依赖前序步骤的问题&#xff0c;将结果定义为状态值dp&#xff0c;然后推导出后续步骤由…

深度学习常见问题

1.YOLOV5和YOLOV8的区别 YOLOv5 和 YOLOv8 是两个版本的 YOLO&#xff08;You Only Look Once&#xff09;目标检测算法&#xff0c;它们在网络架构、性能优化、功能扩展等方面有显著的区别。YOLOv5 是 YOLO 系列的重要改进版本&#xff0c;而 YOLOv8 是最新的一次重大升级&am…

SQL性能优化指南:如何优化MySQL多表join场景

目录 多表join问题SQL 这里解释下 Using join buffer (Block Nested Loop)&#xff1a; 对性能产生的影响&#xff1a; 三种join算法介绍 join操作主要使用以下几种算法&#xff1a; &#xff08;1&#xff09;Nested Loop Join &#xff08;2&#xff09;Block Nested …

搭建企业域名服务器案例

任务要求&#xff1a; 某企业要建立一台应用于以下情况的主域名服务器 拥有一个C类网段地址&#xff0c;为202.101.55.0。企业域名注册为company.com。域名服务器的IP地址定位为202.101.55.55&#xff0c;主机名为dns.company.com。企业网通过路由器与Internet连接。要解析的…

第九届清洁能源与发电技术国际学术会议(CEPGT 2024)

第九届清洁能源与发电技术国际学术会议&#xff08;CEPGT 2024&#xff09; 2024 9th International Conference on Clean Energy and Power Generation Technology (CEPGT 2024) 【早投稿早录用&#xff0c;享受早鸟优惠】 第九届清洁能源与发电技术国际学术会议&#xff0…

记录一个Ajax发送JSON数据的坑,后端RequestBody接收参数小细节?JSON对象和JSON字符串的区别?

上半部分主要介绍我实际出现的问题&#xff0c;最终下面会有总结。 起因&#xff1a;我想发送post请求的data&#xff0c;但是在浏览器中竟然被搞成了地址栏编码 如图前端发送的ajax请求数据 如图发送的请求体&#xff1a; 很明显是keyvalue这种形式&#xff0c;根本就不是…

开源的键鼠共享工具「GitHub 热点速览」

十一长假回来&#xff0c;我的手放在落灰的键盘上都有些陌生了&#xff0c;红轴竟敲出了青轴般的响声&#xff0c;仿佛在诉说对假期结束的不甘。 假期回归的首更&#xff0c;让我们看看又有什么好玩的开源项目冲上了开源热榜。一套键盘和鼠标控制多台电脑的工具 deskflow&#…

supOS加速数实融合发展

作为工业操作系统领军企业&#xff0c;蓝卓受邀参加2024金砖国家新工业革命伙伴关系论坛&#xff0c;深度参与多个环节。在9月11日召开的金砖国家新工业革命伙伴关系论坛产融合作专题研讨上&#xff0c;蓝卓总经理谭彰分享了supOS在产融协同的最新实践&#xff0c;以及supOS进入…

云上考场小程序+ssm论文源码调试讲解

2 关键技术简介 2.1 微信小程序 微信小程序&#xff0c;简称小程序&#xff0c;英文名Mini Program&#xff0c;是一种全新的连接用户与服务的方式&#xff0c;可以快速访问、快速传播&#xff0c;并具有良好的使用体验。 小程序的主要开发语言是JavaScript&#xff0c;它与…

集师知识付费小程序:打造培训机构在线教育的金字招牌 集师知识付费系统 集师知识付费小程序 集师知识服务系统 集师线上培训系统 集师线上卖课小程序

在数字化浪潮的推动下&#xff0c;在线教育已成为教育领域的热门话题。而在众多在线教育平台中&#xff0c;集师知识付费小程序凭借其独特的定位和创新的模式&#xff0c;成功为培训机构打造了一张闪亮的在线教育金字招牌。 集师知识付费小程序&#xff0c;是一个集课程展示、…