1、目標
根據地面屬性(diggable, canDropItem, canPlaceFurniture, isPath, isNPCObstacle)決定角色進行何種操作。比如沒有canDropItem屬性的地面,則不能放置物體。
2、優化保存Item數據
PS:這個是對已有代碼的優化,與本節的主題無關。
打開Assets -> Scripts -> SaveSystem -> SceneSave.cs,
已有的代碼如下:
public class SceneSave
{// string key is an identifier name we choose for this listpublic Dictionary<string, List<SceneItem>> listSceneItemDictionary;
}
已有的數據如下:
sceneSave.listSceneItemDictionary.Add("sceneItemList", sceneItemList);
因為當前只有一個itemList,所以不需要存儲為字典,可以直接存儲為List。
調整后代碼如下:
using System.Collections.Generic;[System.Serializable]public class SceneSave
{// string key is an identifier name we choose for this listpublic List<SceneItem> listSceneItem;
}
然后打開Assets -> Scripts -> Scene -> SceneItemsManager.cs做相應的調整。
調整后的代碼如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;[RequireComponent(typeof(GenerateGUID))]
public class SceneItemsManager : SingletonMonobehaviour<SceneItemsManager>, ISaveable
{private Transform parentItem;[SerializeField] private GameObject itemPrefab = null;private string _iSaveableUniqueID;private GameObjectSave _gameObjectSave;public string ISaveableUniqueID { get { return _iSaveableUniqueID; } set { _iSaveableUniqueID = value; } }public GameObjectSave GameObjectSave { get { return _gameObjectSave; } set { _gameObjectSave = value; } }private void AfterSceneLoad(){parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;}protected override void Awake(){base.Awake();ISaveableUniqueID = GetComponent<GenerateGUID>().GUID;GameObjectSave = new GameObjectSave();}private void OnEnable(){ISaveableRegister();EventHandler.AfterSceneLoadEvent += AfterSceneLoad;}private void OnDisable(){ISaveableDeregister();EventHandler.AfterSceneLoadEvent -= AfterSceneLoad;}public void ISaveableDeregister(){SaveLoadManager.Instance.iSaveableObjectList.Remove(this);}public void ISaveableRegister(){// 將當前對象添加到iSaveableObjectList中SaveLoadManager.Instance.iSaveableObjectList.Add(this);}// 恢復場景public void ISaveableRestoreScene(string sceneName){if(GameObjectSave.sceneData.TryGetValue(sceneName, out SceneSave sceneSave)){if(sceneSave.listSceneItem != null){// scene list items found - destroy existing items in sceneDestroySceneItems();// new instantiate the list of scene itemsInstantiateSceneItems(sceneSave.listSceneItem);}}}private void InstantiateSceneItems(List<SceneItem> sceneItemList){GameObject itemGameObject;foreach(SceneItem sceneItem in sceneItemList){itemGameObject = Instantiate(itemPrefab, new Vector3(sceneItem.position.x, sceneItem.position.y, sceneItem.position.z), Quaternion.identity, parentItem);Item item = itemGameObject.GetComponent<Item>();item.ItemCode = sceneItem.itemCode;item.name = sceneItem.itemName;}}// Destroy items currently in the sceneprivate void DestroySceneItems(){// Get all items in the sceneItem[] itemsInScene = GameObject.FindObjectsOfType<Item>();// Loop through all scene items and destroy themfor(int i = itemsInScene.Length - 1; i > -1; i--){Destroy(itemsInScene[i].gameObject);}}// 保存場景public void ISaveableStoreScene(string sceneName){// Remove old scene save for gameObject if existsGameObjectSave.sceneData.Remove(sceneName);// Get all items in the sceneList<SceneItem> sceneItemList = new List<SceneItem>();Item[] itemsInScene = FindObjectsOfType<Item>();// Loop through all scene itemsforeach(Item item in itemsInScene){SceneItem sceneItem = new SceneItem();sceneItem.itemCode = item.ItemCode;sceneItem.position = new Vector3Serializable(item.transform.position.x, item.transform.position.y,item.transform.position.z);sceneItem.itemName = item.name;// Add scene item to listsceneItemList.Add(sceneItem);}// Create list scene items dictionary in scene save and add to itSceneSave sceneSave = new SceneSave();sceneSave.listSceneItem = sceneItemList;// Add scene save to gameobjectGameObjectSave.sceneData.Add(sceneName, sceneSave);}
}
改動點是:
// 恢復場景
? ? public void ISaveableRestoreScene(string sceneName)
? ? {
? ? ? ? if(GameObjectSave.sceneData.TryGetValue(sceneName, out SceneSave sceneSave))
? ? ? ? {
? ? ? ? ? ? if(sceneSave.listSceneItem != null)? // 精簡了這塊的代碼
? ? ? ? ? ? {
? ? ? ? ? ? ? ? // scene list items found - destroy existing items in scene
? ? ? ? ? ? ? ? DestroySceneItems();
? ? ? ? ? ? ? ? // new instantiate the list of scene items
? ? ? ? ? ? ? ? InstantiateSceneItems(sceneSave.listSceneItem);
? ? ? ? ? ? }
? ? ? ? }
? ? }
3、優化SceneSave.cs
增加保存地面屬性信息。
using System.Collections.Generic;[System.Serializable]public class SceneSave
{// string key is an identifier name we choose for this listpublic List<SceneItem> listSceneItem;public Dictionary<string, GridPropertyDetails> gridPropertyDetailsDictionary; // key是坐標信息,value是地面屬性信息
}
public Dictionary<string, GridPropertyDetails> gridPropertyDetailsDictionary;中,string會保存網格的坐標信息。
4、創建GridPropertyDetails.cs
在Assets -> Scripts -> Map下新增GridPropertyDetails.cs腳本。
[System.Serializable]
public class GridPropertyDetails
{public int gridX;public int gridY;public bool isDiggable = false;public bool canDropItem = false;public bool canPlaceFurniture = false;public bool isPath = false;public bool isNPCObstacle = false;public int daysSinceDug = -1;public int daysSinceWatered = -1;public int seedItemCode = -1;public int growthDays = -1;public int daysSinceLastHarvest = -1;public GridPropertyDetails() { }
}
其中上半部分的屬性是已知的,下半部分的信息是在運行中賦值的。
5、創建GridPropertiesManager.cs腳本
在Assets -> Scripts -> Map下新增GridPropertiesManager.cs腳本。
該類會讀取Assets -> Scriptable Object Assets -> Maps下的3個so_xxx資源文件,然后寫到SceneSave的gridPropertyDetailsDictionary中去。
using System.Collections;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;[RequireComponent(typeof(GenerateGUID))]
public class GridPropertiesManager : SingletonMonobehaviour<GridPropertiesManager>, ISaveable
{public Grid grid;private Dictionary<string, GridPropertyDetails> gridPropertyDictionary;[SerializeField] private SO_GridProperties[] so_gridPropertiesArray = null;private string _iSaveableUniqueID;private GameObjectSave _gameObjectSave;public string ISaveableUniqueID { get { return _iSaveableUniqueID; } set { _iSaveableUniqueID = value; } }public GameObjectSave GameObjectSave { get { return _gameObjectSave; } set { _gameObjectSave = value; } }protected override void Awake(){base.Awake();ISaveableUniqueID = GetComponent<GenerateGUID>().GUID;GameObjectSave = new GameObjectSave();}private void OnEnable(){ISaveableRegister();EventHandler.AfterSceneLoadEvent += AfterSceneLoaded;}private void OnDisable() {ISaveableDeregister();EventHandler.AfterSceneLoadEvent -= AfterSceneLoaded;}private void AfterSceneLoaded(){// Get Gridgrid = GameObject.FindObjectOfType<Grid>();}public void ISaveableDeregister(){SaveLoadManager.Instance.iSaveableObjectList.Remove(this);}public void ISaveableRegister(){SaveLoadManager.Instance.iSaveableObjectList.Add(this);}public void ISaveableRestoreScene(string sceneName){// Get sceneSave for scene - it exists since we created it in initialiseif(GameObjectSave.sceneData.TryGetValue(sceneName, out SceneSave sceneSave)){// get grid property details dictionary - it exists since we created it in initialiseif(sceneSave.gridPropertyDetailsDictionary != null){gridPropertyDictionary = sceneSave.gridPropertyDetailsDictionary;}}}public void ISaveableStoreScene(string sceneName){// Remove sceneSave for sceneGameObjectSave.sceneData.Remove(sceneName);// Create sceneSave for sceneSceneSave sceneSave = new SceneSave();// create & add dict grid property details dictionarysceneSave.gridPropertyDetailsDictionary = gridPropertyDictionary;// Add scene save to game object scene dataGameObjectSave.sceneData.Add(sceneName, sceneSave);}private void Start(){InitialiseGridProperties();}/// <summary>/// This initialises the grid property dictionary with the values from the SO_GridProperties assets and stores the values for each scene in/// GameObjectSave sceneData/// </summary>private void InitialiseGridProperties(){// loop through all gridproperties in the arrayforeach(SO_GridProperties so_GridProperties in so_gridPropertiesArray){// Create dictionary of grid property detailsDictionary<string, GridPropertyDetails> gridPropertyDictionary = new Dictionary<string, GridPropertyDetails>();// Populate grid property dictionary - Iterate through all the grid properties in the so gridproperties listforeach(GridProperty gridProperty in so_GridProperties.gridPropertyList){GridPropertyDetails gridPropertyDetails;gridPropertyDetails = GetGridPropertyDetails(gridProperty.gridCoordinate.x, gridProperty.gridCoordinate.y, gridPropertyDictionary);if(gridPropertyDetails == null){gridPropertyDetails = new GridPropertyDetails();}switch (gridProperty.gridBoolProperty){case GridBoolProperty.diggable:gridPropertyDetails.isDiggable = gridProperty.gridBoolValue;break;case GridBoolProperty.canDropItem:gridPropertyDetails.canDropItem = gridProperty.gridBoolValue;break;case GridBoolProperty.canPlaceFurniture:gridPropertyDetails.canPlaceFurniture = gridProperty.gridBoolValue;break;case GridBoolProperty.isPath:gridPropertyDetails.isPath = gridProperty.gridBoolValue;break;case GridBoolProperty.isNPCObstacle:gridPropertyDetails.isNPCObstacle = gridProperty.gridBoolValue;break;default:break; }SetGridPropertyDetails(gridProperty.gridCoordinate.x, gridProperty.gridCoordinate.y, gridPropertyDetails, gridPropertyDictionary);}// Create scene save for this gameobjectSceneSave sceneSave = new SceneSave();// Add grid property dictionary to scene save datasceneSave.gridPropertyDetailsDictionary = gridPropertyDictionary;// If starting scene set the griProertyDictionary member variable to the current iterationif(so_GridProperties.sceneName.ToString() == SceneControllerManager.Instance.startingSceneName.ToString()){this.gridPropertyDictionary = gridPropertyDictionary;}// Add scene save to game object scene dataGameObjectSave.sceneData.Add(so_GridProperties.sceneName.ToString(), sceneSave);}}/// <summary>/// Returns the gridPropertyDetails at the gridlocation fro the supplied dictionary,/// or null if no properties exist at that location/// </summary>/// <param name="gridX"></param>/// <param name="gridY"></param>/// <param name="gridPropertyDictionary"></param>/// <returns></returns>public GridPropertyDetails GetGridPropertyDetails(int gridX, int gridY, Dictionary<string, GridPropertyDetails> gridPropertyDictionary){// Construct key from coordinatestring key = "x" + gridX + "y" + gridY;GridPropertyDetails gridPropertyDetails;// Check if grid property details exist for coordinate and retrieveif (!gridPropertyDictionary.TryGetValue(key, out gridPropertyDetails)){// if not foundreturn null;}else{return gridPropertyDetails;}}public GridPropertyDetails GetGridPropertyDetails(int gridX, int gridY){return GetGridPropertyDetails(gridX, gridY, gridPropertyDictionary);}/// <summary>/// Set the grid property details to gridPropertyDetails fro the tile at (gridX, gridY) for current scene/// </summary>/// <param name="gridX"></param>/// <param name="gridY"></param>/// <param name="gridPropertyDetails"></param>public void SetGridPropertyDetails(int gridX, int gridY, GridPropertyDetails gridPropertyDetails){SetGridPropertyDetails(gridX, gridY, gridPropertyDetails, gridPropertyDictionary);}/// <summary>/// Set the grid property details to gridPropertyDetails for the title at (gridX, gridY) for the gridPropertyDictionary./// </summary>/// <param name="gridX"></param>/// <param name="gridY"></param>/// <param name="gridPropertyDetails"></param>/// <param name="gridPropertyDictionary"></param>public void SetGridPropertyDetails(int gridX, int gridY, GridPropertyDetails gridPropertyDetails, Dictionary<string, GridPropertyDetails> gridPropertyDictionary){// Construct key from coordinatestring key = "x" + gridX + "y" + gridY;gridPropertyDetails.gridX = gridX;gridPropertyDetails.gridY = gridY;// Set value gridPropertyDictionary[key] = gridPropertyDetails;}}
6、優化Settings.cs
Assets -> Scripts -> Misc -> Settings.cs文件。
增加如下代碼:
// Tilemappublic const float gridCellSize = 1f; // grid cell size in unity units
7、優化UIInventorySlot.cs
Assets -> Scripts -> UI -> UIInventory -> UIInventorySlot.cs
經過前面幾步的準備,現在我們在Drop Item時可以做一層判斷。
調整DropSelectedItemAtMousePosition函數如下:
private void DropSelectedItemAtMousePosition(){if(itemDetails != null && isSelected){Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, -mainCamera.transform.position.z));// If can drop item hereVector3Int gridPosition = GridPropertiesManager.Instance.grid.WorldToCell(worldPosition);GridPropertyDetails gridPropertyDetails = GridPropertiesManager.Instance.GetGridPropertyDetails(gridPosition.x, gridPosition.y);if(gridPropertyDetails != null && gridPropertyDetails.canDropItem){// Create item from prefab at mouse positionGameObject itemGameObject = Instantiate(itemPrefab, new Vector3(worldPosition.x, worldPosition.y - Settings.gridCellSize/2f, worldPosition.z), Quaternion.identity, parentItem);Item item = itemGameObject.GetComponent<Item>();item.ItemCode = itemDetails.itemCode;// Remove item from player's inventoryInventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);// If no more of item then clear selectedif (InventoryManager.Instance.FindItemInInventory(InventoryLocation.player, item.ItemCode) == -1){ClearSelectedItem();}}}}
因為Item的中心在pivot(底部),如果不調整item的中心位置,每次放置Item到地面時都會導致Item的y軸上的落地點比鼠標位置高一點。
所以我們通過new Vector3(worldPosition.x, worldPosition.y - Settings.gridCellSize/2f, worldPosition.z),適當調整y的位置。
UIInventorySlot.cs完整代碼如下:
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System;public class UIInventorySlot : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler, IPointerClickHandler
{private Camera mainCamera;private Transform parentItem; // 場景中的物體父類private GameObject draggedItem; // 被拖動的物體private Canvas parentCanvas;public Image inventorySlotHighlight;public Image inventorySlotImage;public TextMeshProUGUI textMeshProUGUI;[SerializeField] private UIInventoryBar inventoryBar = null;[SerializeField] private GameObject itemPrefab = null;[SerializeField] private int slotNumber = 0; // 插槽的序列號[SerializeField] private GameObject inventoryTextBoxPrefab = null;[HideInInspector] public ItemDetails itemDetails;[HideInInspector] public int itemQuantity;[HideInInspector] public bool isSelected = false;private void Awake(){parentCanvas = GetComponentInParent<Canvas>();}private void OnDisable(){EventHandler.AfterSceneLoadEvent -= SceneLoaded;}private void OnEnable(){EventHandler.AfterSceneLoadEvent += SceneLoaded;}public void SceneLoaded(){parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;}private void Start(){mainCamera = Camera.main;//parentItem = GameObject.FindGameObjectWithTag(Tags.ItemsParentTransform).transform;}public void OnBeginDrag(PointerEventData eventData){if(itemDetails != null) {// Disable keyboard inputPlayer.Instance.DisablePlayerInputAndResetMovement();// Instatiate gameobject as dragged itemdraggedItem = Instantiate(inventoryBar.inventoryBarDraggedItem, inventoryBar.transform);// Get image for dragged itemImage draggedItemImage = draggedItem.GetComponentInChildren<Image>();draggedItemImage.sprite = inventorySlotImage.sprite;SetSelectedItem();}}public void OnDrag(PointerEventData eventData){// move game object as dragged itemif(!draggedItem != null){draggedItem.transform.position = Input.mousePosition;}}public void OnEndDrag(PointerEventData eventData){// Destroy game object as dragged itemif (draggedItem != null) {Destroy(draggedItem);// if drag ends over inventory bar, get item drag is over and swap thenif (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>() != null) {// get the slot number where the drag endedint toSlotNumber = eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>().slotNumber;// Swap inventory items in inventory listInventoryManager.Instance.SwapInventoryItems(InventoryLocation.player, slotNumber, toSlotNumber);// Destroy inventory text boxDestroyInventoryTextBox();// Clear selected itemClearSelectedItem();}else{// else attemp to drop the item if it can be droppedif (itemDetails.canBeDropped){DropSelectedItemAtMousePosition();}}// Enable player inputPlayer.Instance.EnablePlayerInput();}}/// <summary>/// Drops the item(if selected) at the current mouse position. called by the DropItem event/// </summary>private void DropSelectedItemAtMousePosition(){if(itemDetails != null && isSelected){Vector3 worldPosition = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, -mainCamera.transform.position.z));// If can drop item hereVector3Int gridPosition = GridPropertiesManager.Instance.grid.WorldToCell(worldPosition);GridPropertyDetails gridPropertyDetails = GridPropertiesManager.Instance.GetGridPropertyDetails(gridPosition.x, gridPosition.y);if(gridPropertyDetails != null && gridPropertyDetails.canDropItem){// Create item from prefab at mouse positionGameObject itemGameObject = Instantiate(itemPrefab, new Vector3(worldPosition.x, worldPosition.y - Settings.gridCellSize/2f, worldPosition.z), Quaternion.identity, parentItem);Item item = itemGameObject.GetComponent<Item>();item.ItemCode = itemDetails.itemCode;// Remove item from player's inventoryInventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);// If no more of item then clear selectedif (InventoryManager.Instance.FindItemInInventory(InventoryLocation.player, item.ItemCode) == -1){ClearSelectedItem();}}}}public void OnPointerEnter(PointerEventData eventData){// Populate text box with item detailsif(itemQuantity != 0){// Instantiate inventory text boxinventoryBar.inventoryTextBoxGameobject = Instantiate(inventoryTextBoxPrefab, transform.position, Quaternion.identity);inventoryBar.inventoryTextBoxGameobject.transform.SetParent(parentCanvas.transform, false);UIInventoryTextBox inventoryTextBox = inventoryBar.inventoryTextBoxGameobject.GetComponent<UIInventoryTextBox>();// Set item type descriptionstring itemTypeDescription = InventoryManager.Instance.GetItemTypeDescription(itemDetails.itemType);// Populate text boxinventoryTextBox.SetTextboxText(itemDetails.itemDescription, itemTypeDescription, "", itemDetails.itemLongDescription, "", "");// Set text box position according to inventory bar positionif (inventoryBar.IsInventoryBarPositionBottom){inventoryBar.inventoryTextBoxGameobject.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 0f);inventoryBar.inventoryTextBoxGameobject.transform.position = new Vector3(transform.position.x, transform.position.y + 50f, transform.position.z);}else{inventoryBar.inventoryTextBoxGameobject.GetComponent<RectTransform>().pivot = new Vector2(0.5f, 1f);inventoryBar.inventoryTextBoxGameobject.transform.position = new Vector3(transform.position.x, transform.position.y - 50f, transform.position.z);}}}public void OnPointerExit(PointerEventData eventData){DestroyInventoryTextBox();}private void DestroyInventoryTextBox(){if (inventoryBar.inventoryTextBoxGameobject != null) {Destroy(inventoryBar.inventoryTextBoxGameobject);}}public void OnPointerClick(PointerEventData eventData){// if left clickif (eventData.button == PointerEventData.InputButton.Left){// if inventory slot currently selected then deselectif (isSelected == true){ClearSelectedItem();}else // 未被選中且有東西則顯示選中的效果{if(itemQuantity > 0){SetSelectedItem();}}}}/// <summary>/// Set this inventory slot item to be selected/// </summary>private void SetSelectedItem(){// Clear currently highlighted itemsinventoryBar.ClearHighlightOnInventorySlots();// Highlight item on inventory barisSelected = true;// Set highlighted inventory slotsinventoryBar.SetHighlightedInventorySlots();// Set item selected in inventoryInventoryManager.Instance.SetSelectedInventoryItem(InventoryLocation.player, itemDetails.itemCode);if (itemDetails.canBeCarried == true){// Show player carrying itemPlayer.Instance.ShowCarriedItem(itemDetails.itemCode);}else {Player.Instance.ClearCarriedItem();}}private void ClearSelectedItem(){// Clear currently highlighted iteminventoryBar.ClearHighlightOnInventorySlots();isSelected = false;// set no item selected in inventoryInventoryManager.Instance.ClearSelectedInventoryItem(InventoryLocation.player);// Clear player carrying itemPlayer.Instance.ClearCarriedItem();}
}
8、創建GridPropertiesManager對象
在Hierarchy -> PersistentScene下創建空物體命名為:GridPropertiesManager。
添加組件GridPropertiesManager如下(同時會自動創建Generate GUID組件):
然后,鎖定面板,將3個so_xxx 文件拖到So_grid Properties Array中:
保存后執行程序:將采集的corn拖到地板位置,無法放置。
9、繪制CanDropItem區域
點擊Scene1_Farm的GridProperties對象,在Inspector勾選組件啟用。
點擊BoolCanDropItem對象,點擊Tile Palette面板,使用tile在scene界面中放置瓦片。
然后進入Inspector面板,反勾選Tilemap Render組件。
接著點擊GridProperties對象,反勾選GridProperties組件。此時點擊Assets -> Scriptable Object Assets的So_Grid_Properties_Scene1_Farm文件,可以看到Grid Property List有2000+個元素。
接下來,針對Scene2和Scene3進行同樣的操作。
Scene2:
Scene3:
10、效果演示
如上所示,草地位置是有CanDropItem屬性的,所以可以放置南瓜;而屋頂位置沒有CanDropItem屬性,所以無法放置南瓜。