[Unity] Grid System簡介&應用 (多圖)

20 回覆
13 Like 1 Dislike
2023-04-30 20:55:52
最近對2D 嘅建築經營遊戲有啲興趣,於是決定試下喺Unity整返個類似嘅System出嚟
利申:自學Unity,非專業教學,如果有錯誤歡迎補充指正

開始前俾大家睇下啲例子先:
Mindusty


Core Keeper


-----------------------------------------------------


首先開個新2D Project,我用嘅Unity版本係2022.2.7f1


首先我哋要用Grid將地圖割開一格格先(Grid Cell),加個Empty GameObject再撳Add Component加Grid (2D同3D都可以用Grid)


Grid有4個Properties可以set
1) Cell Size: 每一格嘅大小

2) Cell Gap: 每一格嘅間隔

3) Cell Layout: 每一格嘅形狀,有4款可以揀:
Rectangle


Hexagon


Isometric 同埋 Isometric Z as Y


4) Cell Swizzle: 將XYZ重新排序做其中一個選項
-----------------------------------------------------


之後要將mouse postion反映返落去grid cell到,create一個新script:
using UnityEngine;

public class InputManager : MonoBehaviour
{
    //攞Grid Component
    [SerializeField]private Grid grid;

    //顯示Mouse目前所在位置
    [SerializeField]private GameObject mouseIndicator;

    //攞Main Camera
    private Camera mainCamera;

    //Save低Mouse位置
    private Vector2 mousePosition;
    
    private void Start()
    {
        //Camera.main會係套用MainCamera Tag嘅Camera
        mainCamera = Camera.main;
    }

    private void Update()
    {
        UpdateMouseIndicator();
    }

    private void UpdateMouseIndicator(){
        //攞Mouse目前所在位置
        mousePosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);

        //更新mouseIndicator位置
        mouseIndicator.transform.position = grid.WorldToCell(mousePosition);
    }
}

[SerializeField]: 正常private field唔會喺Inspector顯示,加呢句就可以強制serialize個private field

mouseIndicator: 一個要嚟顯示目前所在位置嘅GameObject



Camera.main: 用緊MainCamera Tag嘅Camera


ScreenToWorldPoint: 將座標從Screen Position轉換為World Position

WorldToCell: 將座標從World Position轉換為Cell Position,咁就可以將GameObject正確放喺Cell上面
-----------------------------------------------------


Hierarchy嘅樣


Input Manager


Grid System


-----------------------------------------------------

完成效果



之後會整埋鏡頭移動先可加建築系統
-----------------------------------------------------


詳細嘅document:
Grid
https://docs.unity3d.com/2022.2/Documentation/ScriptReference/Grid.html

SerializeField
https://docs.unity3d.com/2022.2/Documentation/ScriptReference/SerializeField.html

Camera.main
https://docs.unity3d.com/2022.2/Documentation/ScriptReference/Camera-main.html

ScreenToWorldPoint
https://docs.unity3d.com/2022.2/Documentation/ScriptReference/Camera.ScreenToWorldPoint.html

WorldToCell
https://docs.unity3d.com/2022.2/Documentation/ScriptReference/GridLayout.WorldToCell.html
2023-04-30 21:06:59
幾好 留名學野.
2023-04-30 22:40:48
之後到鏡頭移動

喺之前顯示Grid Cell個script到加一個private field控制鏡頭移動嘅速度
[SerializeField]private float cameraMoveSpeed;

加一個新method
private void UpadteCameraMovement(){
        cameraPosition.x = Input.GetAxis("Horizontal");
        cameraPosition.y = Input.GetAxis("Vertical");

        mainCamera.transform.position += cameraPosition * cameraMoveSpeed * Time.deltaTime;
    }


呢個method嘅作用係攞返User嘅Input乘以移動速度乘以每一frame嘅相隔時間,咁就可以確保有好smooth移動

Time.deltaTime: 從上一個frame到當前frame嘅間隔(以秒為單位)

Input.GetAxis():係攞緊Input Manager入面Set咗嘅Axis,分別係:
Horizontal: WS↑↓
Vertical: AD←→

如果想改制喺Input Manager對應嘅Axis到改返就得




