我們來拆解一個種田游戲,這個游戲種類內部的功能還是比較模板化的,我們來一點點說。
我們大體上分為這么幾個部分:
農場運營玩法
角色與玩家互動
物品與背包
存檔和進度管理
用戶界面系統
農場運營
可以大體上分為:
種植系統:支持種植、成長、收獲等完整的植物生命周期;動物系統:包含野生動物、家畜、寵物等,支持喂養、騎乘、馴養等功能;建筑與建造:玩家可以建造、升級、摧毀建筑;采集與合成:支持采集資源、合成物品、制作工具;天氣與時間系統:有晝夜變化、天氣系統,影響作物和動物。
種植系統
既然都當賽博農民了,那我們當然得首先考慮如何實現種植系統,種地當然首先得有plant類和soil類,也就是種植物和土壤。
Plant.cs
public class Plant : Craftable
{[Header("Plant")]public PlantData data; // 植物配置數據public int growth_stage = 0;
首先Plant繼承自Craftable類(可制作物品的基類),包含了專門用于存儲種植類數據的PlantData以及表明種植物生長階段的int類型。
[Header("Time")]
public TimeType time_type = TimeType.GameDays; // 時間類型(天/小時)
public float grow_time = 8f; // 生長所需時間
public bool grow_require_water = true; // 是否需要水才能生長
public bool regrow_on_death; // 死亡后是否重新生長
public float soil_range = 1f; [Header("Harvest")]
public ItemData fruit; // 果實數據
public float fruit_grow_time = 0f; // 果實生長時間
public bool fruit_require_water = true;// 是否需要水才能結果
public Transform fruit_model; // 果實模型
public bool death_on_harvest;
這是一些種植物可能會涉及到的屬性,其中有兩個自定義的類TimeType和ItemData:ItemData是一個物品數據類,繼承自CraftData,用于定義游戲中物品的各種屬性和行為;TimeType是一個枚舉類型,用于定義游戲中時間的計算方式。
private void SlowUpdate()
{// 檢查植物生長if (!IsFullyGrown() && HasUID()){bool can_grow = !grow_require_water || HasWater();if (can_grow && GrowTimeFinished()){GrowPlant();return;}}// 檢查果實生長if (!has_fruit && fruit != null && HasUID()){bool can_grow = !fruit_require_water || HasWater();if (can_grow && FruitGrowTimeFinished()){GrowFruit();return;}}
}
這是我們用于檢測是否滿足生長條件的,如果都滿足我們就執行Grow相關函數。可以看到我們需求的條件有水分、時間和HasUID:一個用于檢查對象是否具有唯一標識符(Unique?ID)的方法。
public void Water()
{if (!HasWater()){if (soil != null)soil.Water();PlayerData.Get().SetCustomInt(GetSubUID("water"), 1);ResetGrowTime();}
}public bool HasWater()
{bool wplant = PlayerData.Get().GetCustomInt(GetSubUID("water")) > 0;bool wsoil = soil != null ? soil.IsWatered() : false;return wplant || wsoil;
}
這個是澆水相關的代碼,如果種植物還沒有被澆水且土壤的實例存在,我們就對土壤實施澆水函數且在玩家的數據中記錄植物被澆水,最后重置生長時間;判斷植物是否被澆水也是去玩家存儲的數據里讀取,只要符合植物沒有澆水或者土壤沒有被澆水就可以返回true——這里的土壤也有一個判斷是否澆水的原因是因為游戲內部允許我們先給土壤澆水之后再種植也可以種植之后再澆水。
public void Harvest(PlayerCharacter character)
{if (fruit != null && has_fruit && character.Inventory.CanTakeItem(fruit, 1)){GameObject source = fruit_model != null ? fruit_model.gameObject : gameObject;character.Inventory.GainItem(fruit, 1, source.transform.position);RemoveFruit();if (death_on_harvest && destruct != null)destruct.Kill();TheAudio.Get().PlaySFX("plant", gather_audio);if (gather_fx != null)Instantiate(gather_fx, transform.position, Quaternion.identity);}
}
這個是收獲相關的代碼,當玩家嘗試收獲植物時,系統首先會檢查三個條件:植物是否有果實數據、植物是否已經結果、以及玩家的背包是否有足夠空間。如果這些條件都滿足,系統會從植物上獲取果實(優先使用專門的果實模型,如果沒有則使用植物本身作為模型),并將果實添加到玩家的背包中。收獲后,系統會更新植物的狀態(移除果實),如果植物被設置為收獲后死亡,則會銷毀該植物。
public SowedPlantData SaveData
{get { return PlayerData.Get().GetSowedPlant(GetUID()); }
}
存檔系統,把種植物的數據存在PlayerData里就好。
public static Plant Create(PlantData data, Vector3 pos, int stage)
{Plant plant = CreateBuildMode(data, pos, stage);plant.buildable.FinishBuild();return plant;
}
Plant的創造方法。
總的來說,Plant類是一個用于管理游戲中植物生長和收獲的核心類。它繼承自Craftable類,包含了植物的基本屬性(如生長階段、生長時間、水分需求等)和核心功能(如生長、澆水、收獲等)。植物系統支持多個生長階段,每個階段都有特定的時間要求,并且可以通過澆水來促進生長。植物可以產出果實,玩家可以收獲這些果實,收獲后植物可能會死亡(取決于設置)。系統還包含了完整的數據保存和加載功能,確保植物的狀態在游戲存檔中保持。
Soil.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 可以澆水的土壤/// </summary>[RequireComponent(typeof(UniqueID))]public class Soil : MonoBehaviour{public MeshRenderer mesh; // 土壤的網格渲染器public Material watered_mat; // 澆水后的材質private UniqueID unique_id; // 唯一標識組件private Material original_mat; // 原始材質private bool watered = false; // 是否已澆水private float update_timer = 0f; // 更新計時器private static List<Soil> soil_list = new List<Soil>(); // 土壤列表void Awake(){soil_list.Add(this);unique_id = GetComponent<UniqueID>(); // 獲取唯一標識組件if(mesh != null)original_mat = mesh.material; // 獲取原始材質}private void OnDestroy(){soil_list.Remove(this);}private void Update(){bool now_watered = IsWatered(); // 當前是否已澆水// 如果狀態改變且有網格渲染器和澆水后的材質if (now_watered != watered && mesh != null && watered_mat != null){mesh.material = now_watered ? watered_mat : original_mat; // 切換材質}watered = now_watered; // 更新澆水狀態update_timer += Time.deltaTime;if (update_timer > 0.5f){update_timer = 0f;SlowUpdate(); // 慢更新}}private void SlowUpdate(){// 自動澆水if (!watered){if (TheGame.Get().IsWeather(WeatherEffect.Rain)) // 如果是下雨天Water(); // 澆水Sprinkler nearest = Sprinkler.GetNearestInRange(transform.position); // 獲取最近的灑水器if (nearest != null)Water(); // 澆水}}// 澆水public void Water(){PlayerData.Get().SetCustomInt(GetSubUID("water"), 1); // 設置玩家數據,標記為澆水狀態}// 移除水public void RemoveWater(){PlayerData.Get().SetCustomInt(GetSubUID("water"), 0); // 設置玩家數據,移除澆水狀態}// 澆水植物public void WaterPlant(){Plant plant = Plant.GetNearest(transform.position, 1f); // 獲取最近的植物if(plant != null)plant.Water(); // 植物澆水}// 判斷是否已澆水public bool IsWatered(){return PlayerData.Get().GetCustomInt(GetSubUID("water")) > 0; // 獲取玩家數據,判斷是否澆水}// 獲取子UIDpublic string GetSubUID(string tag){return unique_id.GetSubUID(tag); // 獲取唯一標識的子標識}// 獲取最近的土壤public static Soil GetNearest(Vector3 pos, float range=999f){float min_dist = range;Soil nearest = null;foreach (Soil soil in soil_list){float dist = (pos - soil.transform.position).magnitude;if (dist < min_dist){min_dist = dist;nearest = soil;}}return nearest;}// 獲取所有土壤public static List<Soil> GetAll(){return soil_list; // 返回所有土壤列表}}
}
Soil類是游戲中土壤系統的核心實現,它繼承自MonoBehaviour并需要UniqueID組件。這個類主要負責管理土壤的澆水狀態和視覺效果。每個土壤對象都包含一個網格渲染器(mesh)和兩種材質(原始材質和澆水后的材質),用于顯示土壤的干濕狀態。土壤系統維護了一個靜態的土壤列表,用于全局管理所有土壤對象。土壤可以通過多種方式被澆水:玩家手動澆水、下雨天氣自動澆水、或者通過灑水器自動澆水。當土壤被澆水時,它的外觀會改變(切換到澆水后的材質),并且這個狀態會被保存在玩家數據中。土壤還可以自動檢測附近的植物并為其澆水,實現了土壤和植物之間的互動。系統還提供了獲取最近土壤和所有土壤的靜態方法,方便其他系統與土壤進行交互。
ActionHoe.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 耕地,以便種植植物/// </summary>[CreateAssetMenu(fileName = "Action", menuName = "FarmingEngine/Actions/Hoe", order = 50)]public class ActionHoe : SAction // 繼承自可序列化動作基類{public float hoe_range = 1f; // 耕地操作的有效范圍(單位:米)// 執行耕地操作public override void DoAction(PlayerCharacter character, ItemSlot slot){// 計算耕地位置:角色位置 + 朝向方向 * 范圍Vector3 pos = character.transform.position + character.GetFacing() * hoe_range;// 獲取角色的耕地功能組件PlayerCharacterHoe hoe = character.GetComponent<PlayerCharacterHoe>();// 若存在組件則調用耕地方法(安全調用避免空引用)hoe?.HoeGround(pos);// 獲取當前裝備槽的物品數據InventoryItemData ivdata = character.EquipData.GetInventoryItem(slot.index);// 減少裝備耐久度(若物品存在)if (ivdata != null)ivdata.durability -= 1;}// 驗證是否可執行耕地操作public override bool CanDoAction(PlayerCharacter character, ItemSlot slot){// 僅當物品位于裝備槽時才允許耕地(確保手持鋤頭)return slot is EquipSlotUI;}}
}
實現了一個耕地動作系統,當玩家在農場游戲中執行耕地操作時:
- ?動態計算耕地位置?:基于玩家角色的當前朝向和預設范圍(
hoe_range
),精準定位前方土壤的交互點。 - ?調用耕地邏輯?:通過?
PlayerCharacterHoe
?組件執行具體的耕地行為(如土壤狀態切換、視覺效果觸發),實現邏輯與動作解耦。 - ?工具耐久損耗?:每次耕地會減少手持農具(如鋤頭)的耐久度,體現工具的消耗屬性。
- ?操作條件驗證?:限制僅當農具被裝備在手中?(而非在背包)時才能耕地,防止空手操作
ActionHarvest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 收獲植物的果實/// </summary>[CreateAssetMenu(fileName = "Action", menuName = "FarmingEngine/Actions/Harvest", order = 50)]public class ActionHarvest : AAction{public float energy = 1f; // 操作消耗的能量// 執行操作public override void DoAction(PlayerCharacter character, Selectable select){Plant plant = select.GetComponent<Plant>(); // 獲取選擇對象的植物組件if (plant != null){string animation = character.Animation ? character.Animation.take_anim : ""; // 獲取角色的收獲動畫character.TriggerAnim(animation, plant.transform.position); // 觸發角色的收獲動畫character.TriggerBusy(0.5f, () =>{character.Attributes.AddAttribute(AttributeType.Energy, -energy); // 扣除角色能量plant.Harvest(character); // 收獲植物的果實});}}// 判斷是否可以執行操作的條件方法public override bool CanDoAction(PlayerCharacter character, Selectable select){Plant plant = select.GetComponent<Plant>(); // 獲取選擇對象的植物組件if (plant != null){return plant.HasFruit() && character.Attributes.GetAttributeValue(AttributeType.Energy) >= energy; // 如果植物有果實并且角色能量足夠,返回 true}return false; // 否則返回 false}}}
?實現了一個植物收獲動作系統,允許玩家在農場游戲中執行收獲操作。其核心邏輯包括:
- ?執行收獲動作?(
DoAction
):播放動畫、扣除體力、觸發植物的收獲邏輯。 - ?驗證操作條件?(
CanDoAction
):檢查目標是否為植物、植物是否有果實、玩家體力是否充足。
在這里我想要補充一下關于ScriptObject的內容:
可以看到這個項目中的Action類都被設計為基于ScriptableObject :動作被設計為可配置的資產,而不是場景中的組件,這使得它們可以被重復使用和輕松配置。
為什么是ScriptObject呢?特殊在哪里?
ScriptableObject是Unity中的一種特殊資源類型,它是一種不需要附加到游戲對象(GameObject)上就能存在的數據容器。
它的特殊之處主要體現在以下幾點:
1. 獨立于場景 :ScriptableObject不依賴于場景中的游戲對象,可以在多個場景之間共享數據,非常適合存儲游戲配置、角色數據、道具信息等需要跨場景使用的內容。
2. 節省內存 :當多個對象引用同一個ScriptableObject實例時,它們共享的是同一份數據,而不是各自復制一份,這可以顯著減少內存占用。
3. 易于編輯 :在Unity編輯器中,ScriptableObject可以像其他資源一樣被創建和編輯,開發者可以直接在Inspector面板中修改其屬性值。
4. 序列化支持 :ScriptableObject可以被序列化,這意味著它的數據可以被保存到磁盤上,并且在游戲運行時被加載。
5. 無需實例化 :與MonoBehaviour不同,ScriptableObject不需要被實例化到場景中,只需要在項目中創建它的實例,然后就可以在代碼中引用它。
6. 適合數據驅動 :ScriptableObject非常適合實現數據驅動的游戲設計,開發者可以將游戲中的各種數據(如角色屬性、武器參數等)存儲在ScriptableObject中,然后在代碼中根據這些數據來驅動游戲邏輯。
在我們之前查看的代碼中, Action 類(如 SAction 、 AAction 等)就是基于ScriptableObject實現的,這使得這些動作可以被配置和復用,非常靈活。
Conclusion
整個種植系統的流程實現如下:
準備階段:創建耕地(ActionHoe.cs)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{[CreateAssetMenu(fileName = "Action", menuName = "FarmingEngine/Actions/Hoe", order = 50)]public class ActionHoe : SAction{public float hoe_range = 1f;public override void DoAction(PlayerCharacter character, ItemSlot slot){Vector3 pos = character.transform.position + character.GetFacing() * hoe_range;PlayerCharacterHoe hoe = character.GetComponent<PlayerCharacterHoe>();hoe?.HoeGround(pos);InventoryItemData ivdata = character.EquipData.GetInventoryItem(slot.index);if (ivdata != null)ivdata.durability -= 1;}public override bool CanDoAction(PlayerCharacter character, ItemSlot slot){return slot is EquipSlotUI;}}
}
?當玩家角色執行鋤地動作時,會在角色前方指定范圍內(hoe_range
)的位置進行鋤地操作(調用PlayerCharacterHoe.HoeGround()
方法);同時減少該裝備(如鋤頭)的耐久度(durability
),且要求裝備必須放置在有效的裝備槽中(EquipSlotUI
)。
種植階段:創建植物(Plant.cs)
// 植物的創建方法
public static Plant Create(PlantData data, Vector3 pos, Quaternion rot, int stage)
{Plant plant = CreateBuildMode(data, pos, stage); // 創建基礎植物對象plant.transform.rotation = rot; // 設置初始旋轉角度plant.buildable.FinishBuild(); // 標記建造完成return plant;
}// 初始化植物
protected override void Awake()
{base.Awake();plant_list.Add(this); // 注冊到全局植物列表selectable = GetComponent<Selectable>(); // 獲取可選組件buildable = GetComponent<Buildable>(); // 獲取可建造組件destruct = GetComponent<Destructible>(); // 獲取可銷毀組件unique_id = GetComponent<UniqueID>(); // 獲取唯一標識組件// 訂閱事件selectable.onDestroy += OnDeath; // 綁定銷毀回調buildable.onBuild += OnBuild; // 綁定建造回調// 初始化生長階段if(data != null)nb_stages = Mathf.Max(data.growth_stage_prefabs.Length, 1);
}
實現了植物的動態創建與生命周期管理,通過工廠方法實例化植物,配置位置/旋轉并完成建造;注冊全局管理、綁定組件事件、初始化生長參數。
生長階段:植物生長邏輯(Plant.cs)
// 生長條件檢測(周期性慢更新,避免每幀檢測)
private void SlowUpdate()
{// 僅當植物未完全成熟且具有唯一標識時執行生長檢測if (!IsFullyGrown() && HasUID()){// 檢查生長條件:若無需水源(grow_require_water=false)或已有水源bool can_grow = !grow_require_water || HasWater();// 同時滿足基礎條件和生長時間結束時觸發生長if (can_grow && GrowTimeFinished()){GrowPlant(); // 執行生長邏輯return; // 跳過后續檢測}}// ...(可能包含其他狀態檢測)
}// 植物生長階段切換方法
public void GrowPlant(int grow_stage)
{// 驗證生長階段有效性(在數據存在且階段合法時)if (data != null && grow_stage >= 0 && grow_stage < nb_stages){// 從玩家數據中獲取當前植物的存檔記錄SowedPlantData sdata = PlayerData.Get().GetSowedPlant(GetUID());// 若存檔不存在(新種植或數據丟失)if (sdata == null){// 非初始生成的植物需清理無效數據if (!was_spawned)PlayerData.Get().RemoveObject(GetUID());// 創建新植物存檔:記錄種類ID、場景、位置、旋轉和生長階段[7](@ref)sdata = PlayerData.Get().AddPlant(data.id, SceneNav.GetCurrentScene(), transform.position, transform.rotation, grow_stage);}else{// 更新已有植物的生長階段[7](@ref)PlayerData.Get().GrowPlant(GetUID(), grow_stage);}// 重置生長計時器(為下一階段準備)ResetGrowTime();// 消耗水資源(模擬生長所需養分)RemoveWater();// 從全局植物列表中移除當前對象plant_list.Remove(this);// 生成新階段的植物預制件(基于存檔UID)[6](@ref)Spawn(sdata.uid);// 銷毀當前階段的植物對象(完成狀態切換)Destroy(gameObject);}
}
實現了游戲植物生長狀態的管理邏輯,具體分為兩部分:
生長條件檢查(SlowUpdate
):定期檢測植物是否未完全成熟(!IsFullyGrown()
)且存在唯一標識(HasUID()
)。若滿足生長條件(無需水源或已有水源?can_grow
)且生長時間結束(GrowTimeFinished()
),則調用?GrowPlant()
?進入下一生長階段。
生長執行邏輯(GrowPlant
):
根據目標生長階段?grow_stage
?更新植物數據:
- 通過?
PlayerData
?獲取或創建植物存檔數據(SowedPlantData
); - 重置生長計時器(
ResetGrowTime()
)、移除消耗的水資源(RemoveWater()
); - 銷毀當前植物對象,生成新階段的植物(
Spawn(sdata.uid)
?和?Destroy(gameObject)
),實現生長狀態切換
結果階段:果實生長邏輯(Plant.cs)
// 果實生長條件檢測(周期性慢更新,避免每幀檢測)
private void SlowUpdate()
{// ...// 若當前無果實、果實配置有效且植物有唯一標識時執行檢測if (!has_fruit && fruit != null && HasUID()){// 生長條件:無需水源(fruit_require_water=false)或已有水源bool can_grow = !fruit_require_water || HasWater();// 同時滿足基礎條件且果實生長時間結束時觸發生長if (can_grow && FruitGrowTimeFinished()){GrowFruit(); // 執行果實生長邏輯return; // 跳過后續檢測}}// ...
}// 果實生長執行方法
public void GrowFruit()
{// 驗證果實配置存在且當前未生成果實if (fruit != null && !has_fruit){has_fruit = true; // 標記果實已生成// 在玩家數據中記錄果實狀態(1表示已生成)PlayerData.Get().SetCustomInt(GetSubUID("fruit"), 1);RemoveWater(); // 消耗生長所需的水資源RefreshFruitModel(); // 刷新模型以顯示果實}
}
生長條件檢測(SlowUpdate
)??
通過周期性檢測(非每幀執行)判斷果實能否生長:需滿足無現存果實、配置有效且具備唯一標識的前提,再驗證水源條件?(若要求)和生長時間是否結束,全部滿足則調用?GrowFruit()。
果實生長執行(GrowFruit
)??
當條件滿足時:
- ?更新狀態?:標記?
has_fruit=true
?表示果實已生成。 - ?數據持久化?:通過?
SetCustomInt
?將果實狀態保存至玩家存檔(鍵名為子UID "fruit")。 - ?資源消耗?:調用?
RemoveWater()
?移除生長消耗的水資源。 - ?視覺更新?:刷新模型顯示果實(如切換預制件或激活子對象)
收獲階段:獲取果實(ActionHarvest.cs&&Plant.cs)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{// 創建該類的Unity資源菜單項(路徑:Create > FarmingEngine > Actions > Harvest)[CreateAssetMenu(fileName = "Action", menuName = "FarmingEngine/Actions/Harvest", order = 50)]public class ActionHarvest : AAction // 繼承抽象動作基類{public float energy = 1f; // 收獲動作消耗的體力值// 執行收獲動作public override void DoAction(PlayerCharacter character, Selectable select){// 獲取目標物體的Plant組件Plant plant = select.GetComponent<Plant>();if (plant != null) // 確認目標為植物{// 獲取收獲動畫名稱(若有配置,否則為空)string animation = character.Animation ? character.Animation.take_anim : "";// 在植物位置觸發動畫character.TriggerAnim(animation, plant.transform.position);// 延遲0.5秒執行收獲(模擬操作耗時)character.TriggerBusy(0.5f, () =>{// 扣除玩家體力character.Attributes.AddAttribute(AttributeType.Energy, -energy);// 調用植物的收獲方法plant.Harvest(character);});}}// 驗證是否可執行收獲public override bool CanDoAction(PlayerCharacter character, Selectable select){// 獲取目標物體的Plant組件Plant plant = select.GetComponent<Plant>();if (plant != null){// 雙重驗證:植物有果實 + 玩家體力充足return plant.HasFruit() && character.Attributes.GetAttributeValue(AttributeType.Energy) >= energy;}return false; // 非植物對象不可操作}}
}
?實現了一個植物收獲動作,包含執行邏輯(DoAction
)和條件驗證(CanDoAction
)。玩家需滿足體力條件且植物有果實才能觸發收獲,執行時會播放動畫、扣除體力并調用植物的收獲邏輯。
// 執行植物收獲操作
public void Harvest(PlayerCharacter character)
{// 驗證:存在果實配置、當前有果實、玩家背包可容納該物品if (fruit != null && has_fruit && character.Inventory.CanTakeItem(fruit, 1)){// 確定物品生成位置(優先使用果實模型位置)GameObject source = fruit_model != null ? fruit_model.gameObject : gameObject;// 玩家背包獲取果實(數量為1,在指定位置生成)character.Inventory.GainItem(fruit, 1, source.transform.position);RemoveFruit(); // 移除果實狀態(內部會設置has_fruit=false)// 若收獲后植物死亡且存在可銷毀組件,觸發植物死亡if (death_on_harvest && destruct != null)destruct.Kill();// 播放收獲音效(如采摘聲)TheAudio.Get().PlaySFX("plant", gather_audio);// 生成收獲特效(如粒子光芒)if (gather_fx != null)Instantiate(gather_fx, transform.position, Quaternion.identity);}
}
?實現了玩家在Unity農場游戲中收獲植物的核心流程:當玩家執行收獲動作時,系統會先驗證目標植物是否存在可收獲的果實(fruit != null && has_fruit
)且玩家背包有空間容納(character.Inventory.CanTakeItem
),通過后則根據植物的果實模型位置動態生成掉落物(優先使用fruit_model
位置,否則用植物根部),并調用GainItem
將果實加入玩家背包;緊接著重置植物狀態(RemoveFruit()
內部會設置has_fruit=false
,可能觸發重新生長計時),并根據配置項death_on_harvest
決定是否銷毀植物(例如收割小麥后植物消失,而果樹則保留枝干繼續生長);最后通過音效系統播放采摘聲(gather_audio
)并在植物位置生成粒子特效(如光芒閃爍的gather_fx
)。
工作流如下:
?- 玩家裝備鋤頭,使用 ActionHoe 在地面創建 Soil 對象
- 玩家種植種子,調用 Plant.Create() 方法創建植物實例
- 植物通過 SlowUpdate() 方法每0.5秒檢查生長條件
- 當滿足時間( GrowTimeFinished() )和水分( HasWater() )條件時,植物調用 GrowPlant() 方法生長到下一階段
- 植物完全成熟后,滿足果實生長條件時,調用 GrowFruit() 方法結出果實
- 玩家使用 ActionHarvest 動作收獲果實,調用 Plant.Harvest() 方法
- 收獲后,植物根據 death_on_harvest 設置決定是死亡還是繼續生長
- 整個過程中的關鍵數據(如生長階段、果實狀態、澆水狀態)通過 PlayerData 進行持久化存儲
動物系統
農場中一般有三種動物:
- 家畜動物 ( AnimalLivestock ):可以進食、產生產品和成長
- 寵物 ( Pet ):可以跟隨玩家、攻擊敵人和挖掘
- 野生動物 ( AnimalWild ):可以游蕩、逃跑或攻擊玩家
每個類都有各自獨特的行為和交互方式,但它們共享一些共同的設計模式和機制。
家畜動物 ( AnimalLivestock )
核心機制 :
- 進食:食用特定組的食物,滿足條件后產生產品或成長
- 產品生產:根據進食次數和時間周期產生產品,可以自動掉落或需要手動收集
- 成長:當進食次數和時間達到要求后,會成長為新的角色類型
- 數據持久化:存儲上次進食時間、成長時間和進食次數
接下來我們來看看源碼是怎么實現的。
核心狀態管理模塊
public enum LivestockState {Wander, // 游蕩:隨機移動FindFood, // 覓食:尋找食物Eat, // 進食:消耗食物Dead // 死亡:停止所有行為
}
private void ChangeState(LivestockState newState) {state = newState;state_timer = 0f; // 重置狀態計時器
}
?通過LivestockState
狀態機控制動物行為(游蕩、覓食、進食、死亡),確保行為邏輯清晰分離。
生存需求模塊
private void SlowUpdate() {if (IsHungry()) target_food = FindFood(); // 饑餓時尋找食物if (CanGrow()) Grow(); // 滿足條件時成長if (CanProduce()) ProduceItem(); // 滿足條件時產出物品
}
模擬動物的饑餓、成長與產出機制,驅動核心游戲循環。
- 饑餓系統?:
IsHungry()
判斷是否需進食(基于eat_interval_time
時間間隔)- 重置計時器
ResetEatTime()
并累加進食次數GetEatCount()
- ?成長系統?:
- 達到進食次數(
grow_eat_count
)且時間滿足(GrowTimeFinished()
)時觸發Grow()
- 替換動物模型為
grow_to
,實現幼崽到成體的演變
- 達到進食次數(
- ?產出系統?:
- 進食達標后調用
ProduceItem()
生成物品(蛋、毛等) - 支持兩種收集方式:直接掉落(
DropFloor
)或手動收集(CollectAction
)
- 進食達標后調用
移動與導航模塊
private void FindWanderTarget() {Vector3 center = wander == WanderNear ? start_pos : transform.position;wander_target = center + Random.insideUnitSphere * wander_range; // 隨機目標點
}
?控制動物移動邏輯,優化性能與行為真實性。
- 游蕩機制?:
WanderBehavior
定義移動模式(原地徘徊/大范圍探索),通過FindWanderTarget()
計算隨機目標點 - ?動態激活?:根據玩家距離動態啟用/禁用AI(
is_active
),減少遠處動物的CPU開銷 - ?防卡死處理?:
IsStuck()
檢測移動異常,自動停止或重新尋路 - ?速度控制?:
is_running
標志區分行走與奔跑(玩家驅趕時觸發)
數據持久化模塊
void Start() {// 從存檔加載數據last_eat_time = PlayerData.Get().GetCustomFloat(GetSubUID("eat_time")); eat_count = PlayerData.Get().GetCustomInt(GetSubUID("eat"));
}
通過PlayerData
保存動物個體狀態,支持存檔/讀檔。
- 時間標記?:
last_eat_time
/last_grow_time
記錄關鍵事件的發生時刻 - ?計數統計?:
GetEatCount()
存儲進食次數,GetProductCount()
存儲待收集物品數 - ?唯一標識?:
GetUID()
為每個動物分配唯一ID,確保數據準確關聯
外部交互接口模塊
public void CollectProduct(PlayerCharacter player) {if (HasProduct()) {player.Inventory.GainItem(item_produce, 1); // 物品進入背包DecreaseProductCount(); // 減少待收集數量}
}
提供玩家與動物交互的API?。
CollectProduct()
:玩家手動收集產出物(如雞蛋)MoveToTarget()
:驅趕動物至指定位置(如趕回畜棚)StopAction()
:強制中斷當前行為(如停止覓食)
寵物 ( Pet )
寵物則是功能更簡單的家畜動物,不負責進食和產出而是更簡單地跟隨玩家。
狀態機控制模塊
public enum PetState
{ Idle = 0, // 空閑:隨機游蕩或待命 Follow = 2, // 跟隨:追蹤主人位置 Attack = 5, // 攻擊:鎖定敵人并攻擊 Dig = 8, // 挖掘:執行挖掘動作 Pet = 10, // 互動:響應撫摸等交互 MoveTo = 15, // 移動:向指定位置行進 Dead = 20 // 死亡:停止所有行為
}
...
...
private PetState _currentState; // 當前狀態私有變量 public void ChangeState(PetState newState)
{ // 退出當前狀態的清理邏輯(可選) if (_currentState == PetState.Attack) { _attackTarget = null; // 清除攻擊目標緩存 } // 更新狀態 _currentState = newState; _stateTimer = 0f; // 重置狀態持續計時器 // 進入新狀態的初始化邏輯(可選) if (newState == PetState.Follow) { _animator.SetBool("Follow", true); // 觸發跟隨動畫 }
}
首先定義了寵物的7種行為狀態,每個狀態對應特定行為,然后實現狀態安全切換,也是基于狀態機設計進行邏輯驅動。
跟隨控制模塊
if (state == PetState.Follow && PlayerIsFar(follow_range)) {character.Follow(GetMaster().gameObject); // 動態追蹤主人
}
?實現寵物動態跟隨主人,優化移動邏輯與性能,PlayerIsFar()
判斷主人距離,觸發跟隨或停止,
- 游蕩模式:隨機生成目標點(
FindWanderTarget()
),使用character.MoveTo()
移動。 - 跟隨模式:通過
character.Follow()
持續追蹤主人位置 character.IsStuck()
檢測移動異常,自動停止
戰斗與挖掘模塊
支持寵物攻擊敵人和挖掘物品的能力
馴服與歸屬模塊
動畫與事件模塊
數據持久化模塊
野生動物 ( AnimalWild )
野生動物本質上就是傳統的敵人設計,只不過在這個項目中被包裝成了動物。
核心枚舉定義
// 動物的狀態
public enum AnimalState
{Wander = 0, // 游蕩Alerted = 2, // 警戒Escape = 4, // 逃跑Attack = 6, // 攻擊MoveTo = 10, // 移動到指定位置Dead = 20, // 死亡
}// 動物的行為
public enum AnimalBehavior
{None = 0, // 無行為,由其他腳本定義Escape = 5, // 看到就逃跑PassiveEscape = 10, // 被攻擊時逃跑PassiveDefense = 15,// 被攻擊時反擊Aggressive = 20, // 看到就攻擊,一段時間后返回VeryAggressive = 25,// 看到就攻擊,一直追擊
}// 游蕩行為
public enum WanderBehavior
{None = 0, // 不游蕩WanderNear = 10,// 在附近游蕩WanderFar = 20, // 超出初始位置游蕩
}
一系列枚舉定義,分別定義了野生動物的狀態、行為和游蕩行為。
狀態機實現
public void ChangeState(AnimalState state)
{this.state = state;state_timer = 0f;lure_interest = 8f;
}private void Update()
{// 動畫bool paused = TheGame.Get().IsPaused();if (animator != null)animator.enabled = !paused;if (TheGame.Get().IsPaused())return;if (state == AnimalState.Dead || behavior == AnimalBehavior.None || !is_active)return;state_timer += Time.deltaTime;if (state != AnimalState.MoveTo)is_running = (state == AnimalState.Escape || state == AnimalState.Attack);character.move_speed = is_running ? run_speed : wander_speed;// 狀態處理if (state == AnimalState.Wander){// 游蕩狀態邏輯...}if (state == AnimalState.Alerted){// 警戒狀態邏輯...}if (state == AnimalState.Escape){// 逃跑狀態邏輯...}if (state == AnimalState.Attack){// 攻擊狀態邏輯...}if (state == AnimalState.MoveTo){// 移動到指定位置狀態邏輯...}// 其他邏輯...
}
感覺沒什么好說的,就是非常常規的FSM。?
感知和反應機制
// 檢測玩家是否在視野范圍內
private void DetectThreat(float range)
{Vector3 pos = transform.position;// 檢測玩家float min_dist = range;foreach (PlayerCharacter player in PlayerCharacter.GetAll()){Vector3 char_dir = (player.transform.position - pos);float dist = char_dir.magnitude;if (dist < min_dist && !player.IsDead()){float dangle = detect_angle / 2f; // /2 每側角度float angle = Vector3.Angle(transform.forward, char_dir.normalized);if (angle < dangle || char_dir.magnitude < detect_360_range){player_target = player;attack_target = null;min_dist = dist;}}}// 檢測其他角色/可摧毀物體foreach (Selectable selectable in Selectable.GetAllActive()){// 檢測邏輯...}
}
實現了一個AI角色的視野檢測功能,主要用于判斷玩家或其他可交互目標是否進入AI的視野范圍:
- 距離篩選?:遍歷所有玩家,只處理距離小于?
min_dist
(初始為?range
)且存活的玩家。 - ?角度判斷?:
Vector3.Angle()
?計算目標方向與AI正前方夾角。angle < dangle
:目標在設定的扇形視野內(如120°視野則?dangle=60°
)。
- ?全向近距離檢測?:
dist < detect_360_range
?時無視角度限制(如距離<3米時任何方向均可見),增強近戰邏輯合理性。 - ?目標鎖定?:滿足條件時更新?
player_target
?為當前玩家,并記錄其距離(后續遍歷中更遠玩家不再覆蓋)。
// 如果被動物看見玩家則進行反應
private void ReactToThreat()
{GameObject target = GetTarget();if (target == null || IsDead())return;if (HasEscapeBehavior()){ChangeState(AnimalState.Escape);character.Escape(target);}else if (HasAttackBehavior()){ChangeState(AnimalState.Attack);if (player_target)character.Attack(player_target);else if (attack_target)character.Attack(attack_target);}
}private void OnDamagedPlayer(PlayerCharacter player)
{if (IsDead() || state_timer < 2f)return;player_target = player;attack_target = null;ReactToThreat();
}private void OnDamagedBy(Destructible attacker)
{if (IsDead() || state_timer < 2f)return;player_target = null;attack_target = attacker;ReactToThreat();
}
?這個是動物實例檢測到玩家后的行為,如果有逃離狀態優先逃離,否則進行攻擊,設定檢測到的玩家為攻擊目標,還加入了2s的狀態切換冷卻時間。
優化機制
void FixedUpdate()
{if (TheGame.Get().IsPaused())return;// 優化,如果距離太遠則不運行float dist = (TheCamera.Get().GetTargetPos() - transform.position).magnitude;float active_range = Mathf.Max(detect_range * 2f, selectable.active_range * 0.8f);is_active = (state != AnimalState.Wander && state != AnimalState.Dead) || character.IsMoving() || dist < active_range;
}private void Update()
{// ...update_timer += Time.deltaTime;if (update_timer > 0.5f){update_timer = Random.Range(-0.1f, 0.1f);SlowUpdate(); // 優化}// ...
}private void SlowUpdate()
{// 非頻繁更新的邏輯...
}
首先在FixedUpdate
中基于距離和狀態動態設定對象的激活狀態(is_active
):當對象處于非活躍狀態(如“閑逛”或“死亡”)且遠離攝像機時,暫停其邏輯更新;反之,若對象正在移動、處于關鍵行為狀態(如攻擊或逃跑),或進入攝像機動態計算的激活范圍(active_range
,該范圍綜合了檢測半徑和交互閾值),則激活更新流程。
其次,通過Update
中的低頻定時器?(update_timer
)將非緊急邏輯(如環境感知或目標決策)從每幀調用降為半秒觸發,并引入隨機時間偏移(Random.Range(-0.1f, 0.1f)
),避免大量對象在同一幀同步執行SlowUpdate
導致的CPU峰值。這種策略既維持了AI響應的實時性,又將高頻計算轉化為可控的中低頻任務,結合動態激活篩選,形成雙重性能屏障
建筑與建造
建筑和建造是兩個相似但不同的概念:建筑主要指新建場景中的建筑(怎么像廢話),而建造則是指新建所有可以使用的物品,包括建筑。
建筑
整個建筑的流程實現是這樣的:玩家選擇建筑→ Buildable 實時驗證位置→確認后調用 Construction.Create() 生成實體。
基本數據結構定義(ConstructionData.cs&&Construction.cs)
ConstructionData 定義了建筑的基本屬性(如預制體、耐久度等),而 Construction 實現了建筑的行為(如建造、摧毀等)。
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 建筑數據文件/// </summary>[CreateAssetMenu(fileName = "ConstructionData", menuName = "FarmingEngine/ConstructionData", order = 4)]public class ConstructionData : CraftData{[Header("--- ConstructionData ------------------")]public GameObject construction_prefab; // 構建建筑時生成的預制體[Header("引用數據")]public ItemData take_item_data; // 可獲取的物品數據[Header("屬性")]public DurabilityType durability_type; // 耐久類型public float durability; // 耐久度// 其他代碼...}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;namespace FarmingEngine
{/// <summary>/// 建筑是可以由玩家放置在地圖上的對象(通過制作或使用物品)/// </summary>[RequireComponent(typeof(Selectable))][RequireComponent(typeof(Buildable))][RequireComponent(typeof(UniqueID))]public class Construction : Craftable{[Header("Construction")]public ConstructionData data; // 建筑的數據[HideInInspector]public bool was_spawned = false; // 是否通過制作或加載生成// 當建造完成時調用private void OnBuild(){if (data != null){// 將建筑數據添加到玩家數據中BuiltConstructionData cdata = PlayerData.Get().AddConstruction(data.id, SceneNav.GetCurrentScene(), transform.position, transform.rotation, data.durability);unique_id.unique_id = cdata.uid; // 設置唯一標識符}}// 當建筑被摧毀時調用private void OnDeath(){// 從玩家數據中移除建筑數據PlayerData.Get().RemoveConstruction(GetUID());// 其他代碼...}// 其他代碼...}
}
?
位置驗證機制( Buildable.cs )
位置驗證是建筑系統的核心安全機制,通過多層檢測確保建筑放置的合法性:
// 檢查是否可以在當前位置建造
public bool CheckIfCanBuild()
{bool dont_overlap = !CheckIfOverlap(); // 無碰撞bool flat_ground = CheckIfFlatGround(); // 地面平坦bool accessible = CheckIfAccessible(); // 可訪問bool valid_ground = CheckValidFloor(); // 地面有效return dont_overlap && flat_ground && valid_ground && accessible;
}
這些是我們要檢測的內容,包括是否與其他物體有碰撞,地面是否平坦,是否可訪問,地面是否支持建筑。
public bool CheckIfOverlap()
{List<Collider> overlap_colliders = new List<Collider>();LayerMask olayer = obstacle_layer & ~floor_layer; // 排除地面圖層// 檢測邊界盒碰撞foreach (Collider collide in colliders){Collider[] over = Physics.OverlapBox(transform.position, collide.bounds.extents, Quaternion.identity, olayer);foreach (Collider overlap in over){if (!overlap.isTrigger) // 忽略觸發器overlap_colliders.Add(overlap);}}// 檢測半徑范圍內碰撞if (build_obstacle_radius > 0.01f){Collider[] over = Physics.OverlapSphere(transform.position, build_obstacle_radius, olayer);overlap_colliders.AddRange(over);}// 過濾有效碰撞foreach (Collider overlap in overlap_colliders){PlayerCharacter player = overlap.GetComponent<PlayerCharacter>();Buildable buildable = overlap.GetComponentInParent<Buildable>();if (player == null && buildable != this) // 排除玩家和自身return true; // 發現有效碰撞}return false;
}
...
...
public bool CheckValidFloor()
{Vector3 center = transform.position + Vector3.up * build_ground_dist;// 檢測中心點和四個方向點Vector3[] points = new Vector3[] { center, center + Vector3.right * build_obstacle_radius, center + Vector3.left * build_obstacle_radius, center + Vector3.forward * build_obstacle_radius, center + Vector3.back * build_obstacle_radius };foreach (Vector3 p in points){RaycastHit hit;bool has_hit = PhysicsTool.RaycastCollision(p, Vector3.down * (build_ground_dist + build_ground_dist), out hit);if (has_hit && PhysicsTool.IsLayerInLayerMask(hit.collider.gameObject.layer, floor_layer))return true; // 至少一個點在有效地面上}return false;
}
?這是具體的檢測邏輯內部實現,通過雙重檢測確保建筑可安全放置:首先在CheckIfOverlap()
中,通過邊界盒(Physics.OverlapBox
)和球形范圍(Physics.OverlapSphere
)檢測障礙物碰撞,利用圖層掩碼(obstacle_layer & ~floor_layer
)排除地面干擾,并過濾觸發器、玩家角色及自身建筑,只要存在其他有效障礙物即返回碰撞狀態(不可建造);其次在CheckValidFloor()
中,通過中心點及四方向偏移點向下發射射線(Physics.Raycast
),驗證至少有一個點命中指定地面圖層(floor_layer
),確保建筑底部有足夠支撐(可建造)。兩者結合形成完整建造校驗:僅當無碰撞障礙物且有地面支撐時,建筑才可放置。
玩家交互流程( PlayerCharacterCraft.cs )
玩家從選擇建筑到確認建造的完整交互鏈。
public void CraftConstructionBuildMode(ConstructionData item)
{CancelCrafting();// 創建建筑預覽體Construction construction = Construction.CreateBuildMode(item, transform.position + transform.forward * 1f);current_buildable = construction.GetBuildable();current_buildable.StartBuild(character); // 進入建造模式current_build_data = item;clicked_build = false;
}
創建建筑預覽體并進入建造模式,允許玩家調整建筑位置。
public void TryBuildAt(Vector3 pos)
{bool in_range = character.interact_type == PlayerInteractBehavior.MoveAndInteract || IsInBuildRange();if (!in_range) return;if (!clicked_build && current_buildable != null){current_buildable.SetBuildPositionTemporary(pos); // 臨時設置位置bool can_build = current_buildable.CheckIfCanBuild(); // 執行位置驗證if (can_build){current_buildable.SetBuildPosition(pos); // 確認位置clicked_build = true;character.MoveTo(pos); // 移動到建造位置}}
}
臨時設置建筑位置并驗證,可建造則確認位置并移動玩家。
public void CompleteBuilding(Vector3 pos)
{CraftData item = current_crafting;if (item != null && current_buildable != null && CanCraft(item, build_pay_slot, true)){current_buildable.SetBuildPositionTemporary(pos);if (current_buildable.CheckIfCanBuild()){PayCraftingCost(item, build_pay_slot, true); // 消耗材料current_buildable.FinishBuild(); // 完成建造// 數據記錄與反饋character.SaveData.AddCraftCount(item.id);character.Attributes.GainXP(item.craft_xp_type, item.craft_xp);TheAudio.Get().PlaySFX("craft", current_buildable.build_audio);// 清理狀態current_buildable = null;current_build_data = null;clicked_build = false;}}
}
?消耗材料、完成建造、記錄建筑數據到玩家數據并清理狀態。
實體創建與持久化( Construction.cs )
最終建筑實體的生成與狀態保存:
public static Construction Create(ConstructionData data, Vector3 pos, Quaternion rot)
{// 實例化預制體GameObject build = Instantiate(data.construction_prefab, pos, data.construction_prefab.transform.rotation);build.transform.rotation = rot; // 應用旋轉// 初始化組件Construction construct = build.GetComponent<Construction>();construct.data = data;construct.was_spawned = true;construct.buildable.FinishBuild(); // 觸發建造完成return construct;
}
實例化建筑預制體并初始化組件,設置建筑數據和唯一ID。
private void OnBuild()
{if (data != null){// 添加到玩家數據BuiltConstructionData cdata = PlayerData.Get().AddConstruction(data.id, SceneNav.GetCurrentScene(), transform.position, transform.rotation, data.durability);unique_id.unique_id = cdata.uid; // 關聯唯一ID}
}
?將建筑數據添加到玩家數據中,實現建筑狀態的保存。
建造
建造系統的核心數據結構是 CraftData 類,它是所有可制作物品的父類:
/// <summary>
/// 可制作物品(物品、建筑、植物)的父數據類
/// </summary>
public class CraftData : IdData
{[Header("顯示")]public string title; // 標題public Sprite icon; // 圖標public string desc; // 描述[Header("制作")]public bool craftable; // 可以制作嗎?public int craft_quantity = 1; // 制作數量public float craft_duration = 0f; // 制作所需時間[Header("制作成本")]public GroupData craft_near; // 制作時需要附近的物體組public ItemData[] craft_items; // 制作所需物品public GroupData[] craft_fillers; // 制作所需填充物public CraftData[] craft_requirements; // 制作前需要建造的物品// 其他代碼...
}
建造系統的工作流程是:檢查是否可以建造 -> 支付建造成本 -> 開始建造模式 -> 檢查建造位置 -> 完成建造。?
public bool CanCraft(CraftData item, bool skip_cost = false, bool skip_near = false)
{if (item == null || character.IsDead())return false;if (character.Attributes.GetAttributeValue(AttributeType.Energy) < craft_energy)return false; // 能量不足bool has_craft_cost = skip_cost || HasCraftCost(item);bool has_near = skip_near || HasCraftNear(item);return has_near && has_craft_cost;
}
檢測是否可以建造對應的物品。
public void PayCraftingCost(CraftData item, bool build = false)
{CraftCostData cost = item.GetCraftCost();foreach (KeyValuePair<ItemData, int> pair in cost.craft_items){character.Inventory.UseItem(pair.Key, pair.Value);}// 其他代碼...
}
?支付建造成本。
public void StartCraftingOrBuilding(CraftData data)
{if (CanCraft(data)){ConstructionData construct = data.GetConstruction();PlantData plant = data.GetPlant();if (construct != null)CraftConstructionBuildMode(construct);else if (plant != null)CraftPlantBuildMode(plant, 0);elseStartCrafting(data);TheAudio.Get().PlaySFX("craft", data.craft_sound);}
}
開始建造模式。
然后執行和前文中一樣的位置驗證和實體創建和持久化即可。
采集與合成
采集
采集系統是游戲中讓玩家能夠從植物或其他資源點獲取物品的核心系統。它允許玩家通過特定操作(如點擊植物)來收獲成熟的果實或其他資源,是游戲中資源獲取和循環的重要環節。
可采集的物品
在我們的項目中,有這些可以被采集的類:
挖掘點 (DigSpot)、物品提供者 (ItemProvider)、動物食物 (AnimalFood)、植物 (Plant)、地面上的物品 (Item)。
DigSpot.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 可以用鏟子挖掘的點,將從可銷毀對象中獲得戰利品/// </summary>[RequireComponent(typeof(Destructible))]public class DigSpot : MonoBehaviour{private Destructible destruct; // 可銷毀對象引用private static List<DigSpot> dig_list = new List<DigSpot>(); // 挖掘點列表void Awake(){dig_list.Add(this); // 添加到挖掘點列表destruct = GetComponent<Destructible>(); // 獲取可銷毀對象組件}public void Dig(){destruct.Kill(); // 挖掘操作,銷毀可銷毀對象}}
}
ItemProvider.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 定時生成物品,玩家可以拾取。例如鳥巢(生成鳥蛋)或釣魚點(生成魚類)等。/// </summary>[RequireComponent(typeof(Selectable))][RequireComponent(typeof(UniqueID))]public class ItemProvider : MonoBehaviour{[Header("物品生成")]public float item_spawn_time = 2f; // 游戲時間(小時)public int item_max = 3; // 最大物品數量public ItemData[] items; // 可生成的物品數據數組[Header("物品獲取")]public bool auto_take = true; // 是否允許角色通過點擊自動獲取物品}
}
AnimalFood.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{public class AnimalFood : MonoBehaviour{public GroupData food_group; // 食物所屬的組數據private Item item; // 物品組件private ItemStack stack; // 物品堆疊組件private Plant plant; // 植物組件public void EatFood(){if (item != null)item.EatItem(); // 如果存在Item組件,則吃掉物品if (stack != null)stack.RemoveItem(1); // 如果存在ItemStack組件,則移除一個物品if (plant != null)plant.KillNoLoot(); // 如果存在Plant組件,則摧毀植物但不掉落物品}}
}
?Plant.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;namespace FarmingEngine
{/// <summary>/// 植物可以播種(從種子開始),并且可以收獲它們的果實。它們還可以有多個生長階段。/// </summary>[RequireComponent(typeof(Selectable))][RequireComponent(typeof(Buildable))][RequireComponent(typeof(UniqueID))][RequireComponent(typeof(Destructible))]public class Plant : Craftable{[Header("Plant")]public PlantData data; // 植物數據public int growth_stage = 0; // 生長階段[Header("Harvest")]public ItemData fruit; // 水果物品數據public bool death_on_harvest; // 收獲后是否死亡[Header("FX")]public GameObject gather_fx; // 收獲特效public AudioClip gather_audio; // 收獲音效}
}
Item.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;namespace FarmingEngine
{/// <summary>/// 物品是可以被玩家拾取、丟棄并放入物品欄的對象。某些物品還可以用作制作材料或被用于制作。/// </summary>[RequireComponent(typeof(Selectable))][RequireComponent(typeof(UniqueID))]public class Item : Craftable{[Header("Item")]public ItemData data; // 物品數據public int quantity = 1; // 數量[Header("FX")]public float auto_collect_range = 0f; // 當在范圍內時將自動被收集public AudioClip take_audio; // 收取時的音頻public GameObject take_fx; // 收取時的特效public UnityAction onTake; // 收取時的事件}
}
?工作流
采集系統的核心是 Selectable 組件,它是玩家與游戲世界交互的基礎。所有可采集的物體都必須掛載這個組件,它定義了物體的交互類型、范圍和可用動作。
// ... existing code ...
public enum SelectableType
{Interact = 0, // 與物體的中心交互InteractBound = 5, // 與碰撞體包圍盒內最近的位置交互InteractSurface = 10, // 表面交互CantInteract = 20, // 可以點擊但無法交互CantSelect = 30 // 無法點擊或懸停
}
// ... existing code ...
采集系統的工作流程主要包括以下幾個步驟:
1. 檢測與選擇
- 玩家接近可采集物體時, Selectable 組件會檢測到玩家
- 物體被高亮顯示(通過輪廓效果)
- 玩家點擊物體觸發交互
2. 交互與采集
- 對于植物,調用 Harvest 方法
- 對于地面物品,調用 TakeItem 方法
- 系統檢查玩家是否滿足采集條件(如距離、工具等)
3. 反饋與獎勵
- 播放采集音效和特效
- 物品被添加到玩家背包
- 采集對象可能會被銷毀或重置狀態
合成
合成系統的核心作用是允許玩家使用現有資源(物品、材料)通過特定規則和流程創建新的物品、建筑或其他游戲元素,豐富游戲的玩法和 progression 體系。
CraftData.cs - 定義了合成數據的基礎結構:
// ... existing code ...
public class CraftData : IdData
{[Header("顯示")]public string title; // 標題public Sprite icon; // 圖標[TextArea(3, 5)]public string desc; // 描述[Header("分組")]public GroupData[] groups; // 所屬分組[Header("制作")]public bool craftable; // 可以制作嗎?public int craft_quantity = 1; // 制作數量public float craft_duration = 0f; // 制作所需時間[Header("制作成本")]public GroupData craft_near; // 制作時需要附近的可選物體組public ItemData[] craft_items; // 制作所需物品public GroupData[] craft_fillers; // 制作所需填充物public CraftData[] craft_requirements; // 制作前需要建造的物品
// ... existing code ...
這段代碼定義了 CraftData 類,它是所有可制作物品(如物品、建筑、植物)的基礎數據類。代碼中:
- 包含了顯示相關的屬性(標題、圖標、描述),用于在UI中展示物品信息;
- 定義了分組屬性,方便對物品進行分類管理;
- 包含制作相關的屬性(是否可制作、制作數量、制作時間、制作排序),控制物品的制作行為;
- 定義了制作成本相關的屬性(所需物品、填充物、先決條件、附近所需物體),用于檢查制作條件;
- 包含經驗和特效相關的屬性,用于獎勵玩家和提供反饋;
- 提供了一系列方法,用于檢查物品所屬分組、獲取不同類型的數據、計算制作成本、加載數據等功能。
CraftStation.cs - 實現了工作臺功能:
// ... existing code ...
[RequireComponent(typeof(Selectable))]
public class CraftStation : MonoBehaviour
{public GroupData[] craft_groups; // 可以進行合成的組數據public float range = 3f; // 工作臺的使用范圍private Selectable select; // 可選擇組件private Buildable buildable; // 可建造組件void Awake(){station_list.Add(this);select = GetComponent<Selectable>();buildable = GetComponent<Buildable>();select.onUse += OnUse; // 注冊使用事件}private void OnUse(PlayerCharacter character){CraftPanel panel = CraftPanel.Get(character.player_id);if (panel != null && !panel.IsVisible())panel.Show(); // 顯示合成面板}
// ... existing code ...
這段代碼定義了 CraftStation 類,它是工作臺的實現類。代碼中:
- 包含了可合成的組數據和工作臺的使用范圍,控制哪些物品可以在該工作臺合成以及玩家需要靠近到什么程度;
- 引用了 Selectable 和 Buildable 組件,分別用于處理玩家的選擇和交互,以及控制工作臺的建造狀態;
- 在 Awake 方法中,將自身添加到靜態列表中,注冊使用事件;
- 在 OnUse 方法中,當玩家使用工作臺時,顯示合成面板;
- 提供了 HasCrafting 方法檢查是否有可合成的組,以及 GetNearestInRange 方法獲取范圍內最近的工作臺。
?MixingPanel.cs - 實現了混合面板的 UI 和合成邏輯:
// ... existing code ...
public class MixingPanel : ItemSlotPanel
{public ItemSlot result_slot; // 結果槽public Button mix_button; // 合成按鈕/// <summary>/// 檢查是否可以進行混合/// </summary>public bool CanMix(){bool at_least_one = false;foreach (ItemSlot slot in slots){if (slot.GetItem() != null)at_least_one = true; // 至少有一個物品}return mixing_pot != null && at_least_one && result_slot.GetItem() == null;}/// <summary>/// 混合物品/// </summary>public void MixItems(){ItemData item = null;foreach (ItemData recipe in mixing_pot.recipes){if (item == null && CanCraft(recipe)){item = recipe;PayCraftingCost(recipe); // 支付合成成本}}if (item != null){crafed_item = item;result_slot.SetSlot(item, 1); // 設置結果槽}}
// ... existing code ...
- 包含了結果槽和合成按鈕,用于顯示合成結果和觸發合成操作;
- 引用了當前玩家和混合鍋,用于交互和數據處理;
- 在 RefreshPanel 方法中,更新面板的顯示狀態,包括結果槽和合成按鈕的可交互狀態;
- 提供了 ShowMixing 方法顯示混合面板, CanMix 方法檢查是否可以進行混合, MixItems 方法執行混合操作;
- 包含了 HasItem 和 HasItemInGroup 方法檢查是否擁有足夠的物品或物品組, RemoveItem 和 RemoveItemInGroup 方法移除物品或物品組;
- 提供了 CanCraft 方法檢查是否可以合成指定物品, PayCraftingCost 方法支付合成成本;
- 在 OnClickMix 方法中,處理合成按鈕的點擊事件;在 OnClickResult 方法中,處理結果槽的點擊事件,將合成物品添加到玩家背包。?
整個合成系統的工作流程如下:
- 接近工作臺 :玩家移動到 CraftStation 的有效范圍內。
- 使用工作臺 :玩家與工作臺交互,觸發 OnUse 事件,打開合成面板。
- 添加材料 :玩家將所需的材料放入合成面板的槽位中。
- 檢查條件 :系統檢查材料是否滿足合成配方的要求,以及是否滿足其他條件(如附近有特定物體)。
- 執行合成 :玩家點擊合成按鈕,系統消耗材料并生成產品。
- 獲取結果 :合成后的產品顯示在結果槽中,玩家可以將其拾取到背包中。?
天氣與時間系統
天氣系統
天氣系統主要負責管理游戲中的天氣狀態,包括天氣的切換、效果展示以及對游戲世界的影響。
定義了兩種天氣效果:
- None :無特殊天氣效果
- Rain :下雨效果
// ... existing code ...
/// <summary>
/// 天氣效果枚舉
/// </summary>
public enum WeatherEffect
{None = 0, // 無效果Rain = 10, // 下雨
}/// <summary>
/// 天氣數據的ScriptableObject類
/// </summary>
[CreateAssetMenu(fileName ="Weather", menuName = "FarmingEngine/Weather", order =10)]
public class WeatherData : ScriptableObject
{public string id; // 天氣數據的唯一標識符public float probability = 1f; // 天氣發生的概率[Header("Gameplay")]public WeatherEffect effect; // 天氣效果枚舉[Header("Visuals")]public GameObject weather_fx; // 天氣效果的游戲對象public float light_mult = 1f; // 光照倍數
}
// ... existing code ...
這個是天氣數據的定義。
// ... existing code ...
/// <summary>
/// 將此腳本放置在每個場景中,用于管理該場景中可能的天氣列表
/// </summary>
public class WeatherSystem : MonoBehaviour
{[Header("Weather")]public WeatherData default_weather; // 默認天氣數據public WeatherData[] weathers; // 可能的天氣數據列表[Header("Weather Group")]public string group; // 具有相同組的場景將同步天氣[Header("Weather Settings")]public float weather_change_time = 6f; // 天氣變化的時間(游戲時間,以小時為單位)private WeatherData current_weather; // 當前天氣數據private GameObject current_weather_fx; // 當前天氣效果對象private float update_timer = 0f; // 更新計時器private static WeatherSystem instance; // 單例實例void Start(){if (PlayerData.Get().HasCustomString("weather_" + group)) // 如果存在保存的天氣數據{string weather_id = PlayerData.Get().GetCustomString("weather_" + group); // 獲取保存的天氣IDChangeWeather(GetWeather(weather_id)); // 更改為保存的天氣}else{ChangeWeather(default_weather); // 否則更改為默認天氣}}void Update(){update_timer += Time.deltaTime; // 更新計時器if (update_timer > 1f) // 每秒更新一次{update_timer = 0f; // 重置計時器SlowUpdate(); // 慢速更新,檢查是否新的一天或天氣變化時間到了}}void SlowUpdate(){// 檢查是否新的一天int day = PlayerData.Get().day; // 獲取當前天數float time = PlayerData.Get().day_time; // 獲取當前游戲時間(小時)int prev_day = PlayerData.Get().GetCustomInt("weather_day_" + group); // 獲取上次保存的天數if (day > prev_day && time >= weather_change_time) // 如果當前天數大于上次保存的天數且當前時間超過天氣變化時間{ChangeWeatherRandom(); // 隨機更改天氣PlayerData.Get().SetCustomInt("weather_day_" + group, day); // 保存當前天數}}// 隨機更改天氣public void ChangeWeatherRandom(){if (weathers.Length > 0) // 如果有定義的天氣數據{float total = 0f;foreach (WeatherData aweather in weathers){total += aweather.probability; // 計算總概率}float value = Random.Range(0f, total); // 隨機一個值WeatherData weather = null;foreach (WeatherData aweather in weathers){if (weather == null && value < aweather.probability)weather = aweather; // 根據隨機值選取天氣數據elsevalue -= aweather.probability; // 減去當前天氣數據的概率}if (weather == null)weather = default_weather; // 如果未選取到天氣數據,則選擇默認天氣ChangeWeather(weather); // 更改天氣}}// 更改天氣public void ChangeWeather(WeatherData weather){if (weather != null && current_weather != weather) // 如果新的天氣不為空且與當前天氣不同{current_weather = weather; // 設置當前天氣為新的天氣PlayerData.Get().SetCustomString("weather_" + group, weather.id); // 保存當前天氣IDif (current_weather_fx != null)Destroy(current_weather_fx); // 銷毀當前天氣效果對象if (current_weather.weather_fx != null)current_weather_fx = Instantiate(current_weather.weather_fx, TheCamera.Get().GetTargetPos(), Quaternion.identity); // 實例化新的天氣效果對象}}
// ... existing code ...
?使用 WeatherData 類定義天氣的基本屬性,包括ID、概率、效果、視覺效果和光照倍數;WeatherSystem 類負責管理天氣的切換和狀態維護,采用單例模式方便全局訪問。天氣會在每天的特定時間(默認6點)自動切換,切換時根據天氣的概率隨機選擇下一個天氣。天氣狀態會被保存到 PlayerData 中,確保游戲重啟后保持一致。
天氣系統的工作流:
1. 初始化 :游戲啟動時,從 PlayerData 中加載保存的天氣狀態,如果沒有則使用默認天氣。
2. 狀態更新 :每秒更新一次,檢查是否進入新的一天且時間超過天氣變化時間。
3. 天氣切換 :如果滿足天氣切換條件,根據天氣概率隨機選擇下一個天氣,并更新相關狀態。
4. 效果應用 :切換天氣后,銷毀舊的視覺效果,創建新的效果,并調整場景的光照強度。
5. 狀態保存 :天氣切換后,將新的天氣狀態保存到 PlayerData 中
時間系統
時間系統負責管理游戲中的時間流逝、晝夜交替以及時間顯示。
// ... existing code ...
/// <summary>
/// 通用游戲數據(僅一個文件)
/// </summary>[CreateAssetMenu(fileName = "GameData", menuName = "FarmingEngine/GameData", order = 0)]
public class GameData : ScriptableObject
{[Header("游戲時間")]public float game_time_mult = 24f; // 值為1表示游戲時間與現實時間同步,值為24表示1小時現實時間對應1天游戲時間public float start_day_time = 6f; // 開始一天的時間public float end_day_time = 2f; // 自動過到下一天的時間[Header("晝夜變化")]public float day_light_dir_intensity = 1f; // 白天方向光強度public float day_light_ambient_intensity = 1f; // 白天環境光強度public float night_light_dir_intensity = 0.2f; // 夜晚方向光強度public float night_light_ambient_intensity = 0.5f; // 夜晚環境光強度public bool rotate_shadows = true; // 是否根據白天轉動陰影
// ... existing code ...
?通過group
分組標識同組場景共享天氣配置,每日固定時間點(weather_change_time
)按概率權重隨機切換天氣(如晴天60%、雨天30%),并利用PlayerData
保存天氣ID("weather_"+group
)及上次變更日期("weather_day_"+group
),確保玩家重新進入時天氣一致;同時,切換天氣時銷毀舊特效(如雨雪粒子),在相機位置實例化新天氣對應的預制體(weather_fx
),實現視覺與邏輯的實時同步
// ... existing code ...
/// <summary>
/// 顯示天數和時間的時鐘
/// </summary>
public class TimeClockUI : MonoBehaviour
{public Text day_txt; // 顯示天數的文本public Text time_txt; // 顯示時間的文本public Image clock_fill; // 時鐘填充圖像void Update(){// 獲取玩家數據PlayerData pdata = PlayerData.Get();// 計算小時數和秒數int time_hours = Mathf.FloorToInt(pdata.day_time);int time_secs = Mathf.FloorToInt((pdata.day_time * 60f) % 60f);// 更新天數和時間的文本day_txt.text = "DAY " + pdata.day;time_txt.text = time_hours + ":" + time_secs.ToString("00");// 判斷時鐘方向(順時針或逆時針)bool clockwise = pdata.day_time <= 12f;clock_fill.fillClockwise = clockwise;if (clockwise){// 順時針填充:從0到1float value = pdata.day_time / 12f;clock_fill.fillAmount = value;}else{// 逆時針填充:從1到0float value = (pdata.day_time - 12f) / 12f;clock_fill.fillAmount = 1f - value;}}
}
// ... existing code ...
?這個是負責顯示時間UI的代碼。從PlayerData
獲取游戲天數(day
)和時間(day_time
),將時間拆分為小時和分鐘(例如?8:30
),并以?"DAY X"
?格式顯示天數,時間文本精確到分鐘(自動補零)
通過Image
組件的圓形填充(clock_fill
)模擬晝夜循環:
- ?正午前(≤12時)??:順時針填充(
fillClockwise=true
),填充比例 =?day_time/12
,表示從凌晨到正午的進度。 - ?正午后(>12時)??:逆時針填充(
fillClockwise=false
),填充比例 =?1 - (day_time-12)/12
,表示從正午到午夜的進度,形成完整的24小時循環效果。