[遊戲製作進度] [大型專案:BK MOD] ~EP.01~ 自定義遊戲地圖編輯器的C#寫法淺談



下一篇:
[遊戲製作進度] [大型專案:BK MOD] ~EP.02~ 自定義遊戲地圖編輯器與2D物理系統架構




遊戲載點:

遊戲名稱:BK-MOD(V0.1.09)
遊戲類型:橫向卷軸動作遊戲
發表日期:2024/10/08

遊戲雛型下載點:
https://drive.google.com/file/d/1OamQtUduZo45rS_ZEUS6mhnlNRLknV8Z






  上一次寫遊戲製作進度的文章,已經是15年前的事情了。今天會執筆繼續寫相關文章,要感謝Twitch台的觀眾"虫馬蟻"大大的熱情推坑,讓小弟想要像小時候一樣,做一款超屌大型改版遊戲,這個想法又重新燃燒起來。其實小弟不知道能夠持續多久,畢竟上一個坑「凱特與繼承的靈」直接胎死腹中....嗯,這就是人生嘛(´・Å・`)




  這回要來介紹的,是如何不用Unity內建的地圖編輯器,自己寫一個客製化的地圖編輯器。當然,這個編輯器只花了小弟兩天撰寫,勢必有很多地方不到位,但是也希望能夠拋磚引玉,讓更多的軟工朋友對這個專案有點興趣(●´ω`●)ゞ