記得放返入Update()入面
private void Update()
    {
        UpdateMouseIndicator();
        //++
        UpadteCameraMovement();
    }


Set返移動速度



為咗顯示移動效果,加左個Square喺中間同另外寫咗個script顯示返我撳咗咩制
效果:

-----------------------------------------------------


下一步就開始整建築system,呢一part會用埋ScriptableObject
由於比較多嘢要整,所以更新時間會長少少
-----------------------------------------------------


詳細嘅document:
Time.deltaTime
https://docs.unity3d.com/ScriptReference/Time-deltaTime.html

Input.GetAxis
https://docs.unity3d.com/ScriptReference/Input.GetAxis.html
2023-05-01 00:26:50
希望呢啲po喺軟件台越嚟越多
2023-05-01 01:46:34
lm
2023-05-01 03:07:51
總算完成咗個建築SYSTEM

由於呢PART比較複雜,我會分幾PART出

首先介紹返成個系統先:
1) FactoryObject - ScriptableObject
用嚟儲存唔同種類嘅Factory,因為係ScriptableObject,無論要加新嘅FactoryObject定係喺Runtime時存取數據都好方便。後面會講多少少有關ScriptableObject嘅嘢

2) FactoryObjectDictionary
將放咗喺Array入面嘅FactoryObject轉做IDictionary儲低,咁樣只要用Factory Name做KEY就可以知道IDictionary入面有冇相應嘅FactoryObject,同存取FactoryObject嘅Data

3) FactorySystem
運算相關Factory嘅生產時間同產出資源,Spawn個陣會套用相關Factory嘅Data

4) Factory Prefab
有FactorySystem呢個Component,每個New Factory都會獨立運行

5) PlacementManager
當User選擇咗要起嘅Factory,原本顯示Grid Cell位置嘅提示會變做揀咗嘅Factory Sprtie,如果所選位置冇Factory先可以起,否則會Factory Sprtie變做紅色。

Left-Click個陣就會用Factory Prefab去Spawn新嘅Factory,Spawn完會即刻將揀咗嘅FactoryObject套用落去。
-----------------------------------------------------


第一PART:
FactoryObject - ScriptableObject
using UnityEngine;

//新增ShortCut去AssetMenu
[CreateAssetMenu(fileName = "NewFactoryObject", menuName = "ScriptableObject/FactoryObject", order = 0)]

public class FactoryObject : ScriptableObject {
    //Factory嘅Sprite
    public Sprite sprite;                                          

    //Factory嘅名
    public string factoryName;                                
    //生產時間
    public float processTime;

    //能源消耗
    public float energyCost;

    //產出資源
    public OutputResource outputResource;

    //簡介
    [TextArea]public string description;                   
}

//產出資源嘅種類
public enum OutputResource{
    NONE,
    WATER
}

係到要講解下咩係ScriptableObject先:

ScriptableObject
一個要嚟儲存大量數據嘅容器,可以當佢係一個模具/預製件,當要建立大量有重複屬性嘅GameObject嘅時候,可以用ScriptableObject去預先建立一個模具,咁樣你加嘅Object會以data嘅形式儲喺Mermory到,每當要Spawn一個相應於ScriptableObject嘅GameObject,佢只會喺Mermory到access data而唔係再加個新data,同一時間只會有一份相同嘅data喺Mermory,最終效果就係減少Memory Usage。


要整個新嘅FactoryObject 可以Right-Click Project Folder揀返FactoryObject


之後會多咗個FactoryObject喺Folder入面


可以喺inspector入面Set返啲數值



如果之後要加多幾款資源,只要係enum入面加嘢就得
public enum OutputResource{
    NONE,
    WATER,
    PLASTIC
}


咁就完成咗整FactoryObject呢PART
-----------------------------------------------------


詳細嘅document:
ScriptableObject
https://docs.unity3d.com/Manual/class-ScriptableObject.html
2023-05-01 12:51:50
巴打好有心
2023-05-01 13:03:41
2023-05-01 13:06:42
巴打會唔會講吓DOTS
2023-05-01 20:30:46
之後可以講吓不過我要計劃下點樣講解同示範先
可能會單獨講左Burst Compiler同Job System先
ECS就留返起DOTS Project嗰陣再講
2023-05-01 22:14:02
第二PART:
FactoryObjectDictionary
using System.IO;
using System.Collections.Generic;
using UnityEngine;

