-
Notifications
You must be signed in to change notification settings - Fork 5
Tutorial‐zh_HK
我不知道你是懷着怎麼樣的一種心態,發現了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引擎的學習教程,網上有又新又好的,去找就是了。
在你第一次打開模板的時候,它應該打開了一個空的場景。大概是長這樣。(如果不是也沒關係,待會的步驟是一樣的)
由於你第一次使用模板,所以我先教你運行一下整個模板,以此看看模板內大概的樣子。
首先找到模板下面的Scenes文件夾,這裏面存放着所有模板內的場景。
然後順着Assets/Scenes/Menu/Story/Story.unity路徑,打開Story場景,這就是原版的講故事場景。
點播放,然後按照你玩原作的方式,稍微玩一下吧。
等你玩完一遍之後,我們就可以真正地開始了。
模板內的腳本有註釋,可以參考註釋進行編寫,此處給予模板大體介紹。
我在這裏簡單寫一下此模板的編寫思路。
Undertale原作,在我的模板內可認爲由以下兩部分組成:
戰鬥內場景,戰鬥外場景。
很簡單,就像它們的字面意思一樣。
而戰鬥內場景也可以細分爲地圖內場景(Overworld)和雜項場景(故事、標題、菜單、重命名等)。
- 例如,地圖內場景有玩家的腳本控制其移動,故事場景有腳本控制幻燈片的淡入淡出,戰鬥內場景也有腳本組成戰鬥系統。 也有一部分腳本是全部場景或大部分場景通用的。
每一類場景中有支撐它們運作的腳本。
- 例如,打字機腳本,以及總控腳本,等等。
在模板的Assets/Scripts內存放着模板內使用的代碼,你在這裏可以看到以下幾個文件夾。
- Battle文件夾——僅在戰鬥內場景使用的腳本。
- Control文件夾——其中的腳本均繼承自ScriptableObject類,用以創建Resources下的數據存儲文件。
- Debug文件夾——正如其名,是Debug所用的腳本。
- Default文件夾——通用/未分類的腳本。
- Obsolete文件夾——棄用的腳本,多爲參考他人使用的腳本。
- Overworld文件夾——僅在戰鬥外場景使用的腳本。
- Volume文件夾——自定義後處理相關的腳本。
目前,你只需要對這些個文件夾的分類有個大致印象就好。詳細內容會在下文進行講述。
這是一個地圖內場景,當然了。
要做一個新的地圖,我們需要新建一個場景。
按下Ctrl + N,在新建場景的頁面,你會看到一個叫Overworld scene的場景模板:
這是這個場景模板的存儲位置(你知道就行 別打開它 除非你知道你在做什麼):
新建場景後,就會出現這麼一個啥都沒有的新的場景。
名爲Grid的Obj就是瓦片地圖,你可以隨心所欲的開始鋪瓦片,但要注意,瓦片的排序圖層一般來說就是“Tilemap”,如圖。
隨便擺了一下。
適當加一點光源和後處理。(沒錯直接搬的Ruins示例場景)
然後可以存儲一下,改個令你印象深刻的名字,放在Scene文件夾裏面,如圖。
然後給你的瓦片地圖加一個碰撞體。我知道可以用Tilemap Collider 2D,但我還是比較習慣用Edge Collider 2D。所以按你喜歡的方式加個碰撞——
但這時,如果你運行了場景,它會報一個錯,如下。
我們順着報錯去找出錯的代碼,就會找到下面這玩意兒。
...
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文件。
咱也加上咱的場景就是了。
一些格式和特殊富文本的寫法可以打開Example-Corridor看裏面的註釋。
但具體的事件呢,咱待會再寫。現在就放一個空的txt在這就得了。
現在進場景,我們發現攝像機在我們走到左邊的時候卡住不動,這是因爲我們還沒有設置攝像機跟隨。
MainCamera裏面的CameraFollowPlayer就是設置攝像機跟隨用的,可以把frisk扔到地圖的最左邊或者最右邊,然後改這個腳本的數值測試攝像機鎖定的範圍。
當然,也可以吧Limit的√去掉,這樣就沒跟隨限制了。
如果不需要跟隨,把這個腳本去掉就行。
設置好的效果:
然後這個場景就有點樣子了,你可以隨便亂跑然後不會飛出去之類的。不過還沒聲。我們在MainControlSummon中解決這個問題。
把BGM拖上去,然後照着上面的信息寫就完事了,一般來說,你只需要把BGM加上,別的不用管。
搞定這個之後,這個場景更好了一點。不過,它現在沒法進行任何交互,但不急,我們馬上就要提到它了。
事件是幹什麼用的?簡單來說,他就是讓你在遊戲內查看某個物體,或者在地圖邊界進入下一個場景用的。
書接上回,我們的場景現在什麼也調查不了,也沒有離開場景的方法。
添加一個事件很簡單。我們需要在場景裏面加一個物體,身上需要有2d碰撞體,就像這樣。
我的建議是可以把它放在Grid/Detectables下面,方便管理。(Detectables是一個新建座標重置過的的空物體)
我們在上面添加OverworldObjTrigger,然後你會看到一大堆亂七八糟的選項,相關的作用上面的註釋有寫因此不再贅述。
我們只需要在調查的時候能彈出一些字來就好。例如什麼“* 這是一個破牆”之類的。但首先,咱還沒有把調查需要的文本寫進去。
還記得之前提過的內置語言包路徑(Assets/Resources/TextAssets/LanguagePacks)麼?找到你之前創建的txt文件。
我們到裏面去進行編輯。你可以參考Example-Corridor內的註釋。
CrackedWall\<image=-1><fx=0>* This is a cracked wall.;
如果你懶得看註釋,那我簡單解釋一下。斜槓前面的是OverworldObjTrigger的檢測文本,後面就是具體的文本內容。
<image=-1>是打字的頭像(-1是沒有頭像)。頭像的精靈存儲在BackpackCanvas裏面的Sprite Changer裏面。
<fx=0>是打字的音效。存儲在Assets/Resources/AudioControl的FxClipType內。
別忘了在OverworldObjTrigger裏面寫上對應的檢測文本。
我們走到牆那裏去調查,然後字就出來嘞!
好了,接下來,我們需要一個離開場景的物體。
我們微調一下場景,弄一個出口。
這裏要設置爲觸發器。
在OverworldObjTrigger裏面設置如下。
勾選ChangeScene後,檢測到玩家就會切換場景。
勾選BanMusic後,音樂會漸出。
SceneName就是切換場景的目的地。
NewPlayerPos便是切換到新場景後玩家的座標。
現在讓我們進遊戲測試一下!
可以看到我們成功來到了另一個場景。
我們的事件就講到這兒了,至於OverworldObjTrigger裏面的其他選項,例如攝像機移動等,可以去看看Example-Corridor裏面怎麼設置的。
Tips:如果你這時停止運行,然後再運行,你會發現玩家的位置移動了。
這是因爲玩家在切換場景時,目的地的座標會存儲在Assets/Resources/OverworldControl的下面這個位置,在加載場景時會把玩家放在這個座標上。
有時候我們需要在場景裏面添加選項。同樣的,我們也要使用OverworldObjTrigger,但組件裏不需要額外的設置,跟上文一樣就行了。
我們需要額外做的有兩項,
首先,你需要在BackpackCanvas里加上OverworldTalkSelect組件。直接加就行。
然後,仿照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。這是檢測文本是否爲“選項事件”的關鍵。
現在我們在場景裏面配置好對應的觸發物體,然後測試一下。
選項有了,但是你點哪個都沒反應。這是因爲你還沒有寫選擇之後的代碼。
我們打開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的預製體,它就是了。把它拖到場景裏面去。
注意被我框住的部分,這就是讓這個物體觸發後可以出現保存窗口的選項。
接着我們繼續在對應的文本里面輸入:
Save\<image=-1><fx=0>* (The save filling you<enter>< >< >with <gradient="White to Red - UTC">determination</gradient>.);
效果如下。
但存儲後,房間的名稱顯示爲null——不然呢?你還沒有輸入房間名稱!
在LanguagePacks\US\UI\Setting.txt裏面另起一行,加上Example-Study\Study Scene;
這下好了!
按下V鍵退回菜單,仍然可以看到!
Tip:<gradient="White to Red - UTC">使用了TMP的漸變富文本。你也可以在Assets/TextMesh Pro/Examples & Extras/Resources/Color Gradient Presets裏面添加你的自定義漸變顏色並使用富文本調用它。
此處列出Overworld相關的全部腳本,用於修改模板源碼的參考。
BackpackBehaviour:Overworld揹包系統的管理器
CameraFollowPlayer:Overworld攝像機跟隨腳本
OverworldObjTrigger:Overworld物體觸發器
OverworldTalkSelect:Overworld選項系統
PlayerBehaviour:地圖內場景裏的玩家控制腳本
SpriteChanger:Overworld對話中更改Sprite
TalkUIPositionChanger:更改對話框UI位置
TriggerChangeLayer:通過Trigger更改SpriteRenderer的層級
TriggerPlayerOut:用於帶動畫器的Overworld物體,在玩家進入/離開時,執行代碼/播放動畫。
我希望你不是直接跳過來看的,哈。
打開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.;
下面這塊是怪物的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.;
怪物名稱通過儲存在BattleControl中的預製體檢測。
下面這塊是怪物的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.;
對照着修改就好。
怎麼讓這些選項觸發後執行代碼呢? 打開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);
你應該很早就注意到戰鬥場景裏面有兩個攝像機。
是的,得益於Unity(理所應當的)支持3D的(這一從它誕生以來就有的)優良傳統,我們可以在場景裏面弄一些3D元素出來。
就像我佈置的這個場景一樣,我覺得佈置場景也沒啥好說的,學過Unity基礎就都會搞。
咱就簡單提一嘴這倆攝像機的區別好了。
在戰鬥場景內你需要注意,“Main Camera”默認不是主攝像機,Unity對主攝像機的判定是根據它的標籤,而很顯然,“Main Camera”的標籤是Untagged。
而3D Camera纔是真的主攝像機,這點需要注意。
兩個攝像機的拍攝內容在模板內顯而易見。
這是“Main Camera”。
這是3D Camera。
在3D Camera的範圍內隨意佈置你的場景吧。
Tips:理論上來說,你完全可以在我模板的基礎上,試着把Overworld場景,或者整個模板……搞成3D的……或者2.5D。如果你有這能力,蛤?
戰鬥框就是戰鬥場景內的MainFrame,你會發現它下面有很多子物體。四個Point便是繪製戰鬥框的四個點,而Back繪製戰鬥框的黑色部分。
我們移動其中一個點,戰鬥框便會進行變形。
戰鬥框的邊框使用LineRenderer繪製,而戰鬥框的黑色部分(以及彈幕的遮罩部分)都使用了shader繪製。
我們如果想要一個加更多點的戰鬥框,如下操作。
以五邊形(五個頂點)的戰鬥框爲例。
首先,在MainFrame的DrawFrameController上,更改頂點數爲5。
然後加一個子物體Point4,你可以直接複製上面的Point。
然後,還記得我們說過戰鬥框的黑色部分以及彈幕的遮罩部分使用了Shader繪製麼?因此我們需要小小的修改一下shader,這並不難,照着下文做就行。
在Back中點編輯,以此編輯DrawPolygon這個Shader,或者在Assets/Shaders/Sprites裏找到DrawPolygon雙擊打開。
雙擊DrawPolygonFull打開這個Sub-graph。
這個Sub-graph裏面放着四個DrawPolygon——這同樣是一個Sub-graph。不管怎樣,讓我們觀察一下這四個DrawPolygon傳入數據的規律。
顯而易見。我們從上往下數,傳入的點的編號爲:
- 3 , 0;
- 0 , 1;
- 1 , 2;
- 2 , 3.
我們以此類推就好了,首先在左側加一個Point4,類型爲Vector2。
下面加上這個。
最上面改成這個。
這樣,傳入的編號從上到下就是
- 4 , 0;
- 0 , 1;
- 1 , 2;
- 2 , 3;
- 3 , 4.
但新加的這個3 , 4的這個DrawPolygon沒有地方連了,我們看右邊的這個AddSuperposition,它同樣是一個Sub-graph。雙擊點開它。
裏面的結構顯而易見,就像AddSuperposition的含義一樣,把一堆Add疊加在了一起而已。
再疊加一層就好了。
保存,切回DrawPolygonFull,把該連上的連上。
保存,回到DrawPolygon,然後老樣子,加一個Point4,然後連上,完事兒!
記得保存!
但這還沒完,因爲你只改好了戰鬥框的Shader,彈幕遮罩的Shader還沒改。我們打開SpriteBattleMask這個shader。它位於Assets/Shaders/Sprites下。
同樣的,加一個點,連上,完事。
運行遊戲看看吧。
可以看到,異形戰鬥框和彈幕遮罩都正常運行。
此處列出戰鬥內相關的全部腳本,用於修改模板源碼的參考。
BattlePlayerController:控制戰鬥內玩家的相關屬性
CameraShake:攝像機搖晃腳本
DialogBubbleBehaviour:敵人對話氣泡控制
EnemiesController:怪物控制腳本
BoardController:擋板控制器
BulletController:彈幕控制器
EnemiesHpLineController:敵人血條控制器
BulletShaderController:彈幕Shader控制器(包括彈幕遮罩)
GameoverController:Gameover控制器
ItemSelectController:物品選擇控制器
SelectUIController: 戰鬥內UI控制器,同時負責玩家回合的控制
SpriteSplitController:啓用該腳本後怪物立馬變成灰
SpriteSplitFly:配合SpriteSplitController。控制單個像素的移動軌跡
TargetController:控制Target(fight裏面的靶子)
TurnController:回合控制器+彈幕對象池
這個場景對應原作剛啓動遊戲時看到的講故事場景。
場景佈置非常簡單,而主要控制這個場景內容的是Story物體下的StorySceneController。
編輯這個場景的方式如下:
在Pics裏面設置顯示的背景圖。
在語言包的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。
對應原作的標題。
這個場景內默認包含一個標題和一個提示文本。
你如果想要它只顯示標題,直接改代碼就行,裏面寫的很簡單我覺得我不用多講。
另外,雖然我在場景裏面使用了圖片顯示標題(因爲我的標題圖片很顯然的經過處理,並不是直接輸入的文本),如果你想使用類似原作標題那樣的標題,你可以直接使用模板內包含的MONSTERFRIENDBACK與MONSTERFRIENDFORE。(UT-MSB,UT-MSF)
如下。
該場景由MenuController控制。 相關文本在語言包的同名txt中更改。
該場景由RenameController控制。 相關文本在語言包的同名txt中更改。
如果你對修改全局框架的需求不大,或者單純就是看不下去下文,可以以後再看,下文屬於比較進階的內容。
我們之前提到了Assets/Scripts內的文件夾,也許你注意到了,Control文件夾的描述很特殊。是的它特別長
我們提到它裏面的腳本是“用以創建Resources下的數據存儲文件”的,如果你不明白這是什麼意思,那麼現在點開Assets/Resources文件夾,你會發現裏面放着挺多東西的。就像這樣。
我們先不管紅框框裏面的內容,看看這些綠框框框住的這些個名字都是“XXXXControl”的文件吧,隨便點開一個,然後你就會看到右邊的檢查器會顯示出來一些數據。
- AudioControl存儲音效相關的數據。
- BattleControl存儲戰鬥內相關的數據。
- ItemControl存儲玩家的物品數據。
- OverworldControl存儲一些通用數據(大多數用於Overworld)
- PlayerControl存儲玩家的數據(同時它也被存檔系統所讀存)
同樣的,有個印象就行,我們會在對應的地方詳細講解它們。
現在,我們要開始講解整個項目中最重要的腳本,MainControl。
MainControl是幹啥的?上面的這些xxxControl,都需要通過MainControl來獲取,並且MainControl還負責對一些其餘數據和語言包的導入。
讓我們去看看場景內,然後你會發現,每一個場景裏面都有一個預製體叫做MainControlSummon。
正如其名,它是生成總控的腳本,此外他還會生成設置系統和音頻系統,這都是我們這部分要講的內容。
它生成的總控(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);
在原先Unity自帶後處理的基礎上,加上了自定義後處理的戰鬥場景。
我知道你在想什麼。停。
模板內有四個新增的後處理。
Chromatic Aberration Component是一個簡單的滾動色差分離效果。
CRT Screen Component是一種類似於老式電視機的效果。
Glitch Art Component包含四種用於實現差錯效果的選項,參考了KinoGlitch。
Stretch Post Component會拉伸遊戲內的顯示區域。我本想以這種方式製作16:9分辨率適配,但我發現拉伸後的顯示區域看上去有點模糊。後來我用Viewport矩形解決了這個問題。簡而言之,這個效果沒啥用。
這幾個後處理的原shader都放在Assets/Shaders/PostProcessing下。
編寫你自己的後處理特效也很簡單,在Assets/Scripts/Volume下存放一些腳本。
它們都是以xxxComponent,xxxRendererFeature的格式命名的。
xxxComponent負責把後處理添加在顯示列表內。我們以Chromatic Aberration爲例,我們在Full Chromatic Aberration.shadergraph中存儲了這些數據。
特別注意的是這裏要使用_MainTex。
而相對應的,我們把這些數據都要寫在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的文件。
你會發現之前內置的後處理都擺在這裏,同樣的,加上你的後處理就大功告成了。
玩家的物品都存儲在PlayerControl下的My Items裏,你會發現這是一個int類型的List,裏面寫的都是數字,每一個數字編號對應一個物品。
其中,0爲沒有物品,110000的編號內都是食用品,1000120000都是武器,20001~30000都是防具。
這些物品的信息(運行時)儲存在ItemControl內。
在食品中,第0個是食品的數據名稱,第1個是食用完它之後會變成另一種什麼食物(類似於原作的棒冰),第2個就是回覆血量(可以是負數),以此類推。
數據名稱是爲了與語言包顯示的名稱相區別而存在的,例如你想加一個食物叫“巧克力”,它在不同語言有不同的叫法,但是數據名稱只有一個(例如chocolate,或者choco,或者……cc?)。
而武器和盔甲更簡單一些,第0個是數據名稱,第1個是atk/def,以此類推。
但你並不能在這裏進行編輯,你應該能看到ItemControl最上面存儲着一個txt文本“ItemData”。
我們打開這個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上。
使用形如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內。
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下。
- 導出正式版遊戲時記得不要勾選開發構建,反之可以勾上,導出時可以查看報錯。
- 在導出完畢之後,要把工程根目錄下的Assets\LanguagePacks整個文件夾複製,然後粘貼在導出後的Undertale Changer Template_Data文件夾內,不然遊戲會報錯。
享受你的遊戲吧!
如果你對這個教程有任何建議和意見,可以聯繫我進行修改——你自己改也行。
感謝你看到這裏!希望你能做出一個完美的同人遊戲!