-
Notifications
You must be signed in to change notification settings - Fork 3
Tutorial
This text is translated by ChatGPT.
I don't know what mindset you have when you discovered the Undertale Changer Template and clicked on its Wiki tutorial page.
- Maybe you're a newcomer who has never dealt with programming, wanting to try making a fan game and stumbled upon this thing.
- Maybe you've used other engines/templates but found them unsatisfactory and ended up here.
- Maybe you're highly skilled in programming and just stumbled upon it by chance. Who knows.
Regardless, from the moment Undertale Changer Template appeared and became open-source, it belongs to everyone who uses it.
... but do you really need it?
I mean, from the day Undertale was born until now.
If you want to make a fan game of Undertale, there are so many ways to do it.
-
Just a simple battle? You can use Unitale or CreateYourFrisk, both written in Lua, to create mods for lightweight and easier-to-start fan games, both of which are based on the Unity engine.
-
Or do you want to go further? TML's UNDERTALE Engine is built on the same game engine, GameMaker Studio, used to create the original Undertale, and it can replicate all the original Undertale features.
-
Are you still not satisfied? There are countless templates/engines based on GameMaker Studio, Godot, Clickteam Fusion, or using languages like C++, Python, and you can find numerous options with a quick search on GitHub or Game Jolt, right?
-
Do you really, truly need Undertale Changer Template?
Think about it carefully.
Undertale Changer Template might not be your best choice.
-
Undertale Changer Template isn't as mature or user-friendly as the options mentioned earlier.
-
Games created with Undertale Changer Template consume more memory. (Although, to be honest, I haven't done any memory optimization.)
-
The development approach of Undertale Changer Template may be quite different from the other templates/engines mentioned (although I haven't used those engines, so I'm not entirely sure), which means it might take some time to learn.
But...
Anyway, I've finished my discouragement. If you still insist on using it, then let's go.
In the following content, I assume you have already studied C# tutorials and have a basic understanding of the C# language.
You should also have some level of familiarity with using the Unity engine (especially its 2D features).
And this should be your first time using this template.
C# and Unity-related tutorials can be found freely on the vast internet.
But if you ask me where to start, my answer would be this and this (both are Chinese C# tutorials).
Additionally, I recommend bookmarking the Unity Scripting API to look up Unity API-related content.
As for learning Unity engine, there are many good resources online, so go and find them.
When you first open the template, it should show an empty scene. It will probably look something like this. (If not, it's okay; the following steps should still apply.)
Since it's your first time using the template, let's run the whole template to get an idea of what it looks like inside.
First, locate the "Scenes" folder within the template. This folder contains all the scenes in the template.
Then, navigate to the "Assets/Scenes/Menu/Story/Story.unity" path and open the "Story" scene; this is the original storytelling scene. Click "Play" and play around a bit, following the style of the original game.
Once you've played through it, we can really get started.
The scripts in the template are commented, and you can refer to the comments while writing. Here's a general introduction to the template.
Let me briefly explain the design approach of this template.
In the original Undertale, you can consider my template to consist of the following two parts:
Scenes inside battles and scenes outside battles.
It's straightforward, just like their literal meanings.
The scenes inside battles can also be divided into overworld scenes and miscellaneous scenes (story, title, menu, renaming, etc.).
- For example, the overworld scenes involve scripts controlling player movement, story scenes have scripts controlling fade-ins and fade-outs of slides, and battle scenes have scripts forming the battle system. There are also some scripts that are used across all or most scenes.
Each category of scene has scripts that support their operation.
- For example, there are typewriter scripts, as well as master control scripts, and so on.
Inside the "Assets/Scripts" folder of the template, you'll find the code used in the template. You'll see several subfolders here.
- The "Battle" folder - scripts used only in battle scenes.
- The "Control" folder - contains scripts that inherit from the ScriptableObject class, used to create data storage files under "Resources."
- The "Debug" folder - as the name suggests, it contains scripts used for debugging.
- The "Default" folder - contains general/uncategorized scripts.
- The "Obsolete" folder - contains deprecated scripts, often used as references from others.
- The "Overworld" folder - scripts used only in scenes outside battles.
- The "Volume" folder - contains scripts related to custom post-processing.
For now, you just need to have a rough understanding of these folders' categorization. Detailed content will be explained in the following sections.
Note: The original text includes some references to specific links and images that cannot be directly translated. I've translated the content and explanations, but certain details (such as the exact images and specific URLs) may not be applicable in this translation.
This is an overworld scene, of course.
To create a new map, we need to create a new scene.
Press Ctrl + N to open the new scene page, and you will see a scene template called "Overworld scene":
This is where the scene template is stored (you know it's there, but don't open it unless you know what you're doing):
After creating the scene, you'll see a new scene with nothing in it:
The "Grid" object is the tilemap; you can start laying down tiles as you like, but remember that the sorting layer for tiles is generally "Tilemap," as shown:
I placed a few tiles randomly:
Add some light sources and post-processing. (Yes, I simply copied from the Ruins example scene)
Now you can save it with a memorable name, and put it in the Scene folder, as shown:
Then add a collider to your tilemap. I know you can use "Tilemap Collider 2D," but I prefer to use "Edge Collider 2D." So, add a collider the way you prefer:
However, if you run the scene now, you'll get an error, as shown below:
Let's follow the error message to find the problematic code, and we'll find this piece of code:
...
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");
}
}
...
This code is responsible for loading language packs, and each overworld scene has a corresponding .txt file to store all the data (dialogue, etc.) for that scene.
The solution is simple; we need to follow the path of the internal language packs (Assets/Resources/TextAssets/LanguagePacks) and add our scene there in the "Overworld" folder, which contains .txt files with the same names as our in-game scenes:
Let's add our scene in there:
The format and special rich text formatting can be found in the Example-Corridor's comments.
But as for the specific events, we'll write about them later. For now, just put an empty .txt in there.
Now, when we enter the scene, we notice that the camera gets stuck when we move to the left. This is because we haven't set up camera following yet.
The "CameraFollowPlayer" script inside "MainCamera" is used to make the camera follow the player. You can move "frisk" to the far left or right of the map and adjust the values in this script to test the camera's locking range.
Of course, you can also uncheck the "Limit" checkbox, and that removes the camera's follow restrictions.
If you don't need camera following, you can remove this script:
Here's the result after setting it up:
Now the scene has a bit more structure; you can freely explore without flying off. However, there's no sound yet. We'll address this issue in "MainControlSummon."
Drag the BGM onto it, then follow the information above to configure it. Generally, you only need to add the BGM, and you can ignore the rest.
Once this is done, the scene is a bit better. But it still lacks interaction, but don't worry, we'll get to that next.
What are events used for? In simple terms, they allow you to inspect objects within the game or use them to transition to the next scene when you reach the edge of the map.
Continuing from where we left off, our scene currently cannot be inspected, nor does it have a way to leave the scene.
Adding an event is straightforward. We need to add an object to the scene that has a 2D collider, like this:
My recommendation is to place it under "Grid/Detectables" for better management (Detectables is an empty object with reset coordinates).
We add "OverworldObjTrigger" to it, and you'll see a bunch of miscellaneous options; their functions are explained in the comments above, so I won't go into detail.
We only need some text to appear when inspected. For example, something like "This is a cracked wall." But first, we haven't added the text we need for inspection.
Remember the previously mentioned internal language packs path (Assets/Resources/TextAssets/LanguagePacks)? Find the .txt file you created earlier.
Let's edit it. You can refer to the comments in Example-Corridor if needed.
CrackedWall\<image=-1><fx=0>* This is a cracked wall.;
If you're too lazy to read the comments, I'll explain briefly. What comes before the slash is the detection text for "OverworldObjTrigger," and what comes after it is the specific text content.
<image=-1> is the typing icon (with -1 indicating no icon). The sprite for the icon is stored in "BackpackCanvas" under "Sprite Changer."
<fx=0> is the typing sound effect. It's stored in "Assets/Resources/AudioControl" under "FxClipType."
Don't forget to write the corresponding detection text in "OverworldObjTrigger":
Now, let's go to the wall and inspect it, and the text will appear!
Alright, next, we need an object to leave the scene.
Let's fine-tune the scene and add an exit.
Set this one as a trigger.
Set the following in "OverworldObjTrigger":
Check "ChangeScene," and the scene will switch when the player is detected.
Check "BanMusic," and the music will fade out.
"SceneName" is the destination scene.
"NewPlayerPos" is the player's coordinates after switching to the new scene.
Now let's test the game!
You'll see that we successfully entered another scene.
Our event explanation ends here. As for the other options inside "OverworldObjTrigger," such as camera movement, you can see how it's set up in the Example-Corridor.
Tips: If you stop running now and run again, you'll notice the player's position has moved.
This happens because when switching scenes, the destination coordinates are stored in "Assets/Resources/OverworldControl" as shown above, and the player is placed at these coordinates when the scene is loaded.
Sometimes, we need to add options within a scene. Similarly, we use "OverworldObjTrigger," but there's no need for additional settings in the component; it works the same way as mentioned earlier.
There are two additional things we need to do:
Firstly, you need to add the "OverworldTalkSelect" component to "BackpackCanvas." Just add it.
Then, follow the format of "BackMenu" in Example-Corridor and write the following text:
Select\<image=-1><fx=1>* (This is a select text.)<enter><enter><fx=-1><size=5> </size>< >< >< >< >< >< > Ok<size=5> </size>< >< >< >< >< >< >< >< >< > Nope<select>Select;
(Fun fact, if you find the long string above hard to read, you can add line breaks as needed.)
Please note that this text ends with "Select," which is the key to detect whether the text triggers an "option event."
Now, configure the corresponding triggering object in the scene and test it.
Now the options are there, but clicking on them doesn't do anything. This is because you haven't written the code for what happens after the selection.
Let's open the "OverworldTalkSelect" script and go to the relevant code after pressing the Z key in the Update section:
if (MainControl.instance.KeyArrowToControl(KeyCode.Z))
{
typeWritter.TypeStop();
switch (select)
{
case 0: // Left option selected
switch (typeText)
{
/*
Typewriter example
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: // Right option selected
break;
}
heart.color = Color.clear;
canSelect = false;
return;
}
Our goal is to play a sound effect and then do nothing when the left option is selected. This is quite simple. Below the existing code for case "BackMenu": ... break;
, add the following code:
case "Select":
AudioController.instance.GetFx(2, MainControl.instance.AudioControl.fxClipBattle);
break;
Here, "Select" corresponds to the "Select" following <select>Select
.
With this done, run the game again and check the effect. You should hear a clear "ding" sound when selecting the left option.
Save events are relatively easy to set up - because I've already prepared a prefab.
In the "Assets/Prefabs" directory, you'll find a prefab named "Save"; that's what you need. Drag it into the scene.
Pay attention to the part I've marked; that's what triggers the option to display the save window.
Next, continue inputting the following text into the corresponding section:
Save\<image=-1><fx=0>* (The save filling you<enter>< >< >with <gradient="White to Red - UTC">determination</gradient>.);
The effect is shown below.
However, after saving, the room's name displays as "null" - because you haven't entered the room name!
In "LanguagePacks/US/UI/Setting.txt," start a new line and add "Example-Study/Study Scene;"
Now it's fixed!
Press the V key to return to the menu, and you can still see it!
Tip: <gradient="White to Red - UTC">
uses TMP's gradient rich text. You can also add your custom gradient colors in "Assets/TextMesh Pro/Examples & Extras/Resources/Color Gradient Presets" and use rich text to call it.
Here is a list of all the scripts related to the Overworld, provided as a reference for modifying the template source code.
BackpackBehaviour: Manager for the Overworld's backpack system.
CameraFollowPlayer: Script for the Overworld camera to follow the player.
OverworldObjTrigger: Trigger for Overworld objects.
OverworldTalkSelect: Overworld option system.
PlayerBehaviour: Script for controlling the player within scenes on the map.
SpriteChanger: Changes the sprite in Overworld dialogues.
TalkUIPositionChanger: Changes the position of the dialogue box UI.
TriggerChangeLayer: Changes the layer of SpriteRenderer through triggers.
TriggerPlayerOut: Used for Overworld objects with animators. Executes code/plays animations when the player enters/leaves.
I hope you didn't just jump here directly, haha.
Open the Battle scene in Assets/Scenes/Battle, and let's get started.
As mentioned earlier, there's a separate folder called "Battle" in the LanguagePacks directory.
Inside, there's a text file called "UIBattleText.txt," and the Turn folder contains the enemy dialogues for each turn, which we'll discuss later.
Let's open "UIBattleText.txt" and scroll down.
The following section is the narrative text for the turns, with a numeric index for each turn:
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.;
The following section contains the ACT options for the monsters. NPC1 / NPC2 represent the monster names, followed by the specific text of the options and the content after the selection.
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.;
The monster names are detected through prefabs stored in BattleControl.
The following section is the Mercy options for the monsters, similar to ACT options.
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.;
You can modify these according to your needs.
How do we make these options trigger code execution? Open SelectUIController, and go to around line 440, where the content is as follows:
switch (selectSon)// Code for ACT triggers goes here
{
case 0:// Monster 0
switch (selectGrandSon)// Option
{
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:// Monster 1
switch (selectGrandSon)// Option
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
}
break;
case 2:// Monster 2
switch (selectGrandSon)// Option
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
}
break;
}
Simply add code at the appropriate locations. For example, the code "GetFx" in the above snippet plays a sound effect when the first option for Monster 0 is selected.
In the Battle scene, there's a TurnController attached to MainControlSummon, which manages the turns. We'll write the bullet patterns for the enemy's turn here.
You'll find an IEnumerator called "_TurnExecute" inside it. You might notice that this IEnumerator is followed by <float>
. This is because I'm using More Effective Coroutines [FREE] in the project to allow coroutines to pause. You can check its documentation here.
The template already includes an example turn, so I'll keep it brief.
To create bullets, you need to use the object pool for bullets named "objectPools[0]."
To get a bullet, use this line:
var obj = objectPools[0].GetFromPool().GetComponent<BulletController>();
This gets a bullet from the pool.
After getting the bullet, you need to initialize it.
The initialization method is as follows in the template's prefab:
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
)
{
// ...
}
You can fill in the values as described in the comments. Your IDE (e.g., Visual Studio) should provide hints on what to input at each position when you're coding.
For example, the following code initializes a bullet:
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
);
The above code initializes a bullet named "DemoBullet," belonging to the category "CupCake," placed on layer 40, with a sprite loaded from "Sprites/Bullet/CupCake," no size offset, starting at coordinates (0, -3.35), with white color and visible inside the mask of the battle frame.
Now you can manipulate the bullet as you like. In the scene, I use the DoTween component to control animations of bullets (and other settings). I recommend checking their official website for documentation.
For example, the following code animates a bullet to move up and down while rotating 360 degrees in 2 seconds. It's quite straightforward, assuming you understand how to use the DoTween component.
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);
By the way, if you can't find the above example codes, they are located within the IEnumerator _TurnNest(Nest nest)
.
This coroutine is used for nested bullet patterns, but if you prefer, you can add a case for 100000 in _TurnExecute
and write a nested bullet pattern there. It would achieve the same effect (though it might look a bit strange).
Finally, don't forget to recycle bullets when they are no longer needed:
objectPools[0].ReturnPool(obj.gameObject);
You may have noticed that the battle scene contains two cameras.
Yes, thanks to Unity's (naturally) excellent support for 3D, you can add some 3D elements to the scene.
Just like the scene I set up, there isn't much to say about scene layout if you've learned the basics of Unity.
Let's briefly discuss the difference between these two cameras.
In the battle scene, you need to be aware that the "Main Camera" is not the default main camera. Unity determines the main camera based on its tag, and "Main Camera" is tagged as "Untagged."
The 3D Camera is the actual main camera, so keep that in mind.
You can see the content captured by the two cameras in the template.
This is the "Main Camera."
This is the 3D Camera.
Feel free to place your 3D elements within the range of the 3D Camera.
Tips: In theory, you can take my template and try turning the Overworld scene or even the entire template into 3D or 2.5D. If you have the skills, why not?
The battle box is the MainFrame within the battle scene. You'll notice that it has several child objects. The four "Point" objects represent
the four corners of the battle box, and the "Back" object draws the black area of the battle box.
By moving one of these points, you can deform the battle box.
The border of the battle box is drawn using a LineRenderer, while the black area of the battle box (and the bullet mask) is drawn using a shader.
If you want a battle box with more than four points, here's how you can do it:
Let's take the example of a five-sided polygon (pentagon) for the battle box.
- On the DrawFrameController of the MainFrame, change the vertex count to 5.
- Add a new child object called "Point4" to the MainFrame. You can simply duplicate one of the existing "Point" objects.
-
Modify the DrawPolygon shader used by the "Back" object. You can find it in Assets/Shaders/Sprites as DrawPolygon.
-
In the DrawPolygon shader, create a new input called "Point4" of type Vector2.
- Adjust the structure as shown below:
- Modify the AddSuperposition sub-graph to accommodate the new point:
- Save the changes and return to the DrawPolygonFull graph. Make the necessary connections.
- Save the changes again and return to the DrawPolygon graph. Add a new "Point4" input and connect it as shown:
Remember to save your changes.
-
Keep in mind that the bullet mask shader also needs modification. Open the SpriteBattleMask shader, located in Assets/Shaders/Sprites.
-
Add a new input called "Point4" of type Vector2, and connect it as shown:
Run the game to see the results.
You should now have a battle box with a pentagonal shape, and both the polygon battle box and the bullet mask should work correctly.
Here's a list of all the scripts related to battles, provided as a reference for modifying the template source code.
BattlePlayerController: Controls the properties of the player during battles.
CameraShake: Script for camera shaking effects.
DialogBubbleBehaviour: Controls the dialogue bubbles for enemies.
EnemiesController: Script for controlling monsters.
BoardController: Controller for barriers.
BulletController: Controller for bullets (projectiles).
EnemiesHpLineController: Controller for enemy health bars.
BulletShaderController: Controller for bullet shaders (including bullet masks).
GameoverController: Controller for game over events.
ItemSelectController: Controller for item selection.
SelectUIController: UI controller for battles, also responsible for controlling player turns.
SpriteSplitController: When enabled, turns monsters immediately gray.
SpriteSplitFly: Used in conjunction with SpriteSplitController. Controls the movement trajectory of individual pixels.
TargetController: Controls the target (the bullseye in the "fight" section).
TurnController: Controller for turns and bullet object pools.
This scene corresponds to the storytelling scene that players see when the game starts.
The scene layout is straightforward, and the main controller for the scene's content is the "StorySceneController" under the "Story" GameObject.
Here's how you can edit this scene:
- Set the background image in the "Pics" folder.
- Edit the text in the "Overworld/Story.txt" file within the language pack. The default text is as follows:
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>
;
The <changeX>
before each sentence prevents skipping using the X key.
<passText=x>
skips the text after x seconds. Use < >
if there's no text afterwards.
<storyFade=x>
fades out and fades in to the Xth image. Specific IDs are built into the game. A negative value means fading in with no image.
<storyMove=(x,y,z)>
moves the image to a certain position, with z representing the move time (use a negative value for absolute value).
<storyMaskT><storyMaskF>
turns the background mask on/off (useful when moving the background image to show only one area).
<storyExit>
fades out and exits to the "Start" scene.
This scene corresponds to the title screen in the original game.
This scene includes a title and a hint text by default.
If you want only the title to be displayed, you can directly modify the code. The setup is quite straightforward.
Additionally, if you'd like to use a title similar to the original game (e.g., "MONSTERFRIENDBACK" and "MONSTERFRIENDFORE"), you can use the provided templates (UT-MSB and UT-MSF).
This scene is controlled by the "MenuController". Relevant text changes are made in the language pack's corresponding text file.
This scene is controlled by the "RenameController". Relevant text changes are made in the language pack's corresponding text file.
If you don't have significant changes to the global framework, or you find the following section difficult to follow, you can revisit it later. This section involves more advanced content.
Previously, we mentioned the folders in Assets/Scripts, and you might have noticed that the description of the "Control" folder is quite special. Indeed, it's quite lengthy.
We mentioned that the scripts inside this folder are "used to create data storage files under Resources." If you don't understand what this means, take a look inside the Assets/Resources folder. You'll find several items there, like this:
Let's not worry about the contents inside the red box for now. Instead, let's focus on the items with green boxes, which are all named "XXXXControl" files. If you open one of them, you'll see some data displayed on the right side.
- AudioControl stores sound-related data.
- BattleControl stores data related to battles.
- ItemControl stores player's item data.
- OverworldControl stores some general data (mostly for the overworld).
- PlayerControl stores player data (and is also read by the save system).
Remember this, and we'll explain each of them in their respective sections.
What is MainControl used for? Well, as the name suggests, it's the main controller for everything. All the "XXXControl" scripts mentioned earlier need to be accessed through MainControl, and MainControl is responsible for importing other data and the language pack.
Let's look at the scenes, and you'll notice that each scene contains a prefab called "MainControlSummon."
As the name implies, this prefab is used to generate the main controller (MainControl) as well as the settings system (Canvas) and audio system (BGM Source). This is what we'll cover in this section.
The generated main controller (MainControl), settings system (Canvas), and audio system (BGM Source) won't be destroyed when switching scenes.
One usage of MainControl is shown below. This code is taken from PlayerBehaviour.cs and is used to heal the player when saving.
You can see that we access the player's HP information through MainControl.instance.PlayerControl.
if (MainControl.instance.PlayerControl.hp < MainControl.instance.PlayerControl.hpMax)
MainControl.instance.PlayerControl.hp = MainControl.instance.PlayerControl.hpMax;
The main controller also stores frequently used methods, such as the following:
This method is used for a black screen transition when switching scenes. Typically, the template uses this method for scene transitions. (If you don't understand it fully, don't worry, as you're unlikely to modify it much.)
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);
}
}
Most of the methods inside the main controller have comments that explain their usage, and you can write your own if needed.
Attach the "TypeWritter" script to an object in the scene to enable typewriter effect. The typewriter effect detects rich text elements like <xxx>
. Here's an example usage:
typeWritter.TypeOpen("<color=red>text123</color>", false, 0, 0, textUI);
In addition to the default Unity post-processing, there's a custom post-processing system for battle scenes.
You're probably wondering what this is. Hold on.
The template has introduced four additional post-processing components:
- Chromatic Aberration Component: A simple scrolling chromatic aberration effect.
- CRT Screen Component: Similar to an old TV screen effect.
- Glitch Art Component: Contains four options for glitch effects, inspired by KinoGlitch.
- Stretch Post Component: Stretches the game display area (note: this effect may not be very useful).
The original shaders for these post-processing effects are located in Assets/Shaders/PostProcessing.
Writing your own post-processing effect is relatively simple. Store your scripts under Assets/Scripts/Volume using the format "xxxComponent" and "xxxRendererFeature."
"xxxComponent" is responsible for adding the post-processing effect to the display list. Let's use Chromatic Aberration as an example. The data for this effect is stored in "Full Chromatic Aberration.shadergraph."
The key thing to note is the use of "_MainTex."
The corresponding "xxxComponent" script should be formatted like this (pay attention to how the variables correspond to the data in the shadergraph, except for "MainTex," which is not needed):
[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;
}
}
After creating your post-processing script, you'll need to modify the "Universal Render Pipeline Asset_Renderer" file located in the root directory of Assets:
This file contains the pre-existing post-processing effects, and you can add your effect by following the same format.
The player's items are stored in "My Items" under "PlayerControl," and you'll notice it's a list of integers. Each number corresponds to an item's identifier.
Specifically, items 0-10000 are consumables, items 10001-20000 are weapons, and items 20001-30000 are armor. The runtime information for these items is stored in "ItemControl."
In consumables, the first entry is the data name of the food, the second is the food it transforms into after consumption (similar to the original game's ice cream), and the third is the HP restoration value (can be negative).
For weapons and armor, the first entry is the data name, the second is the atk/def value, and so on.
However, you shouldn't edit these directly. You should notice a text file named "ItemData" at the top of "ItemControl."
Open this text file to add new items.
baaaaaaaaaaaaaaaa\Foods\@bang\10;
bang\Foods\@Null\5;
pia1\Arms\1\null;
pia2\Arms\999\null;
tatata1\Armors\123\null;
tatata2\Armors\456\null;
The format is straightforward. Add new items here.
Of course, once you add items, they need to match the language pack. Edit the UI/ItemText in the language pack to match the new items.
/* 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>;
The script responsible for controlling the audio system is called AudioController (note: don't confuse it with AudioControl). It's placed on the BGM Source generated by MainControl.
Use code like this to play audio: 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]);
}
The audio to be played is stored in AudioControl.
CanvasController is responsible for controlling UI, including the settings system.
SaveController handles saving and reading data from PlayerControl in JSON format.
Saved data is stored in a "Data" folder in the root directory. If the folder doesn't exist, it'll be created.
The template also uses PlayerPrefs to store some data related to the settings. For example:
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));
The language pack is loaded within MainControl.
Built-in language packs are stored in Assets\Resources\TextAssets.
External language packs are stored in Assets\LanguagePacks.
- When exporting the final version of the game, remember not to check the development build option. However, you can check it for testing purposes during export to view any errors.
- After exporting, make sure to copy the entire "LanguagePacks" folder from the project's root directory and paste it into the exported "Undertale Changer Template_Data" folder. Failing to do this might result in errors in the game.
Enjoy your game!
If you have any suggestions or feedback about this tutorial, feel free to contact me for modifications—you can even make changes yourself.
Thank you for reading this far! I hope you can create a fantastic fan-made game!