Skip to content
AIk edited this page Aug 13, 2023 · 1 revision

Undertale Changer Template Wiki - 教程

前言

我不知道你是懷着怎麼樣的一種心態,發現了Undertale Changer Template,並點開了它的Wiki的教程頁面。

  • 也許你是一位從未接觸過編程的新人,想嘗試做同人遊戲然後發現了這玩意兒。
  • 也許你曾使用過其他的引擎/模板,但是對其不滿而輾轉到這裏。
  • 也許你的編程水平相當之高,只是偶然聽聞。 誰知道呢。

不管怎樣,Undertale Changer Template在它出現並開源的那一刻起,便屬於每一個使用它的人。

……但你真的需要它嗎?

我是說,從Undertale誕生的那一天起,直到現在。

如果你想做一個Undertale的同人遊戲,那方法實在是太多了。

  • 單純的只有一個戰鬥?你完全可以使用同爲Unity引擎編寫的Unitale或者CreateYourFrisk,使用Lua編寫mod並用它們運行,更輕量,也更好上手。

  • 或者你想更進一步?TML的UNDERTALE Engine,它使用了編寫原版Undertale的遊戲引擎——GameMaker Studio開發,並且可以實現所有原版Undertale的功能。

  • 你都不滿意嗎?基於GameMaker Studio、Godot、Clickteam Fusion引擎,C++、Python等等語言的模板/引擎,我都數不清有多少個了,在github、gamejolt上面隨便一搜,你的選擇多得很,不是麼?

  • 你真的,需要Undertale Changer Template嗎?

好好想想。


Undertale Changer Template真的不是你最好的選擇。

  • Undertale Changer Template沒有上述的那些模板/引擎成熟,大概也沒它們好用。

  • Undertale Changer Template所製作的遊戲內存佔用更大。(雖然但是,我確實是一點內存優化也沒做)

  • Undertale Changer Template的開發模式也許和上述模板/引擎很不一樣(雖然但是,我沒用過那些引擎,我也不清楚),這意味着你可能需要一段時間學習。

等等……

不管怎樣,我勸退的話已經扯完了。如果你還是堅持使用它的話。

那就來吧。

須知

在下文的內容中,我假定你已經學習過C#教程,基本掌握C#語言。

同時也一定程度上學習過如何使用Unity引擎(尤其是它的2D包的相關內容)。

並且你是第一次使用此模板。

C#與Unity相關教程可以在廣袤的互聯網上隨意尋找。

