It Lives!
Posted on
March 10, 2017 by
Interkarma
With some back-end and core actions out of the way, it was incredibly satisfying to watch the quest system spring into life today. The two bootstrap quests are now launched with a new character. They don’t do much more than popup messages right now, but everything starts somewhere.
The work from here is “just” building out all the remaining actions and conditions for quest scripting. I say “just” because it’s still a
huge amount of work. But it’s finally starting to feel like I’m getting somewhere!
If this doesn’t look like an awful lot of progress for a few months work, remember it’s a
black triangle milestone. This is a pioneer of more interesting stuff to come.
Questing Part 3 – Anatomy of a Task
Posted on
March 7, 2017 by
Interkarma
Example custom quest action created for this post
The quest system back-end is coming along. I’m sad it’s not
further along by now, but life has a way of disrupting plans. The important thing is I’m still making progress and have some good stuff to share today.
In this post, I will dissect tasks along with their conditions and actions, which together form the meat of a quest. There’s a great deal of technical content ahead, I want this post to be a kind of primer for contributors to quest system. My apologies to those of you who don’t enjoy code-heavy updates.
Before getting started, please have a quick skim through Donald Tipton’s
excellent documentation for his Template v1.11 quest compiler/decompiler. As discussed in
Questing Part 1: Source, these source files are also used by Daggerfall Unity’s quest parser so we have common ground with classic. This even allows quest source to be shared back and forth with classic for testing. I’ve actually rolled back some of my ideas from that first post and will use Template source files directly as-is without any changes. This might have to change in time but right now I’m aiming for total parity.
If you’d like to see some real quest source, you’ll find all of Daggerfall’s decompiled quests in the
StreamingAssets/Questsdirectory (link to GitHub, ignore .meta files). But today we’re just zooming in on tasks and actions to see how these are handled by Daggerfall Unity.
Tasks
Daggerfall quests have four distinct forms of tasks (so far). All of the examples below are from the quest _BRISIEN.
Standard – This is a basic task which does not start unless explicitly triggered somehow. The task name (e.g.
_invitepc_) is also a boolean symbol which can be queried to see if task has been triggered (i.e. is active). The lines under the task header are the conditions and actions making up that task.
_invitepc_ task:
start timer _remindpc_
give pc _letter1_ notify 1026
create npc at _dirtypit_
place npc ladyBrisienna at _dirtypit_
Repeating – These tasks execute continuously until the symbol they reference (the boolean state of another task or variable name) is triggered. In below case, the task will persist until
_exitstarter_ is triggered. Repeating tasks appear to be triggered automatically at startup.
until _exitstarter_ performed:
start quest 999 999
start quest 977 977
start timer _invitepc_
remove log step 0
Variable – A variable is really a kind of task with trigger state only. Trigger state may be set/unset by other tasks.
variable _exitstarter_
Headless – Every quest must have a single headless task. This is the entry point to be executed automatically at quest start-up. Unlike other tasks, a headless task does not have a symbol to query trigger state. It just executes to bootstrap the rest of quest. This is the entry point of _BRISIEN:
-- Quest start-up:
log 1010 step 0
pc at PiratesHold set _exitstarter_
say 1025
At time of writing, Daggerfall Unity will parse through quest source to instantiate tasks and try to match component actions to a registered template (more on this below).
Conditions
Other than being triggered at startup or by other tasks and clock time-outs, a task can have one or more conditions that might cause it to be triggered. For example, if player is in a specific place at a certain time (e.g. Daggerfall at night) then some action can be performed (e.g. play the “vengeance” effect). This makes it possible to chain together tasks which trigger on and off based on the trigger state of other tasks.
I won’t go much into conditions right now as they have not been implemented yet. I’ve just barely stubbed out a bit of starting code that will be replaced later. If you like, you can read more about quest conditions
here.
Actions
A quest action is a bit of text that
does something. This is usually a single thing like playing a sound, displaying a message, or starting another task. Don’t think of actions like a normal programming command though. They aren’t necessarily run and done (although they might be). Try to think of actions as
components attached to a task in a similar way that Unity components are attached to a GameObject. This isn’t a perfect analogy, but its a start. Like GameObjects in Unity, tasks can switch on and off and their component actions perform bits of work over time.
Actions are a great way for contributors to help build out the quest system. There are many different kinds of actions, some will be very simple others very complex.
Building Actions
So how does an action go from a line of text to actually doing something in the game? The rest of this post will cover the fundamentals and show a real working example of a custom action… in action.
The bones of every action begins with the
ActionTemplate class, an abstract implementation of IQuestAction interface. All actions must inherit from ActionTemplate and implement the required parts of IQuestAction. This ensures that every action template has a few key features:
- Pattern – A regex string used to pair a line of source text with this action. Two actions cannot have the same match pattern.
- Test – Checks if provided source string matches the regex pattern expected for this action.
- Create – An action template is special in that it can also factory (i.e. generate) a new instance of itself with default settings. This allows the QuestMachine which hosts active quests to hold a list of self-replicating action templates that can be instantiated as required.
- GetSaveData – Gets a data packet from action live state. This will be passed on to JSON serialization system when saving a game.
- ResoreSaveData – Sends a data packet to action from serialized state. This will be used to restore action state when loading a game.
- Update – Called by the task owning this action. Allows the action to do work every frame as needed.
To show all of this working, I wrote an example called JuggleAction which simulates the player juggling some number of objects with a percent chance to drop one. Click
here for the full source code and I’ll break it down below. Let’s start with the pattern:
public override string Pattern
{
get { return @"juggle (?<numberOfThings>\d+) (?<thingName>\w+) every (?<interval>\d+) seconds drop (?<dropPercent>\d+)%"; }
}
This is just a basic regex match string that looks for a pattern like
“juggle 5 apples every 2 seconds drop 40%”. Everything the action needs to execute is contained in the pattern. Sometimes an action might take different forms and the pattern string must cover these variants also.
The parser uses
Test to find a registered action template with pattern matching source. When a match is found, the JuggleAction template will factory a new instance of itself with default settings by way of
Create.
public override IQuestAction Create(string source, Quest parentQuest)
{
// Source must match pattern
Match match = Test(source);
if (!match.Success)
return null;
// Factory new action and set default data as needed
JuggleAction action = new JuggleAction(parentQuest);
action.thingName = match.Groups["thingName"].Value;
action.thingsRemaining = Parser.ParseInt(match.Groups["numberOfThings"].Value);
action.interval = Parser.ParseInt(match.Groups["interval"].Value);
action.dropPercent = Parser.ParseInt(match.Groups["dropPercent"].Value);
return action;
}
You’ll notice the action parameters are exposed directly by the Match class returned by Test. This makes it easy to read out the values involved. At this time, our new action is ready and is added to a collection stored in the Task object. During quest runtime, the task will call
Update on each action to do the work required. Here it just counts off time and if still holding any objects, calls the Juggle() method. Note that we’re using Time.realtimeSinceStartup instead of Time.deltaTime. The reason for this is that QuestMachine ticks at a slower rate than Unity (currently 10 times per second). So we need to measure time without using something that only changes frame-to-frame.
public override void Update(Task caller)
{
// Increment timer
if (Time.realtimeSinceStartup < nextTick)
return;
// Juggle 'em if you got 'em
if (thingsRemaining > 0)
{
Juggle();
}
// Update timer
nextTick = Time.realtimeSinceStartup + interval;
}
Below is the Juggle() method for completeness. It just spits out some notification text to HUD and randomly decrements object count until none are remaining.
private void Juggle()
{
// Juggle current things
DaggerfallUI.AddHUDText(string.Format("Juggling {0} {1}...", thingsRemaining, thingName));
// We might drop something!
int roll = Random.Range(1, 101);
if (roll < dropPercent)
{
thingsRemaining--;
DaggerfallUI.AddHUDText("Oops, I dropped one!");
}
// Give up if we've dropped everything
if (thingsRemaining == 0)
{
DaggerfallUI.AddHUDText(string.Format("Dropped all the {0}. I give up!", thingName));
return;
}
}
I won’t touch on
GetSaveData and
RestoreSaveData yet as quest state serialization has a ways to go. You can check the full source of JuggleAction linked above for an example implementation.
You might recall I said something about registering new actions with QuestMachine. This might change later, but right now our action class JuggleAction is registered in QuestMachine from RegisterActionTemplates() like below. The template is only being used as a factory so it doesn’t need to pass in an owning quest at construction.
RegisterAction(new JuggleAction(null));
Registering the action template allows the quest machine to find it (using Test) and factory a new instance from the template.
Now that we have an action and registered it to quest machine, we actually need a quest that uses this action for real. I created a cut-down quest just for this example called
__DEMO01.
- Minimal example quest used to demonstrate how to script custom actions
Quest: _BASIC01
QRC:
- No text resources
QBN:
- Headless entry point with custom action
juggle 5 apples every 2 seconds drop 40%
All that remains is to instantiate the quest itself. I will add a console command soon for this, but in the meantime I’m calling the following bit of code from StartGameBehaviour.
GameManager.Instance.QuestMachine.InstantiateQuest("__DEMO01");
This loads our custom quest into the quest machine and starts executing supported actions, which right now is just the demo quest and juggle action. When starting a game, this will be the output:
Next Steps
For now, I will continue to work on the quest machine, parser, and related frameworks. My immediate next step will be to get the full tutorial quest working along with some foundation conditions and actions, and a few supporting user interfaces (quest log, quest debugger UI).
I would like to invite the more experienced contributors to review the quest
source documentation in more detail and see if any actions might fall into their range of interest. I would also love some help with quest resources other than tasks (e.g. Place, Item, Foe, Person, etc.). I’ve stubbed out the
Clock resource as a starting point. If there is something you would like to work with, please start a conversation on the forums and let’s see where it takes us.
If you have any questions or would like to dicuss this post in more detail, please don’t hesitate to find me on the forums!
For more frequent updates on Daggerfall Unity, follow me on Twitter
@gav_clayton.