正文:






  小弟寫Unity的經驗不多,但還是嘗試將以前在遊戲公司淺學、網路Youtube介紹文章、壓榨ChatGPT的結果做個說明。首先是Hierarchy的配置。除了初始的Main CameraDirectional Light等等以外,小弟多生了StageBG(準備放一堆C#自動生成的GameObject子物件用)、2DCanva(這次沒用到,理論上是用來放位置絕對不變的東東像是血條)、TmpObj > BGcell(準備用來複製的物件本人,先設定一開始看不到)。







  接著是美工的部分,小弟將15多年前畫的美美的草皮platform拿來用,以60*60為一個sprite,用Sprite Editor做Slice(切割),差不多就完成了~因為還是Prototype,圖檔都沒有一一命名,但時間允許的話,還是命名一下比較妥當(。í _ ì。)








  接著,您可以觀賞以下影片的Unity撰寫介紹,會詳細告訴您怎麼做Grid System(方格系統),小弟也是以這個C#寫法當作基礎去修改的。



[1] Youtube - Grid System in Unity (Heatmap, Pathfinding, Building Area)







  接下來廢話不多說,上代碼~



MainStage.cs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainStage : MonoBehaviour
{
    private GridMap gridMap;

    private void Start()
    {
        gridMap = new GridMap(10, 10, 10f, new Vector3(0, 0));
    }

    private void Update()
    {
        //Set current camera position
        if (Input.GetKey("w") || Input.GetKey("up"))
            Camera.main.transform.Translate(new Vector3(0, 3f, 0));
        if (Input.GetKey("s") || Input.GetKey("down"))
            Camera.main.transform.Translate(new Vector3(0, -3f, 0));
        if (Input.GetKey("a") || Input.GetKey("left"))
            Camera.main.transform.Translate(new Vector3(-3f, 0, 0));
        if (Input.GetKey("d") || Input.GetKey("right"))
            Camera.main.transform.Translate(new Vector3(3f, 0, 0));

        //Edit WorldMap Background by Mouse & KeyBoard
        Vector3 mousePosition = Input.mousePosition;
        mousePosition.z = Camera.main.transform.position.z;
        Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
        if (Input.GetMouseButtonDown(0))
            gridMap.AddValue(worldPosition);
        if (Input.GetMouseButtonDown(1))
            gridMap.SubstractValue(worldPosition);
        if (Input.GetKeyDown("space"))
            Debug.Log(gridMap.GetValue(worldPosition));

    }
}






GridMap.cs

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GridMap
{
    //input 4 variables of GridMap class
    private int width;
    private int height;
    private float cellSize;
    private Vector3 originPosition;

    //Content of the grid map
    private int[,] gridArray;
    private TextMesh[,] gridTextArray;
    private SpriteRenderer[,] gridSpriteArray;

    //Load full Sprites for game background
    private Sprite[] spriteTemplate = Resources.LoadAll<Sprite>("Platform");

    //Get parent GameObject "StageBG"
    public GameObject StageBG = GameObject.Find("StageBG");

    public GridMap(int width, int height, float cellSize, Vector3 originPosition) 
    {
        this.width = width;
        this.height = height;
        this.cellSize = cellSize;
        this.originPosition = originPosition;

        gridArray = new int[width, height];
        gridTextArray = new TextMesh[width, height];
        gridSpriteArray = new SpriteRenderer[width, height];

        for (int x = 0; x < gridArray.GetLength(0); x++) 
        {
            for (int y = 0; y < gridArray.GetLength(1); y++)
            {
                //Create TextMesh
                GameObject gameObject = new GameObject("World_Text " + x + "-" + y, typeof(TextMesh));
                Transform transform = gameObject.transform;
                transform.SetParent(StageBG.transform, false);
                transform.localPosition = GetWorldPosition(x, y) + new Vector3(cellSize, cellSize) * .5f;
                TextMesh textMesh = gameObject.GetComponent<TextMesh>();
                textMesh.anchor = TextAnchor.MiddleCenter;
                textMesh.alignment = TextAlignment.Center;
                textMesh.text = gridArray[x, y].ToString();
                textMesh.fontSize = 20;
                textMesh.color = Color.white;
                textMesh.GetComponent<MeshRenderer>().sortingOrder = 1;
                gridTextArray[x, y] = textMesh;

                //Create SpriteRenderer
                GameObject gameObject2 = new GameObject("World_Sprite " + x + "-" + y, typeof(SpriteRenderer));
                Transform transform2 = gameObject2.transform;
                transform2.SetParent(StageBG.transform, false);
                transform2.localPosition = GetWorldPosition(x, y) + new Vector3(cellSize, cellSize) * .5f;
                transform2.localScale = new Vector3(10, 10, 10);
                SpriteRenderer spriteRenderer = gameObject2.GetComponent<SpriteRenderer>();
                spriteRenderer.sprite = spriteTemplate[gridArray[x, y]];
                gridSpriteArray[x, y] = spriteRenderer;
            }
        }
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////

    private Vector3 GetWorldPosition(int x, int y) 
    {
        return new Vector3(x, y) * cellSize + originPosition;
    }

    private void GetXY(Vector3 worldPosition, out int x, out int y)
    {
        x = Mathf.FloorToInt((worldPosition - originPosition).x / cellSize);
        y = Mathf.FloorToInt((worldPosition - originPosition).y / cellSize);
    }

    //////////////////////////////////////////////////////////////////////////////////////////////////////

    #region [Value +/-/set/get]

    public void AddValue(int x, int y)
    {
        if (x >= 0 && y >= 0 && x < width && y < height && gridArray[x, y] < spriteTemplate.Length - 1)
        {
            gridArray[x, y] += 1;
            gridTextArray[x, y].text = gridArray[x, y].ToString();
            gridSpriteArray[x, y].sprite = spriteTemplate[gridArray[x, y]];
        }
    }

    public void AddValue(Vector3 worldPosition)
    {
        int x, y;
        GetXY(worldPosition, out x, out y);
        AddValue(x, y);
    }

    public void SubstractValue(int x, int y)
    {
        if (x >= 0 && y >= 0 && x < width && y < height && gridArray[x, y] > 0)
        {
            gridArray[x, y] -= 1;
            gridTextArray[x, y].text = gridArray[x, y].ToString();
            gridSpriteArray[x, y].sprite = spriteTemplate[gridArray[x, y]];
        }
    }

    public void SubstractValue(Vector3 worldPosition)
    {
        int x, y;
        GetXY(worldPosition, out x, out y);
        SubstractValue(x, y);
    }

    public void SetValue(int x, int y, int value)
    {
        if (x >= 0 && y >= 0 && x < width && y < height)
        {
            gridArray[x, y] = value;
            gridTextArray[x, y].text = value.ToString();
            gridSpriteArray[x, y].sprite = spriteTemplate[gridArray[x, y]];
        }
    }

    public void SetValue(Vector3 worldPosition, int value)
    {
        int x, y;
        GetXY(worldPosition, out x, out y);
        SetValue(x, y, value);
    }

    public int GetValue(int x, int y)
    {
        if (x >= 0 && y >= 0 && x < width && y < height)
            return gridArray[x, y];
        else
            return 0;
    }

    public int GetValue(Vector3 worldPosition)
    {
        int x, y;
        GetXY(worldPosition, out x, out y);
        return GetValue(x, y);
    }

    #endregion
}



大致上的程序執行順序是:
  1. StageBG所附加的元件MainStage.cs會首先被執行,透過private GridMap gridMap生成gridMap這個class,並在Start()時初始化方格地圖。
  2. GridMap.cs會根據MainStage.cs傳入的Grid長、寬、方格大小、起始位置去跑2D巢狀迴圈。
  3. 巢狀迴圈最裡面會做兩件事:1.新產生TextMesh(for顯示文字) 2.新產生SpriteRenderer(for顯示圖片),生成的物件全部丟到StageBG底下,未來管理遠近(z軸遠近)就靠StageBG底下的分門別類(也可以是z軸深淺)。
  4. 新生成的TextMesh物件連結到gridTextArray[x, y],新生成的SpriteRenderer物件連結到gridSpriteArray[x, y],未來在變更圖片跟文字,只要動這兩個array即可。至此完成Start()時初始化事件。
  5. MainStage.cs接著跑到Update()並且每個frame都會執行一次。
  6. Update()裡面一開始有個"設定目前Camera位置"的code,這段code會偵測是否持續按下wsad或是上下左右鍵,若是,則根據按鍵移動Camera的(x, y),z則不動
  7. Update()接著跑到一段"用滑鼠鍵盤設定遊戲世界地圖"的內容。首先,先抓取滑鼠於螢幕上的位置,接著將此位置的z軸變更為Camera的z軸(為ScreenToWorldPoint鋪路),最後透過ScreenToWorldPoint指令,將滑鼠座標轉換成遊戲世界座標
  8. 然後,我們再用遊戲世界座標軟換成Grid System座標,gridMap.AddValue、gridMap.SubstractValue、gridMap.GetValue都寫在GridMap.cs裡面,會轉換成Grid用的x, y座標,並且根據此x, y座標改變gridTextArray[x, y]、gridSpriteArray[x, y]當中的特定物件,也就是最後會顯示的文字與圖片。








結語:



  Unity真的是個非常強大的遊戲製作引擎,必備的C#程式語法也不會像C\C++這麼艱澀難啃,非常適合對製作遊戲有熱愛且程設剛初入門的朋友。這麼講感覺小弟的作品已經在收尾了,不,才剛開始而已....(〒︿〒)








相關文章:
[2] [遊戲製作進度] [MMF2] 武器、防具以及飾物裝備

[3] [遊戲製作進度] [MMF2] 拯救貪吃老人大作戰 - 地底的洞窟(一)

[4] [遊戲製作進度] [MMF2]拯救貪吃老人大作戰 - 目前進度列表

沒有留言:

張貼留言