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属性,所以无法放置南瓜。