It Lives!

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.

[gfycat data_id=”InfiniteJitteryCrab”]

Questing Part 3 – Anatomy of a Task

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/Quests directory (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:

[gfycat data_id=”ConsiderateZanyFlyingsquirrel”]

 

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.

Recent Downtime

It has been a frustrating few days after a recent AWS outage borked MySQL connections for our blog and forums. The host resolved this quickly for sites hosted in North America region, but dfworkshop.net was hosted in the closest datacentre to Australia (Asia region), and this continues to be down at time of writing. And after almost 48 hours, I’m still unable to obtain time-frame for fix.

I’ve worked around this by migrating sites to North America region with functioning MySQL servers. Unfortunately, databases couldn’t migrate because source servers still aren’t live. To fix this, I restored from the most recent backup available taken on 25/02/17. For the blog this isn’t a problem as nothing was posted in this time, but for forums this means all posts, replies, PMs, and new user registrations created after 25/02/17 are not part of the restore.

I’ve locked the forums for now and will continue working with host to try and recover a later version of databases. Outcome is dependent on how quickly host can resolve MySQL issue in Asia region. If they can’t fix this in next 24 hours, I’ll unlock forums and we’ll just move on. I’d rather have a functioning site than sweat over a few days of lost posts.

I apologise if you registered to forums or posted anything after the 25th. Forums will be back online within 24 hours and we can pick up where left off one way or another. Thank you for your patience!

Update: Forums are back online now. Unfortunately it doesn’t look like I’m getting those live databases back. This means I have to start from most recent backup as of 25/02/17. Please repost to any active conversations you were participating in prior to outage and we’ll be back on track in no time.