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 list
public 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 list
public 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 scene
DestroySceneItems();
// new instantiate the list of scene items
InstantiateSceneItems(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 scene
private void DestroySceneItems()
{
// Get all items in the scene
Item[] itemsInScene = GameObject.FindObjectsOfType<Item>();
// Loop through all scene items and destroy them
for(int i = itemsInScene.Length - 1; i > -1; i--)
{
Destroy(itemsInScene[i].gameObject);
}
}
// 保存场景
public void ISaveableStoreScene(string sceneName)
{
// Remove old scene save for gameObject if exists
GameObjectSave.sceneData.Remove(sceneName);
// Get all items in the scene
List<SceneItem> sceneItemList = new List<SceneItem>();
Item[] itemsInScene = FindObjectsOfType<Item>();
// Loop through all scene items
foreach(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 list
sceneItemList.Add(sceneItem);
}
// Create list scene items dictionary in scene save and add to it
SceneSave sceneSave = new SceneSave();
sceneSave.listSceneItem = sceneItemList;
// Add scene save to gameobject
GameObjectSave.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 list
public 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 Grid
grid = 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 initialise
if(GameObjectSave.sceneData.TryGetValue(sceneName, out SceneSave sceneSave))
{
// get grid property details dictionary - it exists since we created it in initialise
if(sceneSave.gridPropertyDetailsDictionary != null)
{
gridPropertyDictionary = sceneSave.gridPropertyDetailsDictionary;
}
}
}
public void ISaveableStoreScene(string sceneName)
{
// Remove sceneSave for scene
GameObjectSave.sceneData.Remove(sceneName);
// Create sceneSave for scene
SceneSave sceneSave = new SceneSave();
// create & add dict grid property details dictionary
sceneSave.gridPropertyDetailsDictionary = gridPropertyDictionary;
// Add scene save to game object scene data
GameObjectSave.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 array
foreach(SO_GridProperties so_GridProperties in so_gridPropertiesArray)
{
// Create dictionary of grid property details
Dictionary<string, GridPropertyDetails> gridPropertyDictionary = new Dictionary<string, GridPropertyDetails>();
// Populate grid property dictionary - Iterate through all the grid properties in the so gridproperties list
foreach(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 gameobject
SceneSave sceneSave = new SceneSave();
// Add grid property dictionary to scene save data
sceneSave.gridPropertyDetailsDictionary = gridPropertyDictionary;
// If starting scene set the griProertyDictionary member variable to the current iteration
if(so_GridProperties.sceneName.ToString() == SceneControllerManager.Instance.startingSceneName.ToString())
{
this.gridPropertyDictionary = gridPropertyDictionary;
}
// Add scene save to game object scene data
GameObjectSave.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 coordinate
string key = "x" + gridX + "y" + gridY;
GridPropertyDetails gridPropertyDetails;
// Check if grid property details exist for coordinate and retrieve
if (!gridPropertyDictionary.TryGetValue(key, out gridPropertyDetails))
{
// if not found
return 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 coordinate
string key = "x" + gridX + "y" + gridY;
gridPropertyDetails.gridX = gridX;
gridPropertyDetails.gridY = gridY;
// Set value
gridPropertyDictionary[key] = gridPropertyDetails;
}
}
6、优化Settings.cs
Assets -> Scripts -> Misc -> Settings.cs文件。
增加如下代码:
// Tilemap
public 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 here
Vector3Int 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 position
GameObject 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 inventory
InventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);
// If no more of item then clear selected
if (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 input
Player.Instance.DisablePlayerInputAndResetMovement();
// Instatiate gameobject as dragged item
draggedItem = Instantiate(inventoryBar.inventoryBarDraggedItem, inventoryBar.transform);
// Get image for dragged item
Image draggedItemImage = draggedItem.GetComponentInChildren<Image>();
draggedItemImage.sprite = inventorySlotImage.sprite;
SetSelectedItem();
}
}
public void OnDrag(PointerEventData eventData)
{
// move game object as dragged item
if(!draggedItem != null)
{
draggedItem.transform.position = Input.mousePosition;
}
}
public void OnEndDrag(PointerEventData eventData)
{
// Destroy game object as dragged item
if (draggedItem != null)
{
Destroy(draggedItem);
// if drag ends over inventory bar, get item drag is over and swap then
if (eventData.pointerCurrentRaycast.gameObject != null && eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>() != null)
{
// get the slot number where the drag ended
int toSlotNumber = eventData.pointerCurrentRaycast.gameObject.GetComponent<UIInventorySlot>().slotNumber;
// Swap inventory items in inventory list
InventoryManager.Instance.SwapInventoryItems(InventoryLocation.player, slotNumber, toSlotNumber);
// Destroy inventory text box
DestroyInventoryTextBox();
// Clear selected item
ClearSelectedItem();
}
else
{
// else attemp to drop the item if it can be dropped
if (itemDetails.canBeDropped)
{
DropSelectedItemAtMousePosition();
}
}
// Enable player input
Player.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 here
Vector3Int 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 position
GameObject 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 inventory
InventoryManager.Instance.RemoveItem(InventoryLocation.player, item.ItemCode);
// If no more of item then clear selected
if (InventoryManager.Instance.FindItemInInventory(InventoryLocation.player, item.ItemCode) == -1)
{
ClearSelectedItem();
}
}
}
}
public void OnPointerEnter(PointerEventData eventData)
{
// Populate text box with item details
if(itemQuantity != 0)
{
// Instantiate inventory text box
inventoryBar.inventoryTextBoxGameobject = Instantiate(inventoryTextBoxPrefab, transform.position, Quaternion.identity);
inventoryBar.inventoryTextBoxGameobject.transform.SetParent(parentCanvas.transform, false);
UIInventoryTextBox inventoryTextBox = inventoryBar.inventoryTextBoxGameobject.GetComponent<UIInventoryTextBox>();
// Set item type description
string itemTypeDescription = InventoryManager.Instance.GetItemTypeDescription(itemDetails.itemType);
// Populate text box
inventoryTextBox.SetTextboxText(itemDetails.itemDescription, itemTypeDescription, "", itemDetails.itemLongDescription, "", "");
// Set text box position according to inventory bar position
if (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 click
if (eventData.button == PointerEventData.InputButton.Left)
{
// if inventory slot currently selected then deselect
if (isSelected == true)
{
ClearSelectedItem();
}
else // 未被选中且有东西则显示选中的效果
{
if(itemQuantity > 0)
{
SetSelectedItem();
}
}
}
}
/// <summary>
/// Set this inventory slot item to be selected
/// </summary>
private void SetSelectedItem()
{
// Clear currently highlighted items
inventoryBar.ClearHighlightOnInventorySlots();
// Highlight item on inventory bar
isSelected = true;
// Set highlighted inventory slots
inventoryBar.SetHighlightedInventorySlots();
// Set item selected in inventory
InventoryManager.Instance.SetSelectedInventoryItem(InventoryLocation.player, itemDetails.itemCode);
if (itemDetails.canBeCarried == true)
{
// Show player carrying item
Player.Instance.ShowCarriedItem(itemDetails.itemCode);
}
else
{
Player.Instance.ClearCarriedItem();
}
}
private void ClearSelectedItem()
{
// Clear currently highlighted item
inventoryBar.ClearHighlightOnInventorySlots();
isSelected = false;
// set no item selected in inventory
InventoryManager.Instance.ClearSelectedInventoryItem(InventoryLocation.player);
// Clear player carrying item
Player.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属性,所以无法放置南瓜。