场景切换 创建TransitionManager控制人物的场景切换 在游戏中,我们存在多个场景,我们需要进入不同的场景,以便游戏的游玩性。对于场景的管理,我们使用事件来进行管理,对于每一个场景,我们都设置一个进入的以及出去的碰撞体,每当碰撞体的碰撞器检查到玩家后,开始进行场景的切换,将玩家的位置传送到指定的场景中。在切换场景的时候,我们需要对当前场景的物体也进行管理,因为当前的场景需要被卸载,新场景的加载需要时间,可能会在新场景加载进行访问,就可能找不到相应的物体,进而报空。
例如,当新场景加载时,原有场景被卸载,场景中的相机可能无法访问到新场景的边界信息,因为新场景还未被加载完成,所有会报空,所有我们需要在新场景加载完成后,重新获取场景的边界,这个时候就需要事件处理
具体实现场景切换的逻辑:在Hierarchy窗口中创建一个空物体,让其携带碰撞体的组件,将其的位置移动到需要进行场景切换的位置,同时挂载Teleport脚本。每当检测到玩家的碰撞器,进行场景切换
Teleport
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 namespace MFarm.Transition { public class Teleport : MonoBehaviour { [SceneName ] public string sceneToGo; public Vector3 positionToGo; private void OnTriggerEnter2D (Collider2D other ) { if (other.CompareTag("Player" )) { EventHandler.CallTransitionEvent(sceneToGo, positionToGo); } } } }
TransitionManager类
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 namespace MFarm.Transition { public class TransitionManager : Singleton <TransitionManager >,ISaveable { [SceneName ] public string startSceneName = string .Empty; protected override void Awake () { base .Awake(); SceneManager.LoadScene("UI" , LoadSceneMode.Additive); } private void Start () { ISaveable saveable = this ; saveable.RegisterSaveable(); fadeCanvasGroup = FindObjectOfType<CanvasGroup>(); } private void OnEnable () { EventHandler.TransitionEvent += OnTransitionEvent; } private void OnDisable () { EventHandler.TransitionEvent -= OnTransitionEvent; } private void OnTransitionEvent (string sceneToGo, Vector3 positionToGo ) { StartCoroutine(Transition(sceneToGo, positionToGo)); } private IEnumerator Transition (string sceneName,Vector3 targetPosition ) { EventHandler.CallBeforeSceneUnloadEvent(); yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene()); yield return LoadSceneSetActive (sceneName ) ; EventHandler.CallMoveToPosition(targetPosition); EventHandler.CallAfterSceneLoadedEvent(); } private IEnumerator LoadSceneSetActive (string sceneName ) { yield return SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); Scene newScene = SceneManager.GetSceneAt(SceneManager.sceneCount - 1 ); SceneManager.SetActiveScene(newScene); } } }
同时,我们也需要事件的管理,在场景卸载前以及在场景加载后,写在EventHandler中。使用订阅-发布者模式,一个委托提供很多方法进行订阅,在调用委托时,执行所有订阅后的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static event Action<string , Vector3> TransitionEvent;public static void CallTransitionEvent (string sceneName,Vector3 pos ){ TransitionEvent?.Invoke(sceneName, pos); } public static event Action BeforeSceneUnloadEvent;public static void CallBeforeSceneUnloadEvent (){ BeforeSceneUnloadEvent?.Invoke(); } public static event Action AfterSceneLoadedEvent;public static void CallAfterSceneLoadedEvent (){ AfterSceneLoadedEvent?.Invoke(); } public static event Action<Vector3> MoveToPosition;public static void CallMoveToPosition (Vector3 targetPosition ){ MoveToPosition?.Invoke(targetPosition); }
自定义场景名称 在每个场景中我们都会场景一个挂载碰撞器的空物体用于场景切换,在该物体上,会手动设置跳转场景的名称以及坐标。场景名称是手动输入的字符串,容易出错,通过自定义属性,我们可以创建一个下拉列表,直接选择场景,避免手动输入错误。为了实现上述操作,我们使用SceneNameAttribute类作为标记类,继承自PropertyAttribute,提供一个标记。SceneNameDrawer是实际的自定义绘制器,使用CustomPropertyDrawer特性标记,指定它要处理SceneNameAttribute类型的属性,继承自PropertyDrawer,用于自定义Inspector中的属性显示
具体的实现:
**SceneNameAttribute类:**首先获取到所有的场景,将场景进行自定义分隔,得到完整的场景名称,检测是否得到完整的场景名称,如果得到了,则将其装入属性值中,提供EditorGUI.Popup,在Unity中生成下拉菜单列表,实现自定义的场景名称选择
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 [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 (oldIndex != sceneIndex) 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)" ; } sceneNames[i] = new GUIContent(sceneName); } 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; } }
*SceneNameAttribute类:*只作为一个标记类, // 定义标记类时使用Attribute后缀*, // 使用时可以省略Attribute后缀* [SceneName] // 等同于 [SceneNameAttribute] ,将 [SceneName] 下面的第一个属性进行标记,说明其需要进行特殊显示
1 2 3 4 5 6 7 public class SceneNameAttribute : PropertyAttribute { public const float fadeDuration = 1.5f ; }
场景切换淡入淡出和动态UI显示 在进行场景切换时,我们需要实现一个淡入以及淡出的效果,以便平滑的过渡场景的转换。为了实现淡入淡出的效果,我们在UI场景中创建一个panel作为场景切换的图片,将其的图片默认颜色设置为黑色,alpha设置为不透明(255),同时在该panel下设置一个场景切换的文本,提示玩家下一个场景正在加载,以及一个过场动画,使用Animator来进行动画的播放。实现了淡入的效果的,在淡出时,我们协程方法平滑降低panel的alpha值,实现黑色背景逐渐透明。
Setting类:
1 2 3 public class Settings { {
TransitionManager类:
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 public class TransitionManager { private CanvasGroup fadeCanvasGroup; private bool isFade; private void Start () { fadeCanvasGroup = FindObjectOfType<CanvasGroup>(); } private IEnumerator Transition (string sceneName,Vector3 targetPosition ) { EventHandler.CallBeforeSceneUnloadEvent(); Fade(1 ); yield return SceneManager.UnloadSceneAsync(SceneManager.GetActiveScene()); yield return LoadSceneSetActive (sceneName ) ; EventHandler.CallMoveToPosition(targetPosition); EventHandler.CallAfterSceneLoadedEvent(); Fade(0 ); } 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 ; } }
保存和加载场景中的物品 在场景切换的过程中,我们没有保存场景中的物品的情况。例如,当我们拾取场景中的物品后,切换场景,在切换回来,场景中的物品又可以再次拾取。为此,我们需要实现场景中的物品保存功能。
在DataCollection类中,定义场景物品的坐标属性以及物品的ID(物品的ID可以获得物品的所有信息),在ItemManager类中,使用字典来进行操作,字典的第一个变量存代表场景的名称,第二个代表场景中的物品列表。有了基本的存储的数据结构后,首先需要获取场景所有的物品列表,在GetAllSceneItems()方法进行实现,定义一个临时的场景物品列表,使用FindObjectsOfType- ()找到场景中的所有物品,将物品的ID和位置传入临时的列表中,这样就得到所有的物品(foreach遍历),得到所有的物品,在字典检测是否存在该场景,如果存在,则将更新场景中的物品列表,不存在,则将场景以及物品列表存入字典
在上述得到场景中的所有物品后,我们需要加载场景中的物品。同样定义一个临时的物品列表,使用字典自带的TryGetValue方法判断字典中的当前场景是否存在物品,存在则将物品列表赋值给临时的物品列表。如果临时的物品列表不为空,则说明场景中有物品,我们则将其在场景生成(存储物品的位置)
ItemManager类
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 public class ItemManager { private Dictionary<string ,List<SceneItem>>sceneItemDict = new Dictionary<string , List<SceneItem>>() private void OnEnable () { EventHandler.BeforeSceneUnloadEvent += OnBeforeSceneUnloadEvent; EventHandler.AfterSceneLoadedEvent += OnAfterSceneLoadedEvent; } private void OnDisable () { EventHandler.BeforeSceneUnloadEvent -= OnBeforeSceneUnloadEvent; EventHandler.AfterSceneLoadedEvent -= OnAfterSceneLoadedEvent;; } private void OnBeforeSceneUnloadEvent () { GetAllSceneItems(); } private void OnAfterSceneLoadedEvent () { RecreateAllItems(); } private void GetAllSceneItems () { List<SceneItem> currentSceneItems = new List<SceneItem>(); foreach (var Item in FindObjectsOfType <Item >()) { SceneItem sceneItem = new SceneItem { ItemID = Item.itemID; position = new SerializableVector3(Item.transform.position); } currentSceneItems.Add(sceneItem); } if (sceneItemDict.ContainsKey(SceneManager.GetActiveScene().name)) { sceneItemDict[SceneManager.GetActiveScene().name] = currentSceneItems; } else { sceneItemDict.Add(SceneManager.GetActiveScene().name, currentSceneItems); } } private void RecreateAllItems () { List<SceneItem> currentSceneItems = new List<SceneItem>(); if (sceneItemDict.TryGetValue(SceneManager.GetActiveScene().name,out currentSceneItems)){ if (currentSceneItems!=null ){ foreach (var Item in FindObjectsOfType <Item >()) { Destroy(Item.gameobject); } foreach )(var Item in currentSceneItems){ Item newItem = Instantiate(itemPrefab, item.position.ToVector3(), Quaternion.identity, itemParent); newItem.Init(item.ItemID); } } } } }
DataCollection类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [System.Serializable ] public class SerializableVector3 { public float x,y,z; public SerizlizableVector3 (Vector3 pos ) { x = pos.x; y = pos.y; z = pos.z; } public Vector3 ToVector3 () { return new Vector3(x, y, z); } public Vector2Int ToVector2Int () { return new Vector2Int((int )x, (int )y); } } [System.Serializable ] public class SceneItem { public int ItemID; public SerializableVector3 position; }
设置鼠标指针根据物品调整 在游戏中,我们会有一个独属于
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 public class CursorManager : MonoBehaviour { public Sprite normal,seed,item,tool; public Sprite currentSprite; private RectTransform cursorCanvas; public Image cursorImage; private void Start () { cursorCanvas = GameObject.FindGameObjectWithTag("CursorCanvas" ).GetComponent<RectTransform>(); cursorImage = cursorCanvas.GetChild(0 ).GetComponent<Image>(); currentSprite = normal; SetCursorImage(normal); } private void OnEnable () { EventHandler.ItemSelectedEvent += OnItemSelectedEvent; } private void OnDisable () { EventHandler.ItemSelectedEvent -= OnItemSelectedEvent; } private void Update () { if (cursorCanvas == null ) return ; cursorImage.transform.position = Input.mousePosition; SetCursorImage(currentSprite); } private void SetCursorImage (Sprite sprite ) { cursorImage.sprite = sprite; cursorImage.color = new Color(1 , 1 , 1 , 1 ); } private void OnItemSelectedEvent (ItemDetails itemDetails, bool isSelected ) { if (!isSelected){ currentSprite = normal; } else { currentSprite = itemDetails.itemType switch { ItemType.seed =>seed; ItemType.item =>seed; ItemType.tool =>seed; _=>normal; } } } private bool InteractWithUI{ if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()){ return true ; } return false ; } }
构建地图信息系统 在game中,我们需要在鼠标在点击地图不同的位置能显示我们能在此处干什么,在此处则不可以。
1.创建自定义的瓦片地图,存储每个瓦片的功能逻辑。
使用Grid Information脚本,获取和显示瓦片地图信息的脚本