public class FactoryObjectDictionary : MonoBehaviour
{
    [SerializeField]private FactoryObject[] factoriesList;
    private IDictionary<string, FactoryObject> factoriesDictionary = new Dictionary<string, FactoryObject>();

    private void Start() {
        foreach(FactoryObject factory in factoriesList){
            factoriesDictionary.Add(factory.factoryName, factory);
            Debug.Log("Factory - " + factory.factoryName + " loaded");
        }
    }

    public FactoryObject GetFactoryObjectByName(string _name){
        if(factoriesDictionary.ContainsKey(_name))
            return factoriesDictionary[_name];

        return null;
    }
}

將放咗喺Array入面嘅FactoryObject轉做IDictionary儲低,咁樣只要用Factory Name做KEY就可以知道IDictionary入面有冇相應嘅FactoryObject,同存取FactoryObject嘅Data

ContainsKey
判斷 Dictionary<TKey,TValue> 係咪包含特定KEY

放返啲FactoryObjects入去就搞掂


Play個陣會log返load咗邊啲FactoryObjects

-----------------------------------------------------


詳細嘅document:
IDictionary
https://learn.microsoft.com/en-us/dotnet/api/system.collections.idictionary?view=net-7.0
2023-05-01 22:39:52
第三PART:
FactorySystem
using UnityEngine;
using UnityEngine.UI;

public class FactorySystem : MonoBehaviour
{
    [SerializeField]private Slider slider;

    private bool isInited;
    
    private FactoryObject factory;
    private float processTime;

    private void Update() {
        if(isInited && factory != null){
            processTime -= Time.deltaTime;
            slider.value += Time.deltaTime;

            if(processTime <= 0){
                Debug.Log(factory.outputResource.ToString() + "+ 1");
                processTime = factory.processTime;
                slider.value = 0;
            }
        }
    }

    public void LoadFactoyFromDictionary(FactoryObject _factory){
        factory = _factory;

        if(factory == null){
            Destroy(gameObject);
            return;
        }

        GetComponent<SpriteRenderer>().sprite = factory.sprite;
        processTime = factory.processTime;
        slider.maxValue = processTime;

        isInited = true;
        Debug.Log("Spawned New Factory - " + factory.factoryName);
    }
}

運算相關Factory嘅生產時間同產出資源,Spawn個陣會套用相關Factory嘅Data

如果local嘅factory已經透過LoadFactoyFromDictionary 更新咗,就會開始生產資源
processTime -= Time.deltaTime;
用Set咗嘅生產時間減每一frame嘅時間去計剩餘所需生產時間

slider.value += Time.deltaTime;
用Slider顯示生產進度

如果剩餘所需生產時間<=0,就reset所需生產時間同生產進度
另外會log返生產咗資源,遲啲會加返visual effect同儲低持有資源量


LoadFactoyFromDictionary 入面會set返local嘅factory = _factory;
 if(factory == null){
       Destroy(gameObject);
       return;
 } 

如果load唔到嘢就Destory呢個gameobject

最後係load完data之後就set返isInited = true;
同埋log返Debug.Log("Spawned New Factory - " + factory.factoryName);

-----------------------------------------------------

然後要整一個Prefab hold住個FactorySystem同做範本
Factory Prefab

需要加一個Slider去顯示生產進度


想知點整可以睇Brackeys嘅教學
https://youtu.be/BLfNP4Sc_iA


需要嘅Components


Sprite Renderer
Factory嘅樣

FactorySystem
Load返FactoryObject Data同Handle 生產,將整好嘅sldier放返入去

Box Collider 2D
Detect返同揀個格有冇overlap,配合下一part用
將個size較細少少,因為grid cell條邊重疊咗,較細啲先會正常運作



咁第三PART就完成啦
2023-05-02 11:04:53
2023-05-02 21:49:46
第四PART
首先喺Input Manager加返一個新method
public Vector2 GetCellPosition(){
        return new Vector2(grid.WorldToCell(mousePosition).x, grid.WorldToCell(mousePosition).y);
}


