Unity3D仿星露谷物语开发50之初始化农作物
1、目标
创建一个农作物实例化组件,这将使树木等农作物在设计时被添加到场景中,然后加载到网格的属性中。
这样农作物就不用像之前一样,由Player播散种子后等待慢慢长大。而是在进入游戏时,就有一些各个生长阶段的树木了。
2、修改EventHandler.cs脚本
添加一个新Event来实例化农作物预制体,我们会调用这个Event作为网格属性初始化的一部分。
// Instantiate crop prefabs
public static event Action InstantiateCropPrefabsEvent;public static void CallInstantiateCropPrefabsEvent()
{if(InstantiateCropPrefabsEvent != null){InstantiateCropPrefabsEvent();}
}
3、创建CropInstantiator.cs脚本
在Assets -> Scripts -> Crop下新建脚本命名为CropInstantiator.cs。
该脚本需要附件到预制体以设置网格属性字典中的值。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;/// <summary>
/// Attach to a crop prefab to set the values in the grid property dictionary
/// 主要作用:在场景中实例化农作物预制体,将相关属性数据存储到游戏的网格系统中。
/// 这个脚本的核心是将Inspector中设置的农作物属性数据传递到游戏的网格系统中,传递完成后这个临时示例就不再需要了
/// </summary>
public class CropInstantiator : MonoBehaviour
{private Grid grid;[SerializeField] private int daysSinceDug = -1;[SerializeField] private int daysSinceWatered = -1;[ItemCodeDescription][SerializeField] private int seedItemCode = 0;[SerializeField] private int growthDays = 0;private void OnEnable(){// 通过预制体挂载此脚本的方式,可以在是合理化时快速设置网格属性,而不需要复杂的手动配置过程。EventHandler.InstantiateCropPrefabsEvent += InstantiateCropPrefabs;}private void OnDisable(){EventHandler.InstantiateCropPrefabsEvent -= InstantiateCropPrefabs;}private void InstantiateCropPrefabs(){// Get grid gameobjectgrid = GameObject.FindObjectOfType<Grid>();// Get grid position for cropVector3Int cropGridPosition = grid.WorldToCell(transform.position);// Set Crop Grid PropertiesSetCropGridProperties(cropGridPosition);// Destroy this gameobject// 如果不销毁,场景中会残留大量仅用于数据传递的空壳对象Destroy(gameObject);}private void SetCropGridProperties(Vector3Int cropGridPosition){if(seedItemCode > 0){GridPropertyDetails gridPropertyDetails;gridPropertyDetails = GridPropertiesManager.Instance.GetGridPropertyDetails(cropGridPosition.x, cropGridPosition.y);if(gridPropertyDetails == null){gridPropertyDetails = new GridPropertyDetails();}gridPropertyDetails.daysSinceDug = daysSinceDug;gridPropertyDetails.daysSinceWatered = daysSinceWatered;gridPropertyDetails.seedItemCode = seedItemCode;gridPropertyDetails.growthDays = growthDays;GridPropertiesManager.Instance.SetGridPropertyDetails(cropGridPosition.x, cropGridPosition.y, gridPropertyDetails);}}}
工作流程:
- 开发者在 Unity 编辑器中创建农作物预制体,并设置好各项属性值
- 游戏运行时,当需要在场景中种植农作物时,实例化该预制体
- 预制体上的CropInstantiator脚本响应事件,将属性值写入网格系统
- 数据传递完成后,预制体自动销毁,只保留数据在系统中
- 游戏的农作物生长系统会从网格系统读取数据,控制农作物生长
4、修改SceneSave.cs脚本
确保只在第一次加载场景时实例化这些作物。
using System.Collections.Generic;[System.Serializable]public class SceneSave
{public Dictionary<string, bool> boolDictionary; // string key is an identifier name we choose for this list// string key is an identifier name we choose for this listpublic List<SceneItem> listSceneItem;public Dictionary<string, GridPropertyDetails> gridPropertyDetailsDictionary; // key是坐标信息,value是地面属性信息
}
5、修改GridPropertiesManager.cs脚本
1)属性变量添加如下内容:
private bool isFirstTimeSceneLoaded = true;
2)更新InitialiseGridProperties函数:
添加如下内容:
sceneSave.boolDictionary = new Dictionary<string, bool>();
sceneSave.boolDictionary.Add("isFirstTimeSceneLoaded", true);
然后我们可以在存储场景和恢复场景中使用这个来控制是否 是第一次场景加载或没有。
函数完整代码如下:
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 bool dictionary and set first time scene loaded to truesceneSave.boolDictionary = new Dictionary<string, bool>();sceneSave.boolDictionary.Add("isFirstTimeSceneLoaded", true);// Add scene save to game object scene dataGameObjectSave.sceneData.Add(so_GridProperties.sceneName.ToString(), sceneSave);}
}
3)更新ISaveableRestoreScene函数
添加如下内容:
// get dictionary of bools - it exists since we created it in initialise
if(sceneSave.boolDictionary != null && sceneSave.boolDictionary.TryGetValue("isFirstTimeSceneLoaded", out bool storedIsFirstTimeSceneLoaded))
{isFirstTimeSceneLoaded = storedIsFirstTimeSceneLoaded;
}// Instantiate any crop prefabs iniitially present in the scene
if (isFirstTimeSceneLoaded)EventHandler.CallInstantiateCropPrefabsEvent();/ Update first time scene loaded bool
if(isFirstTimeSceneLoaded == true)
{isFirstTimeSceneLoaded = false;
}
我们在第一次场景加载时使用这个存储值来触发这个农作物实例化,完整代码如下:
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;}// get dictionary of bools - it exists since we created it in initialiseif(sceneSave.boolDictionary != null && sceneSave.boolDictionary.TryGetValue("isFirstTimeSceneLoaded", out bool storedIsFirstTimeSceneLoaded)){isFirstTimeSceneLoaded = storedIsFirstTimeSceneLoaded;}// Instantiate any crop prefabs iniitially present in the sceneif (isFirstTimeSceneLoaded)EventHandler.CallInstantiateCropPrefabsEvent();// If grid properties existif(gridPropertyDictionary.Count > 0){// grid property details found for the current scene destroy existing ground decorationClearDisplayGridPropertyDetails();// Instantiate grid property details for current sceneDisplayGridPropertyDetails();}// Update first time scene loaded boolif(isFirstTimeSceneLoaded == true){isFirstTimeSceneLoaded = false;}}}
4)修改ISaveableStoreScene函数
添加如下内容:
// create && add bool dictionary for first time scene loaded
sceneSave.boolDictionary = new Dictionary<string, bool>();
sceneSave.boolDictionary.Add("isFirstTimeSceneLoaded", isFirstTimeSceneLoaded);
完整代码如下:
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;// create && add bool dictionary for first time scene loadedsceneSave.boolDictionary = new Dictionary<string, bool>();sceneSave.boolDictionary.Add("isFirstTimeSceneLoaded", isFirstTimeSceneLoaded);// Add scene save to game object scene dataGameObjectSave.sceneData.Add(sceneName, sceneSave);
}
6、创建Blue Spruce预制体
在Assets -> Prefabs -> Crop下创建"Instantiate In Scene"目录,这个目录下的农作物预制体会直接添加到场景中,会有砍树时飘落叶+砍完之后露出树桩等特效。
而之前的Scenary中的树只有最基本的功能。(只有Sprite Renderer + Box Collider 2D组件)
在"Instantiate In Scene"下创建子目录命名为"Instantiate Blue Spruce",我们要在该目录下为Blue Spruce的不同生长期创建一些预制体。
在Hierarchy -> PersistentScene下新建物体命名为BlueSpruce_5Days,添加相关组件如下:
在BlueSpruce_5Days下创建子物体命名为SecondSprite,添加相关组件如下:
将BlueSpruce_5Days拖到"Instantiate Blue Spruce"目录下,然后再删除之。
在Assets -> Prefabs -> Crop -> "Instantiate Blue Spruce"目录下,右击BlueSpruce_5Days,Create -> Prefab Variant并重命名为"BlueSpruce_10Days",
修改的内容如下:
右击BlueSpruce_5Days,Create -> Prefab Variant并重命名为"BlueSpruce_15Days",并进行相应的修改如下:
右击BlueSpruce_5Days,Create -> Prefab Variant并重命名为"BlueSpruce_20Days",并进行相应的修改如下:
返回到BlueSpruce_5Days双击,在SecondSprite中修改Sprite Renderer信息如下:
因为其他3个都是基于BlueSpruce_5Days的变体,所以可以看到其他3个的值同样发生了变化。
点击BlueSpruce_20Days,再点击SecondSprite,修改Sprite的值如下:
7、布置Blue Spruce
加载Scene1_Farm场景,然后把刚才创建的杉树预制体放到该场景中。所有物体均放到Scene1_Farm -> Crops目录下。
处理完之后卸载Scene1_Farm。
运行游戏后,我们发现Player经过刚创建的BlueSpruce后面会有Fade的效果,同时砍伐树木之前的特效都在。
查看Scene1_Farm -> Crops下的信息如下,我们发现都是CropStandard(Clone)和CropTreeBlueSpruce(Clone)。
而在设计阶段,我们给Crops的配置信息如下:
为什么Crops下的物体发生了变化?为什么仅仅通过CropInstantiator的配置,树的效果都有了?
答案是:设计阶段放置的物体(比如BlueSpruce_20Days)本来就在CropInstantiator的InstantiateCropPrefabs函数的最后一步Destroy了,它的作用是给Grid配置属性,任务完成后就会自动销毁。
而通过CropInstantiator的配置树的效果生效,是因为在GridPropertiesManager函数中,在ISaveableRestoreScene函数中,有如下的代码:
// Instantiate grid property details for current scene
DisplayGridPropertyDetails();
该函数的代码是:
private void DisplayGridPropertyDetails()
{// Loop throught all grid itemsforeach(KeyValuePair<string, GridPropertyDetails> item in gridPropertyDictionary){GridPropertyDetails gridPropertyDetails = item.Value;DisplayDugGround(gridPropertyDetails);DisplayWateredGround(gridPropertyDetails);DisplayPlantedCrop(gridPropertyDetails);}
}
在DisplayPlantedCrop函数中,会实例化之前自带特效的prefab。
同时,在GridPropertiesManager的AdvanceDay方法中,最后一步就是执行DisplayGridPropertyDetails方法。
植物的状态都是由GridPropertiesManager类控制的。场景被LoadScene时,或者用户播散种子之后,都会执行DisplayGridPropertyDetails方法。
8、创建Canyon Oak预制体并布置
方法同Blue Spruce。
将Instantiate Blue Spruce下的BlueSpruce_5Days拖入到PersistentScene进行编辑。
右击 -> Prefab -> Unpack Completely,解除与prefab中的绑定关系。
重命名为CanyonOak_5Days,其他配置如下:
然后把CanyonOak_5Days放到Assets -> Prefabs -> Crop -> Instantiate Canyon Oak下。
依次创建如下预制体:
CanyonOak_10Days:(需要通过create -> prefab variant的方式)
CanyonOak_15Days:
CanyonOak_20Days(分两步),第一步设置根的sprite:
第二步设置子的sprite:
最后,在Scene中创建CanyonOak树。
本节完毕,可体验游戏。