快轉到主要內容

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

·2492 字·5 分鐘
作者
Keaton凱特
BK-MOD 是一款動作角色扮演遊戲。主角凱特穿著吉祥物布偶裝,透過努力在世界上生存下去。

遊戲製作進度文跟下載點,未來都會放到BK-MOD官方網站~
https://www.bk-mod.com/

下一篇:

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

https://hcyang1227.blogspot.com/2023/04/bk-mod-ep02-2d.html

上一次寫遊戲製作進度的文章,已經是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)

https://www.youtube.com/watch?v=waEsGu–9P8

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

MainStage.cs

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

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
}

大致上的程序執行順序是:

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

https://hcyang1227.blogspot.com/2006/02/blog-post_27.html

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

https://hcyang1227.blogspot.com/2006/09/blog-post.html

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

https://hcyang1227.blogspot.com/2008/04/blog-post.html