Hey there prospective mod-makers. I’m Ross, the lead scripter on Unrest (if you squint hard enough you can catch my name as it zips by in the credits, just in time to mispronounce it). I’m here today to give a basic introduction to modding for our game, following through on setting up the mod directory structure by making a basic playable scenario from scratch.
For this tutorial there are a few requirements:
- A basic text editor or some other software which can safely parse and edit XML. We use Notepad++.
- A plucky go-mod-’em attitude.
Pretty steep. Once that’s in order you’re ready to start.
Creating a Mod
The first thing you’ll want to do is open up the Unrest/mods/ directory and make a folder called “tutorial” to serve as a repository for all of the custom content you intend to make. This folder acts as an alternative to the /res/ directory the base game uses to store its content, so if in doubt about where to put your mod’s files simply look there for examples. If for instance you were making a full conversion, you would put your custom level files in Unrest/mods/tutorial/levels/.
In any case, the file we’re currently looking for is default.xml in /res/. Copy it into /mods/ to start. To make things official, rename default.xml to tutorial.unrmod so Unrest can properly identify it – it’s no longer the default, after all. Once this is done, open it up in a text editor (or if you’ve got it, an XML editor like XML Notepad) and make sure the contents of the file look like this:
<?xml version="1.0" encoding="utf-8"?> <!--Hi there! In order to start modding the game, copy this file to the /mods folder, then rename it to Name_of_mod.unrmod--> <config author="Me" version="0.1" info="Basic Tutorial" website="https://pyrodactyl.com"> <level list="mods/tutorial/levels/levels.xml" start="tut_palace" /> <hud layout="res/layout/hud.xml" /> <sprite animation="res/anims/anims.xml" constant="res/anims/constants.xml" /> <objective layout="res/layout/objectives.xml" /> <map layout="res/layout/worldmap.xml" /> <event list="mods/tutorial/events/list.xml" layout="res/layout/events.xml" conversation="res/layout/conversation.xml" store="res/events/store.xml" char="res/layout/char.xml" /> <!--No, not *that* kind of store. It's called store because it is a global storage of event data--> <people templates="res/characters/templates.xml" list="res/characters/list.xml" /> <inventory layout="res/layout/inventory.xml" db="res/items/db.xml" /> <item list="res/items/items.xml" /> <save auto_1="AutoSave 1" auto_2="AutoSave 2" quick="QuickSave" quit="AutoSave" /> <debug layout="res/layout/debug.xml" /> </config>
From here on out tutorial.unrmod will control the file paths your mod will use. If you eye the excerpt above you’ll notice your mod is currently pulling most of its assets from the /res/ directory rather than /mods/tutorial, which is perfectly all right as today we won’t be changing too much. Needless to say, if you ever need to change something in a /res/ subdirectory, you should be copying the whole thing into your mod’s folder and changing the directory path here in tutorial.unrmod to avoid altering the default game.
Setting Up The Mod Directory
Now that you have a working mod directory, the next step is to import some content for editing. You’ll want to make two folders in your mod directory: /levels/ and /events/. Inside /levels/ make a subdirectory named /tut_palace/ and a levels.xml with the following in it:
<?xml version="1.0" encoding="utf-8"?> <world> <!--Tutorial levels--> <loc id="tut_palace" name="First Tutorial" layout="mods/tutorial/levels/tut_palace/tut_palace_l.xml" res="mods/tutorial/levels/tut_palace/tut_palace_r.xml" /> </world>
As you can see, levels.xml is a manifest for all the level assets in your mod. Each level’s entry consists of three parts:
- loc id: The name of the level as the mod scripts will refer to it. For instance, you’ll notice that “start=” in the node of tutorial.unrmod is telling the game to start on tut_palace.
- layout: A file with the contents of the level itself, such as sprite positions and animations. Popup conversations are also written into _l files, as they’re unique to each instance of an NPC.
- res: Another file which indexes all of the visual resources used by a given level. This script tells the game what to load when you enter a new area. If the area you’re entering has the same _r file as the one you just left, it’ll hold on to those assets in memory and load much faster.
Now we’ll do the same thing for /events/ and create list.xml, which works just like levels.xml, but for each area’s active scripts:
<?xml version="1.0" encoding="utf-8"?> <event_list> <!--Tutorial--> <loc name="tut_palace"> <file name="0" path="mods/tutorial/events/tut_palace/Asha_init.xml" /> <file name="1" path="mods/tutorial/events/tut_palace/Asha_intro.xml" /> <file name="2" path="mods/tutorial/events/tut_palace/HiThere.xml" /> </loc> </event_list>
Keep an eye on the “file name=” values, as these always have to be in numeric order. If you copy an entry to add a new script to your level and two files have the same file name, only one of them will run! And believe me, there are few things more baffling to debug than a script that won’t even start.
For this tutorial we’ll be running three scripts in tut_palace: one to initialize the player character, one to bring up an intro-style splash page, and one that contains a conversation for everyone’s favourite uncle Avinash.Having squared both of these away, it’s time to actually make a level for us to walk around in.
Creating A Level Layout
Make a file inside /levels/tut_palace/ called tut_palace_l.xml with the following contents:
<?xml version="1.0" encoding="utf-8"?> <level> <map path="res/levels/palace/" file="palace_interior_prologue.tmx"> <loc x="610" y="92" /> <clip id="5" x="504" y="5" w="212" h="183" /> </map> <sprites> <player id="pasha" img="1080" x="47" y="29" multiply="32" moveset="205" dir="down" /> <!--Quest NPC's--> <obj id="c_prologue_avinash" img="1099" x="70" y="28" multiply="32" moveset="222" /> <obj id="c_prologue_vijay" img="1073" x="38" y="44" multiply="32" moveset="200" dir="right" /> <obj id="c_palace_guard1" img="1011" x="40" y="40" multiply="32" moveset="159"> <popup loop="true"> <dialog id="0" duration="3000" delay="0" text="(bows) It is an honor, my sovereign."> <trigger type="obj" subject="" operation="p" val="c_palace_guard1" /> </dialog> </popup> </obj> </sprites> <preview path="res/levels/previews/p_palaceint_prologue.png" /> <music id="11" /> </level>
You can skip the following explanations if you’re just looking to finish this tutorial, as they’re not crucial to doing so, but if you’re curious about anything this should answer your questions. The nodes of a _l file are broken down like so:
- map path and file: The location and file name of the Tiled .tmx that defines this area’s backdrop, collisions, and exits. Currently this is pointing to palace_interior_prologue.tmx from the base game, which suits our purposes just fine.
- loc: The X/Y location of the “you are here” marker as it appears on the map when you’re in this area. You can find the map images in /res/gfx/world_map. Opening them in paint and moving the pencil around should list out its X/Y coordinates.
- clip_id: The number of the map file as defined in res/layout/worldmap.xml, followed by the dimensions of a box (the X/Y of the top left corner, then the width and height). Anything inside the box starts out hidden until the player walks into this area to explore it.
- player and obj nodes: Everything we need to know about NPC’s. “obj_id” is their name ID (if this matches an entry in a file indexed by /res/characters/list.xml, it displays that above their head and uses its base opinion values). “img” is the spritesheet they use as we’re about to define in tut_palace_r. “x” and “y” determine the NPC’s location on the map once multiplied by “multiply” (Tiled uses 32×32 pixel tiles, but sometimes you’ll want more precise placement). “moveset” determines their animation frames (see /res/anims/anims.xml), and finally “dir” defines which way the sprite is facing (players have up/down/left/right, NPC’s only left/right).
- popup: Conversation that plays over an NPC’s head. Loop makes the event repeat after use and refresh on each re-load of the area. The “dialogue” subnode contains what they say and the “trigger” node inside that determines when they say it.
- preview path: The location of the level’s thumbnail as seen in the save/load screen.
- music id The song that plays when this area is loaded. As defined in /res/sounds/music.xml.
Creating A Rendered Assets File
Once again in /levels/tut_palace/, make a .xml called tut_palace_r.xml that will determine which visual assets our tutorial level load into memory. Simple enough:
<?xml version="1.0" encoding="utf-8"?> <res> <!--Sprites--> <image name="1000" path="res/levels/palace/sprites/Shyam_scaled.png" /> <image name="1011" path="res/levels/palace/sprites/PalaceGuard1_scaled.png" /> <image name="1073" path="res/levels/palace/sprites/VijayStanding_scaled.png" /> <image name="1080" path="res/levels/palace/sprites/Asha_Prologue_scaled.png" /> <image name="1099" path="res/levels/palace/sprites/Avinash_scaled_prologue.png" /> <!--Items--> <image name="188" path="res/items/map_of_bhimra.png" /> <!--Traits--> <image name="300" path="res/traits/asha_calculating.png" /> </res>
The images for everything from traits to items to NPC’s get stored in this file. Notice how Asha’s player entry in the _l file we just made has an image id that corresponds to her sprite sheet as referenced here at image name=”1080″. With this file in place we should be able to boot up the game, start the mod, and walk around the level, though there won’t be anything to actually do except make some smalltalk with the guards until we write some scripts. Let’s get on that, shall we?
Writing Some Scripts
Ah, the fun part. Welcome to a good day in the life of, well… me. Remember how we set list.xml to look for three scripts a few minutes ago? We’re going to make them in a moment, but first create a folder called /tut_palace/ inside /events/ to hold them. In Unrest’s engine, Events are scripts that run once specific trigger conditions are met and are organized into sequences of nodes which also branch by automatic triggers or player input, usually in the form of progressing conversation branches. Events happen the moment a player enters the area unless triggered by something, and can do everything from sorting an inventory to assigning journal entries to making scene transitions with the fade animation. The ones we want in /events/tut_palace/ right now are…
Asha_init.xml:
<events> <event id="0" title="pasha" dialog="(Test: Filling chapter-start inventory and variables.)" type="silent"> <trigger type="var" subject="AshaTutorialInitialized" operation="!=" val="1" /> <effect type="var" subject="AshaTutorial_money" operation="=" val="451" /> <effect type="money" val="AshaTutorial_money" /> <effect type="map" subject="img" operation="" val="5" /> <effect type="img" subject="" operation="" val="5" /> <effect type="dest" subject="Best Uncle Ever" operation="580" val="75" /> <effect type="journal" subject="cur" operation="The Tutorial" val="I think it's safe to say you're still doing it." /> <effect type="journal" subject="people" operation="Ross" val="Charming, handsome, quick of wit, and exceptionally humble. The man behind the myth... and innumerable bugs besides. It's said he owns seven houses, has been writing a book since before he was born, and with each Steam update his keyboard resounds to the crack of thunder." /> <effect type="journal" subject="location" operation="The Tutorial" val="It may look like just a palace, but things only get better from here. Soon enough you'll be scripting reactive bookshelves." /> <effect type="journal" subject="history" operation="The Tutorial" val="Pay no need to the temporality of this category; if you can read this, you're probably not finished yet." /> <effect type="var" subject="AshaTutorialInitialized" operation="=" val="1" /> <effect type="end" operation="cur" /> </event> </events>
This init event sets up the player character at the start of a chapter. It’s the first event to be run and so should always be numbered 0 in list.xml. Notice the “event id=0″ node, which specifies the first thing that will happen in the event; its type is “silent”, meaning no dialogue box or animation plays when it runs – it will simply execute whatever effects are inside it when the trigger conditions, also specified inside it, are met. In this case the trigger is a variable set within Asha_init which once defined will prevent it from running again and cue in the intro panel we’ll be making as our next event.
On a similar note, you’ll notice that the things you can put inside event nodes fall into a few different categories:
- Effects: Effects are single operations that do just about everything you’re able to specify on one line. Giving items, adding journal entries, and setting variables are all effects. They’re what gets executed before an event node ends or shifts over to the next.
- Triggers: Triggers affect the whole event node they’re in, and all of the triggers in a node have to be met before that event will run (unless you add rel=”or” to their properties, then only one trigger needs to be met). Triggers typically use variables or NPC interaction as input, but can also check items and traits. We can create logic branches by telling an event node to go to two or more other nodes with “next id=”, then setting triggers for each of those. The game will try each in the order they’re written, and the first one to be met will be where it goes.
- Reply Subnodes and Opinion Modifiers: We’ll get to these in a bit. They’re special case inputs used for the player’s reply options.
Onward to Asha_intro.xml!
<events> <event id="0" title="pasha" dialog="You are "Asha," a small but oddly complex player sprite.` `You have much to teach prospective mod creators - the not least of which is your ability to jitter about madly between collision rectangles." type="intro"> <effect type="end" operation="cur" /> </event> </events>
…it’s really that simple. Another single node event that ends (forever) with an “end” effect. This one’s of the “intro” type, so whichever NPC is specified in the “title=” field will show up alongside the fancy text and display their traits if the trait button is clicked. You may also notice that we can use XML escape characters like ” to show quotation marks, and the ` (tilde) key serves as a line break. This also works in normal dialogue nodes and journals.
Finally, we get to the real meat of things. Make a file called HiThere.xml and fill it up with this wondrous stream of script-stuffs:
<events> <event id="0" title="c_prologue_avinash" dialog="Asha, I know it's scary out there; this is the tutorial. The truth is, I never accounted for what would happen if a player tried to leave this room. The start of the game? The gaping abyss? Something far, far worse? It harrows me with fear and wonder." type="reply"> <trigger type="obj" subject="" operation="p" val="c_prologue_avinash" /> <effect type="dest" subject="Best Uncle Ever" operation="-1" val="-1" /> <effect type="save" /> <talk> <reply text="It's not that scary. I've played open-world first person RPGs." next="1" tone="88" /> <reply text="The playtesters don't want to see me walk through walls. Every time they do they yell. I don't like it." next="2" tone="86" /> <reply text="Me too. Best not tempt fate." next="3" tone="87"> <change id="c_prologue_avinash" body="0" tone="0" like="10" fear="0" respect="10" /> </reply> <reply text="I think it only prudent we start this conversation over again." next="9" tone="87"> <change id="c_prologue_avinash" body="0" tone="0" like="-5" fear="0" respect="0" /> <unlock> <trigger type="var" subject="tutorial_SpokenOnce" operation="!=" val="1" /> </unlock> </reply> </talk> </event> <event id="1" title="c_prologue_avinash" dialog="I know you have. You understand that the bugs aren't your fault--right? Your scripters can't control the bugs--only fix them when they happen, and they can only fix the bugs they find. I think even the players understand that deep down." type="reply"> <talk> <reply text="I saw man's head spin on his neck. A saw a man spin on a horse as it was eaten by a bear. How does that even happen?" next="4" tone="71" /> <reply text="But they're writing the scripts. They control what happens--so isn't that their fault?" next="5" tone="67" /> <reply text="I know. Is that why Vijay isn't talking to me?" next="6" tone="89" /> </talk> </event> <event id="2" title="c_prologue_avinash" dialog="Neither do I. Have you any notion how hard it is to keep a straight face while holding a conversation with a vibrating, discombobulated princess? Have some mercy for the scripters; they can't control the bugs--only fix them when they happen. They're doing everything they can." type="reply"> <talk> <reply text="I came out of the wall, though! Why was I still vibrating?" next="4" tone="71" /> <reply text="But they're writing the scripts. They control what happens--so isn't that their fault?" next="5" tone="67" /> <reply text="I know. Is that why Vijay isn't talking to me?" next="6" tone="89" /> </talk> </event> <event id="3" title="c_prologue_avinash" dialog="A wise decision. That door is a menace beyond the scope of my guidance. It's not the scripters' fault; they can't control the bugs--only fix them when they happen. They're doing everything they can." type="reply"> <talk> <reply text="Why all the secrecy? Did nobody test it?" next="4" tone="71" /> <reply text="But they're writing the scripts. They control what happens--so isn't that their fault?" next="5" tone="67" /> <reply text="I know. Is that why Vijay isn't talking to me?" next="6" tone="89" /> </talk> </event> <event id="4" title="c_prologue_avinash" dialog="I have no earthly idea. But truthfully, it's more entertaining that way." type="dlg"> <next id="7" /> </event> <event id="5" title="c_prologue_avinash" dialog="Only if they leave the matter unsolved, despite the power to solve it. To err is human; what's important is what follows." type="dlg"> <next id="7" /> </event> <event id="6" title="c_prologue_avinash" dialog="Asha...that's just a statue. You've been talking to it for weeks now. I begin to worry for you. (He sighs.) There is still much work to be done." type="dlg"> <next id="7" /> </event> <event id="7" title="c_prologue_avinash" dialog="I don't really know why mod support is so uncommon. Who can say? But I reviewed the facts with your parents, and I agree that a thriving mod community will be the best thing that ever happened to Bhimra. Even Vijay agrees, and he's been advising against the whole venture for months. You and the scripters will make things better, Asha. It's your bloodright. Now--you've responsibilities to face." type="dlg"> <next id="8" /> </event> <event id="8" title="c_prologue_avinash" dialog="You'll do fine. Everything is as written." type="dlg"> <trigger type="obj" subject="" operation="p" val="c_prologue_avinash" /> <next id="8" /> </event> <event id="9" title="c_prologue_avinash" dialog="Do you now?" type="dlg"> <effect type="var" subject="tutorial_SpokenOnce" operation="=" val="1" /> <next id="0" /> </event> </events>
Now this is the real deal. The majority of events in Unrest resemble this one to some extent – connecting branches of numbered event nodes which either take the form of reply lists or one-way dialogue that’s advanced by the player. Once you get the hang of this, you can effectively write a conversation-driven visual novel.
The immediate thing to point out is the trigger type in node 0. “obj” triggers are met when the player (operation=”p”) interacts with the object id specified in their “val” field (in this case, c_prologue_avinash, who you put in the _l file earlier). In node 0 this means that this conversation will play when you talk to Avinash.
But what’s that down at node 8? Another “obj” trigger? It is indeed. When an event node directs to a single other event node with a trigger condition that isn’t met, it will stop the conversation, restore control to the player, and wait there until this becomes the case. This one drops the player out of the conversation at the end and waits for the player to talk to Avinash again. Once they do, the conversation progresses to a post-conversation one-liner and loops back on itself with the “next id” effect.
Following this up we have “reply” event nodes. Node 0 is a perfect example of this. Reply nodes work just like normal “dlg” ones except they expect input from the player in the form of several branching responses. These are embedded inside nodes as “reply” subnodes, each of which contains three fields: the text of the reply, the id of the event node this option leads to next, and the response’s tone. Tones are referred to by number, and stored as strings in events/store.xml.
You may notice that some of these replies have special “change id” nodes within them. These modify the opinion values of the given character id, and can also be used in normal “dlg” event nodes. You cannot, however, trigger any other effects inside a reply. For that you’ll have to execute them in the event node the reply points to (consider a “silent” event node that does these effects and points to a normal “dlg” if you want them to happen instantly).
On top of this, the nodes inside of a reply give you a place to put a trigger. If its condition is satisfied that reply option will appear; if not then it will remain invisible. A conversation window can only hold five visible replies at a time, but so long as that’s true you can have an infinite number hidden by various interacting triggers. In our example you can tell Avinash to start the conversation over, but doing this sets a variable so the second time around that reply is hidden from the player. There are many creative ways to use nodes, from shops to shifting opinion-based replies. I haven’t even conceived of them all myself!
And on that note, we’re finished! Go try out your excellent, functioning mod scenario if you haven’t done so already!
Posted In: Unrest
Tagged: game development, modding, rpg, Unrest