PlacementManager
using UnityEngine;
using UnityEngine.EventSystems;

public class PlacementManager : MonoBehaviour
{
    //攞返之前整嘅Scripts
    [SerializeField]private FactoryObjectDictionary factoryObjectDictionary;
    [SerializeField]private InputManager inputManager;
    [SerializeField]private GameObject factoryPrefab;

    //Update返揀咗嘅Factory
    private FactoryObject selectedFactory;

    //Check有冇重疊
    private bool isOverlapped;

    private void Update()
    {
        PlacementHandler();
    }

    private void PlacementHandler(){
        //如果Left-Clcik個陣有揀Factory同冇重疊
        if(Input.GetMouseButtonDown(0) && selectedFactory != null && !isOverlapped){
            //如果Mouse喺UI上面就reutrn
            if (EventSystem.current.IsPointerOverGameObject())
                return;
            
            //Soawn新Factory
            GameObject newFactory = Instantiate(factoryPrefab, inputManager.GetCellPosition(), Quaternion.identity);
            //Set返新Factory嘅object data
            newFactory.GetComponent<FactorySystem>().LoadFactoyFromDictionary(selectedFactory);
        }
    }

    //Method俾UI Button用嚟更新sSle
    public void UpdateSelectedFactory(string _name){
        //透過KEY攞返VALUE
        selectedFactory = factoryObjectDictionary.GetFactoryObjectByName(_name);
        //更新Mouse Indicator嘅sprite
        GetComponent<SpriteRenderer>().sprite = selectedFactory.sprite;
        Debug.Log("Selected - " + selectedFactory.factoryName);
    }

    private void OnTriggerEnter2D(Collider2D other) {
        if(!other.gameObject.CompareTag("Mouse Indicator")){
            isOverlapped = true;
            GetComponent<SpriteRenderer>().color = Color.red;
            Debug.Log("OverLapped!");
        }
    }

    private void OnTriggerExit2D(Collider2D other) {
        if(!other.gameObject.CompareTag("Mouse Indicator")){
            isOverlapped = false;
            GetComponent<SpriteRenderer>().color = Color.white;
        }
    }
}

OnTriggerEnter2D(Collider2D)
如果有其他Object入去所屬Object嘅Collider嘅時候就會觸發入面嘅Code

OnTriggerExit2D(Collider2D)
如果有其他Object離開所屬Object嘅Collider嘅時候就會觸發入面嘅Code

注意: 用OnTrigger前請確保最少其中一個Object有RigiBody先會觸發到

因為Factory一定唔只一個,所以我哋放RigiBody2D喺Mouse Indicator到,減少無謂嘅Component
同埋加一個BoxCollider2D做Trigger


RigiBody2D嘅Gravity Scale set返做0


最後放返齊FactoryObjectDictionary, InputManager 同埋 Factory Prefab
-----------------------------------------------------
然後整個UI Button俾Player揀Factory



喺Button Component入面加返OnClick Listener,揀返PlacementManager UpdateSelectedFactory打返Factory Name落去個field到


咁就完成咗成個建築System
-----------------------------------------------------

最終效果




-----------------------------------------------------


詳細嘅document:

OnTriggerEnter2D
https://docs.unity3d.com/ScriptReference/MonoBehaviour.OnTriggerEnter2D.html

OnTriggerExit2D
https://docs.unity3d.com/ScriptReference/Collider2D.OnTriggerExit2D.html
2023-05-02 21:51:24
暫時計劃下一步會整返有sprite嘅output system同加運輸帶轉移資源
不過嚟緊比較忙
應該星期六先再有Update
2023-05-02 22:15:00
2023-05-02 22:54:05
2023-05-03 17:34:02
2023-05-03 19:28:01
2023-05-04 09:07:42
吹水台自選台熱 門最 新手機台時事台政事台World體育台娛樂台動漫台Apps台遊戲台影視台講故台健康台感情台家庭台潮流台美容台上班台財經台房屋台飲食台旅遊台學術台校園台汽車台音樂台創意台硬件台電器台攝影台玩具台寵物台軟件台活動台電訊台直播台站務台黑 洞