但如果你問我是從哪裏起步的。我的回答是這個這個。(均爲C#中文教程)

此外,建議你收藏Unity腳本API網站,以查詢Unity API相關內容。

至於Unity引擎的學習教程,網上有又新又好的,去找就是了。

開始

在你第一次打開模板的時候,它應該打開了一個空的場景。大概是長這樣。(如果不是也沒關係,待會的步驟是一樣的) image

由於你第一次使用模板,所以我先教你運行一下整個模板,以此看看模板內大概的樣子。

首先找到模板下面的Scenes文件夾,這裏面存放着所有模板內的場景。

然後順着Assets/Scenes/Menu/Story/Story.unity路徑,打開Story場景,這就是原版的講故事場景。 image 點播放,然後按照你玩原作的方式,稍微玩一下吧。

等你玩完一遍之後,我們就可以真正地開始了。

總述

模板內的腳本有註釋,可以參考註釋進行編寫,此處給予模板大體介紹。

我在這裏簡單寫一下此模板的編寫思路。

Undertale原作,在我的模板內可認爲由以下兩部分組成:

戰鬥內場景,戰鬥外場景。

很簡單,就像它們的字面意思一樣。

而戰鬥內場景也可以細分爲地圖內場景(Overworld)和雜項場景(故事、標題、菜單、重命名等)

  • 例如,地圖內場景有玩家的腳本控制其移動,故事場景有腳本控制幻燈片的淡入淡出,戰鬥內場景也有腳本組成戰鬥系統。 也有一部分腳本是全部場景或大部分場景通用的。

每一類場景中有支撐它們運作的腳本。

  • 例如,打字機腳本,以及總控腳本,等等。

在模板的Assets/Scripts內存放着模板內使用的代碼,你在這裏可以看到以下幾個文件夾。 image

  • Battle文件夾——僅在戰鬥內場景使用的腳本。
  • Control文件夾——其中的腳本均繼承自ScriptableObject類,用以創建Resources下的數據存儲文件。
  • Debug文件夾——正如其名,是Debug所用的腳本。
  • Default文件夾——通用/未分類的腳本。
  • Obsolete文件夾——棄用的腳本,多爲參考他人使用的腳本。
  • Overworld文件夾——僅在戰鬥外場景使用的腳本。
  • Volume文件夾——自定義後處理相關的腳本。

目前,你只需要對這些個文件夾的分類有個大致印象就好。詳細內容會在下文進行講述。

地圖內場景

image

這是一個地圖內場景,當然了。


做一個新的地圖

要做一個新的地圖,我們需要新建一個場景。

按下Ctrl + N,在新建場景的頁面,你會看到一個叫Overworld scene的場景模板:

image

這是這個場景模板的存儲位置(你知道就行 別打開它 除非你知道你在做什麼):

image

新建場景後,就會出現這麼一個啥都沒有的新的場景。

image

名爲Grid的Obj就是瓦片地圖,你可以隨心所欲的開始鋪瓦片,但要注意,瓦片的排序圖層一般來說就是“Tilemap”,如圖。

image

隨便擺了一下。

image

適當加一點光源和後處理。(沒錯直接搬的Ruins示例場景)

image

然後可以存儲一下,改個令你印象深刻的名字,放在Scene文件夾裏面,如圖。

image

然後給你的瓦片地圖加一個碰撞體。我知道可以用Tilemap Collider 2D,但我還是比較習慣用Edge Collider 2D。所以按你喜歡的方式加個碰撞——

image

但這時,如果你運行了場景,它會報一個錯,如下。

image

我們順着報錯去找出錯的代碼,就會找到下面這玩意兒。

...
  string LoadLanguageData(string path)
    {
        if (languagePack < languagePackInsideNum)
        {
            return Resources.Load<TextAsset>("TextAssets/LanguagePacks/" + GetLanguageInsideId(languagePack) + "/" + path).text;
        }
        else
        {
            return File.ReadAllText(Directory.GetDirectories(Application.dataPath + "\\LanguagePacks")[languagePack - languagePackInsideNum] + "\\" + path + ".txt");
        }
    }
...

這裏的代碼是負責加載語言包的,而每個地圖內場景都有一個對應的txt文件,用於存儲該場景內的所有數據(對話等)。

我們的解決方案也很簡單,順着內置語言包路徑(Assets/Resources/TextAssets/LanguagePacks)找到你目前在使用的語言包,然後在裏面的“Overworld”文件夾裏,你看到的都是與項目內場景同名的txt文件。

image

咱也加上咱的場景就是了。

image

一些格式和特殊富文本的寫法可以打開Example-Corridor看裏面的註釋。

但具體的事件呢,咱待會再寫。現在就放一個空的txt在這就得了。

現在進場景,我們發現攝像機在我們走到左邊的時候卡住不動,這是因爲我們還沒有設置攝像機跟隨。

image

MainCamera裏面的CameraFollowPlayer就是設置攝像機跟隨用的,可以把frisk扔到地圖的最左邊或者最右邊,然後改這個腳本的數值測試攝像機鎖定的範圍。

當然,也可以吧Limit的√去掉,這樣就沒跟隨限制了。

如果不需要跟隨,把這個腳本去掉就行。

image

設置好的效果:

image

然後這個場景就有點樣子了,你可以隨便亂跑然後不會飛出去之類的。不過還沒聲。我們在MainControlSummon中解決這個問題。

image

把BGM拖上去,然後照着上面的信息寫就完事了,一般來說,你只需要把BGM加上,別的不用管。

搞定這個之後,這個場景更好了一點。不過,它現在沒法進行任何交互,但不急,我們馬上就要提到它了。


事件

事件是幹什麼用的?簡單來說,他就是讓你在遊戲內查看某個物體,或者在地圖邊界進入下一個場景用的。

書接上回,我們的場景現在什麼也調查不了,也沒有離開場景的方法。

添加一個事件很簡單。我們需要在場景裏面加一個物體,身上需要有2d碰撞體,就像這樣。

image

我的建議是可以把它放在Grid/Detectables下面,方便管理。(Detectables是一個新建座標重置過的的空物體)

image

我們在上面添加OverworldObjTrigger,然後你會看到一大堆亂七八糟的選項,相關的作用上面的註釋有寫因此不再贅述。

image

我們只需要在調查的時候能彈出一些字來就好。例如什麼“* 這是一個破牆”之類的。但首先,咱還沒有把調查需要的文本寫進去。

還記得之前提過的內置語言包路徑(Assets/Resources/TextAssets/LanguagePacks)麼?找到你之前創建的txt文件。

image

我們到裏面去進行編輯。你可以參考Example-Corridor內的註釋。

CrackedWall\<image=-1><fx=0>* This is a cracked wall.;

如果你懶得看註釋,那我簡單解釋一下。斜槓前面的是OverworldObjTrigger的檢測文本,後面就是具體的文本內容。

<image=-1>是打字的頭像(-1是沒有頭像)。頭像的精靈存儲在BackpackCanvas裏面的Sprite Changer裏面。

image

<fx=0>是打字的音效。存儲在Assets/Resources/AudioControl的FxClipType內。

image

別忘了在OverworldObjTrigger裏面寫上對應的檢測文本。

image

我們走到牆那裏去調查,然後字就出來嘞!

image

好了,接下來,我們需要一個離開場景的物體。

我們微調一下場景,弄一個出口。

image

這裏要設置爲觸發器。

image

在OverworldObjTrigger裏面設置如下。

image

image

勾選ChangeScene後,檢測到玩家就會切換場景。

勾選BanMusic後,音樂會漸出。

SceneName就是切換場景的目的地。

NewPlayerPos便是切換到新場景後玩家的座標。

現在讓我們進遊戲測試一下!

image

可以看到我們成功來到了另一個場景。

我們的事件就講到這兒了,至於OverworldObjTrigger裏面的其他選項,例如攝像機移動等,可以去看看Example-Corridor裏面怎麼設置的。

Tips:如果你這時停止運行,然後再運行,你會發現玩家的位置移動了。

image

這是因爲玩家在切換場景時,目的地的座標會存儲在Assets/Resources/OverworldControl的下面這個位置,在加載場景時會把玩家放在這個座標上。

image


選項事件

有時候我們需要在場景裏面添加選項。同樣的,我們也要使用OverworldObjTrigger,但組件裏不需要額外的設置,跟上文一樣就行了。

我們需要額外做的有兩項,

首先,你需要在BackpackCanvas里加上OverworldTalkSelect組件。直接加就行。

image

然後,仿照Example-Corridor裏的BackMenu的格式,編寫文本如下。

Select\<image=-1><fx=1>* (This is a select text.)<enter><enter><fx=-1><size=5> </size>< >< >< >< >< >< >   Ok<size=5> </size>< >< >< >< >< >< >< >< >< > Nope<select>Select;

(小知識,如果你看上面這一長串的玩意兒不順眼,你可以適當添加換行。)

請注意這串文本最後有一個Select。這是檢測文本是否爲“選項事件”的關鍵。

現在我們在場景裏面配置好對應的觸發物體,然後測試一下。

image

選項有了,但是你點哪個都沒反應。這是因爲你還沒有寫選擇之後的代碼。

我們打開OverworldTalkSelect腳本,然後翻到Update裏按下Z鍵後的相關代碼。

 if (MainControl.instance.KeyArrowToControl(KeyCode.Z))
            {
                typeWritter.TypeStop();
                switch (select)
                {
                    case 0://選擇了左側選項
                        switch (typeText)
                        {
                            /*
                            打字機示例
                            case "XXX":

                        typeWritter.TypeOpen(MainControl.instance.ScreenMaxToOneSon(MainControl.instance.OverworldControl.owTextsSave, texts[0]), false, 0, 1);
                        break;
                            */

                            case "BackMenu":
                                typeWritter.forceReturn = true;
                                MainControl.instance.OutBlack("Menu", Color.black, true, 0f);
                                AudioController.instance.audioSource.volume = 0;
                                break;
                            
                            default:
                                break;
                        }
                        break;
                    case 1://選擇了右側選項
                        break;
                }
                heart.color = Color.clear;
                canSelect = false;
                return;
            }

我們的目的是,按下左側選項,就會播放一個音效,然後沒了。

這也很簡單。在上面 case "BackMenu":...break;的下面另外寫上下面的代碼。

case "Select":
    AudioController.instance.GetFx(2, MainControl.instance.AudioControl.fxClipBattle);
    break;

這裏的Select就是Select後面的這個Select。

至此,再次運行,看看效果,你應該能聽到選擇左側選項後清脆的“叮”聲。


存檔事件

存檔事件相當容易編寫——因爲我已經提前做好了一個預製體。

在Assets\Prefabs下,你會發現一個叫做Save的預製體,它就是了。把它拖到場景裏面去。

image

注意被我框住的部分,這就是讓這個物體觸發後可以出現保存窗口的選項。

接着我們繼續在對應的文本里面輸入:

Save\<image=-1><fx=0>* (The save filling you<enter>< >< >with <gradient="White to Red - UTC">determination</gradient>.);

效果如下。

image

image

但存儲後,房間的名稱顯示爲null——不然呢?你還沒有輸入房間名稱!

在LanguagePacks\US\UI\Setting.txt裏面另起一行,加上Example-Study\Study Scene;

這下好了!

image

按下V鍵退回菜單,仍然可以看到!

image

Tip:<gradient="White to Red - UTC">使用了TMP的漸變富文本。你也可以在Assets/TextMesh Pro/Examples & Extras/Resources/Color Gradient Presets裏面添加你的自定義漸變顏色並使用富文本調用它。


全部腳本簡介

此處列出Overworld相關的全部腳本,用於修改模板源碼的參考。

image

BackpackBehaviour:Overworld揹包系統的管理器

CameraFollowPlayer:Overworld攝像機跟隨腳本

OverworldObjTrigger:Overworld物體觸發器

OverworldTalkSelect:Overworld選項系統

PlayerBehaviour:地圖內場景裏的玩家控制腳本

SpriteChanger:Overworld對話中更改Sprite

TalkUIPositionChanger:更改對話框UI位置

TriggerChangeLayer:通過Trigger更改SpriteRenderer的層級

TriggerPlayerOut:用於帶動畫器的Overworld物體,在玩家進入/離開時,執行代碼/播放動畫。

戰鬥場景

image

我希望你不是直接跳過來看的,哈。

打開Assets/Scenes/Battle下的Battle場景,讓我們開始吧。


我方回合

上文提到的LanguagePacks文件夾中就有一個單獨的文件夾叫Battle。

裏面有一個文本叫UIBattleText.txt,另外的Turn文件夾裏面存的都是回合裏的敵人對話,這個我們待會再說。

我們打開UIBattleText.txt,翻到下面。

下面這塊是回合的旁白文本,數字序號爲回合數。

Turn\0\* Turn 0.;
Turn\0\* Another version of Turn 0<stop>.<stop>.<stop>.<stop>.<stop>.<stop>.<stop>;
Turn\1\* Welcome to Turn 1<stop>.<stop>.<stop>.<enter>* Of course.;

image

下面這塊是怪物的ACT選項,NPC1 / NPC2是怪物名稱,接着是選項的具體文字,然後是選擇後的內容。

Act\NPC1\Check\* <getEnemiesName> <stop>-<stop> ATK<stop> <getEnemiesATK><stop> DEF<stop> <getEnemiesDEF><stop><enter>* What's this?;
Act\NPC1\Pet\* You pet <getEnemiesName>.<stop><enter>* It makes a sound like "ow.";
Act\NPC1\Glare\* You glare at it fiercely.<stop><enter>* When you gaze into the abyss<stop>.<stop>.<stop>.;
Act\NPC1\Ignore\* You don't look at it.<stop>.<stop>.<stop><enter>< >< >Probably.<stop>.<stop>.;

Act\NPC2\Check\* <getEnemiesName> <stop>-<stop> ATK<stop> <getEnemiesATK><stop> DEF<stop> <getEnemiesDEF><stop><enter>* What's this again?;
Act\NPC2\Compliment\* You compliment <getEnemiesName> on its<enter>< >< >unique appearance.<stop><enter>* <getEnemiesName> looks very puzzled.;
Act\NPC2\Hug\* You pick up <getEnemiesName>.<stop><enter>* <getEnemiesName> seems startled.<stop>.<stop>.<stop><passText>* But it feels happy.;
Act\NPC2\Grin\* You let out an evil grin.<stop><enter>* <getEnemiesName> can't figure out<enter>< >< >what you're doing.;

image

image

怪物名稱通過儲存在BattleControl中的預製體檢測。

image

image

下面這塊是怪物的Mercy選項,和ACT類似。

Mercy\NPC1\Spare\Null;
Mercy\NPC1\Flee\* You can't run away.;
Mercy\NPC2\Spare\Null;
Mercy\NPC2\Flee\* You can't run away.;
Mercy\NPC2\Let It Go\* You wish.;

image

對照着修改就好。

怎麼讓這些選項觸發後執行代碼呢? 打開SelectUIController,切到440行左右的位置,其中內容如下。

                        switch (selectSon)//在這裏寫ACT的相關觸發代碼
                            {
                                case 0://怪物0
                                    switch (selectGrandSon)//選項
                                    {
                                        case 1:

                                            break;
                                        case 2:

                                            Debug.Log(1);
                                            AudioController.instance.GetFx(3, MainControl.instance.AudioControl.fxClipBattle);

                                            break;
                                        case 3:

                                            break;
                                        case 4:

                                            break;
                                    }
                                    break;
                                case 1://怪物1
                                    switch (selectGrandSon)//選項
                                    {
                                        case 1:

                                            break;
                                        case 2:

                                            break;
                                        case 3:

                                            break;
                                        case 4:

                                            break;
                                    }
                                    break;
                                case 2://怪物2
                                    switch (selectGrandSon)//選項
                                    {
                                        case 1:

                                            break;
                                        case 2:

                                            break;
                                        case 3:

                                            break;
                                        case 4:

                                            break;
                                    }
                                    break;
                            }

在對應位置加代碼就行了。例如代碼裏面寫的那個GetFx,它會在選擇怪物0的第1個選項時會播放一個音效。


敵方回合

在戰鬥場景的MainControlSummon上額外掛着一個TurnController,這就是回合的管理器,我們在這裏面寫敵方回合的彈幕。

我們在裏面會看到一個IEnumerator叫_TurnExecute。你會注意到這個IEnumerator接着一個<float>。這是因爲我在項目中使用了More Effective Coroutines [FREE],以此使協程可以暫停。你可以在這裏查看它的相關文檔。

模板內已經有一個示例回合了,我在這就簡單廢話幾句吧。

獲取彈幕需要使用 objectPools[0]這個彈幕的內存池。

var obj = objectPools[0].GetFromPool().GetComponent<BulletController>();

以此獲得一個彈幕。

然後獲得了彈幕之後還沒完,你需要初始化它。

初始化方法在模板的預設中如下。

    /// <param name="name">設置彈幕的Obj的名稱,以便查找。</param>
    /// <param name="typeName">設置彈幕的種類名稱,如果種類名稱與當前的彈幕一致,則保留原有的碰撞相關參數,反之清空。</param>
    /// <param name="layer">玩家爲100,戰鬥框邊緣爲50。可參考。</param>
    /// <param name="sprite">一般在Resources內導入。</param>
    /// <param name="size">設置判定箱大小,可設定多個List,但多數情況下需要避免其重疊。(NoFollow情況下設爲(0,0),會自動與sprite大小同步)</param>
    /// <param name="offset">設定判定箱偏移,List大小必須與sizes相等。</param>
    /// <param name="hit">設定碰撞箱傷害,List大小必須與sizes相等。</param>
    /// <param name="followMode">設置碰撞箱跟隨SpriteRenderer縮放的模式。</param>
    /// <param name="startMask">設置Sprite遮罩模式。</param>
    /// <param name="bulletColor">設置彈幕屬性顏色數據</param>
    /// <param name="startPosition">設置起始位置(相對座標)。</param>
    /// <param name="startRotation">設置旋轉角度,一般只需更改Z軸。</param>
    /// <param name="startScale">若彈幕不需拉伸,StartScale一般設置(1,1,1)。檢測到Z爲0時會歸位到(1,1,1)。</param>
    public void SetBullet(
       string name,
       string typeName,
       int layer,
       Sprite sprite,
       Vector2 size,
       int hit,
       Vector2 offset,
       Vector3 startPosition = new Vector3(),
       BattleControl.BulletColor bulletColor = BattleControl.BulletColor.white,
       SpriteMaskInteraction startMask = SpriteMaskInteraction.None,
       Vector3 startRotation = new Vector3(),
       Vector3 startScale = new Vector3(),
       FollowMode followMode = FollowMode.NoFollow
       )
{
    ...
}

初始化的時候就照着上面的註釋填寫就行了,實際上在你輸入代碼的時候,如果你的IDE是Visual studio,它會給你提示對應位置要輸入什麼東西。

                obj.SetBullet(
                    "DemoBullet",
                    "CupCake",
                    40,
                    Resources.Load<Sprite>("Sprites/Bullet/CupCake"),
                    Vector2.zero,
                    1,
                    Vector2.zero,
                    new Vector3(0, -3.35f),
                    BattleControl.BulletColor.white,
                    SpriteMaskInteraction.VisibleInsideMask
                    );

上面這個初始化的方法用人話翻譯就是: 初始化一個彈幕,它的物體名稱叫做DemoBullet,種類叫做CupCake,放在圖層40,加載了"Sprites/Bullet/CupCake"位置的圖片作爲顯示圖,判定箱與sprite大小一致,傷害爲1,判定箱沒有偏移,起始座標爲(0, -3.35),彈幕屬性顏色爲白色,顯示在戰鬥框遮罩內。

然後你想怎麼耍它都可以!我在場景中使用了DoTween組件來控制彈幕(和設置等等地方)的動畫,建議你去他們官網看看文檔。

類似這樣的代碼就能讓它在兩秒內邊飛上天飛下來邊轉360度,簡簡單單,前提是你得搞明白DoTween組件,咱全靠它呢。

obj.transform.DOMoveY(0, 1).SetEase(Ease.OutSine).SetLoops(2, LoopType.Yoyo);
obj.transform.DORotate(new Vector3(0, 0, 360), 2,RotateMode.WorldAxisAdd).SetEase(Ease.InOutSine);

順帶一提,如果你沒找到上述這些示例代碼,它們都放在了IEnumerator _TurnNest(Nest nest)內。

這個協程是用來做嵌套彈幕用的,但如果你樂意,你可以在_TurnExecute裏面搞個case 100000然後寫嵌套彈幕,效果一樣(但這不是有病麼)。

最後,一定要記得及時回收彈幕!

objectPools[0].ReturnPool(obj.gameObject);

3D場景佈置

你應該很早就注意到戰鬥場景裏面有兩個攝像機。

是的,得益於Unity(理所應當的)支持3D的(這一從它誕生以來就有的)優良傳統,我們可以在場景裏面弄一些3D元素出來。

就像我佈置的這個場景一樣,我覺得佈置場景也沒啥好說的,學過Unity基礎就都會搞。

咱就簡單提一嘴這倆攝像機的區別好了。

image

在戰鬥場景內你需要注意,“Main Camera”默認不是主攝像機,Unity對主攝像機的判定是根據它的標籤,而很顯然,“Main Camera”的標籤是Untagged。

image

而3D Camera纔是真的主攝像機,這點需要注意。

image

兩個攝像機的拍攝內容在模板內顯而易見。

這是“Main Camera”。

image

這是3D Camera。

image

在3D Camera的範圍內隨意佈置你的場景吧。

Tips:理論上來說,你完全可以在我模板的基礎上,試着把Overworld場景,或者整個模板……搞成3D的……或者2.5D。如果你有這能力,蛤?


多邊形戰鬥框

戰鬥框就是戰鬥場景內的MainFrame,你會發現它下面有很多子物體。四個Point便是繪製戰鬥框的四個點,而Back繪製戰鬥框的黑色部分。

image

我們移動其中一個點,戰鬥框便會進行變形。

image

戰鬥框的邊框使用LineRenderer繪製,而戰鬥框的黑色部分(以及彈幕的遮罩部分)都使用了shader繪製。

我們如果想要一個加更多點的戰鬥框,如下操作。

以五邊形(五個頂點)的戰鬥框爲例。

首先,在MainFrame的DrawFrameController上,更改頂點數爲5。

image

然後加一個子物體Point4,你可以直接複製上面的Point。

image

image

然後,還記得我們說過戰鬥框的黑色部分以及彈幕的遮罩部分使用了Shader繪製麼?因此我們需要小小的修改一下shader,這並不難,照着下文做就行。

在Back中點編輯,以此編輯DrawPolygon這個Shader,或者在Assets/Shaders/Sprites裏找到DrawPolygon雙擊打開。

image

image

雙擊DrawPolygonFull打開這個Sub-graph。

image

這個Sub-graph裏面放着四個DrawPolygon——這同樣是一個Sub-graph。不管怎樣,讓我們觀察一下這四個DrawPolygon傳入數據的規律。

顯而易見。我們從上往下數,傳入的點的編號爲:

  • 3 , 0;
  • 0 , 1;
  • 1 , 2;
  • 2 , 3.

我們以此類推就好了,首先在左側加一個Point4,類型爲Vector2。

image

下面加上這個。

image

最上面改成這個。

image

這樣,傳入的編號從上到下就是

  • 4 , 0;
  • 0 , 1;
  • 1 , 2;
  • 2 , 3;
  • 3 , 4.

但新加的這個3 , 4的這個DrawPolygon沒有地方連了,我們看右邊的這個AddSuperposition,它同樣是一個Sub-graph。雙擊點開它。

image

image

裏面的結構顯而易見,就像AddSuperposition的含義一樣,把一堆Add疊加在了一起而已。

再疊加一層就好了。

image

保存,切回DrawPolygonFull,把該連上的連上。

image

保存,回到DrawPolygon,然後老樣子,加一個Point4,然後連上,完事兒!

image

記得保存!

但這還沒完,因爲你只改好了戰鬥框的Shader,彈幕遮罩的Shader還沒改。我們打開SpriteBattleMask這個shader。它位於Assets/Shaders/Sprites下。

同樣的,加一個點,連上,完事。

運行遊戲看看吧。

image

可以看到,異形戰鬥框和彈幕遮罩都正常運行。


全部腳本簡介

此處列出戰鬥內相關的全部腳本,用於修改模板源碼的參考。

image

BattlePlayerController:控制戰鬥內玩家的相關屬性

CameraShake:攝像機搖晃腳本

DialogBubbleBehaviour:敵人對話氣泡控制

EnemiesController:怪物控制腳本

BoardController:擋板控制器

BulletController:彈幕控制器

EnemiesHpLineController:敵人血條控制器

BulletShaderController:彈幕Shader控制器(包括彈幕遮罩)

GameoverController:Gameover控制器

ItemSelectController:物品選擇控制器

SelectUIController: 戰鬥內UI控制器,同時負責玩家回合的控制

SpriteSplitController:啓用該腳本後怪物立馬變成灰

SpriteSplitFly:配合SpriteSplitController。控制單個像素的移動軌跡

TargetController:控制Target(fight裏面的靶子)

TurnController:回合控制器+彈幕對象池

雜項場景

故事場景(Story)

image

這個場景對應原作剛啓動遊戲時看到的講故事場景。

場景佈置非常簡單,而主要控制這個場景內容的是Story物體下的StorySceneController。

image

編輯這個場景的方式如下:

在Pics裏面設置顯示的背景圖。

image

在語言包的Overworld/Story.txt裏面進行編輯。默認文本如下:

Text\<changeX><fx=1>Because it's there.
<passText=2.5><fx=-1> 
<storyFade=6> 
<passText=1><changeX><fx=1>Indeed, it is.
<passText=2.5>  
<storyExit>
;

每一句前的使打字機無法用X鍵跳過。

<passText=x>x秒後跳字,若要之後沒有任何文字,輸< >。 <storyFade=x>漸出然後漸入到第X張圖。具體ID內置在遊戲內。負數爲漸入到沒有圖 <storyMove=(x,y,z)>移動圖片到某位置,z軸填移動時間,負數會取絕對值。 開/關背景遮罩(用於背景圖移動時只顯示一個區域) 漸出結束並退出到Start。


標題場景(Start)


image

對應原作的標題。

這個場景內默認包含一個標題和一個提示文本。

image

你如果想要它只顯示標題,直接改代碼就行,裏面寫的很簡單我覺得我不用多講。

另外,雖然我在場景裏面使用了圖片顯示標題(因爲我的標題圖片很顯然的經過處理,並不是直接輸入的文本),如果你想使用類似原作標題那樣的標題,你可以直接使用模板內包含的MONSTERFRIENDBACK與MONSTERFRIENDFORE。(UT-MSB,UT-MSF)

如下。

image

image

image

image

菜單場景(Menu)


image

該場景由MenuController控制。 相關文本在語言包的同名txt中更改。

重命名場景(Rename)

@HO~QDSDT2XGH(({X1F_B5J

該場景由RenameController控制。 相關文本在語言包的同名txt中更改。

全局設置

如果你對修改全局框架的需求不大,或者單純就是看不下去下文,可以以後再看,下文屬於比較進階的內容。

概述

我們之前提到了Assets/Scripts內的文件夾,也許你注意到了,Control文件夾的描述很特殊。是的它特別長

我們提到它裏面的腳本是“用以創建Resources下的數據存儲文件”的,如果你不明白這是什麼意思,那麼現在點開Assets/Resources文件夾,你會發現裏面放着挺多東西的。就像這樣。

image

我們先不管紅框框裏面的內容,看看這些綠框框框住的這些個名字都是“XXXXControl”的文件吧,隨便點開一個,然後你就會看到右邊的檢查器會顯示出來一些數據。

  • AudioControl存儲音效相關的數據。
  • BattleControl存儲戰鬥內相關的數據。
  • ItemControl存儲玩家的物品數據。
  • OverworldControl存儲一些通用數據(大多數用於Overworld)
  • PlayerControl存儲玩家的數據(同時它也被存檔系統所讀存)

同樣的,有個印象就行,我們會在對應的地方詳細講解它們。


現在,我們要開始講解整個項目中最重要的腳本,MainControl

MainControl是幹啥的?上面的這些xxxControl,都需要通過MainControl來獲取,並且MainControl還負責對一些其餘數據和語言包的導入。

讓我們去看看場景內,然後你會發現,每一個場景裏面都有一個預製體叫做MainControlSummon

image

正如其名,它是生成總控的腳本,此外他還會生成設置系統和音頻系統,這都是我們這部分要講的內容。

image

它生成的總控(MainControl)、設置系統(Canvas)與音頻系統(BGM Source)都不會在場景切換時銷燬。

以下是MainControl的使用方式之一。此代碼取自PlayerBehaviour.cs,用於存檔時回血。

你可以看到我們通過MainControl獲取到了PlayerControl的血量信息。

if (MainControl.instance.PlayerControl.hp < MainControl.instance.PlayerControl.hpMax)
    MainControl.instance.PlayerControl.hp = MainControl.instance.PlayerControl.hpMax;

總控還有一個作用是存儲一些會經常調用到的方法,如下。

該方法用於切換場景時進行黑場過渡,一般來說模板內都會使用這個方法進行場景切換。(看不懂沒關係 因爲我估計你也不會去怎麼改它)

  public void OutBlack(string scene, Color color, bool banMusic = false, float time = 0.5f, bool Async = true)
    {
        blacking = true;
        if (banMusic)
        {
            AudioSource bgm = AudioController.instance.transform.GetComponent<AudioSource>();
            if (time > 0)
                DOTween.To(() => bgm.volume, x => bgm.volume = x, 0, time).SetEase(Ease.Linear);
            else if(time == 0)
                bgm.volume = 0;
            else
                DOTween.To(() => bgm.volume, x => bgm.volume = x, 0, Mathf.Abs(time)).SetEase(Ease.Linear);
        }
        OverworldControl.pause = true;
        if (time > 0) 
        { 
            inOutBlack.DOColor(color, time).SetEase(Ease.Linear).OnKill(() => SwitchScene(scene));
            if (!OverworldControl.hdResolution)
                CanvasController.instance.frame.color = new Color(1, 1, 1, 0);
        }
        else if (time == 0)
        {
            inOutBlack.color = color;
            SwitchScene(scene, Async);
        }
        else
        {
            time = Mathf.Abs(time);
            inOutBlack.color = color;
            inOutBlack.DOColor(color, time).SetEase(Ease.Linear).OnKill(() => SwitchScene(scene));
            if (!OverworldControl.hdResolution)
                CanvasController.instance.frame.color = new Color(1, 1, 1, 0);
        }
    }

總控裏面的大多數方法都有註釋說明用法,你也可以自己寫。


打字機

把TypeWritter腳本掛在場景的某個物體上用於打字。 打字機會檢測形如<xxx>的富文本。 示例:

typeWritter.TypeOpen("<color=red>text123</color>", false, 0, 0, textUI);

image


後處理

image

在原先Unity自帶後處理的基礎上,加上了自定義後處理的戰鬥場景。

我知道你在想什麼。停。


模板內有四個新增的後處理。

image

Chromatic Aberration Component是一個簡單的滾動色差分離效果。

image

CRT Screen Component是一種類似於老式電視機的效果。

image

Glitch Art Component包含四種用於實現差錯效果的選項,參考了KinoGlitch

image

Stretch Post Component會拉伸遊戲內的顯示區域。我本想以這種方式製作16:9分辨率適配,但我發現拉伸後的顯示區域看上去有點模糊。後來我用Viewport矩形解決了這個問題。簡而言之,這個效果沒啥用。

image

這幾個後處理的原shader都放在Assets/Shaders/PostProcessing下。

編寫你自己的後處理特效也很簡單,在Assets/Scripts/Volume下存放一些腳本。

它們都是以xxxComponent,xxxRendererFeature的格式命名的。

xxxComponent負責把後處理添加在顯示列表內。我們以Chromatic Aberration爲例,我們在Full Chromatic Aberration.shadergraph中存儲了這些數據。

image

特別注意的是這裏要使用_MainTex。

image

而相對應的,我們把這些數據都要寫在ChromaticAberrationComponent內,格式如下,你可以發現裏面的變量都和shadergraph中的數據相對應。(除了MainTex,這個是不需要寫的)

[VolumeComponentMenuForRenderPipeline("Custom/Chromatic Aberration",typeof(UniversalRenderPipeline))]
public class ChromaticAberrationComponent : VolumeComponent,IPostProcessComponent
{
    public BoolParameter isShow = new BoolParameter(false, true);
    [Header("Settings")]
    public FloatParameter offset = new FloatParameter(0.02f, true);
    public FloatParameter speed = new FloatParameter(10, true);
    public FloatParameter height = new FloatParameter(0.15f, true);
    public BoolParameter onlyOri = new BoolParameter(false, true);

    public bool IsActive()
    {
        return true;
    }

    public bool IsTileCompatible()
    {
        return false;
    }

}

當你在編寫你的後處理腳本時,一種非常簡單的操作方式就是,複製原有的xxxComponent,xxxRendererFeature——這麼一套腳本,然後打開,ctrl+f然後替換掉該腳本中所有的“ChromaticAberration”(或者類似的東西),改成你自己的。

對於xxxRendererFeature同理,當你改完你的xxxComponent,在裏面加上了該加的變量之後,在你新複製的xxxRendererFeature內直接替換掉原有的那些字符就可以了。

唯一需要注意的是該腳本中的Render方法內的數據需要額外修改,我們這裏仍然以ChromaticAberration爲例,你會發現在Render方法內有幾行賦值的操作。

把這些賦值的代碼改成你的變量就可以了。

        mat.SetFloat("_Offset", chromaticAberrationVolume.offset.value);
        mat.SetFloat("_Speed", chromaticAberrationVolume.speed.value);
        mat.SetFloat("_Height", chromaticAberrationVolume.height.value);
        mat.SetFloat("_OnlyOri", System.Convert.ToInt32(chromaticAberrationVolume.onlyOri.value));

在改完腳本後,我們切回Assets根目錄,有一個叫Universal Render Pipeline Asset_Renderer的文件。

image

你會發現之前內置的後處理都擺在這裏,同樣的,加上你的後處理就大功告成了。


物品系統

玩家的物品都存儲在PlayerControl下的My Items裏,你會發現這是一個int類型的List,裏面寫的都是數字,每一個數字編號對應一個物品。

image

其中,0爲沒有物品,110000的編號內都是食用品,1000120000都是武器,20001~30000都是防具。

這些物品的信息(運行時)儲存在ItemControl內。

image

在食品中,第0個是食品的數據名稱,第1個是食用完它之後會變成另一種什麼食物(類似於原作的棒冰),第2個就是回覆血量(可以是負數),以此類推。

數據名稱是爲了與語言包顯示的名稱相區別而存在的,例如你想加一個食物叫“巧克力”,它在不同語言有不同的叫法,但是數據名稱只有一個(例如chocolate,或者choco,或者……cc?)。

而武器和盔甲更簡單一些,第0個是數據名稱,第1個是atk/def,以此類推。

但你並不能在這裏進行編輯,你應該能看到ItemControl最上面存儲着一個txt文本“ItemData”。

image

我們打開這個txt文本。

baaaaaaaaaaaaaaaa\Foods\@bang\10;
bang\Foods\@Null\5;

pia1\Arms\1\null;
pia2\Arms\999\null;


tatata1\Armors\123\null;
tatata2\Armors\456\null;

格式一目瞭然,你便可以在這添加新的物品了。

當然,物品加入後需要和語言包匹配。在語言包下的UI/ItemText內進行編輯。

/* I T E M */
Item\baaaaaaaaaaaaaaaa\Two servings\<autoFoodFull><enter>* Another test, ahhhhh<stop>.<stop>.<stop>.<passText>* But it went well.\<autoCheckFood><enter>* An indescribable gadget<stop>.<stop>.<stop>.<passText><color=yellow>* Oh, my God, the test was successful!!!\<autoLoseFood>;
Item\bang\A serving of food\* You tasted the rest of this stuff.<stop><enter><autoFood>\<autoCheckFood><enter>* Still nameless<stop>.<stop>.<stop>.\<autoLoseFood>;

Item\pia1\Broken t.knife\<autoArm>\<autoCheckArm>\<autoLoseArm>;
Item\pia2\<color=yellow>Golden Sword PLUS</color>\<autoArm><enter>* Golden Legend This is.\<autoCheckArm><enter>* Um... wtf is that(\* <color=red>You just throw it away?<stop>?<stop>?</color>;


Item\tatata1\T.P.S\<autoArmor>\<autoCheckArmor>\<autoLoseArmor>;
Item\tatata2\Wearable sth\<autoArmor>\<autoCheckArmor>\<autoLoseArmor>;

音頻系統

AudioController是控制音頻系統的腳本(注意不要和AudioControl混淆),它放在MainControl生成的BGM Source上。

image

使用形如AudioController.instance.GetFx(0, MainControl.instance.AudioControl.fxClipBattle);的代碼來播放音頻。

  public void GetFx(int fxNum, List<AudioClip> list, float volume = 0.5f, float pitch = 1, AudioMixerGroup audioMixerGroup = null)
    {
        if (fxNum < 0)
            return;
        GameObject fx = GetFromPool();
        fx.GetComponent<AudioSource>().volume = volume;
        fx.GetComponent<AudioSource>().pitch = pitch;
        fx.GetComponent<AudioSource>().outputAudioMixerGroup = audioMixerGroup;

        fx.GetComponent<AudioPlayer>().Playing(list[fxNum]);
    }

調用的音頻存儲在AudioControl內。

image


設置系統

image

CanvasController負責包含設置系統在內的對UI的控制,包含設置系統的相關內容。


存檔系統


SaveController負責將PlayerControl中的數據存儲爲json與讀取數據。

存檔儲存在根目錄下的Data文件夾,若文件夾不存在會創建一個。

模板內同時使用了PlayerPrefs存儲設置內相關的一些數據。如下。

PlayerPrefs.SetInt("languagePack", MainControl.instance.languagePack);
PlayerPrefs.SetInt("dataNum", MainControl.instance.dataNum);
PlayerPrefs.SetInt("hdResolution", Convert.ToInt32(MainControl.instance.OverworldControl.hdResolution));
PlayerPrefs.SetInt("noSFX", Convert.ToInt32(MainControl.instance.OverworldControl.noSFX));
PlayerPrefs.SetInt("vsyncMode", Convert.ToInt32(MainControl.instance.OverworldControl.vsyncMode));

多語言

語言包在MainControl內進行加載。

內置語言包儲存在Assets\Resources\TextAssets下。

外置語言包儲存在Assets\LanguagePacks下。

導出時的注意事項

  • 導出正式版遊戲時記得不要勾選開發構建,反之可以勾上,導出時可以查看報錯。

image

  • 在導出完畢之後,要把工程根目錄下的Assets\LanguagePacks整個文件夾複製,然後粘貼在導出後的Undertale Changer Template_Data文件夾內,不然遊戲會報錯。

image

享受你的遊戲吧!

尾聲

如果你對這個教程有任何建議和意見,可以聯繫我進行修改——你自己改也行。

感謝你看到這裏!希望你能做出一個完美的同人遊戲!